Правильная ссылка на эту страницу
http://az-design.ru/Support/DataBase/SQL/CelkoJ/02h003.shtml

ГЛАВА 3.
Язык объявления данных

  [Мне нужны] Данные! Данные! Данные! Я не могу слепить кирпич без глины!
  Знаменитый сыщик Шерлок Холмс
  Толковые структуры данных и тупой код работают лучше, чем наоборот
  Эрик С.Реймонд

       Я полагаю, что большая часть плохих SQL-запросов является результатом неудачного дизайна схемы. Плохая схема двусмысленна, требует дополнительных усилий для извлечения данных и возвращает неверные результаты, даже если данные на входе были корректны.
       Начнем с синтаксических правил, которым надлежит следовать при работе с языком объявления данных (data declaration language, DDL), а в следующих главах поговорим о содержании и семантике DDL

Правильно размещайте значение по умолчанию

Обоснование
       Конструкция DEFAULT стоит после типа данных, а конструкция NOT NULL стоит после значения по умолчанию — такой порядок предусмотрен в стандарте SQL-92, хотя в большинстве продуктов разрешается помещать DEFAULT как после типа данных, так и после NOT NULL. Столбец, способный принимать значение NULL, может также обладать значением по умолчанию, поэтому в стандартном расположении имеется смысл. Поскольку мы хотим добиться максимальной согласованности, будем следовать стандарту. Конструкция NOT NULL применяется столь часто, что ее можно оставить на одной строке с конструкцией DEFAULT и типом данных.

Исключения
       Нет.

Тип значения по умолчанию должен совпадать с типом данных столбца

Обоснование
       Это правило кажется совершенно очевидным, но программисты ему не следуют. Сплошь да рядом попадаются столбцы с плавающей точкой, которым по умолчанию дано целочисленное нулевое значение, столбцы типа CHAR(n), в которые по умолчанию помещается строка, содержащая меньше n символов, столбцы типа TIMESTAMP со значением по умолчанию типа DATE. Конечно, в большинстве продуктов SQL к значениям по умолчанию применяется процедура неявного преобразования типа. Но зачем нужны лишние накладные расходы, если можно с самого начала все сделать правильно?

Исключения
       Нет.

Не используйте нестандартные типы данных

Обоснование
       В стандартном SQL более чем достаточно типов данных, чтобы описать практически любые сущности реального мира. Нестандартные же типы данных зачастую несовместимы не только с другими продуктами, но и с другими версиями того же продукта.
       Например, в семействе SQL Server/Sybase имеется тип данных MONEY. Он позволяет при отображении добавлять к числам разделители и обозначения валют, а также применять правила расчетов, отличные от правил для типов NUMERIC или DECIMAL Это означает, что в интерфейсе нужно разбираться с тем, правильно ли обработались разделители и символы валют, а также проверять правильность вычислений. Зачем вставлять в DDL нечто такое, что потом в интерфейсе придется переделывать?
       В языке высокого уровня, наподобие SQL, нет места и таким машинно-зависимым типам как BIT или BYTE. Язык SQL определяется вне зависимости от физической реализации. Этот базовый принцип моделирования информации называется абстрагированием данных (data abstraction).
       Биты и байты позволяют вам подобраться максимально близко к физическому представлению данных. Какого типа компьютер вы используете? Какова длина слова на нем — 8, 16, 32, 64 или 128 битов? Применяется дополнение до единицы или дополнение до двух? Как обстоит дело со значением NULL? Оно должно быть у любого типа данных, в том числе и у BIT. Но бит равен либо 0, либо 1 — третьего (NULL) не дано! Как реализованы биты в хост-языке? Знаете ли вы, что использование значений +1, +0, -0 и -1 для булевой алгебры далеко от согласованности? Это относится ко всем хост-языкам— существующим, разрабатываемым и еще непридуманным. Очевидно, что даже хороший программист не сможет написать переносимый код, опустившись до такого низкого уровня, как работа с индивидуальными битами. Уж если стандарты разрешают работать с десятичными числами, биты нам вообще не нужны!
       На практике возможны две ситуации. Биты могут применяться в качестве индивидуальных атрибутов или в качестве вектора, представляющего некий объединенный признак. Используя бит в качестве индивидуального атрибута, вы ограничиваете его двумя значениями, которые могут быть несовместимы с хост-языком или с другими реализациями SQL, неочевидны конечному пользователю и не допускают расширения.
       Векторный битовый атрибут, состоящий из цепочки значений “да/нет”, является признаком программиста, все еще мыслящего в терминах языков программирования второго и третьего поколений. Представьте себе шесть компонентов решения о предоставлении банковского займа, представленные в виде битовой модели мира второго поколения. У вас имеется 64 возможных вектора, но смысл имеют только 5 из них (нельзя, например, одновременно быть банкротом и иметь хорошую кредитную историю). Чтобы сохранить целостность данных, вы можете принять два решения.
       1. Проигнорировать проблему. На самом деле, большинство новичков так и поступает. Когда база данных превращается в ералаш без какой бы то ни было целостности, они переходят ко второму решению.
       2. Создать сложные ограничения СНЕСК() с использованием пользовательских или нестандартных библиотечных функций для работы с битами, которые заставляют забыть о переносимости кода и замедляют его работу до черепашьей скорости.
       Теперь попробуем добавить к вектору седьмое условие. С какого конца его добавить? Можно ли быть уверенными, что программа после добавления будет работать на всех мыслимых аппаратных платформах? Сумеете ли вы отследить все места, где код обращается к битовому атрибуту по его позиции в векторе?
       Приходится порядком посидеть и подумать над представлением данных высокого уровня, которое было бы достаточно общим для расширения, абстрагирования и переносимости. Должно ли представление решения о займе быть иерархическим, составным, векторным? Нужно ли предусмотреть коды для неизвестных, отсутствующих или неприменимых параметров? Разрабатывать подобные вещи непросто!

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

