InterBase: тормозология и глюконавтика

ОГЛАВЛЕНИЕ

Если суммировать мысли кратко, то по поводу тормозологии interbase: когда делаешь что-то серьёзное, ни одна СУБД не сделает всё за тебя. То есть наличие супер-пупер умного оптимизатора не спасает от проблем, а лишь отодвигает их. В конечном итоге это в чём-то даже хуже, потому что когда ты упрёшься в проблемы, то будет наделано уже столько, что не исправишь. И в этой ситуации начинает играть роль не интеллект оптимизатора, а возможности ручного управления отработкой запросов interbase. В конце концов, ни один оптимизатор не знает о семантике запроса столько, сколько знает разработчик interbase (иначе это не разработчик, а ...). Вот тут-то оказывается, что interbase при внешней простоте предоставляет очень широкие возможности для управления. Фактически, более крутую вещь я видел в PostgreSQL - там можно оптимизатору собственные правила подсовывать, то есть делиться опытом. А уж что касается таких попсовых вещей, как MS SQL, то они interbase в плане управляемости в подмётки не годятся.

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

Предполагается, что читатель знаком с основами БД вообще и InterBase в частности и хочет расширить свои познания. В прочем, не откажусь от обратного - если кто-то найдёт неточности, выскажет пожелания, или поделится своим опытом, всё это будет воспринято. В общем, это - не учебник.


Как устроены файлы БД внутри

Вообще-то писать на эту тему про коммерческие СУБД достаточно сложно, так как большинство поставщиков стремится скрыть внутренние принципы, чтобы не стянули конкуренты. С другой стороны, без определённого уровня знаний невозможно понять, что происходит внутри СУБД и, соответственно, оптимизировать это дело.

Во-первых, базы данных хранятся в файлах. Хотя существуют системы, которые позволяют хранить данные прямо в сырых разделах, этот способ уже вышел из моды, так как современные файловые системы не оказывают существенного торможения, как это было когда-то. Далее - файл состоит из байтов (хотя в unixообразных системах бывают и блочные). И в этом массиве байтов нужно размещать кучу всего: записи данных, индексов, хранимые процедуры, метаданные (структура всего остального), и т. д. Все эти вещи имеют самые различные размеры.

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

Страницы

Таким образом, возникает задача динамического распределения адресного пространства внутри файла(ов) БД. Решается она достаточно простым образом - пространство файла делится на страницы фиксированного размера. Размер обычно должен быть кратным степени двойки для упрощения вычислений и достаточно большим, чтобы в него влезало по несколько записей большинства видов. Если же попадается некоторый объект (скажем BLOB), который в одну страницу не умещается, то его делят на части и снабжают внутренней индексной информацией, куда какой кусок поместили.

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

Естественно, размер страницы влияет на эффективность использования пространства в БД. Примерно такая же ситуация возникает в файловых системах, когда надо разместить файлы в кластерах. Естественно, в конце каждой страницы остаются небольшие промежутки, в которые не умещается ни одна запись. Поскольку обычно в каждой странице размещается связанная информация, скажем, записи только из одной таблицы, то и размер "хвостов" будет различным в страницах различного типа. Чем больше средний размер записи, тем больше "хвосты".

Если размер самих страниц небольшой, то придётся часто делить записи, а обращение к записям и BLOB'ам по частям замедлит производительность. "Хвостов" будет много, но они будут сравнительно небольшие, так что база слегка уплотнится. Большое количество страниц в базе так же слегка замедляет их поиск и обработку. Если же размер будет большим, то "хвостов" будет мало. Размер же их будет зависеть от характера данных.

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

С другой стороны, увеличение размера страницы довольно радикальным образом ускоряет обращения к индексам. О них будет рассказано отдельно. Фактически количество обращений к индексу при поиске записи зависит от глубины дерева, которая напрямую определяется соотношением объёма индексируемых значений и размера страницы. И это - важный повод для того, чтобы сделать страницу по-больше.

Размер страницы по умолчанию в interbase составляет 1 КБ. Когда-то я писал, что это нормально. Теперь я так не считаю. Это мало. Рекомендую 4 или 8 КБ. Последнее, насколько я знаю, предел. Базы с 1 КБ можно эксплуатировать только на очень слабой машине, когда экономия памяти (как оперативной, так и дисковой) имеет решающее значение. Хотя лучше в таких условиях базами данных вообще не заниматься.

По поводу того, что именно выбрать, 4 или 8, могу скзать следующее. В документации по настройке interbase есть тёмные места, в которых непонятно, о каких страницах идёт речь - страницах БД или страницах виртуальной памяти. Отсюда сложности с точной оценкой поведения сервера. Если же настроить размеры страниц равными, неопределённость устраняется. По этой (возможно, субъективной) причине мне больше нравится размер в 4 КБ. По крайней мере на процессорах Intel виртуальная память работает именно такими страницами. Хотя если в базе есть таблицы с миллионами записей, а оперативная память на сервере - сотни мегабайт, то я всё же порекомендую однозначно 8.


Файлы

То, что пропускная способность диска - главнейший фактор при работе с большими массивами данных, думаю, ежу понятно. Если слегка подумать над этим вопросом, то возникнет желание распараллелить обращения по нескольким дискам. Кроме этого, некоторые БД имеют тенденцию вырастать до такого размера, что не умещаются на одном диске. В общем, большие и нагруженные базы надо делить.

В InterBase каждая база состоит из главного файла (primary file) и необязательного набора дополнительных файлов. К сожалению, насколько удалось выяснить, на этом удобства и заканчиваются. Никаких средств, чтобы указать "эту таблицу помести сюда, а этот индекс - сюда" обнаружить не удалось. А жаль.

Кроме этого в Interbase for Netware существует такая полезная вещь, как журнал упреждающей записи (Write Ahead Log). Суть идеи в том, что от базы на отдельный файл (обычно на другом диске) отделяется специальная область для быстрой записи обновлений. Этот журнал по сравнению с основной базой занимает гораздо меньше места, и обновлять его гораздо легче. Кроме того, предусмотрены средства для его деления на части. Таким образом, СУБД получает возможность очень быстро откликаться на обновления, и только затем переносить изменения в основную базу. В фоновом режиме и без задержки пользовательских запросов. Поскольку это удовольствие - только для пользователей Netware, далее на него отвлекаться не буду. При той архитектуре управления транзакциями, которая применяется в interbase толку от этого журнала немного.

Все файлы в основном наборе, кроме последнего, имеют фиксированную длинну, задаваемую при их создании, в случае их полного заполнения растёт только последний. Синтаксис соответствующего оператора create database приведён ниже. Предусмотрен оператор для добавления файлов в базу - alter database.

Вторая особенность, мимо которой нельзя пройти при обсуждении данной темы - это возможность иметь несколько копий одной и той же базы в виде так называемых теневых файлов (shadow). Набор теневых файлов, как я понял, хранит в себе зеркальное отображение страниц из основной последовательности файлов. У такого решения есть как достоинства, так и недостатки.

+ Повышается надёжность. Разложив теневые и основные копии по разным дискам можно получить дополнительную гарантию, что при аварии диска хоть одна копия да выживет.

+ Повышается производительность чтения. Когда при поступлении параллельных запросов или отработке частей одного запроса надо прочитать несколько областей БД, появляется возможность параллельно читать их с разных дисков. Это не всегда возможно без теневых файлов, так как обе области могут оказаться в одном файле на одном диске.

- Места на диске расходуется в два (три, четыре, ... в зависимости от количества теней) раза больше.

- Могут замедлиться обновления. Всё, что пишется, должно записаться в два или больше файлов. Иначе, какая же это тень? В общем, скорость записи реально определяется скоростью самого тормозного из запараллеленых дисков.


Как это всё можно задать

При создании базы - опциями оператора create database. Синтаксис примерно следующий:
create database "имя" ... всякие опции ...
file "имя" ... опции для файла ...
file ...
Среди опций нас в данный момент интересуют:

page_size=nnn
Указывается только для базы в целом и задаёт размер страницы. Он должен быть кратным двойке, начиная с 1024. Изменять можно только в большую сторону, и обычно для этого есть основания, так как 1024 - не самый эффективный вариант.
length=nnn
Задаёт длинну в страницах главного или вторичного файла БД в зависимости от того, где указана опция.
starting at nnn
Задаёт начальную страницу базы, с которой начинается данный файл. Страницы считаются с 1. Таким образом, если первый файл в базе имел размер 10000 страниц, то второй должен быть starting at 10001. Наложения и разрывы в последовательности страниц в файлах не допускаются. Если в данном случае написать starting at 15000, то файл всё равно начнётся с 10001.

Теневые файлы создаются после того, как создан основной набор файлов и, в принципе не обязаны покрывать его весь. Каждой тени присваивается номер (целое положительное число) и все теневые файлы приписываются к определённому номеру тени. Внутри тени с одним номером правила создания файлов примерно такие же, как и в главной последовательности файлов - задаются размеры или (и) начальные позиции в страницах. Операторы называются create shadow/drop shadow. Кроме всего прочего, в этих операторах предусмотрена реакция на ситуацию, когда один из теневых наборов портится или становится недоступным (продолжать в этом случае работу на сохранившися наборах, или нет).

Как это всё можно изменить в существующей базе данных

Совсем коротко: снять с базы резервную копию и восстановить с новыми параметрами.

При создании резервной копии никаких особенностей практически нет. Достаточно указать ссылку на начальный файл главной (не теневой) копии, остальное InterBase найдёт сам.

При восстановлении с помощью программы Server Manager (или командной строки gbak) можно изменить размер страницы и состав файлов в главной копии базы. Первое делается вводом нужного значения в поле редактирования Page Size (-P nnnn), второе - нажатием кнопки Multi-File и добавлением вторичных файлов в список. Суть параметров та же, что и при создании базы данных.

Gbak по идее должен делать многофайловую базу, если вместо одного имени выходного файла написать "имя1 размер1 имя2 размер2 ... имяN". Размеры, как обычно, в страницах. Размер для последнего файла не указывается. Только вот в interbase 4.X приходилось наблюдать глюк, когда файлы создавались, но вся база восстанавливалась в последний, независимо от размеров остальных. В общем, будьте осторожны.

Снимать резервные копии с базы данных с тенями, я, честно говоря, не пробовал, но некоторые вещи в документации на этот счёт меня смутили. Во-первых, имеется опция игнорирования теней при восстановлении. Это значит, что состав теневых файлов, запоминается в резервной копии и восстанавливается по умолчанию. Если так, то почему этого не делается с последовательностью основных файлов?


Таблицы

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

Некоторые подробности о состоянии данных можно узнать от утилиты 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.


Индексы

Индекс (если он только не кластерный, чего в InterBase пока не водится) - это обычно какое-нибудь B+++++дерево, супер-пупер-навороченная хэш-структура, битовая-недобитовая карта или ещё что-либо, что содержит в себе информацию о значениях поля (комбинации полей) и физическом местоположении соответствующей записи целиком, а так же позволяет радикально быстрее, чем прямым перебором, выполнять следующие функции:

  • По конкретному значению выйти на нужную запись. Это, в частности, используется при поиске и при проверке на дублирование значений ключа.
  • Выйти на следующую запись после текущей в порядке возрастания/убывания значений поля. Многократно выполняя эту функцию, можно выбрать всю таблицу в отсортированном порядке. Бывает полезно для отработки соединений.
  • Комбинируя две предыдущие функции можно пройтись по записям с заданным диапазоном значений.
  • Сортировка означает группировку - в процессе прохода по индексу легко идентифицировать границы групп.
  • Выйти на запись с минимальным/максимальным значением поля (как выяснилось, этого InterBase 4.x не умеет, хотя пятый по слухам научился).

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

На естественный вопрос, какая именно структура у индексов interbase я ответить точно не могу. Во-первых, ситуация слегка напоминает таблицы: индекс ведь тоже представляет собой последовательность страниц. Соответственно, индексы тоже могуть быть фрагметированными или разреженными в результате обновлений (соответствующих данных). И, как обычно, все неприятности лечатся только перебэкапом.

Известно так же, что в структуре индексов interbase присутствуют какие-то древовидные элементы. По крайней мере в нескольких местах документации упоминается глубина дерева и его балансировка. Кое-какие сопутствующие советы были даны при обсуждении размеров страниц.

А вот с балансировкой дело довольно нехорошо. Дело в том, что до версии 5.5 interbase не умел автоматически балансировать свои индексы. 5.5 умеет, но только на ODS 9. В остальных случаях единственный способ - разактивизировать индекс и создать его заново. Это проходит для пользовательских индексов, но для системных, то есть тех, которые созданы, как часть ключевых ограничений - никак. Только полный перебэкап.

Казалось бы, что за проблема - балансировка? При случайных обновлениях данных вероятность того, что дерево сильно разбалансируется и станет длинным, действительно мала. Но вот только на практике слишком многие и слишком важные индексы обновляются неслучайно. Классический случай - первичный ключ из генератора. Добавляются значения строго по возрастанию. А это значит, что они будут добавляться строго в одну ветку дерева. В результате эта ветка разрастается в линейный список. И начинаются тормоза. Причём замечу, не при разработке и обычно не при тестировании, а при реальной работе. И далеко не сразу, а тогда, когда база существенно заполнена. Ещё один повод регулярно бэкапиться.

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

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

Вот только применимость таких стурктур, насколько мне известно, сильно ограничена. Как разработчики interbase их присобачили к деревьям и насколько это реально полезно, сказать затрудняюсь.

Хотя есть и другое предположение: что interbase просто вычисляет пересечение и объединение множеств значений из нескольких индексов вполне обычными для деревьев методами. Или чем-то похожим на слияние отсортированных последовательностей, которое будет описано ниже. Только вот биты тут уже получаются непричём. В общем, факт в том, что проход по нескольким индексам работает, и достаточно эффективно. Когда interbase этим не злоупотребляет.

К страницам индексов применимы примерно те же соображения о заполненности, что и к страницам данных таблиц. Та же утилита gstat выдаёт по индексам следующую информацию:

  • Глубину дерева (depth)
  • Количество терминальных страниц (которые со ссылками на записи данных, leaf buckets)
  • Количество ссылок в индексе (nodes)
  • Среднюю длинну данных (average data length), которая почему-то всегда 0.
  • Общее количество продублированных значений и максимальное количество дубликатов одного значения (dup, используется для оценки эффективности поиска записи через данный индекс). Если индекс уникальный, то эти параметры обнуляются - уникальный индекс считается самым эффективным в смысле поиска.
  • Распределение заполненности страниц, как и для таблиц.

Здесь нужно отметить, что некоторые параметры, в частности, связанные с количеством дубликатов не обновляются при обновлении данных. Только при создании/активизации (включая перебэкап) индекса или при выполнении специального оператора set statistics index имя_индекса. Это приводит к тому, что свеженаполненная или массово обновлённая таблица может остаться со старыми, неправильными в новой ситуациями параметрами в индексах. Отсюда interbase может принимать неправильные решения при планировании запросов.

В разделе про таблицы я писал, в частности, о том, как interbase обращается со строками. Всё бы это ничего, данные пертурбации почти не заметны с точки зрения клиента, за исключением одного случая. Когда создаётся индекс, суммарная длинна всех значений, составляющих ключ, обязана уместиться в 256 байт. Иначе interbase пошлёт Вас по-дальше.

Буфер ключа формируется из размеров значений, взятых по максимуму. То есть значения Integer расходуют по 4 байта, Double precision - 8 байт, и т. д. Под строку будет отведена длинна, описанная в метаданных. Независимо от того, char это, или varchar. Зато никаких двухбайтовых заголовков. Правда, у ключа в целом есть заголовок длинной 4 байта.

И вот тут-то выясняется, что строки в национальных кодировках помещаются в индекс в трёхбайтовом формате! То есть упомянутая 20-символьная строка, если она русская, займёт 60 байт. Таким образом, если мы индексируем одно русское строковое поле, то его максимальная длинна может быть (256 - 4) / 3 = 84 символа. Когда-то раньше я говорил, что 64, но тогда я не понимал природу ситуации. Точная цифра - 84. Дальше - Кю. Если же в составе индексируемого ключа присутствуют другие значения, то останется ещё меньше. А индексировать часто нужно именно национальные строки, чтобы не различались заглавные и строчные буквы в соответствии с правлиами нашего языка.

