Хранение GUID и размер индексов

Кузьменко Дмитрий, support@ibase.ru

GUID – уникальный идентификатор. Его длина позволяет использовать GUID на разных машинах с весьма низкой вероятностью совпадения генерируемых GUIDs. Например, все документы (записи) баз данных Lotus Notes идентифицируются собственным GUID. Это позволяет решать проблемы при репликации документов. Некоторые сервера позволяют хранить GUID в его двоичном представлении, размером 16 байт. В IB нет возможности сохранить двоичное значение в строке, а blob для таких целей использовать будет неэффективно. Поэтому выход остается один – хранить GUID как строку.

Генерируемый функцией CoCreateGuid(ClassID) и StringFromCLSID(ClassID, P) GUID получается размером 38 символов, включая обрамляющие фигурные скобки и 4 символа '-'. Например, {C9EAF9E0-9B6B-11D3-9C10-008048E8D560}. Такое представление годится для Registry, но для хранения в качестве первичного ключа несколько избыточно.

Если исключить те самые фигурные скобки и тире, останется 32 символа. Однако IB не может эффективно хранить такие значения. Дело в том, что в строковом выражении GUID меняется в своей левой части. Т. е. младшие разряды строкового GUID находятся в левой части строки.

Как известно, IB производит упаковку ключей и записей при обновлении с помощью алгоритма, похожего на RLE. Это означает, что если создана запись со строкой "Иванов", а затем запись со строкой "Иванова", то ключ индекса по строковому полю для второй записи будет содержать число совпадений с предыдущим ключом, и несовпадающие символы. Буквально для нашего примера – 6'а'. При большом количестве таких упаковок, и при большом количестве совпадений, размер, требуемый для индекса на диске, должен быть существенно ниже.

Попробуем произвести тест. Создадим кроме функции CreateGUID функцию CreateRevGUID (см. guid_udf.zip), где строка GUID будет перевернутой, т. е. младшие разряды окажутся в правой части строки.
 
Примечание. В Windows 2000, XP, 2003 и далее GUID генерируется функцией CoCreateGuid целиком случайно, а не последовательно, как это было на момент написания данного документа.
Создадим две таблицы у каждой по одному полю CHAR(32), и это поле сделаем первичным ключом. В одну таблицу вставим 100 тысяч записей с обычным CreateGUID, а во вторую – с CreateRevGUID.
 
Примечание. В Firebird 2.5 появилась встроенная функция GEN_UUID.
После наполнения размер таблиц оказался одинаковым (это естественно), т. к. для записей при вставке упаковка не производится. (Размер страницы БД – 8К).
NGUIDS (130)
Primary pointer page: 135, Index root page: 136
Data pages: 953, data page slots: 953, average fill: 69%
Fill distribution:
0 - 19% = 0
20 - 39% = 1
40 - 59% = 0
60 - 79% = 952
80 - 99% = 0

А вот размер индексов оказался существенно разным. Для таблицы с CreateGUID:
Index RDB$PRIMARY3 (0)
Depth: 3, leaf buckets: 770, nodes: 100000
Average data length: 25.00, total dup: 0, max dup: 0
Fill distribution:
0 - 19% = 1
20 - 39% = 0
40 - 59% = 763
60 - 79% = 0
80 - 99% = 6
т. е. количество страниц индекса – 770, а для таблицы с CreateRevGUID:
Index RDB$PRIMARY2 (0)
Depth: 2, leaf buckets: 92, nodes: 100000
Average data length: 1.00, total dup: 0, max dup: 0
Fill distribution:
0 - 19% = 1
20 - 39% = 0
40 - 59% = 8
60 - 79% = 0
80 - 99% = 83
т. е. 92 страницы, что в 8 раз меньше!

После backup/restore эта разница оказывается в 2 раза меньшей (т. е. 4 раза), за счет того, что после restore индексные страницы оказываются заполненными на ~100% – индекс RDB$PRIMARY3 после restore занимал 384 страницы.

Разумеется, скорость поиска, выборки и сортировки по упакованным ключам выше, чем обычно, т. к. с диска требуется считывать меньшее количество страниц индекса.

Выводы:
  1. IB производит упаковку ключей индексов, даже если производится только вставка записей.
  2. Упаковываемые ключи плотнее заполняют пространство на страницах данных, чем неупаковываемые
  3. Для функций, аналогичных CreateGUID максимальной эффективности можно достичь, только если часто изменяемая часть строки находится в правой части.
Примечание. Еще большей степени упаковки можно достичь, используя функции PackedGUID. Длина строки при использовании этих функций получается 26 байт. Однако результат функции NxPackGUID необходимо перевернуть так же, как и в CreateRevGUID (или производить конвертацию TGUID со старшего байта).
Примечание. Можно попробовать хранить GUID в естественном виде, т. е. длиной 16 байт. При этом строка должна быть создана как CHARACTER SET OCTETS. Этот набор символов позволяет хранить в строках произвольные символы, в том числе 0x. Однако клиентская часть также должна уметь сохранять строку целой, т. е. не обрезать ее до первого символа 0x, как это принято для строк C или строк pchar.
Например, в IBX или FreeIBComponents для этих целей компоненты, использующие стандартные Fields, не годятся – IBTable, IBDataSet, IBQuery. Они отрезают часть после 0x еще на этапе занесения значения параметров. А вот компонент IBSQL (FIBQuery) вполне подходит, и как записывает данные с 0x в такое поле, так и извлекает их обратно, поскольку работает с XSQLVAR в качестве полей запроса или его параметров.

Подпишитесь на новости Firebird в России

Подписаться