Размещайте объявление PRIMARY KEY в начале оператора CREATE TABLE

Обоснование
       Поставьте объявление первичного ключа в начале описания таблицы, и вы сообщите читателю важную информацию о природе таблицы и о том, как искать в ней информацию. Например, увидев в таблице “Персонал” первый столбец “ssn”, я сразу пойму, что работники идентифицируются по номеру социального страхования (social security number, SSN).

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

Располагайте столбцы в логической последовательности и объединяйте их в логические группы

Обоснование
       Считается, что в реляционной модели физический порядок столбцов в таблице роли не играет. Идентификатором столбца является его имя, а не положение. Тем не менее, в ряде случаев SQL все-таки обращается к физическому расположению столбцов. В частности, в командах SELECT * и INSERT INTO по умолчанию используется тот порядок столбцов, в котором они были объявлены. Правило расположения столбцов очевидно: логическая последовательность лучше беспорядка. Скажем, столбцы для адреса логично расположить в таком порядке: имя, улица, город, государство, почтовый индекс.

Исключения
       Может оказаться, что ваш SQL-продукт не допускает перестановки столбцов, добавленных после окончания разработки схемы. Проверьте, так ли это на самом деле.
       В некоторых случаях бывает удобно воспользоваться особенностями физического расположения столбцов в конкретной реализации SQL. Например, в DB2 для OS/2 изменения в строках постоянной длины записываются от первого измененного байта до последнего измененного байта. В строках переменной длины изменения записываются от первого измененного байта до конца строки — если длина строки действительно изменилась. Если длина строки осталась той же самой, изменения опять же записываются от первого измененного байта до последнего измененного байта. Чтобы оптимизировать работу с базой данных, администратор может:
       — поместить первыми нечасто обновляемые столбцы постоянной длины;
       — затем поместить нечасто обновляемые столбцы переменной длины;
       — последними поместить часто обновляемые столбцы;
       — поставить рядом столбцы, которые, как правило, обновляются одновременно.
       Выполнив эти рекомендации, вы до минимума сократите объем данных, записываемых в журнал изменений. Поскольку ведение журнала может очень серьезно сказываться на производительности, такая экономия будет весьма полезной. Для удобства разработчиков вы всегда вольны создать представление, в котором столбцы будут располагаться в более логически обоснованном порядке.

Выделяйте отступами ссылочные ограничения и действия

Обоснование
       Идея состоит в том, чтобы при чтении выражения CREATE TABLE все объявление столбца визуально представляло собой единый блок. В частности, размещайте конструкции ON DELETE и ON UPDATE в отдельных строках. Стандарт не требует ставить их вместе или в каком-либо определенном порядке. Чтобы не мучаться с выбором, возьмите в качестве основы алфавит и ставьте ON DELETE перед ON UPDATE, если нужны обе конструкции.

Исключения
       Нет.

Давайте имена ограничениям

Обоснование
       Имя ограничения отображается в сообщении об ошибке, когда это ограничение нарушено. С помощью имен ограничений вы сможете создавать понятные сообщения, что существенно облегчает диагностику ошибок.
       Синтаксис прост: “CONSTRAINT <имя>”. В качестве имени нужно использовать понятное описание ограничения, например:

CREATE TABLE Prizes (... award_points INTEGER DEFAULT 0 NOT NULL
    CONSTRAINT award_point_range
    CHECK (award_points BETWEEN 0 AND 100),
... );

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

Исключения
       Имена можно не присваивать ограничениям PRIMARY KEY, UNIQUE и FOREIGN KEY, поскольку при их нарушении большинство SQL-продуктов выдает понятные сообщения об ошибке. Исключение составляет Oracle.
       Без имен ограничений можно обойтись на этапе разработки. Помните, однако, что имена ограничений являются глобальными, а не локальными. В противном случае возможны были бы проблемы с выражением CREATE ASSERTION.

Размещайте проверки СНЕСК() рядом с проверяемым элементом

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

Исключения
       Если ваш SQL-продукт поддерживает выражение CREATE DOMAIN, разместите ограничения DEFAULT и СНЕСК() в описании домена, и этого будет вполне достаточно. Ограничения на несколько столбцов, описания которых стоят далеко друг от друга, сдвигайте в конец описания таблицы. Так вы всегда будете знать, где искать сложные ограничения, избавив себя от необходимости просматривать все выражение DDL. Впрочем, в какой-то степени все сказанное не играет важной роли: как правило, за сведениями об ограничениях следует обращаться к таблицам с информацией о схеме, а не к DDL. С помощью последующих выражений ALTER ограничения можно удалять или добавлять, при этом в системном каталоге будет содержаться корректное текущее состояние, а в DDL, возможно, нет.

Используйте ограничения диапазона численных значений

Обоснование
       Чаще всего в коммерческих моделях данных на численные значения накладывается ограничение: они не должны быть меньше нуля. Взгляните теперь на реальные определения DDL Как часто вам попадаются такие ограничения? Программисты ленивы и до такого уровня детализации не снисходят.

Исключения
       Когда столбец действительно может принимать любые численные значения.

Используйте для строковых значений ограничения LIKE и SIMILAR TO

Обоснование
       Формат текстовых строк можно проверять с помощью предикатов LIKE и SIMILAR TO, но такие проверки в реальных DDL попадаются нечасто. Это, конечно, не столь очевидное ограничение, как ограничение диапазона численных значений, к тому же программисты, молодость которых миновала без знакомства с UNIX, не умеют толком работать с регулярными выражениями, но все же пренебрегать возможностью этой проверки не следует.

Исключения
       Когда столбец действительно может принимать любые строковые значения.