Тем не менее, один мужик (читал где-то на http://interbase.demo.ru) изобрёл способ, как с этим боросться. Я, правда, пока не испытывал, но думаю, должно работать.

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

Далее к таблице приписывается поле с кодировкой nne, той же длинны, что и индексируемое. Затем к таблице приписывается триггер, который по обновлению русского поля автоматически записывает соответствующее значение в верхнем регистре в дополнительное "космополитичное" поле. Если в таблице уже есть данные, нужно её проапдейтить, чтобы дополнительное поле засинхронизировалось с основным. После чего по дополнительному полю строится индекс. Этот индекс сможет проглотить строки до 252 байт длинной. И он будет упорядочен именно без учёта регистра, поскольку строки соответствующим образом обработаны. Во многих случаях этого достаточно.

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

В заключение ещё одно замечание: при поиске по строковому полю индекс используется только при сравнениях с целой строкой (=, <, >). В некоторых случаях индекс используется при starting with. Остальные операции (like, containing) всегда делают свою работу перебором, причём по записям самой таблицы, а не индекса. Хотя индекс содержит информацию колонки таблицы, но в более компактной форме. Однако interbase такое не умеет.


Работа с транзакциями

Есть в InterBase одна сторона, которая делает его очень сильным продуктом. Это как раз та технология, с помощью которой решаются различные аспекты обработки транзакций. Не скажу, что у Борланда здесь всё идеально, но заложенные базовые идеи принципиально более эффективны и прогрессивны, чем у большинства других. Благодаря этому при разработке, основанной на interbase можно просто забыть о многих неприятностях, особенно связанных с блокировками. В общем, изложу сведения на эту тему, почерпнутые из множества мест.

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

Разумеется, реальные схемы блокировок сложнее - они делятся на уровни, типы, разрабатываются сложные протоколы наложения блокировок. Но в целом этот "классический" метод работает по принципу: раз надо предотвратить последствия параллельности, предотвратим саму параллельность. Единственное его достоинство простота ... даже не реализации, а осмысления разработчиком. В семидесятых, когда он появился, это было приемлемо, но сейчас я просто поражаюсь, как можно гнать на рынок блокировочную халтуру, да ещё в таком ассортименте!

К счастью, interbase работает не так. Вместо того, чтобы предотвращать параллельность, он её разрешает, создавая для каждой транзакции видимость данных в том состоянии, в каком ей положено. Для этого в базе данных предусмотрено хранение множества версий страниц с данными (записями, индексами, блобами, ...).

Другая важная особенность "традиционных" СУБД состоит в том, что они содержат журнал транзакций, в котором фиксируются все обновления данных на тот случай, если транзакция завершится неудачно, и эти обновления придётся отматывать назад. Изменения пишутся сначала в журнал, потом в основные данные, а потом из журнала вытираются. Либо, при отмене, переписываются ещё раз.

В interbase журнала транзакций не то, чтобы совсем нет. Он есть, но содержит только номера транзакций, отметки времени, и состояние транзакции. А вот записи данных содержат номера тех транзакций, которыми были сформированы. Что и позволяет interbase разобраться, что кому показывать. Какие данные соответствуют текущему моменту, а какие - нет. При этом ничья работа не останавливается.

Итак, каждой запущенной транзакции присваивается уникальный номер (по возрастанию, как из генератора). Кроме этого транзакция так же получает список состояний других транзакций - какие из них завершены, а какие - нет. Когда впоследствии транзакция ищет запись, она начинает просмотр списка версий с последней. Размер списка, как правило невелик, так как очень редко множество транзакций модифицируют одновременно одну и ту же запись. Если последняя версия принадлежит не текущей транзакции и та транзакция на момент старта не была зафиксирована, то делается переход к предыдущей по времени версии записи. Эти правила могут чуть-чуть меняться в зависимости от режима изоляции (repeatable read, read committed, dirty read), но в целом суть примерно та же. Когда же запись модифицирует запись, она реально добавляет новую версию, уже со своим идентификатором. Удаление - это так же добавление новой версии особого рода.

При чтении версий попутно происходит сборка мусора - освобождение памяти из-под версий, ставших ненужными. Это происходит тогда, когда interbase натыкается на версию, помеченную транзакцией, которая отменена (rolled back). В этом случае interbase не просто продолжает поиск, а выкидывает указанную версию, чтобы на глаза не попалалась.

При всей простоте этого механизма, в нём таится одна неприятная возможность. Почему я и говорил, что у Борланда и здесь не всё идеально. Дело в том, что формирует устаревшие записи одна транзакция, а чистит их - другая. Это значит, что Ваша транзакция, читающая таблицу может быть вынуждена заняться очисткой того, что наваяла в базе предыдущая. И простенький с виду запрос может неожиданно тормознуть.

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

Если Вы пострадали из-за чужой транзакции, то уже ничего не сделаешь. Но если Вы разрабатываете свою программу, то лучше самому такие вещи другим не оставлять. То есть если обновляется одна запись, то можно ничего особенного не делать. Если же идёт массовое обновление таблицы, то лучше всего сразу после этого сделать по ней select count(*) - это прочистит версии и предотваратит дальнейшие аномалии производительности.

В самом предельном случае, если у Вас в базе одна большая таблица, и вы её полностью очищаете одним оператором, то размер базы из-за повторных версий может удвоиться, а время полной выборки таблицы после этой операции будет вдвое-втрое больше, чем было до удаления. Хотя в результате вы получите 0 записей, как и положено. Задержка будет объясняться очисткой версий. На практике, конечно, такое бывает редко, но бывает. Уж лучше бы они завели в сервере какой-нибудь фоновый процесс для очистки.

Когда происходит обновления или удаления, то прежде чем добавить новую версию, проверяется, является ли замещаемая, видмая для данной транзакции версия самой последней. Если да, то всё нормально. Если же нет, то значит мы пытаемся обновить ту версию которую уже обновил кто-то другой. В этом случае interbase фиксирует ошибку и не отрабатывает обновление. То есть конфликты обновлений всегда фиксируются, однако в отличие от блокировочных систем, не путём приостановки возможных причин, а только тогда, когда конфликты реально происходят.

В общем, это работает и в большинстве случаев прекрасно. Если Ваша транзакция только читает данные, то её никто вообще остановить не сможет - здесь параллельность вообще идеальная. Правда её подпортили при переходе к многопоточной реализации сервера. Где-то у них там проблемы с синхронизацией потоков. По крайней мере в версии 4.2. В результате чего тяжёлые запросы имеют свойство блокировать друг друга, а на многопроцессорных машинах - скапливаться в одном процессоре. Старые версии (до 4.2) и версии для Linux и SCO этим не страдают.

Ещё кое-какие замечания по управлению транзакциями есть в разделе по BDE.


Отработка запросов и производительность

Как планируется отработка запроса

Здесь мне, видимо, придётся изложить хотя бы минимальные отрывки из теории оптимизации запросов, так как наше $#@!%$# программистское образование, как я заметил, обычно даёт непростительно мало информации по этому поводу. Это притом, что и литература, и документация обычно льют на эту тему много воды, но реально полезной информации в них так же маловато.

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

Далее, SQL-запросы обычно выражают собой некоторую комбинацию реляционных операций. То есть если быть математически точным, то это всё - враньё: SQL не является строго реляционным языком и делает многие вещи так, как удобнее было разработчикам первых версий из interbaseM в начале семидесятых, а не как правильно теоретически или с точки зрения прикладных задач. В частности, допускает дублирование записей, недопонимает ключевые ограничения, и т. д., но это тема для отдельного разговора про SQL вообще, а не про InterBase.

Большинство запросов представляет собой ту или иную комбинацию соединения, выборки и проекции. Грубо это можно определить следующим образом: в результате соединения двух таблиц получается таблица, состоящая из конкатенаций записей исходных таблиц, причём выбираются только те конкатенации, где выполняется некоторое условие между полями исходных записей. Как правило, это условие - равенство, а соединение называют эквисоединением или естественным соединением. Естественно, что моё определение грубо, как и сам SQL.

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

В результате проекции получается таблица, записи которой содержат лишь часть полей исходной.

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

Знаток InterBase или чего-либо покруче может заметить, что источниками записей могут являться представления и хранимые процедуры. Но что такое представление? Всё тот же запрос! То есть выражение. А его можно алгебраически подставить в другое выражение и получить то, что в конечном счёте даст результат запроса. В interbase всё не совсем так просто, появляются кое-какие ограничения, но в целом происходит примерно так.

Что же касается процедур, то их InterBase воспринимает, как неупорядоченные, неиндексированные, некэшируемые (мало ли что вычислится при следующем вызове) таблицы. В общем, один из наиболее неудобных источников данных и это следует учитывать.

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

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

Ещё один фактор, который учитывают действительно мощные системы, но не InterBase: цели планирования. Можно создать алгоритм, который очень быстро выдаст первую запись, или первые несколько, но остальные будет выдавать сравнительно медленными темпами. А можно тот, который сначала подумает немного дольше, но затем выдаст сразу все записи, потратив в сумме меньше времени. Первый режим рекомендуется для интерактивных систем, а второй - для массовой пакетной обработки. И эти два режима планирования могут давать совершенно различные алгоритмы даже для одного и того же запроса на одних и тех же данных.

Вот только InterBase в отсутствии внешних подсказок всегда работает во втором режиме, то есть оптимизируя (насколько умеет) суммарное время. А между тем большинство продуктов Борланда ориентировано именно на интерактивную обработку. В результате - мерзкие паузы при листании записей, если они извлекаются сколь-нибудь сложным запросом.


Как делаются соединения таблиц

Из всех перечисленных и не перечисленных в предыдущем разделе операций для нас важнейшей является соединение. Дело в том, что остальные операции даже в тупом варианте почти всегда делаются за один проход по исходной таблице. В худшем случае (сортировка) - за n*log(n), где n - количество записей.

Кстати, основание логарифма зависит от размера оперативной памяти (кэша то есть) и быстродействия процессора и на современных (даже персональных) машинах очень велико - сотни и тысячи. Так что про логарифм обычно можно забыть и считать log(n) ~ const.

В то же время, тупое соединение полным перебором всех комбинаций делается за n*m обращений к данным, где в одной таблице n записей, а в другой - m. С учётом того, что записи обычно физически разбросаны по базе, мы имеем квадратичную зависимость обращений к диску от размера данных. А если соединяются три таблицы - то кубическую. И чем дальше, тем хуже. На фоне этого сортировка миллионов записей превращается в мелочь.

Тем не менее, всякая уважающая себя СУБД решает эти вопросы принципиально более быстро. Далее будем рассматривать соединение двух таблиц, так как три и более таблицы можно соединять попарно. И будем считать, что соединение делается по условию равенства полей этих двух таблиц. Представим себе, что обе соединяемые таблицы физически отсортированы. И мы можем читать из них записи подряд без существенных физических затрат.

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

Поскольку записи упорядочены, то каждое чтение приводит к выдаче записи, сравниваемое поле которой не меньше, чем у предыдущей. Значит, чтение приводит к возрастанию (в читаемой таблице) и рано или поздно текущая запись станет либо равной текущей в другой таблице (ура, нашли совпадение!), либо больше. В последнем случае принимаемся аналогичным образом вычитывать другую.

Можно показать, что при таком алгоритме будут учтены все комбинации записей с равными полями. И сделано это будет за один проход. То есть за n+m операций! Очевидно, что это лучше, чем n*m. А на четырёх таблицах n+m+p+q уж совсем лучше, чем n*m*p*q.

Однако в начале рассуждений было сделано предположение о физической отсортированности таблиц. На практике это редко бывает справедливо. Хотя лучшие СУБД предоставляют возможность создания так называемых кластерных индексов. Фактически это не индекс, а способ хранения самих записей в упорядоченном виде. Сортировать всю таблицу полностью было бы накладно, однако можно разрезать её на группы записей с близкими значениями и отсортировать эти группы внутри. Таким образом, "чтение в заданном порядке" будет происходить почти непрерывно, лишь с редкими перескоками от одной группы к другой. Вот только есть огорчающее обстоятельство: в нынешних версиях InterBase ничего подобного вообще нет.

Тем не менее, как было замечено выше, задачу перебора записей в заданном порядке можно решить с помощью индекса. Это решение не совсем такое быстрое, как кластеризация, кроме того случая, когда прямо в индексе содержатся все необходимые поля (последним фактом interbase не воспользуется). Но всё же это быстрее, чем перекапывать все данные. В том числе и по этой причине во многих СУБД, включая InterBase, существует жёсткое правило: все ключи, участвующие в ссылочных (и ключевых тоже) ограничениях должны в обязательном порядке индексироваться. InterBase вообще делает это автоматически, порождая под ключи индексы с именами RDB$PRIMARYnnn, RDB$UNIQUEnnn, а под внешние ключи (ссылки) - RDB$FOREIGNnnn. Здесь везде nnn - номер индекса в базе.

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

Ещё дополнительное замечание: оба используемых индекса должны давать в своих таблицах одинаковый порядок для сравниваемых значений. То есть индексы должны быть по соответствующим полям и если полей несколько, то они должны быть перечислены в одинаковом порядке. Например, если соединяются таблицы T1 и T2 по условию T1.x1=T2.y1 and T1.x2=T2.y2, то для быстрого соединения подойдут пары индексов T1(x1, x2), T2(y1, y2) или T1(x2, x1), T2(y2, y1), но не подойдёт T1(x2, x1), T2(y1, y2).

Согласно справке InterBase однопроходное соединение с помощью индексов называется MERGE. По-русски называют слиянием. Но об этом - чуть позже.

Ну а теперь представим, что индексов нет, а соединять надо. Полный перебор - плохо, это очевидно. В этом случае хороший результат может дать предварительное создание в базе отсортированных (точнее - кластеризованных) копий таблиц с последующим их слиянием. На первый взгляд решение парадоксальное, и даже страшное. Копировать и сортировать только ради одного запроса? Но если вдуматься - сортировка требует n*log(n) операций. Слияние же займет m+n. Порядок суммы этих величин не намного больше, чем m+n. Хотя постоянный коэффициент по сравнению с классическим слиянием будет вдвое-втрое выше. В InterBase это называется словами SORT MERGE.

Наконец, в InterBase есть и третий способ реализации соединения - JOIN. Только не путайте его со словом join, которое пишется в части from оператора select. Это немного другая тема. В данном случае имеется в виду слово join из части plan. Это и есть простой индексный перебор. То есть берутся записи первой таблицы "как есть" (может быть, отфильтрованные с учётом других условий запроса) и для каждой записи ищется пара из второй таблицы по индексу.

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


Детали планирования в Interbase

Начну с того, как можно посмотреть те планы запроса, которые строит InterBase автоматически. Для этого достаточно в Interactive SQL зайти в меню Options \ Basic Settings и включить опцию Display Query Plan. В текстовом isql эта штука называется 'set plan on;'. Полезно бывает так же включить Display Statistics и Display Record Count (set stats on и set count on).

Сам план строится, как особое выражение, исполняемое слева направо. Элементами этого выражения являются способы перебора конкретных таблиц, которых, вообще говоря, три, а объединяются они операциями соединения, которых тоже три. Способы перебора таблиц:

  • имя_таблицы natural
  • имя_таблицы index(имя_индекса, ...)
  • имя_таблицы order имя_индекса

Способы соединения:

  • join(обращение, обращение)
  • merge(обращение, обращение)
  • sort merge(обращение, обращение)

Обращения - это либо способы перебора элементарных таблиц, либо другие соединения, дающие на выходе таблицу для данного элемента плана. Выше я уже высказывал предположения насчёт того, как отрабатываются различные виды соединений. Но соберу всё это ещё раз в одном месте. И так:

Natural означает, что делается полный перебор записей таблицы. Для каждой записи проверяются условия, которые можно проверить сразу. Если таблица - первая в плане и если запись не отброшена, то для неё выбираются подходящие записи из следующей таблицы плана (по индексам или нет, зависит от того, как описана та таблица), и т. д. Обычно такой режим если и применяется, то только к одной таблице в плане. При нескольких таблицах без индексов выгоднее sort merge.

Index означает, что идёт обращение к индексам согласно данным из условия where и, если возможно, уже прочитанным записям предыдущих таблиц плана. На найденных записях проверяются условия запроса и принимается решение, подходят ли они к предыдущим записям соединения.

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

Order означает, что данная таблица будет перебрана вся, но в порядке, заданном указанным индексом. Имеются не совсем проверенные подозрения, что order может искать не с начала таблицы, а с заданного значения, по указанному индексу. Образуется обычно при употреблении в select конструкции order by, но может применяться и для осуществления Merge.

Join означает переборный метод соединения. Эффективным он может быть лишь тогда, когда в результате образуется небольшое количество подходящих друг другу записей. Об этом было сказано и ещё будет подробно обсуждаться. Тем не менее, это - любимый способ соединения, применяемый оптимизатором InterBase.

Merge - слияние, способ однопроходного соединения без физической сортировки. Обращения, служащие аргументами должны быть заранее отсортированными в нужном порядке, то есть где-то в их дебрях должен быть order. В некоторых случаях, когда упорядоченность записей возникает в результате тонких и хитрых эффектов, interbase может это не понять и отказаться выполнять непосредственное слияние, хотя теоретически оно и возможно. Этот способ быть эффективным на небольших и средних по объёму соединениях при наличии хороших индексов и отсутствии сильных посторонних условий в запросе.

Sort Merge - способ однопроходного соединения с предварительной сортировкой. Обращения, служащие аргументами, могут быть получены любым путём и не обязаны быть отсортированными. Эффективно для больших по объёму соединений, но требует создания временных копий соединяемых таблиц, которые часто не умещаются в памяти и поэтому сохраняются сервером во временный файл на диске. Подробности процесса описаны в разделе про сортировку.

Однако, как я говорил в самом начале, нельзя вечно полагаться на автоматическое планирование. К счастью, interbase даёт возможность полностью управлять всеми своими способностями. Синтаксис оператора select в InterBase позволяет в некоторых указывать вручную план запроса. Правда, практика показывает, что эффективные с точки зрения здравого смысла планы эта штуковина иногда отказывается воспринимать под разными предлогами, или без оных. Сама конструкция имеет вид:

select ... from ... where ... group by ... having ... 

plan выражение_для_плана order by ...

Здесь уже бросается в глаза, что такая вроде бы важная конструкция, как order by, вынесена "за скобки". На самом деле эксперименты показывают, что order by может использовать индексы, но только тогда, когда никакие индексы не нужны для условий. То есть допустим (для примера) что:

create table t(x integer, y integer, z integer);

В этом случае запрос "select * from t order by y" будет использовать индекс по t(y), а запрос "select * from t where z=77 order by y" - уже никак, если поиск использует индекс по t(z). По логике, эта задача решаема за один проход (то есть практически мгновенно), если создать индекс по t(z,y). В этом случае можно по индексу быстро выйти на диапазон записей, у которых z=77 и этот диапазон сразу же окажется перечислен в заданном порядке. Однако такой ход уже выше понимания InterBase. Вместо этого InterBase найдёт индекс, у которого самое старшее поле - z, по нему выберет нужные записи, отсортирует их физически (вхолостую), и только потом начнёт выдавать клиенту.

Ещё один неприятный фактор, который можно продемонстрировать уже даже на таком примитивном примере - это замедление работы в результате наличия дополнительных индексов. Да, именно так! Сделав несколько раз create index с одинаковыми определениями, но разными именами, можно существенно замедлить обращения. По крайней мере в interbase 4.X. И не только по обновлению, как может показаться на первый взгляд. Дело в том, что обнаружив несколько индексов, содержащих в старшей части нужные поля, InterBase бросается параллельно читать их все. В некоторых случаях это имеет смысл, но далеко не всегда. Так же нужно заметить, что более поздние версии interbase страдают этим делом всё меньше и меньше.

То есть если в вышеприведённом примере есть индексы t(y), t(y,z), t(y,x), то поиск может пойти в три раза медленнее, чем при наличии только t(y)! Что, правда, легко поправимо руками.
Далее, ещё один немаловажный вопрос: уж коль скоро серверу приходится так часто физически сортировать наборы записей, то что именно он сортирует - сами записи или ссылки на них (выдавая записи клиенту по ссылкам). Оказывается, что именно физические записи. Допустим, что:

create table t1(x integer not null primary key, y integer);
create table t2(z integer, x integer not null references t1(x));
create view v1(z, x, y) as select t2.z, t2.x, t1.y from t1, t2 where t1.x = t2.x;

И допустим, что мы пишем следующую пару запросов:

  1. select z from t2 order by z
  2. select z from v1 order by z

Если разобраться, то оба запроса полностью эквивалентны и для их обработки таблица t1 вообще не нужна. Тем не менее сортировка (предполагаем, что z не проиндексировано) в первом случае пройдёт гораздо быстрее. Дело в том, что в первом случае сортироваться будет только таблица t2, а во втором - соединение таблиц t1 и t2, которое, во-первых, больше по размеру, а во-вторых, его ведь ещё тоже надо получить. А это уже сама по себе "неслабая" задача.

Другой недостаток представлений InterBase состоит в том, что в них нельзя пользоваться такими конструкциями, как union и plan. С планом, в принципе, всё понятно, он реально формируется только при обращении к представлению и сильно зависит от контекста, в котором идёт обращение к этому представлению. Но вот запрет на объединение - это вообще-то гадость существенная. Первые шаги по её преодолению я видел лишь в interbase 6.0 Beta.

Пример плана для представления:

select *
from v1

plan join (v1 t1 index (rdb$primary1), v1 t2 index (rdb$foreign2))

order by x;

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

Далее в связи с представлениями я не могу не упомянуть такую штуку, как distinct. Ни в коем случае нельзя применять его внутри представлений, если хотите сохранить производительность. Вообще-то во многих практических случаях уникальность либо вытекает из ограничений уникальности, наложенных на хранимые таблицы, либо легко может быть получена проходом по индексам. InterBase, как обычно, не способен ни на то, ни на другое. Когда он встречает select distinct (или, что почти то же самое - select count(distinct ...)), он просто копит результирующие записи то ли в отсортированном, то ли в проиндексированном виде, избегая таким образом добавления дубликатов и по окончании запроса выдаёт пользователю то, что получилось в отсортированном же виде (если не задан другой явный порядок через order by, который в данном случае приведёт к пересортировке).

А теперь представьте, что запрос select distinct написан в представлении, а оно включено в другой запрос. Как поступит InterBase? Очень просто - вычислит подзапрос с distinct, а потом, трактуя его как таблицу, вычислит внешний запрос. Замечу, неиндексированную таблицу. И никакие планы тут не помогут. Дело в том, что distinct надёжно заизолирует подзапрос и сослаться извне на него станет уже невозможно, а изнутри его никак не задашь - представление всё-таки. В общем, у меня ни один из вариантов не прошёл. Реальный способ вычисления такого представления останется на выбор InterBase, а он известно, как это делает. Причём, даже если включить опцию Display Query Plan, то ISQL всё равно не сознается, какой применён план. К счастью, в большинстве случаев можно вынести distinct на уровень внешнего запроса и распланировать всё, как положено.

В общем, отсюда мораль: обращайтесь с представлениями осторожнее.

Что касается union, то в подзапросах, входящих в него, планы использоваться можно, но именно как в подзапросах. Указать, как осуществлять сам union, невозможно. Похоже, что у interbase есть только два способа для его осуществления. Просто union делается подобно distinct - сортировкой множества записей с удалением дубликатов, а union all - простым копированием двух результирующих множеств в одно место (хорошо, если напрямую клиенту).

Далее - будьте осторожны с агрегатными функциями (SUM(), AVG(), MIN(), COUNT(), ...). Дело в том, что ни для одной из этих функций InterBase 4.х не применяет индексы. Они всегда подсчитываются полным перебором всех выбранных записей. Разумеется, это не так худо, как перебор всех хранимых записей, но при больших размерах выборки время получается отвратительным. Особенно не рекомендуются такие функции во вложенных коррелированных подзапросах и в группировках с большим количеством групп. Хотя там они обычно и нужны. InterBase 5.х уже умеет искать по индексам MIN() и MAX(). Борланд заявляет, что, мол, "это была ошибка в 4.х и мы её исправили". Хотя теоретически по индексу можно вычислить все агрегатные функции SQL, причём быстрее, чем по хранимым таблицам. Правда, степень улучшения зависит от функции и состояния данных.

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

Далее - будьте осторожны с таким "удобствами" SQL, как between, containing, like и т. п. Почти все подобные словарные" конструкции (за исключением разве что starting with в отдельных случаях при точном сравнении) не используют индексы и решают свою задачу методом полного перебора. Даже between! То есть "select * from t1 where x >= 5 and x <= 500" пойдёт гораздо быстрее, чем "select * from t1 where x between 5 and 500". И если ISQL скажет, что в обоих случаях используется индекс - не верьте, посмотрите на статиситку обращений к диску.


Подзапросы

Во-первых, для хорошего планировщика очевиден тот факт, что многие подзапросы могут быть преобразованы в соединения, и наоборот. Это открывает возможности для применения всех описанных выше методов оптимизации соединений. Однако только не в interbase. Здесь порядок отрабоки можно регулировать планом лишь в пределах одной фразы from. Зато практически никаких ограничений на планы внутри подзапроса нет. На основе вышеприведённой структуры можно привести следующие эквивалентные примеры:

/* 1 */ select z from t2, t1 where t2.x = t1.x and t1.y = 33;
/* 2 */ select z from t2 where x = some (select x from t1 where y = 33);
/* 3 */ select z from t2 where x in (select x from t1 where y = 33);
/* 4 */ select z from t2 where 33 = (select y from t1 where t1.x = t2.x);
/* 5 */ select z from t2 where exists( select * from t1 where t1.x = t2.x and t1.y = 33);

За исключением разве что вариантов 2 и 3 все эти запросы - разные с точки зрения interbase. Хотя на самом деле все пять - совершенно эквивалентные с точки зрения реляционной алгебры. Это, к стати, один из недостатков SQL - слишком много возможностей, чтобы запутать простые вещи. Отсюда мораль - если есть возможность, избавьтесь от подзапросов.

Все подзапросы делятся на коррелированные и некоррелированные. Коррелированность означает, что подзапрос зависит от текущей записи охватывающего его запроса, хотя бы от одного поля. Это, в свою очередь, означает, что данный подзапрос нужно перевычислять для каждой записи охватывающего запроса. Ну, если быть более точным, то достаточно вычислять запрос для разных значений тех полей, от которых он зависит. Как точно поступает interbase я не знаю, но сильно сомневаюсь, что здесь есть какая-то оптимизация.

Некоррелированные запросы, наоборот, не зависят от охватывающего запроса. Они просто возвращают ему значение или набор значений. Значит, их нужно вычислять всего один раз. Это interbase понимает. В приведённых примерах подзапросы 2 и 3 являются некоррелированными, а подзапросы 4 и 5 - коррелированные.

Дальше пойдет сравнительно новый материал, который я накопал в послденее время. Основная суть в том, что все подзапросы, поведение которых мне приходилось детально исследовать (в версии 4.2), вели себя как коррелированные. То есть так, будто отрабатывались на каждую запись охватывающего запроса. На первый взгляд ужасно, но ...

Оказалось, что interbase умеет оптимизировать планы вложенных запросов с учётом охватывающего их контекста. Если вернуться к серии вышеприведённых примеров и рассмотреть запросы 2 или 3, то в них interbase для отработки подзапроса может использовать индексы t1(x) и t1(y). Или даже оба индекса сразу, так как уже было сказано, что поддерживается объединение и пересечение индексов через or или and.

Ну ладно, с y всё понятно - это поле фигурирует в where. А вот x - нет. И если подзапрос попытаться выполнить отдельно, то план с индексом по x так же не будет воспринят. Однако мы имеем дело именно с подзапросом. Который вызывается из охватывающего для поиска соответствия как раз по этому полю x. Причём подобные фокусы порой проходят и на довольно навороченных подзапросах с соединениями.

Однако же тоже не всегда. Если Вы напишете подзапрос, скажем, с having, который практически не поддаётся такому планированию "снаружи внутрь", то остаётся Вам только посочувствовать.

 


Сортировки

И так, индексы - индексами, но рано или поздно (обычно - раньше, чем кажется) interbase вынужден что-то сортитьвать "вручную", то есть трактуя записи, как изначально неупорядоченные. Для этого он начинает выделять, пока возможно, память. Но памяти рано или поздно не хватает. И тогда создаются временные файлы в каталоге \TEMP или /tmp. Факт появления этих файлов, их размер и характер роста наряду с планами так же может много сказать о "тяжести" запроса.

Обычно, при наличии существенных объёмов оперативной памяти, interbase удаётся всё отсортировать буквально за один проход. Судя по всему (и в соответствии с тем, что известно из литературы) он ведёт себя так: по мере поступления (вычитывания или вычисления) записей они сортируются в памяти. Когда память, отведённая под запрос, исчерпывается, отсортированное множество сбрасывается во временный файл, после чего память вновь начинает заполняться сначала. Если повезёт, то записи кончатся до заполнения памяти и временный файл не потребуется.

Когда файл сформирован, он состоит из нескольких отсортированных участков, позиции которых заблаговременно сохранены, видимо в памяти. interbase начинает вычитывать отсортированные участки и выполнять в памяти слияние, похожее на слияние таблиц. Это тем более легко, что отсортированность участков уже обеспечена. Как правило, участков достаточно мало и слияние удаётся сделать за один проход по временному файлу. По крайней мере мне не встречались случаи, чтобы на одну сортировку создавалось несколько файлов. Не заметить перегенерацию стомегабайтного файла я не мог.

После того, как результаты сортировки использованы (отправлены клиенту, в процедуру или другой запрос), временный файл уничтожается. Отсюда следует, что во-первых, во временном каталоге должно хватить места для всех сортировок, которые могут понадобиться для обслуживания приложений, а во-вторых, если дисковые сортировки происходят часто, то нужно обеспечить быстрый доступ к временному каталогу. Лучше всего, если это будет отдельный от основной базы диск.

Откуда берутся сортировки? Во-первых, конечно же, из order by. В ряде случаев это упорядочение можно выполнить по индексу, но часто оказывается, что лучше по другим индексам отфильтровать ненужные записи, а потом уж "вручную" отсортировать оставшееся множество. Кроме того, interbase не поддерживает индексирование по вычисляемым полям, так что сортировать по выражению можно только одним способом.

Во-вторых, group by. На самом деле это то же самое упорядочение, просто с чуть-чуть ослабленными условиями. В частности, для такого упорядочения тоже можно воспользоваться индексом в плане (как обычно, если это не конфликтует с условиями соединения, фильтрации, и того же упорядочения).

Вот что действительно проблематично заставить работать по индексам, так это having. К счастью, эти условия обычно обрабатывают единичные записи, образовавшиеся после группировки. То есть тормоза образуются главным образом в group by, а не в having. Если, конечно, не написать кривой подзапрос.

В-третьих, источник сортировки - sort merge. Не самый эффективный, но часто удовлетворительно эффективный способ соединения. Пара таблиц (возможно, образовавшихся в результате других частей запроса) сортируется по полям, на котоыре наложено условие соединения. После чего по результатам сортировки делается проход, во время которого собранные данные отправляются клиенту или (в совсем тяжёлом случае) на вход другогозапроса, после чего временные файлы уничтожаются. Мне приходилось видеть план запроса, сгенерированный interbase, в котором одна таблица фактически сортировалась трижды: два раза - чтобы соединить её с другими таблицами (сначала с одной, затем по другому полю - с другой), а в третий - по полям в order by. Разумеется, это был повод для ручной оптимизации.

В-четвёртых, сортировка делается при создании индексов. То есть create index по объёмистому множеству полей тоже может породить временный файл. Это касается любого, прямого или косвенного их создания. В частности, индексы создаются на заключительной стадии восстановления БД из бэкапа, при создании ссылочных и ключевых ограничений целостности. Последние, если их создавать вместе с таблицей, временных файлов не потребуют, так как таблица пока пуста и сортировать ещё нечего.

В-шестых, похоже, что через сортировку в interbase делается distinct.


Как получаются планы

Чтобы лучше понять, как лучше оптимизировать работу interbase, крайне желаетльно понимать, как именно он генерирует планы автоматически, то есть в тех случаях, когда в пользовательском запросе план явно не указан. Во-первых, планирование проводится отдельно для каждого подзапроса любого рода. Сюда входят, в частности, запросы внутри процедур, представления с distinct, части union и тому подобные вещи. То есть interbase делит при необходимости выражение на куски select и оптимизирует каждый из них независимо. Кроме, разве что, "хороших" представлений. Планы вложенных запросов просто вызываются при отработке охватывающих планов столько раз, сколько необходимо.

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

    1. Приведение условия where к нормальной форме. Оно трансформируется с целью поделить его на максимально возможное количество условий, связанных через and, которые, разумеется, сами должны стать как можно проще.
    2. Распределение условий. Например, если в результате предыдущей нормализации получилось a=b and ... b=c, то добавляется дополнительное условие a=c. Формулировку условия по законам логики это не нарушит, но может выявить новые, более удобные связи в запросе. Скажем, если a, b, и c - поля из разных таблиц, то может оказаться выгоднее соединить сначала a и с, что не следует напрямую из первоначальной формулировки.

Сказанное в некоторой степени распространяется и на неравенства, но здесь возможностей обычно меньше.

Между прочим, совсем умные (не interbase) планировщики сюда же добавляют ограничения целостности (check(), следствия unique, ...). Условия получаются ещё жёстче, а значит фильтрация - эффективнее. InterBase, конечно, глуп, но ситуация вполне моделируема руками - не бойтесь подсказать ему очевидное, дописав ещё условия.

    1. Формирование потоков. Полученный набор условий сопоставляется с индексами на полях и выявляются так называемые "потоки" (не путать с потоками в распараллеливании вычислений). То есть поток - это таблица, которую можно перебрать по индексу и это будет соответствовать одному из элементов условия.
    2. Формирование "рек". Уж любят эти буржуи поэтические названия. В данном случае река - это комбинация потоков с предыдущего шага, связанных непосредственно связанных условием или набором условий. То есть река - это то, что можно реализовать через процедуру слияния, merge. Если есть больше двух потоков, отсортированных по одним и тем же (исходя из равенств) полям, то они объединяются в одну реку.
    3. Выбор самой широкой реки. Вот здесь у авторов то ли interbase, то ли его документации плоховато. Потому что они обозвали этот этап выбором "самой дллинной реки". Хотя из текста (и из здравого смысла) следует, что выбирать надо именно самую широкую, то есть содержащую максимальное количество потоков, и следовательно, реализующую за раз максимальное количество соединений. А вовсе не самую длинную по количеству записей - это не цель для повышения производительности, скорее наоборот.

В общем, выбирается самая широкая река и исключается из набора доступных потоков. После чего из оставшихся вновь выбирается самая широкая, и т. д. До тех пор, пока не будут исчерпаны все потоки (таблицы с условиями).

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

  1. Объединение выбранных рек. Независимо от того, насколько это тяжело, выбранные реки объединяются. То есть делается либо sort merge, либо join, в зависимости от того, что доступнее и удобнее. Вообще-то если бы interbase был немного по-умнее, то он бы учитывал, что "идеальные" реки на предыдущем шаге могут осложнить ситуацию на этом, и наоборот. Но interbase не умеет возвращаться назад, то есть если он принял решения по поводу потоков, а затем рек, то он перейдёт к этому этапу и будет мучиться с тем, что есть.

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

  • Он приводит к физически выполнимым планам
  • Планы с большой вероятностью будут достаточно эффективными
  • Существует сравнительно небольшая вероятность зайти "в тупик". То есть план будет получен, но он будет неэффективным.
  • Решения принимаются на основе статистики, которая не всегда соответствует действительности, что делает процесс более случайным и увеличивает вероятность неправильного выбора.
  • Исходя из предыдущего замечания - существует вероятность, что по ходу эксплуатации базы эффективные решения изменятся на неэффективные, что приведёт к неожиданному и резкому снижению производительности. Хотя возможен и обратный процесс. И то, и другое практически невозможно реально оттестировать.
  • Имеются сообщения о глюках в оптимизаторах 4.Х и 5.Х. В частности, они проявляются в том, что в некоторых случаях запросы на одних и тех же данных с одной и той же статистикой, отличающиеся порядком перечисления таблиц и даже полей в select, давали разные планы. Что дополнительно свидетельствует о случайности выбора, делаемого автоматическим планировщиком.
  • Вся вышеказанная оптимизация в interbase 4.X касается в основном обычного соединения таблиц путём перечисления их во from через запятую. Что же касается запросов с конструкциями типа t1 xxx join t2, то они обычно принудительно рассматриваются, как отдельная "река" и отрабатываются практически, как записаны. 5.Х стал в этом отношении чуть-чуть умнее, но лишь чуть-чуть.

В общем, вывод один: хочешь иметь гарантированную производительность - пиши план руками. Благо такая возможность имеется. На первый взгляд может показаться, что это громадный недостаток interbase, но поработав с ним по-дольше, я склоняюсь к мысли, что это не совсем так. Разумеется, желательно, чтобы СУБД была по-умнее. Это снимет необходимость ручной оптимизации в простых случаях, то есть для большинства операций.

Однако идеального планировщика не бывает. Известно, что даже самые умные оптимизаторы в Oracle и DB2 не всегда поступают правильно. И решения их тоже носят вероятностный характер, поскольку такова статистика параметров БД. Просто вероятность правильных решений в них гораздо выше.

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

По-этому все критичные для приложения операции луше как минимум контролировать, каким образом они делаются. Если окажется, что выбранный оптимизатором план достаточно эффективен, то лучше вписать этот же план в запрос руками, чтобы не дать серверу возможность "неожиданно передумать" в будущем. Если же план поддаётся улучшению, то тем более требуется ручное вмешательство. Только так можно гарантировать отсутствие тормозов. Иначе не спасут никакие CASE'ы, архитектуры, компоненты, и т. д. В конце концов, идеальная оптимизация основана на знании семантики запроса, которая полностью известна только разработчику (а иначе он не разработчик, а ... молчу кто).

И здесь я должен сказать спасибо авторам interbase. Дело в том, что в большинстве других СУБД имеются достаточно слабые механизмы для ручного управления планированием. В большинстве случаев лучшее, что можно написать - один индекс для каждой таблицы. Ни в каком порядке их нужно соединять, ни по какой технологии, уже не напишешь. Причём такова ситуация в тех продуктах, которые счиюатся гораздо более "мощными", чем interbase. Может быть они и "мощны", но только до определённого предела, который определяется интеллектом встроенного оптимизатора. Ценность interbase как раз и состоит в возможности свободно работать далеко за этим пределом.


Как написать запрос с планом

Во-первых, прочитать и усвоить хотя бы данный документ полностью. Особенно то, что написано выше. Лучше, конечно, почитать и более серьёзной литературы.

Во-вторых, нужно усвоить важный момент - в данном случае мы пишем именно запрос и именно с планом. То есть не план к существующему запросу. И не такой запрос, чтобы interbase сам сообразил к нему по возможности эффективный план. Обе эти дополнительные задачи тоже решаемы в той или иной мере. Но мы договариваемся пойти по наиболее эффективному пути. То есть план должен максимально соответствовать запросу, а запрос - необходимости дальнейшено планирования. Иногда и структуры данных стоит подогнать под потребности запроса.

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

В качестве отправной точки лучше начать с написания в ISQL запроса без плана, не обращая внимание на эффективность. То есть прежде, чем решать, как именно должен отрабатываться запрос, нужно определиться с тем, что он должен делать. И определиться достаточно чётко, чтобы довести замысел до выражения SQL, делающего то, что требуется.

В некоторых случаях, когда используются компоненты, генерирующие запросы (типа TTable в Delphi) запрос нужно отловить с помощью средств мониторинга (SQL Monitor).

Далее нужно это самое выражение прогнать в Interactive SQL со включёнными показами планов, статистики и числа записей. Иногда, если запрос должен возвращать много записей бывает можно заменить "select поля" на "select count(*)" или "select distinct поле" на "select count(distinct поле)", чтобы избавиться от вывода самих данных и увидеть только характеристики запроса. Дело в том, что count в interbase работает методом перебора (по крайней мере в известных мне версиях), так что на планирование такая замена существенно не повлияет. Даже order by сохраняется внутри count(), как бы бессмысленно это ни выглядело.

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

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

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

Здесь, правда, есть множество возможностей сделать исключение из правила. Дело в том, что если через фильтрующие выражения проходит одна запись из 1000, то вторая стратегия может привести к перебору 1000 хранимых записей на каждую выданную. Это без учёта соединений и прочих спецэффектов. В таких случаях бывает лучше плюнуть на то, что сортировка расходует время и провернуть запрос по первой схеме.

Далее: если уж дело доходит до того, что нужно сортировать большой объём данных, то бывает полезным отсортировать не целиком записи, а только их ключи. То есть если запись целиком занимает 500 байт, а ключ+поле_сортировки занимают 50, то имеет смысл отсортировать только последних, взять, сколько надо записей из полученного списка, и лишь потом по ключам выбрать из базы их полные даные. Это позволит резко сократить объём сортируемых данных и, как следствие:

  1. Повысит вероятность того, что interbase сможет отсортировать всё в памяти при первом же заходе.
  2. Если памяти всё же не хватит, радикально сократит объём дискового ввода-вывода и потребность во временном дисковом пространстве.

В случае применения такой сортировки ключей запрос может распадаться на два: извлекающий ключи и извлекающий полную запись по ключу. Собственно, именно на этом в основном основано ускорение при работе с моим компонентом TDataMonster. Хотя на момент написания этого документа он реализует далеко не все приёмы, кототорые я здесь излагаю. В частности, большой недостаток - частая необходимость иметь представление для реализации второй стадии выборки, что, как выяснилось в последнее время ограничивает свободу действий.

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

  • Основные таблицы. Они обычно содержат главные данные, необходимые в запросе, а так же ссылки (на справочники, подчинённые или родительские таблицы). Как правило, имеет смысл начинать план запроса именно со сканирования этих таблиц. Они же обычно бывают самыми большими. Конечная сортировка order by так же обычно делается по полям основных таблиц.
  • Присоединяемые таблицы. Обычно это справочники, но могуть быть подчинённые или родительские таблицы, поля которых необходимо выводить или применять в вычислениях вместе с полями основных. Присоединение данных таблиц обычно не связано с фильтрацией исходного множества. В частности по-этому справочники довольно часто подсоединяются через outer join.
  • Фильтрующие таблицы. Их иногда включают в запрос, чтобы отфильтровать его по наличию фильтрующих записей.

Присоединяемые таблицы обычно либо отходят на вторую фазу (выборка полных записей), либо в конец плана, если всё делается одним запросом. Основная цель - выполнить эффективно соединение основных и фильтрующих таблиц. Как правило, анализ начинается с того, насколько полно выбираются основные таблицы.

Если они выбираются целиком или почти целиком (исключаются лишь редкие записи), то следует интерактивную выборку следует основывать на индексном переборе той из основных таблиц, по полю которой идёт сортировка. При этом для грида придётся создать два индекса - в прямом и обратном направлении. То есть план будет начинаться примерно так: plan join (ГлавнаяТаблица order i_ГлавнаяТаблицаСортировка, Присоединяемая index (rdb$primary666), ...). То есть перебираем главную таблицу в заданном порядке, после чего к найденной записи подцепляем остальные по индексам. Индекс должен быть возрастающим или убывающим в зависимости от направления выборки (это обеспечивается в Монстре).

Ещё одно замечание: в процессе эволюции различных экземпляров базы системные индексы могут получать различные номера. Так что писать эти номера в запросах приложения достаточно опасно - после первого же перебэкапа запрос может отказать. Лучше воспользоваться специальной функцией, которая извлекает индекс по ключевым полям (в проекте "Архив" такая функция имеется в dmShareDatabase).

Если же главные таблицы сильно фильтруются, то есть от таблицы остаётся сравнительно мало (в процентном отношении) записей, то стратегия будет другая. Здесь однако тоже есть одно исключение - если из главной таблицы берётся непрерывный диапазон записей по тому же полю, что и сортировка. В таком случае следует работать всё-таки применить индексный перебор, так как такая фильтрация его обычно не нарушает. В остальных случаях следует отталкиваться именно от условий фильтрации или/и соединения главных таблиц и, может быть, фильтрующих таблиц.

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

При проектировании такого соединения, если одна таблица фильтруется сразу по нескольким условиям, нужно стремиться создвавать индексы, охватывающие сразу несколько полей для нескольких условий. Правда, на все условия индексов не напасёшься, да и при их избытке появляются неприятные побочные эффекты. Но во многих случаях это и не требуется. В частности, нужно учитывать, что индекс может быть использован для поиска по части его полей, при условии, что эти поля - старшие в индексе. Так что выбирая порядок, нужно думать и о других запросах тоже.

Кроме этого, существует такой полезный метод соединения таблиц, как слияние (merge), который тоже организуется по индексам. Об этом писалось в разделе о соединениях. К сожалению, это редко удаётся на практике, так как interbase соглашается выполнять слияние только тогда, когда соответствующие таблицы и индексы не задействованы в фильтрации или других соединениях. Иными словами, сначала слияния, потом всё остальное. Кроме того, у interbase часто не хватает ума, чтобы осознать тот факт, что результаты некоторых фильтраций тоже могут быть отсортированы. Поясню примером:

create table t1(x integer, y integer);

create index i_t1_x_y on t1(x, y);
create table t2(z integer);

create index i_t2_z on t2(z);
select ... from t1, t2 where t1.x = 333 and t1.y = t2.z

Очевидно, что если в последнем запросе отработать сначала фильтр по t1.x, по индексу i_t1_x_y, то полученный остаток от t1 будет упорядочен по y вследствие природы упомянутого индекса. А значит, его можно безо всяких сортировок слить с таблицей t2 по индексу i_t2_z. Это можно было бы выразить планом plan merge (t1 index (i_t1_x_y), t2 index (i_t2_x)). Но, как я сказал, не судьба. Похожий план пройдёт, если вместо merge написать join. Это будет означать, что первое условие отработает по старшей части индекса, причём информация о сортировке будет потеряна. Потом к каждой оставшейся записи t1 будут подсоединены записи t2 и здесь уже проблем с выбором индекса не будет. Вроде бы похожий процесс, но по каждой найденной записи t1 делается отдельный независимый проход по t2, от корня индекса и до самих записей. Очевидно, что диск будет дёргаться больше, да и вычислений отнюдь не уменьшится.


Ещё хуже ситуация, если к указанному запросу приписать order by t1.x. Очевидно, что этот порядок следует из индекса i_t1_x_y. И ни при первом, ни даже при втором плане делать ничего больше не нужно - просто выдать записи клиенту. Но interbase, опять же это понять не в состоянии. Он будет пытаться сортировать полученный результат и если памяти не хватит, то создаст временный файл. И до самого завершения "сортировки" клиент ни одной записи не увидит.

Сразу же получить нужную сортировку можно планом plan join (t1 order i_t1_x_y, t2 index (i_t2_x)). То есть таблица t1 сразу будет выбрана в нужном порядке, но t2 к ней будет подсоединяться не слиянием, как и в предыдущем плане. Это, правда, не критично, если подходящих записей в t1 мало или если клиент выбирает лишь первые из них (скажем, для грида).

Теперь о присоединяемых таблицах. При двухэтапном подходе (выбор ключей, а затем полных записей) они однозначно уходят на второй этап. Здесь вполне допустим обычный индексный поиск ( join (главная таблица ..., присоединённая index (rdb$primarynnn)). Если это справочник и он небольшого размера, то проблем не будет при любой форме запроса. Как правило, бывает именно так. Но иногда могут возникать проблемы и здесь придётся обрабатывать такую таблицу примерно теми же методами, что и основную, как описано выше.

Большие неприятности создаются внешними соединениями. Если написать from t1 left outer join t2 on y = z, то оптимизатор interbase по своей глупой природе имеет маниакальную привычку: просканировать сначала t1 natural, t2 index (i_t2_z), и лишь потом поверх этого отработать остальные условия в процессе перебора. Практика показывает, что подсунуть что-либо существенно иное практически невозможно. Если кто-нибудь раскопает методику, как заставить его делать хотя бы слияние, буду крайне благодарен. Едиснтвенное, что можно бывает сделать, это "пропихнуть" часть условий в сканирование "внешней" таблицы, в данном случае - t1. То есть указать руками, что фильтроваться через t1 index (...). А уж если после этого нужно упорядочение, то сортировки записей избежать практически невозможно.

Представлений следует по возможности избегать. Как ни печально это звучит, но это так. Особенно если в них есть distinct или внешние соединения. В таких случаях interbase будет упираться всеми силами, чтобы не взять нормальные планы, причём не давая вразумительных объяснений. Что именно не пройдёт можно надёжно выяснить только экспериментом в Interactive SQL. Если же представление всё же приходится использовать, то нужно ссылаться на входящие в него таблицы по их псевдонимам. Пример приведён выше.

Подзапросам уже был посвящён отдельный раздел. В большинстве случаев оптимальных результатов можно достигнуть, если преобразовать подзапрос в соединение. Для начинающих: это возможно, гораздо чаще, чем кажется. Просмотрите внимательно, что именно выбирает Ваш подзапрос и как его данные соединяются с данными родительского запроса. Посмотрите примеры в вышеприведённом разделе, может быть одно из приведённых там преобразований подойдёт и в вашем случае. Если же ситуация всё-таки не позволяет, то во-первых, проследите чтобы подзапрос сам по себе был оптимизирован, а во-вторых, чтобы взаимодействовал с родительским запросом наилучшим образом.

И в заключение: если не видите эффективного решения, не бойтесь применять sort merge. Эта, казалось бы, страшная операция (как же, обе таблицы пересортировать, или сколько их там не соединилось), иногда оказывается самой быстрой! Например, если нужно соединить две большие таблицы, а индексов по нужным полям нет. Если придётся сортировать 100 тыс. записей для показа в гриде всего 10 из них, то это, конечно, плохо. Но если это же нужно для выборки 20 тыс., то не так уж.

Правила для повышения эффективности здесь примерно те же, что и для любых сортировок - максимально отфильтровать множества, сократить размеры записей до минимума, а остальное присоединить потом. То есть желательно, чтобы в sort merge участвовали только поля, по которым идёт соединение и ключи. И только потом к результатам слияния подсоединить справочники, или что там ещё понадобится. Ведь 20 тыс. записей, скажем, по 50 байт - это всего 1 МБ. Не так уж много для современного сервера.


Когда план написать нельзя

Нет добра без худа. Особенно в interbase. То если есть возможность писать планы, то есть места, где это не получится. Таких мест, а точнее, видов мест, по большому счёту я могу указать три (может быть, я что-то упустил):

  1. Хранимые процедуры. Это ограничение стало менее актуальным в 5.x, но тем не менее. Во-первых в interbase до 5.x Вы просто не сможете перебэкапить базу, если сошлётесь на индекс из процедуры. Хотя в остальном всё будет работать нормально. Однако с учётом глюков, работа с такой базой ничем хорошим не кончится. Пятый interbase перебэкапливает такую базу нормально, но надёжно сослаться на системный индекс по-прежнему невозможно. То есть после перемены его номера процедура откажет. В крайнем случае придётся создать дублирующий. Сгенерировать динамический запрос внутри процедуры так же нереально.
  2. "Блокирующие" планирование представления с distinct или прочими неприятностями.
  3. Использование на клиенте компонентов, которые не позволяют рулить планами. Классический (и клинический) пример TTable в Delphi или DbiOpenTable() в BDE, что вообще-то одно и то же. О чём тоже есть отдельный раздел.

Самое неприятное в этих ситуациях то, что они не могут дать гарантированной скорости, в отличие от планов. Но можно повысить вероятность хорошей работы. Во-первых, надо всеми средствами отловить непосредственно сами запросы. Далее нужно исследовать их в ISQL и сопоставить с повадками планировщика interbase. После чего попытаться переформулировать запросы в процедурах или настройки компонентов так, чтобы по возможности ограничить инициативу interbase "хорошими" вариантами.

В процедурах бывает полезно расщепить запрос на несколько вложенных циклов for select. Каждый цикл должен быть как можно проще и его условия должны максимально соответствовать индексам. Или наоборот, создать индексы под условия. В идеале каждый запрос должен образовывать одну "реку", охватываемую одним индексом. При этом запросы должны как можно жёстчё фильтроваться, чтобы минимизировать количество итераций.

В клиентских компонентах следует по возможности ссылаться не на представления, а на хранимые таблицы. Если есть возможность изменять структуру таблиц, то лучше сделать так, чтобы поля, не участвующие в наиболее важных запросах, хранились в отдельной таблице. Подсоединяемые таблицы лучше всего трансформировать в вычисляемые поля, вычитываемые по текущей записи заранее подготовленным запросом (в проекте "Архив" - dmShareDatabase.QLookup()).


Пароли и права доступа

Данная тема состоит из двух основных вопросов: как interbase узнаёт, с кем имеет дело (с каким пользователем) и как принимаются решения относительно того, давать пользователю возможность выполнить операцию, или нет.

Первый вопрос выясняется в момент подключения к БД. Во-первых, все средства, предусматривающие подключение, начиная от функций interbase API и кончая интерактивными утилитами предоставляют возможность явно задать имя и пароль. И если значения указаны, то они имеют приоритет надо всеми остальными умолчаниями. Если же нет, то пробуются два других источника - переменные окружения ISC_* и пользователи Unix, если дело происходит под соответствующей системой.

Из переменных окружения нас в данном случае интересуют две: ISC_USER=имя_пользователя и ISC_PASSWORD=пароль. Эти переменные одинаково воспринимаются подо всеми операционками, которые поддерживает InterBase. Кроме того, они одинаково воспринимаются как утилитами командной строки, так и интерактивными программами, потому что на клиенте их проверяет gds32.dll (linterbasegds.so).

Наконец, их так же проверяет и сервер. По этой причине запускать сервер с установленными значениями - значит дать по умолчанию всем доступ под заданным именем. Клиенту достаточно будет ничего не указать, и сервер возьмёт значения переменных. Это, разумеется, бывает удобно при отладке, когда требуются частые подключения, но черезвычайно опасно при реальной эксплуатации. Нужно помнить об этой возможности.

Несмотря на то, что сегодня interbase применяется в основном под Windows, по своей природе и истории это в основном юниксовый продукт. Соответственно его система управления правами в большей степени оринетирована на Юниксы. В частности, interbase умеет доверять юниксовой системе, принимая соединения от неё пользователей без проверки пароля. Для этого достаточно, чтобы имя пользователя (без учёта регистра) совпало с именем пользователя, зарегистрированного в системе и чтобы этот пользователь подключался из системы, которой доверяет сервер.

Доверие между системами устанавливается традиционым для Юниксов способом. Во-первых, каждая система доверяет сама себе. То есть подвключения в пределах одного компьютера пройдут без проблем. Внешние подключения должны исходить с клиентов, перечисленных в файле /etc/hosts.eqiv на сервере. Или в файле /etc/gds_hosts.equiv. Первый - общесистемный, его воспринимают все сервисы. Второй Борландовцы придумали под себя. Форматы одинаковы и описаны в юниксовой документации.

Ещё одно условие состоит в том, что на доверяемом клиенте должен работать сервис auth, способный подтвердить: да, это соединение в моей системе исходит действительно от процесса, принадлежащего указанному пользователю. Поскольку такой сервис обитает исключительно в Юниксах, все упомянутые механизмы доверия системе работают только между Юниксами. Если же клиент или сервер - не Юникс, то тогда ничего не остаётся, кроме как явно указывать имя и пароль другими способами. С другой стороны, в Юниксе тоже можно явно указать имя с паролем, чтобы подключиться под правами пользователя, отличного от текущего в системе.

И так, сервер выудил через параметры, переменные и доверия имя и пароль пользователя. Как же он узнает, допустимы ли они для него, или нет? Для этого существует специальная БД, которая обычно лежит непосредственно в установочном каталоге interbase и как правило, называется, isc4.gdb. Именно туда заносятся все пользователи, регистрируемые через gsec или Server Manager.

В базе всего две таблицы.

HOST_INFO - дополнительная информация об узлах сети. То есть о доверяемых системах, как описано выше. Полей всего два:

HOST_NAME - имя узла. Имеется в виду hostname из TCP/IP. Обратите внимание: без домена! По какой-то странной причине практически все утилиты InterBase не воспринимают имена с доменами. То есть нельзя написать gw.krista.ru, можно только просто gw. Соответственно клиент и сервер должны быть в одном домене.
HOST_KEY - какой-то ключ для данного узла. По всей видимости, предусмотрен какой-то дополнительный механизм для проверки возможности доверия через ключи. Только вот где указать этот ключ на клиенте, я в документации откопать так и не смог.

USERS - информация о самих пользователях.

USER_NAME - имя пользователя в InterBase.
SYS_USER_NAME - хотя по умолчанию имена пользователя в системе и в InterBase должны совпадать, вероятно предусмотрена (не задокументированная сейчас) возможность задавать произвольное соответствие. Это одна из гипотез по поводу существования этого следующего поля. Другая: пользователь, входящий в interbase под указанным именем обязан быть в системе тем-то и принадлежать к группе такой-то, если они указаны. Что из этого правда и работает ли вообще хоть что-то я не проверял.
GROUP_NAME - имя группы. См. замечание к предыдущему пункту.
UID - Численное значение идентификатора пользователя согласно Юниксу. В других системах игнорируется. Как я понял, именно под этим идентификатором в системе запускается процесс (или может где-то - поток), обслуживающий текущего пользователя. При условии, что головной процесс работает под правами root. Данный идентификатор самым непосредственным образом влияет на права доступа к файлам БД.
GID - аналогично предыдущему полю идентификатор группы.
PASSWD - пароль в зашифрованном виде. Шифруются и хранятся только первые 8 символов (хотя вводить можно и больше) согласно классическому юниксовому алгоритму. То есть можно взять какую-нибудь шифровку пароля из /etc/shadow и записать её сюда. И пользователь interbase приобретёт пароль взятый из Юникса.
PRIVILEGE - по всей видимости, ненулевое значение должно указывать на то, что пользователь привилегированный. Прикол в том, что в записи для SYSDBA там обычно null, как и у обычных пользователей.
COMMENT - какой-то комментарий к пользователю. Тоже обычно null.
FIRST_NAME - Имя.
MIDDLE_NAME - Отчество.
LAST_NAME - Фамилия.
FULL_NAME - поле, вычисляемое из FIRST_NAME, MIDDLE_NAME, LAST_NAME.

Итак, как мы видели, имеется предостаточно информации о том, под какими правами должен существовать в системе процесс или поток, работающий на пользователя. Эта информация актуальна для Unix, а так же для Windows NT при работе через протокол NetBEUI. Во всех остальных случаях все процессы InterBase работают под теми правами, под которыми их запустили.

Поведение под Unix описано в разделе Установка под Linux. Оно радикальным образом зависит от того, обезопасились ли Вы при установке, отобрав у interbase права root, или нет. Если да, то несмотря на все настройки, процессы interbase будут работать из-под одного и того же UID. Все файлы БД должны быть доступны ему для записи.

Если же interbase запускается, как root, то всё зависит от пользователя. В первую очередь interbase смотрит, не заданы ли для пользователя конкретные UID и GID. Если да, то процесс переключается на них. Иначе в системе ищется пользователь с тем же именем (ещё раз повторюсь: без учёта регистра). Если найден, то производится переключение на него. А вот если не найден, то процесс остаётся нормально работать под правами root! То есть любое наличие пользователя в InterBase, не прописанного в системе означает дыру в безопасности всей системы. В сочетании с возможностью писать UDF не составляет труда запустить привилегированный shell и получить полную власть.

Таким образом, установка под Unix черезвычайно опасна и требует либо очень внимательного сопровождения, либо переключения на непривилегированный UID. В заключение отмечу, что interbase делает особое исключение для SYSDBA и root. Подключения под SYSDBA порождают в системе процесс с правами root (если есть возможность), а подключения под системным пользователем root на уровне БД считаются эквивалентыми SYSDBA.

Что же касается NT, то там похожие эффекты возникают в NetBEUI. Этот протокол, а точнее, способ его использования Энтями, позволяет соединениям в сервере наследовать права того клиента, который соединение создал. Клиент же при установлении соединения предъявляет те права, под которыми пользователь зашёл в клиентскую систему. Это означает, что сервер сможет открыть БД от имени данного клиента тогда, и только тогда, когда данный клиент имеет права на доступ к файлу БД.

Самый большой прикол состоит в том, что пользователь interbase, в отличие от случая с Юниксом, здесь вообще не причём! Клиентский пользователь Windows предъявляется серверу Windows. Правда, проверка файловых прав делается после того, как пользователь прошёл обычную проверку InterBase. Если клиент вошёл в домен NT, то соответственно его подключение трактуется, примерно как попытка открыть файл БД по сети. Если же клиент в домен не входил, то с точки зрения сервера он рассматривается, как посторонний пользователь. То есть в этом случае база должна быть Read/Write для всех.

Отсюда мораль: если не хотите запутаться - пользуйтесь TCP/IP. Он учитывает только то, что прописано в InterBase, и ничего больше.

Итак, interbase идентифицировал пользователя, убедился, что это точно он, породил ему процесс (или поток) с нужными правами, и процесс успешно открыл файл БД. Всё, можно работать. Рассмотрим теперь, как определяются права пользователей на отдельные объекты БД. Операционная система здесь уже полностью не причём.


Сначала опишу базовый механизм, а потом отдельно отмечу, как он расширяется в версиях 5.Х с помощью ролей. Чтобы долго не мучиться с терминологией, назовём отдельные элементы прав грантами. Грант - то, что порождается оператором grant и уничтожается оператором revoke. Так же при создании объектов БД автоматически порождаются гранты создателю. Этим дело не ограничивается - для большинства объектов запоминается их владелец.

При каждом обращении пользователя к объекту БД проверяется наличие соответствующих грантов. Или, если делается операция по уничтожению объекта или корректировке метаданных, то проверяется соответствие текущего пользователя владельцу. Единственное исключение - пользователь SYSDBA, для него все эти проверки опускаются.

Гранты хранятся в БД в системной таблице RDB$USER_PRIVILEGES. Вытекающие из них права хранятся в RDB$SECURITY_CLASSES. Хотя теоретически и то, и другое можно править руками, лучше этого не делать, так как там достаточно "тёмных мест", нигде не задокументированных.

Каждый грант характеризуется:

  • Пользователем, которому даются право. Если в качестве такого пользователя указано PUBLIC, значит право даётся всем. Кроме этого можно указать:
    • View имя_представления
    • Procedure имя_процедуры
    • Trigger имя_триггера
  • В таком случае объект БД получит право на операцию независимо от того, какой пользователь её инициировал. В самых последних версиях мне попадалось упоминание о том, что можно давать грант группе пользователей Unix.
  • Пользоватеелм, который даёт право. interbase согласно стандарту SQL отслеживает всю цепочку передачи прав от владельца объекта или администратора. Если кто-то лишается гранта, то автоматически уничтожаются все последующие гранты в этой цепочке. С другой стороны, на одно и то же право пользователь может получить несколько грантов от разных пользователей разными путями. В этом случае право будет действовать, пока жив хоть один грант.
  • Видом операции, на которую даётся право. В качестве них могут выступать:
    • select, insert, update, delete - соответствующие операции над данными.
    • references - означает право сослаться на поля из своей таблицы через foreign key.
    • execute - означает право вызвать процедуру.
    • all означает select, insert, update, delete, references.
  • Объектом БД, над которым можно производить операцию.
  • Списком полей, если объектом является отношение, а правом - update или references.
  • Возможностью получателя передавать этот грант дальше другим пользователям (with grant option, или admin option для ролей).

Ещё раз повторюсь, эти данные заносятся в rdb$user_privileges, исходя из чего автоматически формируются rdb$security_classes. При реальной работе для проверки используется именно последняя таблица, которая может содержать информацию не только вытекающую из грантов - ссылки на неё есть в нескольких местах системных метаданных. Хотя в большинстве случаев на это можно не обращать внимание.

Фундаментальная слабость данной системы заключается в том, что очень неудобно давать права группам (пользователей, процедур, и т. п.) - приходится повторять их для каждого элемента группы. У нас в проекте "Архив" даже была для этой цели разработана специальная программа UserManager. Нормальное решние от Борладна появилось лишь в версии 5.0 (совсем точно - ODS 9.0, которая поддерживается пятёркой).

Решение это называется ролью. Роль - это абстрактный носитель набора грантов. Любые вышеперечисленные виды грантов можно собрать в кучу и передать роли. То есть роль в данном случае будет выступать в качестве получателя грантов.

С другой стороны, роль может выступать в качестве вида права. То есть можно дать роль в качестве права пользователю, процедуре, и т. п. Только здесь есть одна важная и неприятная особенность. В отличие от других видов прав, которые работают сразу же после создания (точнее commit'а), роль начинает работать только тогда, когда пользователь подключается с этой ролью. Зачем понадобилось так ограничивать это дело, не ясно. Ведь это перекрывает значительную часть возможных удобств.

Таким образом, роль является дополнительным свойством соединения с сервером. Все утилиты interbase начиная с версии 5 позволяют ввести параметр role или указать ключ командной строки -role. Кроме этого расширен оператор connect:
connect "база"
user "пользователь"
password "пароль"
role "роль" ...;
При этом роль должна быть одной из тех, что назначены данному пользователю. И в течение всего соединения она будет только одна. Если при подключении роль не указать, то будут действовать лишь гранты, выданные традиционным образом.


Глюки с целостностью

Кроме того, что InterBase - не самый эффективный сервер, он ещё и не самый надёжный. Здесь я не буду рассматривать наиболее клинический случай (5.0), но тем не менее... Ненадёжность проявляется во многих формах: и в том, что сервер может внезапно повиснуть, и в том, что может непредсказуемо запороться файл БД, и в том, что корректные операции или их последовательности приводят к некорректным результатам, и в том, что средства диагностики всё это дело не всегда вылавливают. (А не глюк ли, что во всём мире это называется InterBase, а у нас - interbase Database?). Тем не менее, если придерживаться определённых принципов, то неприятностей можно избежать. Но всё по порядку.

Как InterBase падает

Что касается причин падения, то во-первых, InterBase, как и любая другая программа, может упасть от того же, что и соответствующая платформа вообще. Здесь работает очевидная истина: выбор "падучей" системы вроде Windows рано или поздно выйдет боком. Но народ в наше время обычно предпочитает тупо терпеть.

Так же необходимо следить и за железом. Полезно провести тесты с копированием больших файлов (десятки, лучше - сотни МБ) и куч мелких файлов (тысячи, лучше - десятки тысяч) с последующим сравнением скопированного. Если копии разошлись, то сервер на такую машину ставить однозначно нельзя. Если не поможет смена софта (драйверов или всей операцонки), то такую машину лучше выкинуть. Разумеется, отрицательным образом на надёжности сказываются разного рода "разгоны" железа.
Второй фактор, определяющий падучесть - архитектура сервера. Дело в том, что лично мне за всё время приходилось видеть три способа организации вычислений внутри InterBase.

  1. Классический для Unix способ с порождением отдельного процесса на каждое пользовательское соединение (gds_inet_server) + ещё 1 общерулящий процесс (gds_lock_mgr). Такая архитектура является наиболее надёжной, так как общий процесс выполняет мало функций (пользовательские запросы в него не заходят вообще) и практически никогда не падает, а падение обслуживающего процесса затрагивает только одного клиента и система в целом остаётся работоспособной. По такой технологии, насколько я знаю, работают серверы InterBase до 4.0 включительно, кроме NetWare.
  2. Новомодная ныне многопоточная технология, основанная на переделывании "многопроцессной" версии. Состоит в том, что все параллельные единицы исполнения (обзываемые в данном случае потоками) сваливаются в общую кашу в пределах одного процесса. InterBase 4.0 for NetWare, а так же большинство воплощений 4.2 представляют собой один многопоточный процесс, однако в основе потоков лежит тот код, который раньше работал в отдельных процессах. Так что надёжность отдельного потока примерно та же. Это даёт небольшой выигрыш в производительности, хотя по-моему, если бы Борланд направил силы на улучшение оптимизатора запросов, пользы было бы гораздо больше. В общем, теперь сбой в одном потоке валит весь сервер.
  3. Сервер 5.х, переписанный специально под многопоточную архитектуру. Окончательная деградация надёжности, отдельный случай.

Следующий фактор - способ использования сервера. Если в базе хранятся простые таблицы с минимальным количеством ограничений и к ним идут простые запросы, то всё может работать вполне надёжно. Если же пытаться использовать навороченные средства, то иногда можно добиться жутких результатов.
Ещё один важный источник глюков - модули внешних функций (external function, UDF), разновидностью которых являются блобовые фильтры (blob filters). С одной стороны, это хорошо, что Борланд даёт возможность доделывать те вещи, которые не желает делать сам. Но с другой стороны - всё это реализовано по принципу: "шаг влево, шаг вправо - расстрел". То есть нормально в этих библиотеках работают только функции, которые читают параметры, что-то внутри себя вычисляют (скажем, ищут подстроку), и возвращают единственный скалярный результат. Если попытаться обратиться к какому-либо внешнему модулю, установить с кем-либо связь, вызвать исключение, и т. п., то результаты непредсказуемы. В том смысле, что неизвестно: вылетит сразу или потом, в неподходящий момент. Причём на каких конкретно принципах основаны ограничения, что можно, а что нельзя - непонятно.

Есть правда слухи, что начиная с 5.5 interbase научился корректно перехватывать исключения, вылетающие из UDF, но в остальных версиях, как я видел, ни чем хорошим это не кончалось.

Всё это ещё можно терпеть в сервере с независимыми процессами. Там и ограничений меньше, и функции вызываются из одного потока, так что параллельные вызовы исключены, и последствия сбоев не такие кошмарные - ограничены одним процессом, то есть пользовательским соединением. В многопоточном же сервере любой модуль грузится один раз, а вызывают его - как хотят, в любые моменты времени и любое параллельное количество раз. И отбиваться от этой параллельности должен разработчик, а иначе при подключении второго пользователя всё накроется. Или после подключения десятого, как повезёт. Или пару месяцев спустя начнёт звонить разъярённый заказчик.

То есть ни сервер не защищён от модулей, ни модули от сервера. Единственная защищённая от функций версия, которую я видел - InterBase for NetWare. Там внешние функции просто не поддерживаются, что в прочем не мешает ему рушить всю систему по другим причинам.

Кроме этого в некоторых случаях наблюдались совсем странные падения в момент, когда один пользователь модифицировал метаданные, а другие в это время обращались к соответствующим данным. К сожалению, глюк настолько мимолётный и неожиданный, что подробно изложить условия и причины я затрудняюсь.
Последствия падения можно разделить на три вида: повреждения БД, повреждения системы и разорванные соединения. Повреждения системы определяются её архитектурой и колеблются от "гарантированно валится всё" (NetWare) до "исчезает одно соединение с клиентом" (Unix без потоков).

Про наиболее распространённую платформу, Windows могу сказать, что падение InterBase повреждает её хоть и не всегда, но относительно часто. Причём это по моим наблюдениям в равной степени касается и Win90, и NT. Наиболее любимый трюк первой (на моём компьютере) - оставить в памяти какой-то процесс, который пожирает производительность. Когда отлаживаешь что-либо и сервер падает по несколько раз, торможение становится всё заметнее, пока работа не станет совсем невозможной. Замечено, что тормозящий процесс остаётся тогда, когда на момент падения к серверу было более одного подключения. В общем, убивайте внимательно.

Что же касается NT, то в ней повреждения проявляется в виде какой-то внутренней ошибке при попытке вновь запустить сервис InterBase.

Соединения клиента с InterBase, как известно, бывают четырёх видов:

  • Local - участок общей памяти, когда клиент и сервер работают в одной машине
  • TCP/IP - протокол Internet и сетей Unix
  • SPX/IPX - протокол сетей NetWare
  • NetBEUI - протокол сетей Microsoft и interbaseM

Наиболее надёжный из них - первый. Как только на одном конце соединения происходит авария, соединение закрывается. Но по сети он, понятное дело, работать не будет. Из сетевых мне представляется наиболее удобным TCP/IP. Во-первых, работает везде. NetBEUI не работает с NetWare, IPX, наоборот, только с NetWare, а сервер под Win95 вообще поддерживает сетевой обмен только через TCP/IP. Во-вторых, этот протокол быстрее любых других выясняет о разрыве соединения на другом конце.
По крайней мере это дело контролируемо, и даже под Виндами. По некоторым данным в реестре, в HKEY_LOCAL_MACHINE \ CurrentControlSet \ Services \ VxD \ MSTCP имеются параметры:

  • KeepAliveTime - время в миллисекундах, в течение которого TCP/IP ждёт в режиме бездействия содинения, прежде чем начать его тестировать. По умолчанию стоит два часа (охренел малость Мелкософт). При работе с локальными сетями лучше поставить одну-две минуты, а при наличии доступа в Инет - минут 10 - 15 (переведя в миллисекунды, разумеется).
  • KeepAliveInterval - время между тестовыми пакетами, тоже  в миллисекундах. По умолчанию - 1 секунда.
  • MaxDataRetries - количество тестовых пакетов. Если после данного количества тестов не получено ни одного ответа, соединение считается разорванным.

Я пробовал с ними экспериментировать, но ничего хорошего не добился. Под NT вписывание этих параметров в нужную ветку просто ни к чему не привело. Под 95 привело. К тому, что соединение просто обламывалось при истечении таймаута бездействия. То есть тестовых пакетов будто и не было. Дальше следовало бы поработать сетевым анализатором - что там реально происходит. Но на это нужно время и желание ...

Остаётся надеяться только на меры со стороны разработчиков будущих версий. В interbase 5.6 согласно документации в файле interbaseCONFIG появилась пара новых параметров - CONNECTION_TIMEOUT nnn и DUMMY_PACKET_INTERVAL nnn. Оба делают примерно то же самое, но независимо от транспортного протокола. Первый параметр задаёт время в секундах, после которого закрывается неоткликающееся соединение, а второй - интервал между тестовыми запросами в время бездействия соединения. По умолчанию оба параметра закомментированы и в интерфейс нигде не выведены. Рекомендуется поставить руками, скажем, 160 и 50.

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

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

Когда же падает клиент, на сервере остаётся открытое соединение, через которое сервер ждёт запросов. Если соединения каким-либо способом учитываются (скажем, не допускается повторное подключение того же пользователя), то "висячие" соединения создадут проблемы. В прочем, для TCP/IP это лечится всё тем же способом.

И уж раз мы говорим про соединения, то упомяну ещё одну особенность комбинации NetBEUI + NT. В этом варианте поток, обслуживающий клиента, работает "от имени" этого клиента. Это значит, что чтобы иметь доступ к базе ему мало быть зарегистрированным в interbase. Надо быть также зарегистрированным в NT и войти в сеть MS под таким именем, чтобы NT к себе пустила. И в довершение ко всему нужно, чтобы у пользователя NT были права на чтение и запись файла БД. Если хотя бы одно звено в этой цепочке маразмов не выполнено, то NT может не пустить к себе соединение NetBEUI. Смысл этой защиты тем более неясен, что соединение TCP/IP в тех же самых условиях проблем не создаёт.


Физически запорченная база и что с ней делать

И так, сервер обвалился. Что может произойти с базой? В нормальных СУБД от этого должны оставаться недописанные страницы данных и элементы журнала транзакций. При следующем старте сервер должен прочитать журнал, проанализировать его и либо дописать то, что недописано, либо отмотать изменения назад согласно журналу. В общем, база возвращается в корректное состояние и начинается приём запросов от клиентов.

Это как по уму. На практике же в InterBase иногда возникают ситуации, когда БД после падения восстанавливается не полностью и начинает работать с базой, в которой есть повреждённые элементы. Это касается падений по внутренним причинам, а не по внешним, типа отключения питания.

При этом все дальнейшие операции чтения и обработки содержимого базы, похоже, выполняются в предположении, что база корректна. И при попытке обработать мусор могут возникнуть новые сбои, новое падение сервера, новые ошибки в структуре файла БД, и т. д. Чтобы гарантированно это предотвратить, нужно после каждой аварии до начала работы принимать меры по ремонту базы. Меры (с помощью ServerManager или аналогичных утилит командной строки) могут быть следующие:

  • Проверка базы (Validation). Проверяет корректность внутренних структур файла БД и выявляет порченные. Это в теории. На практике было достаточно много случаев, когда база, прошедшая проверку без ошибок, в дальнейшем вела себя, как откровенно некорректная, в том числе не снималась резервная копия. С другой стороны, были случаи, когда Server Manager страшно ругался, даже отказывался приступать к проверке, в то время, как с точки зрения клиентских подключений никаких аномалий не наблюдалось. В целом, если Validation ругается на базу, то в ней что-то не то (хотя и трудно понять насколько это опасно), и с ней лучше не работать. По крайней мере в данном сервере. Если же не ругается, то база возможно корректна, хотя и не обязательно. Обычно такая проверка достаточна, лишь если сервер упал по внешней причине.
  • Снятие резервной копии (Backup). В ходе этого процесса сервер прочитывает всю базу и преобразовывает её содержимое в другой формат. При этом отбрасываются индексы, результаты компиляции хранимых процедур и прочее, что можно потом восстановить по данным и метаданным. В результате имеется шанс отловить те нарушения, которые игнорирует Validation. Хотя есть и такие, которые нормально попадают в резервную копию. В отличие от Validation, это ничего не лечит в исходной базе, а лишь проверяет её.
  • Снятие и восстановление из резервной копии (Backup - Restore). Те ошибки, которые беспрепятственно попадают в резервную копию, обычно вызывают ругань при восстановлении. Иногда бывает возможно слегка обновить исходную копию базы и вновь прогнать Backup - Restore. Если удастся добиться, чтобы этот процесс прошёл полностью корректно, то это даст наибольшую гарантию, что база, получившаяся в результате не содержит ошибок. Кроме всего прочего, этот способ обычно сокращает объём базы, так как в процессе копирования производится "сборка мусора" и ликвидируется неиспользуемое пространство. Кроме этого, происходит балансировка деревьев в индексах и корректировка их статистических параметров.

Как запороть базу средствами SQL

И так, существуют вещи, которые в InterBase можно сделать одними конструкциями SQL, но которые с точки зрения других конструкций или средств (а так же по жизни) являются некорректными. Так, мне приходилось наблюдать, как не удавалось создать ссылочное ограничение, которое ссылалось на поле, по которому было множество разных индексов. Это притом, что уже существовали ссылочные ограничения, созданные до индексов. Когда часть индексов была уничтожена, ссылочное ограничение создалось нормально. Причём в простых случаях этот эффект промоделировать не удалось. Могу сказать лишь, что ссылок было много, индексов было много и часть из них дублировалась, то есть, были индексы asсending и descending под одним и тем же наборам полей.

Особая песня - добавление полей типа not null. Представим себе, что в таблице уже есть данные, и мы добавляем такое поле. Естественно, что существующие записи будут расширены какими-то значениями. Но какими? Null использовать формально нельзя. По логике нужно применить значение default, если оно есть, а если нет - то отменить операцию, как некорректную. Вот только InterBase добавляет поля всегда в состоянии null, в нарушение всяких ограничений и никогда не использует default.

Причём, если в дальнейшем вы захотите это поле прочитать, то прочитается именно null, а если обновить, то будет ругань о том, что пустое значение недопустимо. И так будет даже тогда, когда обновляется другое поле той же записи. По всей видимости, InterBase обновляет записи в следующем порядке:

  1. Запись читается в память
  2. Вызываются триггеры before update в заданном порядке
  3. Проверяются все ограничения, наложенные на запись (а не только те, которые касаются обновляемых полей)
  4. Запись сохраняется
  5. Вызываются триггеры after update.

Таким образом, если прочитается null и триггеры это поле не исправят, то этап проверки гарантированно выругается.

Ещё более прикольно такие поля ведут себя при резервном копировании. Операция создания резервной копии проходит абсолютно безболезненно, без каких-либо замечаний. А вот при восстановлении процесс доходит до кривых полей и вылетает с руганью. Отсюда мораль: лучше всего резервные копии проверять "не отходя от кассы", о чём писалось выше.

Ещё один источник глюков - таблицы метаданных. Хотя они доступны для обновления только администратору, но ведь всё-таки доступны. Так что учитывая, что не все операции над структурой базы реализуемы операторами (якобы) SQL92, желание что-то исправить напрямую иногда возникает. Формально на этих таблицах висят какие-то триггера, которые иногда ругаются при попытках что-либо не совсем корректно обновить. Однако если руками залезть в метаданные, то ничего такого уже не гарантируется.

К примеру, можно создать генератор, потом процедуру, его использующую, а потом генератор из метаданных грохнуть. И грохнется, как миленький. А процедура будет спокойно работать. И можно вновь создать такой генератор с тем же именем. А процедура по-прежнему будет использовать старый счётчик, выживший где-то в дебрях системы. И все попытки сделать set generator будут действовать именно на новую версию. Вот если сделать alter procedure или drop/create, тогда начнёт использоваться новый генератор.

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

Разумеется, мощным оружием в борьбе с физической целостностью являются внешние функции, о которых уже писалось в разделе, посвящённом "падучести" сервера. Когда такая функция разрушает серверный процесс, он в лучшем случае может прервать обновление физических структур базы в совершенно произвольном месте (да здравствует многопоточность!), а в худшем - пойдёт писать в БД данные, предварительно запоротые в памяти или в неправильные страницы. Для описания здесь возможных последствий мне потребовалась бы непереводимая игра слов с использованием местных идиоматических выражений. В общем, регулярно проверяйте базу!

Ещё пример: создаём процедуру с определёнными параметрами, затем вызываем её из другого места и в конце делаем alter procedure с совершенно другими параметрами. Ясно, что старый вызов после этого станет некорректным, но InterBase на это ничего не скажет. А что произойдёт при вызове в устаревшем формате ... как меня это достало! Не пытайтесь вызывать процедуры с ткамими ссылками, если Вам о них известно. Лучший вариант - грохнуть всё, что связано с ситуацией и пересоздать заново.

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


Прочие странности и неприятности

Вложенность вызовов

Коль скоро что-то можно вызывать вложенно, то возникает вопрос, сколько раз, и до каких пределов. Насколько мне известно из опыта, максимальная вложенность вызова триггеров находится в диапазоне 6-8 (хотя в 5.5 её по слухам раздули до 700-1000). Выяснилось это при реализации каскадного удаления. То есть клиент удаляет запись, запускается триггер, удаляет другую запись, запускается триггер, ... и так до ошибки.

Процедуры же можно вызывать до 1000 раз. По крайней мере, так говорит официальная документация. Собственно, описанный глюк именно через процедуры и разрешился. То есть теперь в нашем проекте триггера каскадного удаления ничего сами не удаляют, а вызывают процедуры sp_bd_xxxx. И вложенные вызовы идут уже между ними.

Установка, переустановка и гроханье

Linux

Документация Борланда гласит, что ставить InterBase 4.0 можно только на Red Hat 4.2. А 5.6 - под Red Hat 6.0. И то, и другое - большое гадство. Изо всех дистрибутивов выбрали самый дорогой коммерческий и изо всех сил пытаются привязаться к нему.

Самое интересное, что и в Slackware, и в Debian, и в RedHat 5.1 четвёрка ставится совершенно одинаково и нормально работает. Что действительно зависит от диструбитива, так это interbasePerl - он (0.5) у меня почему-то ожил только под Slackware.

С 5.6 всё оказалось сложнее - эта версия скомпилирована с glinterbasec 2.1. Самое обидное, что для реальной работы никаких специфических функций этой библиотеки не требуется - я смотрел импорты. Всё то же самое, могли бы скомпилировать и с glinterbasec 1.0, как четвёрку. Стянуть свежий glinterbasec можно с ftp://prep.ai.mit.edu/pub/gnu/glinterbasec/. Только вот дело это нелёгкое - похуже ядра. Одного места на диске для сборки понадобится 400 МБ. И дополнительные пакеты придётся стянуть из других мест. В общем, если страшно, но хочется поставить, можете попросить скомпилированный пакет у меня.

Поставляется дистрибутив в довольно странном виде: .tar, а внутри ещё один .tar + install + readme. Внешний .tar надо распаковать в какой-нибудь временный каталог и запустить install из-под root'а. Инсталлятор фактически представляет собой скрипт, который спрашивает, куда ставить и распаковывает второй .tar туда, куда сказали. Попутно он правит /etc/inetd.conf и /etc/services, чтобы прописать новый сервис.

Здесь есть одна детская неожиданность: при попытке поставить в самое логичное место - /usr/interbase инсталлятор заявил, что непосредственно туда поставить не может, а поставит в /usr/interbasease/i586...всякая_фигня.../, а потом сделает ссылку /usr/interbase -> /куда/реально/поставил. И самое интересное, что обещание выполнил и всё запахало. Только вот никак не пойму, зачем это?

Далее надо сообщить inetd, что его конфигурация изменилась. Те, кому лень думать могут тупо перезагрузить систему. Кто поумнее сделает kill -HUP номер_процесса_inetd или что-то подобное.
На этом официальная установка заканчивается, и начинается ручная доводка. Дело в том, что после установки все процессы InterBase запускаются под root'ом и имеют неограниченные права. И соответственно все обращения к диску, все создаваемые базы числятся под root'ом. В сочетании с глюком всех InterBase'ов - возможностью клиентов произвольно создавать базы в любом месте файловой системы - получается страшное оружие для чайников. В других, менее защищённых системах это еще можно понять, но в Unix - просто хамство.

Таким образом, нужно срочно (а ещё лучше - заранее) создать специального пользователя (скажем, iserver), заблокировать ему вход в систему и переподчинить всё, что инсталлятор понаставил: chown iserver.users /куда/поставлено. Потом надо ещё переправить /etc/inetd.conf, заменив root на iserver в строке для gds_db. После этого будет пахать гораздо безопаснее (в том смысле, что взломавший InterBase ничего кроме баз не запортит), но нужно следить, чтобы файлы БД имели доступ rw для этого самого iserver.

В качестве альтернативы можно предложить занести всех без исключения пользователей Unix в InterBase, причём сообщив uid каждого из них. В этом случае серверные процессы пользователей будут работать из-под правильного uid и всё тоже нормализуется. Однако как только найдётся пользователь, не прописанный в interbase, он получит права root'а. Уж лучше бы они сделали переключение на nobody, как, например, в Apache.

Борланд врёт, что данному серверу требуется 32 МБ памяти. На самом деле требуется примерно 2-3 МБ на активное подключение, то есть цифра 32 соответствует примерно 10 одновременно работающим клиентам. Если клиенты готовы терпеть лёгкое торможение, то можно и по 1 МБ на человека. С учётом того, что ядру Linux нужно около 4 МБ для собственного счастья, этот сервер должен быть работоспособным, начиная примерно с 8 МБ.

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

Грохнуть достаточно просто:

  1. Убрать ссылки на InterBase (gds) из /etc/inetd.conf и /etc/services.
  2. Оповестить inetd так же, как и при установке.
  3. Грохнуть ветку каталогов и, возможно, символическую ссылку, которые создал инсталлятор.
  4. Грохнуть повисшие символические ссылки /usr/linterbase/linterbaseg...so, созданные инсталятором.
  5. Грохнуть служебного пользователя, если он был создан и больше не нужен. 

NetWare

Фундаментальный дебилизм для этой системы: для установки мало иметь NetWare, нужна ещё и Windows на клиенте. То есть надо найти такого клиента, войти с него в сервер под SUPERVISOR'ом и запустить install.exe. Дальше всё поставится традиционным образом, но запускаться не будет.

Для запуска нужно сделать load iserver.nlm с командной строки или, для автоматического старта, из autoexec.ncf.

И опять, как и в случае с Linux, Борланд плюёт на файловую защиту - все БД, создаваемые любыми пользователями, реально создаются с супервизорскими правами и в любом месте, где скажет клиент. Только вот NetWare - не Unix, и наложить защиту "руками" в нём никак не получается. Буду рад, если кто-нибудь придумает. А пока остаётся только сохранять бдительность.
Установка в основном происходит в каталог (если правильно помню) sys:interbas\, за исключением самого исполняемого модуля - sys:system\iserver.nlm. Чтобы грохнуть, нужно сначала выгрузить iserver, если он работает (unload iserver), а затем стереть всё ненужное.

Windows 95

Существует несколько разновидностей InterBase, работающих под Windows, в том числе и 95. Мне попадался Local Server 4.0 и Interbase for Windows 4.2. Разница была в том, что первый поддерживал только локальные соединения, а второй признавал ещё и TCP/IP, правда, до 5 соединений одновременно. Плюс так же традиционная разница между 4.0 и 4.2.

В остальном же InterBase for Windows - стандартный виндовский продукт со всеми общепринятыми глюками. То есть ставится в задаваемый пользователем каталог, но попутно кидает файлы в каталоги Windows и System, а так же залезает в несколько мест в реестре. Среди продуктов жизнедеятельности в файловой системе - interbas.ini, gds32.dll, драйвер ODBC (забыл, как называется файл).

Основная проблема возникает тогда, когда надо поставить сервер на место предыдущей установки или поверх продуктов, которые содержат в себе клиент InterBase. Если попытаться поставить прямо "поверх", то часть файлов перепишется, и возникнет каша из версий, которая работать с большой вероятностью не будет. Единственный гарантированный и надёжный способ - грохнуть всю систему (каталог Windows) и переустановить всё, что нужно, включая InterBase.

Для менее радикальной переустановки следует полностью вычистить старый InterBase. Насколько я себе представляю на данный момент, для этого нужно следующее:

  • Деинсталировать InterBase стандартным способом, сходив на Панель.
  • Вытереть каталог, в который был установлен InterBase (он вполне может и выжить).
  • Вытереть из каталогов Windows и System все прочие продукты жизнедеятельности InterBase.
  • Пройтись по реестру, ища слово "rbas". Дело в том, что могут встретиться слова InterBase, INTERBAS, IntrBase, и им подобные. Разумеется, грохать нужно не всё подряд, а только те записи, которые действительно относятся к InterBase.
  • На всякий пожарный случай перегрузиться.
  • Проверить ещё раз, не осталось ли чего в каталогах или реестре.
  • Установить InterBase заново.

Windows NT

В целом InterBase for NT выглядит так же, как и for Windows 95. Отличие в том, что не урезана функциональность - поддерживаются все режимы и все сетевые протоколы. Поведение этого сервера и в установке, и в работе так же отличается не сильно.

Таким образом, общий сценарий при переустановках тот же. Вот только одна большая загвоздка - попытка миграции с 5.0 на 4.2 не приводит к желаемому результату даже при полном выполнении вышеописанного сценария. То есть где-то в тайных дебрях системы остаются следы прежней установки и новая этого пережить не может.


 

Версии (4.Х, 5.Х, 6.Х)

Если совсем кратко: какой смысл в удвоении производительности, если простои из-за сбоев начинают отнимать её половину? Ну а в деталях подробности следующие.

Во-первых, пятёрка под Windows ставится в другой каталог. Старая ставилась в \Program Files\Borland, новая - в \Program Files\Interbase Corp. Таким образом, если при переустановке не принять специальных мер, можно остаться с двумя комплектами файлов, причём не известно, на какие файлы будут указывать настройки в реестре. В шестёрке от отдельного каталога InterBase Corp опять отказались.

Второе крупное отличие: сервер для большинства платформ (кроме Linux и SCO - счастливые люди) полностью многопоточный, переписан специально с ориентацией на это. С одной стороны, это сказывается положительно на производительности (хотя InterBase 5 по-прежнему не производит впечатления самого быстрого сервера), с другой - радикально ухудшает надёжность. Падает по всем тем же причинам, что и другие виды InterBase, но гораздо охотнее.

Далее, в Interbase 5 по всей видимости имеется гораздо больше ошибок, и это признают даже авторы. По поводу некоторых из них можно найти разъяснения на http://www.interbase.com/tech/knowledgebase/index.html. В частности, в некоторых случаях некорректно отрабатываются вспомогательные вещи типа подсчёта количества записей в результате запроса. Так же признана и низкая стабильность этого сервера. Это, разумеется, не значит, что он совсем ни на что не годен. Тем не менее, в состав сервера введён дополнительный процесс (InterBase Guardian), который предназначен единственно для того, чтобы в нужный момент перезапустить сам сервер. В некоторых рекламных статьях это решение хвалят, как меру по повышению надёжности, но на самом деле это - позорная заплата, означающая бессилие разработчиков перед горой глюков.

Остаётся добавить, что в свежевыпущенных версиях 5.5 и 5.6 часть глюков по слухам исправлена. Насколько это правда, сказать без опыта реальной эксплуатации пока не могу, но первые впечатления хорошие.

Что касается производительности, то она действительно выросла. Причём иногда существенно, а иногда - нет. В прочем, этот эффект наблюдался и раньше: 4.2 быстрее и глючнее, чем 4.0, а 5.0 - чем 4.2. Выше я уже упоминал в нескольких местах, что вопросы с производительностью отдельных операций в V5 решены. Интеллект планировщика запросов в целом повысился. Однако есть и то, что осталось незыблемым - это операции, связанные с генерацией и сортировкой промежуточных таблиц, не влезающих в оперативную память. Такие таблицы выгружаются в файлы во временном каталоге и ни их размер не уменьшается, ни скорость обработки не возрастает.


Тотально обновляемые представления

Если покопаться в современной литературе по реляционной теории, то можно найти полный и эффективный комплект правил, позволяющий обновить практически любое представление, которое формирует поля путём непосредственной выборки из таблиц, а не путём вычислений (не все вычисления обратимы, разумеется). Вот только разработчикам SQL, и тем более InterBase на это всё наплевать. В большинстве случаев они реализуют лишь наиболее тупые способы обновления.

Тем не менее, кое-что в InterBase обновляется. При этом на запрос, формирующий представление накладываются почти классические для SQL ограничения - выбирать данные только из одной таблицы, не группировать (в том числе и скрытно через distinct). А вот дальше начинаются странности. Стандарт SQL говорит ещё, что нельзя сортировать (order by) и нельзя делать вычисляемые поля. Ну, первое-то, понятно, бзик, так как отследить соответствие хранимой записи в любом случае просто. А вот второе в InterBase, как ни странно работает. То есть если представление удовлетворяет всем прочим требованиям, но содержит вычисляемые поля, но невычисляемые всё равно будут обновляться. И даже вставлять записи можно, если не указывать значения для вычисляемых полей.

Однако всё это на самом деле мелочи, потому что обновляемым можно сделать любое представление. Достаточно навесить на него триггера и всё будет в порядке. Внутри триггеров, естественно, должны быть прописаны правила обновления. Более конкретно, когда клиент делает какую-либо обновляющую операцию над записью, в представлении происходит следующее.

  • Вызываются триггера before операция в нужном порядке.
  • Получившиеся значения из серии new.поле проверяются на соответствие not null.
  • Если операция обновления реализуема с точки зрения InterBase, то он её делает. Если же не реализуема и нет ни одного триггера, то ругается.
  • Если представление создано with check option, то проверяется, что новая запись появилась в представлении. Иначе генерируется ошибка и операция отменяется.
  • Вызываются триггеры after операция в обычном порядке.

Зная эти особенности, можно заставить обновляться почти всё, что в InterBase выглядит таблицеобразно (за исключением некоторых видов хранимых процедур). Достаточно навесить триггеры на before и в них описать, как обновляются хранимые данные. Но нужно помнить о некоторых подводных камнях.

Во-первых, когда приписываешь эмуляцию обновления, нужно точно удостовериться, что представление "естественным" образом не обновляемо. С учётом вышеупомянутых отклонений я бы посоветовал алхимический метод: сделать create view и до навешивания триггеров попробовать обновиться. Не идёт - навешиваем триггера. Идёт, но неправильно (не так как надо - и такое может случиться), делаем представление необновляемым, а потом опять же навешиваем триггера.

Во-вторых, если понадобилось сделать представление "естественно необновляемым", то сделать это легче всего, соединив читаемую таблицу в RDB$DATABASE. Эта таблица относится к системным метаданным и всегда содержит одну запись. В результате такого соединения количество записей, читаемых через представление не изменится, но поскольку соединение с формальной точки зрения делается, то естественная обновляемость пропадёт. После чего её можно спокойно восстанавливать искусственно (триггерами).

В-третьих, InterBase пытается отслеживать для полей представления признаки not null. Вычисляемые поля всегда считаются nullable, а извлекаемые их хранимых полей - наследуют этот признак. То есть если поле в хранимой таблице not null, то и в представлении оно будет not null. Причём даже в том случае, если по логике представления оно может быть nullable, как скажем в случае внешнего соединения:

create table t1(

    x integer not null primary key,

    name varchar(100) not null,

);

create table t2(

    y integer not null primary key,

    name varchar(100) not null

    ref_x integer references t1(x)

);

create view v(y, name_y, name_x) as

    select t2.y, t2.name, t1.name

    from t1 left outer join t2 on t1.x = t2.ref_x;

В данном случае name_x будет истрактовано, как not null, хотя на практике такие значения в этом поле вполне могут встретиться, если ref_x is null. Тем не менее, триггеры на вставку и обновление можно приписывать смело. В начало триггеров на вставку или обновление нужно добавить строку вида new.name_x = 'чего-нибудь', а потом это поле никак не использовать. В результате к моменту проверки значение будет присутствовать и обманутый (поделом) InterBase не выругается.


 

Как это будет по-русски

Известно, что вся компьютерная отрасль развивается под жёстким давлением американского шовинизма. То есть эти буржуи проектируют все свои продукты в предположении, что ихняя Америка - единственная страна на Земле. Потом иногда спохватываются (в лучшем случае) и добавляют функции поддержки национальных кодировок, но поскольку добавление происходит задним числом, практически всегда получается криво. Причём поддержка тех стран, в которых продукт продаётся, сделана ещё хоть как-то культурно. Над остальными же просто издеваются.

InterBase в этом смысле - не исключение. То есть зачатки национальной поддержки в стиле SQL92 наблюдаются. В частности, имеются понятия набора символов (character set) и сравнения (collation), в системных метаданных предусмотрено хранение их параметров. Вот только операторов create/drop character set, create/drop collation нет. Честно говоря, я их кроме как в стандарте SQL нигде больше и не видел. А жаль.

То, что реализовано, так же удобством не отличается. Приходится каждый раз при создании БД или подключении через Interactive SQL тыкать InterBase носом в правильные настройки.

В целом национальная поддержка оперирует двумя понятиями:

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

Более конкретно в InterBase имеется:

  • Кодовые страницы CYRL (кириллица в кодировке ДОС) и WIN1251 (в кодировке Windows). Не смотря на поддержку Unix, нет ни КОИ8, ни хотя бы ISO8859-5, что довольно странно.
  • Сравнения DB_RUS, PDOX_CYRL, CYRL для кодировки CYRL (не вникал в различия, так как никогда не приходилось пользоваться) и  PXW_CYRL, WIN1251 для кодировки WIN1251.

Если указать кодировку без сравнения, то по умолчанию установится сравнение, одноимённое с кодировкой. А одноимённые сравнения практически всегда сравнивают строки с учётом регистра, обеспечивая лишь примитивную алфавитную сортировку. Более полезные сравнения, игнорирующие регистр и правильно сортирующие строки со смешанным верхним/нижним регистром называются по-другому. Причём если установить кодировку по умолчанию для базы ещё можно, то последовательность сравнения - нет. Хотя, как обычно, существует обходной и очень ненадёжный манёвр с метаданными.

Если же не указать кодировку по умолчанию вообще, то установится NONE, то есть отсутствие какого-либо слежения за байтами, помещаемыми в строки. При всей своей примитивности - достаточно опасная вещь. В строки с такой кодировкой можно записать хоть русский, хоть какой-либо другой текст.

Но вот корректно его сравнить (буква "Ё" практически во всех кодовых таблицах стоит отдельно от остального алфавита) или преобразовать в верхний регистр уже не удастся. И преобразовать в другую кодировку внутри InterBase - тоже! Ведь InterBase ничего не знает о том, какие символы хранятся в кодировке NONE. Один путь - вытащить строки на клиента, а потом загнать обратно в сервер. Так что такую опасность надо учитывать и принимать меры заранее. Другой - применить UDF.

Всё дальнейшее изложение будет идти с ориентацией на кодировку Windows. На самом деле для ДОСа никаких существенных отличий нет (кроме идентификаторов).

И так, первым делом надо создать БД с правильной кодировкой по умолчанию. Делается это путём дописывания в конец оператора create database следующей опции: default character set win1251. Далее надо все поля с русским текстом объявлять как collate PXW_CYRL. Везде, где воспринимается collate.

Альтернативный способ состоит в обновлении метаданных, которое нужно проделывать после каждого перебэкапа. Это плохо, потому что ко всему ещё и опасно, но зато может сработать на уже существующей базе.

И так, надо назначить данной кодировке правильную последовательность сравнения по умолчанию. InterBase в этом отношении злопамятен, так что если сначала создать таблицу со строковыми полями, а затем поменять сравнение, то на ранее созданные поля это не подействует. В общем, данная установка умолчания делается парой операторов:

update RDB$CHARACTER_SETS

  set RDB$DEFAULT_COLLATE_NAME = 'PXW_CYRL'

  where RDB$CHARACTER_SET_NAME like 'WIN1251%'

;

commit work

;

После этого можно приступать к созданию таблиц и прочих вещей - строки будут корректно сравниваться и сортироваться без учёта регистра. Существующие поля можно исправить через RDB$FIELDS и RDB$RELATION_FIELDS.

Остаётся поговорить о настройке клиента. Обычно это сводится главным образом к настройке BDE, о котором немного ниже, но мелких пакостей добавляет и Interactive SQL. Последний никогда не запоминает кодовую страницу, в которой работает клиент и никогда не пытается её опознать автоматически (что так же возможно практически всегда). Вместо этого в качестве кодировки устанавливается NONE со всеми её "приятностями".

Что же касается BDE, то здесь правильный русский драйвер называется AnCyrr на диске и PDOX ANSI Cyrillic в интерактивных настройках. При инициализации соединения с базой BDE использует драйвера в следующем порядке:

  • Предполагается, что клиент работает в кодировке, соответствующей языковому драйверу по умолчанию в системе.
  • Для определения кодировки строк в БД первым делом используется языковый драйвер, настроенный в алиасе
  • Если алиаса нет, или в нём языковый драйвер не указан, используется языковый драйвер для драйвера БД (в нашем случае - для InterBase)
  • Если и для драйвера БД языковый драйвер не указан, используется драйвер по умолчанию для системы.

В общем, наилучший способ - сразу же после установки BDE пройтись по всем настройкам и везде, где упоминается языковый драйвер настроить PDOX ANSI Cyrillic. Рай от этого не наступит, но проблемы сведутся к минимуму.


 

BDE и InterBase - тормоза-братья

Несмотря на тормозологическую развитость InterBase фирма Borland не ограничилась только серверной частью своей архитектуры и разработала соответствующий продукт для клиента. По логике вещей - BDE должен отделять приложения от особенностей конкретной СУБД, подстраивая поступающие с клиентов запросы оптимальным образом. На деле же во многих случаях происходит наоборот - драйвер InterBase от той же фирмы Borland использует лишь часть возможностей этого сервера и его языка, причём далеко не лучшим образом. При этом средства разработки от Borland часто стимулируют использование как раз тех вещей, которые максимально тормозят обработку.

И так, клиенты через BDE к серверу и открывают датасеты (наборы записей). Открыть их можно с помощью SQL-запроса select, или просто указав имя таблицы (представления). Открытый датасет можно листать вперёд-назад, можно искать запись по значениям полей, можно накладывать фильтр по связям с другими датасетами или по (почти) произвольному выражению. Кроме того, можно обновлять текущую запись в датасете или исполнять произвольные "обновляющие" запросы, не заботясь об управлении транзакциями на уровне отдельных операторов. Казалось бы, всё замечательно, но ...

Метаданные

Драйвер InterBase обожает читать свои собственные метаданные по каждому поводу. При каждой попытке открыть в первый раз за сеанс таблицу или запрос получается по моим наблюдениям как минимум пара запросов к таблицам RDB$XXXX. Самое интересное, что эти таблицы, будучи довольно сильно нагруженными, во многих случаях не проиндексированы или проиндексированы далеко не по всем полям, по которым идёт поиск. После более длительных наблюдений у меня сложилось впечатление, что индексируются только те поля, которые участвуют в соединении нескольких таблиц. Это, конечно, правильное решение, но соединение - не единственная операция, требующая индексирования. Конкретные ситуации лучше исследовать с помощью  SQL-монитора.

В пределах сеанса работы BDE обычно помнит метаданные, так что если открыть эту же таблицу с теми же параметрами или запрос с тем же исходным текстом, то повторных чтений обычно не происходит. Но стоит только что-то изменить (скажем, фильтр у таблицы или условие where у запроса), как при следующей попытке открыть это дело будет вновь прочитана куча того же мусора.

Мораль: даже мелкий с виду запрос через BDE может проделать кучу непредвиденной работы.

Частично данную проблему можно решить с помощью механизма Schema Cache (кэширование схемы), реализованного в драйвере InterBase. В его настройках надо указать каталог на локальном диске, где хранить метаданные, количество таблиц, метаданные которых можно туда выгружать и срок хранения, в течение которого метаданные считаются годными.

Однако, как обычно, одно неудобство меняется на другое. Если до истечения таймаута сделать что-нибудь вроде alter table, то у клиента всё равно останутся старые метаданные. В результате - глюки и ругань на массовое несоответствие типов. Это, конечно, редко случается в реально эксплуатируемой системе, но в процессе разработки - сплошь и рядом. В таких случаях лучше отключить Schema Cache и терпеть. Или, если уж нехорошая ситуация возникла эпизодически, остановить все клиентские приложения в машине и стереть содержимое каталога, отданного под Schema Cache.


 

Прокрутка датасетов-таблиц

Допустим, что мы открыли таблицу и листаем её в заданном направлении. Перед BDE встаёт задача: есть текущая запись и нужно найти следующую (предыдущую) в заданном порядке. На уровне SQL-запросов, идущих в сервер, листание превращается в запросы (предполагаем, что для таблицы назначена сортировка по одному полю и листание идёт вперёд):

select имена_всех_полей

       from таблица

       where ключ is null or ключ > ключ_текущей_записи

       order by ключ asc

Особенности:

  • Всегда читаются все поля запрошенной таблицы, независимо от того, нужны они приложению или нет.
  • При обратном направлении прокрутки меняются направления сортировки и сравнения.
  • Ключ всегда проверяется на null, даже если он в метаданных БД прописан, как not null. InterBase при любых order by всегда выдаёт записи с null последними, так что "нормальную" сортировку это не портит, хотя и тормозит.
  • BDE, как правило, вычитывает из этого запроса не все записи, а столько, на сколько пользователь пролистал таблицу в данном направлении.
  • Комбинация where и order by приводит к тому, что записи внутри сервера сортируются физически, причём все, попадающие под условие, несмотря на предыдущий пункт. Так что на больших таблицах такая прокрутка в принципе не может быть эффективной. Об этом и последующих эффектах писалось в планировании запросов.
  • Прокрутка "в начало" или "в конец" приводит к примерно аналогичным запросам с немного другой формулировкой, но не менее тормозным.
  • Сортировка по двум полям (ключ1;ключ2) приводит к условию поиска вида "where (ключ1 > ключ1_текущей_записи) or ((ключ1 = ключ1_текущей_записи or ключ1 is null) and (ключ2 > ключ2_текущей записи or ключ2 is null))". Суть в том, что проверяются все возможные комбинации сравнения ключей и наличия null'ов в них, чтобы сгенерировать все записи которые либо "точно больше" текущей, либо "возможно больше". При трёх полях запрос становится ещё более страшным. Причём страшность обратно пропорциональна эффективности.
  • В случае с наложенным фильтром условие может быть ещё сложнее, но это отдельный разговор.

Прокрутка датасетов-запросов

В стандарте SQL предусмотрено, что одни запросы могут быть произвольно прокручиваемыми, а другие - нет. Условия, когда произвольная прокрутка возможна, примерно в том же духе, что и для обновляемости представлений. Это означает, что большинство полезных запросов на уровне InterBase прокручиваются строго вперёд по одной записи. Тем не менее, BDE эмулирует для них произвольную прокрутку. И делается это варварским способом, который я лично называю маниакальное чтение. Суть в том, что после открытия запрос читается от начала до нужного места и все полученные записи сохраняются в памяти на клиенте. При попытке листать назад приложению предъявляются эти прочитанные записи. К чему это приводит, я расписывать не буду.

В Delphi существует свойство запроса, именуемое UniDirectional. Согласно документации, оно должно определять, как открывается запрос: в однонаправленном режиме или в режиме неограниченной прокрутки. На самом деле если запрос допускает только однонаправленный режим, то он и будет открываться в этом режиме независимо от UniDirectional. Реально эта опция позволяет лишь чуть-чуть сэкономить в очень простых случаях. В остальных маниакальное чтение гарантировано.

Фильтрация

Когда я разобрался, как действует этот механизм, у меня сложилось впечатление, что у Borland правая рука не знает, что делает левая. Дело в том, что существуют два способа реализации фильтра:

  1. Путём формирования соответствующего условия в SQL-запросе.
  2. Путём маниакального вычитывания всех записей на клиента с последующей их проверкой.

В двух случаях фильтрация реализуется гарантированно по второму сценарию: когда фильтруется SQL-запрос (у BDE не хватает умственных способностей, чтобы его осмыслить и модифицировать, хотя существуют реляционные системы, в которых такие фокусы являются нормой) и когда фильтрация делается пользовательским обработчиком (в Delphi по событию OnFilterRecord).

В случае с таблицей теоретически возможно реализовать все фильтрующие конструкции средствами SQL. Ведь InterBase поддерживает приведение строк к верхнему регистру через UPPER() и сравнение с подстрокой с помощью containing, с шаблоном с помощью like. Вот только BDE (или его драйвер InterBase) об этом почему-то не знает. Таким образом, если условие состоит лишь из сравнений по полному точному значению с учётом регистра символов (или сравнений чисел и т. п.), то такой запрос будет сконвертирован в часть where отправлен на сервер. Но стоит это требование хоть в чём-то нарушить - и проверка всего фильтрового выражения перейдёт к клиенту с соответствующей прокачкой всех записей таблицы (разве что с учётом фильтра по MasterSource) через сеть.


 

Транзакционные хитрости

Как известно, в Delphi, если не определять явную транзакцию, то все заботы возьмут на себя компоненты и InterBase. Это на словах. На деле происходит следующее (подробности работы с несколькими Database и Session опускаю):

  • Открытие датасетов делается в рамках текущей транзакции. Если её ещё нет, то она запускается.
  • Попытка что-либо удалить или запостить в датасете приводит к операции commit.
  • Попытка выполнить ExecSQL для запроса или ExecProc для хранимой процедуры, а так же любой Refresh опять же приводит к commit.

В InterBase API существует две разновидности операции: просто commit и commit retaining. Обычный commit приводит к тому, что все запросы, открытые в рамках транзакции закрываются. BDE, чтобы не смущать приложения, сразу же начинает новые транзакции, переоткрывает запросы, а затем во многих случаях вычитывает содержимое запроса до конца. В свете того, что было сказано выше о прокрутке, понятно, что это приведёт к повторному прочитыванию данных от начала запроса до текущей позиции. Но в данном случае BDE идёт дальше - прочитывает всё содержимое запроса.

 

Мораль: закрывайте все ненужные запросы, иначе рискуете тормознуть, если не на часы, то на многие минуты.  К счастью это не касается внутренних запросов, с помощью которых читаются таблицы (и на том спасибо).

Операция commit retaining, напротив, сохраняет все открытые запросы, и даже не портит позицию текущей записи. С одной стороны, это хорошо, так как не приводит к маниакальному чтению. С другой - BDE не так часто этим пользуется, по крайней мере, с настройками по умолчанию. Кроме того, при такой фиксации сохраняется часть блокировок на данных. Так что рано или поздно, если не делать полный commit, пользователи начнут натыкаться на непонятные сообщения об ошибках.

Ещё одно замечание: явные вызовы TDatabase.Commit дают всегда полный commit, независимо от прочих настроек, о чём непосредственно ниже.

Копаясь на сайте InterBase, я наткнулся на небольшое упоминание данной проблемы. К сожалению, подробности особо не расписывались, а из трёх предложенных методов решения ни один не лишён крупных недостатков:

    1. Ограничить в параметрах драйвера или алиаса максимальное количество записей с помощью параметра MaxRows. На самом деле это не решает проблему. Лишние записи всё равно вычитываются. Просто любое обращение будет обрублено после заданного количества записей. В частности, если у вас имеется большая таблица, которую вы позволяете пользователю листать, то в процессе листания он рано или поздно наткнётся на такое ограничение.

Консультанты из Borland предлагают снабдить приложение кнопкой "показать следующие записи". Короче говоря, издеваются.

  1. В параметрах алиаса или драйвера поставить Flags=4096. Вообще-то "официальная" документация тщательно скрывает смысл битов этого числа и настоятельно рекомендует оставлять их все нулями. Тем не менее, насколько я могу судить, бит 12 (дающий 4096) заставляет драйвер использовать commit retaining ведзе, где возможно. Это радикальным образом соратит маниакальные обращения, но приведёт к тому, что пользователи начнут натыкаться на ссобщения о блокировках при попытке редактировать одну и ту же запись. Причём для снятия блокировок придётся делать полный commit, который при данных настройках возможен только "руками". Так пишет Борланд. На самом деле я не обнаружил каких-либо оснований для такого предупреждения, так что рекомендую всем работать в режими 4096.
  2. В параметрах поставить Flags=4608. Это - комбинация битов 12 и 9 (4096 + 512). В целом режим похож на предыдущий, но уровень изоляции транзакций принудительно устанавливается на read committed. Это снижает число блокировок, но чтобы гарантированно увидеть все свежие изменения в БД, программа должна сделать полный commit.

Системные таблицы

Общие замечания

Это далеко не полный список таблиц и их полей.

Все имена, хранимые в базе, обычно хранятся в формате CHAR(31), так что значения оказываются дополненными пробелами до этой длинны. Буквы приводятся к верхнему регистру. Так что если со стороны программы приходит имя в обычном виде, то сравнивать можно примерно так: rdb$чего-то like upper(имя||'%'). Хотя иногда это опасно - может попасться другая таблица, у которой начало имени соответствует заданному.

Многие поля хранят такую штуку, как BLR. Это, по всей видимости, такой своеобразный промежуточный код, который возникает в результате компиляции различных объектов (процедур, триггеров, представлений, вычисляемых полей, и т. п.) и затем в нужные моменты исполняется ядром interbase.

Надо сказать, что многие из данных таблиц могут оказывать радикальное действие на соответствующие операции. В частности, при открытии таблиц BDE обожает вычитывать метаданные по полям. При этом далеко не все из этих таблиц надлежащим образом проиндексированы. Так, производительность оператора revoke может подскочить в отдельных случаях более чем на порядок, если проиндексировать RDB$USER_PRIVILEGES. Таким образом, если Вас не устравивает скорость, посмотрите SQL-монитором, к каким таблицам идёт обращение и если на соответствующих полях нет индексов - создайте.

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

Кроме этого, иногда удаётся выправить метаданные, чтобы реализовать то, что не поддерживается официально, через create/alter/drop. Правда, никаких гарантий, но всё же ... У этой операции одна особенность: изменения вступают в силу только в момент фиксации транзакции. То есть commit будет долгим и может легко привести к сбою.

И так:

RDB$CHARACTER_SETS - кодировки символов.

RDB$CHARACTER_SET_NAME - имя кодировки. Может употребляться в конструкциях SQL без кавычек.
RDB$FORM_OF_USE - что-то зарезервированное, во всех системных кодировках - null.
RDB$NUMBER_OF_CHARACTERS - количество символов в кодировке, во всех системных кодировках - null.
RDB$DEFAULT_COLLATE_NAME - имя сравнения по умолчанию. Сравнения описываются в RDB$COLLATIONS.
RDB$CHARACTER_SET_ID - имя уникальный номер кодировки. Некоторые системные таблицы ссылаются на кодировку по нему.
RDB$SYSTEM_FLAG - признак того, что это системная кодировка (1), а не введённая пользователем (0). Как вводить пользовательские - неизвестно.
RDB$DESCRIPTION - текстовое описание, комментарий. У всех системных - null.
RDB$FUNCTION_NAME - нечто зарезервированное. У всех системных - null.
RDB$BYTES_PER_CHARACTER - количество байт на символ. У большинства кодировок - 1, у некоторых азиатских - 2, а у Unicode, как ни странно, 3.

RDB$CHECK_CONSTRAINTS - ограничения целостности. Исключая ссылочные и ключевые.

RDB$CONSTRAINT_NAME - имя самого ограничения. Безымянные обзываются в формате INTEG_nnn.
RDB$TRIGGER_NAME - имя ... то ли триггера, то ли поля. То есть для ограничений типа not null это имя поля (без привязки к таблице), а для остальных - имя системного триггера (CHECK_nnn), сгенерированного для контроля соотеветствующего ограничения.

RDB$COLLATIONS - сравнения символов.

RDB$COLLATION_NAME - название сравнения.
RDB$COLLATION_ID - номер сравнения, видимо, в пределах кодировки потому что много совпадающих.
RDB$CHARACTER_SET_ID - номер кодировки (RDB$CHARACTER_SETS. RDB$CHARACTER_SET_ID), в рамках которой работает данной сравнение.
RDB$COLLATION_ATTRinterbaseUTES - какие-то зарезервированные атрибуты. У всех стандартных - null.
RDB$SYSTEM_FLAG - признак системности (0 - пользовательское, не 0 - системное). У всех стандартных = 1.
RDB$DESCRIPTION - комментарий. У всех стандартных - null.
RDB$FUNCTION_NAME - что-то зарезервированное. У всех стандартных - null.

RDB$DATABASE - глобальные параметры базы данных. В этой таблице в нормальных условиях должна быть только одна запись.

RDB$DESCRIPTION - комментарий к базе. Обычно null.
RDB$RELATION_ID - какой-то зарезервированный номер отношения. Есть подозрение, что это номер поколения структуры базы. К RDB$RELATIONS отношения, видимо, не имеет.
RDB$SECURITY_CLASS - имя класса безопасности согласно RDB$SECURITY_CLASSES. Если установлено, то применяется ко всем объектам в базе. Обычно - null, то есть никаких дополнительных ограничений.
RDB$CHARACTER_SET_NAME - кодировка по умолчанию для базы согласно RDB$CHARACTER_SETS.

RDB$DEPENDENCIES - зависимости между объектами БД. Ну очень полезная таблица, но иногда вручая - не все зависимости бывают видны. Даже апосле перебэкапа.

RDB$DEPENDENT_NAME - что зависит.
RDB$DEPENDED_ON_NAME - от чего зависит.
RDB$FIELD_NAME - на случай, если предыдущее поле - отношение, от какого поля зависит. Если от объекта вообще, то null.
RDB$DEPENDENT_TYPE - тип того, что зависит.
RDB$DEPENDENT_ON_TYPE - того, от чего зависит.

Типы зависимых и зависящих объектов кодируются числами:
0 - таблица
1 - представление
2 - триггер
3 - вычисляемое поле
4 - проверка (что за зверь?)
5 - процедура
6 - индекс по выражению (хотя вообще-то такие вещи в нынешнем interbase не поддерживаются)
7 - исключение
8 - пользователь (интересно, это как?)
9 - поле
10 - индекс (никому не посоветую такую зависимость, будут глюки с бэкапом)
Подобности для конкретной БД можно извлечь из RDB$TYPES.


 

RDB$EXCEPTIONS - исключения. Хотя предусмотрено хранение системных, на самом деле видны только пользовательские.

RDB$EXCEPTION_NAME - идентификатор.
RDB$EXCEPTION_NUMBER - номер.
RDB$MESSAGE - сообщение, которое получит пользователь при срабатывании.
RDB$DESCRIPTION - комментарий (всё время null)
RDB$SYSTEM_FLAG - признак системности. По документации 0 - пользовательское, не 0 - системное. Реально - везде null.

RDB$FIELD_DIMENSIONS - размерности полей массивных типов. Записи ссылаются на RDB$FIELDS, то есть сгенерированные внутренние домены для полей.

RDB$FIELD_NAME - ссылка на RDB$FIELDS. RDB$FIELD_NAME.
RDB$DIMENSION - номер измерения. Измерения нумеруются с нуля, видимо, от старших.
RDB$LOWER_BOUND - нижняя граница в данном измерении.
RDB$UPPER_BOUND - верхняя граница в данном измерении.

RDB$FIELDS - поля. Реально эта таблица описывает специальные внутренние домены, которые автоматически генерируются для каждого поля, хотя попадаются и настоящие домены. Названия самих полей и привязку к отношениям нужно искать в RDB$RELATION_FIELDS.

RDB$FIELD_NAME - имя пользовательского домена или сгенериорованное имя домена в виде RDB$nnn.
RDB$QUERY_NAME - в документации говорится, что оно не используется. Реально либо null, либо пустая строка.
RDB$VALIDATION_BLR - в документации говорится, что не используется. Вероятно, предусматривалось для какого-то проверочного кода.
RDB$VALIDATION_SOURCE - в документации говорится, что не используется. Вероятно, исходник для предыдущего поля.
RDB$COMPUTED_BLR - код для вычисления на случай, если это поле - вычисляемое.
RDB$COMPUTED_SOURCE - исходник выражения для вычисляемого поля.
RDB$DEFAULT_VALUE - значение по умолчанию для поля в двоичном виде.
RDB$DEFAULT_SOURCE - исходник значения по умолчанию для поля.
RDB$FIELD_LENGTH - содержит длинну в поля. Для большинства нестроковых типов - 8 байт, для LONG и FLOAT - по 4, для SMALLINT - 2.
RDB$FIELD_SCALE - количество знаков после запятой в числах фиксированного формата. Хранится в отрицательном виде. У остальных полей - 0.
RDB$FIELD_TYPE - тип данных поля. smallint = 7, integer = 8, quad = 9, float=10, d_float=11, char=14, double=27, date=35, varchar=37, blob=261. Подробности можно извлечь из rdb_types
RDB$FIELD_SUB_TYPE - подтип поля. Для блобов: 0 = неизвестный, 1 = текст, 2 = BLR (внутренний код interbase, результат компиляции выражений), 3 = ACL (список прав доступа), 4 = зарезервировано, 5 = закодированные метаданные таблицы, 6 = описание распределённой транзакции, завершившейся необычным образом. Для символьных полей: 0 = неизвестно, 1 = двоичные данные.
RDB$MISSING_VALUE - не используется.
RDB$MISSING_SOURCE - не используется.
RDB$DESCRIPTION - комментарий. Тоже реально не используется.
RDB$SYSTEM_FLAG - 0 = пользовательское поле. 1 = системное (по документации - не 0).
RDB$QUERY_HEADER - не используется.
RDB$SEGMENT_LENGTH - размер сегмента для блоба. Какими кусками его будут читать и писать.
RDB$EDIT_STRING - по всей видимости, какая-то строка формата редактирования. Реально не используется, везде либо null, либо пустая строка.
RDB$EXTERNAL_LENGTH - физическая длинна поля внешней таблицы, если поле в такой таблице.
RDB$EXTERNAL_SCALE - масштаб целочисленного поля внешней таблицы. Значение, взятое оттуда домножается на 10 в этой степени.
RDB$EXTERNAL_TYPE - тип данных внешнего поля. То же, что и для RDB$FIELD_TYPE выше, с дополнением: строка в формате Си (завершённая нулём) имеет код 40.
RDB$DIMENSIONS - количество измерений для поля типа массива. Для скалярных полей = 0.
RDB$NULL_FLAG - признак not null, если 1. Иначе обычно в этом поле null.
RDB$CHARACTER_LENGTH - длинна символа в байтах.
RDB$COLLATION_ID - номер сравнения символов согласно RDB$COLLATIONS.
RDB$CHARACTER_SET_ID - номер кодировки символов согласно RDB$CHARACTER_SETS.

RDB$FILES - вторичные и теневые файлы базы. Главный файл здесь не описывается. Про структуру файлов базы есть отдельный раздел.

RDB$FILE_NAME - имя файла. Между прочим, 253 символа. Видимо, если реальный путь будет длиннее, то будут проблемы.
RDB$FILE_SEQUENCE - номер файла в данной последовательности (главной или теневой).
RDB$FILE_START - начальная страница файла.
RDB$FILE_LENGTH - длинна файла в страницах.
RDB$FILE_FLAGS - какие-то флаги, зарезервировано в системных целях.
RDB$SHADOW_NUMBER - номер теневой последовательности. Файлы главной последовательности имеют значение 0 или null, теневые последовательности нумеруются положительными числами.

RDB$FILTERS - блобовые фильтры. Это такая разновидность внешних функций, которая подключается к interbase для конвертации подтипов блобов.

RDB$FUNCTION_NAME - имя фильтра в базе.
RDB$DESCRIPTION - комментарий. Наверняка, как обычно не используется.
RDB$MODULE_NAME - имя модуля (путь к нему в операционке), в котором сидит функция.
RDB$ENTRY_POINT - точка входа в модуль, которую надо вызвать.
RDB$INPUT_SUB_TYPE - подтип блоба, который функция принимает на вход.
RDB$OUTPUT_SUB_TYPE - подтип блоба, который функция выдаёт на выход.
RDB$SYSTEM_FLAG - признак системности (ненулевое значение - системный фильтр, иначе - пользовательский).

RDB$FORMATS - форматы таблиц. Фактически - сколько раз таблица альтерилась, столько записей сюда и поступает. Однако старые форматы, похоже после перебэкапов выбывают. Поскольку представления в interbase не альтерятся, речь идёт только о хранимых таблицах.

RDB$RELATION_ID - ссылка на RDB$RELATIONS, о какой таблице идёт речь.
RDB$FORMAT - номер формата.
RDB$DESCRIPTOR - описание формата. Блоб, который теоретически должен в каком-то формате описывать таблицу. Что там реально - неизвестно.

RDB$FUNCTION_ARGUMENTS - параметры внешних функций. Детализация RDB$FUNCTIONS.

RDB$FUNCTION_NAME - имя функции, ссылка на RDB$FUNCTIONS.
RDB$ARGUMENT_POSITION - номер параметра, начиная с 0.
RDB$MECHANISM - способ передачи параметров. 0 = по значению, 1 = по ссылке.
RDB$FIELD_TYPE - тип значения. Набор тот же, что и в RDB$FIELDS.
RDB$FIELD_SCALE - то же, что и в RDB$FIELDS.
RDB$FIELD_LENGTH - то же, что и в RDB$FIELDS.
RDB$FIELD_SUB_TYPE - видимо подтип блоба, как и в RDB$FIELDS, но в документации написано, что зарезервировано на будущее. То есть видать не поддерживается пока.
RDB$CHARACTER_SET_ID - кодировка символов, ссылка на RDB$CHARACTER_SETS.

RDB$FUNCTIONS - внешние функции (UDF).

RDB$FUNCTION_NAME - имя функции
RDB$FUNCTION_TYPE - зарезервировано на будущее. Реально - 0.
RDB$QUERY_NAME - альтернативное имя функции, которое можно использовать в ISQL. Реально - пустая строка.
RDB$DESCRIPTION - комментарий. Реально не используется.
RDB$MODULE_NAME - имя модуля (путь в операционке), в котором находится функция.
RDB$ENTRY_POINT - точка входа в загрузочном модуле, определённом в предыдущем поле.
RDB$RETURN_ARGUMENT - номер аргумента согласно RDB$FUNCTION_ARGUMENTS, в котором описан возвращаемый функцией результат. То есть тип результата описывается, как один из аргументов.
RDB$SYSTEM_FLAG - признак системности. Реально везде null. По документации 0 = пользовательская функция, 1 = системная.

 

RDB$GENERATORS - генераторы.

RDB$GENERATOR_NAME - имя генератора.
RDB$GENERATOR_ID - внутренний номер генератора в базе.
RDB$SYSTEM_FLAG - признак системности. По документации - значение 0=пользовательский, больше 0 - системный. Реально системные имеют знаение 1, а пользовательские - null.

RDB$INDICES - индексы. Фактически служит отправной точкой для описания ключевых и ограничений любого рода.

RDB$INDEX_NAME - имя индекса (чаще всего RDB$PRIMARY... - для первичного ключа, RDB$FOREIGN... - для внешнего)
RDB$RELATION_NAME - имя таблицы
RDB$UNIQUE_FLAG - 1, если индекс уникальный, 0 или null, если нет.
RDB$FOREIGN_KEY - имя другого индекса, который определяет поля таблицы, на которую идёт ссылка из полей этого индекса.
RDB$INDEX_ID - какой-то непонятный код, зависящий от структуры индекса. Явно не уникальный. Сказано, что всегда формируется автоматически и "руками не трогать".
RDB$DESCRIPTION - описание индекса. Поскльку операторы не предусматривают ввода описаний, то обычно здесь хранится null.
RDB$SEGMENT_COUNT - количество полей в индексе и соответствующих записей RDB$INDEX_SEGMENTS
RDB$INDEX_INACTIVE - признак неактивности индекса. 1 означает неактивный, 0 или null - активный.
RDB$INDEX_TYPE - что-то зарезервированное. В моей тестовой базе всё время null.
RDB$SYSTEM_FLAG - признак системности. Как обычно, 1=да, 0 или null = нет.
RDB$EXPRESSION_BLR - это и следующее поле видимо предназначены для обработки индексов по сложным выражениям (а не по полям). Как ими можно реально воспользоваться я не представляю. Данное поле должно представлять собой откомпилированное во внутренний код выражение, по результатам которого строится индекс. В реальных индексах обычно null.
RDB$EXPRESSION_SOURCE - исходный текст выражения, по которому строится индекс (если кто-то найдёт способ его создать). В обычных индексах всегда null.
RDB$STATISTICS - так называемая "селективность" индекса. Представляет собой вещественное число, показывающее долю значений, с которыми в таблице есть несколько записей. Подсчитывается соотношение именно значений, а не записей. То есть если из трёх значений одно представлено в нескльких экземплярах, то селективность будет 0.3333... независимо от того, сколько раз оно там продублировано. По крайней мере так показывают мои эксперименты.
По понятным причинам селективность уникальных индексов всегда должна быть равна 1. Фактически это число показывает, какова вероятность того, что при поиске записей по заданному значению мы найдём не более одной записи. Естественно, чем эта вероятность выше, тем лучше, поскольку поиск по такому индексу позволяет максимально сократить количество обрабатываемых записей.
Однако здесь таится одна важная опасность - статистика обновляется только при физическом (пере)создании индекса (alter inactive/active или backup-restore) или по оператору set statistics index имя_индекса.

RDB$INDEX_SEGMENTS - поля, входящие в индекс.

RDB$INDEX_NAME - имя индекса (фактически - ссылка на RDB$INDICES)
RDB$FIELD_NAME - имя поля
RDB$FIELD_POSITION - порядковый номер поля в пределах индекса

RDB$LOG_FILES - файлы журнала упреждающей записи (WAL), которые одно время (в эпоху версии interbase 4.X) применялись для ускорения обновлений БД под NetWare. Теперь они ушли в прошлое и данная таблица обычно пуста. Хотя для совместимости её пока поддерживают.

RDB$PAGES - представление, дающее информацию о страницах БД. Вопреки документации реально здесь отображены далеко не все страницы, а только приписанные к таблицам. Блобовые, генераторные, системные и прочие сюда не входят. Та же документация не советует обновлять это представление, поскольку можно легко запороть базу. Однако зачем тогда вообще оно сделано обновляемым?...


RDB$PAGE_NUMBER - физический номер страницы в базе.
RDB$RELATION_ID - номер таблицы, ссылка на одноимённое поле в RDB$RELATIONS.
RDB$PAGE_SEQUENCE - номер страницы в последовательности страниц данной таблицы.
RDB$PAGE_TYPE - тип страницы. Кодировку типов пока не знаю.


RDB$PROCEDURES - хранимые процедуры. Параметры описываются отдельной таблицей (ниже).


RDB$PROCEDURE_NAME - имя процедуры.
RDB$PROCEDURE_ID - внутренний номер процедуры в базе.
RDB$PROCEDURE_INPUTS - количество входных параметров процедуры.
RDB$PROCEDURE_OUTPUTS - количество выходных параметров.
RDB$DESCRIPTION - описание (как обычно, null).
RDB$PROCEDURE_SOURCE - исходный текст процедуры, начиная после слова "as".
RDB$PROCEDURE_BLR - откомпилированный внутренний код InterBase.
RDB$SECURITY_CLASS - имя класса безопасности согласно RDB$SECURITY_CLASSES.
RDB$OWNER_NAME - владелец процедуры (пользователь, который её создал).
RDB$RUNTIME - какая-та дополнительная информация, используемая для ускорения выполнения (я так думаю - компиляции или запуска) процедуры.
RDB$SYSTEM_FLAG - признак системности. Обычно 0 потому что системных процедур interbase не создаёт.

RDB$PROCEDURE_PARAMETERS - параметры хранимых процедур.


RDB$PARAMETER_NAME - имя параметра.
RDB$PROCEDURE_NAME - имя процедуры, к которой относится параметр. Фактически ссылка на предыдующую таблицу.
RDB$PARAMETER_NUMBER - номер параметра. Нумерация идёт с нуля отдельно для входных и выходных параметров в пределах процедуры.
RDB$PARAMETER_TYPE - 0=входной, 1=выходной.
RDB$FIELD_SOURCE - ссылка на RDB$FIELDS, где описывается тип данных и прочие подобные вещи.
RDB$DESCRIPTION - описание, обычно null.
RDB$SYSTEM_FLAG - признак системности. Непонятно, зачем он у отдельного параметра, как может часть параметров процедуры быть системными, а часть не быть? В действительности обычно устанавливается в null.

RDB$REF_CONSTRAINTS - дополнительная информация по ссылочным ограничениям

RDB$CONSTRAINT_NAME - имя самого ограничения
RDB$CONST_NAME_UQ - имя ограничения того ключа, на который идёт ссылка
RDB$MATCH_OPTION - тип совпадения. В 4.Х всегда 'FULL'. В пятёрке видимо должно работать 'PARTIAL'.
RDB$UPDATE_RULE - правило обновления. В 4.Х всегда 'RESTRICT'. В пятёрке возможно заработает 'CASCADE', 'SET NULL', 'SET DEFAULT''.
RDB$DELETE_RULE - правило удаления (аналогично правилу обновления)

 

RDB$RELATIONS - параметры отношений (как таблиц, так и вьюшек)

RDB$VIEW_SOURCE - исходник select'а вьюшки в виде текстового блоба или null для хранимых таблиц.
RDB$VIEW_BLR - вариант предыдущей информации, откомпилированный во внутреннее представление interbase.
RDB$DESCRIPTION - описание отношения (обычно null).
RDB$RELATION_ID -
RDB$SYSTEM_FLAG - 1 для системных таблиц
RDB$DBKEY_LENGTH - длинна внутреннего ключа для физической ссылки на запись в БД. Согласно документации для таблиц должно быть 8, а для вьюшек - 8 * количество таблиц в конструкции from.
RDB$FIELD_ID - количество полей. Обозвали странновато.
RDB$RELATION_NAME - имя отношения .
RDB$SECURITY_CLASS - класс безопасности согласно RDB$SECURITY_CLASSES.
RDB$EXTERNAL_FILE - имя внешнего файла для внешней таблицы или null для обычной.
RDB$RUNTIME - какая-то информация в двоичном виде, которую использует interbase.
RDB$EXTERNAL_DESCRIPTION - описание ко внешнему файлу. Опять же, как обычно, null.
RDB$OWNER_NAME - имя пользователя-владельца , создавшего таблицу.
RDB$FORMAT - количество версий структуры таблицы (1 при создании и на 1 больше после каждого alter table)
RDB$DEFAULT_CLASS - класс безопасности по умолчанию для вновь добавляемых к таблице полей. То есть тоже ссылка на RDB$SECURITY_CLASSES.
RDB$FLAGS - какое-то совсем странное поле, упомянутое, но не описанное в документации.

RDB$RELATION_CONSTRAINTS - общие параметры ограничений

RDB$CONSTRAINT_NAME - имя ограничения
RDB$CONSTRAINT_TYPE - тип ограничений ('FOREIGN KEY', 'PRIMARY KEY', 'NOT NULL', 'UNIQUE', 'CHECK', 'NOT NULL') . Странность состоит в том, что в пятёрке здесь упоминаются и ограничения not null, хотя не указывается, какое именно поле. Как связать это дело с RDB$RELATION_FIELDS, непонятно.
RDB$RELATION_NAME - имя таблицы, на которую наложено ограничение, то есть ссылка на RDB$RELATIONS.
RDB$DEFERRABLE - признак того, что проверку можно откладывать на конец транзакции. В настоящий момент эта возможность в interbase не поддерживается и по-этому всегда должно быть 'NO'.
RDB$INITIALLY_DEFERRED - признак того, что проверка изначально в отложеном режме. По той же самой причине всегда 'NO'.
RDB$INDEX_NAME - имя индекса для ключевых ограничений.

RDB$RELATION_FIELDS - поля отношений (таблиц и вьюшек)

RDB$FIELD_NAME - имя поля
RDB$RELATION_NAME - имя отношения
RDB$FIELD_SOURCE - ссылка на запись RDB$FIELDS с дальнейшей детализацией по этому полю.
RDB$QUERY_NAME - альтернативное имя (?) для поля, которое якобы можно использовать в ISQL.
RDB$BASE_FIELD - имя хранимого поля, из которого извлекается данное (для полей вьюшек)
RDB$EDIT_STRING - согласно документации - не используется.
RDB$QUERY_HEADER - согласно документации - не используется.
RDB$UPDATE_FLAG - согласно документации - не используется.
RDB$FIELD_ID - номер поля, под которым оно должно фигурировать в кусках кода BLR. Меняется при перебэкапе. Вообще в это дело лучше не лезть. Хотя интересно было бы найти описание структуры BLR.
RDB$VIEW_CONTEXT - номер базовой таблицы вьюшки, из которой берётся данное поле. (см. RDB$VIEW_RELATIONS ниже) .
RDB$DESCRIPTION - описание, как обычно, null.
RDB$DEFAULT_VALUE - значение default в формате BLR (результат компиляции выражения из следующего поля).
RDB$DEFAULT_SOURCE - текст выражения default
RDB$SYSTEM_FLAG - признак системности (1 = да, 0 или null = нет).
RDB$SECURITY_CLASS - класс безопасности для контроля прав над данным полем.
RDB$NULL_FLAG - 1 для полей not null
RDB$FIELD_POSITION - номер поля в списке полей отношения (начиная с 0)
RDB$COMPLEX_NAME - зарезервировано для будущего использования.
RDB$COLLATION_ID - ссылка на сравнение (RDB$COLLATIONS) для полей, содержащих текстовую информацию.

RDB$ROLES - роли. Таблица, как и вся поддержка этого механизма, существует только начиная с пятёрки и ODS 9.


RDB$ROLE_NAME - имя самой роли.
RDB$OWNER_NAME - имя пользователя-владельца роли.

RDB$SECURITY_CLASSES - классы безопасности. На эту таблицу идут ссылки их таблиц, полей, процедур и других объектов, где может понадобиться как-то ограничивать права доступа.


RDB$SECURITY_CLASS - имя класса. Обычно генерируется автоматически в формате RDB$ЧЕГО_НИБУДЬ.
RDB$ACL - собственно права доступа в виде блоба. Точный формат неизвестен, но ISQL обычно показывает в довольно удобочитаемом виде.
RDB$DESCRIPTION - описание, как обычно, null.

RDB$TRANSACTIONS - распределённые транзакции. Местные транзакции отрабатываются целиком в дебрях базы и в этой таблице не показываются.


RDB$TRANSACTION_ID - номер транзакции.
RDB$TRANSACTION_STATE - состояние. 0 = limbo (формируется), 1 = committed (зафиксирована), 2 = roolled back (отменена).
RDB$TIMESTAMP - согласно документации - зарезервировано на будущее.
RDB$TRANSACTION_DESCRIPTION - описание транзакции на случай если связь прервётся и придётся разбираться, откуда она взялась.

  

RDB$TRIGGERS - триггеры.


RDB$TRIGGER_NAME - имя триггера.
RDB$RELATION_NAME - имя таблицы или вьюшки, на которой он висит (то есть ссылка на RDB$RELATIONS).
RDB$TRIGGER_SEQUENCE - номер позиции триггера. То, что пишется после слова position в операторе create trigger. Триггеры срабатывают в порядке возрастания этих номеров. В пятёрке, если номера совпадают, то в порядке алфавитного возрастания имени, в чётверке - неизвестно.
RDB$TRIGGER_TYPE - 1 = before insert, 2 = after insert, 3 = before update, 4 = after update, 5 = before delte, 6 = after delete.
RDB$TRIGGER_SOURCE - исходный текст триггера (начиная после слова "as"). У системных триггеров может быть null.
RDB$TRIGGER_BLR - результат компиляции предыдущего поля в BLR.
RDB$DESCRIPTION - описание, как обычно, null.
RDB$TRIGGER_INACTIVE - признак неактивности триггера.
RDB$SYSTEM_FLAG - признак того, что триггер системный (1 = да, 0 или null = нет).
RDB$FLAGS - в документации рядом с этим полем оставлено пустое место.

RDB$TRIGGER_MESSAGES - малопонятная таблица с какими-то сообщениями приписанными к триггерам. Изначально заполнена ссылками на несколько системных триггеров. Как и для чего можно задействовать эти сообщения мне пока не понятно.

RDB$TRIGGER_NAME - имя триггера.
RDB$MESSAGE_NUMBER - номер сообщения.
RDB$MESSAGE - текст сообщения согласно документации. Реально - какой-то идентификатор. У записей с одинаковыми идентификаторми в этом поле так же совпадают поля номера. Что бы это значило?


RDB$TYPES - типы для перечислений кодировок, сравнений символов, типов триггеров и прочих подобных вещей. Точнее, здесь перечисляются списки допустимых числовых кодов и немного более понятные идентификаторы к ним. В общем, если данный документ в каких-то кодах расходится с вашей базой, загляните в эту таблицу - должна быть более точная информация.


RDB$FIELD_NAME - имя поля. Обычно - RDB$... типа integer или smallint.
RDB$TYPE - число, одно из возможных значений для поля.
RDB$TYPE_NAME - идентификатор для этого числа в этом поле.
RDB$DESCRIPTION - описание, как обычно, null.
RDB$SYSTEM_FLAG - признак системности, везде 1.

RDB$USER_PRIVILEGES - права пользователей на объекты БД. То, что даётся через оператор grant и отнимается через revoke.


RDB$USER - пользователь, которому даются права.
RDB$GRANTOR - от кого даются права.
RDB$PRIVILEGE - какие именно права. Хотя поле может хранить до 6 символов, как правило порождается по отдельной записи на каждое право. Сами права кодируются буквами S=select, I=insert, U=update, D=delte, X=execute, R=reference, M=member of (для ролей?).
RDB$GRANT_OPTION - есть ли право дальнейшей передачи.
RDB$RELATION_NAME - на что даются права. Может быть именем отношения, процедуры или роли.
RDB$FIELD_NAME - имя поля, если права даются не на весь объект, описанный предыдущим полем.
RDB$USER_TYPE - тип того, кто RDB$USER. Возможные значения можно выяснить через select * from rdb$types where rdb$field_name like '%OBJECT_TYPE%'.
RDB$OBJECT_TYPE - тип того, кто RDB$RELATION_NAME.

RDB$VIEW_RELATIONS - соответствие между вьюшками и их базовыми оношениями . Для каждой вьюшки заводится столько записей, сколько таблиц упомянуто во фразе from её главного select'а.

RDB$VIEW_NAME - имя представления
RDB$RELATION_NAME - имя базового отношения
RDB$VIEW_CONTEXT - номер базового отношения для данного представления
RDB$CONTEXT_NAME - укороченное имя таблицы в части from оператора select.
 

Другие материалы

Выдержки из interbase 5.5. Release Notes

Мои личные комментарии - курсивом.

Известные неисправленные глюки (Сами перечисляют, 15 штук.):

    • Не поддерживаются скобки в union. То есть нельзя написать select ... union (select ... union all select ...).
    • gpre не поддерживает конструкцию BASED_ON из ANSI C.
    • Невозможно создать foreign key, ссылающийся на первичный ключ, по которому созданы дополнительные уникальные индексы. Мы на этот глюк натыкались и он остался.
    • isc_dsql_execute() не берёт запроc типа execute procedure, если у процедуры нет входных параметров
    • Бэкап идёт очень медленно, если в недрах базы накопилось большое количество старых версий записей. Мы с Машей с этим однажды столкнулись на практике: база размером в четверть гигабайта не могла забэкапиться целую неделю!
    • Если не установлен корректный лицензионный файл и сделать попытку запустить сервер через interbaseMGR, то попытка окончится неудачей, хотя сервер всё равно запуститься и заглушить его будет невозможно (Фиг их знает, что они имеют в виду. Ведь на самом деле interbaseMGR не запускает сервер, а лишь подключается к нему)
    • Программы, состоящие из более чем одного файла, обработанного gpre не компонуются из-за продублированных идентификаторов
    • isql -extract, если его запустить на древней базе (3.x), содержащей определения на GDML, а не SQL, выдаёт смесь из конструкций GDML и SQL.
    • Клиент interbase до 4.2 включительно не воспринимает ситуацию, когда сервер на том конце разорвал соединение. К стати, утверждается, что ошибка исправлена в клиенте 5 и почему её поминают в этом разделе - непонятно.
    • Имена серверных библиотек должны обязательно указываться с расширением .DLL, если дело происходит под NT. Утверждается, что это глюк Микрософта (и мы с ним сталкивались). Любопытно, что для обхода советуют руками обновлять rdb$functions.
    • Оказывается, ошибки при create procedure, которая ссылается на несуществующий генератор, могут физически разрушить некоторые системные индексы. Рекомендуют этого избегать, а если произошло - немедленно ремонтировать базу со включённой опцией Validate Record Fragments.
    • Сервер можно обрушить запросом: "SELECT RDB$DB_KEY FROM ANY_STORED_PROCEDURE;". Красота!
    • Если забэкапить базу с доменами, но без таблиц, то она не разбэкапится (а я-то думал, что глюк с индексами в процедурах - единственный)
    • Насчёт возможных причин "Remote Interface not licensed" говорят следующее:
      • Это может быть старый gds32.dll - переустановить. Причём рекомендуют старое деинсталировать, а потом файлы руками грохнуть. Такой вот у них деинсталятор.
      • Возможно, в лицензионном файле не прописано - сами (!) предлагают прописать ID: ISC30811, certificate key: ca-9-3a-0.
      • HKEY_LOCAL_MACHINE/ Software/ Borland/ Database

 

      Engine: настроить DLLPATH.
  • Помнится, когда пытались переустанавливать то ли 4 поверх 5, то ли 5 поверх 4, вылазила ошибка про "interbasecheck". Оказывается, надо залезть в совершенно левое место реестра - HK_CURRENT_USER/Environment - и исправить PATH с двоичного типа на строковый. Двоичным его делает Install Shield, в частности тот, который ставит Delphi или JBuilder и это - его официально признанный глюк.

  


Глюки, о которых утверждается, что они официально устранены в 5.0. (Попытался подсчитать - аж в глазах зарябило: порядка 70 штук):

  • Вложенные поздапросы слишком тормозные (ой ли?)
  • Оптимизатор использует лишние индексы. То есть все, которые теоретически можно использовать в данной ситуации. Хотя реально обычно нужно лишь подмножество.
  • Почти идентичные запросы расходуют ЦПУ с разницей в 400 раз (Уууу...).
  • Статистика в ISQL выдаёт отрицательное время (сам видел)
  • Проблемы с производительностью при работе через тормозную сеть (Мы собирались проэкспериментировать с модемом. Между прочим, я подумал - модем не нужен. Просто соединить COM-порты двух машин нуль-модемом и настроить на низкую скорость, скажем, 9600 Кбит. Получим очень точную картину).
  • 3870 Запоротая база (интересно, что они имели в виду?)
  • Вьюшки на основе идентичных запросов оптимизировались по-разному (По-моему - это свидетельство планирования "от фонаря". Такому продукту точно нельзя доверять.)
  • Проблема оптимизатора (не разъясняют, какая) приводила к низкой производительности
  • Backup не восстанавливал признак Enable Forced Writes. (Точно. Поставишь gbak5, начинает восстанавливать.)
  • 5599 Проблема с производительностью вьюшек (ой ли?)
  • Синтаксический анализатор не позволяет update'ить одну таблицу из другой
  • Нужен способ указать расположение каталогов Temp и InterBase. (На самом деле всё можно обойти с помощью ссылок и переменных окружения. Просто это не было документировано.)
  • 6879 gds consistency check (differences record too long (182)) Вроде как я такое видел.
  • Плохая производительность соединений с индексами (Ха-Ха!!!)
  • Gbak обзывает всё "volume 0" (Не видел)
  • Gstat не показывает, была ли база заглушена в однопользовательский режим (Точно)
  • Большие планы не показываются (Да и маленькие порой тоже. И по-моему это глюк клиента, так как проявляется при подключении к серверу как 4, так и 5)
  • Запрос на вьюшке не использует индексы (Не всегда, но хотелось бы верить, что исправили)
  • Обломившийся клиент продолжает потреблять ЦПУ в связи с EVENT'ами (На самом деле далеко не только по этому поводу. interbase исправно дорабатывает все запросы, которые успел принять, какими бы громадными они ни были и что бы с клиентом ни произошло)
  • "GRANT ALL ON table TO user" при 95 таблицах занимает 5 минут. (Ну это мы и так знаем, как лечится)
  • Соединение вьюшек выдаёт ошибку "запоротая база" (Ой как здорово, что мы этого ещё не пробовали! И не будем.)
  • Нет предупреждения, если drop'ается поле, участвующее в сложном индексе с несколькими полями (На самом деле там много каких предупреждений недостаёт)
  • gbak обламывается, если поле числа с масштабом (видимо, имеется в виду decimal или numeric) менятся на простой Integer. (В упор не пойму, причём здесь gbak)
  • Для удалённых grant/revoke требуется лицензия "D". (Речь о текстовом лицензионом файле interbase, в котором прописаны права на различные функции и ключи на их их использование. Лицензия "D" на самом деле официально отвечает за право обновлять метаданные.)
  • Не защищена таблица rdb$user_privileges. (Я проверил и протащился - действительно любой пользователь может навставлять себе любых прав!!!)
  • gbak не восстанавливает базу, если процедуры содержат явные планы (Вот оно!!!)
  • gbak не делает ABORT, если процедуры с планами ссылаются на неактивные индексы (Вот этого я не понял. Ведь сам факт наличия плана прерывает разбэкапный процесс. Или они имели в виду именно создание бэкапа, а не восстановление? Или они понимают что-то другое под ABORT? Или имелись в виду вообще не процедуры?)
  • UDF (функции из внешней DLL) не могут возвращать блобы (не пробовал)
  • Иногда в статистике выдаётся общее время запроса меньше, чем "чистое" время процессора (Это не единственный повод подозревать эту статистику во вранье)
  • isql, извлекая структуру таблицы, забывает имя ссылочного ограничения (На самом деле, если сделать View Metadata, то всё нормально. Если сделать Extract metadata for Table, то покажет только Primary Key. А если сделать Extract Metadata for Database, то извлекается всё, но в виде отдельных alter'ов. Так что реально теряется либо всё, либо ничего. Где это они видели?)
  • Удалённый доступ через последовательные порты ненадёжен (Интересно, почему? По-моему, interbase тут вообще непричём - всё зависит от протокола.)
  • В Unix-версии серверный процесс gds_inet_server запускается даже для тех пользователей, для которых не прописаны лицензии (Видимо, имеется в виду ограничение на количество подключений. Ну и мало ли, что запускается - главное, чтобы не работал. С учётом того, как оптимизирован запуск процессов в Юниксах, это не должно быть проблемой.)
  • Производительность gbak низкая (Подробности не разъясняют. Действительно, gbak5 работает по-быстрее, но я бы не назвал это исправлением глюка)
  • Дублирование проиндексированных полей не проверяется, когда они импортируются из внешнего файла.
  • ISQL иногда вылетает на длинных запросах, содержащих ошибку
  • Сервер падает от некоторых коррелированых подзапросов, на NetWare как следствие, падает вся система. (Этот глюк известен мне ещё с института. Надо написать коррелированный подзапрос на месте поля в части select. В некоторых комбинациях сервер действительно может обвалиться. Хотя в других случаях может и пройти. Правда, выдаст всякую муру.)
  • SYSDBA не может измнять права пользователей, если перебэкапить базу из версии 3 в 4 (Ха-Ха)
  • gpre портит имена именованных транзакций (К сожалению, мы никогда не писали приложения interbase на Це. Утилита именно для этого и очень интересная.)
  • Нет сообщения от isql при попытке соединения с некорректным именем и паролем. (Видимо, имеется, в виду isql, который с командной строки)
  • Иногда руками удаляются rdb$triggers.
  • Иногда подсчёт записей distinct неверен. (Ха-Ха-Ха! InterBase считать не умеет!)
  • Distinct заставляет игнорировать order by. (Вот не замечал! Что действительно есть, так это то, что distinct без order by всё равно приводит к сортировке по всем полям подряд, начиная с первого. По всей видимости, interbase таким образом выявляет дубликаты. Но вот чтобы забыть потом это всё отсортирить по order by?... Эту ошибку помянули потом ещё раз. Даже в этом у них глюк.)
  • Привилегия References на таблицы не задокументирована (Совершенно точно. Существует в 4 версии отдельное право создавать ссылочные ограничения)
  • Не проходят гранты на группу. (Сделал Extract metadata для isc4.gdb, где хранятся пользователи и оказалось, что там в полях хранятся имена не только пользователей, но и групп, их соответствие пользователям системы (в стиле Unix: UID, GID), а так же информация о компьютерах, которая, видимо, призвана ограничить доступ с конкретных машин. Вот только как этим всем воспользоваться? То что grant'ы можно вставлять руками в метаданные, факт известный)
  • isql не анализирует количество кэш-буферов (Причём оно здесь?)
  • Ошибки с кодировками в базах перебэкапленых из 3.3 в 4.0
  • gds_lock_print -i обламывается (Имеется в виду юниксовая версия утилиты, которая печатает список текущих блокировок)
  • Из-за предварительной выборки при удалённом доступе теряются точные коды ошибок (не замечал)
  • Drop procedure делает Access Violation (Да чего с ними только не бывает)
  • Классы доступа не переносятся из 3 в 4 (Имеется в виду одно из полей в дебрях системных метаданных)
  • Исключение в триггере не предотвращает удаление записи (Класс!)
  • Коррелированный подзапрос не выдаёт правильное количество полей (На самом деле из подзапроса вообще не должны вылезать поля - это недокументированный приём, который ко всему ещё и сервер уронить может)
  • Некоторые утилиты gds_xxxx закрывают за собой только 20 файлов (На самом деле, в Unix часто так поступают. Всё равно система всё закроет. Могли бы и не причислять это к глюкам.)
  • gbak обламывается, если процедура вставляет необновляемую вьюшку (Что-то такое нам попадалось. Вообще из процедур лучше обновлять только хранимые таблицы.)
  • gpre чего-то неправильно обрабатывает в COBOL'овских программах
  • isql -x неправильно выдаёт описания доменов
  • Error: Unsuccessful metadata update: depth exceeded (recursive definition). Никогда не видел.
  • ON UPDATE SET DEFAULT feature (RI/CASCADE) не работает. (Так оказывается, другие режимы работают!!!)
  • Сообщения с длинной сообщения, но без самого сообщения, обламывают сервер (Интересно, про что это?)
  • Внешняя функция, возвращающая cstring длинной более 32752 делает GPF в interbaseserver.exe (У нас все строковые функции возвращают cstring(254), который воспринимается так же, как varchar. С учётом того, что передать на вход функции более 512 байт вообще не получалось, непонятно, откуда могут взяться такие длиннющие строки на выходе. Только в результате глюка внутри самой функции.)
  • Глюк в оптимизаторе при том же запросе с другим порядком перечисления полей (Опять эта тема ...)
  • Файл базы во владении пользователя root - не лучшее решение. (Вообще-то interbase под Unix ставится весь под пользователя root. Что и есть крайне нехорошо. Приходится потом руками восстанавливать безопасность системы.)
  • Solaris затыкается при запуске gbak. (Если это так, то это позорная проблема Соляриса. Unix не имеет права затыкаться от пользовательских утилит. По крайней мере, я в Linux и FreeBSD про такое не слышал. Если, конечно, ядра не отладочные.)
  • gper может сделать segmentation falult при обработке динамических запросов в программе на COBOL'е. (Это точно не про нас.)
  • Внешние таблицы - дыра в системе безопасности
  • Внешние таблицы некорректно бэкапятся и восстанавливаются (По-моему внешними таблицами вообще можно пользоваться только во временных целях для того, чтобы закачать в базу кучу данных)
  • Система Solaris может повиснуть при создании ссылочных ограничений (Позор джунглям!!!)
  • Нет опций для -user и -password в gsec (Ниччё не понимаю. Есть там такие параметры. И нормально работают.)
  • В программах на Це и Коболе получаются разные коды ошибок (Не всё ли равно?)
  • isql неправильно выдаёт метаданные по международному домену (Кодировку имеют в виду что ли?)
  • Differences record is too long (182) Подробности не расшифровывают
  • Временные имена полей (select выражение as имя, ...) не работают с union. (Не замечал)
  • Имя кодировки включается в значение по умолчанию для поля (А почему бы и нет, если поле строкове? В чём тут глюк?)
  • Нарушения целостности хранимых процедур могут обрушить сервер (Эка невидаль! Только вот действительно ли устаранено?)
  • Не задокументирована возможность обновлять вьюшки через триггера (Ну мало ли?)
  • gbak -o обламывается на базе со внешними функциями (Сколько помню, он всегда обламывался при работе со внешними функциями в любом режиме)
  • get_blob_segemnt, put_segemnt, и BLB_lseek вешают многопоточный сервер Мораль: не пользуйтесь многопоточными, если есть возможность.
  • select, включающий order by вешает сервер (Хоть бы пример привели, паразиты)
  • Ошибка взаимоблокировки (deadlock) в 4.2.1
  • Если клиентской функции isc_dsql_fetch() (то есть чтение очередной записи из запроса) передать в качестве параметра XSQLDA вместо ссылки NULL, то удалённый сервер повиснет (Я тащусь!)
  • "4.5 Sync err, localhost err and server dies" По-моему, это непереводимая игра слов с использованием местных идиоматических выражений.
  • isql -x не извлекает данные из баз до 4.5 (C учётом предыдущих ошибок, что он вообще правильно извлекает? И что такое 4.5: interbase или ODS?)
  • gbak спотыкается на взаимоблокировке при восстановлении
  • gpre -x не работает (Ну и фиг с ним)
  • Проблема с дропаньем таблицы в ISQL после дропанья хранимых процедур
  • "Depth exceeded error from schema file in ISQL1_05 TCS test" Ниччё не понимаю!
  • Сервер использует "interbase", что не разрешено в качестве имени пользователя на HP10 (По-моему, это глюк HP10)
  • Приложения на Коболе возвращают SQLCode 0, когда требуется -100
  • Неоднозначное обновление по позиции курсора, если есть несколько курсоров с таким именем (Это из области работы с gpre)
  • В анализаторе динамического SQL: можно обозвать курсор зарезервированным словом и этим словом больше нельзя будет пользоваться
  • Создание ограничения приводит к созданию индекса, что делает невозможным drop table/field. (Не замечал. Разве что foreign key, то тут всё понятно, оно и не должно дропаться.)
  • isql не показывает ограничение not null (По-моему - враньё)
  • Сервер падает от select, у которого в order by длинное поле varchar
  • Операции не всегда корректно наследуют привилегии процедур (Мы этим не пользуемся, хотя механизм интересный.)
  • gstat -x не показывает всех своих опций
  • "Cannot read error for isc_guard1.machine if start as interbase user"
  • Consistency check error, если грант делается перед грантом юниксовой группе (Вот кто бы рассказал, как заставить interbase работать с юниксовыми групами. Кроме как пихать их руками в ISC4.GDB)
  • Сообщения об ошибке при distinct в подзапросах (По-моему ооочень гремучая комбинация, лучше не пробовать. Если не обвалит сервер, то заткнёт производительность - точно.)
  • Ошибка при использовании select distinct на таблице с индексом по соответствующему полю в хранимой процедуре (по четвергам чётного месяца за 11 минут до полуночи при полной луне и юго-западном ветре ...)
  • Сервер не возвращает память операционной системе (Нечего было давать такому серверу)
  • Таймаут при глушении базы в однопользовательский режим с помощью gfix не работает (Зато там есть такой ключ: -immediate)
  • "ALTER TABLE ... DROP CONSTRAINT command drops interbaseserver" Клинический случай
  • Отсутствие drop generator не разъяснено в документации
  • gbak напрямую в стример не работает
  • alter table ... drop поле не проходит, когда должно
  • select с одиночным count() обваливает сервер (Не видел)
  • gpre забывает добавить END-IF в программе на Коболе
  • Индексы использовали character set ID вместо чего-то там, в результате чего в некоторых случаях вылезала ошибка not found.
     

Глюки, о которых утверждается, что они официально устранены в 5.1.1 (20 штук)

  • Добавление поля с default приводит к заполнению его нулями (в оригинальном тексте: "zeroes", то есть численными нулями) вместо значения default. (На самом деле давно известный глюк, но в данном случае тоже описан с глюком. Дело в том, что заполнение производится null'ами, а не численными нулями.)
  • Невозможно инициализировать подсистему event'ов, если в системе нет соответствующих семафоров (А куда тут денешься? Почему это глюк?)
  • Инструменты для лицензирования должны всегда создавать лицензию для пробной эксплуатации (evaluation), пользователь не должен удалять эту лицензию
  • Обновление или вставка записей в одну таблицу из другой обрушивает сервер (по-моему мне это попадалось)
  • Дата окончания пробной лицензии (evaluation) обрабатывается с нарушением Y2000 (А мы-то всё трубим, что interbase в этом отношении полностью корректен!)
  • Сервер иногда не инкрементирует ID следующей транзакции (Вот вам и надёжность! Фактически это означает слияние двух транзакций.)
  • WISQL не поддерживает механизм ролей SQL
  • SQL-скрипт повисает на grant ...
  • ... и это, оказывается, физически портит базу
  • Параметр isql по поводу ролей не работает
  • "grant роль to пользователь" не генерируется isql -e
  • Утечки памяти на сервере (По-моему в Виндах это просто неизлечимо. Даже Unix'ы этим периодически страдают.)
  • Размер серверного процесса при больших нагрузках превышает лимит системы и сервер повисает
  • Серверный процесс только растёт, не освобождая память
  • Сервер не освобождает области памяти динамического SQL при отключении клиента (Прелести многопоточности)
  • Временная взаимоблокировка при gfix -attach
  • Сервер сам завершается, когда кончается память у менеджера блорировок
  • Разнообразные утечки памяти на клиенте
  • Нет чёткого сообщения об ошибке, когда кончается память
  • Нужна поддержка IPX/SPX в клиентах Win32, чтобы подключиться к серверу NetWare. (Очевидный факт и на уровне interbase всегда обеспечивался. В чём тут глюк?)

 


Глюки, которые официально исправлены в 5.5 (52 шт)

  • Новый установочный скрипт и файл interbaseLICENSE не документированы в README
  • Справка к WISQL не обновлена
  • Сервер повисает из за глюков в менеджере блокировок
  • Вызов хранимой процедуры из InterClient'а вешает сервер (Интересно, они вообще когда-нибудь думают защищать свой протокол?)
  • Обновления, сделанные триггером не отматываются при rollback (Ни чего себе!)
  • Исключения (а при некоторых кодировках, включая win1251 - порча базы) при попытках перекодировать из Unicode_fss
  • Слишком большое количество версий метаданных портит базу, в частности, из за многих alter trigger (Надо учитывать. Правда, не говорят, при каком количестве.)
  • Большое количество grant'ов на одну таблицу портит базу
  • Невозможно загрузить gdsintl, если interbase установлен в путь, включающий точку. Обходной путь задокументирован
  • Запрос ко вьюшке, основанной на другой вьюшке, обрушивает сервер
  • gfix -attach не даёт активной транзакции завершиться
  • Многопользовательский тест допускает только 96 подключений к NT
  • Восстановление из бэкапа может не пройти, если база содержит хранимую процедуру, основанную на вьюшке
  • "WARNING: column EM_FNAME is not defined in table RDB$PAGES" (Я это видел, когда разбэкапливал базу 4 утилитой gbak 5.1. Главная странность в том, что RDB$PAGES не имеет отношения к полям.)
  • cast() возвращает неправильный результат для чисел numeric < 1
  • Улучшение документации на лицензирование 5.0
  • Преобразование numeric в char даёт ошибку, если char() слишком длинный
  • Преобразование максимального по модулю отрицательного целого в строку даёт ошибку
  • Ошибки при select'ах на внешних файлах
  • Слишком большое количество (свыше 200) alter table в скрипте приводят к сообщению "request depth exceeded"
  • isc_info_base_level врёт о версии базы
  • gbak портит принадлежность хранимых процедур пользователям
  • gbak не проходит на некоторых базах, содержащих триггера для поддержки ссылочной целостности
  • gbak портит принадлежность таблиц пользователям (По-моему, они это уже исправляли. Дежа вю?)
  • grant на хранимые процедуры ведёт себя глючно
  • rtrim() в запросе select обрушивает сервер (Здесь и далее видимо имеются в виду функции из того dll, который прилагается к версии 5 и выше. Оказывается, он ничем не лучше нашего в его начальном состоянии)
  • Ltrim(), rtrim(), lower() возвращают неправильную длинну строки char()
  • Lower() возвращает некорректные результаты
  • Функции, скомпилированные Borland C++, не возвращают память
  • Дропанье триггера дропает сервер под HP-UX
  • SYSDBA не может сделать revoke, если соответствующие привилегии были переданы пользователем дальше
  • alter trigger inactive обрушивает сервер (ни разу не видел, хотя в 5.0 может и есть)
  • Сервер под NT падает, если триггера по многу раз подряд дропаются или альтерятся
  • Внешние функции иногда обрушивают сервер (Это действительно так, особенно когда не отлажены, но такова уж природа вызова этих функций из недр interbase. Может быть имеется в виду какой-либо случай, когда функция корректна, но сервер падает. Вполне может быть, но подробностей они не разъясняют.)
  • Gbak 5.1.1 иногда не восстанавливает .gbk-файлы, сделанные в предыдущих версиях (Я пробовал восстанавливать 4.0-4.2, всё проходило без особенных неожиданностей. По крайней мере ничего такого, чтобы глючило в 5.x, но не глючило в 4.x. Наоборот - да.)
  • Скрипт ISQL обрушивает сервер (опять не разъясняют детали, конкретных примеров выше - сколько угодно)
  • Alter на процедуру, используемую другой процедурой привощит к отрубающей ошибке bugcheck (Не разъясняют, что значит "использует": то ли активный в данный момент вызов, то ли просто ссылку изнутри тела)
  • show triggers работает очень медленно из-за недоступного системного индекса (То, что у них индексов на метаданных хронически не хватает, глюк известный. А вот чтобы show triggers тормозил, я как-то не замечал.)
  • Многошаговые триггера могут не исполнить все шаги (Ого! Надо учесть.)
  • Объекты базы данные после восстановления из бэкапа принадлежат тому пользователю, под которым делалось подключение для этого восстановления (Похоже, они на этом глюке помешались. Видел сообщения о нём раз 5. Чуть ли не во всех версиях. Интересно, они его всё-таки исправили в 5.5 или в списке глюков 6.0 тоже будет такая же строчка?)
  • Невозможно сделать alter на процедуру, которая ссылается на другую процедуру, если у той изменился список параметров (Ну это мы и без них заметили)
  • Ограничения check не проверяют результаты триггеров
  • Сервер interbase обваливается, если попытаться дропнуть триггер, используемый в данный момент
  • Проблемы в мурыжере, извините, в менеджере блокировок
  • Сервер обваливается, когда проедуру дропают сразу же после вызова
  • Show grant пропускает ключевое слово Trigger (Похоже, у них совсем крыша поехала. Зачем гранты на триггера?)
  • Невенрая проверка права Execute для хранимых процедур
  • Слишком много информационных сообщений в Interbase.log (По-моему, наоборот, слишком мало. Ни в чём разобраться невозможно. Наиболее важные детали, как правило, оказываются опущенными. И повлиять на этот процесс невозможно. Весьма хреново.)
  • Неправильные путь в параметре gsec -database вешает NT Server (Это глюк обоих продуктов)
  • Офигенный рост памяти при восстановлении базы с некоторыми разновидностями взаимозависимых хранимых процедур
  • Commit на некоторых видах хранимых процедур обрушивает свервер (Во-первых, не говорят, что должно быть в процедуре. Во-вторых, не говорят, к чему Commit: к созданию процедуры или к её последующему вызову.)
  • Сервер обрушивается и/или портит базу, если более 50 пользователей одновременно мучают его тяжёлыми запросами достаточно длительный промежуток времени

Глюки, официально исправленные в 5.6

  • Утечка памяти при сортировке. Оказывается, предыдущие версии в процессе длительных пересортировок записей в рамках отработки планов не всегда возвращали системе блоки памяти, взятые под сортировку. Правда, существенных перерасходов я лично не наблюдал.
  • Попытка несколько раз создать и уничтожить одну и ту же хранимую процедуру при третьей попытке приводит к обвалу сервера. Видели мы такое, и не только с процедурами. Хотелось бы надеяться, что правда сделали.
  • Использование условий с min() в операторе delete приводило к потере данных. В чём именно заключалась потеря данных, не расшифровывают. В качестве примера приводят запрос:
    delete from foo
    where ( select min(bar) from foo );
  • Длительные запросы приводили к зависанию других пользователей до завершения работы запроса. Деталей не расшифровывают. Судя по всему, нас хотят уверить, что вылечены глюки с распределением нагрузки между внутрисервеными потоками и пользовательскими соединениями. Хотелось бы верить, да вот не верится, пока сам не увижу.
  • Попытка при определении вычисляемого поля в таблице (computed by) сослаться на процедуру (то есть поле вычислялось результатом процедуры) подвешивает сервер. На самом деле попытки комбинировать процедуры и запросы во многих случаях приводит к неприятностям. Единственные исключения, без которых, пожалуй, от них вообще не было бы толку, это вызов запроса изнутри процедуры или запрос к одной единственной процедуре. В общем мораль: мухи - отдельно, котлеты - отдельно.
  • Плохо написанный триггер или хранимая процедура могут переполнить внутренний стек сервера и повесить его. На самом деле с процедурой это действительно можно, а вот с триггером. В четвёрке максимальная вложенность вызовов триггеров составляет всего 8. Как при такой глубине может что-то переполниться?... Хотя по некоторым сведениям, в interbase 5.6 глубина триггерной рекурсии может доходить до 700 и даже 1000, то есть старое ограничение сильно ослаблено. Но всё же это не повод, чтобы злоупотреблять.
  • План запроса, сгенерированный для внешних соединений не работал в версиях 4.2.1 ... 5.5. Подробности опять не разъясняют. На самом деле там дело было в том, что очевидно работоспособные и эффективные с виду планы не воспринимались interbase. Чаще всего ругань была о неприменимости индекса. Те же планы, что генерировались автоматически, обычно исполнялись нормально. По крайней мере на моей памяти. А вообще-то хорошо бы было, чтобы в этой области навели порядок - вещь нужная.
  • Исправлены функции из стандартной библиотеки (которая поставляется с пятёркой): LTRIM(), RTRIM(), LOWER().
  • Distinct может вернуть некорректное количество строк, если отрабатывается по плану, содержащему неуникальный индекс. Приводится пример:
    select distinct customer from sales s, customer c
    where s.cust_no = c.cust_no and total_value > 10000 PLAN JOIN ( C ORDER CUSTNAMEX, S INDEX (RDB$FOREIGN25) )
    Предполагается, что CUSTNAMEX неуникальный. Причём такие планы могут генерироваться и автоматически.
    Я уже писал, что distinct обрабатывается через упорядочение. А это значит, что для его реализации могут быть использованы индексы. В пятёрке видимо. Так вот оказалось, что interbase всегда ведёт себя так, будто попавшийся индекс уникальный.
  • Оптимизатор неправильно обрабатывал пары равенств на неуникальных индексах. Это приводило к разрастанию серверного процесса и иногда к зависанию.
  • Попытка уничтожить и вновь создать используемый в данный момент триггер подвешивала сервер. Ну это уж всем известно, что метаданные править нужно только при полной отключке.
  • Использование агрегатных функций на представлении, уже содержащем агрегатные функции может подвесить сервер. От приведённого ими примера я просто протащился.
    create view vw_shipping ( orderid, lag, shipvia, summation) as 
    select o.orderid, o.shipdate - o.saledate, shipvia,
    (select sum(total) from lineitem li where li.orderid = li.orderid)
    from orders o;
  • select lag, avg(summation) from vw_shipping group by lag order by lag;
    Уж сколько раз предупреждали, что селект в селекте до добра не доводит ... И в который раз уже борладовцы обещали, что это заработает. Не верю! Не делайте так никогда!
  • select count(*) из вьюшки, содержащей соединение может подвесить сервер после третьего исполнения подряд. Уууу... А интересно, хоть что-нибудь со вьюшками можно сделать БЕЗОПАСНО?
  • Одновременный коммит кучи хранимых процедур при недостатке виртуальной памяти может обрушить сервер. Давно известная мораль: обновления метаданных должны быть автокоммитными.
  • Автоматическая сборка мусора (sweep) может обрушить сервер, если попадётся таблица с метаданными, содержащими индексы по полям, допускающим значения null и незагруженные в данный момент в память. Кто не загружен - метаданные, таблицы или индексы по их формулировке не разберёшь. Но если это правда, то глюк получается интересный. Сервер должен падать сам собой без видимых причин, причём очень редко.
  • Многократное выполнение процедуры, содержащей ссылку на UDF, без коммитов между вызовами процедуры может повесить сервер. Что-то такое мы подозревали. По крайней мере у нас в Архиве, где UDF - жизненная необходимость. Пока. Но чтобы избавиться от них придётся попотеть ...
  • Left outer join может некорректно обрабатывать значения default у полей.
  • cast() может выдавать некорректные результаты. Приводят пример:
    create table t1 (f1 integer);
    create table t2 (f1 numeric(15,1));
    insert into t2 values(1.0);
    insert into t2 values(10.0);
    insert into t2 values(100.0);
    insert into t2 values(1000.0);
    commit;
    insert into t1

    select cast(f1 as integer)
    from t2;
    commit;

    select * from t1;

    F1
    ==========
    0
    1
    10
    100
  • Подумаешь, на порядок ошиблись ...
  • Обновление таблицы с полями varchar() суммарной длинной более 32 КБ может запороть базу из-за ошибок в подсчёте внутренней длинны записи. Весьма хреновая вещь, надо сказать. Мы как-то предполагали, что уж varchar() работает корректно. То есть ограничение в 32 К надо трактовать не как ограничение на длинну одного строкового поля, а на суммарную длинну записи. Хотя опять же, эти паразиты не договаривают, какая длинна имеется в виду: объявленная в метаданных или реальная длинна строк. И с заголовками (2 или 4 байта) или "в сыром виде". Тем не менее, к границам видимо лучше не приближаться.
  • Можно объявить процедуру, которая вызывает UDF с некорректным числом аргументов, после чего такая процедура будет вешать сервер, не будет альтериться или удаляться.
  • Запросы продолжают исполняться до конца даже после того, как клиент аварийно оборвал соединение. Вообще-то эта проблема давно всех доставала и похоже, что в 5.6 ей уделили наконец-то серьёзное внимание. Из другого источника стало известно, что в указанной версии в файле interbaseCONFIG появились новые параметры. О чём писалось в разделе 'Как InterBase падает'. По умолчанию эти параметры закомментированы и по-видимому это означает отсутствие контроля. Рекомендуется поставить явные значения.
  • Запрос, у которого в условии where строковое поле сравнивается с константой длинной больше, чем у поля, может обрушить сервер. Как просто!
  • Использование компонента interbaseX для получения событий от сервера нагружает процессор на 100% при закрытии соединения NetBEUI. Мораль: давно доказано, что все нормальные пользователи подключаются через TCP/IP.
  • Динамическая выгрузка gds32.dll не освобождает все ресурсы. В общем, если вы коннектитесь через BDE несколько раз, то лучше полностью перезапускать программу.
  • Validation может выругаться на испорченные индексы, если заглушить сервис interbase при активных соединениях с клиентами.
  • Скрипты со ссылками на несуществующие внешние файлы могут обрушить сервер.
  • Gds32.linterbase больше не совместим с Borland C++ Builder. Ну дают!
  • Неудачное обновление уникального ключа может запороть базу: "Statement failed, SQLCODE = -902 internal gds software consistency check (wrong record length (183)) ".