11 января 2024 года, выпущен Firebird 5.0 — восьмой основной выпуск СУБД Firebird, разработка которого началась в мае 2021 года. В Firebird 5.0 команда разработчиков сосредоточила свои усилия на повышении производительности СУБД: параллельное выполнение backup, restore, sweep, создания и перестроение индексов, улучшение масштабирования в многопользовательской среде, ускорение повторной подготовки запросов (кеш компилированных запросов), улучшение оптимизатора, улучшение алгоритма сжатия записей. Кроме того, появились и новые возможности в языке SQL и PSQL.


В версии Firebird 5 также появился встроенный инструмент для профилирования SQL и PSQL, что существенно облегчит поиск узких мест и отладку сложных SQL.


Базы данных, созданные в Firebird 5.0, имеют версию ODS (On-Disk Structure) 13.1. Firebird 5.0 позволяет работать и с базами данных с ODS 13.0 (созданные в Firebird 4.0), но при этом некоторые возможности будут недоступны.


Для того чтобы переход на Firebird 5.0 был проще, в утилиту командной строки gfix был добавлен новый переключатель -upgrade, который позволяет обновлять минорную версию ODS без длительных операций backup и restore.


Также хочется отметить тот факт, что новый релиз Firebird доступен сразу на 11 платформах, включая ARM для Linux и Android. Скачать готовые сборки и дистрибутивы можно по адресу https://firebirdsql.org/en/firebird-5-0/.


Далее я перечислю ключевые улучшения, сделанные в Firebird 5.0, и их краткое описание. Подробное описание всех изменений можно прочитать в Firebird 5.0 Release Notes. Кроме того подробный разбор новых функций Firebird 5.0 вы можете найти в серии статей на ресурсе ibase.ru.



Улучшение алгоритма сжатия данных


Как известно, в Firebird записи таблиц располагаются на страницах данных (DP) в сжатом виде. Это сделано для того, чтобы на одной странице поместилось как можно больше записей, а это в свою очередь экономит дисковый ввод-вывод. До Firebird 5.0 для сжатия записей использовался классический алгоритм Run Length Encoding (RLE).


В Firebird 5.0 был разработан усовершенствованный алгоритм сжатия RLE (со счётчиком переменной длины). Этот алгоритм доступен
только в базах данных с ODS 13.1 и выше.


Улучшенный алгоритм сжатия RLE имеет следующие преимущества:


  • записи таблиц, содержащие поля с типом VARCHAR(N), хранятся компактнее, если N достаточно велико. Особенно это актуально для полей с кодировкой UTF8, в которой для хранения одного символа резервируется 4 байта;
  • короткие последовательности (менее 8 одинаковых байт) больше не сжимаются. С одной стороны, степень сжатия записи может стать немного ниже, чем было ранее, но с другой — это экономит процессорное время;
  • в некоторых случаях запись невозможно сжать, и при применении алгоритма RLE её размер может увеличиться. Начиная с ODS 13.1, если сжатая запись длиннее оригинала, то она сохраняется "как есть" и помечается специальным флагом.

Благодаря новому алгоритму сжатия записей извлечение записей и их распаковка происходит быстрее на 5-10%.


Параллельное выполнение задач


Начиная с версии 5.0, Firebird может выполнять некоторые задачи, используя несколько потоков параллельно. Часть этих задач использует параллелизм на уровне ядра Firebird, другие реализованы непосредственно в утилитах.


Для обработки задачи с несколькими потоками движок Firebird запускает дополнительные рабочие потоки и создает внутренние рабочие соединения.


По умолчанию параллельное выполнение отключено. Существует два способа включить параллелизм в пользовательском соединении:


  • Установить количество параллельных рабочих процессов в DPB, используя тег isc_dpb_parallel_workers;
  • Установить количество параллельных рабочих процессов по умолчанию с помощью параметра ParallelWorkers в firebird.conf.

Некоторые утилиты (gfix, gbak), поставляемые с Firebird, имеют ключ командной строки -parallel для установки количества параллельных рабочих процессов.


В настоящее время параллелизм может быть использован в следующих задачах:


  • Создание резервной копии с помощью утилиты gbak (до 4 раз быстрее);
  • Восстановление из резервной копии с помощью утилиты gbak (до 3 раз быстрее);
  • Ручной sweep с помощью утилиты gfix и автоматический sweep (до 6 раз быстрее);
  • Обновление icu с помощью утилиты gfix;
  • Построение и перестроение индексов (CREATE INDEX ..., ALTER INDEX ... ACTIVE).

Кэш подготовленных запросов


Любой SQL запрос проходит две обязательные стадии: подготовку (компиляцию) и собственно выполнение.


Во время подготовки запроса происходит его синтаксический разбор, выделение буферов под входные и выходные сообщения, построение плана запроса и дерева его выполнения.


Если в приложении требуется многократное выполнение одного и того же запроса с разным набором входных параметров, то обычно отдельно вызывается prepare, хендл
подготовленного запроса сохраняется в приложении, а затем для этого хендла вызывается execute. Это позволяется сократить затраты на переподготовку одного и того же запроса
при каждом выполнении.