Помните, что параметрам времени присуща длительность

       Нет такого понятия, как момент времени. Спросите об этом у Эйнштейна или древнегреческого философа Зенона, известного своими парадоксами. Помните: у параметров времени есть явная или неявная длительность, то есть время начала и время конца. В неявной модели время описывается одним столбцом, в явной — двумя.
       Например, назначая дату платежа, вы обычно подразумеваете любое время от начала этого дня и до полуночи следующего. Говоря, что человек работал такого-то числа, вы неявно имеете в виду определенный промежуток времени, скажем, восьмичасовой рабочий день.
       Помните, что к столбцу со временем применимо ограничение DEFAULT CURRENT_TIMESTAMP и что в качестве неопределенного времени окончания можно использовать значение NULL Ограничение СНЕСК() способно при необходимости округлять значения времени до начала ближайшего года, месяца, дня, часа, минуты или секунды.

Старайтесь не использовать типы данных REAL и FLOAT

       В большинстве коммерческих приложений математика с плавающей точкой не нужна. В SQL имеются гибкие типы данных NUMERIC и DECIMAL, лишенные ошибок округления, присущих числам с плавающей точкой. Исключение составляют научные и статистические данные.

Ограничения, охватывающие несколько столбцов, размещайте максимально близко к этим столбцам

Обоснование
       Не заставляйте читателя заглядывать в несколько мест, чтобы найти все столбцы, включенные в ограничение. Выделять ограничение отступами не нужно, но стоит разделить его на две строки: одну с ключевым словом CONSTRAINT и вторую с ключевым словом СНЕСК():

CREATE TABLE Prizes
    (...
     birth_date DATE NOT NULL,
     prize_date DATE NOT NULL,
CONSTRAINT over_18_to_win
     CHECK (birth_date + INTERVAL 18 YEARS >= prize_date),
  ...);

Исключения
       Это не всегда бывает возможно физически, особенно если в ограничении используется много столбцов.

Размещайте ограничения СНЕСК() табличного уровня в конце объявления таблицы

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

