Конвертация типов полей через COMPUTED BY

IB Database обладает интересной недокументированной возможностью. Обычно при объявлении вычисляемого поля COMPUTED BY придерживаются синтаксиса
<col_def> = col { datatype | COMPUTED [BY] (< expr>) | domain}
    [DEFAULT { literal | NULL | USER}]
    [NOT NULL] [ <col_constraint>]
    [COLLATE collation]

Как видите, синтаксис предлагает указать либо тип столбца (datatype), либо вычисляемое выражение (computed by). Указанием "или/или" является символ |. Обычно тип COMPUTED BY поля совпадает с исходным, на основе которого оно строится. Однако существует возможность явно определить конкретный тип столбца, даже не совпадающий с исходным.

Можно проделать следующий эксперимент – создать таблицу с приведенной структурой:
(определение PRIMARY KEY можно опустить, т. к. здесь оно введено лишь для удобства работы с такой таблицей из Database Explorer, т. к. BDE не позволяет обновлять таблицы, не имеющие первичного ключа)
CREATE TABLE TESTCOMP(
t_data FLOAT NOT NULL PRIMARY KEY,
c_int INTEGER computed by (t_data),
c_num NUMERIC(15, 2) computed by (t_data),
c_char CHAR(20) computed by (t_data))

Теперь попробуйте ввести в эту таблицу записи со следующим значением T_DATA: 1.88, 3.2, 3.51. Вы увидите, что поле типа FLOAT сохраняет на диске не совсем те значения, которые вы вводили. Поле C_INT содержит округленное значение T_DATA. Поле C_NUM будет содержать либо точное значение T_DATA либо округленное, в зависимости от значения параметра псевдонима BDE ENABLE BCD = TRUE/FALSE. А вот C_CHAR будет содержать более точное значение вещественного числа C_DATA.

При использовании этого трюка весьма полезным является просмотр значаний типа NUMERIC(15, 2) в строковом выражении. Дело в том, что вещественные числа не могут храниться с точностью целых, поэтому введенное 1.88 будет выглядеть в NUMERIC(15, 2) как 1.88, но на деле (в виде строки) окажется равным 1.8799999952316.

Отсюда следует несколько выводов:
  • точность вещественных чисел ограничена, и число, хранимое как вещественное, никогда нельзя сравнивать на равенство (это общеизвестное правило)
  • точность полей типа FLOAT весьма низка (эквивалент дельфийского single), поэтому вместо них лучше использовать DOUBLE PRECISION
  • не все типы преобразуются из одного в другой – поле NUMERIC(15, 2) как INTEGER COMPUTED BY... будет содержать 0.
  • при обработке вещественных чисел (округление, агрегация, сравнение, сложение/вычитание и умножение/деление) нужно учитывать погрешность. Также известное бухгалтерское правило – при умножении и делении первым выполняется умножение.
  • не следует использовать вещественные типы в качестве первичных ключей таблиц – из-за погрешности или особенности обработки вещественных чисел процессорами клиента и сервера, якобы одно и то-же число может оказаться разным, и запись по первичному ключу может оказаться "потерянной". Например запрос select * from testcomp where t_data = 1.88 может выдать пустой результат.

В Firebird 2.1 добавлена конструкция из SQL 2003, похожая на computed by:
colname [coltype] GENERATED ALWAYS AS (expression)

В этом случае, например, столбец c_char мог бы быть объявлен как
c_char char(20 generated always as (t_data)

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

Подписаться