Начиная с Firebird 5.0 поддерживается кэш компилированных (подготовленных) запросов для каждого соединения.


Это позволяет сократить затраты для повторной подготовки одних и тех же запросов,
если в вашем приложении не используется явное кэширование хендлов подготовленных запросов (на глобальном уровне это не всегда просто).


Помимо запросов верхнего уровня в кэш подготовленных запросов попадают также хранимые процедуры, функции и триггеры. Содержимое кэша компилированных запросов можно
посмотреть с помощью новой таблицы мониторинга MON$COMPILED_STATEMENTS.


Поддержка двунаправленных курсоров в сетевом протоколе


Поддержка двунаправленных (прокручиваемых) курсоров впервые появилась в Firebird 3.0. Они так же доступны в PSQL и через API интерфейс.


Однако до Firebird 5.0 прокручиваемые курсоры не поддерживались на уровне сетевого протокола. Это обозначает, что вы могли использовать API двунаправленных курсоров в своём приложении, только если ваше подключение происходит в embedded режиме. Начиная с Firebird 5.0, вы можете использовать API прокручиваемых курсоров даже если соединяетесь с базой данных по сетевому протоколу, при этом клиентская библиотека fbclient должна быть не ниже версии 5.0.


Если ваше приложение не использует fbclient (например, оно написано на Java или .NET), то соответствующий драйвер должен поддерживать сетевой протокол Firebird 5.0. Например, Jaybird 5 поддерживает двунаправленные курсоры в сетевом протоколе.


Улучшение оптимизатора


В Firebird 5.0 оптимизатор запросов подвергся самым значительным изменениям со времён Firebird 2.0. Далее я перечислю что именно изменилось в оптимизаторе Firebird 5.0.


Стоимостная оценка HASH vs NESTED LOOP JOIN


Соединение потоков с помощью алгоритма HASH JOIN появилось в Firebird 3.0. До Firebird 5.0 метод соединения HASH JOIN применялся только при отсутствии индексов по условию связи или их неприменимости, в противном случае оптимизатор выбирал алгоритм NESTED LOOP JOIN с использованием индексов. Это не всегда оптимально. Если большой поток соединяется с маленькой таблицей по первичному ключу, то каждая запись такой таблицы будет читаться многократно, кроме того, многократно будут прочтены и страницы индексов, если они используются. При использовании соединения HASH JOIN меньшая таблица будет прочитана ровно один раз.


Естественно, стоимость хеширования и пробирования не бесплатны, поэтому выбор, какой алгоритм применять, происходит на основе стоимости.


Стоимостная оценка HASH vs MERGE JOIN


Алгоритм соединения слиянием MERGE JOIN был временно отключен в Firebird 3.0 в пользу соединения алгоритмом HASH JOIN. Обычно он применялся в тех случаях, когда использование алгоритма NESTED LOOP JOIN было неоптимальным, то есть, в первую очередь при отсутствии индексов по условию связи или их неприменимости, а также при отсутствии зависимости между входными потоками.


В большинстве случаев соединение методом HASH JOIN более эффективно, поскольку не требуется выполнять предварительную сортировку потоков по ключам соединения, но есть случаи, когда MERGE JOIN более эффективен. Теперь в ряде случаев оптимизатор может предпочесть алгоритм соединения MERGE JOIN вместо HASH JOIN.


Трансформация OUTER JOIN в INNER JOIN


Существует ряд проблем с оптимизацией OUTER JOINs в Firebird.


Во-первых, в настоящее время OUTER JOIN может быть выполнен только одним алгоритмом соединения NESTED LOOP JOIN, что может быть изменено в следующих версиях. Если возможно, то будет использован индекс по ключу присоединяемой таблицы, но как мы уже видели выше — это не гарантирует наиболее быстрое выполнение.


Во-вторых, при соединении потоков внешними соединениями порядок соединения строго фиксирован, то есть, оптимизатор не может изменить его, чтобы результат оставался правильным.


Однако, если в условии WHERE существует предикат для поля "правой" (присоединяемой) таблицы, который явно не обрабатывает значение NULL, то во внешнем соединении нет смысла. В этом случае начиная с Firebird 5.0 такое соединение будет преобразовано во внутреннее, что позволяет оптимизатору применять весь спектр доступных алгоритмов соединения.


Если LEFT JOIN использовался в качестве подсказки для указания порядка соединения очень активно, то переписать множество запросов на новый лад может быть проблематично. Для таких разработчиков существует параметр конфигурации OuterJoinConversion в firebird.conf или database.conf. Установка параметра OuterJoinConversion в false отключает трансформацию Outer Join во внутренние соединение. Обратите внимание на то, что этот параметр является временным решением для облегчения миграции, в будущих версиях Firebird он может быть удалён.


Раннее вычисление инвариантных предикатов


Начиная с Firebird 5.0, если фильтрующий предикат инвариантен, то он не вычисляется заново для каждой новой записи. Если значение инвариантного предиката равно FALSE, то извлечение записей из входного потока немедленно прекращается. Предикат является инвариантным, если его значение не зависит от полей фильтруемых потоков.