CREATE TABLE Prizes
(...
CONSTRAINT only_5_prizes_each_winner 
     CHECK (NOT EXISTS (SELECT *
                          FROM Prizes AS P1 
                      GROUP BY P1.contestant_id HAVING COUNT(*) > 5
                       )
           ), 
CONSTRAINT nojnissing_ticket_nbrs 
     CHECK ((SELECT MAX(ticket_nbr) - MIN(ticket_nbr) + 1
               FROM Prizes AS P1) 
          = (SELECT COUNT(ticketjibr)
               FROM Prizes AS P1)
           );

Исключения
       Нет.

Используйте для многотабличных ограничений выражение CREATE ASSERTION

Обоснование
       В SQL-продуктах эти ограничения поддерживаются не особенно широко, хотя и являются законной частью синтаксиса SQL-92. Их предикаты действуют не на одну, а сразу на несколько таблиц. Это означает, что и описываться они должны на более высоком уровне. Размещайте многотабличные ограничения СНЕСК() в выражениях CREATE ASSERTION, а не в описаниях таблиц. С практической точки зрения, все ограничения, примененные к пустой таблице, должны возвращать значение TRUE. Выражение CREATE ASSERTION позволяет задавать такое поведение. Имя утверждения ведет себя так же, как имя ограничения.

CREATE ASSERTION enough_money_to_pay_prizes
AS
CHECK ((SELECT SUM(pnze_money)
          FROM Prizes AS P1)
    <= (SELECT SUM(cash_on_hand)
          FROM Bank));

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

Используйте для каждой проверки собственное ограничение СНЕСК()

Обоснование
       Создавайте несколько простых ограничений CHECK() с собственной конструкцией для каждого, вместо написания одного сложного ограничения со множеством тестов.
       Имя, присвоенное ограничению, отображается в сообщении об ошибке, когда ограничение нарушено. Если все проверки проводятся в рамках единой конструкции CHECK(), какое имя ей дать? Представьте себе, что в одном ограничении вы одним махом проверяете правильное использование прописных букв, пробелов и длину строки почтового адреса. Можно, конечно, присвоить этому ограничению имя наподобие “ошибочный адрес” и надеяться, что пользователь сам сообразит, что сделал неправильно. Но гораздо лучше будет организовать отдельные проверки, тем самым дав пользователю более конкретное указание на ошибку.

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

Если у таблицы нет ключа, это не таблица

Обоснование
       Здесь мы подбираемся к самой сути таблицы. Беда в том, что многие новички вообще не понимают, что такое ключ. Ключ должен состоять из подмножества атрибутов (столбцов) таблицы. Нет и не может быть универсального “безразмерного” ключа. Поскольку не существует двух одинаковых наборов сущностей, уникальные атрибуты для них должны подбираться на индивидуальной основе. Господь, к несчастью, не присвоил каждой созданной им вещи 17-буквенный код на иврите.
       В табл. 3.1 приводится классификация ключей.

Табл. 3.1. Типы ключей

  Естественный ключ Искусственный ключ Явный физический указатель Системный суррогатный ключ
Строится по реальной модели данных Да Нет Нет Нет
Проверяем по реальной модели Да Нет, исходит из надежного источника Нет Нет
Проверяем сам по себе Да Да, например, по синтаксису, по контрольному разряду Нет Нет
Переносим на другую платформу Да Да Нет Нет
Доступен пользователю Да Да Да Нет, может быть изменен системой

       1. Естественный ключ представляет собой набор атрибутов из таблицы и действует как уникальный идентификатор. Он виден пользователю.Его можно проверить на корректность как по реальной модели данных,так и сам по себе. Пример, универсальный код продукта UPC можно прочитать на упаковке, проверить сканером, сравнить с данными на Web-узле производителя.
       2. Искусственный ключ представляет собой дополнительный атрибут, специально введенный в таблицу для использования в качестве ключа. Он виден пользователю, не связан напрямую с реальной моделью данных, но может быть проверен сам по себе — по синтаксису, по контрольному разряду. Пример: свободные коды из схемы UPC, которые пользователь может присвоить своему продукту. Корректность кодов можно проверять только в пределах вашей организации. Если вы занимаетесь разработкой ключа самостоятельно, помните, что эго дело не из легких. Подробнее об этом — в главе 5.
       3. Явный физический указатель не основан на модели данных и доступен пользователю. Предсказать или проверить его значение нельзя. Система вычисляет его, основываясь на физическом хранении данных. Пример: поля IDENTITY в семействе T-SQL, другие нестандартные нереляционные средства автонумерации, указатели, основанные на номерах цилиндра и дорожки жесткого диска в Oracle. Технически, это вообще не ключи, так как к логической модели данных они не имеют никакого отношения. Удобны для ленивых, не желающих думать программистов-“чайников”. Наихудший способ программирования на SQL
       4. Системный суррогатный ключ генерируется системой для замещения реального ключа на основе атрибутов из таблицы; пользователю недоступен. Пример: алгоритмы хэширования Teradata. Определяющее значение имеет тот факт, что пользователь ни при каких обстоятельствах не видит суррогатный ключ, не может использовать его в командах DELETE и UPDATE или создать командой INSERT. Если бы он мог это сделать, то немедленно повредил бы целостность данных, нарушив соответствие между реальными и суррогатными ключами. Суррогатные ключи поддерживаются системой.
       Обратите внимание, что суррогатные ключи иногда путают с физическими указателями; на самом же деле они концептуально различны.

Автонумерация не может использоваться в качестве реляционного ключа

Обоснование
       Средства автонумерации представляют собой пережиток ранних версий SQL, предназначенных для систем с последовательным хранением данных — на физически смежных страницах, строках и столбцах, как в стопке перфокарт или на магнитной ленте. У большинства программистов с той поры ментальная модель данных не изменилась.
       Но физически последовательная запись представляет собой лишь один из способов хранения реляционной БД, причем, далеко не лучший. Основная идея реляционной БД заключается в том, что пользователь вообще ничего не должен знать о том, как хранятся данные, не говоря уже о том, как писать код, зависящий от конкретного физического представления в конкретной версии конкретного продукта.
       Первое практическое соображение таково: автонумерация является нестандартной функцией, поэтому ждите проблем при переходе на другую версию того же продукта или на другой продукт. Впрочем, новички искренне полагают, что переносить код им никогда не придется! То ли они работают на умирающую компанию, то ли считают свой код безнадежным и не верят, что он может пригодиться кому-то еще...
       Но давайте обратимся к логическим проблемам. Во-первых, попробуйте создать таблицу с двумя столбцами. Можно ли задать для обоих автонумерацию? Если тип данных нельзя присвоить более чем одному столбцу, то это по определению вообще не тип данных. Это свойство физической таблицы, а не данных в ней.
       Теперь создайте таблицу с одним столбцом и задайте для него автоиумерацию. Можно ли вставлять, изменять и удалять числа из этого столбца? Если строки нельзя вставлять, изменять и удалять, то это по определению не таблица.
       Наконец, создайте простую таблицу с одним скрытым столбцом автонумерации и несколькими другими столбцами. Используйте операторы наподобие:

INSERT INTO Foobar (a, b, с) VALUES ('a1', 'b1', 'c1');
INSERT INTO Foobar (a, b, с) VALUES ('a2', 'b2', 'c2');
INSERT INTO Foobar (a, b, с) VALUES ('a3', 'b3', 'c3'); 

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

INSERT INTO Foobar (a, b, с) 
SELECT x, у, z FROM Floob;

       Результатом выполнения запроса является таблица, то есть неупорядоченный набор данных. Какова должна быть ее автонумерация? Весь набор вставляется в таблицу Foobar одновременно, а не по одной строке за раз. Имеется n! способов пронумеровать n строк, на каком вы остановитесь? В реальности используется тот физический порядок, в котором оказались записи. Опять этот нереляционный “физический порядок”!
       В действительности все еще хуже. Если тот же самый запрос выполнить еще раз, но с новой статистикой или после удаления или добавления индекса, в результате нового плана выполнения набор записей может быть возвращен в другом физическом порядке. Как с точки зрения физической модели объяснить, что тем же самым строкам во втором запросе присвоены другие автоматические номера? В реляционной модели строки, содержащие одинаковый набор атрибутов, должны обрабатываться одинаково.
       Использование автонумерации в качестве первичного ключа является признаком отсутствия модели данных. После какого-либо сбоя такую базу данных, скорее всего, придется воссоздавать с нуля.
       Вот так создают таблицы с данными о водителях неопытные новички:

CREATE Drivers
    (driver_id     AUTONUMBER NOT NULL PRIMARY KEY,
     ssn           CHAR(9) NOT NULL REFERENCES Personnel(ssn),
     vin           CHAR(17) NOT NULL REFERENCES Motorpool(vin));

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

CREATE Drivers
    (ssn    CHAR(9)  NOT NULL REFERENCES Personnel(ssn),
     vin    CHAR(17) NOT NULL REFERENCES Motorpool(vin),
     PRIMARY KEY (ssn, vin));

       Другая проблема возникает, когда естественный ключ существует (а в корректной модели данных он есть обязательно). В этом случае столбцы можно обновлять как по ключу, так и по автоматическому номеру. Поскольку нет способа связать ключ с автономером, не будет и целостности данных.
       Чтобы убедиться в этом, рассмотрим типичную для новичков схему. Я таких называю “id-иотами”, поскольку у них в таблицах обязательно есть столбец “id” с автонумерацией:

CREATE TABLE Personnel
   (id    AUTONUMBER NOT NULL PRIMARY KEY,-неправильный ключ 
    ssn   CHAR(9) NOT NULL,-настоящий ключ
..);
INSERT INTO Personnel VALUES ('999999999*, ..);

       Изменим строку в таблице Personnel, ориентируясь на значение столбца “id”:

UPDATE Personnel

SET ssn = '666666666' WHERE id = 1;

       или на значение естественного ключа:

UPDATE Personnel
SET ssn = '666666666' WHERE ssn = '999999999';

       Теперь удалим строку и создадим ее заново:

BEGIN ATOMIC
DELETE FROM Personnel WHERE id = 1;
INSERT INTO Personnel VALUES ('666666666', ..);
END;

       Что случилось при этом с таблицами, которые ссылались на таблицу Personnel? Представьте себе таблицу с данными о футбольной команде компании, в которой также есть столбцы “id” и “ssn”. При изменении номера социального страхования необходимо каскадное DRI-изменение, но на основе столбца “id” я этого сделать не смогу и потому не имею представления, сколько разных значений SSN будет записано в БД для этого работника. Столбец “id” в лучшем случае не нужен, в худшем же попросту опасен.
       Наконец, обратимся к авторитетам и процитируем Кодда (1979): “Есть три трудности, возникающие при идентификации элементов базы данных с помощью ключей, к которым имеет доступ пользователь.
       1. Реальные значения пользовательских ключей определяются пользователями и могут ими изменяться. Допустим, при слиянии двух компаний объединяются две базы данных о работниках. В результате менять придется некоторые или все табельные номера работников.
       2. В двух отношениях могут использоваться различные пользовательские ключи (например, один, основанный на номере социального страхования, и другой, основанный на табельном номере работника), хотя оба они указывают на один и тот же элемент данных.
       3. Иногда возникает необходимость в поиске информации об элементе базы данных, которому значение пользовательского ключа еще неприсвоено или у которого оно уже отсутствует, например, о соискателе, который еще не принят на работу, или о сотруднике, ушедшем на пенсию.
       Из этих трудностей вытекает важное следствие: объединение по общему ключу может дать результаты, отличные от объединения по общему элементу. Возможное решение состоит в организации доменов, содержащих назначенные системой суррогатные ключи. Пользователи БД могут заставить систему создать или удалить суррогатный ключ, но изменять его значение они не могут, да оно им и неизвестно. Это означает, что суррогатный ключ работает подобно индексу: он создается пользователем, но управляется системой и пользователю никогда не показывается. То есть, не используется в запросах, DRI — ни в чем, что делает пользователь”.

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

Файлы — не таблицы

       Если попытаться выразить эту мысль максимально просто, речь идет о различии между физическим и логическим представлением данных, хотя на самом деле все сложнее. Файловая система — это просто собрание файлов, далеко не все из которых реально нужны. База данных — это единый блок информации, в котором целое предприятие представлено в виде таблиц, ограничений и пр.
       Файлы независимы друг от друга, а таблицы базы данных взаимосвязаны. Вы открываете БД целиком, а не отдельные таблицы в ней, тогда как файлы открываются по отдельности. Действие в отношении одного файла не затрагивает другие файлы, таблицы же взаимодействуют друг с другом посредством DRI, процедур-триггеров и т.д.
       Основополагающая идея БД — собрать информацию таким образом, чтобы избежать избыточного хранения данных во многих файлах и обеспечить независимость от языка программирования.
       Файл состоит из записей, записи — из полей. Записи в файле упорядочены, доступ к ним может осуществляться по физическому расположению; в отношении таблицы это неверно. Говоря о файле, можно говорить “первая запись”, “последняя запись”, “следующие п записей”, в таблице же понятие первой и последней строки отсутствует.
       Файл обычно привязан к определенному языку — попробуйте прочитать файл, созданный с помощью Фортрана, программой на Коболе. БД не привязана к конкретному языку; внутренние типы данных SQL преобразуются в типы данных хост-языка.
       Поле существует лишь постольку, поскольку существует читающая его программа; столбец существует, поскольку он включен в таблицу базы данных. Столбец независим от хост-языка приложения, которое будет к нему обращаться.
       В процедурном языке оператор “READ a, b, с FROM FileX;” не приведет к тому же результату, что оператор “READ b, с, a FROM FileX;”. Выполнив оператор “READ a, a, a FROM FileX;”, вы дважды перезапишете значение локальной переменной. В SQL оператор “SELECT a, b, с FROM TableX” вернет те же данные, что и оператор “SELECT b, с, a FROM TableX”, поскольку данные идентифицируются по имени, а не по положению.
       Поле может иметь постоянную или переменную длину, тип данных поля может меняться (union в Си, VARIANT в Паскале, REDEFINES в Коболе, EQUIVALENCE в Фортране).
       Столбец — это скалярное значение, извлеченное из единого домена (домен = тип данных + ограничения + отношения) и представляемое одним и только одним типом данных. Вы не обязаны иметь ни малейшего представления о внутреннем физическом представлении столбца, поскольку никогда с ним не сталкиваетесь.
       Рассмотрим типы данных, связанные со временем: в SQL Server данные типа DATETIME (так в этом продукте назван тип TIMESTAMP) представляются двоичным числом (представление времени, принятое в UNIX), а в DB2 тип данных TIMESTAMP представляется строкой цифр (представление, принятое в Коболе). Но вы не должны об этом беспокоиться, заботясь о самих данных, а не об их физическом представлении.
       У полей нет ограничений, нет отношений, нет типа данных; каждое приложение назначает эти параметры самостоятельно, и они необязательно совпадают! Отсутствие контроля за целостностью данных было одной из причин появления реляционных БД. У строк и столбцов есть ограничения. Записи и поля могут содержать все, что угодно, что зачастую и происходит! Поговорите об этом с любым программистом, который когда-либо занимался организацией большого массива данных. Мне особенно нравится, когда в электронной складской ведомости в поле для номера изделия я вижу слова “Ненавижу эту работу”.
       Кодд (1979) определил строку как представление простого одиночного факта. Запись обычно представляет собой комбинацию множества фактов. Иными словами, размер файла не нормирован; вы просто запихиваете в него все новые данные в расчете на то, что соберете в нем все необходимое. Когда системе нужны новые данные, вы добавляете в конец записи новые поля. Так и возникают записи, размер которых исчисляется килобайтами.

Подбирайте ключи с подходящими свойствами

Обоснование
       Составление списка желаемых свойств ключа — хороший способ проверить дизайн данных.
       1. Уникальность первое и наиважнейшее свойство ключа. Без уникальности ключ по определению не ключ. Впрочем, это свойство необходимое, но не достаточное.
       Уникальность имеет контекст. Идентификатор может быть уникальным в пределах одной БД, в пределах всех БД предприятия, уникальным вообще. Предпочтителен, конечно, третий вариант.
       В промышленности добиться универсальной уникальности просто, например при помощи стандартного кода типа идентификационного номера автомобиля (vehicle identification number, VIN). Уникальность в пределах предприятия обеспечивают коды, подобные местному телефонному номеру или адресу электронной почты. Идентификатор, уникальный в пределах только одной базы данных, особого смысла не имеет, поскольку лишен остальных желаемых свойств.
       2. Неизменность второе свойство. Первый тип неизменности — неизменность в пределах схемы. Она необходима как для ключевых, так идля простых столбцов. Одному и тому же элементу данных необходимо одно и то же представление, где бы в схеме он ни появлялся. Он не должен относиться к типу CHAR(n) в одном месте и к типу INTEGER вдругом. К нему должен применяться тот же базовый набор ограничений. Иными словами, если мы используем в качестве идентификатора VIN и наложили на него ограничение, что допустимы только VIN автомобилей “Форд”, то нельзя использовать это ограничение в одной таблице и пренебречь им во всех остальных.
       Второй тип неизменности — неизменность во времени. Ключи не должны меняться слишком часто или непредсказуемо. Вопреки распространенному мифу это не означает, что ключи вообще не могут меняться. По мере того как расширяется область их применения, они должны под нее подстраиваться.
       К примеру, 1 января 2005 г. США добавили еще одну цифру к штрих-коду UPC, применяемому в розничной торговле. Причинами стали глобализация, снижение уровня американского промышленного превосходства и распространение Европейского артикула (European Article Number, EAN). Тринадцатиразрядным стал и “книжный” код ISBN.
       3. Понятность весьма удобно, если пользователь что-то знает о данных. Это еще не контроль корректности, но уже близко к нему. Контроль предполагает, что корректность ключа можно проверить с помощью некоего процесса. Понятность означает, что о корректности кода можно что-то сказать навскидку, поскольку вы осведомлены о контексте. Скажем, кодовое обозначение заболевания ничего не скажет пациенту, но будет вполне понятно медицинскому работнику.
       4. Контроль корректности предполагает, что корректность ключа можно проверить без обращения к внешнему источнику. Например, я знаю,что дата 30.02.2004 г. невозможна, поскольку в общепринятом календаре 30 февраля отсутствует. Контрольные разряды и коды фиксированного формата представляют собой два способа проверки корректности.
       5. Проверяемость ключа зависит от контекста и уровня доверия. Когда я расплачиваюсь чеком в супермаркете, кассир с готовностью верит, что фотокарточка на водительских правах принадлежит мне, какой бы неудачной она ни оказалась. Но ситуация может измениться: в сети супермаркетов “Kroger” вводится сейчас система сканирования отпечатков пальцев, наподобие той, что уже действует во многих банках. Чтобы получить паспорт, я должен предъявить свидетельство о рождениии пройти процедуру снятия отпечатков пальцев. Тут уровень доверия ниже. Перед предоставлением допуска к секретной информации человека проверяют с особенной тщательностью — уровень доверия существенно ниже.
       Ключ без возможности проверки нарушает целостность данных и в конечном итоге ведет к накоплению некачественной информации.
       6. Простота. Ключ должен быть максимально простым, но не более того. Чем длиннее ключ, тем больше с ним будет связано ошибок. Правда, хранение и передача длинных ключей уже не представляют такой проблемы, как это было 40 или 50 лет назад.
       То, что просто для одного человека, сложно для другого. В качестве примера чрезмерно сложного кода, находящегося в международном применении, можно рассмотреть международный стандартный номер банка (International Standard Bank Number, IBAN). Способ обработки номера IBAN определяется кодом страны в начале строки, которая может содержать до 34 символов (букв и цифр). Почему? Потому что у каждой страны — свои законы, валюта, способ нумерации банковских счетов... По сути, IBAN представляет собой национальный банковский код, спрятанный внутри международного стандарта (см. http://www.ecbs.org/iban/iban.htm).
       В наше время все больше становится программистов, которым приходится разрабатывать базы данных, не имея ни малейшего опыта работы с ними. Не зная ничего другого, они старательно имитируют номер записи (пережиток последовательной файловой системы) или идентификатор объекта (последствия знакомства с ООП) при помощи IDENTITY, ROWID и других нестандартных средств автонумерации в SQL-продуктах. Эта магическая, универсальная, безразмерная методика абсолютно не подходит для реляционных БД, зависит от текущего физического состояния оборудования и по сути представляет собой неудачную попытку возрождения магнитной ленты. Опытные дизайнеры БД предпочитают продуманные ключи, основанные на стандартных кодах UPC, VIN, ISBN и т.д. Им известно, что данные необходимо непрерывно поверять реальностью. Проверенный внешний источник для этого незаменим.
       Оправданий для халтурного программирования придумано много, приведем основные в виде вопросов и ответов.

Вопрос: Разве естественный составной ключ не может стать очень длинным?
       Ответ №1 Ну и что? Размер ключа имел определяющее значение в 1950-х годах, когда в нашем распоряжении были маломощные компьютеры. Но теперь на дворе XXI век! Меня, кстати, всегда забавляет количество идиотов, которые заменяют составной ключ, состоящий из двух-трех целых чисел, на громоздкий код GUID, который не будет понят ни человеком, ни другим компьютером, на том основании, что так легче программировать.
       Ответ №2 Эту проблему можно разрешить с помощью подходящей реализации SQL. Например, SQL-продукт компании Teradata предназначен для работы с очень большими БД, и в нем вместо обычных индексов активно применяется хэширование. Фирма гарантирует, что для любого поиска понадобится не более двух просмотров, вне зависимости от размера БД. При использовании древовидного индекса количество просмотров увеличивается по мере роста БД.
       Ответ №3 Длинный ключ не всегда отрицательно сказывается на производительности. Я могу, например, с помощью составного ключа получить индекс, включающий все столбцы, необходимые для запроса, так что для выполнения запроса обращение к основной таблице вообще не понадобится.

Вопрос. Разве плохо, что в текущей версии SQL-продукта мое приложение будет работать с максимальной скоростью?
       Ответ №1 Я бы, конечно, тоже к этому стремился, если бы хотел потерять все преимущества абстрактной модели данных, копить ненужную информацию и распрощаться с переносимостью кода. Почитайте тематические группы новостей, и вы узнаете, сколько трудностей порождается использованием физических указателей даже в пределах одного продукта.

Не разделяйте атрибуты

Обоснование
       Разделение атрибута означает, что вы моделируете один и тот же атрибут в нескольких местах схемы, нарушая правила нормальной формы “доменключ” (Domain-key Normal Form, DKNF) и существенно усложняя программирование. Разделить атрибут можно несколькими способами, описанными в следующих разделах.

Разделение по таблицам

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

Разделение по столбцам

       Атрибут моделируется в виде набора столбцов, имеющих смысл лишь совместно (например, в одном столбце стоит число, а в другом — единица измерения). Выход состоит в том, чтобы все числовые данные записывать в едином заранее выбранном масштабе.
       Чаще всего подобная неприятность случается с типом данных BIT (см. раздел “Не используйте нестандартные типы данных”). Встретятся вам и попытки форматирования длинных текстовых строк путем разделения, например, одного 100-символьного столбца на два 50-символьных. Обычно таким образом пытаются избавиться от необходимости автоматического разбиения на строки при отображении строки в интерфейсе. Но что вы будете делать, если вам придется печатать тот же текст на устройстве с длиной строки в 25 символов?
       Другой распространенный вариант — программировать в таблице динамические изменения домена. При этом один столбец содержит домен (то есть, метаданные) для другого столбца (с данными).
       Вот какой показательный пример динамического изменения домена опубликовал в одной из групп новостей по SQL-программированию Гленн Карр (Glenn Carr). Он намеревался вести статистику футбольных матчей; здесь его схема приводится в упрощенном виде. Я убрал из нее около десятка других дизайнерских ошибок, чтобы мы могли сосредоточиться на проблеме динамической смены доменов:

CREATE TABLE Player_Stats 
   (league_id      INTEGER NOT NULL,
    player_id      INTEGER NOT NULL,  -- внутренняя нумерация игроков
    game_id        INTEGER NOT NULL,
    stat_field_id  CHAR(20) NOT NULL, -- домен для столбца number_value
    number_value   INTEGER NULL,

       Столбец stat_field_id содержит имя статистики (количество пробежек, число преодоленных ярдов, количество перехватов), значение которого размещено в той же строке, в столбце number_value. Перепишем этот фрагмент:

CREATE TABLE Player_Stats
   (league_id   INTEGER NOT NULL,
    player_nbr  INTEGER NOT NULL,
FOREIGN KEY (league_id, player_nbr) REFERENCES Players (league_id, player_nbr) ON UPDATE CASCADE,
    game_id     INTEGER NOT NULL REFERENCES Games(game_id) ON UPDATE CASCADE,
    completions INTEGER DEFAULT 0 NOT NULL CHECK (completions >= 0),
    yards       INTEGER DEFAULT 0 NOT NULL CHECK (yards >= 0), -- список статистик можно продолжить
PRIMARY KEY (league_id, player_nbr, game_id));

       Проверка показывает, что игрок идентифицируется парой параметров (league_id, player_nbr). В исходной таблице Players для идентификации игроков использовался столбец с автонумерацией Player_id типа IDENTITY. Но ведь на форме у каждого игрока есть номер; давайте использовать для идентификации его! Конечно, тут возможна проблема повторного использования номера другим игроком, но я уверен, что в футбольных лигах это делается по определенным правилам, и уж конечно это не те правила автонумерации, что задаются устройствами в компьютере мистера Карра.
       В переписанной схеме тривиальным станет вычисление составных статистик, например средней длины пробежки в ярдах. Самой сложной частью кода станет предотвращение деления на ноль. В оригинальном дизайне ту же задачу пришлось бы решать с помощью ресурсоемкого и сложного набора объединений таблицы с самой собой. Оставляю это упражнение читателю.

Исключения
       Это не совсем исключение. Можно использовать столбец для изменения единицы измерения (не домена), относящейся к другому столбцу. Допустим, я записываю температуры в градусах Кельвина, Цельсия или Фаренгейта, помещая в соседнем столбце стандартное сокращение единицы измерения. Но мне необходимо отдельное представление для каждой шкалы, чтобы все температуры выражались в градусах Фаренгейта для американцев и в стоградусной шкале для остального мира. Я также хочу, чтобы пользователи могли через представления обновлять температуры в тех единицах, которые им удобны.
       Более сложный пример: хранение в базе данных по международным транзакциям денежных сумм и ISO-кодов валют. Домен неизменен; во втором столбце всегда содержится код валюты, а не размер обуви или температура. Необходимо представление, которое переводило бы любую валюту в единую шкалу: евро, йены, доллары или любую другую. Здесь появляется еще зависимость от времени, поскольку курсы обмена все время меняются.

Разделение по строкам

       Атрибут моделируется как набор названий параметров и их значений, размещенных в отдельных строках. Классический пример — параметры времени в списках событий:

CREATE TABLE Events 
   (event_name CHAR(15) NOT NULL,
    event.time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT MULL,
  ...);
INSERT INTO Events
VALUES (('start running1,'2005-10-01 12:00:00'),
        ('stop running', '2005-10-01 12:15:13'));

Время — это длительность, а не мгновение. Вот как выглядит правильный DDL -

CREATE TABLE Events 
   (event_name          CHAR(15) NOT NULL,
    event_start_time    TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    event_finish_time   TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
  CHECK (event_start_time < event_finish_time),
    ..);
INSERT INTO Events
VALUES ('running', '2005-10-01 12:00:00', '2005-10-01 12:15:13');

Исключения
       Нет.

       Приведенные выше примеры представляют собой просто плохие схемы, возникающие, как правило, в результате невнятного представления данных в виде логической модели. Это случается с программистами старой школы, не избавившимися от прежних привычек. В те давние времена каждая магнитная лента помечалась временем записи, и их обработка основывалась на однозначном соответствии между временем и физическим файлом. Таблицы с именами типа “Зарплата_Январь”, “Зарплата_Февраль” просто имитируют магнитные ленты.
       Другой источник ошибок — попытка продублировать в DDL структуру бланка или диалогового окна. Часто оказывается, что в таблицу с информацией о заказе включен номер строки, просто потому что он есть на бумажном бланке заказа. Понятно, что в складской описи заказанный товар идентифицируется по артикулу, коду UPC или другому коду, но никак не по номеру строки на бланке.

Не применяйте в реляционной БД объектно-ориентированный дизайн

Обоснование
       Много лет назад в городе Рапид-Сити (Южная Дакота) состоялось совещание комитета по стандартам БД INCITS H2 (известного так же, как комитет ANSI ХЗН2). Двумя достопримечательностями совещания были гора Рашмор и Бьерн Страуструп (Bjarne Stroustrup). Г-н Страуструп сделал доклад про то, как в Bell Labs специально для нас разрабатывают язык C++ и объектно-ориентированное программирование, а потом мы перешли к вопросам.
       Один из вопросов заключался в том, как мы должны использовать объектно-ориентированное программирование в SQL Он ответил, что фирма Bell Labs испытала четыре различных подхода к этой проблеме и — при всех ее талантах — пришла к выводу, что делать этого не следует. Объектно-ориентированный подход хорош для программирования, но смертелен для данных.

Таблица не является экземпляром объекта

       В правильно разработанной схеме таблицы не появляются и не исчезают, подобно экземплярам объекта. Таблица представляет собой набор сущностей, или отношение. Возможность их появления (CREATE TABLE) и исчезновения (DROP TABLE) словно перемещает нас в волшебный мир, в котором новые сущности создаются мановением руки любого пользователя. Точно так же нет в SQL и идентификаторов объекта. Идентификаторы GUID, автонумерация и все остальные нестандартные указатели в длительной перспективе оказываются бесполезными. Я много раз наблюдал за попытками втиснуть модели ООП в SQL, и все они разваливались максимум через год. Каждая опечатка становится новым атрибутом, запросы, выполнить которые было бы просто в реляционной модели, превращаются в многотабличные монстроподобные внешние объединения, избыточность нарастает по экспоненте, разработать ограничения практически невозможно, поэтому можно распрощаться с целостностью данных, и т.д.
       При обсуждении преимуществ ООП и реляционной модели в группе новостей comp.databases.theory, которое состоялось в октябре 2004 г., один опытный программист написал так:
       Хочу вам сказать то, что вы и так знаете, — вы на 100% правы. Я увяз в попытках приспособить объектно-ориентированную схему к реляционной БД. Трудно даже представить себе, сколько гимнастики потребовалось мне для выполнения простейшего запроса. Потребовалось шесть человеко-часов (я и еще один программист потратили три часа), чтобы получить в итоге запрос, эквивалентный следующему:

SELECT *  FROM Field_Offices;

       Нужные данные содержали название офиса, адрес, имя менеджера и телефон. Окончательная версия запроса занимала почти целую страницу, требовала объединения различных таблиц для каждого элемента данных (каждый элемент данных представляет собой объект, а у каждого объекта — собственные атрибуты, и потому для него требуется собственная таблица). Добавьте к этому чудовищные таблицы со связями, необходимые для получения правильного экземпляра каждого объекта.
       Кстати, который экземпляр правильный? Конечно же, самый последний, если только он не помечен, как неиспользуемый. Если он помечен нужно искать экземпляр, который помечен, как используемый. К сожалению, у метки не всегда одно и то же значение. Эти таблицы со связями — самые большие в БД. Всего лишь за год они выросли до миллионов строк, необходимых для отслеживания менее чем 80000 экземпляров объектов.

Не используйте в реляционных БД дизайн “сущность-атрибут-значение”

       Дизайн “сущность-атрибут-значение” (Entity-Attribute-Value, EAV) особенно популярен среди “чайников”, принадлежащих к экстремальной школе разработки ПО. Ее девиз “Сначала программируй, потом думай”.
       Методика эта вкратце состоит в создании одной огромной таблицы с тремя столбцами метаданных: название сущности, название атрибута, значение атрибута. Это позволяет пользователям в процессе работы с БД создавать новые сущности. Если один из них хочет внести в БД сущность “батон”, а другой — “булка”, они оба имеют возможность это сделать.
       Значения должны храниться с использованием наиболее общего типа данных, поэтому в модели EAV широко используются столбцы VARCHAR(n). Попробуйте наложить на такой столбец какое-либо ограничение.

Исключения
       Нет. Имеются лучшие инструменты для сбора данных в свободном формате.




<<< Пред. Оглавление
 
След. >>>

Дата последнего изменения:
Thursday, 21-Aug-2014 09:10:55 MSK


Постоянный адрес статьи:
http://az-design.ru/Support/DataBase/SQL/CelkoJ/02h003.shtml