InterBase: тормозология и глюконавтика
Страница 5. Таблицы


Таблицы

Ну, во-первых, таблица - это последовательность страниц. То есть образование внутри базы на подобие файла на диске. Только в отличие от файла, над ней могут проделываться более зверские операции типа изъятия кусков (записей) из середины. Очевидно, что от этого начинается фрагментация. Конечно, любая СУБД старается по мере возможностей и интеллекта с этим бороться. В частности, сливать соседние страницы, если они полу- и более пустые. Но не всегда это возможно. Потому нужно учитывать, что массовое удаление записей не обязано ускорить доступ к оставшимся. Полную гарантию дефрагментации базы даёт только полный перебэкап.

Некоторые подробности о состоянии данных можно узнать от утилиты gstat. Особенно с ключом -a. В частности, по таблицам выдаётся номера начальных (первой страницы со списками страниц и первой страницы с данными), количества страниц и средняя степень заполнения. Низкие значения однозначно свидетельствуют об избыточной фрагментации, высокие - не обязательно. Плотная таблица может состоять из страниц, разбросанных по всей базе. Кроме этого выдаётся весьма любопытная статистика о распределении степени заполнения страниц - сколько заполнено в пределах 20%, сколько - от 20 до 40 и так далее до 100%. Очевидно, что достичь 100% заполнения страниц довольно сложно, да и обновлять такие таблицы проблематично. Тем не менее, желательно следить, чтобы подавляющая часть страниц попадала в последние два диапазона.

Что касается страниц внути, то они, понятно, заполнены записями. Точный формат, разумеется, неизвестен, но можно сказать точно, что каждая запись может быть переменного размера и имеет заголовок. В заголовке указывается версия формата таблицы (форматы можно немного посмотреть через rdb$formats) а так же битовый массив, в котором каждому полю nullable соответствует один бит. Бит, как Вы наверное уже догадались, обозначает, есть в записи значение этого поля или нет. То есть на хранение поля в состоянии null interbase расходует всего один бит!

Но не только по этой причине записи имеют разный размер. Другая причина - строковые поля. Известно, что с точки зрения ползьзователя имеются два типа данных - char() фиксированной длинны и varchar() - переменной. Внутри же и те и другие слегка преобразовываются и хранятся в "переменном" виде. Но обо всём по порядку.

Во-первых, строки на диске и при обработке в памяти хранятся существенно разным способом. На диске они всегда хранятся в достаточно упакованном виде, занимая минимальное пространство переменного размера. Причём это относится как к char, так и к varchar. В первом приближении можно считать, что они хранятся совсем одинаково. На самом деле разница составляет два байта. Сказанное одинаково справедливо как для ODS 8, так и для ODS 9.

Если более точно, то поле типа char(n) в структуре физической записи состоит из 2 байт заголовка, описывающих длинну строки без учёта концевых пробелов, после чего идёт сама строка, окромя этих же пробелов, которые за ненадобностью отбрасываются.

При считывании таких строк interbase всегда заранее знает из метаданных, какой длинны они должны быть (то самое n) и дополняет их в памяти пробелами "до кондиции". Таким образом, в обрабоку эти строки всё равно попадут с пробелами на хвосте (если, конечно, строка не была во всю длинну заполнена другими символами). При записи же пробелы будут опять обрезаны.

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

Что касается полей varchar(n), то они физически на 2 байта длиннее, чем char(n)! Это не шутка. Дело в том, что над ними проделывается совершенно та же операция с отбрасыванием хвостовых пробелов (только хвостовых!), что и с char. Однако в данном случае остаётся неизвестным, до какой длинны доводить строку при считывании. По-этому в заголовок кроме хранимой длинны добавляется вторая, длинна с точки зрения пользователя, тоже занимающая 2 байта.

По-моему последнее есть большая глупость со стороны авторов interbase, так как строки varchar обычно используются как раз тогда, когда концевые пробелы не нужны. Но уж ладно, то, что есть, работает неплохо.

Отсюда, к стати, следует одно ограничение - строки не могут быть длиннее 32К. Реально - чуть меньше 32К, на пару десятков байт, точную цифру не помню. То есть двухбайтовые размеры строк учитываются в interbase со знаком, хотя не совсем понятно, зачем. В общем, если максимальный размер укладывается в данное ограничение, то нет существенной разницы (с точки зрения БД), какой размер задавать: 20, 200, или 20000 - на хранении это никак не скажется. Скорее, оно будет работать как своеобразное ограничение целостности. Правда, BDE и Delphi по каким-то рудиментарным соображениям считают поля свыше 255 байт большими и представляют их клиенту, как блобы. Но interbase в этом уже не виноват.