Простейшим случаем инвариантного предиката является "фейковое" ложное условие фильтрации 1=0, однако существуют и более сложные случаи.


Эффективное выполнение IN со списком констант


До Firebird 5.0 предикат IN со списком констант был ограничен 1500 элементами, поскольку обрабатывался рекурсивно, преобразуя исходное выражение в эквивалентную форму.


То есть,


F IN (V1, V2, ... VN)

преобразуется в


F = V1 OR F = V2 OR .... F = VN

Начиная с Firebird 5.0, обработка предикатов IN <list> является линейной. Лимит в 1500 элементов увеличен до 65535 элементов.


Списки констант в IN, предварительно оцениваются как инварианты и кэшируются как двоичное дерево поиска, что ускоряет сравнение, если условие необходимо проверить для многих записей или если список значений длинный.


Если список очень длинный или если предикат IN не является избирательным, то сканирование индекса поддерживает поиск групп с использованием указателя одного уровня (т. е. по горизонтали), а не поиск каждой группы от корня (т. е. по вертикали), таким образом, используя одно сканирование индекса для всего списка IN.


Стратегия оптимизатора ALL ROWS vs FIRST ROWS


Существует две стратегии оптимизации запросов:


  • FIRST ROWS — оптимизатор строит план запроса так, чтобы наиболее быстро извлечь
    только первые строки запроса;
  • ALL ROWS — оптимизатор строит план запроса так, чтобы наиболее быстро извлечь все
    строки запроса.

Начиная с Firebird 5.0, по умолчанию используется стратегия оптимизации, указанная в параметре OptimizeForFirstRows конфигурационного файла firebird.conf или database.conf.
OptimizeForFirstRows = false соответствует стратегии ALL ROWS, OptimizeForFirstRows = trueсоответствует стратегии FIRST ROWS.


Вы можете изменить стратегию оптимизатора на уровне текущей сессии с помощью оператора:


SET OPTIMIZE FOR {FIRST | ALL} ROWS

Кроме того, стратегия оптимизации может быть переопределена на уровне SQL оператора с помощью предложения OPTIMIZE FOR. SELECT запрос с предложением OPTIMIZE FOR имеет следующий синтаксис:


SELECT ...
FROM [...]
[WHERE ...]
[...]
[OPTIMIZE FOR {FIRST | ALL} ROWS]

Предложение OPTIMIZE FOR всегда указывается самым последним в SELECT запросе. В PSQL его необходимо указывать перед предложением INTO.


Новые возможности в языке SQL


Поддержка предложения WHEN NOT MATCHED BY SOURCE в операторе MERGE


Оператор MERGE производит слияние записей источника и целевой таблицы (или обновляемым представлением). В процессе выполнения оператора MERGE читаются записи источника и выполняются INSERT, UPDATE или DELETE для целевой таблицы в зависимости от условий.


Синтаксис оператора MERGE выглядит следующим образом:


MERGE
  INTO _target_ [[AS] _target_alias_]
  USING <source> [[AS] _source_alias_]
  ON <join condition>
  <merge when> [<merge when> ...]
  [<plan clause>]
  [<order by clause>]
  [<returning clause>]
