Маленькие хитрости в Interbase

Обычно это называют FAQ или ЧАВО, однако здесь я решил поместить может и не такие частые, зато весьма нетривиальные вопросы, над которыми пришлось поломать голову.
  • Cталкивался ли кто-нибудь с ошибкой при создании индекса на строковые поля размером больше 84?
  • Как сделать так, что бы у двух пользователей, работающих с одной и той же таблицей, данные на экране обновлялись синхронно?
  • Можно ли подключить компоненты прямого доступа (например, FIBPlus) через соединение BDE ?
  • Как перестроить/деактивировать индекс для внешнего ключа ?
  • Неприятная особенность read_committed транзакции...
  • Особенность запуска Classic Server под Win2000
  • Различия в возвращаемых данных IB6 и IB4
  • Сброс/установка флага NOT NULL у поля
  • А как сбросить NOT NULL флаг в бэкапе?
  • Какую конфигурацию сервера выбрать?
  • Засада в Interbase под Linux
  • Маленькая особенность STARTING WITH
  • Можно ли сделать триггер на SELECT?
  • Как получить имена индексов для построения плана запроса?
  • Особенности работы Foreign Key
  • Как сделать, чтоб триггер не срабатывал при вставке какой-то записи?
  • Как задать тип данных для вычисляемого поля?

  • Q: Cталкивался ли кто-нибудь с ошибкой при создании индекса на строковые поля размером больше 84?

    A: Есть такое дело. Более подробно об этом и о работе с русскими буквами в IB можно узнать на www.ibase.ru. Как правило, проблема состоит в том, что требуется длинное поле и быстрый поиск по нему. Я выкрутился создав более короткое поле и индекс по этому короткому полю, (соответственно в запросе ставлю двойное условие: where name=?N1 and nameidx=?N2) :

    ALTER TABLE Wares
    ADD NAMEIDX VARCHAR(40) CHARACTER SET WIN1251;
    
    CREATE INDEX WareNameIdx ON WARES(NAMEIDX);
    
    /* Замечание от Д.Кузьменко: т.к. в этом поле все символы хранятся уже приведенными к UPPER, то COLLATE PXW_CYRL указывать не нужно, а значит, допуситмая длина поля NAMEIDX составит 252 символа */
    SET TERM ^ ;
    CREATE TRIGGER Wares_NI_idx for Wares
    before insert position 0
    AS BEGIN
      NEW.NAMEIDX='';
      NEW.NAMEIDX=UPPER(NEW.NAME);
    
    /* вообще-то IB не позволяет обрезать строки просто так, здесь возникает exception, который перехватывается следующей строкой и подавляется (NAME varchar(512) collate pxw_cyrl, Waranty - просто любое другое поле), при этом кусок строки все равно присваивается. :-) Вероятно это баг сервера, но работает на всех известных версиях - идея Prenosil */
      WHEN ANY DO New.Waranty=New.Waranty;
    END^
    
    CREATE TRIGGER Wares_NU_idx for Wares
    before update position 0
    AS BEGIN
      if ((NEW.NAMEIDX is null) or (New.Name!=Old.Name)) then BEGIN
        NEW.NAMEIDX='';
        NEW.NAMEIDX=UPPER(NEW.NAME);
        WHEN ANY DO New.Waranty=New.Waranty;
        END
    END^
    SET TERM ; ^
    

    Другая идея (если всегда требуется только точное соответствие): использовать какое-нибудь хэширование, например, MD5 или DES, но тут уж без UDF не обойтись.

    P.S. В Firebird 1.0 появилась функция SUBSTRING(<string1> FROM <start> FOR <number>), позволяющая вырезать подстроку из строки, а в 2.х добавилась еще встроенная функция LEFT(<string>,<number>), возвращающая <number> первых символов строки, в результате вышеописанный трюк с WHEN уже не нужен :)


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

    A: Это обсуждалось уже много раз и в разных конференциях. Общий вывод был такой:

    1. Можно использовать события (events). Для этого в триггерах на вставку/ изменение/ удаление записей надо поставить команду посылки события POST_EVENT "I am changed", а в программе использовать компонент для их отлова. К сожалению, события кривовато работают, самый непрятный момент - то что после завершения программы поток живет еще 7 секунд (и не спрашивайте меня - почему), результат - access violation. Один из лучших компонентов для подписки на события сервера и их обработки на клиенте, в котором решено большинство проблем - SuperIBEventAlerter, включен в поставку FIBPlus 4.4 (неофициальный сайт разработчиков: users.i.com.ua), а также компоненты TIBEvents из InterBase Express (начиная с версии IBX v4.5) базируются на этом коде.
    2. Обновлять данные автоматически в момент прихода события - неправильно (представьте, что у Вас не 2, а 202 сотрудника редактируют один датасет, при этом он достаточно объемный, собирает данные из десятка таблиц и открывается секунд 10...), хотя можно - если таблица маленькая, меняется редко, и еще хорошо бы следить, в каком состоянии сейчас датасет (dsBrowse, dsEdit, ...) чтоб пользователь не разбил компьютер при очередной потере редактируемых данных в результате обновления датасета :-)
    3. Самое лучшее - бросить на форму кнопку "Обновить", которая включается при получении события, а пользователь пусть сам решает, когда ее нажать и обновить данные.
    4. Можно переоткрывать датасет по таймеру через заданные интервалы времени, но это серьезно повышает загрузку сервера
    5. Еще вариант - http://www.ibase.ru/devinfo/ClientRefresh.htm, но если честно, мне он кажется слишком сложным. Вроде бы в IB 6.x/FB обещают сделать возможность регистрации на события по маске, тогда вместе с событием будет возможность передачи номера обновленного документа.

    Q: Можно ли подключить компоненты прямого доступа (например, FIBPlus) через соединение BDE ?

    A: Можно! Такой вопрос обычно возникает при миграции большого проекта с BDE на компоненты прямого доступа (IBX, FIBPlus, ...), когда не хочется держать два разных соединения с сервером, ресурсы которого не бесконечны.

    Делается это просто (например, для FIBPlus). Допустим, у нас есть DB1:TDatabase. Добавим на форму FibDB:TpDatabase, а в секцию USES модули BDE и IBase. После соединения компонента DB1 с базой данных (FibDB закрыт), выполняем команды:

    var h : tisc_db_handle;
    begin
     FibDB.DBName:='Cloned';
     DBTables.Check(DbiGetProp(HDBIOBJ(DB1.Handle),
                    dbNATIVEHNDL, @h, sizeof(tisc_db_handle), l));
     FibDB.Handle:=h;
    end;
    

    Все, можно работать! Данный подход не имеет побочных эффектов и позволяет растянуть "переезд" на произвольное время.


    Q: Как перестроить/деактивировать индекс для внешнего ключа ?

    A: Идея от Alex Tischenko. В одной транзакции делаем

    1)
    update rdb$indices
    set rdb$index_inactive = 3 where rdb$index_name = 'RDB$FOREIGN117'
    /* значение "3"-служебное, используется GBAK при restore */
    2)
    update rdb$indices
    set rdb$index_inactive = 0 where rdb$index_name = 'RDB$FOREIGN117'
    3)
    commit;
    И через 3-4 мин огромный индекс перестраивается !

    Неприятная особенность read_committed транзакции...

    В Interbase версий 4.0-6.х и Firebird имеют особенность (на момент написания заметки неисправленную), позволяющую при определенных условиях получить неверные данные в read_committed транзакции. Особенность состоит в том, что в read_committed транзакции версии записей проверяются по общему TIP, а не по его статичной копии. В результате если при выполнении SQL оператора какая-то из параллельных транзакций перейдет в состояние commit, то часть данных в выборке будет учитывать изменения в этой транзакции, а часть - нет.
    Например, если вставлять много записей, делая commit каждые 30000, то в другом приложении, подсчитывающем их количество через select count(*) можно получить количество записей, не кратное 30000. Проверить наличие этой особенности можно так:

    create table test (ID integer)

    и в одной задаче (app1) в read_commited транзакции:

          C:=0; P:=0;
          ibquery.sql.text:='Select count(*) from test';
          While C<500000 Do
           Begin
             ibquery1.open;
             C:=ibquery1.Fields[0].asinteger;
             if C<>P then
              begin
                P:=C; RichEdit1.Lines.Add(IntToStr(P));
              end;
             ibquery1.Close;
           end;

    а в другой (app2, в общем, не важно какая транзакция):

        j:=0; IBTransaction1.starttransaction;
        For I:=1 to 600000 do
         begin ibquery1.Params[0].asinteger:=I;
           ibquery1.execsql;
           inc(j);
           if j=30000 then
            begin j:=0;
              IBTransaction1.commit;
              IBTransaction1.starttransaction;
            end;
         end;
        IBTransaction1.commit;

    запускаем app1, app2 и ждём.

    В принципе объем можно уменьшить, у меня получилось
    5568
    30000
    60000
    90000
    116866
    120000
    134731
    ...


    Q: При установке IB4.0 (а возможно и любого classic server) под Win2000 количество коннектов по TCP ограничено цифрой в 110-120. По NetBEUI это число выше: около 170. При попытке установления следующего коннекта, у клиента выскакивает сообщение:
    "Unable to complete network request to host "server". Failed to establish a connection. System cannot find the parameter specified."
    IBREMOTE пишет в EventLog сервера:
    "Application failed to initialize properly (0xc0000142)"
    Что делать???

    A от Aleksey Karyakin: Необходимо в настройках сервиса ibremote поставить галку "Allow service to interact with desktop". Проблема в размере non-interactive desktop heap, который по умолчанию равен 512 К. Если сделать "Allow interact...", то используется interactive desktop heap, который 3М. Размеры heap-ов можно регулировать реестром, но сильно увеличивать тоже нельзя, поскольку они статические и общий объем памяти под них тоже ограничен. Подробно все описано на www.microsoft.com в:
    Q126962 Increasing the Desktop Application Heap
    Q225102 "Failed initialization on Comctl32.dll" from a Service
    Q184802 User32.dll or Kernel32.dll Fails to Initialize


    Различия в возвращаемых данных между IB5-6 и IB4

    Про отличия в обработке данных IB5 и IB6 от предыдущих версий написано много, но 2 особенности почему-то забывают, а они тем временем доставляют ряд неприятностей при разработке клиентского приложения, работающего с разными версиями IB:

  • запрос типа:
       Select Field1, (select sum(Field2) from T2 where T2.N=T1.N) from T1
    вернет разный тип второй колонки в IB 4.x (= varchar(127)) и более старших версиях (равен типу Field2).
    Для корректной работы датасета придется сделать CAST((select ...) as ...) и привести столбец к нужному типу;

  • Запрос с UNION может вернуть разные имена колонок в IB 4.x (обычно это ColumnN или F_N) и в более старших версиях, при этом IB4 полностью игнорирует предикат переименования колонки "AS".
    Тут уж ничего не поделаешь, придется подстраиваться под кривые наименования IB4: выполняем запрос в WISQL или еще какой-нибудь универсальной программе, запоминаем имена полей результата и жестко прописываем их в запросе через AS. Теперь IB5-6, честно отработав переименование, вернут ту же кривизну что и IB4. B-)
  • Еще особенность: если поле NUM - цифровое, а IDENT - текстовое и содержит русские символы, то запрос
       SELECT NUM || IDENT FROM SomeTable 
    прекрасно отработает в старших версиях, но вернет ошибку "Cannot transliterate..." в IB4. Решение: сделать первое поле текстовым. Этого можно добиться, поменяв IDENT и NUM в запросе местами, добавить в начале какой-нибудь символ:
       SELECT 'A' || NUM || IDENT FROM SomeTable 
    или просто сделав CAST(NUM as VARCHAR(...)).

    Q: Как сбросить / установить флаг NOT NULL у поля в уже заполненной базе данных?

    A: Такая проблема возникает при backup/restore когда к заполненной таблице было добавлено NOT NULL поле, тогда backup проходит нормально, а вот restore - нет. На www.ibase.ru есть специальная утилитка, позволяющая обнаружить такую ситуацию. Решается все очень просто: запрос

    update rdb$relation_fields
      set rdb$null_flag = null
    where rdb$relation_name = 'МОЯ_ТАБЛИЦА'
      and rdb$field_name = 'МОЕ_ПОЛЕ' and rdb$null_flag = 1;
    
    сбросит данный аттрибут. ('МОЯ_ТАБЛИЦА' и 'МОЕ_ПОЛЕ' должны быть набраны заглавными буквами, замечание: в третьем диалекте допускаются Case Sensitive названия)

    Q: А можно как-то сбросить NOT NULL флаг в бэкапе, если он не восстанавливается из-за этой ошибки, а исходная база не сохранилась?

    A: Вообще говоря можно, но 100% результат я не гарантирую. Все нижеизложенное вы делаете на свой страх и риск. Хотя, если манипуляции проводятся над копией бэкапа, то никакого риска и нету
    Прежде всего рекомендую заглянуть на сайт http://www.ibsurgeon.com, который предлагает услуги и утилиты для ремонта баз данных и бэкапов. Но если время не терпит, то можно рискнуть попробовать хакерский метод. Итак, делаем рестор только метаданных и анализируем его: где стоит противный NOT NULL.

    Если NOT NULL стоит в домене, то сначала в бэкапе надо найти нужный домен по его имени, а затем в HEX режиме искать последовательность
    4C 26 04 01 00 00 00 27 04 0A ...
    чтобы сбросить флаг NOT NULL из нее надо вырезать 6 байт, начиная с "26", т.е. должно остаться
    4C 27 04 0A ...

    Если NOT NULL стоит в поле, то надо по имени найти в бэкапе нужную таблицу и нужное поле В HEX режиме ищем ту же самую последовательность из 6 байт (она расположена перед началом следующего поля):
    22 04 01 00 00 00 26 04 01 00 00 00 00 04 01
    вырезаем из нее 26 04 01 00 00 00, получается
    22 04 01 00 00 00 00 04 01

    Сохраняем, пытаемся отресторить, если получилось - заполняем NULLы, ставим у поля снова NOT NULL и работаем дальше. Не получилось - увы, ищем другой метод.

    P.S. Т.к. формат бекапа меняется от версии к версии, необходимо проверять то что описано выше: берем рабочую базу, делаем бэкап метаданных, ресторим, ищем нужное поле, смотрим, где стоит NOT NULL: у поля или у домена, и снимаем NOT NULL.
    Делаем еще один бэкап, в HEX режиме ищем то, что описано выше в обоих бэкапах, сравниваем, убеждаемся что все так и есть (или все совсем не так ), далее по обстоятельствам.

    P.P.S. При ремонте одного из бэкапов получилось так, что NULL-ы попали в поля первичного ключа, и при ресторе эти записи просто выкинулись. Мне повезло: это была второстепенная табличка, где потеря записей была не критична. Однако примите к сведению.

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


    Q: Какую конфигурацию сервера выбрать?

    A: Сложный вопрос, есть много рекомендаций на этот счет, например:
    Ten Things You Can Do To Make InterBase Scream,
    Некоторые решения старых проблем,
    Как добиться максимальной скорости работы с HDD.
    И совет от Ded по поводу выбора Superverver vs Classic: "Если нужно чтоб пошустрее выполнялось много мелких однотипных запросов из разных соединений, не будет тяжелых запросов, долго и настойчиво занимающих процессор, машина однопроцессорная, допустимо падение сервера целиком из-за проблем в одном соединении с быстрым перезапуском - Супер. Если нужно, чтоб ресурсоёмкий длинный запрос не слишком тормозил остальные соединения, чтоб соединения падали по одному, машина многопроцессорная с большим ОЗУ, но всё это с несколько сниженным общим быстродействием - Классика."

    Приведу также конфигурацию своего сервера, которой я вполне доволен (активно работает около 100 юзеров, есть тяжелые запросы):

    CPU: 2x Pentium III 1000 (512kb, 133MHz)
    RAM: 4x SDRAM Reg ECC 512 Mb 133MHz
    HDD: 4x IBM 18.2Gb DDYS-T18350 обединены в RAID 5
    RAID-контроллер: AMI MegaRaid 475 (128Mb cache + baterry)
    LAN: 2x 3Com 980 вместе с коммутатором 3Com SuperStack 3 Switch 3300 объединены в канал 200 Mbit
    OS: Win2000 Advanced Server
    IB 4.0 (Classic)
    Коментарии: столь старая версия IB была выбрана только потому, что на момент установки это был единственный классик под Windows, а в дальнейшем я исходил из принципа "работает - и не трогай". Чем хорош классик - тем что завалить его целиком очень сложно, при падении отдельного клиента все остальные продолжают нормально работать. Как потом выяснилось, у классика также нормально распределяется нагрузка между двумя процессорами.
    Реально, 2Gb памяти оказалось многовато. IB выделяет и освобождает память динамически, поэтому 100 клиентов занимают в среднем около 700Mb памяти. Плюс память на OS, disk cache, т.е. 1-1.5Gb должно было хватить.
    Вторая сетевая карта также оказалась лишней: пиковая загрузка достигается только при копировании файла backup-а на другую машину, а так - не более 1 Mb/сек при средней около 200 Kb/сек.
    Удачной находкой оказалась батарейка для памяти RAID. Ее поисками я занялся после глюка UPS-a, оставившего от базы одни обломки. Теперь даже при грубом выдергивании шнура питания из сервера батарея удержит память в течение суток и при последующем включении все данные корректно запишутся на диск.

    Итак, лично мое мнение: при выборе "железа" не надо экономить на памяти и быстрой дисковой системе, остальное менее важно.


    Засада в Interbase под Linux: "караул! я могу удалить или переименовать файл работающей БД!" (по следам обсуждения на forums.demo.ru)

    Ну, на самом деле это не засада, а просто особенность файловой системы UNIX, о которой часто забывают пришедшие из DOS/Windows. Дело в том, что файловая система и система каталогов в UNIX - суть вещи разные, т.е. у файла нет имени как такового, а есть дескриптор inode, в котором хранятся параметры файла. Система имен и каталогов - совершенно отдельная структура, ссылающаяся на inode файлов. Соответственно ничто не мешает файлу иметь несколько имен (называемых hard links), эти имена совершенно равноправны, их можно добавлять и удалять, и когда их количество станет равным нулю - тогда ОС удалит файл физически. Если в этот момент файл был кем-то открыт, то удаление произойдет после его закрытия.
    Сервер Interbase при подключении нового пользователя проверяет внутренний список открытых баз, и если запрашиваемая база там есть - использует уже имеющийся дескриптор.
    Исходя из этих двух фактов получается следующее: вы можете удалить имя файла БД, при этом сервер будет нормально работать с уже "удаленной" базой, пока существует хоть один открытый коннект, при этом все новые попытки коннекта к этой базе будут направляться все на тот же "удаленный" файл. И только после отсоединения последнего пользователя база будет удалена физически.
    То же самое произойдет при переименовании файла базы: пока сохраняется хоть один открытый коннект, все новые клиенты будут направляться на старую базу. И попытки замены базы простым переименованием без проверки того, что все от нее отключились могут привести к весьма печальным последствиям.


    Маленькая особенность STARTING WITH

    Иногда требуется выбрать из таблицы строки, являющиеся подстрокой заданной строки, например, найти название города (скажем, 095) по заданному номеру телефона (0951234567). При этом не все догадываются, что обычный синтаксис CONTAINING или STARTING WITH можно вывернуть наизнанку:

    select * from Table1
      where '0951234567' containing Field1
    или
    select * from Table1
      where '0951234567' starting with Field1
    
    однако в случае со STARTING WITH есть засада, когда поле Field1 - текстовое. Например, если в isc4.gdb выполнить запрос:
    select User_Name from Users
      where 'SYSDBA123' starting with USER_NAME
    
    мы ничего не получим. Однако, если использовать CAST:
    select User_Name from Users
      where 'SYSDBA1234' starting with cast(USER_NAME as varchar(128))
    
    то все заработает, хотя USER_NAME и так имеет тип varchar(128). Наиболее вероятный источник этой загадки - неверное использование сервером индекса, т.к. в первом случае (без CAST) план запроса выглядит так:
    PLAN (USERS INDEX (USER_NAME_INDEX)),
    а во втором:
    PLAN (USERS NATURAL)

    Q: Можно ли сделать триггер на SELECT?

    A: Вообще говоря, нет. Однако есть хитрый способ, предложенный Prenosil:

    1. создать selectable хранимую процедуру, в которой можно делать что угодно - вставлять записи в лог, проверять права пользователя на чтение определенных записей и т.д.
    2. переименовать исходную таблицу
    3. создать view с именем исходной таблицы, в котором вызывается хранимая процедура, например:
      CREATE VIEW USERS (USER_NAME) AS
          SELECT *
            FROM users2
           WHERE EXISTS (SELECT * FROM log_proc(...));
    4. Разрешить пользователям обращаться только к этому VIEW, а никак не к исходной таблице.
    Можно, конечно, и сразу создать хранимую процедуру, возвращающую необходимый набор полей и записей, но тут есть свои плюсы и минусы. Пример конкретной реализации рекомендуется посмотреть у Prenosil-а.

    Q: Как получить имена индексов для построения плана запроса?

    A: Вот таким запросом

      select rdb$index_name from rdb$relation_constraints
      where rdb$constraint_type=:Ttype
      and rdb$relation_name=:Ttable
    где Ttype - тип индекса, например, 'PRIMARY KEY', Ttable - имя таблицы (uppercase), например, 'MYTABLE' (замечание: в третьем диалекте допускаются Case Sensitive названия таблиц)

    Q: При добавлении в таблицу новой записи, в которой есть Foreign Key, если запись в Master таблице с таким ключом в этот момент редактируется, я получаю отлуп:

        lock conflict on no wait transaction.
    violation of FOREIGN KEY constraint "INTEG_332" on table "MYTABLE"

    A: Это особенность реализации FK в Interbase/Firebird (на данный момент это версии 7 и 1.5 соответственно). Чтобы избежать таких ситуаций, необходимо часто редактируемые поля (например, хранящие агрегаты) вынести в отдельную таблицу и организовать связь с исходной таблицей 1-к-1. Если это невозможно по каким-то причинам, то рекомендуется использовать CachedUpdates и максимально короткие пишущие транзакции, это позволит свести такие конфликты к минимуму.


    Q: Как сделать, чтоб триггер не срабатывал при вставке какой-то записи? Сделать его inactive, а затем active в той же транзакции не получается...

    A: Менять метаданные в ходе нормальной работы - это как минимум дурной тон. Варианты:
    1) В той же таблице делается вспомогательное поле, в которое записывается какой-нибудь флаг, который анализируется триггером, а затем сбрасывается (это возможно только в BEFORE ... триггерах)
    2) Делается дополнительная таблица (в версиях Firebird, начиная с 2.5, для этой цели удобно использовать временные таблицы), в которую в той же транзакции вставляется первичный ключ записи, для которой не нужно срабатывание триггера. Триггер проверяет наличие такой записи и в конце работы эту запись из таблички удаляет (ну, или это делает другой, самый последний триггер) .


    Q: Как задать тип данных для вычисляемого поля?

    A: Обычно этого делать не требуется, т.к. тип результата определяется по используемым данным. Однако бывают накладки, например, если ваш код должен одинаково работать как в 1м, так и в 3м диалекте.
    Например, поле

        DURATION     COMPUTED BY (TIME_END-TIME_START)
    в 1м диалекте будет типа DOUBLE PRECISION, а в 3м - BIGINT.

    Чтобы этого не происходило, используйте CAST:

        DURATION     COMPUTED BY (CAST(TIME_END-TIME_START AS double precision))

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


    (c) Konstantin Beliaev, 2001-2011

  • Comments