Если же нужно больше 32К, то точно придётся использовать блобы. Блоб в interbase может быть длинной до 2ГБ, читаться и писаться частями (см. CreateBlobStream в Delphi). Зато под каждый экземпляр блоба отводится, как минимум, отдельная страница БД (в общем случае - последовательность страниц). К некоторым блобам (sub_type text) применимы понятия кодировки. В самой таблице с блобовыми полями хранится не блоб, а ссылка на него в виде так называемого BLOB handle. Эта же ссылка передаётся и клиенту (если это настоящий блоб, а не только что упомянутая самодеятельность BDE). После чего клиент может с ней работать, как с handle открытого файла в операционке.

Ещё у блоба есть такой параметр - segment size. Он определяет размер буферов памяти, предназначенных для доступа к этому блобу и следовательно, размер тех кусков, которыми данные будут передаваться в/из приложения. По умолчанию он установлен в 80 байт. Это смехотворно мало. Знающие люди советуют установить не меньше размера страницы БД, и я с ними полностью согласен. Особенно радикально это скажется при работе в сети - запросы на 4 КБ обычно работают как минимум на порядок (обычно - ещё больше) быстрее, чем запросики на 80 байт.

Теперь зададимся "глупым" вопросом: а сколько байт занимает символ? Кому-то может показаться странным, но у китайцев, японцев, и некоторых других восточных национальностей кодировки включают символы из двух и даже из трёх байтов. То есть часть байтов рассматриваются как префиксы, за которыми обязательно должен следовать один или два байта.

interbase рассчитан на максимальную длинну символа в 3 байта. Строки, хранимые в записях всегда занимают ровно столько байт, сколько надо. То есть если в строке 3 однобайтовых и 5 трёхбайтовых символов, то она займёт 18 байт. Плюс, разумеется заголовки и манипуляции с хвостовыми пробелами (пробел = 1 байт), как описано выше. Все наши кодировки - однобайтовые и никаких спецэффектов на диске не дают.

Прикол же в том, что перед обработкой в памяти все строки в национальных кодировках (то есть окромя character set none), конвертируются "по максимуму" в трёхбайтовый формат! В том числе это относится и к построению индексов, о чём чуть позже. Таким образом, даже русский природно однобайтовый текст растягивается в трёхбайтовый формат. Уж не знаю, чего они там этим самым выиграли, но это так. Я пробовал индексировать Unicode, которую считал исконно двухбайтовой кодировкой, и выяснилось, что даже её растягивают в 3 байта. Как в последствии оказалось, Уникод сейчас тоже подрастянули, и даже до 4 байт, так как количество иероглифов, обнаруженных на Земле, превысило 64К. Ещё прикол: в серверные функции UDF строки передаются всё-таки в упакованном формате, минимальным количеством байт, и обратно принимаются так же. Хоть здесь проблемы не создали.

Дальнейшие подробности и неприятности на эту тему освещены в разделе про индексы.

Теперь я бы хотел вернуться ещё раз к записям и рассказать про одну мерзопакостную вещь. Как уже было сказано, interbase может хранить в базе несколько версий структуры одной и той же таблицы. Фактически alter table приводит к добавлению очередной версии на базе предшествующей. Данные при этом не меняются. interbase их вообще не трогает. Это становится возможным благодаря тому, что в заголовке каждой записи, как опять же было сказано хранится версия структуры. При любых попытках вставить или обновить запись её структура обновляется до последней. Остальные же продолжают храниться в старой.

Казалось бы, замечательная оптимизация, но разработчики interbase допустили один фундаментальнейший глюк. Если добавить поле с каким-либо default, а затем прочитать в память запись старого формата, то добавленное поле будет проинициализировано null. Даже несмотря на default. И даже несмотря на not null! И даже если это будет противоречить всем check(). Самая большая пакость из этого глюка возникает тогда, когда с БД делается резервная копия. Бэкап пройдёт нормально, без единого замечания. Но восстановление будет невозможным - дойдя до первой записи с незаконным null'ом, interbase выругается и остановится. В общем, назад свою базу Вы не получите, если не примете меры ДО бэкапа.

Бороться с этим можно только одним способом - не забывать делать инициализацию default руками, всегда поступая так:

alter table Таблица add Новое поле ...;

update Таблица set НовоеПоле = ЗначениеDefault;

commit;

Это во-первых, приведёт все записи к самой свежей структуре, а во-вторых, обеспечит правильный default.

 
« Предыдущая статья