``` ::= _tablename_ | (<select_stmt>)

<merge when> ::=
    <merge when matched>
  | <merge when not matched by target>
  | <merge when not matched by source>

<merge when matched> ::=
  WHEN MATCHED [ AND <condition> ]
  THEN { UPDATE SET <assignment_list> | DELETE }

<merge when not matched by target> ::=
  WHEN NOT MATCHED [ BY TARGET ] [ AND <condition> ]
  THEN INSERT [ <left paren> <column_list> <right paren> ]
  VALUES <left paren> <value_list> <right paren>

<merge when not matched by source> ::=
  WHEN NOT MATCHED BY SOURCE [ AND <condition> ] THEN
  { UPDATE SET <assignment list> | DELETE }

В Firebird 5.0 появились условные ветки <merge when not matched by source>, которые позволяют обновить или удалить записи из целевой таблицы, если они отсутствуют в источнике данных.


Теперь оператор MERGE является по-настоящему универсальным комбайном для любых модификаций целевой таблицы по некоторому набору данных.


Пример использования:


MERGE INTO price
USING tmp_price
ON price.good_id = tmp_price.good_id
WHEN NOT MATCHED
  -- добавляем если не было
  THEN INSERT(good_id, name, cost)
  VALUES(tmp_price.good_id, tmp_price.name, tmp_price.cost)
WHEN MATCHED AND price.cost <> tmp_price.cost THEN
  -- обновляем цену, если товар есть в новом прайсе и цена отличается
  UPDATE SET cost = tmp_price.cost
WHEN NOT MATCHED BY SOURCE
  -- если в новом прайсе товара нет, то удаляем его из текущего прайса
  DELETE;

Предложение SKIP LOCKED


В Firebird 5.0 появилось предложение SKIP LOCKED, которое может использоваться в операторах SELECT .. WITH LOCK, UPDATE и DELETE. Использование этого предложения заставляет движок пропускать записи, заблокированные другими транзакциями, вместо того, чтобы ждать их, или вызывать ошибки конфликта обновления.


Использование SKIP LOCKED полезно для реализации рабочих очередей, в которых один или несколько процессов отправляют работу в таблицу и выдают событие, в то время как рабочие потоки прослушивают события и читают/удаляют элементы из таблицы. Используя SKIP LOCKED, несколько работников могут получать эксклюзивные задания из таблицы без конфликтов.


SELECT
  [FIRST ...]
  [SKIP ...]
  FROM <sometable>
  [WHERE ...]
  [PLAN ...]
  [ORDER BY ...]
  [{ ROWS ... } | {OFFSET ...} | {FETCH ...}]
  [FOR UPDATE [OF ...]]
  [WITH LOCK [SKIP LOCKED]]

UPDATE <sometable>
  SET ...
  [WHERE ...]
  [PLAN ...]
  [ORDER BY ...]
  [ROWS ...]
  [SKIP LOCKED]
  [RETURNING ...]

DELETE FROM <sometable>
  [WHERE ...]
  [PLAN ...]
  [ORDER BY ...]
  [ROWS ...]
  [SKIP LOCKED]
  [RETURNING ...]

Пример использования SKIP LOCKED


Подготовка метаданных


create table emails_queue (
  subject varchar(60) not null,
  text blob sub_type text not null
);

set term !;

create trigger emails_queue_ins after insert on emails_queue
as
begin
  post_event('EMAILS_QUEUE');
end!

set term ;!

Отправка приложением или подпрограммой


insert into emails_queue (subject, text)
values ('E-mail subject', 'E-mail text...');

commit;

Клиентское приложение


-- Клиентское приложение может прослушивать событие EMAILS_QUEUE, 
-- чтобы отправлять электронные письма, используя этот запрос:

delete from emails_queue
  rows 10
  skip locked
  returning subject, text;

Может быть запущено более одного экземпляра приложения, например, для балансировки нагрузки.


Частичные индексы


В Firebird 5.0 при создании индекса появилась возможность указать необязательное предложение WHERE, которое определяет условие поиска, ограничивающее подмножество записей таблицы для индексирования. Такие индексы называются частичными индексами. Условие поиска должно содержать один или несколько столбцов таблицы.


Определение частичного индекса может включать спецификацию UNIQUE. В этом случае каждый ключ в индексе должен быть уникальным. Это позволяет обеспечить уникальность для некоторого подмножества строк таблицы.


Определение частичного индекса также может включать предложение COMPUTED BY, таким образом, частичный индекс может быть вычисляемым.


Полный синтаксис создания индекса выглядит следующим образом:


CREATE [UNIQUE] [ASC[ENDING] | DESC[ENDING]] 
INDEX _indexname_ ON _tablename_
{(<column_list>) | COMPUTED [BY] (<value_expression>)}
[WHERE <search_condition>]

<column_list> ::= _col_ [, _col_ ...]

Пример создания частичных индексов


--- частичный индекс с исключением значения NULL
CREATE INDEX IDX_HORSE_DEATHDATE
ON HORSE(DEATHDATE) WHERE DEATHDATE IS NOT NULL;

-- частичный индекс с условием фильтрации не входящим в ключ индекса
CREATE UNIQUE INDEX IDX_UNIQUE_DEFAULT_MAN_EMAIL
ON MAN_EMAILS(CODE_MAN) WHERE DEFAULT_FLAG IS TRUE;

Пакет RDB$BLOB_UTIL


Традиционно работа c BLOB внутри PSQL кода обходилась дорого, поскольку при любой модификации BLOB всегда создаётся новый временный BLOB. Это приводит к дополнительному потреблению памяти, а в ряде случае и к разрастанию файла базы данных для хранения временных BLOB.


В Firebird 4.0.2 для решения проблем конкатенации BLOB была добавлена встроенная функция BLOB_APPEND. В Firebird 5.0 был добавлен встроенный пакет RDB$BLOB_UTIL с процедурами и функциями, который позволяет достичь ещё большей эффективности при других манипуляциях с BLOB.


Средства для поиска узких мест


В Firebird 5.0 появился новый инструмент для поиска узких мест — профилировщик запросов.


Профилирование SQL и PSQL


Одной из задач разработчика или администратора базы данных является выяснение причин "тормозов" информационной системы.


После того, как медленные запросы "отловлены" трассировкой, можно приступать к их оптимизации. Однако такие запросы могут быть довольно сложны, а иногда и вовсе вызывать хранимые процедуры, поэтому необходим инструмент профилирования, который поможет выявить узкие места в самом запросе или вызываемом PSQL модуле. Начиная с Firebird 5.0, такой инструмент появился.


Профилировщик позволяет пользователям измерять затраты на производительность кода SQL и PSQL. Это реализовано с помощью системного пакета в движке, передающего данные в плагин профилировщика.


Пакет RDB$PROFILER может профилировать выполнение кода PSQL, собирая статистику о том, сколько раз была выполнена каждая строка, а также ее минимальное, максимальное и суммарное время выполнения (с точностью до наносекунд), а также статистику об открытии и извлечении записей из неявных и явных SQL курсоров. Кроме того, можно получать статистику SQL курсоров в разрезе источников данных (методов доступа) расширенного плана запроса.


Чтобы собрать данные профилирования, пользователь должен сначала запустить сеанс профилирования с помощью функции RDB$PROFILER.START_SESSION. Эта функция возвращает идентификатор сеанса профилирования, который позже сохраняется в таблицах снимков профилировщика. Позже вы можете выполнить запросы к этим таблицам для анализа пользователем. Сеанс профилировщика может быть локальным (то же соединение) или удаленным (другое соединение).


После запуска сеанса статистика PSQL и SQL операторов собирается в памяти. Сеанс профилирования собирает данные только об операторах, выполненных в соединении, связанном с сеансом. Данные агрегируются и сохраняются для каждого запроса (т. е. выполняемого оператора). При запросе к таблицам моментальных снимков пользователь может выполнять дополнительную агрегацию для каждого оператора или использовать вспомогательные представления, которые делают это автоматически.


Сеанс можно приостановить, чтобы временно отключить сбор статистики. Позже его можно возобновить, чтобы вернуть сбор статистики в том же сеансе.


Чтобы проанализировать собранные данные, пользователь должен сбросить данные в таблицы моментальных снимков, что можно сделать, завершив или приостановив сеанс (с параметром FLUSH, установленным в TRUE), или вызвав RDB$PROFILER.FLUSH. Данные сбрасываются с помощью автономной транзакции (транзакция начинается и завершается с конкретной целью обновления данных профилировщика).


Все процедуры и функции пакета RDB$PROFILER содержат параметр ATTACHMENT_ID, который следует указывать если вы хотите управлять удалённым сеансом профилирования; если этот параметр равен NULL или не указан, то процедуры и функции управляют сеансом локального профилирования.


Пример использования профилировщика


Подготовка метаданных:


create table tab (
    id integer not null,
    val integer not null
);

set term !;

create or alter function mult(p1 integer, p2 integer) returns integer
as
begin
    return p1 * p2;
end!

create or alter procedure ins
as
    declare n integer = 1;
begin
    while (n <= 1000)
    do
    begin
        if (mod(n, 2) = 1) then
            insert into tab values (:n, mult(:n, 2));
        n = n + 1;
    end
end!

set term ;!

Запуск профилировщика:


select rdb$profiler.start_session('Profile Session 1', null, null, null, 'DETAILED_REQUESTS') from rdb$database;

set term !;

execute block
as
begin
    execute procedure ins;
    delete from tab;
end!

set term ;!

execute procedure rdb$profiler.finish_session(true);

commit;

execute procedure ins;

commit;

select rdb$profiler.start_session('Profile Session 2') from rdb$database;

select mod(id, 5),
       sum(val)
  from tab
  where id <= 50
  group by mod(id, 5)
  order by sum(val);

execute procedure rdb$profiler.finish_session(true);

commit;

Анализ данных профилирования:


set transaction read committed;

select * from plg$prof_sessions;

select * from plg$prof_psql_stats_view;

select * from plg$prof_record_source_stats_view;

select preq.*
  from plg$prof_requests preq
  join plg$prof_sessions pses
    on pses.profile_id = preq.profile_id and
       pses.description = 'Profile Session 1';

select pstat.*
  from plg$prof_psql_stats pstat
  join plg$prof_sessions pses
    on pses.profile_id = pstat.profile_id and
       pses.description = 'Profile Session 1'
  order by pstat.profile_id,
           pstat.request_id,
           pstat.line_num,
           pstat.column_num;

select pstat.*
  from plg$prof_record_source_stats pstat
  join plg$prof_sessions pses
    on pses.profile_id = pstat.profile_id and
       pses.description = 'Profile Session 2'
  order by pstat.profile_id,
           pstat.request_id,
           pstat.cursor_id,
           pstat.record_source_id;

Помимо новых инструментов, были расширены возможности и ранее существующих инструментов.


По табличная статистика в isql


По табличная статистика показывает сколько записей для каждой таблицы при выполнении запроса было прочитано полным сканированием, сколько с использованием индекса, сколько вставлено, обновлено или удалено и другие счётчики. Значения этих счётчиков с давних пор доступно через API функцию isc_database_info, что использовалось многими графическими инструментами, но не консольным инструментом isql. Значения этих же счётчиков можно получить через совместное использование таблиц мониторинга MON$RECORD_STATS и MON$TABLE_STATS или в трассировке. Начиная с Firebird 5.0, эта полезная функция появилась и в isql.


По умолчанию вывод по табличной статистики выключен.


Для её включения необходимо набрать команду:


SET PER_TAB ON;

А для отключения:


SET PER_TAB OFF;

Команда SET PER_TAB без слов ON или OFF переключает состояние вывода статистики.


Полный синтаксис этой команды можно получить, используя команду HELP SET.


Пример вывода по табличной статистики:


SQL> SET PER_TAB ON;

SQL> SELECT COUNT(*)
CON> FROM HORSE
CON> JOIN COLOR ON COLOR.CODE_COLOR = HORSE.CODE_COLOR
CON> JOIN BREED ON BREED.CODE_BREED = HORSE.CODE_BREED;

                COUNT
=====================
               519623

Per table statistics:
--------------+---------+---------+---------+---------+---------+---------+---------+---------+
 Table name   | Natural | Index   | Insert  | Update  | Delete  | Backout | Purge   | Expunge |
--------------+---------+---------+---------+---------+---------+---------+---------+---------+
BREED         |      282|         |         |         |         |         |         |         |
COLOR         |      239|         |         |         |         |         |         |         |
HORSE         |         |   519623|         |         |         |         |         |         |
--------------+---------+---------+---------+---------+---------+---------+---------+---------+

Улучшенный вывод планов


В выводе подробного плана теперь различаются определяемые пользователем операторы SELECT (сообщаемые как select expression), объявленные PSQL курсоры и подзапросы (sub-query). Как legacy, так и explain планы теперь также включают информацию о положении курсора (строка/столбец) внутри модуля PSQL.


Планы хранимых процедур


Можно ли получать планы хранимых процедур по аналогии с тем, как мы получаем планы для EXECUTE BLOCK?


Ответ: и да и нет.


Если мы пойдёт простым путём, то есть попытаемся посмотреть план процедуры для следующего запроса, то ответ будет "Нет".


SELECT *
FROM SP_PEDIGREE(?, 5, 1)

Select Expression
    -> Procedure "SP_PEDIGREE" Scan

Как и ожидалось, отображён план запроса верхнего уровня без деталей планов курсоров внутри хранимой процедуры. До Firebird 3.0 такие детали отображались в плане, но они были перемешаны в кучу и разобрать там что-либо было очень затруднительно.


Но не расстраивайтесь. В Firebird 5.0 появился кеш подготовленных запросов, и таблица мониторинга MON$COMPILED_STATEMENTS отображает его содержимое. Как только мы подготовили запрос, содержащий нашу хранимую процедуру, то эта процедура также попадает в кеш компилированных запросов и для неё можно посмотреть план с помощью следующего запроса:


SELECT CS.MON$EXPLAINED_PLAN
FROM MON$COMPILED_STATEMENTS CS
WHERE CS.MON$OBJECT_NAME = 'SP_PEDIGREE'
  AND CS.MON$OBJECT_TYPE = 5
ORDER BY CS.MON$COMPILED_STATEMENT_ID DESC
FETCH FIRST ROW ONLY

Планы внутри процедуры SP_PEDIGREE
Cursor "V" (scrollable) (line 19, column 3)
    -> Record Buffer (record length: 132)
        -> Nested Loop Join (inner)
            -> Window
                -> Window Partition
                    -> Record Buffer (record length: 82)
                        -> Sort (record length: 84, key length: 12)
                            -> Window Partition
                                -> Window Buffer
                                    -> Record Buffer (record length: 41)
                                        -> Procedure "SP_HORSE_INBRIDS" as "V H_INB SP_HORSE_INBRIDS" Scan
            -> Filter
                -> Table "HUE" as "V HUE" Access By ID
                    -> Bitmap
                        -> Index "HUE_IDX_ORDER" Range Scan (full match)
Select Expression (line 44, column 3)
    -> Recursion
        -> Filter
            -> Table "HORSE" as "PEDIGREE HORSE" Access By ID
                -> Bitmap
                    -> Index "PK_HORSE" Unique Scan
        -> Union
            -> Filter (preliminary)
                -> Filter
                    -> Table "HORSE" as "PEDIGREE HORSE" Access By ID
                        -> Bitmap
                            -> Index "PK_HORSE" Unique Scan
            -> Filter (preliminary)
                -> Filter
                    -> Table "HORSE" as "PEDIGREE HORSE" Access By ID
                        -> Bitmap
                            -> Index "PK_HORSE" Unique Scan

Трассировка события COMPILE


В Firebird 5.0 появилась возможность отслеживать новое событие трассировки: парсинг хранимых модулей. Оно позволяет отслеживать моменты парсинга хранимых модулей, соответствующее затраченное время и самое главное — планы запросов внутри этих модулей PSQL. Отслеживание плана также возможно, если модуль PSQL уже был загружен до начала сеанса трассировки; в этом случае о плане будет сообщено во время первого выполнения, замеченного сеансом трассировки.


Для отслеживания события парсинга модуля в конфигурации трассировки появились следующие параметры:


  • log_procedure_compile — включает трассировку событий парсинга процедур;
  • log_function_compile — включает трассировку событий парсинга функций;
  • log_trigger_compile — включает трассировку событий парсинга триггеров.

Допустим, у нас есть следующий запрос:


SELECT * FROM SP_PEDIGREE(7435, 8, 1);

Для того чтобы в сеансе трассировки отслеживать план хранимой процедуры, необходимо установить параметр log_procedure_compile = true. В этом случае при подготовке этого запроса или его выполнении в логе трассировки появится событие парсинга процедуры, которое выглядит так:


Содержимое трассировки
2023-10-18T20:40:51.7620 (3920:00000000073A17C0) COMPILE_PROCEDURE
  horses (ATT_30, SYSDBA:NONE, UTF8, TCPv6:::1/54464)
  C:\Firebird\5.0\isql.exe:10960

Procedure SP_PEDIGREE:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Cursor "V" (scrollable) (line 19, column 3)
    -> Record Buffer (record length: 132)
        -> Nested Loop Join (inner)
            -> Window
                -> Window Partition
                    -> Record Buffer (record length: 82)
                        -> Sort (record length: 84, key length: 12)
                            -> Window Partition
                                -> Window Buffer
                                    -> Record Buffer (record length: 41)
                                        -> Procedure "SP_HORSE_INBRIDS" as "V H_INB SP_HORSE_INBRIDS" Scan
            -> Filter
                -> Table "HUE" as "V HUE" Access By ID
                    -> Bitmap
                        -> Index "HUE_IDX_ORDER" Range Scan (full match)
Select Expression (line 44, column 3)
    -> Recursion
        -> Filter
            -> Table "HORSE" as "PEDIGREE HORSE" Access By ID
                -> Bitmap
                    -> Index "PK_HORSE" Unique Scan
        -> Union
            -> Filter (preliminary)
                -> Filter
                    -> Table "HORSE" as "PEDIGREE HORSE" Access By ID
                        -> Bitmap
                            -> Index "PK_HORSE" Unique Scan
            -> Filter (preliminary)
                -> Filter
                    -> Table "HORSE" as "PEDIGREE HORSE" Access By ID
                        -> Bitmap
                            -> Index "PK_HORSE" Unique Scan
     28 ms

Заключение


На этом обзор новинок Firebird 5.0 окончен. Разработчики Firebird проделали огромную работу, за что им огромная благодарность.


Подробное описание всех изменений можно прочитать в Firebird 5.0 Release Notes. Кроме того подробный разбор новых функций Firebird 5.0 вы можете найти в серии статей на ресурсе ibase.ru.

Комментарии (20)


  1. Gallemar
    11.01.2024 17:56
    +3

    Ура! Поздравляю с новой версией!


  1. qw1
    11.01.2024 17:56
    +1

    записи таблиц, содержащие поля с типом VARCHAR(N) хранятся компактнее, если N достаточно велико

    Я-то думал, что у varchar(N), N - всего лишь ограничение при валидации поля, а реально строка хранится как пара (длина, массив[длина])

    А на самом деле что, строка всегда добивается пробелами (нулями) до N?

    Я-то думал, что разницы не будет, объявлять столбец в таблице как varchar(100) или varchar(200), если используется не более 50. А оказывается, не надо делать объявления "про запас".

    С другой стороны, изменение типа столбца с varchar(100) на varchar(200) происходит довольно быстро, не похоже, что страницы с данными перезаписываются. То есть, движок умеет читать сохранённый varchar(100), когда по описанию столбца ожидается varchar(200). Ну и зачем тогда в страницу писать лишних 200 пробелов и сжимать их RLE, если можно записать столько, сколько есть в строке, а движок сконвертирует при чтении.


    1. sim_84 Автор
      11.01.2024 17:56
      +1

      Подход с кодированием байтов в зависимости от типа вместо сжатия данных пробовался разработчиками и оказался более медленным. Проще иметь буфер фиксированной длины, чем постоянные реалокации памяти.

      Что касается смены тип с varchar(100) на varchar(200), то это происходит быстро потому что в реальности записи на диске вообще не изменяются. Просто записывается новый формат таблицы, а при извлечении данных записи со старым форматом преобразуются к новому на лету.


      1. qw1
        11.01.2024 17:56

        Ну вот такая идея. Если в поле с типом varchar(200) реально записан 1 символ, в страницу писать, как будто это было поле varchar(1). Неужели это медленнее, чем RLE-шить 200 знаков?
        Вроде как переаллокаций не нужно, ведь читатель страницы не знает, что там будет varchar(1) и заранее аллоцирует 200. Для ввода этой фичи даже ODS менять не надо.


        1. sim_84 Автор
          11.01.2024 17:56

          Из курсора читается обычно не одна запись. Предположим в первой записи один символ, а во второй 200, в третьей 50. Предлагаешь на каждом фетче буфер переаллоцировать? Сейчас переаллокаций не происходит вообще. Выделяется буфер фиксированного размера и в нем просто перезаписываются байтики.


          1. qw1
            11.01.2024 17:56

            Буфер всегда под varchar(200), как указано в описании поля таблицы.

            Если же физически в странице varchar(100), сейчас движок что делает? Как-то распаковывает без переаллоцирования. А что ему мешает так же распаковывать то из varchar(4), то из varchar(1), с разных записей разные длины.


            1. sim_84 Автор
              11.01.2024 17:56
              +2

              Во-первых сжимаются не отдельные varchar, а запись целиком. То есть потенциально могут быть сжаты и другие типы данных если там null.

              Во-вторых как я уже говорил кодирование в зависимости от типа данных пробовалось и оно оказалось медленней. Возможно где-то что-то упустили. Но факт остается фактом.

              И в-третьих, firebird 5.0 не вводит новую мажорную ods. RLE использовался еще со времен interbase, просто сейчас его усовершенствовали. Таким образом 5 ка может работать с базами данных 4 ки.

              В будущих версиях сжатие/ кодирование записи может изменится.


              1. qw1
                11.01.2024 17:56

                Тогда удивительно, как работает alter column.
                Допустим, была запись

                (NAME VARCHAR(30), GENDER INT)
                

                это значит, что 30 байт строка, за ней число.
                сделали

                ALTER COLUMN NAME TYPE VARCHAR(50)
                

                Движок теперь считает, что после распаковки записи, GENDER надо искать не по смещению 30, а по смещению 50. Но когда он читает старую запись со старой страницы, как он понимает, что смещение 30? В базе хранятся все версии метаданных и у каждой записи есть ссылка на версию?


                1. sim_84 Автор
                  11.01.2024 17:56
                  +2

                  Каждая запись хранит номер формата (1 байт). Формат записи содержит все необходимые сведения для декодирования записи в том числе типы и смещения. Но когда для таблицы делаешь alter более 255 раз будет больно (либо бекап рестор, либо пересоздание таблицы с полной перезаливкой данных). Впрочем не любой alter меняет формат. Это одна из особенностей фб, которая с одной стороны позволяет делать быстрый alter таблицы без ее блокировки, но с другой ограничивает полет фантазий разработчика.


                  1. qw1
                    11.01.2024 17:56
                    +1

                    Значит, ничего не поделать. Придётся при указании размера varchar(N) постоянно пребывать в сомнениях: а не мало ли я выбрал, а не много ли...


  1. qw1
    11.01.2024 17:56

    Соединение потоков с помощью алгоритма HASH JOIN появилось в Firebird 3.0

    Было бы круто иметь синтаксис, как в SQL Server, с явным указанием алгоритма:

    inner HASH join table1
    left LOOP join table2
    outer MERGE join table3
    

    В ручных оптимизациях этим можно пользоваться.


    1. sim_84 Автор
      11.01.2024 17:56

      Это нестандартно и не переносимо, поэтому такого точно не будет, а вот поддержка хинтов Аля оракул не помешала бы.


      1. qw1
        11.01.2024 17:56

        Ваше дело, конечно.
        Но подумайте о программистах. В трёхстраничном запросе сопоставлять join-ы где-то из середины страницы и хинтами в шапке. Напутать можно. А если хинты привязаны к тому месту, где они применяются, всё гораздо прозрачнее. То же самое с хинтами по индексам в SQL Server.


        1. Ivan22
          11.01.2024 17:56

          ну есть хинты ms style - в самих джоинах, есть хинты oracle stype - все в коментах в начале, а есть хинты postgres style :/


  1. qw1
    11.01.2024 17:56

    Полный синтаксис создания индекса выглядит следующим образом

    Как будто синтаксис дополнился указанием направлением сортировки перед словом INDEX
    А есть ли разница между

    CREATE DESC INDEX horse_deathdate ON horse(deathdate)
    

    и классическим

    CREATE INDEX horse_deathdate ON horse(deathdate DESC)
    


    1. sim_84 Автор
      11.01.2024 17:56

      Не знаю где вы этот "классический" вариант нашли. Его в Firebird с роду не было. Направление всегда указывалось для всего индекса, а не отдельного столбца.

      А дополнился синтаксис предложением where, которое фильтрует ключи записей по некоторому условию. Те что не соответствуют предикату просто не будут попадать в индекс.


      1. qw1
        11.01.2024 17:56
        +2

        Не знаю где вы этот "классический" вариант нашли

        В том же SQL Server. Я думал, оно в стандарте ))

        Его в Firebird срроду не было

        Тогда печаль. Иногда супер-важно, для каждого столбца задать своё направление упорядочивания.


        1. sim_84 Автор
          11.01.2024 17:56
          +1

          Microsoft SQL Server в некоторых случаях далек от SQL стандарта. В прочем ни один он, все так или иначе отклоняются от стандарта, ибо стандарт обычно формируется постфактум, когда коммерческие субд уже реализовали некоторую фичу и пропихивают ее в стандарт.


          1. qw1
            11.01.2024 17:56
            +1

            Просто пример, где это удобно:

            create index OBJ_LAST_VERSION on OBJECTS (OBJECT_ID, VERSION desc);
            
            select first 1 * from OBJECTS where OBJECT_ID=:objId order by VERSION desc;
            

            Тут для столбца VERSION задана обратная сортировка для выборки последней версии указанного объекта.
            Поэтому будет чтение строго одной записи из индекса, а не вычитываение всех записей по OBJECT_ID, а потом сортировка во внутреннем буфере и возврат первой строки, как если бы на VERSION не стоял DESC.
            Впрочем, наверное можно обойти COMPUTED BY полем, но не каждый догадается, как.


  1. kossmak
    11.01.2024 17:56

    По синдрому утенка прям любимая СУБД - отрадно, что живёт-развивается.

    Автору спасибище за подробности