Вы находитесь на странице: 1из 117

Стиль программирования Джо

Селко на SQL
 

Селко Д. 
       Стиль программирования Джо Селко на SQL / Пер. с англ. — М.: Издательство «Русская
Редакция»; СПб.: Питер, 2006. — 206 стр.: ил. 
       ISBN 5-469-01396-0 («Питер») 
       ISBN 5-7502-0276-3 («Русская Редакция») 
       Книга предназначена программистам, работающим с SQL и желающим повысить свою
квалификацию. В ней рассматриваются вопросы, связанные с созданием стандартизованных SQL-
приложений, не привязанных к конкретным SQL-продуктам, приводятся многочисленные
практические рекомендации по созданию наиболее эффективных и универсальных кодов.
Рассматриваются правила назначения имен элементам данных, оформления программ,
разработки выражений DDL и DML Описано использование представлений и хранимых процедур.
Отдельные главы посвящены общим теоретическим вопросам, связанным с теорией измерений и
разработкой систем кодирования для элементов баз данных. 
       Книга состоит из 10 глав, библиографии и приложения, содержащего справочную информацию
по различным стандартам, пригодным для использования в различных базах данных. 
       УДК 004.45 
       ББК 32.973.26-018.2/С36
ОГЛАВЛЕНИЕ

ВВЕДЕНИЕ...................................................................................3

ГЛАВА 1. Имена и элементы данных.....................................5

ГЛАВА 2. Шрифты, пунктуация и интервалы.....................17

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

ГЛАВА 4.  Шкалы и измерения..............................................42

ГЛАВА 5.  Схемы кодировки данных...................................49

ГЛАВА 6.  Выбор подхода к написанию кода.....................57

ГЛАВА 7.  Представления.......................................................78

ГЛАВА 8. Хранимые процедуры............................................89

ГЛАВА 9. Эмпирические правила кодирования...............100

ГЛАВА 10.  Думаем на SQL...................................................107

ПРИЛОЖЕНИЕ 1......................................................................115

ПРИЛОЖЕНИЕ 2......................................................................117
ВВЕДЕНИЕ
       В этой книге я вовсе не пытаюсь учить вас программировать на SQL. Хотите — прочитайте
предыдущее предложение еще раз. Если вас интересует именно обучение, есть учебники и
получше. Эта книга призвана стать вашей второй книгой по SQL, но отнюдь не первой. 
       Я предполагаю, что вы уже обладаете некоторыми навыками программирования на SQL и
теперь хотите их усовершенствовать. Чтобы познакомиться с тонкостями SQL, почитайте
другую мою книгу — SQLforSmarties  (3-е издание, 2005). Я стараюсь ставить в основу обучения
логичность и ясность и учить читателя мыслить в декларативных терминах, а не в терминах
процедурного или объектно-ориентированного программирования. 
       Вряд ли найдется SQL-программист, не обладающий многолетним стажем работы с каким-
либо процедурным или объектно-ориентированным языком. Обыкновенно предлагают освоить
один конкретный SQL-продукт, выучив язык методом проб и ошибок или по книге с заголовком
наподобие “SQL для идиотов”, “Учим SQL за десять легких или пять трудных уроков”, а то и
похуже. 
       Это просто абсурд! Человеку требуется не менее пяти лет, чтобы стать
квалифицированным плотником или поваром. Почему же считается, что превратиться в гуру
SQL он может за пару дней? За эго время реально стать лишь плохим SQL-программистом,
который разговаривает на диалекте SQL из популярного в данном месте SQL-продукта, к тому
же с сильным акцентом от известных ему других языков программирования. Чтобы вернуться с
облаков на землю, почитайте Teach Yourself Programming in Ten YearsПитера Норвига (Peter
Norvig, www.norvig.com/21-days.html) или статью No Silver Bullets Фреда Брукса (Fred Brooks),
опубликованную в апреле 1987 г. журналом Computer (№ 20 (4), с. 10-19). 
       Беда еще и в том, что жертвы подобной системы обучения часто не осознают, что
являются плохими программистами. Бывает так, что весь коллектив работает по схожим
правилам, так что они ничего другого просто не видели. Иные же “лезут в бутылку”, если кто-то
пытается указать им на недостатки. Взгляните на сообщения в группах новостей, посвященных
SQL, и вы увидите, что большинство программистов желает не расправиться с проблемой
всерьез и надолго, а просто получить сиюминутное решение для конкретной ситуации. 
       В группах новостей по изготовлению мебели их вопросы звучали бы так: “Каким камнем
лучше всего забивать шурупы в красное дерево?”. Чтобы осчастливить такого программиста,
ему достаточно посоветовать большую гранитную глыбу. Но попробуйте рассказать ему об
отвертке — вызовете вспышку гнева в свой адрес.

Цель книги
       Как же учились быть хорошими программистами мы, старики, когда Землей правили
динозавры? В конце 1970-х, когда в нашу жизнь вошло структурированное программирование,
одним из лучших наших помощников стала серия книг “[Pascal | FORTRAN | COBOL | BASIC]
with Style: Programming Proverbs”, написанная Генри Ледгардом (Henry Ledgard) и несколькими
его коллегами из Массачусетского технологического института. Их обложки оформлялись в
духе викторианского романа: с ангелами, свитками, старомодными типографскими элементами.
Каждой книге, как викторианскому роману, полагался подзаголовок типа “Основы хорошего
программирования с многочисленными примерами для усовершенствования стиля и сноровки”.
Эти и другие книги сыграли большую роль в жизни большинства из нас, поскольку научили
думать так, как должны думать хорошие программисты. 
       В этом же состоит цель и моей книги — усовершенствовать стиль и качество
программирования на SQL Говоря точнее: 
       1. Помочь отдельному программисту писать программы на стандартном SQL, без
акцента и диалекта. Отказаться от старых привычек трудно, но не невозможно. Лучше же всего
учиться правильному образу действий с самого начала. Любитель пишет код для себя.
Профессионал разрабатывает программу, которую будут использовать и обслуживать другие
люди. Мой опыт говорит, что пройдет не меньше года программирования на SQL, прежде чем
на вас снизойдет просветление и выувидите триединство мира: логика, модели данных и
множества. 
       2. Предоставить коллективу программистов внутренний стандарт
программирования. Каждому правилу я пытался дать продуманное обоснование, а также
рассказал об исключениях там, где мне удалось их увидеть. Вы вольны не согласиться с
некоторыми из моих предложений, но тогда уж извольте провести исследование и подкрепить
свою позицию примерами. Вряд ли можно считать аргументом заявления типа: “Да мы в нашей
конторе всегда так программируем, стало быть, такова воля Божья!”. 
       Руководителю коллектива, опирающегося на мои правила, в случае недовольства
сотрудников всегда можно будет обвинить во всем книгу (и ее автора). Благодаря ей вы
сохраните согласованность действий. Даже если позже окажется, что я в чем-то ошибся,
значительно проще исправлять ошибки, сделанные в единой системе. 
       3. Предоставить программистам необходимые инструменты для оценки решаемой
проблемы с точки зрения SQL. He устаю повторять, что требуется не меньше года, чтобы
“въехать” в SQL и избавиться от привычек, навязанных процедурным программированием.

Благодарности
       Структура главы о представлениях принадлежит Крейгу Маллинсу (Craig Mullins) (см.
статью для www.DBAzine.com). Стиль форматирования позаимствован из моих публикаций за
последнее десятилетие в журналах СМР и других. Данные о правилах присвоения имен в
реальных продуктах предоставил Питер Гулутзан (Peter Gulutzan) — также в статье для
www.DBAzine.com. Правила разработки приставок к именам в главе 1 основаны на внутренних
стандартах корпорации Teradata. Масштабы, величины и схемы кодирования фигурировали в
нескольких моих старых колонках в журналах DBMS  и Database Programming&Design,  а затем
были объединены в моей книге Data & Database.  В тексте я всегда старался упомянуть моих
помощников, но в группах новостей участников так много, что я несомненно кого-то пропустил. 
       Разумеется, я благодарен Генри Ледгарду за серию “Programming Proverbs”, которая
служила мне источником вдохновения. 
       Особенная благодарность — новичкам-программистам, пишущим плохие программы. Это
замечание кажется саркастическим, но таковым не является. Многие из них столкнулись с DBA
или SQL по воле руководства, которое не обеспечило их обучением или опытным наставником.
Их не в чем обвинить, конечно, если они не упорствовали в своем невежестве. Их ошибки в
синтаксисе, семантике и стиле показали мне, как они думают. Правильный диагноз — первый
шаг к излечению.

Исправления, замечания и последующие издания


       Исправления и дополнения к будущим изданиям направляйте непосредственно в
издательство Morgan-Kaufmann или мне по электронному адресу jcelko212@earthlink.net.
ГЛАВА 1.
Имена и элементы данных
  Есть такой старый анекдот.
— Когда я был маленьким, у нас было три
кошки.
— И как их звали?
— Кошка, кошка и кошка.
— Ерунда какая-то. Как же вы их различали?
— А какая разница? Кошки все равно на имена
не откликаются!

       Наши данные тоже не придут к вам на зов, если вы не присвоите им четкие и понятные имена.
Это важная часть любого проекта базы данных (БД). Неудачные имена для элементов данных
приводят к тому, что код бывает трудно, а то и невозможно прочитать. 
       Невозможность чтения — не шутка. В старину компании, разрабатывавшие программное
обеспечение, нарочно искажали имена и удаляли из исходного кода форматирование, чтобы
скрыть от покупателей алгоритм. Эта традиция все еще жива, хотя, может быть, изначальное
намерение и утрачено. В августе 2004 г. в одной из групп новостей по SQL была опубликована
программа, в которой все имена состояли из одной буквы и длинной цепочки цифр. 
       В настоящее время существуют стандарты метаданных ISO-11179, описывающие правила
именования элементов данных и регистрации стандартов. Поскольку это стандарт ISO, его
надлежит применять не только в SQL, но и вообще везде. 
       Стандартизация, немного печатного мастерства и некоторый здравый смысл — вот слагаемые
успешной работы.

Имена
       В старые добрые времена у каждого программиста были собственные правила назначения
имен. К несчастью, зачастую они оказывались весьма замысловатыми. Лично мне особенно
нравился парень, который для имен в программах на Коболе выбирал специфическую тему: для
одной программы — страны, для другой — цветы и т. д. Это, конечно, чересчур даже для
программиста, но вообще системы имен, смысл которых понятен только автору, но не
посторонним, встречаются часто. 
       Скажем, первый Фортран, с которым я работал, допускал имена не длиннее шести символов,
поэтому я стал адептом шестибуквенных имен. Программисты, которые начинали со слабо
типизированных языков или с языков без типов, испытывают привязанность к венгерской нотации.
От старых привычек трудно отказываться. 
       Когда разработка ПО перестала быть уделом одиночек, каждая фирма создавала
собственные правила именования и закрепляла их в своеобразных словарях. Вероятно, наиболее
распространены были правила MIL STD 8320.1, разработанные Министерством обороны США, но
вне федерального правительства популярными они так и не стали. Разработка фирменных
стандартов, безусловно, представляла собой шаг вперед по сравнению с прежней
бессистемностью, но в каждой фирме каноны чем-то отличались. В одних действительно
создавались формальные правила назначения имен, в других за элементом данных просто
закреплялось первое присвоенное ему имя. 
       Теперь у нас есть стандарты ISO-11179, которые распространяются все шире, стали
обязательными для некоторых правительственных заказов и включаются в продукты для работы с
хранилищами данных. В этот стандарт встроены инструменты и хранилища стандартизованных
схем кодирования. Одним словом, будущее принадлежит ISO-11179 и XML как стандартному
формату обмена данными.

Следите за длиной имен


Обоснование 
       В стандарте SQL-92 длина идентификатора ограничена 18 символами, как это было в старых
стандартах Кобола. В современных реализациях SQL допускаются более длинные имена, но на
самом деле вряд ли есть что-то, что нельзя выразить 18 символами. Максимальные длины имен
наиболее важных объектов схемы SQL в стандартах ISO и некоторых популярных SQL-продуктах
приводятся в табл. 1.1.

Табл. 1.1. Длины идентификаторов

  SQL-92 SQL-99 IBM MS SQL Oracle


Столбец 18 128 30 128 30
Ограничение 18 128 18 128 30
Таблица 18 128 128 128 30

       Числа в таблице выражены в байтах или символах. Максимальная длина в символах может
быть меньше максимальной длины в байтах, если вы используете многобайтовый набор
символов. 
       Не увлекайтесь сверхдлинными именами. Людям предстоит их читать, набирать и
распечатывать. Им также придется разбираться в их смысле, искать в словаре данных и т.п.
Наконец, имена могут использоваться в хост-программах, у которых будут собственные
ограничения на их длину. 
       Но и в другую крайность ударяться тоже не надо. Во времена, когда длина столбца была
ограничена 18 байтами, в Bachman — старом средстве разработки баз данных DB2 — иногда
логическое имя атрибута преобразовывалось в физическое имя столбца путем удаления из него
всех гласных. Вряд ли можно назвать этот подход удачным. В таких “конденсированных” именах
можно разобраться только после многодневного исследования.

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

Не используйте в именах спецсимволы


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

Табл. 1.2. Символы, допустимые в именах

  Стандартный IBM Oracle Microsoft


SQL
Первый символ Буква Буква, $ # @ Буква Буква, # @
Последующие Буква, цифра, _ Буква, цифра, $ # Буква, цифра,$ * _ Буква, цифра,* @
Символы @_
Различие верхнего Нет Нет Нет По желанию
и нижнего регистра
Название   Обычный Идентификатор Регулярный
идентификатор без кавычек идентификатор

       Как правило, первым символом имени должна быть буква, а остальные символы могут быть
буквами, цифрами и символом подчеркивания “_”. В различных СУБД допускается применение
также символов $, # или @, но ни в одной СУБД не допускается применение всех трех сразу.
Вообще, нет ни одного спецсимвола, который можно было бы спокойно использовать в любом
продукте. В ПО Microsoft, например, имена, начинающиеся символами @ или #, имеют особое
значение. В Oracle нельзя использовать спецсимволы в именах некоторых объектов. 
       Да и с буквами все ли ясно? В оригинальном SQL разрешалось использовать только
латинские буквы верхнего регистра, что означает 26 вариантов. В наши дни репертуар несколько
расширился, но не злоупотребляйте буквами, не входящими в набор символов Latin-1, и вот
почему. 
       1. В продуктах IBM буквы распознаются не всегда корректно. Буквой считается любой
многобайтовый символ за исключением пробела. Регистр символа система не определяет. 
       2. В продуктах IBM и Oracle используется набор символов из БД. При миграции могут
возникнуть проблемы с экзотическими буквами. Продукты Microsoft работают с символами Unicode
и с этой проблемой не сталкиваются. 
       В стандарте SQL-92 не разрешается заканчивать идентификатор символом подчеркивания. Не
стоит также вставлять в имя несколько подчеркиваний подряд. При современном лазерном
качестве печати пересчитать их будет сложновато.

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

По возможности не используйте идентификаторы в кавычках


Обоснование 
       Идентификаторы в кавычках (quoted identifiers) впервые появились в стандарте SQL-92.
Предполагалось, что они будут использоваться для создания псевдонимов столбцов, улучшая
читаемость распечаток. В реальности же они противоречат принципам многоуровневой
архитектуры, ставят под вопрос переносимость кода и провоцируют программиста на создание
неуклюжих систем имен. Основные характеристики идентификаторов с ограничителями (delimited
identifiers) суммируются в табл. 1.3.

Табл. 1.3. Символы, допустимые в идентификаторах с ограничителями

  Стандартный SQL IBM Oracle Microsoft


Ограничители "" "" "" "" или [ ]
Первый символ Любой Любой Любой Любой
Последующие Любой Любой Любой Любой
символы
Различие Да Да Да По желанию
верхнего и
нижнего регистра
Название Идентификатор с Идентификатор Идентификатор в Идентификатор с
ограничителями с кавычках ограничителями
ограничителями

       Если правила использования символов в именах кажутся вам слишком жесткими, вы вольны
обойти их, поместив идентификатор в двойные кавычки. В результате получится так называемый
идентификатор с ограничителями (или идентификатор в кавычках, в терминологии Oracle).
Идентификатор с ограничителями может начинаться с любого символа и вообще содержать любой
символ. Конечно, возникает неясность с использованием внутри идентификатора самого символа
". Стандартный способ — записать его дважды (“Работ""ники”), но в документации он явно описан
не всегда. 
       Поддержка имен с ограничителями практически универсальна, с двумя важными
исключениями: 1) продукты IBM допускают использование только букв и цифр для меток и имен
переменных в хранимых процедурах; 2) в продуктах Microsoft не разрешается использовать
идентификаторы в кавычках при сброшенном флаге QUOTED_IDENTIFIER. Первое исключение
связано, вероятно, с тем, что в продуктах IBM процедуры SQL перед компиляцией “переводятся”
на другой компьютерный язык. 
       Рассмотрим в качестве примера создание таблицы с делимитированным идентификатором в
качестве имени:
CREATE TABLE "t" ("columni" INTEGER NOT NULL);

       Теперь обратимся к таблице так, словно ее имя является обычным идентификатором:

SELECT columni FROM t;

       Сработает? Согласно стандарту SQL, не должно, но может сработать в продукте Microsoft.
Причины обсуждаются в разделе “Разработайте строгие правила использования прописных букв”. 
       Идентификаторы в кавычках не особенно хорошо стыкуются с хост-языками, особенно когда в
идентификаторе имеются пробелы или спецсимволы. Вот, например, вполне корректное
выражение для вставки данных:

INSERT INTO Table ([field with space]) VALUES (value);

       Объект ADO сгенерирует следующий код:

INSERT INTO Table (field with space) VALUES (value);

       что является синтаксической ошибкой.

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

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


Обоснование 
       Правила работы с регистрами различны в разных продуктах. В стандартном SQL, а также
продуктах IBM и Oracle обычные идентификаторы переводятся в верхний регистр, а регистр
идентификаторов в кавычках остается неизменным. В продуктах Microsoft правила использования
регистров определяются не типом идентификатора, а заданным умолчанием. По умолчанию
регистры не различаются, то есть “t” равно “Т”. 
       С распознаванием регистров связаны две проблемы. Во-первых, согласно стандарту SQL,
идентификатор в кавычках "t" и обычный идентификатор t различаются. Во-вторых, Microsoft не
следует стандарту SQL. Поэтому трудно придумать правило построения имен, которое подошло
бы всем.

Исключения 
       Я предложу вам простой набор правил, основанный на принципах читаемости и эстетичности,
вовсе не претендуя на его единственность. 
       1. Не используйте идентификаторы с ограничителями. 
       2. В IBM используется только верхний регистр. К сожалению, код в верхнем регистре трудно
читать. Кроме того, у читателя возникнет подозрение, что вы до сих пор работаете с
перфокартами. 
       3. В Microsoft и Oracle нижний регистр используется везде, где он уместен. К сожалению,
определение уместности не всегда оказывается вполнечетким. Часть зарезервированных слов
набирается в верхнем регистре, часть — в нижнем и т.д.
Создавайте имена согласно стандарту ISO-11179

       Это относительно новый стандарт ISO для метаданных, и понятен он пока далеко не всем. К
счастью, те его правила, которые нужны программисту SQL, просты и очевидны. Настоящая
проблема в том, что многие люди нарушают эти правила. В сокращенном виде правила для
элементов данных, разработанные комитетом стандартов метаданных NCITS L8, опубликованы на
следующих сайтах:

http://pueblo.lbl.gov/~olken/X3L8/drafts/draft.docs.html

http://lists.oasis-open.org/archives/ubl-
ndrsc/200111/msg00005.html

PDF-файл:

www.oasis-
open.org/committees/download.php/6233/c002349_IS0_IEC_11179

Черновик:

www.iso.org/iso/en/ittf/PubliclyAvailableStandards/c002349_IS0_I
EC_11179-1_1999(E).zip

       Стандарт ISO-11179 разбит на шесть разделов: 


       11179-1: Framework for the Specification and Standardization of Data Elements Definitions 
       11179-2: Classification for Data Elements 
       11179-3: Basic Attributes of Data Elements 
       11179-4: Rules and Guidelines for the Formulation of Data 
       11179-5: Naming and Identification Principles for Data 
       11179-6: Registration of Data Elements

ISO-11179 для SQL


Обоснование 
       Формальные стандарты хороши, но слишком общи. Удобно превратить их в набор правил,
написанных на языке, понятном разработчику SQL Некоторые из данных здесь формулировок
являются результатом консенсуса экспертов и взяты из групп новостей и частной переписки. 
       Согласно правилам стандарта ISO-11179-4 скалярный элемент данных должен удовлетворять
следующим требованиям. 
       1. Он уникален (в пределах своего словаря данных). 
       2. Он назван с использованием единственного числа. 
       3. В имени поясняется, чем является элемент, а не чем он не является. 
       4. Имя читается, как описательная фраза. 
       5. Имя содержит только общепринятые сокращения. 
       6. Имя не содержит вложенных определений других элементов данных или понятий. 
       7. Таблицы, наборы и другие сборные элементы именуются обобщающими понятиями во
множественном числе. 
       8. В имени процедуры содержится глагол. 
       9. В имя копии (псевдоним) таблицы включено имя базовой таблицы и причина создания
копии. 
       В теории все это звучит прекрасно, но в реальном мире на имена накладываются
дополнительные практические ограничения, например ограничение длины или допустимые
символы. Другая проблема состоит в том, что у элемента данных в зависимости от контекста его
использования могут быть разные имена. В отчете он назван так, в файле электронного обмена
данными (electronic data interchange, EDI) по-другому, причем оба имени могут отличаться от
имени в БД. Но в пределах одной БД использовать разные имена для одного элемента не стоит,
да и в разных БД одного предприятия этим не стоит злоупотреблять. К сожалению, найти
подобные разногласия без хорошего словаря очень трудно. Словарь данных должен включать
внешние имена и их контекст.

Исключения 
       Над всеми нами довлеет проклятие старых БД, старых файловых систем и прочих традиций.
Если у элемента имеется устоявшееся и понятное имя, его можно использовать даже вопреки
стандарту. Например, для столбца с почтовым индексом формально правильным будет имя
“us_postal_code”, но вместо него можно поставить более привычное “zip_code” или даже просто
“zip”.

Уровни абстрагирования
       Разработка имени начинается на концептуальном уровне. Класс объекта представляет идею,
абстракцию или предмет реального мира, например дерево или страну. Свойство, например,
высота или идентификатор, описывает все объекты класса. Это позволяет создавать
словосочетания типа “высота дерева” или “идентификатор страны”, комбинируя класс и свойство. 
       Этот уровень — логический. Полное логическое имя элемента данных должно включать
способ представления значений из его области определения (набора допустимых значений). Так
мы получаем в качестве возможных элементов данных “меру высоты дерева”, “имя
идентификатора страны” и “код идентификатора страны”. 
       Между именем идентификатора и кодом идентификатора имеется различие, правда, столь
тонкое, что мы можем не захотеть его моделировать. В этом случае необходимо правило, в
соответствии с которым название свойства исключается из имени элемента данных. При этом в
структуре элемента свойство сохранится, лишь перестав быть частью его имени. 
       Некоторые логические элементы данных могут считаться общепринятыми, если они четко
определены и применяются многими организациями. Например, названия и коды стран описаны в
стандарте ISO-3166 “Codes for the Representation of Names of Countries”, который можно
использовать в качестве справочника. 
       Заметьте, что, по правилам стандарта ISO-11179, это самый высокий уровень, на котором
появляются истинные элементы данных: у них есть класс, свойство и способ представления. 
       Далее идет прикладной уровень. Он обычно реализуется при помощи неких конкретных
уточнений, соответствующих поставленной задаче: выделения подмножества из области значений
данных, добавления ограничений, гарантирующих, что мы будем иметь дело лишь с допустимыми
значениями. Например, мы используем коды стран ISO-3166, но реально работаем только с
европейскими государствами. Это означает, что нас интересует подмножество стандарта, которое
временным изменениям практически не подвержено. С другой стороны, набор стран, в которых в
текущем году выпало больше всего осадков, может на протяжении года меняться неоднократно. 
       Эти уточнения включаются в имя посредством добавления к логическому имени конкретного
квалификатора. Например, если нужно составить список стран, с которыми у некой организации
есть торговые соглашения, элемент данных запроса можно назвать “trading_partner_country_name”
(название страны-торгового партнера), подчеркнув тем самым его роль. Область значений будет
представлять собой подмножество стран из стандарта ISO-3166. 
       Ниже всего располагается физический уровень. На нем находятся имена, которые фигурируют
в заголовках столбцов таблицы, описаниях файлов, разметке EDI-файлов и т.д. Эти имена могут
быть, с одной стороны, сокращенными из-за различных ограничений на длину или набор
символов, с другой стороны, могут содержать информацию о формате и источнике. 
       В реестре все имена элементов данных и компоненты имен всегда будут указываться в паре
со своим контекстом, чтобы мы легко могли установить их источник и назначение. Цель
заключается в возможности проследить за каждым элементом данных от его создания до любого
применения, под каким бы именем он ни фигурировал.

Избегайте описательных префиксов


Обоснование 
       Среди новичков бытует странный обычай описывать какие-то черты элемента данных в
текущей таблице с помощью приставок. Он, вероятно, также берет свое начало в доисторических
временах последовательных файловых систем, когда на физическое расположение файла
действительно приходилось обращать внимание. 
       Особенно глупо выглядит приставка “tbl-”. Прежде чем возражать, что эта приставка отражает
природу того, к чему относится идентификатор, вспомните, что в SQL имеется только одна
структура данных. Что еще это может быть? Вы же не ставите приставку “сущ-” перед каждым
написанным вами существительным! Вряд ли она облегчит чтение. Это похоже на то, как
маленькие дети называют что ни попадя “штучкой”.

Быть чем-то значит быть чем-то конкретным. Не быть чем-то


конкретным или быть чем-то вообще значит быть ничем (Аристотель).

       Следующая по глупости приставка — имя таблицы. Неужели один и тот же элемент данных
полностью меняет свою природу от таблицы к таблице? Скажем, в столбцах “orders_upc” и
“inventory_upc” явно содержатся коды UPC, но, присвоив им разные имена, вы тем самым
подразумеваете, что в вашей модели данных коды UPC для заказов и для описи представляют
собой “две большие разницы”. 
       Полный кошмар начинается, когда в ссылках на внешний ключ имя переменной составляется
из имени базовой таблицы в качестве приставки и названия столбца “id”. Запросы полны
операторов типа “Orders.ID=OrderlD”, и работа с ними быстро превращается в игру “Разгляди
точку”, сопряженную с мучительным разгадыванием смысла тысяч различных столбцов “ID” в
словаре данных. 
       Префиксы наподобие “vw” для представлений говорят о том, как в схеме реализована
виртуальная таблица, но к модели данных это не имеет никакого отношения. Если позже я решу
заменить представление обычной таблицей, мне придется менять имя. А что, если в схеме уже
есть таблица с той же основой имени? 
       В равной степени странны и опасны имена столбцов, начинающиеся с обозначения типа
данных. Тип описывает физическое представление данных, а не их смысл в используемой модели.
Словарь данных можно выбрасывать, если вы всякий раз должны гадать, как называется столбец
с номером заказа — “intorder_nbr”, “strorder_nbr” или даже “forder_nbr”, хотя он мог бы называться
просто “order_nbr”. Если пользователь не помнит тип данных столбца, он всегда может заглянуть в
его DDL-определение. 
       Наконец, последняя проблема с приставками — добавление символов “РК_” к первичному
ключу или “FK_” к внешнему ключу. Использование столбца в качестве ключа — это всего лишь
один из способов его применения, не имеющий отношения к фундаментальной природе данных.
Ключи всегда можно отождествить с помощью слов “PRIMARY KEY” или “FOREIGN KEY...
REFERENCES...” в описании столбцов. 
       Самый странный вариант этого правила попался мне на Web-узле компании,
специализирующейся на программировании для Oracle. Согласно ему, ограничения СНЕСК()
должны были называться так: “<имя таблицы>_СК_<имя столбца>”. Такое имя, во-первых, не
несет никакой информации о природе ошибки, во-вторых, ограничивает возможное количество
ограничений: по одному на столбец в данной таблице. К тому же, непонятно, как быть с
ограничениями, в которых проверяются значения двух и более столбцов. 
       Те же правила и рекомендации применимы к именам любых объектов схемы. Вам встретятся
приставка “usp_” для пользовательских хранимых процедур, “trig_” для триггеров и т.п. В MS SQL
Server это может привести к серьезным проблемам, поскольку префикс “sp_” используется для
системных процедур и имеет в архитектуре особое значение. 
       Если объект схемы выполняет какое-то действие (триггер, процедура), используйте формат
имени <глагол_объект>. Подробнее об этом — в главе 8.

Исключения 
       Другие мнения вы найдете по адресу: http://www.craigsmullins.com/dbt0999.htm. 
       Посмотрите также серию статей: 
       http://www.sqlservercentral.com/columnists/sjones/codingstandardspartlfor-matting.asp; 
       http://www.sqlservercentral.com/columnists/sjones/codingstan-dardspart2formatting.asp.

Разработайте стандартную систему суффиксо в


       Приведенный ниже примерный список суффиксов основан на внутренних стандартах
компании Teradata. 
       — “_id” Идентификатор. Уникален в пределах схемы и используется для обращения к данной
сущности, где бы в схеме она ни появлялась. Никогда не используйте имена “<имя таблицы>_id”.
Они основаны не на сути данных, а на их расположении; такие столбцы вряд ли могут эффективно
применяться в качестве ключа. Просто имя “id” имеет слишком общий вид, чтобы быть полезным
хоть кому-нибудь. К тому же, в вашем словаре данных таких имен окажется множество, все
разные, нос одним именем и, вероятно, отнесенные к какому-нибудь перегруженному типу
данных. 
       — “_date” или “dt” Дата, причем всегда дата чего-либо: приема на работу, рождения,
увольнения и т.п. Не должно быть столбцов, имя которых обозначало бы дату вообще. 
       — “_nbr” или “пит” Номер, последовательность цифр, служащих для обозначения чего-либо.
Не применяйте суффикс “_по”, поскольку он похож на английский вариант логического значения
“да/нет”. Лично я предпочитаю “nbr”, поскольку в некоторых европейский языках такое сокращение
стандартно. 
       — “name” или “nm” Имя. Тут и пояснять нечего. Почитайте в главе 4 про шкалу
наименований. 
       — “code” или “_cd” Стандартный код. Почерпнут из надежного источника, обычно за
пределами предприятия. Примером может служить почтовый индекс, поддерживаемый
соответствующими государственными службами. Смысл кода понятен, как правило, из контекста,
поэтому какие-то особые пояснения к нему не требуются. 
       — “size” Промышленный или внутренний стандарт размера одежды, обуви, конвертов,
шурупов и пр. Обычно берется из какого-либо каталога стандартов. 
       — “_tot” Сумма, итоговое значение, логически отличное от суммируемых величин. 
       — “_seq” Последовательность, порядковый номер. От обычного номераотличается тем, что
не допускает пропусков. 
       — “tally” Результат подсчета. Почитайте в главе 4 про абсолютную шкалу. 
       — “_cat” Категория, обозначение для характерного набора сущностей, как правило,
почерпнутое из внешнего источника. Пример — классификация видов живых существ. 
       — “class > Более детальный код, отражающий не столь существенные различия в пределах
категории. Пример — классификация растений. 
       — “type” Менее формальный, чем класс, код типа объекта. Типам, к томуже, разрешается
перекрываться. Например, одни и те же водительскиеправа могут относиться и к категории А, и к
категории В. 
       Различие между типом, классом и категорией кроется в строгости соответствующих критериев.
Категория четко определена: вам чаще всего не нужно напряженно размышлять, чтобы отнести
данную сущность к животным, овощам или минералам. 
       Класс объединяет сущности, связанные общим признаком: животное может быть
классифицировано как млекопитающее или как рептилия. В некоторых случаях правила
разделения на классы оказываются неочевидными, как, например, в случае с утконосом —
яйцекладущим млекопитающим, обитающим в Австралии. Часто такие исключения ведут к
появлению новых классов, в случае с утконосом — к появлению отряда однопроходных. 
       Тип — наименее конкретный и субъективный способ разделения сущностей. Например, в
одних штатах мотоцикл на трех колесах юридически считается мотоциклом, в других штатах —
автомобилем, в третьих он считается автомобилем только при наличии заднего хода. На практике
категорию, класс и тип часто смешивают. Если приведенные выше правила противоречат
промышленному стандарту, отдавайте предпочтение последнему 
       — “status” Внутренний код, отражающий состояние сущности, определяемое множеством
факторов. Например, кредитное состояние “credit_status” может быть рассчитано на основании
данных из нескольких источников. 
       — “_addr” или “_lос” Адрес или расположение сущности. 
       — “_img” Изображение (jpg, gif и т.п.). 
       Иногда могут также понадобиться суффиксы для обозначения единицы
измерения. Всегда  проверяйте, нет ли для элемента данных готового стандарта ISO.

Имена таблиц и представлений должны подчиняться


стандартам и выражаться существительными
множественного числа
Обоснование 
       Во всех возможных случаях предпочтение следует отдавать промышленным стандартам.
Основанные на них имена будут понятны людям, а определения стандартов — поддерживаться
квалифицированными специалистами. 
       Например, в США старая Стандартная промышленная классификация SIC (Standard Industrial
Classification) была заменена Североамериканской системой промышленной классификации
NAICS (North American Industry Classification System). Этот новый код был разработан совместно
США, Канадой и Мексикой, чтобы обеспечить сопоставимость статистической информации по всей
Северной Америке. Сокращения NAICS и “naics_code” вполне понятны специалистам по
экономической статистике, хотя для большинства из нас они, вероятно, выглядят необычно. 
       Если к вашей ситуации промышленный стандарт в чистом виде не применим, постарайтесь
придумать его модификацию. Скажем, если я имею дело только с мексиканскими автомобилями, я
могу отразить это ограничение в имени таблицы “VIN_Mexico”. Если подходящего стандартного
имени нет, подбирайте обобщающее имя или существительное во множественном числе. 
       Обобщающие имена таблиц предпочтительнее простых существительных единственного
числа, поскольку таблица — это не одно значение, а их набор. Когда я говорю “сотрудник”, в
голове у меня рисуется образ одиноко стоящего сотрудника. Я говорю “сотрудники”, и образов
становится много. Но стоит произнести “персонал”, и мысленный образ внезапно становится более
абстрактным — общее понятие лишено конкретных лиц. 
       В SQL допускается использовать одно и то же имя и для таблицы, и для столбца в ней, но
делать этого не следует. Во-первых, такое наименование столбца противоречит правилам,
которые мы только что обсудили, поскольку в нем отсутствует квалификатор. Во-вторых, оно
означает, что либо таблица не названа как набор, либо столбец не назван как скаляр.

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

CREATE TABLE Constants


(lock CHAR(1) DEFAULT "X" NOT NULL PRIMARY KEYCHECK (lock = X),
pi REAL DEFAULT 3.141592653 NOT NULL,
e REAL DEFAULT 2.718281828 NOT NULL,
phi REAL DEFAULT 1.618033988 NOT NULL);

INSERT INTO Constants DEFAULT VALUES;

       Оператор INSERT создает единственную строку, поэтому единственное число в имени
таблицы вполне уместно. Столбец “lock” гарантирует, что строка будет только одна. Другой способ
выполнить то же самое — создать представление VIEW:

CREATE VIEW Constant (pi,e,phi,..)


AS VALUES (3.141592653,2.718281828,1.618033988,..);

       Достоинство этого способа в том, что представление нельзя изменить. Недостаток — в том
же.

Имена корреляций подчинены тем же правилам, что и другие


имена. Почти всегда
Обоснование 
       Имена корреляций — это те же имена. Они основаны на имени базовой таблицы,
представления, столбца или на выражении, с помощью которого созданы. Удобно, что в этом
случае у читателя всегда перед глазами контекст, поэтому имя корреляции может быть более
кратким. 
       Имя корреляции гораздо чаще называют псевдонимом (alias), но мне пока хотелось бы
сохранить формальный подход. В SQL-92 псевдонимы создаются необязательным оператором
AS, но его обязательно нужно вставлять, чтобы присвоение альтернативного имени было
очевидным. 
       Ни в коем случае не прибегайте к порочной практике присвоения бессмысленных
корреляционных имен, основанных на алфавитной последовательности. Такое, к сожалению,
случается очень часто и существенно затрудняет обслуживание кода. Представьте себе
программу, в которой для таблицы “Personnel” в одном выражении создан псевдоним А, в другом
— D, в третьем — Q: исключительно на основании положения таблицы в конструкции FROM. 
       Корреляционные имена столбцов для вычисляемых элементов данных должны следовать тем
же правилам, что и имена обычных столбцов. Скажем, читателю не составит труда разобраться в
выражении “salary + COALESCE(commission, 0.00)) AS total_pay”. 
       Корреляционное имя таблицы или представления должно основываться на имени базовой
таблицы и отражать роль, которую копия таблицы играет в выражении (например, “SELECT ...
FROM Personnel AS Management, Personnel AS Workers”, если таблица в запросе используется
дважды). 
       Теперь поясню, почему я написал в заголовке слова “почти всегда”. В случае, если вы
создаете несколько имен корреляции для одной и той же таблицы, удобно добавлять к ним номер
в виде суффикса (например, “SELECT ... FROM Personnel AS P1, Personnel AS P2”). По номеру
читатель узнает, сколько псевдонимов для данной таблицы создается в выражении. 
       По сути, имена корреляции представляют собой местоимения и играют ту же роль, что и
местоимения в обычной речи: делают предложения более короткими и удобными для чтения. 
       Псевдоним табличного выражения должен быть простым, коротким и основанным на
логическом смысле табличного выражения:

SELECT ..
FROM (Personnel AS P1 INNER JOIN SoftballTeams AS S1 ON P1 ssr = S1 ssn)
AS CompanyTeam (..)
WHERE ..;

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

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

Имена таблиц-отношений должны быть общепринятыми,


понятными терминами
Обоснование 
       Таблицы и представления часто применяются для моделирования отношений “один ко
многим” или “многие ко многим”. Если у отношения есть общеупотребительное имя, понятное в
данном контексте, используйте его. Для новичков типично создавать имена для таблиц-
отношений, комбинируя имена исходных таблиц. Например, таблицу, для которой уместно было
бы имя “Семьи”, они называют “МужчинаЖенщина”, “МужьяЖены” или еще более странно.
Аналогично, имя таблицы “Распределение” выглядит более осмысленным, чем “Студенты_Курсы”.
Стоит только задуматься над этим вопросом, и выбор имени не составит труда. 
       Конечно же, конкатенация имен оказывается бессильной, когда в отношении участвует более
трех сторон, как, например, в покупке недвижимости участвуют продавец, покупатель и банк.

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

В имена объектов доступа к метаданным схемы можно


включать структурную информацию
       Это правило неприменимо к информационным таблицам схемы, имена которых
стандартизованы. Речь идет лишь об индексах и прочих элементах, непосредственно связанных с
хранением и доступом к данным. Вполне приемлем суффикс “_idx”.

Обоснование 
       Здесь мы следуем все тому же принципу: имя должно объяснять, что им обозначено. В случае
индексов и других элементов, непосредственно связанных с хранением и доступом к данным,
важно подчеркнуть, что они не имеют отношения к модели данных.
Исключения 
       Это правило неприменимо к объектам схемы, которые видны пользователю.

Ошибки в именовании элементов данных


       Мы достаточно поговорили о том, как нужно называть элементы данных. Теперь давайте
посмотрим, как предложенные правила чаще всего нарушаются.

Избегайте невнятных имен


Обоснование

В этом есть какая-то скрытая непристойность, а я не выношу скрытости


Гручо Маркс

       Крайний вид невнятности — настолько общее имя, что оно не говорит вообще ничего,
например “Дата”, “Количество”... Представьте себе столбец “Дата”. Сразу возникает вопрос: дата
чего? Приема на работу? Рождения? Встречи? Увольнения? Смерти? 
       Другая крайность — строка квалификаторов, противоречащих друг другу. Вот типичный для
новичка пример: “type_code_id”. Если это идентификатор (id), то он должен быть уникальным для
каждой обозначаемой им сущности, как идентификационный номер автомобиля (vehicle
identification number, VIN). Если это код (code), то он берется из надежного источника и не должен
быть уникальным. Если это тип (type), какой систематике он принадлежит? Чего уж мелочиться —
можно и еще один квалификатор добавить: “type _code_id_value”! 
       А ведь гораздо понятнее было бы написать просто “customer_type”...

Исключения 
       Нет. 
       Некорректные имена элементов данных имеют своим источником невежество и объектно-
ориентированное программирование (ООП). В частности, объектно-ориентированные
программисты обязательно добавляют суффикс “_id” к любому первичному ключу любой таблицы,
не понимая, что SQL — сильно типизированный язык, и в нем сущности в ходе выполнения
программы своего типа не меняют. Иногда имена бывают просто абсурдными. Взгляните на
таблицу цветов:

CREATE TABLE TblColors


(color_value_id INTEGER NOT NULL PRIMARY KEY,
color_value VARCHAR(50) NOT NULL);

       Что означает суффикс “_value_id”? Сразу ясно, что создатель не сильно задумывался над
именем. Теперь представьте, что мы решили использовать в БД цветовую систему Pantone: у нас
есть надежный источник и точное описание, потому что мы все продумали! Выражение будет
выглядеть так:

CREATE TABLE Colors


(pantone_nbr INTEGER NOT NULL PRIMARY KEY,
color_description VARCHAR(50) NOT NULL);

Избегайте имен, которые меняются от места к месту


Обоснование 
       Хуже всего, когда имя атрибута меняется от таблицы к таблице. Рассмотрим в качестве
примера слегка подчищенный фрагмент кода из группы новостей, посвященной SQL

SELECT Incident.Type, IPC.DefendantType,


Recommendation.Notes, Offence.StartDate, Offence.EndDate,
Offence.ReportedDateTime, IPC.URN
FROM IPC
INNER JOIN Incident ON IPC.URN = Incident.IPCURN
INNER JOIN IncidentOffence ON Incident.URN = IncidentOffence.IncidentURN
INNER JOIN Offence ON Offence.URN = IncidentOffence.OffenceURN
INNER JOIN IPCRecommendation ON IPCURN = IPCRecommendation.IPCURN
INNER JOIN Recommendation ON IPCRecommendation.RecommendationID =
Recommendation.ID;

       Полные имена таблиц трудно читать, но новичок, написавший код, считал, что имя
таблицы обязательно должно быть частью имени столбца. Ведь в старых программах на Коболе
все было именно так! Это означает, что если у вас есть сотня таблиц, в каждой из них один и тот
же атрибут будет иметь новое имя, поэтому о нормальном словаре данных вам остается только
мечтать. Кстати, обратили внимание, как рябит в глазах от множества точек? 
       Теперь взгляните на исправленную версию, которая четко центрирована на таблице IPC
(схема-“звезда”).

SELECT 11.incident_type,IPC.defendant_type, R1.notes,


01.start_date,01.end_date, 01.reported_datetime,IPC.urn
FROM Incidents AS 11, IPC, Recommendations AS R1, Offences AS 01,
WHERE IPC. recommendation_id = R1.recommendation_id
AND IPC.urn = 01.urn AND IPC.urn = 11.urn
AND IPC.urn = RLurn AND 11.urn = 01.urn;

       Я понятия не имею, что такое URN, но в данном случае понятно, что речь идет о каком-то
стандартном для этой задачи идентификаторе. А теперь взгляните на разнообразные URN в
оригинальном запросе — URN, IPCURN, OffenseURN... Чувствуешь себя, как в сувенирной лавке
при крематории. 
       Вы меняете свое имя, переходя из комнаты в комнату? Конечно, нет! Вот так и корректное имя
элемента данных зависит от его смысла, а не расположения.

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

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


Обоснование 
       Главный принцип современного моделирования данных — разделение логической модели и
физической реализации. Это позволяет использовать одну и ту же модель на различных
платформах. 
       В старые добрые времена логическая и физическая реализации были неразделимы. Подробно
я буду говорить об этом в главе 3, пока же просто правило — не применяйте нестандартные
физические указатели. Они противоречат самой идее ключа в реляционной модели. 
       Стремление новообращенных программистов SQL использовать IDENTITY, GUID, ROWID или
другое нестандартное средство автонумерации в качестве ключа проистекает из привычки
работать с магнитными лентами. Им хочется знать, в каком порядке строки добавлялись к БД,
точно так же, как раньше им нужно было знать порядок добавления записей в конец магнитной
ленты!

Исключения 
       Ну разве что вам действительно зачем-то нужно имитировать файл последовательного
доступа с помощью таблицы SQL, не беспокоясь о целостности данных.
ГЛАВА 2.
Шрифты, пунктуация и интервалы
       Программы обычно отображаются с помощью моноширинных шрифтов. За более чем
столетнюю историю пишущих машинок и десятилетия перфокарт мы убедились, что в
моноширинном варианте код действительно легче читается. В частности, знаки препинания в этом
случае занимают такое же пространство, как и буквы, тогда как в пропорциональных шрифтах они
бы зачастую просто терялись.

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

Война
в Крыму.
Все в
в дыму.

       При первом прочтении практически никто не замечает, что один из предлогов “в” напечатан
дважды. Это — особенность того, как мы просматриваем блоки слов в вертикальном направлении.
Она важна при работе с кодом: при чтении программ глаз совершает множество вертикальных
движений, которых нет при чтении простого текста. 
       Несколько лет назад по Интернету гулял следующий текст:

“Солгсано унёчым из Кмерибжда, внажо, чотбы на сове мтесо в своле


пдалопаи паврея и псоелндяя бквуы. Дуигре бувкы мугот сяттоь в бе-
сопдярке, но ткест всё рнаво бдеут чтатьися без порлбем. Это сявпазо с
тем, что мзог човеекла чтаеит не оыетдльне бвкуы, а цыеле совла”.

       Я не уверен, что этот фрагмент действительно имеет своим источником Кембриджский
университет, но в качестве иллюстрации он нам подходит. Поскольку компилятор гарантирует
отсутствие в коде опечаток, читатель знает, что ему ожидать от следующего слова, с гораздо
большей уверенностью, чем при чтении обычного текста. Тут уж слова не просто воспринимаются,
как целое, они еще и ожидаемы. Скажем, увидев в программе на Паскале или другом языке из
семейства Алгола ключевое слово “IF”, я ожидаю, что выражение закончится ключевым словом
“THEN”. 
       Давайте поговорим о некоторых основных правилах набора программ, которые опираются на
то, как люди читают код.
Используйте в именах только буквы, цифры и символы
подчеркивания
Обоснование 
       Предложенный набор символов будет работать в любом языке программирования. Очень
удобно использовать одни и те же имена, как в базе данных, так и в хост-программе для работы с
ней. 
       Например, в некоторых продуктах SQL допускается использование символа “#”, но в других
языках программирования он имеет особое значение, и применять его в именах нельзя.

Исключения 
       На компьютерах, которые программируются с помощью перфокарт, вам придется
ограничиться только буквами верхнего регистра. Впрочем, трудновато вообразить такую ситуацию
в XXI веке. 
       В некоторых реализациях SQL определенные имена обязательно должны начинаться со
спецсимволов, не оставляя вам выбора. Например, в диалектах Sybase/SQL Server T-SQL имена
временных таблиц должны начинаться с символа #, а имена параметров — с символа @. Однако
следите за тем, чтобы имена сохраняли свою уникальность и после удаления спецсимвола. Это
облегчит перенос программ на более современные реализации. 
       Не используйте в качестве первого или последнего символа имени символ подчеркивания. Он
заставляет думать, что у имени пропущена некая составная часть. Подчеркивания в начале или
конце имени также теряются при чтении напечатанной программы. И уж, конечно, не ставьте два
или более символа подчеркивания подряд. На распечатке, полученной с помощью качественного
лазерного принтера, пересчитать их будет невозможно.

Используйте буквы нижнего регистра в скалярах — именах


столбцов, параметрах и переменных
Обоснование 
       Тексты книг и газет набирают в нижнем регистре, потому что он читается легче, да и быстрее.
Это основы типографики, подтверждаемые исследованиями, которые впервые были проведены
Вудвортом (1938), а позднее повторены Фишером (1975). Участников просили прочитать
сравнимые объемы текста, напечатанные только в верхнем или только в нижнем регистре. В обоих
исследованиях текст нижнего регистра прочитывался участниками на 5-10% быстрее.

Исключения 
       Единственное исключение, которое приходит в голову, — это опять же перфокарты.

Начинайте имена объектов схемы с прописной буквы


Обоснование 
       Объекты схемы — это таблицы, представления, хранимые процедуры и т.п. Во многих языках
с прописной буквы начинаются предложения и имена собственные. Читатели ожидают подобного и
от имен SQL He стоит их разочаровывать.

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

Записывайте прописными буквами зарезервированные слова


Обоснование 
       Слова, набранные в верхнем регистре, воспринимаются как целое, а не как
последовательность слогов или букв. Они притягивают взгляд и производят впечатление важности
— именно на этом основано их применение в заголовках и вывесках. Типографы обозначают
форму слова термином Боита,  который описан в книге Сэнгера (1975). Представьте себе, что
каждая буква окружена прямоугольной рамкой. В слове, набранном строчными буквами, эти рамки
будут разной высоты и располагаться будут на разных уровнях (из-за надстрочных и подстрочных
элементов букв). В результате, форма слова окажется “зубчатой”, словно оно построено из
кирпичиков разного размера. 
       Форма слова, набранного прописными буквами, — всегда простой и ровный прямоугольник,
который зрительно выделяется, даже будучи окруженным строчными буквами. Сравните
выражения:

Select а, Ь, с from foobar where flob = 23;

SELECT a, b, с FROM Foobar WHERE flob = 23;

       Видите, как легко выделить операторы при чтении? Еще быстрее код будет читаться при
расположении каждой конструкции на отдельной строке:

SELECT a, b, с
FROM Foobar
WHERE flob = 23;

       Впрочем, о правилах вертикального набора мы поговорим позже.

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

       Ключевые слова бывают двух видов — зарезервированные и незарезервированные.


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

<ключевое слово ::= Зарезервированное слово> | <незарезервированное слово>


<незарезервированное слово> ::= ADA
| С | CATALOG_NAME | CHARACTER_SET_CATALOG | CHARACTER_SET_NAME
| CHARACTER_SET_SCHEMA | CLASS_ORIGIN | COBOL | COLLATI0N_CATALOG
| COLLATION.NAME | COLLATION_SCHEMA | COLUMN JIAME | COMMAND_FUNCTION
| COMMITTED
| CONDITION_NUMBER | CONNECTION_NAME | CONSTRAINT_CATALOG
| CONSTRAINT_NAME
| CONSTRAINT_SCHEMA | CURSOR_NAME
| DATA | DATETIME_INTERVAL_CODE
| DATETIME_INTERVAL_PRECISION | DYNAMIC_FUNCTION
| FORTRAN
| LENGTH
| MESSAGE_LENGTH | MESSAGE_OCTET_LENGTH | MESSAGE_TEXT | MORE | MUMPS
| NAME | NULLABLE | NUMBER
| PASCAL | PLI
| REPEATABLE | RETURNED_LENGTH | RETURNED_OCTET_LENGTH
| RETURNED_SQLSTATE
| ROW.COUNT
| SCALE | SCHEMA_NAME | SERIALIZABLE | SERVER_NAME | SUBCLASS_ORIGIN
| TABLE_NAME | TYPE
| UNCOMMITTED | UNNAMED
Зарезервированное слово> :: =
ABSOLUTE | ACTION | ADD | ALL | ALLOCATE | ALTER | AND
| ANY | ARE | AS | ASC
| ASSERTION | AT | AUTHORIZATION | AVG
| BEGIN | BETWEEN | BIT | BIT_LENGTH | BOTH | BY
| CASCADE | CASCADED | CASE | CAST | CATALOG | CHAR | CHARACTER
| CHAR_LENGTH
| CHARACTER_LENGTH | CHECK | CLOSE | COALESCE | COLLATE | COLLATION
| COLUMN | COMMIT | CONNECT | CONNECTION | CONSTRAINT
| CONSTRAINTS | CONTINUE
| CONVERT | CORRESPONDING | COUNT | CREATE | CROSS | CURRENT
| CURRENT_DATE | CURRENT_TIME | CURRENT_TIMESTAMP | CURRENT_USER
| CURSOR
| DATE | DAY | DEALLOCATE | DEC | DECIMAL | DECLARE | DEFAULT
| DEFERRABLE
| DEFERRED | DELETE | DESC | DESCRIBE | DESCRIPTOR | DIAGNOSTICS
| DISCONNECT | DISTINCT | DOMAIN | DOUBLE | DROP
| ELSE | END | END-EXEC | ESCAPE | EXCEPT | EXCEPTION
| EXEC | EXECUTE | EXISTS
| EXTERNAL | EX1RACT
| FALSE | FETCH | FIRST | FLOAT | FOR | FOREIGN | FOUND | FROM
| FULL
| GET | GLOBAL | GO | GOTO | GRANT | GROUP
| HAVING | HOUR
| IDENTITY | IMMEDIATE | IN | INDICATOR | INITIALLY | INNER | INPUT
| INSENSITIVE | INSERT | INT | INTEGER | INTERSECT | INTERVAL | INTO
| IS
| ISOLATION
| JOIN
| KEY
| LANGUAGE | LAST | LEADING | LEFT | LEVEL | LIKE | LOCAL | LOWER
| MATCH | MAX | MIN | MINUTE | MODULE | MONTH
| NAMES | NATIONAL | NATURAL | NCHAR | NEXT | NO | NOT | NULL
| NULLIF | NUMERIC
| OCTET_LENGTH | OF | ON | ONLY | OPEN | OPTION | OR
| ORDER | OUTER
| OUTPUT | OVERLAPS
| PAD | PARTIAL | POSITION | PRECISION | PREPARE | PRESERVE | PRIMARY
| PRIOR | PRIVILEGES | PROCEDURE | PUBLIC
| READ | REAL | REFERENCES | RELATIVE | RESTRICT | REVOKE | RIGHT
| ROLLBACK | ROWS
| SCHEMA | SCROLL | SECOND | SECTION | SELECT | SESSION
| SESSION_USER | SET
| SIZE | SMALLINT | SOME | SPACE | SQL | SQLCODE | SQLERROR | SQLSTATE
| SUBSTRING | SUM | SYSTEM_USER
| TABLE | TEMPORARY | THEN | TIME | TIMESTAMP | TIMEZONE_HOUR
| TIMEZONE_MINUTE
| TO | TRAILING | TRANSACTION | TRANSLATE | TRANSLATION | TRIM
| TRUE
| UNION | UNIQUE | UNKNOWN | UPDATE | UPPER | USAGE | USER | USING
| VALUE | VALUES | VARCHAR | VARYING | VIEW
| WHEN | WHENEVER | WHERE | WITH | WORK | WRITE
| YEAR
| ZONE

       Специфические зарезервированные слова могут встречаться в конкретных реализациях Их


тоже нужно набирать в верхнем регистре

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


Обоснование 
       Наш глаз приучен к словам, у которых прописная буква может стоять лишь в начале. Встретив
слово с Переменным регистром, мозг автоматически разбивает его на части. В частности, слово,
которое содержит прописную букву, но начинается со строчной, читатель сначала “просканирует”
до прописной буквы, а затем вернется назад, чтобы осмыслить его начальные слоги. 
       Вторая проблема состоит в выработке соглашения об использовании прописных букв в словах
с переменным регистром. Можно ведь, например, писать и “upcCode”, и “UpcCode”, и “UPCcode”, и
“UPCCode”. Кончится это тем, что одно и то же имя будет у вас присутствовать в нескольких
вариантах. 
       Читать подобные слова еще сложнее, если регистр в них меняется несколько раз
(“пЕрЕМеНнЫй РЕгисТр”), и это опять-таки подтверждается исследованиями. Смит (1969) и
Мейсон (1978) доказали это с помощью проверки скорости чтения. Полацек, Велл и Шиндлер
(1975) проверяли способность пациентов находить одинаковые слова. Мейер и Гучера (1975)
показали, что при перемешивании регистров увеличивается время принятия решений о категориях
слов.

Исключения 
       В некоторых словах использование Переменного регистра является естественным
(“MacDonald”). В качестве имен объектов их использование допустимо. Но в именах скаляров
применять Переменный регистр не следует.

Пробелы
       Разделяйте элементы языка пробелами, не давайте им сливаться в непрерывный поток.
Пишите, например, “foobar = 21”, а не “foobar=21”, как это часто делается. Многие современные
программисты еще застали времена перфокарт, когда длина строки была ограничена и на
пробелах приходилось экономить. Программу на языке управления заданиями (job control
language, JCL) для семейства IBM/360, скажем, можно написать, вообще не используя пробелы.
Современные языки программирования не связаны столь жесткими ограничениями, и мы наконец
можем писать программы так, словно люди важнее компьютеров.

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

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

Применяйте естественные правила пунктуации


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

SELECT flobs a flobs.b,flobs.c,fleq.d FROM Flobs,Foobar.Fleq;

SELECT flobs a, flobs.b, flobs.c, fleq.d FROM Flobs, Foobar, Fleq;

       2. Размещайте запятые в конце строки, а не в ее начале. Запятая, точка сзапятой, знак
вопроса или точка являются признаками конца, а не начала. Поместив запятую в начале строки,
вы заставляете глаз смещаться влево в поисках пропущенного слова:

SELECT flobs.a ,flobs.b ,flobs.с ,fleq d FROM Flobs ,Fleq

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

SELECT flobs.a, flobs.b, flobs.c, --взаимосвязанная группа


fleq.d FROM Flobs, Fleq;

       3. Начинайте операторы с новой строки или по крайней мере ставьте пробел после точки с
запятой. 
       4. Разделяйте слова пробелами, даже если делать это необязательно.
Исключения 
       Поскольку SQL все-таки отличается от “разговорного” языка, иногда приходится делать
уступку его синтаксическим правилам. 
       Многие привычки по оформлению программ уходят корнями в перфокарточное прошлое и в
наш век мониторов и текстовых редакторов утратили всяческое основание. Например, практика
размещения в строке только запятой и имени переменной возникла в те времена, когда каждая
строка представляла собой перфокарту. Программисты таким образом облегчали себе изменение
кода — чтобы заменить один вариант кода другим, достаточно было вытащить из стопки одну
перфокарту и заменить ее другой. При наличии монитора оправдания у этой практики нет
никакого. 
       Да и вообще нет нужды слишком “вытягивать” код по вертикали. В европейских языках мы
читаем слева направо и только потом сверху вниз. Прочитайте, например, такое слово:

В
Е
Р
Т
Т
И
К
А
Л
Ь
Н
О.

       Заметили опечатку? Около трети читателей не замечают. Точно так же трудно отлавливать
ошибки в длинных вертикальных списках имен. В ряде случаев вертикальное выравнивание
операторов SQL действительно позволяет делать код более наглядным, но злоупотреблять им не
следует.

Не сокращайте зарезервированные слова


Обоснование 
       SQL позволяет пропускать одни зарезервированные слова и сокращать другие. Но вы все-таки
по возможности используйте в программах их полные версии. Это правило оправдывало себя в
Коболе, работает оно и в SQL 
       Скажем, при создании псевдонима ключевое слово AS можно пропустить, написав, например,
в конструкции FROM не “Personnel AS PI”, а просто “Personnel PI”, или поставив в списке SELECT
не “(salary + commission) AS totaljpay”, a “(salary + commission) total_pay”. Но в подобных ситуациях
слово AS определенно подчеркивает, что речь идет именно о псевдониме, а не о простом
перечислении. 
       Слово INTEGER можно сократить до INT, a DECIMAL — до DEC, но здесь очевидно
предпочтительны полные варианты. Первое сокращение можно перепутать с зарезервированным
словом “into” а второе — с английским сокращением декабря (“Dec”).

Исключения 
       Исключение составляют краткие обозначения символьных типов данных — CHAR(n) вместо
CHARACTER(n), VARCHAR(n) вместо VARYING CHARACTER(n), NCHAR(n) вместо NATIONAL
CHARACTER(n) и NVARCHAR(n) вместо NATIONAL VARYING CHARACTER(n). Полные названия
слишком длинны, и читать их неудобно. Что ж, некоторые сокращения допускаются даже в Коболе,
самом многословном языке программирования на земле.

Отдавайте предпочтение стандартным ключевым словам SQL


Обоснование 
       Иногда у вас есть выбор между стандартной конструкцией SQL и ее реализацией в конкретном
продукте. Отдавая предпочтение стандартному варианту, вы сделаете свой код понятным
программисту, который не знает вашего диалекта. Кроме того, вашу программу не нужно будет
переписывать, чтобы запустить в другом SQL-продукте. Стандартный код не принесет вам
неприятностей, когда вариант из конкретной реализации будет изменен или вообще отброшен.
Этот неприятный сюрприз уже случился однажды, когда создатели нескольких продуктов внесли в
них версии оператора OUTER JOIN из стандартного SQL и отказались от собственных
нестандартных реализаций. Например, программистам, работавшим с SQL Server, пришлось
отвыкать от оператора “*=”. 
       Другой недостаток нестандартных конструкций — их переменчивость и непредсказуемость.
Например, от продукта к продукту SQL Server варьируется совместимость типа данных BIT со
значением NULL Oracle не отличает NULL и пустую строку. Таких примеров множество. Поскольку
внешнего стандарта нет, разработчик волен делать, что ему заблагорассудится.

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

Отдавайте предпочтение стандартным конструкциям SQL


Обоснование 
       Помимо очевидных преимуществ использования стандартных конструкций, замечу также, что
нестандартные конструкции действительно зачастую непредсказуемы! В электронной
документации к Microsoft SQL Server имеется следующий комментарий к синтаксису конструкции
UPDATE.. FROM..:

“Результаты выполнения оператора UPDATE не определены, если в нем имеется


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

       Эта неопределенность пришла на смену прежнему поведению, принятому в семействах


Sybase и Ingres, когда оператор UPDATE.. FROM., производил многократные обновления — по
одному для каждой присоединяемой строки второй таблицы. 
       В старых версиях Sybase/SQL Server, если во внедренном запросе строка базовой таблицы
фигурировала более одного раза, эта строка и обрабатывалась несколько раз. Реляционные
принципы при этом нарушаются полностью, но в конкретной физической реализации добиться
такого поведения проще простого. Вот наглядный пример:

CREATE TABLE T1 (х INTEGER NOT NULL);


INSERT INTO T1 VALUES (1);
INSERT INTO T1 VALUES (2);
INSERT INTO T1 VALUES (3);
INSERT INTO T1 VALUES (4);
CREATE TABLE T2 (x INTEGER NOT NULL);
INSERT INTO T2 VALUES (1);
INSERT INTO T2 VALUES (1);
INSERT INTO T2 VALUES (1);
INSERT INTO T2 VALUES (1);

       Теперь попробуем обновить Т1, удвоив значения в тех строках, значения в которых совпадают
с одним из значений таблицы Т2:

UPDATE T1 SET Т1.х = 2 * Т1.х FROM T2 WHERE T1.x = Т2.х; SELECT


* FROM T1; Реальное Ожидаемое X X ======== ======== 16 2 2 2 3 3
4 4
       Конструкция FROM возвращает перекрестное произведение (CROSS JOIN), поэтому одна и та
же операция над одной и той же строкой повторяется четырежды (1 => 2 => 4 => 8 => 16). Пример
очень простой, но суть вы уловили. Есть такие тонкости в произведениях таблиц, которые вместе с
мутировавшим синтаксисом T-SQL, приводят к зацикливанию изменений, к таблицам, результат
обработки которых зависит от порядка строк, и т.д. 
       В более поздних версиях SQL Server и Sybase эта проблема решена по-разному. В Sybase,
например, в неявном запросе скрыто выполняется оператор “SELECT DISTINCT”. А вот Standard
SQL отличается согласованностью и ясностью в отношении псевдонимов, представлений и
производных таблиц. 
       Согласно модели Standard SQL, если бы в операторе UPDATE можно было использовать
псевдоним, то это означало бы создание копии содержимого базовой таблицы с этим
псевдонимом, которая затем обновлялась бы и удалялась по окончании работы оператора — с
базовой таблицей фактически ничего бы не происходило. Если бы в операторе UPDATE можно
было использовать конструкцию FROM, это означало бы создание результирующего набора, его
обновление и удаление по окончании работы оператора — снова без внесения изменений в
базовую таблицу. 
       Почему вообще существует такой специфический, двусмысленный и противоречащий
стандартной модели синтаксис? В оригинальном продукте Sybase физическая модель была
такова, что подобное “расширение” реа-лизовывалось относительно легко. Ни стандартов, ни
вообще глубокого понимания реляционной модели тогда не было. Программисты привыкли к
такому синтаксису, и исправить это было почти невозможно. 
       В середине 1970-х я жил в Индианаполисе. Моим соседом был выпускник частного колледжа
“Дженерал Моторс” (General Motors), работавший на эту компанию. На своей первой должности он
занимался отчетами о несчастных случаях на производстве. Как-то вечером за пивом он принялся
рассказывать мне реальные истории, случившиеся на разных заводах “Дженерал Моторс”.
Проведя на своей должности год, он пришел к выводу, что все несчастные случаи на производстве
представляют собой замысловатые попытки самоубийства. Чтобы хоть немного ускорить работу,
рабочие изобретали хитроумные приспособления, позволяющие обойти защиту оборудования.
Допустим, можно сделать зажим, который будет удерживать один из двух защитных
переключателей небольшого штампующего станка. Теперь вы можете одной рукой нажимать
вторую кнопку, а другой рукой — быстро вставлять в станок деталь. Конечно, работает эта схема
лишь до тех пор, пока в станок не попадает сама эта другая рука. Пренебрежение безопасностью и
корректностью ради скорости рано или поздно выходит боком.

Исключения 
       Ваш SQL продукт может не поддерживать стандартный синтаксис для некоторых операций.
Например, в Oracle нет поддержки выражения CASE, зато имеется функция DECODE(),
выполняющая примерно те же операции.

Коридоры и вертикальное выравнивание


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

SELECT 11.incidentjtype, IPC.defendant_type, R1.notes,


O1.start_date, O1.end_date, 01.reported_datetime, IPC.urn
FROM Incidents AS 11, IPC, Recommendations AS R1, Offences AS 01,
WHERE IPC.recommendation_id = R1.recommendation_id
AND IPC.urn = 01.urn AND IPC.urn = 11.urn
AND IPC.urn = Ri.urn AND 11.urn = 01.urn;

       и с коридором (правые границы ключевых слов выровнены друг с другом):


SELECT I1.incident_type,
IPC.defendant_type,
R1.notes,
01.start_date,
01.end_date,
01.reportedatetime,
IPC.urn
FROM Incidents AS 11,
IPC,
Recommendations AS R1,
Offences AS 01,
WHERE IPC.recommendation_id = R1.recommendation_id AND IPC.urn = 01.urn
AND IPC.urn = 11.urn AND IPC.urn = FM.urn AND 11.urn = 01.urn;

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

Обоснование 
       В языке управления данными (data manipulation language, DML) нам необходим баланс между
отступами и коридорами. Обратите внимание, что каждый подзапрос внутри себя связан
коридором, а различные подзапросы выделены отступами:

SELECT DISTINCT pilot


FROM PilotSkills AS PS1
WHERE NOT EXISTS (SELECT *
FROM Hangar
WHERE NOT EXISTS (SELECT *
FROM PilotSkills AS PS2
WHERE PS1.pilot = PS2.pilot
AND PS2.plane = Hangar.plane));

Исключения 
       Подзапрос всегда заключается в скобки, так что можно считать приоритетным выравнивание
парных скобок:

SELECT DISTINCT pilot


FROM PilotSkills AS PS1
WHERE NOT EXISTS (SELECT *
FROM Hangar
WHERE NOT EXISTS (SELECT *
FROM PilotSkills AS PS2
WHERE PS1.pilot = PS2.pilot
AND PS2.plane = Hangar.plane
)
);

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

SELECT C1.cust.name,
C1.street_address,
C1.city, С1.state,
C1.zip,
P1.payment_1,
P1.payment_2,
P1.payment_3,
P1.payment_4,
P1.payment_5,
P1.payment_6,
P1.payment_7,
P1.payment_8,
P1.payment_9,
payment_10,
FROM Customers AS С1,
Payments AS P1
WHERE C1.cust_id = P1.cust_id;

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

Группируйте операторы с помощью пустых строк


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

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

Исключения 
       Отказаться от добавления пустых строк можно, если они приводят к нежелательным разрывам
в коде.
ГЛАВА 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). Попробуйте наложить на такой столбец
какое-либо ограничение.

Исключения 
       Нет. Имеются лучшие инструменты для сбора данных в свободном формате.
ГЛАВА 4. 
Шкалы и измерения
Прежде чем поместить данные в БД, вы должны тщательно продумать способы их представления
и обработки. Большинство программистов никогда не слышало о теории измерений, между тем,
без знакомства с ней эффективного способа представления информации не найти. Хотя тема этой
главы и не имеет прямого отношения к стилю программирования на SQL, она задаст основу для
решения вопросов, которые встают перед разработчиком любой схемы.

Теория измерений
Измеряйте все измеримое и старайтесь делать 
измеримым то, что пока таковым не является.
Галилей (1564-1642)

       Теорией измерений называется раздел прикладной математики, связанный с анализом


данных. Результат измерения — не то же самое, что измеряемый атрибут, а само измерение — не
просто процесс назначения числа предмету или его атрибуту. Измерение — это выделение
структурного свойства сущности, которое может быть выражено числом или другим вычисляемым
символом. Основу измерения составляет шкала, а упомянутые числа или символы представляют
собой единицы измерения. 
       Как ни странно, основы теории измерений лежат в области психологии, а не математики или
информатики. В частности, оригинальные идеи об уровнях измерений и о классификации шкал
принадлежат американскому психологу С.С.Стивенсу (S.S.Stevens). Шкалы разделяются на
несколько классов в зависимости от свойств, которыми обладают или не обладают. Нас будут
особо интересовать следующие свойства шкал. 
       1. Наличие начальной точки, или точки отсчета. Ее иногда называют нулем шкалы, но это
необязательно должно быть число 0. Например, в измерении расстояний между объектами
естественным началом отсчета является 0 метров — меньших расстояний не бывает. У
температурной шкалы начальная точка определяется абсолютным нулем — ничто не может быть
холоднее 0 градусов Кельвина. А вот у времени естественного начала отсчета нет: оно идет из
бесконечного прошлого в бесконечное будущее. 
       2. С единицами измерения можно выполнять определенные операции. Массы можно
складывать и вычитать, получая в результате также массу/ Складывать имена или размеры обуви
нельзя, но их можно сравнивать. 
       3. У единиц имеется естественный порядок. Можно говорить о том, что данное событие
произошло до или после другого события. Один предмет может быть тяжелее, длиннее или
горячее другого. Алфавитный порядок является скорее произвольным, чем естественным — если
те же предметы перечислить на другом языке, алфавитный порядок будет иным. 
       4. Для любой пары величин существует естественная метрика. Метрика не имеет
отношения к Метрической системе (СИ). Это функция, которая подчиняется следующим трем
правилам. 
       a. Метрика между объектом и им самим равна нулю шкалы. В математической нотации это
свойство записывается так: М(а, а) = 0. 
       b. От перемены мест объектов метрика не меняется. В математической нотации М(а, b) = M(b,
а). 
       c. Существует такая операция сложения, что М(а, b) + М(b, с) > М(а, с). Это неравенство
известно как неравенство треугольника. 
       Несмотря на простой арифметический вид эти выражения имеют более общий смысл. Нуль в
первом свойстве представляет собой точку отсчета шкалы и необязательно является числом 0. В
математической записи третьего правила фигурируют арифметические знаки “плюс” и “больше
или равно”, но на самом деле они символизируют более общие отношения упорядочения. Знак
“больше или равно” относится к естественному порядку измеряемых атрибутов. Знаком “плюс”
отмечена некая осмысленная операция в отношении этого порядка, а не только арифметическое
сложение. 
       Наличие операций сложения и сравнения особенно полезно, поскольку означает, что
измерения можно проводить в числах и что с этими числами можно выполнять простые
преобразования. Например, восприятие человеком звука и света примерно пропорционально
кубическому корню интенсивности, то есть, если удвоить интенсивность света или звука, то
восприятие увеличится всего на 20%. Более формально это звучит так: воспринимаемая
интенсивность равна физической интенсивности в степени 0,3. Зная об этом, дизайнеры звуковой
техники используют регуляторы громкости, шкалы которых размечены равноотстоящими
штрихами, хотя на самом деле они работают в логарифмической шкале. 
       Метрика шкалы необязательно обладает всеми тремя метрическими свойствами. Рассмотрим
в качестве примера способ измерения расстояний не в единицах длины, а в единицах работы —
старая китайская система, в которой использовались разные единицы расстояния для движения в
гору и под гору. Обладает ли эта система свойством M(a, a) = 0? Да. При перемещении туда, где
вы уже находитесь, работа не совершается. Обладает ли она свойством М(а, b)= М(b}  а)?  Нет. Под
гору идти легче, чем в гору. Подчиняется ли она неравенству M(a, b) +M(b, с) >М(а, с)?  Да. Работа,
совершаемая при прямом перемещении из пункта А в пункт Б, всегда будет меньше либо равна
работе, совершаемой с промежуточными остановками и отклонениями от прямого пути.

Непрерывные и дискретные величины


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

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

Градуировка, погрешность и точность


       Сравните обычную линейку со штангенциркулем. Оба предназначены для измерения длины,
оба работают в одной системе единиц — но есть важное различие. Штангенциркуль обладает
большей точностью благодаря более тонкой градуировке. Градуировка представляет собой
свойство шкалы, зависящее, например, от размера минимального деления на линейке. В Европе
на линейках используются миллиметровые деления, в США — деления в 1/32 дюйма. 
       Погрешность определяет близость измеренного значения к реальной величине. От точности
зависит воспроизводимость измерения. И погрешность, и точность зависят от градуировки, но они
отличаются друг от друга. Попробую объяснить разницу на примере со стрелковыми мишенями,
которые представляют собой ни что иное как шкалы для оценки меткости стрелка. У большой
мишени диапазон больше, чем у маленькой. Чем больше колец на мишени, тем выше ее
разрешение. Начинаем стрелять. Чем ближе попадания к центру мишени (к желаемой цели), тем
ниже погрешность. Чем ближе они друг к другу, тем выше точность (воспроизводимость
результата). Разница между точностью и погрешностью очевидна: если я хороший стрелок, но у
моей винтовки сбит прицел, выстрелы лягут кучно, но в стороне от “яблочка”. 
       Говоря о точности и погрешности, не стоит предаваться самообману. Исследования говорят,
что впечатление от числа пропорционально квадрату количества цифр после запятой. Компьютер
провоцирует многих людей на использование длинных десятичных “хвостов”, хотя бы это и не
имело никакого смысла. Например, при строительстве дорог в США в качестве минимальной
единицы измерения используется десятая доля фута. Более высокая точность просто не нужна, но
сколько студентов инженерных специальностей сдает свои работы с размерами, указанными с
точностью до десятитысячной доли фута? Ну не прикладывают штангенциркуль к асфальту! 
       Впрочем, базы данных, как правило, при многих вычислениях задавать точность просто не
позволяют. На самом деле, стандарт SQL допускает разное количество цифр после запятой в
результатах многих арифметических операций в зависимости от конкретной реализации.

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

Шкалы наименований
       Шкала наименований — простейшая из шкал. В ней каждому элементу измеряемого
множества назначается уникальный символ — обычно номер или имя. Например, список городов
представляет собой шкалу наименований. Конечно, с философской точки зрения многие люди не
сочтут составление списка измерением. Поскольку невозможно четко сказать, какое свойство
измеряется, не должно быть и шкалы, скажут они. 
       У множества (в математическом смысле этого слова) нет естественной точки отсчета, а
значит, нет и упорядоченности. Мы, как правило, выстраиваем имена в алфавитном порядке, но с
тем же успехом могли бы выстраивать их по длине, или по частоте встречаемости, или еще по
какому-нибудь атрибуту. Единственная осмысленная операция со шкалой наименований —
сравнение. Можно задать вопрос: “Этот город — Нью-Йорк?” Возможные варианты ответа — “да”,
“нет”, “неизвестно”. В базах данных шкалы наименований встречаются очень часто, поскольку их
применяют в качестве уникальных идентификаторов.

Шкалы категорий
       Шкала категорий, пожалуй, следующая по простоте. В ней сущность приписана к некой
категории, которой назначен уникальный символ — опять же, номер или имя. Например, животные
разделяются на рептилий, млекопитающих и пр. Чтобы категории имели смысл, они должны
попадать в пределы одного класса. 
       Многие не считают измерением и разделение на категории. Отнесение к той или иной
категории может определяться большим набором свойств, с чем связаны две потенциальные
проблемы. Во-первых, сущность может попадать в несколько категорий. Например, утконос
представляет собой теплокровное, яйцекладущее животное с мехом. Млекопитающие являются
живородящими, теплокровными и мехом обладают далеко не всегда. Во-вторых, сущность может
не попасть ни в одну из имеющихся категорий. Вот найдем мы на Марсе создание с мехом и
хлорофиллом, и готовой категории для него не окажется. 
       Напрашиваются два решения: создать новую категорию (отряд однопроходных для утконосов
и ехидн) или разрешить сущности относиться к нескольким категориям. В собрании подмножеств
нет естественного начала отсчета и упорядочения. Единственная осмысленная операция со
шкалой категорий — проверка принадлежности к множеству: “Это млекопитающее?” — с
возможными вариантами ответа “да”, “нет”, “неизвестно”.

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

Порядковые шкалы
       В порядковых шкалах имеется упорядоченность, но нет начала отсчета и допустимых
операций. Например, геологи применяют для обозначения твердости минерала шкалу Mooca
(Moh's Scale for Hardness, MSH), основанную на упорядоченном наборе стандартных минералов:
тальк имеет твердость 1, гипс — 2, кальцит — 3, флюорит — 4, апатит — 5, ортоклаз — 6, кварц —
7, топаз — 8, корунд — 9, алмаз — 10. 
       Чтобы определить твердость неизвестного минерала, нужно попробовать провести им черту
по отшлифованной поверхности одного из стандартных минералов; если на поверхности остается
царапина, исследуемый минерал тверже. Обратите внимание, что можно получить один и тот же
результат для двух минералов, твердости которых близки, но не одинаковы, а также что возможны
минералы мягче нижнего предела или тверже верхнего. У шкалы Мооса нет начала отсчета, и
операции с ней невозможны (сложив 10 тальковых единиц, мы все равно не получим алмаз). 
       Вероятно, чаще всего в наши дни приходится встречаться с порядковыми шкалами в
различных опросах, когда вам представляют некое утверждение и предлагают выразить степень
согласия или несогласия с ним. Возможные варианты обычно даются в виде ответов,
варьирующихся от “совершенно согласен” до “совершенно не согласен”. 
       Рассмотрим еще один пример — попарный выбор сортов мороженого. Сказав, что шоколадное
мороженное вкуснее ванильного, вы можете считать, что изрекаете непреложную истину, но
выразить эту непреложность в виде числа у вас не получится. Отсутствие числовой меры
означает, что результаты подобных опросов бессмысленно усреднять; все, что вы можете сделать
— это представить их в виде гистограммы с количеством респондентов в каждой категории. 
       Еще один недостаток порядковых шкал — возможное отсутствие
транзитивности. Транзитивностью называется следующее свойство отношений: если верны
утверждения R(a, b) и R(b, с),  то верно и утверждение R(a, с).  Например,
если а  тяжелее b  и b  тяжелее с, то а тяжелее с.  В реальном мире, где имеются отношения
“тяжелее”, “старше” и т.п., это свойство кажется неизбежным. Однако в случае с мороженым все не
так очевидно. Если в кафе не оказалось шоколадного мороженого, его посетители могут
предпочесть ванильное — банановому, банановое — ореховому, ореховое — ванильному.
Различия начинают приобретать философский оттенок, из-за чего многие люди отказываются
признать шкалой отношения, лишенные транзитивности.

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

Шкалы интервалов
       У интервальных шкал есть метрика, упорядоченность, допустимые операции с единицами, но
нет начала отсчета. Лучший пример интервальной шкалы — календарь. В качестве календарного
начала отсчета принято некое произвольное историческое событие, а все измерения выполняются
относительно него в идентичных интервальных единицах. 
       Метрикой является число дней между двумя датами. Проверим три метрических свойства.
(1) М(а, а) = 0:  количество дней между сегодня и сегодня равно нулю”. (2) M(a, b) = M(b, a): от
сегодняшнего дня до следующего понедельника столько же дней, сколько от следующего
понедельника до сегодняшнего дня. (3)M(a, b) + M(b, c) = M(a, c): сумма дней от сегодняшнего дня
до следующего понедельника и от следующего понедельника до Нового года равна количеству
дней от сегодняшнего дня до Нового года. Порядок дней естественен и неизменен: 1 июля 1900 г.
было раньше 1 июля 1993 г. Возможно произвольное объединение базовых единиц (дней) в другие
единицы (недели, месяцы, годы). 
       Не думайте, что в метрике можно применять только арифметическое сложение. Существуют
также и логарифмические шкалы, в которых сложение заменяется умножением, а вычитание —
делением. Например, логарифмическими являются шкала звездных величин, шкала Рихтера
оценки мощности землетрясений, шкала интенсивности звука (децибелы).
Шкалы отношений
       Именно о шкалах отношений думает большинство людей, когда речь идет об измерениях. У
этих шкал есть начало отсчета (обычно математический ноль), упорядоченность и сопоставленный
с ними набор арифметических операций. Их называют шкалами отношений, поскольку все
измерения выражаются относительно эталонной единицы или интервала. 
       Примером шкал отношений могут служить длина, масса и объем. Выбор единицы измерения
произволен: масса мешка с песком одна и та же, будь она выражена хоть в килограммах, хоть в
фунтах. Другое полезное свойство шкал отношений — независимость единиц от измеряемой
сущности: килограмм пуха весит столько же, сколько килограмм железа.

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

Табл. 4.1. Свойства шкал

Шкала Упорядоченность Начало Математические Пример


отсчета операции
Наименований Нет Нет Нет Названия населенных
пунктов
(“Звенигород”)
Категорий Нет Нет Нет Семейства животных
(псовые, кошачьи)
Абсолютная Да Да Да Десяток яиц
Порядковая Да Нет Нет Опросы (степени
согласия)
Рангов Да Да Нет Соревнования (места)
Интервалов Да Нет Да Время (часы, минуты)
Отношений Да Да Да Длина (метры), масса
(граммы)

       Не все, конечно, просто со шкалами. Возьмем, например, привычную температурную шкалу. У
нее есть начало отсчета, есть упорядоченность, но операция сложения к температуре
неприменима.

Преобразование шкал
       Шкалы можно выстроить в некую логическую последовательность, опираясь на то, какие
операции с ними возможны.
       Конкретный атрибут не обязан безусловно принадлежать к одной из этих шкал. Например, в
опросных листах часто смешивают порядковые и интервальные шкалы, считая, что измеряемый
атрибут представляет собой гладкую непрерывную функцию. Иными словами, при статистической
обработке предполагается, что варианты “совершенно согласен”, “согласен”,... “совершенно не
согласен” разделены примерно равными интервалами, хотя в реальности никаких интервалов
между ними нет. Шкалу, содержащую только значения “да” и “нет”, можно считать как крайним
случаем интервальной шкалы, так и крайним случаем абсолютной шкалы. 
       Важный принцип теории измерений состоит в том, что шкалы можно преобразовывать друг в
друга, если они принадлежат к одному типу и применяются для измерения одного атрибута.
Абсолютные шкалы преобразованию не поддаются — потому и называются абсолютными. Пять
яблок — это пять яблок, как бы вы их ни пересчитывали или раскладывали на столе. Шкалы
наименований преобразуются друг в друга при наличии между ними соответствия. Допустим,
вооружившись словарем, названия городов можно легко перевести с английского на польский.
Проблемы возникают, когда соответствие между шкалами не является взаимно однозначным. Во
многих европейских языках имеется слово “кузен”, которым обозначаются дети братьев и сестер
родителей. С точки зрения традиций, никаких различий между взаимоотношениями кузенов нет.
Иное дело — Китай. Там кузены обозначаются различными словами в зависимости от того,
являются ли они детьми брата или сестры одного из родителей. Не меньшее значение имеет и
возраст. Скажем, старший сын старшего брата вашего отца обозначается особым словом и
является специфическим видом кузена, перед которым у вас есть специфические социальные
обязательства. Трудности перевода, однако. 
       Порядковые шкалы преобразуются в порядковые шкалы посредством монотонных функций, то
есть, при преобразовании сохраняется упорядоченность. Скажем, для шкалы Мооса можно
выбрать другой набор минералов, металлов, керамики, но если один камень мягче другого, то это
отношение не изменится при переходе от одной шкалы к другой. Конечно, и тут возможны
проблемы с однозначностью преобразования. Например, может оказаться, что новая шкала
способна выявить различия в твердости, которых шкала Мооса не показывала. 
       Преобразование шкал рангов также осуществляется с помощью монотонных функций, и с той
же потенциальной неоднозначностью. Обобщая проблемы преобразования порядковых шкал и
шкал наименований, можно сказать так: сущности, которые согласно одной шкале кажутся
одинаковыми, в другой шкале оказываются различными. Это связано с различиями в диапазонах и
градуировке. 
       Одну интервальную шкалу можно получить из другой с помощью линейного преобразования,
имеющего вид у = а* х + b.  Оно сохраняет порядок, но смещает начальную точку. Например,
градусы Цельсия превращаются в градусы Фаренгейта по формуле F = 9,0 / 5,0 * С + 32. 
       Шкалы отношений преобразуются друг в друга посредством умножения на постоянный
множитель, поскольку порядок и начало отсчета у них одинаковые. Допустим, чтобы превратить
массу из килограммов в фунты, нужно воспользоваться формулой p = 0,4536 * k.

Производные единицы
       Во многих используемых нами шкалах применяются не основные, а производные единицы,
которые составляются из основных: километры в час (время и расстояние) или квадратные
километры (расстояние и расстояние). Для создания производных единиц годятся только шкалы
отношений и интервалов. Результатом комбинирования абсолютной шкалы со шкалой отношений
или интервалов является не измерение, а статистический параметр. Например, объединив вес
(шкала отношений) и количество жителей Нью-Йорка (абсолютная шкала), мы можем вычислить
средний вес ньюйоркца, что является статистической характеристикой, а не измерением. 
       Система единиц СИ основана на семи основных единицах (метр — длина, килограмм —
масса, секунда — время, ампер -- сила тока, градус Кельвина — температура, моль — количество
вещества, кандела — сила света). В стандарте ISO 2955 (“Information processing — Representation
of SI and other units for use in systems with limited character sets”) описаны обозначения единиц СИ в
символах ASCII. В эти обозначения включены скобки, пробелы, символы умножения (точка
посреди строки), деления (косая черта) и возведения в степень (верхний индекс). У большинства
распространенных производных единиц также имеются собственные имена. Например, 10 кг • м /
с2 — это 10 ньютонов (единиц силы).

Разделители и обозначения единиц


       В базах данных результаты измерений хранятся в двоичном формате, но при вводе и выводе
этих данных человек ожидает увидеть их в виде читаемых символов и разделителей. Перед
числовыми данными, после них и даже посреди может стоять явное или неявное обозначение
единицы измерения. 
       Если я напишу $25,15, вы поймете, что в качестве единицы измерения использован доллар —
на это указывает символ, стоящий перед числом. В записи “160 кг” сокращенное обозначение
единицы — килограмма — стоит после числа. Строка “12 марта 1989 г.” очевидно представляет
собою дату — на это указывают название месяца, пробелы и сокращенное обозначение года.
Однако вы без труда распознаете дату и в строке “12.03.1989”. 
       В базах данных разделители, как правило, не хранятся. Исключение составляют
нестандартные типы данных MONEY и CURRENCY, имеющиеся во многих реализациях SQL
Разделители и единицы нужны лишь для отображения данных, поэтому их хранение в самой БД
представляет собой напрасный расход ресурсов. Можно, конечно, разместить обозначения единиц
в отдельном столбце по соседству со столбцом с числовыми данными, но такая таблица будет
выглядеть неуклюже. Если все числа выражены в одних и тех же единицах, специальный столбец
для них не нужен. Если числа выражены в разных единицах, перед проведением вычислений вам
все равно придется привести их к единой шкале. Почему бы не соблюсти единство с самого
начала? По миру ходит немало страшных историй о том, как в международных компаниях с
отделениями в США и Европе перепутывались английская и международная системы мер, что
приводило к различным печальным последствиям. 
       В идеале администратор должен обеспечить внутреннее единство единиц измерения во всей
базе данных. Если без использования различных единиц не обойтись, их можно показывать
пользователю посредством представления, в котором будут спрятаны все необходимые
преобразования. При этом офис в США увидит данные в английских единицах, а офис в Европе —
в единицах СИ, и никто не узнает, что перед выводом данные как-то преобразуются.

Рекомендации по использованию шкал в базах данных


       В этом разделе приведены общие рекомендации по использованию шкал в БД. Не стоит
воспринимать их как жесткие правила — у всех у них есть исключения. 
       1. Как правило, чем шире круг возможных преобразований шкалы, тем ограниченнее
возможности ее статистической обработки. По измерениям в шкале отношений можно
вычислить практически любую статистику. К измерениям в шкале наименований статистика почти
не применима. 
       2. С помощью конструкций СНЕСК() в объявлении таблицы обеспечьте включение в БД
только допустимых значений. Если ваш SQL-продукт допускает создание доменов из стандарта
SQL-92, используйте эту возможность для создания шкал. У шкал наименований имеется список
допустимых значений; у других шкал можно проверять диапазон. С помощью конструкции
DEFAULT обязательно задавайте значениепо умолчанию — начальную точку шкалы, NULL или
другое. 
       3. В десятичных дробях задавайте на одну цифру после запятой больше, чем требуется
для представления данных. В большинстве реализаций SQL ошибки округления уменьшаются
при увеличении количества цифр после запятой. К сожалению, точность представления чисел и
правила округления меняются от реализации к реализации, поэтому один и тот же запрос на
разных продуктах может давать немного различающиеся результаты. С другой стороны, в отличие
от многих старых систем SQL более милосерден: он позволяет администратору изменять
параметры числового столбца (точность и диапазон) безпотери существующих данных и запросов.
Для отображения большегоколичества цифр могут понадобиться изменения в хост-программе. 
       При работе с валютами вам, возможно, придется поближе познакомиться с законами и
бухгалтерскими правилами. В Евросоюзе имеются собственные правила по расчетам в евро, в
США — по расчетам в долларах. 
       4. По возможности храните в БД данные в основных единицах. Это не всегда возможно,
поскольку зачастую в результатах измерения фигурируют только производные единицы. Взгляните
на манометр насоса, которым вы накачиваете шины: он проградуирован в Паскалях (ньютонах на
квадратный метр), и вы не сможете разделить его показания на ньютоны и квадратные метры.
Никогда не храните в одной и той же таблице и основные единицы, и вычисляемые по ним
производные единицы. Это не только напрасный расход памяти, но и источник возможных ошибок:
если вы, обновив столбец с основными единицами, забудете пересчитать столбец с
производными единицами. Кстати, на большинстве компьютеров пересчет производных единиц
идет гораздо быстрее, чем их считывание с диска. 
       5. Придерживайтесь одних и тех же обозначений и единиц. В частности, не смешивайте
формат даты ISO и ANSI, не выражайте длину то в метрах, то в сантиметрах. В идеале, во всех
приложениях данные должны выражаться в одних и тех же единицах.
ГЛАВА 5. 
Схемы кодировки данных
       Никто не записывает данные сразу в БД. Сначала их соответствующим способом кодируют и
лишь затем помещают в столбец. Слова принадлежат к определенному языку и составляются из
букв; данные измерений записываются числами. Правда, к буквам и числам мы настолько
привыкли, что уже не рассматриваем их в качестве кодов. Часто мы также не задумываемся о том,
что одну и ту же сущность или свойство можно идентифицировать (и, следовательно,
закодировать) различными способами. Как обозначить пациента больницы — именем или
номером медицинского страхового полиса? Это может зависеть от того, для кого предназначается
база данных — для врача или для страховой компании. Как идентифицировать песню — по
названию, по позиции в альбоме или обоими способами? Или стоит включить в БД мелодию — в
виде нот или в виде файла мультимедиа? Никто не обучает программистов составлению схем
кодировки, поэтому зачастую они составляются “на лету”. Причем во многих случаях самодельной
схеме отдается предпочтение даже при наличии общепринятого стандарта кодирования.
Начинающие программисты считают, что о разработке схемы кодировки они вообще заботиться не
должны — это дело компьютера, пусть он и разбирается. С появлением SQL такое отношение
только укрепилось благодаря иллюзии, что любые ошибки проекта можно позже исправить с
помощью оператора ALTER. 
       Да, компьютер способен решить множество проблем, но программы для ввода и проверки
данных очень сложны и трудны в обслуживании. Запросы к базам данных с замысловатыми
схемами кодировки сложны в разработке и ресурсоемки в исполнении. При этом рано или поздно
разбираться с кодированием все равно придется человеку. Неудачные схемы кодировки
обязательно приводят к неверному вводу и выводу и в конце концов становятся причиной
появления некорректной модели данных.

Плохие схемы кодировки


       В качестве примера рассмотрим систему учета автомобилей в одном южном штате. Она
начиналась, как система с перфокартами, написанная на Коболе. Большинство читателей,
вероятно, слишком молоды, чтобы помнить перфокарты. Напоминаю: перфокарта — это лист
жесткой бумаги, на котором двоичному представлению символов соответствует набор пробитых и
непробитых позиций. Возможные позиции для отверстий выстроены в 80 вертикальных колонок,
каждая из которых используется для кодирования одного символа — отсюда фиксированная
длина строки. Перфокарты пробивались на специальном устройстве с клавиатурой наподобие
клавиатуры пишущей машинки: по мере того как оператор набирал текст, устройство
автоматически меняло перфокарты. 
       В исходной системе учета автомобилей на каждой карточке выделялась одна колонка для
одноразрядного кода типа автомобиля: частный, с наемным шофером, такси, грузовик,
маршрутный автобус и т.п. Шло время, и в систему добавлялись новые типы — для ветеранов
различных войн, для университетских выпускников, в общем, для любой группы лоббистов,
которая обладала достаточной политической властью, чтобы пробить себе право на специальный
номерной знак. 
       Скоро количество типов превысило 10, и одноразрядной системы для них хватать перестало.
Место для добавления еще одного разряда на перфокартах было, но в Коболе используются поля
фиксированной длины, и потому изменение разметки карты было невозможно без корректировки
программ и настройки перфорирующих устройств. 
       Поначалу проблема была решена следующим образом: оператор вместо цифры вводил знак
препинания, расположенный на одной с цифрой клавише. Сначала в схему попал один знак
препинания, за ним другой, и вскоре в системе кодов были все символы, соответствующие
верхнему ряду клавиш на клавиатуре. 
       К несчастью, размещение знаков препинания на клавиатуре менялось от устройства к
устройству, поэтому перед обновлением базы данных для каждого набора перфокарт приходилось
разрабатывать специальную программу для преобразования раскладки к оригинальным кодам
модели IBM 026. Такая практика продолжалась даже тогда, когда все эти устройства
переместились в свой механический рай. 
       Просто контролировать попадание кода в заданный числовой диапазон было нельзя.
Приходилось использовать простую программу, которая проверяла совпадение вводимого кода с
более чем 20 допустимыми значениями. Звучит как будто не очень внушительно, но учтите — за
один только квартал система должна была обработать более трех миллионов записей. 
       При этом нужно было еще помнить, на какой именно машине создана данная запись.
Естественно, количество ошибок было очень велико. Если бы код с самого начала сделали
двухразрядным (от 00 до 99), никаких проблем не возникло бы. Если бы я создавал эту систему
сегодня, я бы просто завел для номера столбец типа INTEGER и мог бы себе позволить столько
номеров, сколько понадобится. 
       О втором примере сообщило в 1987 г. издание Information Systems Week.  Собственно, вся
история была заключена в первом предложении: “По-видимому, хаос и огромное число ошибок в
работе новой системы управления соцобеспечением Нью-Йорка связаны с чрезмерным
увеличением количества кодов, необходимых для ввода данных, и с вытекающей из этого
сложностью обучения операторов”. В остальной части статьи рассказывалось о попытке
объединить в новой системе несколько старых. В результате слияния количество ошибок возросло
с 2 более чем до 20%, поскольку толком слить имевшиеся схемы кодировки так и не удалось. 
       Как при встрече распознать плохую схему кодировки? Один из характерных признаков —
отсутствие возможности расширения. Поговорите с кем-нибудь, кому приходилось
переконфигурировать систему с записями фиксированной длины при переходе от старых к новым
почтовым индексам. Физически в SQL такой проблемы нет, но она может проявиться на
логическом уровне. 
       Другое свойство плохих схем — неоднозначные коды. Наверное, самый забавный случай
связан с попыткой ввести в итальянскую телефонную систему “службу точного времени”. Для нее
был подобран трехзначный телефонный номер, совпавший с междугородным кодом Милана, в
результате чего никто не мог дозвониться в Милан, не узнав попутно, который час. 
       Такие вещи происходят чаще, чем кажется, причем наиболее типичная форма ошибки —
слишком вольная трактовка кода, соответствующего случаю “Другое”. Очень разные случаи
кодируются как идентичные, и в результате выполнения запроса пользователь получает
некорректную информацию. 
       В плохой схеме кодировки нет кодов для отсутствующих, неизвестных, неприменимых или
неклассифицируемых значений. В классической истории рассказывается о человеке, который ради
шутки зарегистрировал для своей машины номерной знак “NONE” и вскоре получил по почте
тысячи штрафных квитанций. В дорожной полиции не было специального кода для случаев, когда
на квитанции не указан номер машины, и потому в поле для номера они писали просто “none”
(нет). Как только в БД появился номерной знак этого несчастного, СУБД зарегистрировала
совпадение и отправила на его адрес все неоплаченные квитанции, в которых отсутствовал номер
автомобиля. 
       Вы, вероятно, скажете, что в SQL эту проблему легко решить с помощью значения NULL? Увы,
к сожалению, во многих функциях SQL оно игнорируется. SQL-запрос:

SELECT tag_nbr, SUM(fme)


FROM tickets
GROUP BY tag_nbr;

предназначенный для выдачи суммы штрафов для каждого автомобиля, сгруппирует все записи,
не содержащие номера, и выдаст полную сумму для этой группы, словно эти записи относятся к
одной машине. Но вам, скорее всего, хотелось бы видеть информацию по каждому такому случаю
в отдельности, потому что вряд ли во всей Калифорнии отыщется всего один автомобиль без
номера. 
       Имеются также небольшие, но существенные различия между пропущенными, неизвестными,
неприменимыми, неклассифицируемыми и ошибочными значениями. Например, в международной
классификации заболеваний код 999 999 применяется для неклассифицируемой болезни, то есть,
болезни, наличие которой у пациента сомнений не вызывает, но диагностировать которую не
удается. Это безрадостное состояние, конечно, отличается от отсутствующего кода заболевания
(пациент только что пришел в больницу, может быть, даже и не болен), неприменимого кода
(осложнения беременности для мужчины), неизвестного кода (болен, ожидает результаты
анализов) или ошибочного кода (в графе для температуры пациента значится 100°С).

Типы схем кодировки


       Далее я расскажу о своей классификации схем кодировки и приведу рекомендации по их
использованию. Вы могли уже встречать некоторые из этих идей в библиотечном деле или в
других областях, но применительно к обработке данных, насколько я знаю, никто классификацию
разрабатывать не пытался.
Перечисляющая кодировка
       Перечисляющая кодировка состоит в упорядочении значений атрибута и последующем
присвоении каждому значению числа или буквы. Числа обычно предпочтительнее букв, поскольку
при добавлении новых значений их можно увеличивать безгранично. Перечисляющие схемы
хороши для коротких списков, по по мере увеличения длины их эффективность падает. 
       Запомнить длинный список кодов сложно, да и упорядоченность с добавлением новых
значений, скорее всего, будет нарушена. Упорядочивать значения лучше всего на основе их
естественного чередования, если такое имеется. Это может быть хронологический порядок (1
случилось раньше, чем 2) или порядок выполнения (1 необходимо сделать раньше, чем 2). Иногда
используют такой порядок: сначала часто встречающиеся значения, потом — реже встречающиеся
значения. Наиболее частым случаям можно присвоить коды покороче. Уместны списки,
упорядоченные по физическим характеристикам (масса, цвет и пр.). 
       Высказавшись в пользу естественного упорядочения, я должен признать, что гораздо чаще в
реальной жизни встречается алфавитный порядок, поскольку его легче реализовать на
компьютере. В стандарте ANSI X3.31, например, имена округов США сначала выстроены по
алфавиту (отдельно в каждом штате), а затем пронумерованы.

Кодировка единиц измерения


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

Кодировка аббревиатурами
       Сокращения нужны, чтобы сэкономить пространство, занимаемое значением атрибута, но
вместе с тем сохранить его понятность. Аббревиатура может быть как переменной, так и
постоянной длины, но компьютерщикам, конечно, ближе второй вариант. В качестве примера
взгляните на двух-буквенные обозначения штатов (СА — Калифорния, AL — Алабама), которые
пришли на смену сокращениям переменной длины (соответственно, Calif, и Ala.). 
       Добротная система аббревиатур очень удобна, но по мере роста количества кодируемых
значений повышается вероятность путаницы. Трехбуквенные коды больших аэропортов еще
можно запомнить: LAX — Лос-Анжелес, SFO — Сан-Франциско, SVO — Шереметьево, но
разобраться с кодами небольших аэропортов гораздо сложнее. 
       Еще один пример — стандартные коды стран ISO-3166, которые могут быть двухбуквенными,
трехбуквенными или числовыми. Их поддерживает сетевой координационный центр RIPE.

Алгоритмические коды
       Алгоритмическая кодировка заключается в создании кода из значения по определенному
алгоритму. Алгоритм должен быть обратимым, чтобы из кода можно было восстановить исходное
значение. Хотя это и не обязательно, код обычно бывает короче (по крайней мере, его размер
ограничен сверху) и упорядоченнее исходных значений. Наиболее типичным видом
алгоритмического кодирования является шифрование — очень важный процесс, заслуживающий
отдельного рассмотрения. 
       В астрономии используется алгоритмическое кодирование дней — так называемая юлианская
дата,  которая представляет собой количество дней, прошедших с определенного дня (1 января
4713 г. до н. э.) в далеком прошлом. Юлианский день фактически есть преобразование даты в
целое число. Для более приземленных целей удобна упрощенная юлианская дата — количество
дней, прошедших с начала года. Она, очевидно, заключена в пределах от 1 до Зб5 или 366.
Применение алгоритма требует компьютерного времени как на вводе, так и на выводе данных, но
кодирование, как правило, окупает эти затраты, так как позволяет легко осуществлять поиск и
расчеты, которые были бы невозможны с исходными данными. 
       Другой пример — хэш-функции, которые преобразуют одни численные значения в другие,
предназначенные для хранения. Вариантом алгоритмической кодировки можно также считать
округление чисел перед помещением в БД. 
       Разница между аббревиатурой и алгоритмом не особенно четкая. Сокращение можно считать
особым случаем алгоритма, описывающим удаление или замену букв. Чтобы все-таки разделить
их, обращайте внимание на следующие признаки 
       1. То, что легко понимается человеком, является аббревиатурой. 
       2. Результат алгоритмического кодирования легкому пониманию не поддается. 
       3. Алгоритмическое кодирование может возвращать один и тот же код для нескольких
значений. Аббревиатура всегда однозначна.

Иерархические схемы кодировки


       В иерархической схеме кодировки набор значений разделяется на несколько категорий, те, в
свою очередь, — на несколько подкатегорий, и так далее, пока не будет достигнут некий нижний
уровень. Такие схемы называют также вложенными или древовидными. У каждой категории есть
определенное значение, а подкатегории его уточняют. 
       Наиболее очевидный пример — почтовый индекс, разделяющий территорию США на
отдельные регионы. При чтении слева направо каждая следующая цифра уточняет расположение
адресата: регион, штат, город и, наконец, почтовое отделение. Рассмотрим индекс 30310. Номера
с 30000 до 39999 соответствуют юго-востоку США. Номера с 30000 по 30399 распределены по
Джорджии, а индексы с 30300 по 30399 указывают на Атланту. Весь индекс, 30310,
идентифицирует почтовое отделение на западной окраине города. Для расшифровки индекса его
нужно читать по цифрам слева направо — сначала одну цифру, потом две, потом оставшиеся
две. 
       Другой пример — библиотечная классификация DDC (Dewey Decimal Classification),
применяемая в американских библиотеках. Номера в шестой сотне (начинающиеся с 5) покрывают
“Естественные науки”, номера 510-519 соответствуют математике и, наконец, код 512 обозначает
алгебру. Схему можно расширять далее, добавляя позиции после десятичной точки для
обозначения подразделов алгебры. 
       Иерархические схемы кодировки хороши для работы с большими объемами данных,
обладающих естественной иерархической структурой. Структурированную информацию легко
организовать и представить, но при разработке подобных схем также возникают проблемы. 
       Во-первых, древовидная структура не обязана быть сбалансированной: для некоторых
категорий необходимо больше кодов, чем для других. В системе DDC мало кодов для восточных и
древних религий, что отражает предпочтение, отдававшееся христианским и иудаистским текстам.
Между тем, в наши дни в Библиотеке Конгресса США явно лидирует буддизм — книг по нему
больше, чем по любой другой религии Земли. 
       Во-вторых, размещение определенных категорий на дереве может со временем оказаться
неудачным. Например, в системе DDC книги по логике представлены кодом 164 из раздела
“Философия”, тогда как более уместен был бы код из математического раздела. В XIX веке
математической логики просто не было, а сегодня мало кому придет в голову искать книги по
логике в философском разделе. Создатель системы DDC просто закрепил в ней понятия своего
времени — как поступают и многие современные программисты.

Векторные коды
       Вектор состоит из фиксированного числа компонентов. Они могут быть упорядоченными и
неупорядоченными, обладать постоянной или переменной длиной, быть взаимно зависимыми или
независимыми, но они всегда присутствуют в заданном количестве, и код имеет смысл лишь при
наличии всех компонентов. 
       Самый распространенный векторный код — дата, состоящая из числа, месяца и года.
Компоненты даты, конечно, имеют некоторый смысл и сами по себе, но во всей полноте суть даты
раскрывается лишь при наличии всех трех компонентов. Порядок их расположения особого
значения не имеет: в мировой практике вы, вероятно, найдете все возможные сочетания.
Допустимые значения для числа зависят от года (високосный или нет) и месяца (который может
иметь 28, 29, 30 или 31 день). Компоненты могут разделяться точками (28.09.2005), косыми
чертами (28/09/2005), пробелами (28 сентября 2005 г.) или вообще никак не разделяться
(28092005). 
       Другой пример — код ISO размеров автомобильных покрышек, составляемый из диаметра
колеса в дюймах, типа покрышки (буквенный код) и ее ширины в миллиметрах. Код 15R155
соответствует 15-дюймовой радиальной покрышке шириной 155 миллиметров, а код 15SR155 —
покрышке тех же размеров, но с опоясывающим металлическим кордом. Несмотря на смесь
американских и международных единиц это вполне общее физическое описание покрышки. 
       Векторные схемы информативны, кроме того, они позволяют подобрать для каждого
компонента наилучший способ кодирования, но вместе с тем иногда приходится придумывать, как
разделить код на составные части (во многих СУБД имеются функции для разделения на
компоненты дат, адресов и имен). Сортировка по компонентам практически невозможна, разве что
в том порядке, в каком они стоят в коде. 
       Еще один недостаток заключается в том, что неудачный выбор кода всего для одного
компонента делает бесполезной всю схему. Расширение кода тоже проблематично. Что если в код
покрышки нужно будет включить еще толщину в миллиметрах? Раз появляется еще одно число,
его придется отделять пунктуацией. Вообще, такие переделки обычно чреваты необходимостью
вносить правки в очень многие программы.

Составные коды
       Составной код представляет собой комбинацию произвольного числа нескольких
компонентов. Как и в векторной кодировке, компоненты могут быть упорядоченными и
неупорядоченными, зависимыми и независимыми, разделяться пунктуацией, пробелами и пр.
Часто в составном коде присутствует иерархическая структура, которая выстраивается с помощью
уточняющих компонентов, которые добавляются к коду справа. Иногда в качестве составного кода
используется список свойств, каждое из которых может присутствовать или отсутствовать.
Порядок компонентов тоже не является обязательным. 
       Составные коды были популярны в мастерских начала XX в. К изделию прикреплялся
бумажный ярлык, на котором каждый рабочий записью подтверждал завершение очередного этапа
обработки. Составные коды и по сей день используются в авиационной промышленности: с
помощью длинного кода описывается набор деталей, входящих в состав компонента (его
называют корневым или родительским), который расположен в начале кода. Еще один вариант
составного кода — неупорядоченный кворум-код. Чтобы некое условие считалось выполненным, в
таком коде должно присутствовать n компонентов из k.  Например, чтобы деталь считалась
прошедшей контроль качества, ее должны одобрить любые три инспектора из пяти. 
       Самый популярный составной код — список ключевых слов, стоящий в начале какого-либо
документа и описывающий его содержание. Ключевые слова назначаются документу автором или
библиотекарем. Как правило, они выбираются из ограниченного специализированного словаря. В
начале компьютерной эры популярность составных кодов снизилась, поскольку из-за переменной
длины их трудно было хранить в первых компьютерных системах, в которых записи имели
фиксированную длину (помните перфокарты?). Чтобы такие коды можно было корректно
сортировать, их приходилось хранить в виде строк, выровненных влево. 
       Неудачно разработанный составной код может быть неоднозначным. Например, какой код
стоит в начале цепочки “1234” — 1 или 12? В базах данных составные коды обычно преобразуются
в набор флажков “да/нет”, расположенных в смежных столбцах файла, то есть, фактически из
истинных составных кодов превращаются в булевы векторные коды.

Общие правила разработки кодировок


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

Опирайтесь на существующие стандарты кодирования


       Использование стандартных схем кодирования — очевидная рекомендация. Применение
одних и тех же кодов существенно облегчает сбор данных и обмен ими. Кроме того, можно быть
уверенным в том, что специалист, который кроме разработки кодировки ничем иным не занимался,
справился с этой задачей лучше, чем человек, которому приходится придумывать коды и
одновременно обслуживать базу данных. 
       Если область, в которой вам приходится работать, для вас в новинку, непременно обратитесь
к эксперту. Это кажется очевидным, но мне пришлось однажды работать над проектом
музыкальной БД, в котором программисты старательно избегали разговоров с
профессиональными библиотекарями, также участвовавшими в проекте. В результате, записи
идентифицировались по GUID, а не по номерам из каталога Шванна (Schwann), что было бы
вполне логично. Не нашли эксперта — самостоятельно ищите стандарты в Интернете. Проверьте,
нет ли нужного стандарта ISO, правительственного стандарта, почитайте материалы конкретных
промышленных групп и организаций.
Предусматривайте возможность расширения
       Обязательно предусматривайте возможность расширения кодировки. Оператором ALTER
можно превратить однобуквенный код в двухбуквенный, но нельзя изменить размещение
символов в отчетах и на экране. Обязательно задавайте размер кода на одну позицию больше,
чем по вашему мнению необходимо. Визуально строка “01” гораздо больше похожа на код, чем
просто “1”, которую легко спутать с количеством.

Явно задавайте коды для отсутствующих значений


Обоснование 
       Старайтесь по возможности избегать кодов NULL, задавая в схеме кодирования специальные
коды для отсутствующих значений. В SQL значения NULL обрабатываются не так, как обычные
значения. Кроме того, NULL ничего не говорит о том, какое именно значение отсутствует. 
       Часто для отсутствующих значений применяют коды, состоящие из одних нулей или одних
девяток. Например, в ISO пол кодируется так: 0 — неизвестный, 1 — мужской, 2 — женский, 9 — не
определен. Последний код используется, например, для обозначения субъектов права, у которых
нет пола, скажем, корпораций. 
       В версиях Фортрана до стандарта 1977 г. “чистые” (без единого отверстия) столбцы
перфокарты считались нулями. С другой стороны, столбец, в котором действительно был записан
ноль, содержал пробитые отверстия. Этим, кстати, пользовались в целях безопасности,
специально добавляя нули в левой части кода, чтобы довести его до фиксированной длины, не
оставляя “чистых” столбцов. В Фортране-77 проблема путаницы между пустым значением и
нулевым значением была решена, но она все еще живет в непродуманных SQL-системах, которые
неспособны отличить NULL от пустой строки или нуля. 
       Коды отсутствующих значений, состоящие из девяток или букв “Z”, при сортировке
оказываются в конце экрана или отчета. Расположение значений NULL при сортировке зависит от
реализации.

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

Не показывайте коды пользователю


       По возможности старайтесь не показывать конечному пользователю “чистые” коды, а
переводите их в понятные значения. Это, конечно, не относится к общеупотребительным кодам,
которые и так будут распознаны пользователем. Скажем, большинству людей двухбуквенное
обозначение штата понятно без расшифровки. С другой стороны, мало кто сможет без перевода
разобраться в сложных кодах оплаты, которые применяются некоторыми телефонными
компаниями. 
       Трансляция кодов включает в себя их форматирование. Иногда достаточно включить в код
пунктуацию (дефисы, запятые) или символ денежной единицы. Однако, в многоуровневой
архитектуре форматирование осуществляется не в БД, а в интерфейсной программе. Типичная
для новичка ошибка — попытка вставить лишние нули или разделители в числовое значение.
Превратив тем самым число в строку, вы теряете все возможности проведения вычислений с
числовыми и календарными значениями. 
       Для трансляции кодов применяются специальные таблицы. Они не являются представлением
сущности или отношения. Обращение к ним подобно вызову функции в процедурном языке.
Общий вид такой таблицы таков:

CREATE TABLE Какие-тоКоды


(код <тип_данных> NOT NULL PRIMARY KEY,
определение <тип_данных> NOT NULL);

Иногда определение также включается в состав первичного ключа. В описание столбца с кодом
могут включаться ограничения СНЕСК(), но, как правило, внутри приложения особо беспокоиться о
проверках не приходится, ибо такие таблицы разрабатываются за пределами БД и используются
только для чтения.
Не помещайте все коды в одну таблицу
       Бывают случаи, когда некий бездарный способ действия настолько распространен, что
получает собственное имя. В особо тяжелых случаях, как и положено болезни, вместо имени
применяется аббревиатура. Впервые мне пришлось столкнуться с болезнью OTLT (One True
Lookup Table, единая истинная таблица кодов) в 1998 г., в одном из форумов CompuServe, и с тех
пор в различных группах новостей я вижу ее ежегодно. 
       Суть этой болезни в том, что вместо создания собственной таблицы для каждой кодировки, мы
все кодировки помещаем в одну огромную таблицу. Схема этой таблицы выглядит так:

CREATE TABLE OTLT (


тип_кода INTEGER MOT NULL,
код VARCHAR(n) NOT NULL,
определение VARCHAR(m) NOT NULL,
PRIMARY KEY (тип_кода, код));

       На практике числа m и n обычно равны чему-то вроде 255 или 50 — значениям по умолчанию
для данного SQL-продукта. 
       Оправдать объединение всех кодировок в одну таблицу обычно пытаются тем, что в этом
случае для поддержания всех кодировок программисту достаточно написать одну интерфейсную
программу. Но это полный маразм, которому ни в коем случае нельзя поддаваться. Прежде чем
читать следующие разделы, попробуйте самостоятельно составить список недостатков этой
методики, а потом сверьте с моим списком. Не упустил ли я чего-нибудь? 
       1. Нормализация. Если взглянуть в корень, то этот подход не оправдывает себя, так как
представляет собой попытку нарушить требования первой нормальной формы. Я, конечно, вижу,
что у таблицы OTLT есть первичный ключ и что все столбцы в базе данных SQL должны быть
скалярными и принадлежать к одному типу данных. И тем не менее я уверенно утверждаю, что эта
таблица не приведена к первой нормальной форме. Тот факт, что два домена используют один и
тот же тип данных, еще не делает их одним и тем же атрибутом. Добавочный столбец “тип_кода”
меняет домен других столбцов и тем самым противоречит требованиям первой нормальной
формы, поскольку не является атомарным. Таблица должна моделировать один набор сущностей
или одно отношение, а не сотни их. Как говорил Аристотель: “Быть чем-то значит быть чем-то
конкретным. Не быть чем-то конкретным илибыть чем-то вообще значит быть ничем”. 
       2. Объем памяти. Для хранения таблицы OTLT требуется больше памяти,чем для отдельных
таблиц с кодировками — из-за лишнего столбца стипом кода. Представьте себе, что вы
объединили в одной таблице международную классификацию болезней (ICD) и библиотечную
классификацию DDC. Каждый раз при переходе к другой кодировке вам придется извлекать
таблицу OTLT целиком. 
       3. Типы данных. Всем кодировкам насильственно приписан один типданных: строка
максимальной длины, которая может потребоваться в настоящем или будущем для одной из
кодировок таблицы. Тип VARCHAR(n) не всегда представляет собой лучший способ хранения
данных. Кто-нибудь обязательно вставит в таблицу огромную строку, которая замечательно
выглядит на экране, но в своей невидимой правой части содержит лишние пробелы или другие
символы. Во многих SQL-продуктах с точки зрения хранения и доступа к данным
предпочтительнее применять тип CHAR(n). С числовыми кодами можно выполнять
арифметические операции, проверять диапазоны, контрольные цифры и т.п. с помощью
ограничений СНЕСК(). Коды в виде дат можно преобразовывать в названия праздников и других
событий. Не существует универсального типа данных, который подходил бы ко всем случаям.
Если в одной кодировке допустимы значения NULL, в таблице OTLT они будут допустимы во всех
кодировках. 
       4. Проверка. Единственный способ применить ограничение CHECK() в таблице OTLT —
написать огромную конструкцию CASE:

CREATE TABLE OTLT


(тип_кода CHAR(n) NOT NULL
CHECK (тип_кода IN (<тип 1>, .... <тип n>)),
код VARCHAR(n) NOT NULL
CHECK (CASE WHEN тип_кода = <тип 1>
AND <проверка 1>
THEN 1
--предполагаем, что в вашем SQL-продукте размер CASE неограничен
WHEN тип_кода = <тип п>
AND <проверка п>
THEN 1
ELSE О END = 1),
определение VARCHAR(m) NOT NULL,
PRIMARY KEY (тип_кода, код));

       Это означает, что проверка будет занимать очень много времени, поскольку любое изменение
нужно будет проверять во всех конструкциях WHEN, пока SQL не найдет то из них, что возвращает
TRUE. Придется также добавить ограничение CHECKQ в описание столбца “тип_ко-да”, чтобы
пользователь не мог создать ошибочную кодировку. 
       5. Гибкость. В таблице OTLT предусмотрен один столбец для кода, стало быть, ее нельзя
использовать для кодов, состоящих из n  значений,если n  > 1. Допустим, если я хочу
преобразовывать в название местности пару координат “широта-долгота”, мне придется добавить
в таблицу еще один столбец. 
       6. Обслуживание. В разных кодировках возможно одно и то же значение кода, поэтому вам
придется постоянно следить за тем, с какой именно кодировкой вы работаете. Например, как код
1CD, так и код DDC имеют одну структуру — три цифры, точка, три цифры. 
       7. Безопасность. Чтобы запретить некоторым пользователям просмотр одной из кодировок, к
таблице OTLT придется добавить представления,в которых с группой пользователей
сопоставлялся бы тип кода, который им разрешено изменять. В этом случае от оправдания единой
таблицы вообще мало что остается: в интерфейсе теперь приходится иметь дело с несколькими
представлениями почти так же, как пришлось бы работать с несколькими таблицами. 
       8. Отображение. В интерфейсную программу приходится передавать все кодировки без
исключения, что связано с большими накладными расходами и является потенциальным
источником ошибок.

Храните коды в базе данных


       Таблицы с кодами должны быть частью БД. Их можно использовать для проверки вводимых
данных, для преобразования кодов, для создания документации. 
       В 1993 г. я был потрясен, увидев, как в одной из крупнейших больниц Лос-Анжелеса служащая
по старинке искала коды болезней в огромной амбарной книге, вместо того чтобы вывести их на
экран терминала. В больнице все еще работали старые мэйнфреймы IBM с терминалами 3270, на
которых оператор даже справочную систему не мог вызвать. В системе “клиент-сервер” таблицу с
кодами для уменьшения сетевого трафика можно загрузить на отдельные рабочие станции. Еще
лучше сделать на основе этой таблицы раскрывающийся список, чтобы уменьшить вероятность
опечаток. Изменения в кодах в этом случае не заставят переписывать приложение. Если коды
меняются со временем, в таблицу можно включить столбцы с датами начала и окончания
действия кода. Это позволит корректно обращаться не только к актуальным, но и к старым
данным.

Многосимвольные кодировки
       Некоторые СУБД поддерживают кодировки ASCII, EBCDIC и Unicode. Вам необходимо знать
об этом, чтобы корректно настраивать сравнения и нормализацию текста. 
       Предикат “<строка> IS [NOT] NORMALIZED” в стандарте SQL-99 констатирует, относится ли
строка Unicode к одной из четырех нормальных форм (т. е. D, С, KD и КС). Термин нормальная
форма имеет здесь значение, отличное от его применения в реляционном контексте. В модели
Unicode один символ может составляться из нескольких других символов. Над некоторыми
латинскими буквами могут вставляться диакритические знаки. 
       Определенные последовательности букв заменяются лигатурами. В некоторых языках,
например корейском и вьетнамском, иероглифы составляются в результате объединения
символов как по вертикали, так и по горизонтали. В некоторых языках одна и та же буква
отображается по-разному в зависимости от положения в слове, например сигма в греческом
или и  с акцентом в чешском. Говоря коротко, писать — это не просто ставить буквы друг за
другом. 
       В стандарте Unicode определен порядок таких конструкций в соответствующих нормальных
формах. Один и тот же видимый результат можно получить с помощью разного расположения
символов или даже с помощью разных наборов символов, но при организации поиска по тексту
вам удобнее будет знать, что текст нормализован, чем пытаться анализировать буквы, лигатуры,
иероглифы “на лету”. За подробностями и бесплатным ПО обращайтесь на сайт www.unicode.org.
ГЛАВА 6. 
Выбор подхода к написанию кода
Цезарь: Прости его, Теодот. Он варвар и полагает,
что обычаи его острова суть законы природы.
Цезарь и Клеопатра, Дж. Бернард Шоу, 1898
{Перевод М. Богословской и С. Боброва. 
Полн. собр. пьес в 6 т. Т. 2 - Л.: Искусство, 1979}

       В этой главе обсуждается написание хороших DML-выражений в стандартном SQL. Это
означает, что они переносимы и поддаются оптимизации на большинстве диалектов SQL.
Под переносимостью я понимаю одно из нижеследующего. Либо код стандартизован и может
запускаться без редактирования на других диалектах SQL стандарт подразумевает
переносимость. Либо для использования в другой реализации SQL код нуждается лишь в простой
механической правке, поскольку использованные в нем функции настолько универсальны, что в
том или ином виде имеются на большинстве платформ: переносимость не подразумевает
стандартности. Описание этой концепции вы найдете в руководстве по созданию переносимых
кодов “X/Open SQL Portability Guides”. 
       Главная проблема людей, начинающих программировать на SQL, состоит в том, что они не
могут забыть процедурное или объектно-ориентированное программирование, которому учились
раньше. Они не учатся мыслить в терминах наборов данных и предикатов, а попросту
воспроизводят решения, к которым привыкли в своем первом языке программирования. Джерри
Вайнберг (1978) отметил этот факт еще 25 лет назад в классической книге Psychology of Computer
Programming. Он преподавал язык PL/I (справка для самых молодых читателей: разработанный
IBM и некогда очень популярный язык PL/I являлся гибридом Фортрана, Кобола и Алгола). Так вот,
Вайнберг обнаружил, что легко может определить первый язык программирования, изученный
студентом, глядя на то, как он работает с PL/I. Я сам в 1989 г. столкнулся с тем, что могу угадать
национальность студента по его программе на Си или Паскале. 
       Вторая проблема заключается в том, что человек учится программировать на определенном
SQL-диалекте и думает при этом, что его диалект является стандартом. В 2004 г. я проходил
собеседование по поводу приема на работу. Мне предложили оценить различные платформы с
точки зрения предстоящего существенного увеличения объема баз данных компании. На
протяжении всего собеседования интервьюер задавал мне “общие” вопросы по SQL, основанные
на архитектуре хранения данных в том единственном SQL-продукте, который он знал сам. 
       Его продукт не предназначался для работы с очень большими базами данных (Very Large
Database, VLDB), сам он ничего не знал о Nucleus, Teradata и других продуктах для VLDB. На
протяжении всей карьеры он разбирался в тонкостях одной версии одного продукта и не мог
переключиться на что-либо другое даже концептуально. Понятно, что перспектив служебного
роста у него нет. 
       Конечно, всегда найдется поле деятельности и для специалистов по программированию на
определенном диалекте, но использование нестандартного диалекта должно быть последним
средством, к которому следует прибегать лишь в исключительных случаях и никогда — при первом
знакомстве с SQL. Это все равно как хирургия: опухоль, которая не поддается лечению другими
способами, нужно вырезать, но нельзя хвататься за скальпель, когда пациент приходит к вам с
обыкновенным волдырем.

Отдавайте предпочтение стандартным конструкциям


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

Используйте стандартный синтаксис OUTER JOIN


Обоснование 
       Вот как работает стандартный оператор OUTER JOIN в SQL-92. Рассмотрим две таблицы:

Таблица1 Таблица2
a b a c
======== ========
1 w 1 r
2 х 2 s
3 у 3 t
4 z

и оператор OUTER JOIN:

Таблица1
LEFT OUTER JOIN Таблица2
ON Таблица1.а = Таблица2.а <== условие объединения
AND Таблица2.с = 't; <== условие на одну таблицу

       В данном запросе мы назовем первую таблицу сохраняемой, а вторую — изменяемой. То, что


я собираюсь вам предложить, эквивалентно стандартам ANSI/ISO. 
       1. Строим перекрестное объединение (CROSS JOIN) двух таблиц, затем проверяем каждую
строку таблицы-результата. 
       2. Если результат проверки предиката для некоторой строки равен TRUE,сохраняем ее, а
также удаляем из результата перекрестного объединения все порожденные ею строки. 
       3. Если результат проверки предиката для некоторой строки равен FALSE или UNKNOWN,
тогда сохраняем столбцы из сохраняемой таблицы, задаем значение NULL для всех столбцов
изменяемой таблицы, а затем удаляем дубликаты. 
       Проделаем это вручную, обозначив символом @ строки, для которых выполнен первый
предикат, и символом * строки, для которых выполнен второй предикат:

Таблица1 CROSS JOIN Таблица2


a b а с
============================
1 w 1 r @
1 w 2 s
1 w 3 t *
2 х 1 r
2 х 2 s @
2 х 3 t *
3 у 1 r
3 у 2 s
3 у 3 t @* <== результат проверки равен TRUE
4 z 1 r
4 z 2 s
4 z 3 t *
Таблица1 LEFT OUTER JOIN Таблица2
a b а с
================================
3 у 3 t <= единственная строка с результатом TRUE
--------------------------------
1 w NULL NULL Набор дубликатов
1 w NULL NULL
1 w NULL NULL
2 x NULL NULL
2 x NULL NULL
2 x NULL NULL
3 у NULL NULL <== порождена строкой с результатом TRUE -
удаляем
3 у NULL NULL
4 z NULL NULL
4 z NULL NULL
4 z NULL NULL

Окончательные результаты:

Таблица1 LEFT OUTER JOIN Таблица2


a b a с
================================
1 w NULL NULL
2 x NULL NULL
3 у 3 t
4 z NULL NULL

       Основное правило таково: каждая строка из сохраняемой таблицы представлена в таблице-
результате как минимум одной строкой.

Расширенное равенство и нестандартный синтаксис


       Пока не были установлены стандарты, все разработчики пользовались немного различным
синтаксисом со слегка различной семантикой. В большинстве из них использовался расширенный
оператор равенства (extended equality operator), основанный на оригинальной версии Sybase.
Однако при работе с расширенным равенством возникает ряд серьезных проблем. Рассмотрим
две таблицы:

Suppliers SupParts
supno supno partno qty
========= ==================
S1 S1 P1 100
S2 S1 P2 250
S3 S2 P1 100
S2 P2 250

       Вот как выглядит оператор OUTER JOIN с расширенным равенством в стиле Sybase:

SELECT *
FROM Supplier, SupParts
WHERE Supplier.supno *= SupParts.supno AND qty < 200;

Применяя сначала OUTER JOIN, получаем:

Suppliers LOJ SupParts


supno supno partno qty
========================
S1 S1 P1 100
S1 S1 P2 250
S2 S2 P1 100
S2 S2 P2 250
S3 NULL NULL NULL

Затем применяем предикат qty < 200:

Suppliers LOJ SupParts


supno supno partno qty
=========================
S1 S1 P1 100
S2 S2 P1 100

       Если проделать те же действия в обратном порядке, результат будет иным:

Suppliers LOJ SupParts


supno supno partno qty
S1 S1 P1 100
S2 S2 P1 100
S3 NULL NULL NULL

       В Sybase используется один порядок, в Oracle — противоположный. В Centura (она же Gupta)
можно выбрать, какой из вариантов использовать. В SQL-92 вы явно указываете порядок
выполнения операций. Можно накладывать условие после объединения:

SELECT * FROM Supplier


LEFT OUTER JOIN
SupParts
ON Supplier.supno = SupParts.supno WHERE qty < 200;

       А можно проверять его во время объединения:

SELECT * FROM Supplier


LEFT OUTER JOIN SupParts
ON Suppliei supno = SupParts supno AND qty < 200,

       Другая проблема заключается в том, что в расширенном операторе равенства вы не можете
использовать одну и ту же таблицу в качестве и сохраняемой, и изменяемой. В SQL-92 это очень
просто сделать. Вот как можно задать поиск студентов, которые выбрали курс Math 101 и могли
выбрать курс Math 102:

SELECT C1 student, C1 math C2 math


FROM (SELECT - FROM Courses WHERE math = 101) AS C1
LEFT OUTER JOIN
(SELECT * FROM Courses WHERE math = 102) AS C2
ON C1 student = C2 student,

Исключения 
       Нет. Почти все разработчики, как крупные, так и второстепенные, используют сегодня
инфиксный оператор OUTER JOIN в том виде, что принят в ANSI SQL Если в старых кодах вам
встретятся другие варианты записи, исправьте их, руководствуясь тем, что написано выше.

Инфиксный синтаксис INNER JOIN и CROSS JOIN необязателен,


но привлекателен
       В SQL-92 операторы INNER JOIN и CROSS JOIN введены, чтобы дополнить операторы
OUTER JOIN и “закруглить” нотацию. Функции, выполняемые операторами INNER JOIN и CROSS
JOIN, реализованы в конструкции FROM и, в отличие от оператора OUTER JOIN, не дают
программисту ничего нового.
Обоснование 
       Оператор CROSS JOIN — очень удобное средство документации, пропустить его в коде
намного сложнее, чем простую запятую. Аналогично, использование INNER JOIN при написании
кода вместо сокращенного INNER помогает документировать код. 
       Однако оператор INNER JOIN может оказаться визуально слишком сложным, поэтому иногда
стоит предпочесть старый синтаксис. Он позволяет размещать все предикаты вместе и
группировать их в определенном порядке для удобства чтения, основываясь на психологическом
“правиле пяти”. Оно гласит, что человек затрудняется работать более чем с пятью вещами
одновременно, испытывает серьезные проблемы с семью вещами и совершенно неспособен
справиться с девятью (Миллер 1956). Поэтому при работе с двумя-тремя таблицами инфиксные
операторы хороши, но польза от них сомнительна, если таблиц больше пяти. Визуально сложно
ассоциировать конструкцию ON с операторами INNER JOIN. В частности, схема “звезда” имеет
легко распознаваемую структуру объединений таблицы фактов с таблицами размерностей, как в
этом псевдокоде:

SELECT ..
FROM Facts, Dim1, Dim2, .., DimN
WHERE Facts.a1 = Dim1.a
AND Facts.a2 = Dim2.a
AND Facts.an = DimN.a

       Читатель просмотрит правую часть конструкции WHERE сверху вниз и увидит размерность в
вертикальном списке. 
       Популярен следующий прием — помещать условия объединения в конструкцию FROM с
использованием синтаксиса INNER JOIN, а аргументы поиска — в конструкции WHERE. Некоторые
начинающие пользователи уверены, что это единственно допустимый способ, хотя это и не так.
Тем не менее, если аргументы поиска меняются, то удобно помещать их все вместе. 
       Следуйте простому правилу применения объединений в старом стиле: число таблиц в
конструкции FROM должно быть на одну больше, чем число условий объединения в конструкции
WHERE. Это означает, что в объединениях у вас не возникает циклов. Если разница между
числом таблиц и числом условий объединения больше единицы, недостающее условие может
привести к нежелательному перекрестному объединению. Старый стиль:

SELECT 01.order.jibr, ..
FROM Orders AS 01,
OrderDetails AS D1
WHERE OLorder_nbr = D1.order_nbr
AND DLdept = 'mens wear";

новый стиль:

SELECT O1.order_nbr, ..
FROM Orders AS 01
INNER JOIN
OrderDetails AS D1
ON 01.order_nbr = D1.order_nbr
AND DLdept = 'mens wear";

Смешанный стиль:

SELECT O1.order_nbr, ..
FROM Orders AS O1
INNER JOIN
OrderDetails AS D1
ON O1.order_nbr = D1.order_nbr
WHERE DLdept = 'mens wear';

Исключения 
       Инфиксные операторы объединения должны применяться, если в конструкции FROM есть
оператор OUTER JOIN. Дело в том, что для операторов OUTER JOIN важен порядок выполнения.
Контролировать его с помощью скобок и предикатов проще, если все они находятся в одном
месте. 
       На практике, когда у вас в конструкции FROM более пяти таблиц, следует предпочесть
традиционный синтаксис, поскольку в этом случае довольно трудно визуально соотнести
конструкции ON с таблицами и именами корреляций. Эта рекомендация проистекает из “правила
пятерки”, упоминавшегося ранее.

Используйте синтаксис ISO для времени


Обоснование 
       В стандартном SQL разрешен единственный формат вывода времени — “гггг-мм-дд
чч:мм:сс.ссссс”, основанный на ISO-8601. Согласно Федеральному стандарту обработки
информации (Federal Information Processing Standards, FIPS) при отображении секунд требуется
точность как минимум до пятого десятичного знака. Другие способы отображения недопустимы,
если вы собираетесь работать с ПО, основанным на стандартах ISO. 
       В стандартном SQL определен минимальный набор операторов для работы с данными во
временном формате. Все они доступны в любом SQL-продукте, хотя и с несколько различным
синтаксисом. Например, в T-SQL вызов функции DATEADD в виде “DATEADD (DD, 13, birthdate)”
приводит к прибавлению к датеbirthdate 13 дней. В стандартном SQL для того же действия
предусмотрено следующее выражение: “birthdate + INTERVAL'13'DAY”. 
       Вы можете задать отображение в формате ISO-8601 в любом SQL-продукте и выполнить
99,99% работы с датами и временем без использования специфических функций. Проблемы могут
возникнуть при переносе кода, поэтому заведите набор комментариев, касающихся любых
различий между вашим диалектом и стандартом.

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

Используйте стандартные и переносимые функции


Обоснование 
       Стандартный SQL не предназначен для вычислений и потому не располагает библиотекой
функций Фортрана или какого-либо пакета для статистических расчетов. SQL — это не язык для
работы с текстом, поэтому в нем нет библиотеки функций языков ICON или Snobol. Все, чем вы
располагаете в SQL-92, — это четыре действия арифметики и основные операторы для работы со
строками. Тем не менее, разработчики всегда включают в свои продукты дополнительные
операторы, поэтому вы можете создавать переносимые коды с другими математическими и
строковыми функциями. Наиболее распространены такие дополнительные математические
функции: взятие модуля, округление (или отбрасывание дробной части), возведение в степень и
взятие логарифма. Наиболее распространенные дополнительные строковые функции: замена,
обращение порядка символов и копирование.

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

Используйте максимально компактные конструкции


Entia non sunt multiplicanda praeter necessitatem
(He следует умножать сущности сверх необходимости )
Вильям Оккам (1280-1349)
Все должно быть сделано настолько просто, насколько это возможно, но не проще.
приписывается Альберту Эйнштейну

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

Избегайте лишних скобок


Обоснование 
       Когда новички видят автоматически сгенерированный SQL-код, в котором расставлены
многочисленные вложенные скобки, они думают, что именно так код и пишется. Рассмотрим
простой пример:

SELECT a, b, с
FROM Foobar
WHERE (a = b) AND (с < 42);

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

Исключения 
       Скобки в умеренном количестве облегчают чтение большого количества стоящих рядом
предикатов. Сравните:

SELECT application_nbr
FROM LoanApplications
WHERE years_employed > 5
OR net_worth > loan_amt
AND monthly__expenses < 0.25 * loan_amt
OR collateral > 2.00 * loan_amt AND age > 25
OR collateral > loan_amt
AND age > 30
OR years_employed > 2
AND net_worth > 2.00 * loan_amt
AND Age > 21
AND monthly_expenses < 0.50 * loan_amt;

SELECT application_nbr
FROM LoanApplications
WHERE years_employed > 5
OR (net_worth > loan_amt
AND monthly_expenses < 0.25 * loan_amt)
OR (collateral > 2.00 * loan_amt AND age > 25)
OR (collateral > loan_amt AND age > 30)
OR (years_employed > 2
AND net_worth > 2.00 * loan_amt
AND age > 21
AND monthly_expenses < 0.50 * loan_amt);

       В следующем разделе мы разберем, как в подобных ситуациях использовать оператор CASE.
Использование выражений CASE
       Выражение CASE — это именно выражение, а не управляющий оператор, поскольку оно
возвращает величину определенного типа данных. Поскольку язык SQL является декларативным,
в нем нет порядка выполнения операторов, которым можно было бы управлять, как это делается
оператором CASE в других языках. Число новичков, которые не понимают разницы между
выражением и оператором, просто пугает. Сама эта идея пришла из языка программирования
ADA. Вот как выглядит формальный BNF-син-таксис для оператора CASE:

Спецификация case > ::= <простой case> | <case с поиском>


<простой case> ::=
CASE <операнд>
<простая конструкция when>...
[<конструкция else>]
END
<case с поиском> ::=
CASE
конструкция when с поиском>...
[<конструкция else>]
END
<простая конструкция when> :*= WHEN <операнд> THEN <результат>
<конструкция when с поиском> : = WHEN <условие поиска> THEN <результат>
<конструкция else > ::= ELSE <результат>
<операнд> .:= <значение>
<результат> ::= <результирующее выражение> | NULL
<результирующее выражение> ::= <значение>

Выражение CASE с поиском 


       Выражение CASE с поиском, возможно, одна из наиболее часто используемых версий CASE.
В ней конструкции WHEN и THEN выполняются слева направо. Первая конструкция WHEN, в
которой проверяется истинность условия, возвращает величину, заданную в конструкции THEN,
причем вы вольны вставлять выражения CASE друг в друга. Если в выражении CASE не
содержится конструкции ELSE, СУБД по умолчанию вставит конструкцию “ELSE NULL”. Чтобы
возвратить NULL в конструкции THEN, используйте выражение CAST (NULL AS <тип данных>). Я
всегда рекомендую явно определять конструкцию ELSE. Вы сможете изменить ее позднее, когда
найдете что-нибудь определенное для вывода.

Простое выражение CASE 


       Простое выражение CASE определяется как выражение CASE с поиском, в котором все
конструкции WHEN являются проверками равенства операнду CASE. Например:

CASE iso_sex_code
WHEN 0 THEN 'Unknown'
WHEN 1 THEN 'Male'
WHEN 2 THEN 'Female'
WHEN 9 THEN N/A'
ELSE NULL
END

можно записать таю

CASE
WHEN iso_sex_code = 0 THEN 'Unknown'
WHEN iso_sex_code = 1 THEN 'Male'
WHEN iso_sex_code = 2 THEN 'Female'
WHEN iso_sex_code = 9 THEN 'N/A'
ELSE NULL
END

Однако в этом определении есть одна тонкость. Выражение:


CASE foo
WHEN 1 THEN 'bar'
WHEN NULL THEN 'no bar'
END

преобразуется к виду:

CASE
WHEN foo = 1 THEN 'bar'
WHEN foo = NULL THEN 'no bar' -- ошибка
ELSE NULL
END

       Второй конструкции WHEN всегда сопоставлено значение UNKNOWN. По возможности


используйте простое выражение CASE.

Другие виды выражения CASE 


       В стандартном SQL определены другие функции с использованием выражения CASE, которые
делают язык немного более компактным и простым в использовании. Например, функция
COALESCE() для одного или двух выражений может быть определена следующим образом: 
       1. COALESCE (<value ехр #1>) эквивалентно (<value exp #1>). 
       2. COALESCE (<value exp #1>, <value exp #2>) эквивалентно:

CASE
WHEN <value exp #1> IS NOT NULL
THEN <value exp #1>
ELSE <value exp #2>
END

Мы можем ввести рекурсивное определение для n выражений, где n  больше либо равно 3,


следующим образом: COALESCE (<value exp #1>, <value exp #2>,...,n)  эквивалентно:

CASE
WHEN <value exp #1> IS NOT NULL
THEN <value exp #1>
ELSE COALESCE (<value exp #2>, ..., n)
END

Аналогично, NULLIF (<value exp #1>, <value exp #2>) эквивалентно:

CASE
WHEN <value exp #1> = <value exp #2>
THEN NULL
ELSE <value exp #1>
END

       Используйте наиболее компактную форму подобных CASE-выражеиий и не пытайтесь


подробно расписать их компоненты.

Избегайте лишних выражений


Обоснование 
       Современные ядра SQL, как правило, замечательно сообразительны. Но это не всегда было
так, поэтому старые SQL-программисты иногда добавляют в конструкцию WHERE лишние
предикаты. Например, если в таблице Foobar ни один из столбцов не может принимать значение
NULL, то в выражении:

SELECT a, b, с
FROM Foobar
WHERE a = b
AND b = с
AND с = а;

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

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

Ищите наиболее компактную форму


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

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

Используйте предикаты BETWEEN, а не AND


Обоснование 
       Рассмотрим простой запрос:

SELECT a, b, с
FROM Foobar
WHERE a <= b AND b <= с;

Его можно записать так

SELECT a, b, с FROM Foobar WHERE b BETWEEN a AND c,

       Предикат BETWEEN более компактен и дает читателю информацию о соотношении между
тремя столбцами, которое при длинном списке условий поиска может не быть столь очевидным.

Исключения 
       Это правило разумно с точки зрения удобочитаемости, но не всегда надежно с точки зрения
исполнения. Например, в DB2 для z/OS выражение “<название столбца> BETWEEN <выражение>
AND <выражение>” является индексируемым предикатом первой ступени и выполняется
эффективнее — уж позвольте мне не объяснять, что такое предикат первой ступени. 
       С другой стороны, выражение “<значение> BETWEEN <столбец 1> AND <столбец2>” является
неиндексируемым предикатом второй ступени. Если вы то же самое выражение перепишете с
использованием двух предикатов вида <=, то можете получить индексируемый предикат первой
ступени, более эффективный в исполнении. Такой же план выполнения применяется к предикатам
вида “<столбец 1> BETWEEN <столбец 2> AND <столбецЗ>”. Впрочем, эти правила варьируются
от СУБД к СУБД и от платформы к платформе. Кроме того, они теряют смысл по мере
совершенствования оптимизаторов.

Используйте предикаты IN(), а не OR


Обоснование 
       Предикат IN() впервые был введен в язык программирования Паскаль. В SQL он имеет две
формы: списка и подзапроса. В форме списка он имеет в правой части набор величин или
выражений, разделенных запятой. Предикат возвращает TRUE, если один из элементов набора
совпадает с элементом в левой части предиката. Он представляет собой краткую форму записи
списков или предикатов OR. Рассмотрим пример:

SELECT a, b, с
FROM Foobar
WHERE a = b
OR a = с;

Его можно переписать так:

SELECT a, b, с FROM Foobar WHERE a IN (b, с);

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

Исключения 
       Остерегайтесь значений NULL! Предикат IN() определяется как цепочка OR-предикатов,
поэтому:

a IN(x, у, z)

означает

((а = х) OR (a = у) OR (a = z))

       Из этого следует:

a IN (x, у, NULL)

означает

((а = х) OR (a = у) OR (a = NULL)) ((а = х) OR (a = у) OR UNKNOWN)

       Помните, что NULL — не то же самое, что UNKNOWN. В SQL-92 нет булева типа данных. Вы
не можете применять AND, OR и NOT к значению NULL Предикат NOT IN() определяется как
отрицание предиката IN():

a NOT IN (x, у, z)

означает:

NOT (a IN (x, у, z))


NOT ((a = х) OR (a = у) OR (a = z))
(NOT(a = x) AND NOT(a = у) AND NOT(a = z)) -- закон де Моргана
((a <> x) AND (a <> y) AND (a <> z)) -- определение

       Зададим в качестве одного из элементов списка значение NULL

((а <> х) AND (a <> у) AND UNKNOWN) (UNKNOWN)

       Если вы хотите найти в списке пару к значению NULL, используйте COALESCE() для значения
NULL в левой части выражения:

WHERE a IN (x, у, COALESCE (z, a))

       Это несколько более понятно, чем:


WHERE (a IN (x, у) OR z IS NULL)

Используйте CASE вместо сложных вложенных предикатов


       В работе с конструкцией WHERE есть один хитрый ход — использовать выражение CASE для
сложного предиката с логическим следованием. Если вы забыли логику, преподаваемую на
первом курсе, логический оператор следования записывается как стрелка и означает “из р следует
q” или “если истинно р, то истинно и q”:

WHERE CASE
WHEN <условие поиска #1> THEN 1
WHEN <условие поиска #2> THEN 1
ELSE 0 END = 1

       Функция, которая возвращает единицу или ноль, принимая предикат в качестве параметра, в
логике и теории множеств называется характеристической функцией.  Посмотрите еще раз
правила для выражения CASE в разделе “Использование выражений CASE” и вы поймете, о чем
речь. Определенный порядок выполнения конструкций WHEN можно использовать для
оптимизации действия программы и избавления от излишних проверок. Вы также вольны
помещать выражения CASE внутри конструкций WHEN и THEN, содержащих выражение CASE и
представлять логику этой конструкции в виде древовидной структуры:

WHERE CASE
WHEN <условие поиска #1> THEN CASE
WHEN <условие поиска #1.1>
THEN 1
WHEN <условие поиска #1.2>
THEN 1 ELSE О END WHEN <условие поиска #2> THEN 1
ELSE 0 END = 1

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

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

UPDATE Teams
SET score = score + 1; --увеличение счета

       Он не дает информации о том, что означает этот “счет” и почему его нужно увеличивать. 
       В стандартном SQL комментарии начинаются с двух черточек (--) и заканчиваются с началом
новой строки, так как первые реализации SQL были написаны для мэйнфреймов IBM и
использовали перфокарты. Для современных компьютеров такой вариант неудачен, так как они
способны хранить текст в свободном формате. Разбиение текста программы на строки может
расщепить комментарий и приведет к сообщению об ошибке. Кроме того, SQL поддерживает
унарный оператор вычитания, поэтому такие комментарии в некоторых редких случаях могут стать
двусмысленными и очень сильно затруднить работу компилятора. В более поздних стандартах для
обозначения комментариев предусмотрена пара символов /* и */ в стиле языка Си. Лучше всего
пользоваться именно ими. Похожие пары скобок предусмотрены и многими разработчиками. 
       SQL-программисты не любят сопровождать код комментариями, вероятно, из-за того что SQL
выполняет много действий в одном операторе, а программисты приучены давать комментарии на
уровне отдельных операторов, а не описывать предназначение кода в целом. Более высокий
уровень абстракции запутывает их. Они не хотят помещать комментарии на уровне конструкций,
поскольку с их точки зрения это портит внешний вид кода. 
       Преодолейте это. Вам необходимы комментарии, описывающие блоки SQL на высоком
уровне, а также более детальные комментарии для особенно важных конструкций. Постарайтесь
составлять комментарии, не нацеленные специально на SQL-программистов, и пишите их на
простом языке. Например, не говорите “реляционное деление транспортных средств на доступных
водителей”, подразумевая, что читатель знает, что такое “реляционное деление”. Вместо этого
напишите “список водителей, которые могут управлять всеми транспортными средствами в
гараже”. Давайте ссылки на документацию к схеме и приложению. Это, конечно, подразумевает,
что эта документация есть, актуальна и хорошо составлена. 
       Если у вас есть достаточно времени, вы можете как настоящий гуру SQL-программирования
применить и такой прием. Сохраняйте в качестве комментариев те выражения, что успешно
выполняли поставленную задачу, но были затем заменены лучшим вариантом. В SQL то, что было
лучшим в одной ситуации, часто вскоре перестает быть таковым. Не заставляйте следующего
программиста начинать с “чистого листа”, поделитесь с ним своим опытом.

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

Хранимые процедуры
       Всегда начинайте хранимую процедуру с комментария, который, как минимум, содержит
информацию об авторе, дате написания и последовательности вносимых в нее изменений. Это
требование — основа управления ПО. Добавьте подробное описание назначения модуля. Имя
процедуры должно иметь вид “<действие><объект>”. При необходимости снабдите комментарием
каждый параметр.

Комментарии к управляющим операторам


       Комментарии к управляющим конструкциям IF-THEN-ELSE, BEGIN-END и WHILE-DO будут
выглядеть как комментарии в любой процедурной программе. Более сложные SQL-операторы
нуждаются в комментариях как в самом начале, так и на уровне конструкций.

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

Избегайте подсказок оптимизатору


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

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

Используйте DRI-каскадирование вместо триггеров


Обоснование 
       Существуют стандарты ANSI/ISO для триггеров, но их синтаксис и семантика продолжают
оставаться крайне нестандартизованными. Триггер связан с некоторой таблицей и запускается
тогда, когда с таблицей базы происходит определенное событие. Его код обычно написан на языке
3GL Под событием в базе понимается нечто, изменяющее данные, — вставка, обновление или
удаление. 
       В стандарте ANSI триггер не запускается при вставке данных, но в продуктах некоторых
разработчиков это делается. В полном ANSI-стандарте с одной таблицей может сопоставляться
несколько триггеров, которые запускаются последовательно до или после какого-либо события в
базе. В большинстве продуктов такой развитый контроль за триггерами отсутствует. С другой
стороны, синтаксис и семантика DRI-каскадов хорошо определены и стандартизированы. 
       В ноябре 2004 г. некий новичок поместил на форуме Web-сайта SQL Server Central вопрос под
заголовком “Нужна помощь с вычислительным триггером”. У него возникли затруднения с
созданием триггера для проверки единиц в “числовом поле”. На самом деле, настоящая проблема
была в том, что он не знал, что поле и столбец — это не одно и то же. 
       В его БД был столбец типа FLOAT по имени Length. Предполагалось, что ввод данных в него
осуществляется в сантиметрах, но некоторые люди упорно вводили длину в метрах и
миллиметрах. Спрашивающий пытался написать триггер, который запускался бы по событиям
UPDATE или INSERT и проверял значение длины. Если оно больше 10, тогда это, скорее всего,
миллиметры, их число надо разделить на 10. Если число меньше нуля, тогда оно, вероятно,
записано в метрах и должно быть умножено на 100.

CREATE TRIGGER SetCentimeters


AFTER INSERT ON Samples
UPDATE Samples
SET Length
= (CASE
WHEN Length > 10.00 THEN Length / 10.00
WHEN Length < 0.00 THEN Length * 100.00
ELSE Length END)
WHERE Length NOT BETWEEN 0.00 AND 10.00,

       Однако это неправильный ответ, написанный на процедурном языке. Правильный ответ —
задать ограничение в DDL, как-то так:

Length DECIMAL(2,1) NOT NULL


CONSTRAINT lengthjLn_centimeters_only
CHECK (Length BETWEEN 0.01 AND 9.99)

       Триггер, конечно, исправит ошибку “на лету”, но подлинная-то цель должна состоять в том,
чтобы ошибок вообще не было.

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

Используйте хранимые процедуры SQL


       В каждый SQL-продукт включена некоторая разновидность языка 4GL, позволяющая писать
хранимые процедуры, которые находятся в базе данных и могут выполняться из прикладной
программы. Несмотря на то, что существует стандарт SQL/PSM, на момент написания этой книги
ему следуют только Mimer и IBM. Остальные разработчики пользуются собственными 4GL T-SQL
для семейства Sybase/SQL Server, PL/SQL из Oracle, Informix-4GL из Informix и так далее. Для
получения более детальной информации по этому вопросу я рекомендую приобрести
замечательную книгу Джима Мелтона Understanding SQVs Stored Procedures(ISBN 1-55860461-8).
Хранимые процедуры имеют следующие преимущества. 
       — Безопасность. Пользователи могут делать только то, что позволяют делать хранимые
процедуры, тогда как динамический SQL и другие нестандартные средства открывают им доступ к
самой БД. Связанные с этим проблемы сохранности и безопасности данных очевидны. 
       — Легкость обслуживания. Хранимую процедуру легко заменить улучшенной версией или
перекомпилировать. Все прикладные программы, которые вызывают эту процедуру, будут
работать и с обновленной версией. 
       — Сокращение сетевого трафика. Поскольку при использовании хранимых процедур по сети
пересылаются только параметры, сетевой трафик ниже, чем при пересылке SQL-кода. 
       — Согласованность. Если какая-то операция выполняется с привлечением хранимой
процедуры, она всегда будет выполняться одинаково.В противном случае вам нужно следить,
чтобы все программисты (настоящие и будущие) проделывали эту операцию правильно.
Программисты — не воплощение зла, но люди из плоти и крови. Поставьте им задачу, чтобы
клиенту было как минимум 18 лет, и один программист напишет “age > 18”, а другой — “age >= 18”
без какого-либо злого умысла. Не следует ждать, что они запомнят все коммерческие правила и
всегда будут писать безупречные коды. 
       — Модульность. Однажды создав библиотеку хранимых процедур, выможете вновь и вновь
использовать ее для создания новых процедур. Зачем каждую неделю изобретать колесо? 
       В главе 8 описан общий подход к тому, как писать хранимые процедуры в SQL Если вы
заглянете в любую группу новостей по SQL, то увидите коды, внушающие страх. Вероятно,
программистам больше не читают базовых курсов по разработке ПО. Или же они думают, что
старые правила неприменимы к 4GL-языкам современных разработчиков.

Избегайте пользовательских функций и расширений


Обоснование 
       SQL — язык, ориентированный на работу с массивами данных, а не со скалярами, но
программисты будут всячески стараться обходить эту модель, чтобы вернуться к тому, что они
знают, создавая пользовательские функции на других языках и помещая их внутрь БД. 
       Есть два вида пользовательских функций и расширений. В некоторых SQL-продуктах
допускается включать в состав БД функции, написанные на других языках, словно они являются
частью SQL В других имеется специфический язык, позволяющий пользователю создавать
расширения. 
       Даже SQL/PSM позволяет писать пользовательские функции на любом языке
программирования стандарта ANSI X3J, в котором имеются конверторы типов данных и интерфейс
с SQL В операторе CREATE PROCEDURE для этой цели существует конструкция LANGUAGE. 
       В Microsoft пошли еще дальше. Общеязыковая среда выполнения (common language runtime,
CLR) на SQL Server может включать в себя код, обработанный любым компилятором, если он
создает CLR-модуль. Технология Data Blade от Illustra сейчас является частью Informix. В IBM
имеются расширения функциональности базовой СУБД. 
       Рациональное зерно, стоящее за всеми этими пользовательскими функциями и
расширениями, заключается в том, что продукт разработчика становится более мощным. Однако
пользовательские функции трудно поддерживать, они влияют на переносимость кода и могут
нарушить целостность данных.
Исключения 
       В некоторых редких случаях подобные инструменты действительно облегчают жизнь, но
большая часть приложений, работающих с данными, хорошо работают и со стандартным SQL
Решение применить пользовательские функции должно быть хорошо обоснованным. Будьте
готовы к тому, что от вас потребуется много дополнительной работы.

Проблемы с многоязыковой поддержкой


       Языки программирования работают по-разному, поэтому, позволяя нескольким языкам
функционировать внутри БД, вы рискуете нарушить целостность данных. Вот простой пример: как
языки сравнивают строки? Семейство Xbase не учитывает регистр и усекает длинные строки,
тогда как SQL дополняет более короткие строки и учитывает регистр. Как в вашем языке действует
функция MOD(), если один или оба аргумента являются отрицательными? Как язык производит
округление и усечение?

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

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

Избегайте вторичных индексов


       Во-первых, индексы применяются не в каждом SQL-продукте. Nucleus основан на сжатых
битовых векторах, Teradata использует хэширование и так далее. Однако различного вида
древовидные индексы достаточно распространены, чтобы о них стоило упомянуть. В
руководстве Х/Ореп SQL Portability Guidesприведен базовый синтаксис, который используется в
различных диалектах с некоторыми дополнениями. Пользователю может быть предоставлена
возможность выбрать тип используемого индекса. 
       Первичный индекс — это индекс, созданный для реализации в БД ограничений PRIMARY KEY
и UNIQUE. Без них схема не будет корректной моделью данных, так как ни у одной таблицы не
будет ключа. 
       Вторичный индекс — это дополнительный индекс, создаваемый администратором для
повышения производительности. Схема будет возвращать те же ответы, что и без них, но,
возможно, не совсем своевременно или даже не в исторически обозримый промежуток времени. 
       Индексы — это один из элементов, рассматриваемых оптимизатором при построении плана
выполнения. Порядок применения индекса зависит от его типа, от типа запроса и от вида
статистического распределения данных. Небольшое изменение в одном из перечисленных
факторов может позже привести к новому плану выполнения. Помня об этом, мы теперь можем в
общих чертах обсудить индексы с древовидной структурой. 
       Если в некотором операторе предполагается использовать более чем несколько процентов
таблицы, то индексы игнорируются, и таблица просматривается от начала до конца.
Использование индексов привело бы к большим накладным расходам, чем просто перебор строк. 
       Основная проблема с индексами заключается в том, что избыточные или неиспользуемые
индексы занимают некоторую область памяти и должны обновляться при каждом изменении
базовой таблицы. Они замедляют операции обновления, вставки или удаления данных из
таблицы. Изредка индексы также способны запутать оптимизатор, приведя его к плохому
решению. В некоторых SQL-продуктах имеются специальные инструменты, которые предлагают
индексы, опираясь на реальный оператор, переданный в SQL-ядро. Не пренебрегайте этими
средствами.

Избегайте связанных подзапросов


Обоснование 
       На заре SQL оптимизаторы плохо справлялись с упрощением сложных SQL-выражений со
связанными подзапросами. Они вслепую запускали циклы внутри циклов, снова и снова
просматривая таблицу из самого внутреннего цикла. Далее рассмотрен пример, иллюстрирующий
подобное поведение. В этих двух запросах столбец “х” не может принимать значение NULL, а
таблица “Foo” много больше таблицы “Bar” Оба запроса приводят к одинаковому результату.
Сравните:

SELECT a, b, с
FROM Foo
WHERE Foo.x IN (SELECT x FROM Bar);

SELECT a, b, с
FROM Foo
WHERE EXISTS (SELECT *
FROM Bar
WHERE Foo.x = Bar.x;

       В старых реализациях SQL предикат EXISTS() объединил бы две таблицы, что заняло бы
много времени. Предикат IN() поместил бы меньшую таблицу в главное хранилище и просмотрел
бы ее, возможно, сортируя для ускорения поиска. Но теперь это делается не совсем так. В
зависимости от конкретного оптимизатора и методов доступа, связанные подзапросы могут уже не
быть такими громоздкими, какими были раньше. В некоторых продуктах допускается создание
индексов, которые “предварительно” соединяют таблицы, позволяя обрабатывать такие запросы
быстрее. 
       Однако людям трудно читать связанные подзапросы, да и оптимизаторы тоже до сих пор не
всегда достаточно сообразительны. Рассмотрим таблицу, которая моделирует ссуды и платежи с
кодом состояния для каждого платежа. Это классическое отношение “один ко многим”. Задача
заключается в том, чтобы выбрать ссуды, у которых все платежи имеют статус “F”:

CREATE TABLE Loans


(loan_nbr INTEGER NOT NULL,
payment_nbr INTEGER NOT NULL,
payment_status CHAR(1) NOT NULL
CHECK (payment_status IN ('F', 'U', 'S')),
PRIMARY KEY (loan.nbr, paymentjibr));

       Одно решение задачи состоит в использовании связанного скалярного подзапроса в списке
SELECT:

SELECT DISTINCT (SELECT loan_nbr FROM Loans AS L1 GROUP BY


L1.loan_nbr HAVING COUNT(L1.payment_status) =
C0UNT(L2.loan_nbr)) AS parent FROM Loans AS L2 WHERE
L2.payment_status = 'F' GROUP BY L2.1oan_nbr;

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

SELECT loan_nbr
FROM Loans
GROUP BY loan_nbr
HAVING MAX(payment_status) = "F"
AND MIN(payment_status) = 'F';

       Разобраться в рекурсивных ссылках и корреляциях трудно как человеку, так и машине.
Большинство оптимизаторов недостаточно сообразительны, чтобы упростить запрос описанным
образом.

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

Избегайте предложений UNION


Обоснование 
       Предложения UNION обычно не поддаются хорошей оптимизации. Поскольку в них требуется
отбрасывать избыточные дубликаты, они заставляют большинство ядер SQL производить
сортировку, прежде чем представить результаты пользователю. По возможности вместо UNION
используйте UNION ALL. И уж, конечно, никогда не запускайте цепочку предложений UNION,
основанных на одной и той же базовой таблице, Такой код можно написать с использованием
предикатов OR или с выражениями CASE. 
       Рассмотрим в качестве примера ужасающе неправильного употребления SQL процедуру,
создающую динамический SQL, который затем выдает отчет. Кроме очевидного нарушения
основных правил разработки ПО, код был настолько велик, что превышал предельный размер
текстового файла в SQL Server. В нем производилась попытка создать полный отчет по базе
данных, используя предложения UNION. Чтобы расположить 12 строк отчета в правильном
порядке, каждор! присваивается буква алфавита. Чтобы показать полную версию кода,
потребовалось бы несколько страниц, поэтому здесь приведен фрагмент, отвечающий за вывод
данных на печать. Я не пытался переделать этот код, поэтому в приведенном кусочке содержится
много нарушений принципов хорошего кодирования.

UNION
SELECT DISTINCT 'J' AS section,
'NUMBER CHECKS' AS description,
' ' AS branch,
COUNT(DISTINCT GL.source) AS totali,
0 AS total2
FROM GeneralLedger AS GL
WHERE GL.period >= :start_period
AND GL.period <= :end_period
AND GL.year_for_period = :period_yr
AND GLaccountjiumber IN ('3020')
AND GL.journal_id IN ('CD')
UNION
SELECT DISTINCT 'С1 AS section,
'CASH RECEIPTS' AS description,
'' AS branch,
SUM(GL.amount) * -1 AS totali,
0 AS total2
FROM GeneralLedger AS GL
WHERE GL.period >= :start_period
AND GL.period <= :end_period
AND GL.year_for_period = :period_yr
AND GL.account_number = '1050'
AND GL.journal_id IN ('CR')
UNION
SELECT DISTINCT 'D' AS section,
'NUMBER INVOICES' AS description,
' ' AS branch,
COUNT(DISTINCT GL.source) AS totali,
0 AS total2
FROM GeneralLedger AS GL
WHERE GL.period >= :start_period
AND GL.period <= :end_period
AND GL.year_for_period = :period_yr
AND GL.account_number IN ('6010', '6090')
AND GL.journal.id IN ('SJ')
UNION
SELECT DISTINCT
'E1 AS section,
'VOUCHER TOTAL1 AS description,
' ' AS branch,
SUM(GL.amount) * -1 AS totali,
0 AS total2
FROM GeneralLedger AS GL
WHERE GL.period >= :start_period
AND GL.period <= :end_period
AND GL.year_for_period = :period_yr
AND GL.account_number = '3020'
AND GL.journal_id IN ('PJ',TJ1)
UNION
SELECT DISTINCT
'F' AS section,
'CHECKS PRINTED' AS description,
' ' AS branch,
SUM(GL.amount) AS totali,
0 AS total2
FROM GeneralLedger AS GL
WHERE GL.period >= :start_period
AND GL.period <= :end_period
AND GL.year_for_period = :period_yr
AND GL.account_number IN ('3020')
AND GL.journal_id IN ('CD')
UNION
SELECT DISTINCT
'K' AS section,
'NUMBER VOUCHERS' AS description,
' ' AS branch,
COUNT(DISTINCT GL.source) AS totali,
0 AS total2
FROM GeneralLedger AS GL
WHERE GL.period >= :start_period
AND GL.period <= :end_period
AND GL.year_for_period = :period_yr
AND GL.account_number IN ('3020')
AND GL.journal_id IN ('PJ', 'TJ');

       Обратите внимание, что “section”, “description” и “branch” — это “заглушки”, выделяющие место
для столбцов в других предложениях UNION, не показанных здесь. Последнюю часть кода можно
оформить в виде связной отдельной процедуры:

CREATE PROCEDURE GeneralLedgeSummary (start_period DATE, end_period DATE)


SELECT
COUNT(DISTINCT CASE WHEN acct_nbr = '3020' AND journal_code = 'CD'
THEN source ELSE NULL END),
-SUM(CASE WHEN acct._nbr = '1050' AND journal_code ='CR'
THEN amount ELSE 0.00 END),
COUNT(DISTINCT CASE WHEN acct_nbr IN ('6010', '6090') AND journal_code =
'SJ'
THEN source ELSE NULL END),
-SUM(CASE WHEN acct__nbr = '1050' AND journal_code = 'CR'
THEN amount ELSE 0.00 END),
SUM(CASE WHEN acctjibr = '3020' AND journal_code = 'CD1
THEN amount ELSE 0.00 END),
COUNT(DISTINCT CASE WHEN acct_nbr = '3020' AND journal_code IN ('PJ',
'TJ')
THEN source ELSE NULL END)
INTO j_tally, c_total, d_tally, e_total, f_total, k_tally
FROM GeneralLedger AS GL
WHERE period BETWEEN start_period AND end_period;

Исключения 
       Иногда UNION (или UNION ALL) — это действительно то, что вам нужно. Другие подобные
операции в SQL-92 — EXCEPT (EXCEPT ALL) и INTERSECT (INTERSECT ALL) —- до сих пор не
нашли широкого применения.

Тестирование SQL
       Когда вы впервые пишете схему, вы, возможно, сгенерируете для нее некоторые тестовые
данные. Полистав литературу, вы узнаете о так называемом наборе Армстронга (Armstrong set) —
минимальном числе строк, которое необходимо для тестирования всех ограничений схемы.
Построить набор Армстронга непросто, но проверить схему можно и с меньшими усилиями.

Тестируйте все возможные комбинации NULL-значений


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

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

Тестируйте все ограничения СНЕСК()


Обоснование 
       Выделите все ограничения CHECK в DDL и просмотрите их. Проверьте — действительно ли
одним и тем же элементам данных во всех таблицах сопоставлены одни и те же правила? Если
модель корректна, к некоторым атрибутам всегда будут применимы одинаковые ограничения
CHECK. Например, коды UPC будут проверяться одинаково в любом месте схемы. У других
атрибутов возможны различные ограничения в различных таблицах. Например, разумно
предположить, что на количество единиц товара повсеместно наложено ограничение “CHECK
(quantity >= 0)”. Но вот в одном месте вам встретилось ограничение “CHECK (quantity > 0)”. Ошибка
это, или нулевое значение здесь действительно исключено? Вам необходимо посмотреть и
проверить.

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

Будьте внимательны к символьным столбцам


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

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

Исключения 
       Нет.
ГЛАВА 7. 
Представления
Слепцы и слон
Джон Годфри Сакс (John Gogfrey Saxe) (1816-1887)* * Перевод Д. 3. Вибе.

Шесть индостанских мудрецов, познания столпы, 


Решили рассмотреть слона (хоть были все слепы). 
Не помешает слепота избранникам толпы!
Один на ощупь подошел, подпрыгнул сгоряча, 
Потрогал твердый бок слона и отбежал, крича: 
“То, что зовете вы слоном, — стена из кирпича!”
Второй же бивень ощутил, задев его плечом: 
“Округлость, гладкость, острие! Стена здесь ни при чем! 
И потому слона сравнить приходится с копьем!”
У третьего из мудрецов фантазия своя. 
Он длинный хобот ухватил, безумно вопия: 
“Извилист слон, увертлив слон и гибок, как змея!”
К ноге четвертый подошел, провел по ней рукой 
И произнес: “Все ясно мне. И мой ответ такой: 
Напоминает чудо-зверь древесный ствол простой!”
К слону подкрался пятый член собранья мудрецов.
Слону он ухо теребил, сказав в конце концов:
“Слон — словно веер! Ясно то слепейшим из слепцов!”
Шестой приблизился туда, где слон хвостом махал. 
Столь тонких длинных вееров нигде он не встречал. 
“Слон на веревочку похож!” — шестой мудрец сказал.
Упрямо спорили слепцы среди высоких трав, 
И каждый спорил о своем, суров и величав, 
И каждый видел правды часть, всей правды не узнав!
Мораль:
Теологам, ведущим спор, я посвятил сей стих. 
В слепом невежестве они до края дней своих 
Готовы спорить о слоне, неведомом для них!

       Представление (view) — это виртуальная таблица, задаваемая оператором SELECT, который


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

Имена представлений подчинены тем же правилам, что и имена


таблиц
Обоснование 
       Представление является логической таблицей. Подобно физической таблице, оно состоит из
строк и столбцов. Представление, как и базовую таблицу, допускается применять в операторах
SELECT, UPDATE, DELETE и INSERT. Поэтому к именам представлений предъявляются те же
требования, что и к именам таблиц. К слову сказать, те же правила применимы и к псевдонимам,
синонимам, производным таблицам, функциям, возвращающим таблицы и пр. 
       Существует абсурдная традиция добавлять к имени представления символы “v” или “vw” в
виде суффикса или префикса. По-видимому, ее источником являются программисты, привыкшие к
слабо типизированным языкам с венгерской нотацией или работавшие в файловых системах, в
которых префиксы применялись для определения физического расположения файла. Между
прочим, согласно стандарту ISO-11179, символы “vw” предполагают, что данная таблица как-то
связана с фольксвагенами. В общем, если кому-то нужно отличить представление от таблицы, за
исчерпывающей информацией он может обратиться к информационной таблице схемы. Правда,
для этого нужно быть как минимум администратором системы. 
       С представлениями определенного типа нельзя выполнять операторы INSERT, UPDATE и
DELETE. Пользователям, которые нуждаются в выполнении этих действий, можно предоставить
триггеры INSTEAD OF, так что они вообще не узнают, с чем конкретно имеют дело: с
представлением или таблицей.

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

Всегда указывайте имена столбцов


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

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

Представления обеспечивают безопасность на уровне строк и


столбцов
       Одно из наиболее важных преимуществ представления заключается в расширении
возможностей защиты данных в SQL С помощью представления пользователю можно открыть
доступ лишь к ограниченному подмножеству строк, столбцов или строк и столбцов. 
       Рассмотрим таблицу “Персонал”, в которой содержится информация о работниках
организации: имя, адрес, должность, дата рождения и оклад. Доступ ко всей этой информации
нужен далеко не каждому пользователю. В частности, от большинства пользователей наверняка
нужно скрыть данные об окладе. Чтобы добиться этого, вы создаете представление, которое не
содержит столбца с окладом и открываете для большинства пользователей доступ к этому
представлению, а не к базовой таблице. Столбец с окладом не будет им виден. 
       Теперь представим себе таблицу с информацией о различных проектах. В ней, вероятно,
будут столбцы с названием, описанием, датой начала и фамилией ответственного работника.
Допустим, по правилам вашей организации доступ к информации о проекте имеет только тот
сотрудник, который им руководит. Включив в базовую таблицу с информацией о проектах код
авторизации ответственного сотрудника, вы можете создать представление на основе значения
CURRENT_USER:

CREATE VIEW MyProjects (..)


AS
SELECT ..
FROM Projects WHERE authonzed_user = CURRENT_USER;

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

CREATE VIEW MyProjects (..)


AS
SELECT ..
FROM Projects AS P
WHERE CURRENTJJSER
IN (SELECT team_user_id
FROM PirojectTeams AS PT
WHERE P.team_nbr = PT.team_nbr);

       Интересные возможности открываются при использовании в представлениях величин


CURRENT_TIMESTAMP или CURRENT_DATE. С их помощью можно автоматически обновлять
расписания и другие списки, связанные со временем:

CREATE TABLE AssignmentSchedule


(ssn CHAR(9) NOT NULL REFERENCES Personnel(ssn) ON UPDATE CASCADE
ON DELETE CASCADE,
task_code CHAR(5) NOT NULL,
start_date TIMESTAMP NOT NULL,
end_date TIMESTAMP NOT NULL,
CHECK (start_date < end_date),
PRIMARY KEY (upc, start_date));
CREATE VIEW Assignments (now, ssn, task_code)
AS
SELECT CURRENT_TIMESTAMP, ssn, task_code
FROM AssignmentSchedule
WHERE CURRENTJIMESTAMP BETWEEN start_date AND end_date;

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

Представления обеспечивают эффективные пути доступа


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

Представления скрывают от пользователя сложность задачи


       Это применение представлений родственно тому что было описано в разделе “Представления
обеспечивают безопасность на уровне строк и столбцов”: с помощью представления вы можете
скрыть от пользователя особенно сложные фрагменты SQL-программы. Это особенно полезно,
когда в вашей организации есть сотрудники, лишь начинающие знакомится с SQL (программисты,
аналитики, менеджеры или просто рядовые пользователи БД). 
       Рассмотрим в качестве иллюстрации код для реляционного деления — одной из восьми
основных операций реляционной алгебры (Кодд 1979). Суть его состоит в том, что таблица-
делитель используется для разделения таблицы-делимого и составления таблицы-частного.
Таблица-частное включает в себя те значения одного столбца, для которых в таблице-делителе
имелись все значения второго столбца. 
       Обратимся к конкретному примеру. У нас есть таблица PilotSkills с данными о летчиках и
самолетах, которыми они способны управлять (делимое). У нас есть таблица с данными о
самолетах, которые стоят в ангаре (делитель). Нам нужны имена пилотов, которые могут
управлять любым самолетом в ангаре (частное). Чтобы получить этот результат, мы делим
таблицу PilotSkills на самолеты в ангаре:

CREATE TABLE PilotSkills


(pilot CHAR(15) NOT NULL,
plane CHAR(15) NOT NULL,
PRIMARY KEY (pilot, plane));
CREATE TABLE Hangar
(plane CHAR(15) NOT NULL PRIMARY KEY);

       Вот как можно написать нужный запрос:

CREATE VIEW QualifiedPilots (pilot)


AS
SELECT DISTINCT pilot
FROM PilotSkills AS PS1
WHERE NOT EXISTS (SELECT *
FROM Hangar
WHERE NOT EXISTS (SELECT *
FROM PilotSkills AS PS2
WHERE (PS1.pilot = PS2.pilot)
AND (PS2.plane = Hangar.plane)));

       Понятно, что неопытный SQL-программист такую конструкцию сразу не напишет. Зато
оператор “SELECT pilot FROM QualifiedPilots;” дастся ему без труда. Более того, определение
VIEW можно скорректировать, и пользователь об этом даже не узнает. Вы можете, например,
захотеть переписать определение деления так:

CREATE VIEW QualifiedPilots (pilot)


AS
SELECT PS1.pilot
FROM PilotSkills AS PS1, Hangar AS H1
WHERE PS1.plane = H1.plane
GROUP BY PS1.pilot
HAVING COUNT(PS1.plane) = (SELECT COUNT(plane) FROM Hangar);

Представления обеспечивают корректность производных


данных
       Еще одно назначение представлений — обеспечение согласованности производных данных с
помощью создания в представлении новых столбцов, основанных на арифметических
выражениях. Например, можно создать представление со столбцом tot_comp, определенным как
(зарплата + комиссионные + премия). Поскольку имя столбца находится на уровне таблицы, его
можно использовать во вложенном операторе SELECT. Вот так делать нельзя:

SELECT emp_id, (salary + commission + bonus) AS tot_comp


FROM Payroll
WHERE tot_comp > 12000.00;

       А вот так — можно:

CREATE VIEW PayrollSummary (emp_id, tot_comp)


AS
SELECT emp_id, (salary + commission + bonus)
FROM PayrollSummary;

       при условии, что дальше идет:

SELECT emp_id, tot_comp


FROM PayrollSummary
WHERE tot_comp > 12000.00;

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

Представления позволяют переименовывать столбцы и


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

CREATE TABLE Stores


(store_nbr INTEGER NOT NULL PRIMARY KEY,
store_name CHAR(35) NOT NULL,
...);
CREATE TABLE Personnel
(ssn CHAR(9) NOT NULL PRIMARY KEY,
last_name CHAR(15) NOT NULL,
first_name CHAR(15) NOT NULL,
...);

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

CREATE TABLE JobAssignments


(store_nbr INTEGER NOT NULL REFERENCES Stores (store_nbr)
ON UPDATE CASCADE
ON DELETE CASCADE,
ssn CHAR(9) NOT NULL PRIMARY KEY REFERENCES Personnel( ssn)
ON UPDATE CASCADE
ON DELETE CASCADE,
start_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
end_date TIMESTAMP CHECK (start_date <= end_date),
job_type INTEGER DEFAULT 0 NOT NULL,
CHECK (jobjtype BETWEEN 0 AND 99),
PRIMARY KEY (store_nbr, ssn, start_date));

       Допустим, что код должности job_type = 0 соответствует значению “не назначено”, job_type = 1
соответствует ученику помощника грузчика, и так далее, пока не доберемся до кода 99 —
управляющий магазином. Понятно, что управляющий в каждом магазине только один. В стандарте
SQL-92 это ограничение записывается так:

CHECK (NOT EXISTS


(SELECT store_nbr
FROM JobAssignments
WHERE job_type = 99))

GROUP BY store_nbr HAVING COUNT(*) > 1))

       Но во многих SQL-продуктах не допускается применять ограничение СНЕСК() к таблице в


целом, и ограничение CREATE ASSERTION на уровне схемы они не поддерживают. Как же быть?
Воспользоваться триггером? Но ведь для этого понадобится — о, ужас! — процедурная
программа! Несмотря на наличие SQL/PSM и других стандартов, большинство разработчиков
реализуют различные модели триггеров с собственными 4GL-языками, но я — фанатик и хочу
чистого SQL-решения. 
       Создадим две таблицы:

CREATE TABLE Job_99_Assignments


(store_nbr INTEGER NOT NULL PRIMARY KEY REFERENCES Stores (store_nbr)
ON UPDATE CASCADE
ON DELETE CASCADE,
ssn CHAR(9) NOT NULL REFERENCES Personnel (ssn)
ON UPDATE CASCADE
ON DELETE CASCADE,
start_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
end_date TIMESTAMP CHECK (start_date <= end_date),
job_type INTEGER DEFAULT 99 NOT NULL CHECK (job_type = 99));
CREATE TABLE Job_not99_Assignments
(store_nbr INTEGER NOT NULL REFERENCES Stores (store_nbr)
ON UPDATE CASCADE
ON DELETE CASCADE,
ssn CHAR(9) NOT NULL PRIMARY KEY REFERENCES Personnel(ssn)
ON UPDATE CASCADE
ON DELETE CASCADE,
start_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
end_date TIMESTAMP CHECK (start_date <= end_date),
job_type INTEGER DEFAULT 0 NOT NULL
CHECK (job_type BETWEEN О AND 98) -- кода 99 быть не должно
);

       Теперь создадим представление с помощью UNION:

CREATE VIEW JobAssignments (store_nbr, ssn, start_date, end_date, job_type)


AS
(SELECT store_nbr, ssn, start_date, end_date, job_type
FROM Job_not99_Assignments
UNION ALL
SELECT store_nbr, ssn, start_date, end_date, job_type
FROM Job_99_Assignments)

       Совместное действие ключей и ограничений на значение столбца job_type в обеих таблицах
гарантирует, что в каждом магазине будет только один управляющий. Далее нужно будет добавить
в описание представления триггер INSTEAD OF, чтобы пользователи могли вставлять, обновлять
и удалять данные. 
       Упражнение для читателя: как организовать проверку того, что у каждого управляющего не
более двух помощников?

Обновляемые представления
       Стандарт SQL-92, по правде сказать, весьма консервативен в отношении того, какие
представления можно обновлять. Обновляемое представление должно удовлетворять следующим
требованиям. 
       1. Представление должно иметь в основе оператор SELECT, основанный на одной и только
одной таблице. Впрочем, допускается многоуровневое создание представления на основе других
представлений. 
       2. Представление должно включать все столбцы, связанные ограничениями UNIQUE или
PRIMARY KEY. Это гарантирует, что каждая строка представления соответствует одной и только
одной строке базовой таблицы. 
       3. Всем столбцам базовой таблицы, отсутствующим в представлении,должно быть присвоено
значение по умолчанию, или же эти столбцы должны допускать значение NULL Причина очевидна:
когда высоздаете в представлении новую строку, вставляя ее в базовую таблицу, система должна
знать, чем заполнять столбцы, которых нет впредставлении. 
       В реальных продуктах обновлять допускается и другие представления, но в любом случае за
обновлением всегда скрываются правила вставки, обновления и удаления строк, которые
связывали бы строки представления со строками базовой таблицы.

Конструкция WITH CHECK OPTION


       Конструкцию WITH CHECK OPTION в описании представления явно недооценивают. Конечно,
непросто разобраться в том, как она работает во вложенных представлениях. Но суть ее такова:
она не дает операторам UPDATE и INSERT INTO выйти за пределы набора значений, заданного
для обновляемого представления. Рассмотрим, например, такое представление:

CREATE VIEW NewYorkSalesmen (ssn, name, ...)


AS
SELECT ssn, name, ...
FROM Salesmen
WHERE city = 'New York';

       Теперь обновим его:

UPDATE NewYorkSalesmen SET city = 'Boston';

       В результате при следующем обращении к представлению NewYorkSalesmen оно окажется


пустым. Вряд ли мы добивались именно этого. А вот если бы мы определили представление так:

CREATE VIEW NewYorkSalesmen (ssn, name, ...)


AS
SELECT ssn, name, ...
FROM Salesmen
WHERE city = 'New York' WITH CHECK OPTION;

       система обнаружила бы нарушение и отвергла бы обновление.

Триггеры INSTEAD OF
       Некоторые представления нельзя обновлять, но вы можете скрыть это от пользователя с
помощью триггера INSTEAD OF. Он выполняется при попытке выполнить действия INSERT,
UPDATE или DELETE, заменяя их. Синтаксис от продукта к продукту меняется, но в целом
выглядит как-то так:

CREATE TRIGGER <имя триггера>


ON <имя таблицы>
[BEFORE | AFTER | INSTEAD OF]
[INSERT| DELETE | UPDATE]
AS [<выражение SQL> | BEGIN ATOMIC {<выражение SQL>;} END]

       Понятно, что для каждой таблицы или представления можно определить только по одному
триггеру INSTEAD OF на выражение INSERT, UPDATE или DELETE. Однако допускается
создавать одно представление на основе другого, задавая в каждом из них собственный триггер
INSTEAD OF. Использовать триггеры INSTEAD OF в обновляемых представлениях с параметром
WITH CHECK OPTION нельзя. Вместо этого можно определить триггер INSTEAD OF в описании
базовой таблицы, но это будет несколько странно, поскольку в вашем распоряжении есть триггеры
BEFORE и AFTER.

Не создавайте представления просто так


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

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

Не давайте представлениям размножаться


Обоснование 
       Смысл этого правила очень прост. Зачем создавать что-то ненужное? Оно просто напросто
занимает место, которое можно было бы занять чем-нибудь нужным. 
       При создании любого объекта SQL в информационные таблицы схемы добавляются новые
записи. Создание ненужных объектов схемы приводит к тому, что Крейг Маллинс (Craig Mullins)
называет забиванием каталога.  Например, в DB2 для каждого представления SQL могут
создаваться строки в четырех информационных таблицах схемы, связанных с представлениями
(SYSVTREE, SYSVLTREE, SYSVIEWS и SYSVIEWDEP), и трех информационных таблицах схемы,
связанных с таблицами (SYSTABLES, SYSTABAUTH и SYSCOLUMNS). 
       Не помешает обзавестись специальной программой, которая проверяла бы наличие
представлений, на которые нигде нет ссылки. Проверяйте также, нет ли у вас двух представлений,
решающих одну и ту же или почти одну и ту же задачу. Если таковые найдутся, удалите одно из
них.

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

Синхронизируйте представления с базовыми таблицами


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

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

Неправильное использование представлений


Часто бывает так, что представление в прошлом использовалось как средство для достижения
некой цели, которое со временем устарело и утратило актуальность по причине расширения
возможностей СУБД. 
      

Использование представлений для поддержки доменов


Обоснование 
       Очень жаль это признавать, но большинство СУБД не поддерживают доменов. Домены были
включены в исходную реляционную модель и с самого начала должны были стать частью SQL. По
большому счету, домен указывает возможный диапазон значений столбца, хотя, конечно, это
определение слишком упрощено. Например, в предикате допускается сравнивать только столбцы
из одного домена. 
       Функциональность доменов можно частично имитировать посредством представлений с
параметром WITH CHECK OPTION, гарантирующим сохранение целостности представления при
обновлении. Это означает, что все данные, добавленные или отредактированные с помощью
представления, будут согласованы с определением представления:

CREATE VIEW Personnel (ssn, name, sex, ...)


AS
SELECT ssn, name, sex, ...
FROM ISBN0008 -- имя, которое вы никому не хотите показывать
WHERE sex IN (0, 1, 2) -- коды iso
WITH CHECK OPTION;

В наши дни имитация доменов с помощью представлений все еще возможна, но теперь имеется и
лучший способ добиться того же результата, а именно ограничения CHECK(): 
      

CREATE TABLE Personnel


(ssn CHAR(9) NOT NULL, name, sex, ...)
sex INTEGER DEFAULT 0 NOT NULL CHECK (sex IN (0, 1, 2)),

       Использовать ограничение CHECK() проще, чем создавать представление с параметром WITH
CHECK OPTION.

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

Незаменимые представления
Обоснование 
      

В прошлом представления часто использовались в качестве единственного решения проблемы


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

CREATE VIEW StockSummary


(ticker_sym, min_price, max_price, avg_price)
AS
SELECT ticker_sym, MIN(price), MAX(price), AVG(price)
FROM Portfolio
GROUP BY ticker_sym;

       После создания представления с помощью приведенного ниже оператора SELECT


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

SELECT P.ticker_sym, P.quote_date, S.min_price, S.max_price, S.avg_price,


(P.price - S.avg__price) AS fluctuation
FROM Portfolio AS P, StockSummary AS S
WHERE P.ticker_sym = S.ticker_sym;

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


данным. Однако с появлением табличных выражений (их иногда называют встроенными
представлениями) такой способ использования представлений устарел. Почему? Потому что
теперь можно взять тот код, который использовался для создания представления, и вставить его
непосредственно в выражение SQL, в котором предполагалось использовать представление.
Выражение SQL для предыдущего примера выглядит так:

SELECT P.ticker_sym, S.min_price, S.max_price, S.avg_price,


(P.price - S.avg_price) AS fluctuation
FROM Portfolio AS P,
(SELECT ticker_sym, MIN(price), MAX(price), AVG(price)
FROM Portfolio
GROUP BY ticker_sym) AS S
WHERE P.ticker_sym = S.ticker_sym;

       To есть, вместо создания и обслуживания представления мы можем использовать табличное


выражение.

Исключения 
       Создавайте представление, если выражение используется во многих местах и имеет четкое
значение в модели данных.
Не подменяйте базовую таблицу представлением
Обоснование 
       Часто приходится слышать странный совет: создавать в приложении SQL по одному
представлению на базовую таблицу. Крейг Маллинс (Craig Mullins) называет это “большим
представленческим мифом”. Предполагается, что эта мера ограждает приложение от изменений в
БД за счет того, что вместо таблиц программы приложения получают доступ только к
представлениям. Если в базовую таблицу вносится изменение, программы редактировать не
нужно, поскольку они имеют доступ не к таблице, а к представлению. 
       Никакого сколько-нибудь существенного обоснования у этого правила нет. Больше того, опыт
свидетельствует, что использовать представления в качестве замены базовых таблиц не нужно.
На первый взгляд эта рекомендация кажется разумной, но на практике бездумного создания
представлений нужно избегать. Изменения в БД должны скрупулезно анализироваться независимо
от того, представления или таблицы используются в приложении. Рассмотрим простейшее
изменение схемы — добавление нового столбца. Если вы не добавите этот столбец и к
представлению, ни одна программы не получит к нему доступ. Конечно, если вы не создадите еще
одно представление, в которое этот столбец будет включен. Но если вы будете создавать новое
представление при каждом добавлении столбца в таблицу, ваша схема скоро превратится в
свалку представлений. Возникает естественный вопрос: которое из них использовать в конкретной
программе? Те же аргументы применимы и к удалению столбцов, переименованию таблиц и
столбцов, объединению и разделению таблиц. 
       В общем, если вы выполняете правила хорошего программирования на SQL, проблема
изоляции программ и данных с помощью представлений перед вами не встанет. Отвергнув
“большой представленческий миф”, вы сэкономите массу времени, которое иначе пришлось бы
потратить на обслуживание всей этой массы представлений.

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

Используйте материализованные представления


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

CREATE VIEW NewYorkSalemen (ssn, first_name, ...)


AS
SELECT ssn, first_name, ...
FROM Personnel
WHERE city = 'New York';

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

SELECT ssn, first_name, ...


FROM NewYorkSalemen
WHERE firstname = 'Joe1;

       на практике превращается в:

SELECT ssn, first_name, ...


FROM (SELECT ssn, first_name, ...
FROM Personnel
WHERE city = 'New York') AS NewYorkSalemen (ssn, first_name, ...)
WHERE firstname = 'Joe';

       а в дереве разбора будет, вероятно, выглядеть так:

SELECT ssn, first_name, ...


FROM Personnel AS NewYorkSalemen (ssn, first_name, ...)
WHERE city = "New York"
AND firstname = 'Joe';

       Если представлением пользуются несколько сотрудников, его, вероятно, стоит


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

Исключения 
       Нет.
ГЛАВА 8.
Хранимые процедуры
На каком бы языке вы ни писали, как программист вы обязаны добиться 
наилучшего результата с теми средствами, что вам доступны. 
Хороший программист преодолеет недостатки языка и неуклюжесть 
операционной системы, но даже сверхпрекрасная среда программирования 
не спасет плохого программиста.
Керниган и Пайк

       В каждом SQL-продукте имеется та или иная разновидность языка 4GL, позволяющая писать
процедуры, которые хранятся в БД и могут вызываться из хост-программы. Все эти языки имеют
блочную структуру, но отличаются друг от друга возможностями и языковыми моделями.
Например, T-SQL представляет собой простой однопроходный компилятор, основанный на языках
Си и Алгол. Он задумывался не как язык для разработки приложений, а всего лишь как средство
для решения сиюминутных задач в базе данных SQL Server. В отличие от него язык PL/SQL
фирмы Oracle основан на языках Ада и SQL/PSM. Это сложный язык, вполне подходящий для
разработки приложений, как и язык Informix, генерирующий код на языке Си, который пригоден для
использования на многих платформах. 
       Это означает, что в разговоре о хранимых процедурах SQL я должен буду соблюдать
необходимый уровень общности. Но страшнее другое — мне придется обучить программистов
SQL базовым принципам составления программ. Взгляните на образцы SQL-кодов в группах
новостей: в большинстве своем они написаны так, словно Йордана (Yourdon), Демарко (DeMarco),
Дикстры (Dijkstra), Вирта (Wirth) и многих других просто не было. Люди, проснитесь!
Разработанные ими правила применимы к любому языку программирования, ибо создавались для
программирования вообще.

Большинство 4GL-языков SQL не предназначены для разработки


приложений
Обоснование 
       Большая часть специфических процедурных языков, добавляемых к SQL разработчиками, ни в
коей мере не предназначена для замены языков разработки приложений (обратите внимание на
исключения). Это не более чем микроязыки для выполнения простых процедур внутри базы
данных. 
       У классического микроязыка нет настоящего ввода-вывода; ваши возможности ограничены
выдачей сообщений на стандартное устройство. Нет возможности управлять файлами, проводить
сложные вычисления, отображать окна. Такие языки нужны для написания триггеров или простых
процедур по обслуживанию БД. Всегда считалось и считается, что они не должны быть длиннее
примерно 50 строк. 
       И это замечательно! Во многоуровневой архитектуре отображение и сложные расчеты
выполняются хост-языком на представительском уровне. Но стоит заглянуть в группу новостей по
SQL, и вы немедленно встретите там новичков, которые хотят управлять отображением данных
средствами БД. Они хотят дописывать нули впереди числа в операторе SELECT, объединять в
одну строку имена и фамилии, нумеровать строки на дисплее и еще много всего. SQL
предназначен исключительно для извлечения данных и не имеет никакого отношения к
представительскому уровню.

Исключения 
       Языки Informix 4GL, Progress, Oracle PL/SQL и некоторые другие действительно
предназначены для разработки приложений. Иногда такой язык появлялся даже раньше самой
СУБД. Программа на специализированном языке быстро выполняется, легко пишется и обладает
другими замечательными свойствами. Оборотной стороной медали, естественно, является
отсутствие переносимости.
Основы разработки программ
       Просто поразительно, как мало программистов SQL знакомо с основами разработки программ.
В группах новостей нередко встречаешь программиста, который просит написать ему
определение связности (cohesion) или сцепления (coupling). Очевидно, основам профессии в
свое время он не обучился, а теперь ему нужно сдавать какой-то тест. С некоторым смущением я
приступлю теперь к изложению материала, который вам должны были преподавать на первом
курсе. 
       Описанные ниже принципы справедливы для любого процедурного языка программирования,
но в SQL у них появляются свои особенности, поскольку это язык непроцедурный,
ориентированный на работу с наборами данных и отягощенный вопросами взаимосовместимости.

Связность
       Связность описывает, насколько хорошо модуль приспособлен для решения одной и только
одной задачи, насколько он логически связен. В идеале модуль должен обладать очень сильной
связностью, чтобы ему можно было присвоить имя в формате <глагол><объект>, где <объект> —
логическая единица модели данных. 
       Имеется несколько видов связности. Ниже я перечислил их от самого плохого к самому
лучшему: 
       1. Конъюнктивная; 
       2. Логическая; 
       3. Временная; 
       4. Процедурная; 
       5. Коммуникационная; 
       6. Информационная; 
       7. Функциональная. 
       Один модуль может обладать несколькими видами связности. 
       — Конъюнктивная связность. Наихудший вид связности. Он означает, что в модуле под
одной крышей собрано несколько не относящихся друг к другу задач. Так бывает при
использовании динамического SQL или при передаче в качестве параметра имени таблицы. 
       Допустим, имя модуля InsertNewCustomer() ясно говорит о том, что вы намереваетесь
работать с таблицей, содержащей данные о клиентах. А вот процедура InsertNewRecord,
предназначенная для добавления строк в любую таблицу схемы, слишком обща, чтобы обладать
хорошей связностью. Она будет использоваться и для волынок, и для свадеб, и для осьминогов, и
для любой новой таблицы, которая будет добавлена в схему позже. 
       Программисты должны избегать динамического SQL, поскольку он лишен связности и
попросту опасен. Пользователь, которому приходится задавать имя таблицы, волен также
включить в него добавочные операторы SQL. Например, вместо того чтобы передать просто имя
таблицы, он может передать в процедуру строку “Foobar; DELETE FROM Foobar; COMMIT” и
нанести серьезный ущерб БД. Использование динамического SQL также говорит о том, что
программист не считает себя достаточно компетентным для написания кода и передает эту
обязанность случайному пользователю. 
       Такой подход к программированию обычно является результатом попытки выполнить
операции с метаданными посредством информационных таблиц схемы. У реализаций SQL есть
собственные средства для работы с метаданными, и их не нужно переписывать. 
       — Логическая связность. Модули способны решать набор взаимосвязанных задач, но
вызывающий модуль выбирает из них только одну. Худший образчик такого подхода встретился
мне в 2004 г. в группе новостей по SQL Server. Некоему программисту приказали разместить все
процедуры в одном модуле. Затем в зависимости от параметра осуществлялся выбор, который из
пятидесяти с лишним модулей нужно выполнять и какие параметры использовать. 
       Так любят поступать объектно-ориентированные программисты. Каждая таблица кажется им
объектом особого рода, а процедура напоминает метод объекта. Это на самом деле не так. 
       — Временная связность. Модуль выполняет набор действий, которые должны выполняться в
одно время. Классический пример — объединение в модуле задач, связанных с запуском или
остановкой системы. Этот метод типичен для старых программистов на Коболе, которые привыкли
работать на системах пакетной обработки. 
       — Процедурная связность. Модуль осуществляет последовательность шагов процесса,
которые должны выполняться в определенном порядке.Этот стиль также типичен для
программистов, привыкших к работе всистемах пакетной обработки. Они же, как правило, создают
много временных таблиц для хранения промежуточных результатов. 
       — Коммуникационная связность. Все элементы оперируют одним и тем же входным
набором данных или создают один и тот же выходной набор данных. Части обмениваются
информацией с помощьюобщих данных в глобальной таблице. 
       — Информационная связность, или последовательная связность. Выходные данные
одного элемента поступают на вход другого элемента, но в отличие от случая логической
связности коды для всех действий совершенно независимы друг от друга. 
       — Функциональная связность. Модуль служит для решения одной и только одной задачи
или для достижения единственной цели. Лучшим примером такой связности являются
математические функции. Функциональная связность — наша главная цель. Именно благодаря ей
SQL еще называют функциональным языком. 
       Процедурная, коммуникационная, информационная и функциональная связности в SQL более
сложны, чем в языке 3GL, поскольку в SQL у нас есть транзакции. Логически транзакция
представляет собой один шаг, хотя и состоит из отдельных выражений SQL/

Сцепление
       Модули, которые должны выполняться в определенном порядке, обладают сильным
сцеплением. Если их можно выполнять независимо друг от друга, они слабо сцеплены.
Существует несколько видов сцепления, которые перечислены ниже — от худшего к лучшему: 
       1. По содержимому; 
       2. По общей области; 
       3. По управлению; 
       4. По образцу; 
       5. По данным. 
       Вот какими свойствами они обладают. 
       — Сцепление по содержимому означает, что один модуль непосредственно обращается к
содержимому другого модуля. Например, модульх передает управление на локальную метку в
модуле у  или модифицирует выражение в модуле у. Такие модули неразрывно связаны друг
сдругом. Сцепление по содержимому опасно, но в продуктах SQL 4GL оно поддерживается редко.
Главное правило — не передавать в качестве параметра SQL 4GL процедуру. 
       — Сцепление по общей области означает, что несколько модулей имеют доступ к одним и
тем же глобальным данным. В SGL-языках этому соответствуют глобальные переменные. В SQL
сцепление по общей области происходит при использовании для передачи информацииобщих
глобальных таблиц. Оно может быть опасным, если контрольвзаимосовместимости не налажен
должным образом. 
       — Сцепление по управлению означает, что один модуль управляет логической структурой
другого, например, если модуль х вызывает модуль y, и у  затем определяет, какое действие
должен выполнить модуль х. Сцепление по управлению происходит, скажем, когда в качестве
аргумента передается управляющее выражение-переключатель. В SQL так бывает с
подзапросами, которые обращаются к другим частям схемы впредикатах, управляющих
выполнением кода. 
       — Сцепление по образцу означает, что в вызываемый модуль передается таблица целиком,
хотя в действительности используются лишь некоторые ее столбцы. Основной пример в SQL —
использование оператора SELECT *. 
       — Сцепление по данным двух модулей означает, что все аргументы являются скалярными
элементами данных. Такие модули проще обслуживать, поэтому нужно всегда стремиться именно
к сцеплению по данным. При этом уменьшается вероятность того, что изменения в одном модуле
или таблице распространятся на другие модули или таблицы.

Используйте классическое структурное программирование


       Хотя слово “структурированный” спрятано в самом названии SQL, многие программисты ведут
себя так, словно структурной революции вовсе не было. Они все еще пишут программы в
допотопном стиле, который сами называют экстремальным или жестким, скрывая за этими
эпитетами элементарную корявость. 
       В классическом структурном программировании имеется три вида структур управления
потоком. 
       1. Конкатенация выражения, заключенные в квадратные скобки, выполняются
последовательно. В SQL/PSM это выражается конструкцией “BEGIN [ATOMIC] .. END”, а в
специализированных 4GL-языках часто просто ключевыми словами “BEGIN.. END”. На значении
ключевого слова ATOMIC мы тут останавливаться не будем. 
       2. Выбор булево выражение определяет, какой из двух блоков выражений будет выполнен. В
SQL/PSM для этого используются ключевые слова“IF .. THEN .. [ELSE ..] END IF;”, а в 4GL-языках
— “IF .. THEN .. [ELSE ..];”или “IF .. [ELSE ..];”. Синтаксис во всех случаях выглядит примерно
одинаково, так что вы не запутаетесь. 
       3. Итерация блок выражений выполняется повторно, пока значение булева выражения равно
TRUE. В SQL/PSM для этого используются ключевые слова “WHILE .. LOOP.. END WHILE;”. В
других продуктах вам попадется вариант “WHILE.. DO..”. Опять же синтаксис от продукта к
продукту меняется не сильно. 
       Важная характеристика всех этих управляющих структур — наличие одной точки входа и
одной точки выхода. В любом коде, который написан с их помощью, также будет одна точка входа
и одна точка выхода. Оператор GO TO в классическом структурном программировании не
используется. 
       В некоторых языках допускается с помощью оператора RETURN() досрочно завершать
выполнение функции, задавая возвращаемое ею значение. В других можно осуществлять выбор
из нескольких вариантов с помощью оператора CASE или подобного. Но, только придерживаясь с
максимальной строгостью правил классического структурного программирования, вы получите
безопасный, проверяемый и простой в обслуживании код.

Цикломатическая сложность
       Существует ли какое-нибудь простое правило, которое позволило бы отличить плохую
процедуру от хорошей? Да, и даже не одно. В 1970-х годах мы провели немало исследований по
определению метрик ПО и добились неплохих результатов. Метрика, основанная на
цикломатической сложности, изобретена Томом Мак-Кабе (1976). Для коротких процедур ее
несложно вычислить даже вручную. За метрику в данном случае берется число точек принятия
решения в модуле, увеличенное на единицу. Точка принятия решения — это точка ветвления
блок-схемы программы. В хорошо структурированной 4GL-nporpaммe на точки принятия решения
укажут ключевые слова — IF, WHILE, каждый вариант в структурах CASE или SWITCH, если они
поддерживаются языком. 
       Метрика простой процедуры не превышает 5. Если ее значение заключено в пределах от 6 до
10, стоит подумать об упрощении. Если метрика превышает 10, считайте упрощение
обязательным. Есть и другие метрики, но большинство из них вычисляются по более сложным
правилам.

Не создавайте себе проблем с переносимостью


Обоснование 
       Мы уже говорили о том, как создавать переносимые выражения SQL, но 4GL-nporpaммы также
должны быть переносимыми. Поскольку языки, о которых мы ведем разговор, принадлежат
различным разработчикам, в каждом из них в обязательном порядке имеются особенности,
несовместимые с другими 4GL-языками для SQL Старайтесь делать программы ясными и
простыми, чтобы с ними легко разобрался даже программист, освоивший один из ЗGL-языков.
Если вы ограничитесь тремя основными управляющими структурами, при переносе кода на другую
платформу вам достаточно будет произвести несколько чисто механических изменений. Впрочем,
есть и еще несколько правил.

Не создавайте временные таблицы


       В стандартном SQL правом создания временных таблиц обладает только человек с
полномочиями администратора. Некоторые языки от разработчиков игнорируют это правило,
позволяя создавать временные таблицы в ходе выполнения программы. Не используйте эту
возможность. Применяйте вместо временных таблиц подзапросы, производные таблицы или
представления. Применение временных таблиц обычно является признаком плохого дизайна.
Чаще всего их используют для хранения промежуточных данных, как мы в 1950-х годах
использовали временные магнитные ленты. 
       Есть два основных вида обработки ошибок. В семействе Sybase/SQL Server применяется
последовательная модель. После выполнения очередного выражения SQL задает значение
глобальной переменной с кодом ошибки. Программист должен написать программу для анализа
этой переменной и выполнения необходимых поправок. 
       В модели SQL/PSM используется модель прерываний. Существует глобальный параметр
SQLSTATE (старый SQLCODE более не действует), который способен передавать в кэш несколько
значений. Эти значения приводят к выполнению действий, заданных в выражениях WHENEVER,
связанных с программным блоком. Обработка ошибок — трудное дело, поэтому в
соответствующих местах модуля не скупитесь на комментарии. 
       Как можно больше действий выполняйте посредством выражений SQL, а не 4GL-nporpaммы. В
идеале хранимая процедура должна содержать единственное SQL-выражение, возможно, с
несколькими параметрами. Если выражений несколько, их последовательность лучше всего
оформлять посредством конструкции “BEGIN [ATOMIC].. END”. Вы теряете очки с каждой
конструкцией “IF..THEN..ELSE”, не говоря уже о циклах.

Не используйте курсоры
Обоснование 
       Курсор  (cursor) представляет собой способ преобразования набора данных в
последовательный файл для чтения в хост-языке. У курсоров в стандартном SQL масса
возможностей, а в различных продуктах — и того больше. 
       Курсоры обладают плохой переносимостью и обычно работают существенно медленнее
непроцедурных выражений SQL — медленнее на порядки величины. В целях безопасности ядро
SQL предполагает, что внутри курсора может случиться все, что угодно, и потому размещает эту
транзакцию на самом высоком уровне, блокируя других пользователей. 
       Зачем же они тогда вообще нужны? Главные причины — незнание основ SQL и старые
привычки. Модель курсоров в SQL следует семантике ленточных файлов, от которой многие не в
силах отказаться. Вот детальная аналогия:

ALLOCATE <имя курсора> = назначаем канал магнитофону


DECLARE <имя курсора> CURSOR FOR . = монтируем ленту и объявляем файл
OPEN <имя курсора> = открываем файл
FETCH <направление курсора> <имя курсора> INTO <локальные переменные>
= читаем в программе по одной записи за раз, затем перемещаем головку
чтения/записи
CLOSE <имя курсора> = закрываем файл
DEALLOCATE <имя курсора> = освобождаем магнитофон

       Если вы добавите к этому использование временных таблиц в качестве временных лент, то
получите полный аналог ленточной системы образца середины 1950-х годов без каких бы то ни
было следов реляционного мышления. В 2004 году пример такого подхода был опубликован в
одной из групп новостей по SQL Server. Новичок написал один курсор для просмотра первой
таблицы и размещения строк, удовлетворявших некоему критерию, во временную таблицу. Второй
курсор просматривал вторую таблицу, упорядоченную по ключу; внутри этого цикла третий курсор
просматривал временную таблицу, выбирал совпадающие строки и производил обновление. Это
был классический образчик приемов пятидесятилетней давности, реализованных средствами SQL.
Все 25 или около того выражений этого кода были в итоге заменены одним оператором UPDATE
со скалярным выражением подзапроса. Этот оператор работал почти в тысячу раз быстрее.

Исключения 
       Курсор может пригодиться лишь в экстремальных обстоятельствах, например для
исправления неудачно составленной таблицы, в которой так много дубликатов строк или неверных
данных, что ее действительно нужно просматривать строка за строкой, прежде чем применить к
ней оператор ALTER TABLE, чтобы исправить ошибки дизайна. Ниже перечислены возможные
варианты применения курсоров. 
       1. С помощью курсоров можно создавать средства для работы с метаданными, но лучше этого
не делать, ограничившись средствами, которые предлагает вам разработчик. Непосредственное
вмешательство в информационные таблицы схемы чревато неприятностями. 
       2. С помощью курсоров в SQL можно решать NP-полные задачи, когда вас устраивает первый
попавшийся ответ, попадающий в определенные пределы, например задачи о коммивояжере и
рюкзаке. Но при работе с БД подобные задачи попадаются нечасто, и решать их лучше
средствами процедурных языков и переборных алгоритмов. 
       3. В T-SQL и других продуктах, где до сих пор применяется физически непрерывное хранение
даных, медиана гораздо быстрее рассчитывается при помощи курсора, чем при помощи решений,
основанных на наборах данных. В продуктах с другими методами хранения или индексации
медиана вычисляется тривиально. 
       4. Теоретически можно написать код, который будет работать хуже курсора. Рассмотрим в
качестве примера слегка подчищенный код, опубликованный в группе новостей по SQL Server в
ноябре 2004 года. У автора имелась таблица с миллионом строк, и ему надо было “кое-что
сделать” с каждой из них. Других подробностей он не сообщил. В сообщении содержался
псевдокод на диалекте T-SQL, который в переводе на стандартный SQL выглядел примерно так:

CREATE PROCEDURE TapeFileRoutine()


BEGIN
-- используем временную таблицу в качестве временной ленты
DECLARE maxrecs INTEGER;
DECLARE current_row INTEGER;
DECLARE temp_a INTEGER;
DECLARE temp_b INTEGER;
INSERT INTO ScratchTape (record_nbr, temp_a, temp_b)
SELECT {{автонумератор}}, coll, col2
FROM MyBigTable;
SET maxrecs = (SELECT COUNT(*) FROM ScratchTape);
SET current_row = 0;
WHILE (current_row < maxrecs)
DO
-- извлекаем значения
SELECT colj, col_2 INTO temp_a, temp_b
FROM ScratchTape
WHERE rec_id = current_row; -- здесь манипулируем данными
SET current_row = current_row + 1;
END WHILE;
END;

       Да-да, вы наблюдаете алгоритм для работы с последовательным ленточным файлом,


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

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


процедурного кода
Обоснование 
       Оптимизатор неспособен использовать управляющие структуры из кода 4GL для подбора
плана выполнения. Таким образом, чем больше логики будет реализовано при помощи выражений
SQL, тем быстрее будет работать система. Больше всего времени у хранимой процедуры уходит
на доступ к данным. Вот как выглядела раскладка по времени для типичного процессора с
частотой 1 ГГц летом 2001 года.

Выполнение одной инструкции = 1 нс (1/1 000 000 000) сек


Извлечение слова из кэша первого уровня - 2 нс
Извлечение слова из основной памяти = 10 нс
Извлечение слова из следующего положения на диске = 200 нс
Извлечение слова из нового положения на диске (поиск) = 8 000 000 нс

       Избавившись от нескольких обращений к диску, я выиграю по времени выполнения гораздо


больше, чем если напишу более быстрый код. В обозримом будущем никаких существенных
улучшений в плане поиска данных на диске не предвидится.
Используйте конструкцию CASE вместо IF-THEN-ELSE
       В качестве примера рассмотрим задачу обновления цен в книжном магазине. Она
использовалась как упражнение в старом учебном курсе по Sybase SQL, чтобы показать, зачем
нужны курсоры. Нам нужно уменьшить на 10% цену дорогих книг (больше 25 долларов) и повысить
на 10% цену дешевых книг, чтобы возместить потери. Первый, к сожалению, неработающий порыв
большинства начинающих SQL-программистов выглядит так:

CREATE PROCEDURE IncreasePnces()


LANGUAGE SQL
DETERMINISTIC
BEGIN
UPDATE Books
SET price = price * 0.90
WHERE price >= 25.00;
UPDATE Books
SET price = price * 1.10
WHERE price < 25.00;
END;

       Допустим, у нас есть книга ценой 25 долларов. Первый оператор UPDATE снижает ее цену до
22,50, второй повышает до 24,75. Изменив порядок операторов, мы проблему не решим. В том
старом упражнении предлагалось использовать курсор и обновлять по одной книге за раз —
примерно так:

BEGIN
DECLARE BookCursor CURSOR FOR SELECT price FROM Books FOR UPDATE;
ALLOCATE BookCursor;
OPEN BookCursor;
FETCH Bookcursor;
WHILE FOUND
DO
IF price >= 25.00
THEN
UPDATE Books
SET price = price * 0.90
WHERE CURRENT OF BookCursor;
ELSE
UPDATE Books
SET price = price * 1.10
WHERE CURRENT OF BookCursor; END IF;
FETCH NEXT Bookcursor;
END WHILE;
CLOSE BookCursor;
DEALLOCATE BookCursor;
END;

       С помощью конструкции CASE ту же задачу можно решить так:

UPDATE Books
SET price = CASE WHEN price >= 25.00 THEN price * 0.90;
ELSE price * 1.10
END;

       Кода меньше, работает он быстрее. Правило таково: посмотрите, нет ли в блоках выражения
IF почти идентичных выражений SQL. Если есть, объедините их в одну конструкцию CASE.
Используйте нумерационные таблицы вместо циклов
       Нумерационная таблица — это таблица с единственным столбцом, который содержит
последовательный ряд чисел от 1 до n.  Вот один из способов сгенерировать такую таблицу:

CREATE TABLE Sequence


(seq INTEGER NOT NULL PRIMARY KEY);
CREATE PROCEDURE MakeSequence()
LANGUAGE SQL
DETERMINISTIC
BEGIN
INSERT INTO Sequence (seq) VALUES(1);
WHILE (SELECT MAX(seq) FROM Sequence) > 1000
DO INSERT INTO Sequence (seq)
SELECT MAX(seq)+1 FROM Sequence;
END WHILE;
END;
Но быстрее будет так:
CREATE TABLE Sequence (seq INTEGER NOT NULL PRIMARY KEY);
CREATE PROCEDURE MakeSequence()
LANGUAGE SQL
DETERMINISTIC
INSERT INTO Sequence (seq)
SELECT hundred * 100 + ten * 10 + unit + 1
FROM (VALUES (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) AS Units(umt)
CROSS JOIN
(VALUES (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) AS Tens(ten)
CROSS JOIN
(VALUES (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) AS Hundreds(hundred);

       Применение конструкции CROSS JOIN — еще один способ избавиться от циклов. Чтобы
определить, можно ли им воспользоваться, попробуйте в предложении, которое описывает
проблему, поставить перед существительными слово “множество”, скажем, “множество книг”
вместо “книга”. Грамматически, может, получится и не совсем корректно, но поможет вам
осмыслить задачу в терминах множеств и наборов данных. 
       Допустим, перед нами стоит задача преобразовать строку с целочисленными значениями,
которые разделены запятыми, в столбец таблицы. Это делается с помощью цикла WHILE,
вырезающего из строки значение и преобразующего его в целое число:

CREATE PROCEDURE Parser(IN input.string VARCHAR(255))


DETERMINISTIC
LANGUAGE SQL
BEGIN
DECLARE parm_nbr INTEGER; SET parm_nbr = 0;
DECLARE val INTEGER; SET val = CAST(NULL AS INTEGER);
SET input_string = TRIM (BOTH mput_string);
WHILE CHAR_LENGTH(input_string) > 0
DO BEGIN
SET parm_nbr = parm_nbr +1
IF POSITION(','IN input_string) > 0
THEN BEGIN
SET val = SUBSTRING (input_string
FROM 1
FOR POSITION(',' IN input_string)-1);
SET input_string = SUBSTRING (input_string
FROM CHAR_LENGTH(input_string)
- POSITION(',' IN input_string));
END
ELSE BEGIN
SET val = input_string;
SET input_string = ''; --пустая строка
END;
IF END;
INSERT INTO ParmList VALUES (parm_nbr, CAST(val AS INTEGER));
END WHILE;
END;

       С помощью таблицы Sequence ту же задачу можно решить так:

CREATE PROCEDURE Parser(IN input_string VARCHAR(255))


DETERMINISTIC
LANGUAGE SQL
BEGIN
INSERT INTO ParmList (parm_nbr, parm)
SELECT COUNT(S2.seq),
CAST (SUBSTRING (',' || input_string || ',' FROM MAX(S1.seq + 1) FOR
(S2.seq - MAX(S1.seq + 1))) AS INTEGER)
FROM Input_strings AS 11, Sequence AS S1, Sequence AS S2
WHERE SUBSTRING (',' || input_string || ',' FROM SLseq FOR 1) = ','
AND SUBSTRING (',' || input_string || ',' FROM S2.seq FOR 1) = ','
AND S1.seq < S2.seq
AND S2.seq <= CHAR_LENGTH (input_string) + 2
GROUP BY input_string, S2.seq;
END;

       Жизнь становится проще, если списки в строках начинаются и заканчиваются также запятыми.
Копии S1 и S2 таблицы Sequence используются для поиска пар запятых. Весь набор подстрок
между ними извлекается и преобразуется в числа в одном непроцедурном шаге. Тут весь фокус
состоит в том, чтобы удостовериться, что запятая слева — ближайшая ко второй запятой.
Преимущество непроцедурного подхода становится очевидным при модификации этого кода для
обработки целой таблицы, состоящей из строк с числами и запятыми:

CREATE TABLE InputStrings


(list_name CHAR(10) NOT NULL PRIMARY KEY,
input_string VARCHAR(255) NOT NULL);
INSERT INTO InputStrings VALUES ('first', '42,34,567,896');
INSERT INTO InputStrings VALUES ('second', '312,534,997,896');

       Процедуру для обработки одной строки можно заменить представлением:

CREATE VIEW Breakdown (list_name, parm_nbr, param)


AS
SELECT list_name, COUNT(S2.seq),
CAST (SUBSTRING (',' || I1.input_string || ',', MAX(S1.seq + 1),
(S2.seq - MAX(S1.seq +1)))
AS INTEGER)
FROM InputStrings AS I1, Sequence AS S1, Sequence AS S2
WHERE SUBSTRING (',' || I1.input_string || ',' FROM S1.seq FOR 1) = ','
AND SUBSTRING (',' || I1.input.string || ',' FROM S2.seq FOR 1) = ','
AND S1.seq < S2.seq
AND S2.seq <= CHAR_LENGTH (I1.input_string) + 2
GROUP BY I1.list_name, I1.input_string, S2.seq;

Используйте календарные таблицы для работы с датами


Обоснование 
       Всегда начинайте работу над новым проектом с создания нумерационной и календарной
таблиц. Таблица-календарь индексируется по дате, а в неключевом поле размещается
информация о том, какое значение данная дата имеет для вашей организации. Рабочий это день
или выходной? Какова его юлианская дата? К какому финансовому периоду он относится? Короче
говоря, все, что может вызывать необходимость в принятии каких-то решений. 
       Таблица с датами на 20 лет занимает всего около 7 тыс. строк — это мелочь. О том, как
работать с такой таблицей, читайте в группах новостей или в работе Селко (1999).

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

Выполняйте расчеты с помощью вспомогательных таблиц


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

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

Скалярные и структурированные параметры


       В стандартном SQL-92 нет ни массивов, ни списков. Есть только одна структура данных —
таблица. Это может быть основная таблица, производная таблица, представление — но это
всегда таблица. В процедурных языках применяются другие структуры — массивы, списки, записи.
Привыкшие к ним программисты отчаянно пытаются протащить их в SQL Результат — неуклюжий
и непроизводительный код. Но бывает и хуже, когда программист прибегает к динамическому SQL,
чтобы выстроить выражение или целую программу “на лету”. 
       В вызове хранимой процедуры ожидается скалярный параметр, а не структурированный или
динамический. Прибегнув к некоторым хитростям, вы сможете воспользоваться преимуществами
хранимых процедур, сохранив некоторую гибкость. Типичная проблема состоит в передаче списка
значений в предикат IN(), как в этом псевдокоде:

SELECT a, b, с FROM Foobar WHERE a IN (<список параметров>);

Типичное неудачное решение — использование динамического SQL, в котором в качестве списка


параметров передается список значений, разделенных запятыми. Можно, как мы делали в разделе
“Не создавайте себе проблем с переносимостью”, поместить список в таблицу и написать такое
выражение:

SELECT a, b, с FROM Foobar WHERE a IN (SELECT aa FROM ParmList);

       Но еще лучше загрузить данные списка в таблицу с помощью выражения INSERT INTO.
Детали от продукта к продукту будут варьироваться, но в стандартном SQL это делается так.

INSERT INTO Parmlist (parm) VALUES (1), (2), (3), (4);

       У списка VALUES() должно быть заранее известно количество строк, но его легко превратить в
динамический список, добавив в него значения NULL или другие пустые значения. На стороне БД
вы должны будете от них избавиться, а также при необходимости с помощью SELECT DISTINCT
удалить повторяющиеся значения. Полностью выражение на хост-языке будет выглядеть как-то
так:

INSERT INTO Parmlist (parm)


SELECT DISTINCT parm
FROM (VALUES (:h1), (:h2), (:h3), (:h4)) AS X(parm)
WHERE X.parm IS NOT NULL;

He пользуйтесь динамическим SQL


       Динамический SQL медленен и опасен. Он также говорит о том, что программисту не удалось
толком спроектировать приложение, и потому он передает этот груз пользователям. Назначение
динамического SQL — разработка инструментария для метаданных, а не приложений. Инструмент
для работы с метаданными обращается с объектами схемы как с объектами схемы, а не как с
элементами модели данных.

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

SELECT name, rank, serial_nbr


FROM CombatMarines
WHERE sex = :input_sex_code;

       Если параметр “:input_sex_code” равен 1 (мужской пол в кодировке ISO), тогда лучший способ
выполнения запроса — просмотр таблицы. Если параметр равен 2 (женский пол), лучше
воспользоваться индексом. Если у параметра любое другое значение, нужно просто вернуть
пустой набор. Конечно, все это зависит от реализации. Однако современные оптимизаторы
способны создавать несколько планов выполнения, опираясь на статистику, и выбирают нужный
на основании значения параметра. Короче говоря, возвращаемся к старому правилу —
оптимизатору нужно верить!

SQL-инъекция
       SQL-инъекция — одна из разновидностей хакерских атак, в которой злоумышленник
размещает свой SQL-код в вашей процедуре и выполняет ее. Как только вы разрешили
использовать динамический SQL в хранимой процедуре или генерировать выражения SQL в
клиентском коде, вы в опасности. Вот пример функции, которая генерирует простую строку
динамического SQL, предложенный esquel@sommarskog.se:

CREATE FUNCTION Search_Orders (custname VARCHAR(60))


RETURNS VARCHAR(3000)
RETURN ('SELECT * FROM Orders WHERE ' || COALESCE (custname, '1=1'));

       Допустим, что параметр custname поступает непосредственно из интерфейса без какой бы то
ни было фильтрации или проверки. Злоумышленник может передать в качестве параметра строку:

SET custname = ' 1=1; DROP TABLE Orders;';

       Результирующее SQL-выражение будет выглядеть так:

'SELECT * FROM Orders WHERE 1=1; DROP TABLE orders;'

       Хост-программа выполнит это выражение, и таблицы как ни бывало. 


       Конечно, вряд ли у простого пользователя есть право на удаление таблиц. Но с помощью
SQL-инъекции можно выполнять любые выражения. Сначала нападающий определяет, какой ввод
приводит не к ошибке времени выполнения, а к синтаксической ошибке. Так он узнает, что на
стороне БД имеется динамический SQL. Затем пишется код, который при необходимости
завершается точкой с запятой или символом комментария, чтобы отбросить ту часть запроса, что
будет стоять после него. Немного проб и ошибок, и хакер найдет, как причинить вред. 
       Первая линия обороны — не давать пользователям больше прав, чем им нужно для работы.
Простому пользователю достаточно разрешить использование оператора SELECT. Но лучше
всего просто не использовать динамический SQL.
ГЛАВА 9.
Эмпирические правила кодирования
       Приведенные здесь приемы и правила не являются математически точными. Больше того,
некоторые из них покажутся вам довольно странными. Но, как заметил однажды Ларри
Константайн (Larry Constantine), задача метода, указать вам, что делать дальше, когда вы не
знаете, что делать дальше. Вам остается только надеяться, что метод даст вам, может быть, не
самое удачное, но, по крайней мере, работоспособное решение. 
       Рассмотрим несколько простых задач программирования, по мере продвижения вперед
применяя к ним различные правила. Рассмотрим так называемую проблему выбора партнера по
танцу. Дан список людей и их пол. Ваша задача — составить из них пары.

CREATE TABLE People


(name VARCHAR (35) NOT NULL PRIMARY KEY,
gender INTEGER DEFAULT 1 NOT NULL
CHECK (gender IN (1,2))); -- коды полов

       Далее мы рассмотрим классическую задачу заказов: имея модель данных по заказам клиентов
на продукты со склада, ответить на различные запросы. Эта схема не полна, но для
демонстрационных целей вполне сгодится.

CREATE TABLE Orders


(order_nbr INTEGER NOT NULL,
...);
CREATE TABLE OrdersDetails
(order_nbr INTEGER NOT NULL REFERENCES Orders (order_nbr)
ON UPDATE CASCADE ON DELETE CASCADE,
sku CHAR(1O) NOT NULL REFERENCES Inventory (sku)
ON UPDATE CASCADE ON DELETE CASCADE,
description CHAR(20) NOT NULL,
qty INTEGER NOT NULL CHECK(qty > 0),
unit_price DECIMAL(12,4) NOT NULL,
...);

Четко формулируйте спецификации


       Хотя это и кажется очевидным, я все-таки подчеркну: ключевое слово — четкость. Претензии
к ней нужно предъявлять в самом начале работы. Рассмотрим некоторые реальные примеры для
схемы, моделирующей типичную БД заказов. 
       1. “Я хочу видеть самую дорогую позицию в каждом заказе”. Что подразумевать под
наиболее дорогой позицией в заказе — максимальную цену единицы товара или максимальную
полную цену по графе (количество единиц, умноженное на цену единицы)? 
       2. “Я хочу видеть, сколько игрушечных гномов заказал каждый из клиентов”. Как
представить в наборе тех клиентов, которые не заказали ни одного гнома? Нулем или значением
NULL? Если они возвратили купленные игрушки, что мне отразить — первоначальный заказили
конечные результаты? Отражать ли мне отсутствие заказа значением NULL или хранить данную
информацию в виде нуля? 
       3. “Суммы скольких заказов превышают 100 долларов?”. Учитывать ли заказы, сумма
которых в точности равна 100 долларам? 
       В примере о “партнере по танцу” нам необходимо определиться со следующими вопросами: 
       1. Каким образом нам формировать пары? 
       2. Как поступить, если на танцевальной площадке кавалеров больше, чемдам (или наоборот)? 
       3. Может ли кто-либо иметь более одного партнера? Если да, как их другза другом закрепить? 
       Определить спецификации в действительности труднее, чем написать код. При наличии ясной
и прозрачной спецификации код, можно сказать, уже написан.
Представляйте все существительные наборами
       Большим подспорьем в программировании на SQL является мышление в терминах множеств,
а не выполняемых шагов, в которых элементы данных обрабатываются по одному за раз.
Формулировки типа “для каждого X...”  отравляют мысленный образ стоящей задачи. Решение
необходимо искать в совокупности характеристик, а не в индивидуальных характеристиках.
Например, как найти все заказы, которые точно совпадают по количеству заказанных единиц? 
       В данном случае, один из подходов — сравнение количественных значений для каждого из
заказов и отклонение тех заказов, в которых эти значения не совпадают. Данный подход приведет
к операциям с курсором или внутреннему объединению. Далее представлена версия кода с
использованием механизма внутреннего объединения. Версию кода с использованием курсора я
опущу:

SELECT D1.order_nbr
FROM OrderDetails AS D1
WHERE NOT EXISTS (SELECT *
FROM OrderDetails AS D2
WHERE D1.order_nbr = D2.order_nbr
AND DLqty <> D2.qty);

       А вот как можно найти нужные заказы по совокупности указанных свойств:

SELECT order_nbr
FROM OrderDetails
GROUP BY order_nbr
HAVING MIN(qty) = MAX(qty);

Удаляйте глаголы из предложений, описывающих абстрактную


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

Вам по-прежнему доступны “заглушки”


       На одном из рисунков известного карикатуриста Сидни Харриса (Sydney Harris) изображены
два математика перед доской, на которой посреди длинной цепочки сложных математических
доказательств стоят слова. “А затем происходит чудо”. И математик-слушатель говорит
математику-автору: “Вот этот шаг надо бы описать поподробнее”. 
       Аналогичный прием мы используем в процедурных языках программирования — вставляя
“заглушку”, до поры до времени не зная, что делать в этом месте программы. Например, вы
разрабатываете программу по расчету зарплаты для компании, которая имеет сложную систему
выплаты премий. Она вам пока непонятна или не задана, и вы пишете процедуру-“заглушку”,
которая возвращает константу или просто выдает сообщение о своем выполнении. Это позволит
вам продолжить работу с теми фрагментами процедуры, которые вам ясны. 
       Указанное труднее осуществить в декларативных языках программирования. Модули
процедурного языка не обременены жесткими связями, тогда как предложения и запросы
оператора SELECT представляют собой монолитную единицу программного кода. В процедурных
языках программирования вы без труда напишете код, предназначенный для тестирования
модуля. Для конструкций SQL сделать это гораздо проблематичнее. 
       Вернемся к задаче выбора партнера по танцу. Я мог бы подойти к ее решению, предположив,
что необходимо разделить кавалеров и дам на две отдельные группы. Но допустим, что я пока не
знаю, как написать этот код. Поэтому я с помощью текстового редактора вставляю заглушку —
псевдокод:

SELECT Ml.name AS male, FLname AS female


FROM (<чудо-парни>) AS M1(name, <заглушка для парней>)
FULL OUTER JOIN
(<чудо-девушки>) AS F1(name, <заглушка для девушек>)
ON M1 <заглушка для парней> ?? F1 <заглушка для девушек>;

       Заключенный в угловые кавычки псевдокод я позже заменю на совокупность табличных


столбцов, подзапросы или что-нибудь другое. В данный момент они являются просто метками.
Также я использую метку “??” для указания на взаимосвязь между кавалерами и дамами. Затем я
могу перейти на следующий уровень вложения и заменить метку<чудо-парни>  подзапросом
следующего содержания:

(SELECT P1.name, <заглушка для парней>


FROM People AS P1
WHERE P1.gender = 1) AS M1 (name, <заглушка для парней>)

       Такой же шаблон для подзапроса применим и к метке <чудо-девушки>. Я знаю, что теперь мне
необходимо определиться с кодом для метки <заглушка для парней>.Первым делом я обращаю
внимание на столбцы в таблице People. Единственным интересующим меня атрибутом в данной
таблице является пол. Я устанавливаю значение для этого атрибута таю для кавалеров оно равно
1, для дам — 2. 
       Я пробую следующее:

SELECT MLname AS male, F1.name AS female


FROM (SELECT Pl.name, P1.gender
FROM People AS P1
WHERE P1.gender = 1) AS M1 (name, gender)
FULL OUTER JOIN
(SELECT Pl.name, gender
FROM People AS P1
WHERE P1.gender = 2) AS F1 (name, gender)
ON M1.gender = 1 AND F1.gender = 2;

       Легко увидеть, что данный фрагмент представляет собой всего-навсего неловко
замаскированный оператор объединения CROSS JOIN. Добавить, что ли, уточнение по именам?

SELECT M1.name AS male, F1.name AS female


FROM (SELECT P1.name, P1.gender
FROM People AS P1
WHERE P1.gender = 1) AS M1 (name, gender)
FULL OUTER JOIN
(SELECT P1.name, gender
FROM People AS P1
WHERE P1.gender = 2) AS F1 (name, gender)
ON M1.gender = 1 AND FLgender = 2 AND M1.name <= F1.name;

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

SELECT M1.name AS male, F1.name AS female


FROM (SELECT P1.name, COUNT (P2.name)
FROM People AS P1, People AS P2
WHERE P2.name <= P1.name AND P1.gender = 1 AND P2.gender = 1
GROUP BY PLname) AS M1 (name, rank)
FULL OUTER JOIN
(SELECT PLname, COUNT (P2.name)
FROM People AS P1, People AS P2
WHERE P2.name <= P1.name AND P1.gender = 2 AND P2.gender = 2
GROUP BY PLname) AS F1 (name, rank)
ON M1.rank = F1.rank;

He беспокойтесь об отображении данных


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

SELECT P1.name, P1.gender, COUNT(P2.name) AS rank


FROM People AS P1, People AS P2
WHERE P1.gender = P2.gender
AND P2.name <= P1.name
GROUP BY PLname, P1.gender;

       В результате будет получен набор, отсортированный по столбцу name,  в каждой строке


которого будет находиться только один человек с дополнительным атрибутом rank.Два человека с
совпадающими значениями этого атрибута образуют пару. Хотя вид полученной информации в
данном случае отличается от рассмотренного выше, ее содержание не изменилось. Просто оно
получено с помощью более простого запроса. Заметим, что в обоих случаях оставшиеся без пар
танцоры расположены по алфавиту в конце списка. 
       В простой клиент-серверной системе для передачи клиентскому компьютеру уже
отсортированного результирующего набора вы можете добавить в объявление курсора
предложение ORDER BY. Но в многоуровневых распределенных архитектурах сортировка и
другие функции отображения данных должны выполняться по-разному в зависимости от
местоположения узлов. Например, информация, предназначенная для клиентского компьютера,
расположенного в США, должна отображаться в английской системе величин и сортироваться по
штатам. Та же информация, но уже предназначенная для компьютера, находящегося в Европе,
должна отображаться в международной системе единиц и сортироваться по странам.

Первые шаги в SQL требуют специального подхода


       Генри Ледгард (1975) тонко подметил: 
       “Дерево, которое начало сохнуть, вылечить невозможно. Это справедливо и в отношении
высыхающих компьютерных программ. Не стоит тратить время на восстановление структуры,
деформированной “заплатками” и удаленными фрагментами, и на исправление программы,
которая построена на изначально плохом алгоритме. В лучшем случае, вы получите громоздкую,
неэффективную, непонятную и неудобную в обслуживании программу. Что произойдет в худшем
случае, я даже подумать не решаюсь”. 
       Особенно это справедливо для SQL. Но для языка определения данных DDL (Data Definition
Language) и языка манипулирования данными DML (Data Manipulation Language) процесс
сопровождения кода различен, что является следствием декларативной природы SQL. Язык DDL
статичен, тогда как инструкции DML имеют динамический характер. Это означает, что результат
применения одного и того же оператора CREATE всегда один и тот же. Но план выполнения и
результат операторов SELECT, INSERT, UPDATE или DELETE будут меняться со временем.

Плохой DDL должен быть отброшен


       Плохая инструкция DDL приводит к искаженному функционированию всего кода. Вернемся к
нашей простой схеме “Партнер по танцу”. Что, если при определении данной схемы для
атрибута gender был бы использован нестандартный битовый тип данных из конкретного диалекта
SQL? Такой код нельзя было бы быть перенести на другие диалекты SQL На клиентских узлах
необходимо было бы выполнять низкоуровневые манипуляции с битами. Невозможно было бы
установить связь с другими источниками данных, использующими стандарты ISO. 
       Проектирование схемы данных — сложная задача. Вряд ли вы полностью справитесь с ней за
один присест. Реконструкция базы данных займет определенное время и потребует реорганизации
существующих данных, но выбор другого пути еще хуже. 
       Когда я жил в Солт-Лейк-Сити, на одном из собраний группы программистов я встретился с
человеком, который попал именно в такую ситуацию. Изначально плохо спроектированная база
данных, использовавшаяся для передачи лабораторных отчетов в клиники, начала разваливаться
на части из-за возросшего объема работы. На обновление и добавление данных стали уходить
чуть ли не все 24 часа в сутки, и для программистов приближающийся крах был очевиден. В свою
очередь, руководство, за исключением окриков, другого решения проблемы не видело. 
       Несколько месяцев спустя я столкнулся с тем, как ошибочное объявление столбца привело к
неправильному формированию партии медикаментов, отгружаемых для одного из бедственных
районов Африки. Разработчик, пытаясь немного сократить объем базы данных, нарушил правило
первой нормальной формы. Все значения по количеству упаковок разного объема он в
определенной последовательности разместил в одном столбце, в дальнейшем обращаясь к этим
значениям с помощью подстрочных функций. Позже, стараясь сократить огромные расходы на
транспортировку в район военных действий, поставщики решили формировать упаковки меньшего
объема. И теперь первое “подполе” в указанном столбце содержало количество упаковок,
фактически состоящих не из пяти, а из одной единицы. Но жестко завязанное на первоначальную
схему клиентское приложение ничего об этом не знало. Хотелось бы вам выбирать, какие четверо
из пяти детей умрут вследствие небрежного программирования? Теперь понятно, что
подразумевалось в последнем предложении из цитаты Ледгарда?

Сохраняйте DML-наработки
       Плохая инструкция DML может выполняться в несколько раз медленнее, чем хорошая. Но в
SQL трудно сказать, что такое хорошо, а что такое плохо. В процедурных языках программист
всегда имеет в своем распоряжении понятное программное окружение, в котором одна и та же
программа всегда выполняется одним и тем же образом. SQL принимает решение о способе
выполнения запроса на основании статистики о самой БД и доступных ресурсах. С течением
времени эти данные могут и должны меняться. Поэтому наилучшее сегодняшнее решение может
оказаться недостаточно хорошим завтра. 
       В 1988 году Паскаль (Pascal) опубликовал классический обзор о системах БД для
персональных компьютеров того времени. Паскаль сконструировал семь разных, но логически
эквивалентных запросов. И сама БД, и все запросы были довольно простыми. При оценке времени
выполнения запросов использовались одинаковые аппаратные платформы. 
       Оптимизатор СУБД Ingres оказался достаточно интеллектуальным, чтобы во всех запросах
найти общие черты. Система использовала один и тот же план выполнения и показала одинаково
хорошую производительность для всех запросов. У других систем временные показатели
оказались неровными: самые плохие отличались от лучших на порядок и более. Так, в случае с
Oracle наихудший показатель отличался от наилучшего более чем в 600 раз. 
       Я рекомендую вам сохранять наработки, сделанные в DML, чтобы вы смогли повторно
использовать их в случае изменения оптимизатора и/или окружения, в котором он функционирует.
Поместите старый вариант запроса в виде комментария к новой версии, чтобы другой
программист при необходимости мог найти его.

Не мыслите категориями блок-схем


       Когда мы пытаемся решить какую-то задачу, как это ни странно, некоторым из нас нравится
рисовать что-то на листке бумаги. Даже простенькая диаграмма помогает разобраться в сложном
вопросе, особенно когда вы изучаете что-нибудь новое. Мы — создания с визуальным
восприятием бытия. 
       Для облегчения программирования на процедурных языках программисты пользовались блок-
схемами, определенными в стандарте ANSI X3.5. Все эти схемы базируются на блочно-стрелочной
идеологии, очерчивая поток данных и/или управления в процедурной системе. Если вы
используете такие устаревшие инструменты, готовьтесь, что у вас будут получаться устаревшие
системы. Вы будете писать код SQL, но по сути он будет процедурным.

Рисуйте диаграммы для множеств


       Рисуя диаграммы для множеств, вы будете создавать приложения, ориентированные на
работу с множествами. Например, изобразим предложения GROUP BY в виде маленьких
изолированных овалов внутри другого овала большего размера. Таким образом, мы представили
их в виде подмножеств другого множества. Прибегайте к хронологической линии, работая с
временными запросами, но помните: при работе с множествами нет потока данных. Схема,
ориентированная на множества, существует вне времени и связана только ограничивающими
условиями. 
       Вероятно, наиболее ярким примером разницы между “блочно-стрелочным” подходом и
диаграммами для множеств являются модели представления древовидных структур с помощью
списков смежных вершин (adjacency list) и вложенных множеств (nested sets). Примеры этих
моделей вы найдете с помощью поисковой системы Google или купив экземпляр моей книги Trees
and Hierarchies in SQL for Smarties  (Селко, 2004). Диаграммы для каждой из этих моделей
показаны на рис. 9.1.

Рис. 9-1а. Деревья, представленные посредством списка смежных вершин

Рис. 9-1б. Деревья, представленные посредством вложенных множеств

Изучайте свой диалект


       Хотя вы и должны при написании кода пытаться придерживаться стандартного SQL, важно
знать также, какие диалектные конструкции поддерживаются в вашей системе. Например, в старых
продуктах, основывающихся на последовательных файловых структурах, важную роль играет
конструирование индексов и ключевых полей. С другой стороны, ядро Nucleus от Sand Technology
рассматривает всю БД как набор сжатых битовых векторов и не имеет специальных механизмов
индексации, потому что все индексируется автоматически.

Представьте, что предложение WHERE обладает


“многопроцессорной архитектурой”
       Это самый странный заголовок в главе, так что будьте внимательны. В идеальной
многопроцессорной системе каждый процессор занимается выполнением исключительно своей
задачи. Допустим, что у вас имеется некоторая рабочая таблица, структурно определенная
предложением FROM. Представьте себе, что каждой строке этой таблицы назначен один
процессор, который будет проверять в этой строке поисковое условие отбора, записанное в
предложении WHERE. Здесь мы имеем дело с одним из вариантов правила Пурнеля (Pournelle):
“Одна задача — один процессор”. 
       Если каждую строку в вашей таблице можно независимо от других проанализировать на
предмет соответствия простому условию поиска, значит, ваша схема данных, скорее всего, имеет
хорошую реляционную структуру. Если же при выполнении запроса необходимо ссылаться на
другие строки той же таблицы, или обращаться к внешним источникам данных, или запрос в
соответствие с этими условиями вообще не может быть выполнен, у вас, вероятно, проблемы с
нормализацией. 
       Мы уже рассматривали модель вложенных множеств и модель списка смежных вершин для
деревьев. Рассматривая одну строку таблицы обособленно от остальной ее части, сможете ли вы
ответить на один из основных вопросов об узлах моделируемого дерева? Вы, конечно, спросите,
что такое “вопрос об узле”. Далее представлен их короткий список, который применяется для
древовидных структур в теории графов. 
       1. Является ли узел оконечным? 
       2. Является ли узел корневым? 
       3. Насколько большим является поддерево, подчиненное данному узлу? 
       4. Выше или ниже располагается один рассматриваемый узел относительнодругого? Или они
находятся на одном и том же уровне иерархии? 
       Четвертый вопрос чрезвычайно важен, поскольку является основой для операций сравнения в
иерархических структурах. Как видите, модель вложенных множеств может дать ответ на все
перечисленные вопросы. Напротив, модель списка смежных вершин не может ответить ни на один
из них.

Используйте группы новостей и Интернет


       Интернет — величайший в мире источник информации, так что учитесь пользоваться им. Вы
найдете в нем целый ряд групп новостей, посвященных специфичным деталям вашей системы
или многочисленным вопросам общего характера. Задавая вопрос в группе новостей, включите в
письмо все инструкции DDL. Это необходимо, чтобы люди не маялись вопросом, какие ключевые
поля, ограничения, типы данных присутствуют в схеме, какова декларативная целостность
ссылочных данных и т. д. Хорошо также включить в сообщение пример данных, которые помогут
разъяснить, какие результаты вы хотите получить. 
       В большинстве SQL-продуктов имеются инструментальные средства, генерирующие
инструкции DDL нажатием одной клавиши. К сожалению, результат работы этих инструментов
оставляет желать лучшего, так как представляет собой код, в основном, неудобочитаемый для
человека. Вам нужно будет сократить реальные таблицы так, чтобы оставить только информацию,
необходимую для пояснения вашей проблемы. Неразумно включать в сообщение предложение
CREATE TABLE, описывающее таблицу из 100 столбцов, когда для вас представляют интерес
только два из них. Затем уберите из описания ограничения и другие элементы, руководствуясь
правилами, приведенными в этой книге. Фактически, вы просите людей бесплатно сделать за вас
работу. Так что, будьте любезны обеспечить их достаточной и удобной для восприятия
информацией. 
       Если вы студент и просите сделать за вас домашнюю” работу, помните, что представление
чужой работы, как своей собственной, может стать веским основанием для провала на экзамене и
даже для исключения из университета. Когда вы пишете такое письмо, отметьте, что это
домашняя работа, укажите наименование учебного заведения, факультет и имя преподавателя.
Это гарантирует, что ваши действия впоследствии не приведут к плачевным результатам.
ГЛАВА 10. 
Думаем на SQL
Нас подводят не вещи, которых мы не понимаем, а вещи, 
которые кажутся понятыми, но таковыми не являются.
Артемус Уорд, американский юморист (1834-1867)

       Самым значительным препятствием при изучении SQL является необходимость мыслить
категориями логических структур и множеств, а не последовательностей и процессов. Давайте
потратим немного времени, чтобы проанализировать, откуда все-таки берутся ошибки. Теорией вы
уже овладели, но сможете ли применить ее на практике?
       Я пытался обобщить ошибки, которые совершают начинающие программисты, и пришел к
выводу, что, возможно, самым сложным для них является необходимость мыслить категориями
множеств. Рассмотрим классическую головоломку (рис. 10.1), в которой предлагается посчитать
количество сложенных стопкой брусков размерности 1x1x2. 
       Обычно люди совершают ошибку, пытаясь последовательно сосчитать все бруски. Для этого
требуется хорошее пространственное воображение, которым обладают далеко не все из нас. 
       Правильный же подход к решению этой задачи таков. Давайте представим, что куб заполнен
полностью. Так как размер куба 4 х 5 х 5, то он может вместить 50 брусков. В нашем случае для
полного заполнения куба, как нетрудно сосчитать, необходимо еще три бруска, так что всего в кубе
содержится 47 брусков. При этом расположение брусков внутри куба не имеет никакого значения. 
       В данной главе рассматриваются примеры, содержащие такие ошибки. Все они основаны на
реальных сообщениях, опубликованных в группах новостей. Чтобы не путаться в диалектах и
особенностях различных систем, все они переведены в стандартный SQL. В некоторых случаях, я
переименовал элементы данных, в других оставил их имена неизменными. Очевидно, что
окончательный замысел авторов этих примеров до конца мне не известен, и мотивацию их
конкретных действий я пытаюсь определить наугад. В любом случае, я думаю, что смогу отстоять
свои доводы.

Рис. 10.1. Классическая головоломка: Сколько блоков размером 2x1x1 находится внутри куба?
Невидимая часть полностью заполнена.

Плохой стиль программирования на SQL и процедурных языках


       Как пример отсутствия познаний в применении реляционных подходов к программированию,
рассмотрим сообщение, опубликованное в группе новостей comp.databases.ms-sqlserver в январе
2005 г. Тема сообщения — “Как искать в таблице отсутствующие записи” — уже говорит о том, что
автор письма мыслит в терминах файловой системы, а не в терминах системы управления
реляционными базами данных. 
       В описании таблицы содержался обычный для новичка столбец id, и не было ни ключа, ни
каких-либо ограничений. В таблице содержались строки для каждого дня одного года, которые
идентифицировались порядковым номером недели внутри года (от 1 до 53) и порядковым
номером дня недели (от 1 до 7). Ниже представлена структура рассматриваемой таблицы. От
оригинальной она отличается только именами атрибутов:

CREATE TABLE WeeklyReport


(id INTEGER AUTONUMBER NOT NULL, - плохой SQL!
week_nbr INTEGER NOT NULL,
day_nbr INTEGER NOT NULL);

       После удаления бесполезного столбца id и добавления ограничений мы получаем такую


таблицу:

CREATE TABLE WeeklyReport


(week_nbr INTEGER NOT NULL CHECK(week_nbr BETWEEN 1 AND 53),
day_nbr INTEGER NOT NULL CHECK(day_nbr BETWEEN 1 AND 7),
PRIMARY KEY(week_nbr, day_nbr));

       Хотя на словах автор указал на множество ограничений, в определении таблиц он их


применить не удосужился. У новичков таблица ассоциируется с файлом, а не с множеством. Для
них единственным критерием, в соответствии с которым данные необходимо поместить в файл,
является сам процесс записи в файл. А файл не может проверить достоверность содержащихся в
нем данных. Введение в таблицу автонумерации симулирует нумерацию записей, свойственную
последовательной файловой системе.
       Сама задача заключалась в нахождении первого отсутствующего дня внутри каждой недели с
последующим добавлением соответствующей строки. Если к записываемым данных и
предъявлялись какие-либо требования, то в описании они представлены не были. Вот
собственное решение автора задачи (оно переведено на стандартный SQL с диалекта T-SQL с
небольшими изменениями в именах переменных и атрибутов):

CREATE FUNCTION InsertNewWeekDay (IN my_week_nbr_nbr INTEGER)


RETURNS INTEGER LANGUAGE SQL
BEGIN
DECLARE my_day_nbr INTEGER;
DECLARE result_day_nbr INTEGER;
SET my_day_nbr = 1;
xx:
WHILE my_day_nbr < 8 DO
IF NOT EXISTS (SELECT * FROM WeeklyReport
WHERE day_nbr = my_day_nbr
AND week_nbr = my_week_nbr_nbr) THEN
BEGIN
SET result_day_nbr = my_day_nbr;
LEAVE xx;
END;
ELSE
BEGIN
SET my_day_nbr = my_day_nbr + 1;
ITERATE xx;
END;
END IF;
END WHILE;
RETURN result_day_nbr;
END;

       Это типичная имитация оператора цикла FOR, который использовался во всех языках
программирования третьего поколения. Однако если вы посмотрите на этот код в течение двух
секунд, то увидите, что он плох и как программа на процедурном языке! Отсутствие
программистских навыков не исправляется даже средствами SQL Больше того, последствия
неудачного решения, вызванного стремлением подражать процедурным языкам
программирования, в SQL только усугубляются. Оптимизаторы и компиляторы SQL не рассчитаны
на оптимизацию кода, написанного в стиле, свойственном процедурным языкам. Даже если автор
и решил придерживаться процедурного стиля программирования, он должен был следовать
классическому построению кода. А для этого нужно было удалить лишние локальные переменные
и избавиться от замаскированных операторов безусловного перехода GOTO:

CREATE FUNCTION InsertNewWeekDay (IN my_week_nbr INTEGER)


RETURNS INTEGER LANGUAGE SQL
BEGIN
DECLARE answerjibr INTEGER;
SET answer_nbr = 1;
WHILE answer_nbr < 8 DO
IF NOT EXISTS (SELECT * FROM WeeklyReport
WHERE day_nbr = answer_nbr
AND week_nbr = my_week_nbr)
THEN RETURN answer_nbr;
ELSE SET answer_nbr = answer_nbr + 1;
END IF;
END WHILE;
RETURN CAST (NULL AS INTEGER); - приводит к ошибке
END;

       Здесь необходимо обратить внимание на следующее упущение автора. Он не объяснил, как
следует обходиться с той неделей, в которой уже представлены все семь дней. В исходной
структуре таблицы благодаря отсутствию ограничений в качестве номера дня и недели
допускается любое целое значение. В исправленном описании любое значение номера дня
недели, выходящее за рамки интервала от 1 до 7, нарушает ограничение для первичного ключа.
Это, конечно, не лучшее решение, но, по крайней мере, оно позволяет удовлетворить очевидным
требованиям с минимальными усилиями. 
       Сможем ли мы решить эту задачу без циклической структуры и получить чисто непроцедурное
SQL-решение? Да, и здесь возможно несколько вариантов. Конечной целью поиска
отсутствующего дня недели является вставка в таблицу строки. Так почему бы не сделать все это
в рамках одной процедуры, вместо того чтобы осуществлять поиск в одной функции, а
последующее добавление строки — на последующем процедурном шаге? Давайте будем мыслить
на уровне процесса в целом, а не в рамках последовательных шагов. 
       Первое решение смотрится неуклюже и трудно обобщается. Но оно работает достаточно
быстро, если оптимизатор обеспечивает вынос всех подзапросов из операторов WHEN и их
обработку как единого целого. В данном решении локальные переменные уже не используются:

CREATE PROCEDURE InsertNewWeekDay (IN my_week_nbr INTEGER)


LANGUAGE SQL
INSERT INTO WeeklyReport (week_nbr, day_nbr)
VALUES (my_week_nbr,
(CASE WHEN 1 NOT IN (SELECT day_nbr FROM WeeklyReport WHERE week_nbr =
my_week_nbr) THEN 1
WHEN 2 NOT IN (SELECT day_nbr FROM WeeklyReport WHERE week_nbr =
my_week_nbr) THEN 2
WHEN 3 NOT IN (SELECT day_nbr FROM WeeklyReport WHERE week_nbr =
my_week_nbr) THEN 3
WHEN 4 NOT IN (SELECT day_nbr FROM WeeklyReport WHERE week_nbr =
my_week_nbr) THEN 4
WHEN 5 NOT IN (SELECT day_nbr FROM WeeklyReport WHERE week_nbr =
my_week_nbr) THEN 5
WHEN 6 NOT IN (SELECT day_nbr FROM WeeklyReport WHERE week_nbr =
my_week_nbr) THEN 6
WHEN 7 NOT IN (SELECT day_nbr FROM WeeklyReport WHERE week_nbr =
my_week_nbr) THEN 7
ELSE NULL END; -- NULL нарушает ограничение для первичного ключа

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

CREATE PROCEDURE InsertNewWeekDay (IN my_week_nbr INTEGER)


LANGUAGE SQL
INSERT INTO WeeklyReport (week_nbr, day_nbr)
(SELECT my_week_nbr, MIN(n)
FROM (VALUES (1),(2),(3),(4),(5),(6),(7)) AS Weekdays(n)
WHERE NOT EXISTS (SELECT * FROM WeeklyReport AS W
WHERE W.weekjibr = my_week_nbr
AND Weekdays.n = W.day_nbr));

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

CREATE PROCEDURE InsertNewWeekDay (IN my_week_nbr INTEGER)


LANGUAGE SQL
INSERT INTO WeeklyReport (week_nbr, day_nbr)
SELECT my_week_nbr, MIN(n)
FROM (VALUES (1),(2),(3),(4),(5),(6),(7)
EXCEPT
SELECT day_nbr
FROM WeeklyReport AS W
WHERE W.week_nbr = my_week_nbr) AS N(n);

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

CREATE PROCEDURE InsertNewWeekDay (IN my_week_nbr INTEGER)


LANGUAGE SQL
INSERT INTO WeeklyReport (week_nbr, day_nbr)
SELECT my_week_nbr, MIN(n)
FROM (SELECT seq
FROM Sequence
WHERE seq <= 7 -- изменить на любое значение
EXCEPT SELECT day_nbr
FROM WeeklyReport AS W
WHERE W.week_nbr = my_week_nbr) AS N(n);

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


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

Отождествление столбцов с полями данных


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

CREATE PROCEDURE SurveySummary()


LANGUAGE SQL
BEGIN
DECLARE sche_yes INTEGER;
DECLARE sche_no INTEGER;
DECLARE sche_mb INTEGER;
DECLARE sche_other INTEGER;
DECLARE how_yes INTEGER;
DECLARE how_no INTEGER;
DECLARE how_mb INTEGER;
DECLARE how_other INTEGER;
DECLARE paaexpl_yes INTEGER;
DECLARE paaexpl_no INTEGER;
DECLARE paaexpljnb INTEGER;
DECLARE paaexpl_other INTEGER;
SET sche.yes = (SELECT COUNT(*)
FROM SurveyForms
WHERE sche =1);
SET sche.no = (SELECT COUNT(*)
FROM SurveyForms
WHERE sche = 2);
SET schejnb = (SELECT COUNT(*)
FROM SurveyForms
WHERE sche = 3);
SET sche_other = (SELECT COUNT(*)
FROM SurveyForms
WHERE NOT (sche IN (1, 2, 3)));
SET how.yes = (SELECT COUNT(*)
FROM SurveyForms
WHERE howwarr =1);
SET how_no = (SELECT COUNT(*)
FROM SurveyForms
WHERE howwarr = 2);
SET how_mb = (SELECT COUNT (*)
FROM SurveyForms
WHERE howwarr =3);
SET how_other = (SELECT COUNT(*)
FROM SurveyForms
WHERE NOT (howwarr IN (1,2,3)));
SET paaexpl_yes = (SELECT COUNT(*)
FROM SurveyForms
WHERE paaexpl =1);
SET paaexpl_no = (SELECT COUNT(*)
FROM SurveyForms
WHERE paaexpl = 2);
SET paaexpl_mb = (SELECT COUNT (*)
FROM SurveyForms
WHERE paaexpl = 3);
SET paaexpl_other = (SELECT COUNT(*)
FROM SurveyForms
WHERE NOT (paaexpl IN (1, 2, 3)));
DELETE FROM SurveyWorkmgtable;
INSERT INTO SurveyWorkingtable
VALUES (sche_yes, sche_no, sche_mb, sche_other, How_yes, how_no, how_mb,
how_other, Paaexpl_yes, paaexpl_no, paaexpl_mb, paaexpl_other);
END;

       Почему автор письма создает десяток локальных переменных и затем для их инициализации
использует скалярные подзапросы? Да потому, что он по-прежнему мыслит терминами языков
программирования 3GL В Коболе или в среде программирования любого другого процедурного
языка решение аналогичной задачи происходит так. Файл, содержащий исходные данные,
считывается по одной записи за раз. Из каждой такой записи последовательно слева направо
считываются находящиеся в ней поля данных. Последовательность конструкций IF-THEN
анализирует эти поля, наращивая при выполнении определенных условий соответствующий
счетчик. Когда считан весь файл, результаты его обработки записываются в рабочий файл для
дальнейшего составления сводки. 
       Очевидно, что автор письма считает каждый столбец исходной таблицы полем данных и
пытается получить значение для него отдельно от всего остального. Он где-то подсмотрел
возможность использования подзапросов и решил задачу с их помощью. Подзапросы обычно не
очень хорошо оптимизируются, так что в данному случае время выполнения кода в
действительности будет даже больше, чем для кода, непосредственно написанного на SQL/PSM и
имитирующего классическую программу на Коболе. 
       Вот как выглядит подход к решению задачи, ориентированный на множества:
CREATE PROCEDURE SurveySummary()
LANGUAGE SQL
BEGIN
DELETE FROM SurveyWorkmgtable;
INSERT INTO SurveyWorkmgtable (sche_yes, sche_no, ... paaexpl_other)
SELECT SUM (CASE WHEN sche = 1
THEN 1 ELSE 0 END) AS sche_yes,
SUM (CASE WHEN sche = 2
THEN 1 ELSE 0 END) AS sche_no,
SUM (CASE WHEN paaexpl NOT EN (1, 2, 3)
THEN 1 ELSE 0 END) AS paaexpl_other
FROM SurveyForms;
END;

       Суть в том, что нужно сразу оценивать, что должно быть сделано с каждой строкой таблицы, и
не начинать рассмотрение задачи со столбцов. Ответ прост: нам нужен итоговый ответ на
определенные вопросы. Слово “итог” подводит нас к функциям SUM() или COUNT(). Как
пользоваться выражением CASE, мы помним. 
       В заключение зададимся вопросом, почему для получения итоговых данных вместо хранимой
процедуры не используется представление?

Мышление категориями процессов, а не описаний


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

CREATE TABLE Users


(user_id CHAR(8) NOT NULL PRIMARY KEY,
password VARCHAR(IO) NOT NULL,
max_reserves INTEGER NOT NULL
CHECK (max_reserves >= 0));
CREATE TABLE Reservations
(user_id CHAR(8) NOT NULL REFERENCES Users(user_id)
ON UPDATE CASCADE
ON DELETE CASCADE,
item_id INTEGER NOT NULL REFERENCES Items(item_id));

       Исходное описание задачи выглядело так:

“Каждому клиенту разрешается заказать не более n предметов. Всякий раз, когда клиент
делает заказ, проверяется информация из поля (sic!) maxjresewes таблицы Users. Затем в
таблицу Reservations добавляется запись (sic!), а поле maxjresewes соответствующим
образом обновляется. Мне хотелось бы узнать, существует ли лучший способ решения
этой задачи. Пока у пользователя есть возможность заказать больше п товаров, если он
зарегистрируется с двух компьютеров”.

       Сначала автору предложили хранимую процедуру, которая на SQL/PSM выглядела бы так.

CREATE PROCEDURE InsertReservations (IN max_reserves INTEGER,


IN my_user_id CHAR(8), IN my_item_id INTEGER)
LANGUAGE SQL
BEGIN
DECLARE my_count INTEGER;
SET my_count = (SELECT COUNT(*)
FROM Reservations
WHERE user_id = my_user_id);
IF my_count >= nax_reserves
THEN RETURN ('Объем заказа достиг максимального значения!');
ELSE INSERT INTO Reservations (user_id, item_id)
VALUES(my_user_id, my_iten_id);
END IF;
END;

       Делать максимально допустимый объем заказа входным параметром процедуры неразумно,
так как ему может быть присвоено любое значение. Локальная переменнаяmy_count  является
излишней. SQL — ортогональный язык, и везде, где используется скалярная переменная, можно
применить скалярный подзапрос. 
       Строки — это не записи, а столбцы — не поля данных. SQL является декларативным, а не
процедурным языком. Для него последовательность процедурных шагов “Извлечь > проверить >
вставить > обновить” является бессмысленной. Вы должны внушить себе, что сама схема заказов
должна быть такой, чтобы клиент не смог превысить свое ограничение. Думайте логически, а не
категориями процесса:

CREATE PROCEDURE MakeReservation


(IN my_user_id CHAR(8),
IN my_item_id INTEGER)
LANGUAGE SQL
BEGIN
INSERT INTO Reservations (user_id, item_id)
SELECT my_user_id, my_item_id
FROM Users AS U
WHERE U.user_id = my_user_id
AND U.max_reserves
>= (SELECT COUNT(*)
FROM Reservations AS R
WHERE R.user._id = my_user_id); -- здесь нужно добавить
обработчик ошибки
END;

       Чтобы не записывать число заказов в локальную переменную, вы можете использовать


прямой подзапрос. 
       В реальной ситуации для отчетности вам может потребоваться представление такого вида:

CREATE VIEW Loans (user_id, max_reserves, current_loans) AS


SELECT U.user_id, U.max_reserves, COUNT(*)
FROM Reservations AS R, Users AS U
WHERE R.user.id = U.user_id
GROUP BY U.user_id, U.max_reserves;

Попытка заставить схему выглядеть как бланк


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

CREATE TABLE Timecards


(user_id CHAR(8) NOT NULL,
punch.time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
event.flag CHAR(3) DEFAULT 'IN' NOT NULL CHECK(event_flag IN
('IN','OUT')),
PRIMARY KEY (user_id, punch_time));

       Чтобы такая схема давала ответы даже на основные запросы, необходимо согласовать между
собой времена прихода и ухода. Кодд (1979) писал, что строка в СУБД должна соответствовать
факту. Важно понимать, что речь идет о полном факте, а не о его половине. В данном случае
неполнота заключается в том, что одного появления Джона на работе в 09:00 недостаточно, чтобы
рассчитать размер его заработка в этот день. Для этого необходимо знать, что Джон был на
работе с 09:00 до 17:00. При правильном описании таблицы в каждой ее строке должен быть
заключен полный факт. В нашем случае это должно выглядеть так:

CREATE TABLE Timecards


(user_id CHAR(8) NOT NULL,
in_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
out_time TIMESTAMP,-- NULL означает текущее время
CHECK(in_time < out_time),
PRIMARY KEY (user_id, in_time));

       Многие начинающие SQL-программисты боятся использовать пустые значения (NULL), но


здесь они весьма кстати. Мы не знаем будущего и потому не можем заранее присвоить
содержательное значение атрибуту out_time. 
       Другой пример — бланк заказа, напрямую перенесенный в DDL В общих чертах, он обычно
выглядит так:

CREATE TABLE Orders


(order.nbr INTEGER NOT NULL PRIMARY KEY,
order.total DECIMAL(12,2) NOT NULL,
CREATE TABLE OrdersDetails
(order_nbr INTEGER NOT NULL,
line_nbr INTEGER NOT NULL,
PRIMARY KEY (order_nbr, line_nbr),
item_id INTEGER NOT NULL REFERENCES Inventory(item_id),
qty_ordered INTEGER NOT NULL CHECK (qty_ordered > 0)

       Итоговая сумма заказа order_Total  может быть рассчитана на основании данных


таблицы OrdersDetails, и потому присутствие этого атрибута в таблице Orders является
избыточным. Однако ячейка для общей суммы заказа присутствует в бумажном варианте бланка и
добросовестно переносится новичками в определение таблицы. 
       Никто не покупает и не продает номер строки. Покупатели заказывают товар. Но в бумажном
бланке заказа строки нумеруются. По аналогии номера строк line_nbrприсутствуют и в
таблице OrderDetails.  Это неудобно, потому что, если указать один и тот же товар в нескольких
строках, в дальнейшем необходимо будет их консолидировать. Иначе заказчик рискует, например,
лишиться оптовой скидки, да и дисковое пространство из-за избыточных данных тратится
впустую. 
       При этом каждая строка отражает неполный факт. Например, в одной строке указано, что я
заказал две пары туфель, а в другой строке этого же заказа указано, что я заказал три пары
туфель. Полный же факт заключается в том, что я заказал пять пар туфель. 
       В 2004г. я указал на это одной программистке, разрабатывавшей схожую схему. Она
настойчиво уверяла, что им необходима нумерация строк, чтобы была возможность в точности
восстановить бланк заказа в его первоначальном виде. Но далее в той же теме она жаловалась,
что ее сослуживцы тратили по несколько часов в день для проверки количества заказанных
товаров, поскольку их поставщики не использовали подходящую модель для представления
обобщенных и отсортированных данных.
ПРИЛОЖЕНИЕ 1
Военные стандарты
       DoD 8320.1 -М-1, процедуры стандартизации элементов данных. 
       DoD Directive 8320.1, администрирование данных Министерства обороны США. 
       http://www.dtic.mil/whs/directives/corres/html/83201.htm. 
       http://www.abelia.com/498pdf/498ARAPX.PDF.

Стандарты метаданных
       По этому адресу Вы найдете краткий обзор правил комитета по стандартам метаданных
NCITS L8: 
       — http://pueblo.lbl.gov/~olken/X3L8/drafts/draft.docs.html. 
       — http://lists.oasis-open.org/archives/ubl-ndrsc/200111/msg00005.html. Адрес PDF-файла: 
       — http://www.oasis-open.org/committees/download.php/6233/c002349_ISO_IEC_l 1179. 
       Предварительный вариант: 
— http://www.iso.org/iso/en/ittf/PubliclyAvailableStandards/c002349_ISO_IEC_l1179-l_1999(E).zip.

Стандарты ANSI и ISO


       — Основы СИ (Метрической системы). 
       — ISO 31 “Quantities and Units (14 parts)”. 
       — ISO 1000 “SI Units and Recommendations for the Use of Their Multiple andof Certain Other Units
for the Application of the SI”. 
       — ISO 2955 “Information Processing — Representation of SI and Other Unitsfor Use in Systems with
Limited Character Sets”. 
       Справочник по стандартам ISO 31 и ISO 1000 можно приобрести по адресу: 
       — http://www.iso.org/iso/en/prods-services/prods-services/otherpubs/Quality.PublicationList?
CLASSIFICATION=HANDBOOKS#090201. 
       — ISO 639-1:2002 “Codes for the Representation of Names of Languages —Part 1: Alpha-2 Code”. 
       — ISO 639-2:1998 “Codes for the Representation of Names of Languages — Part 2: Alpha-3 Code”. 
Коды языков доступны по адресу: 
       — http://www.loc.gov/standards/iso639-2/iso639jac.html. 
       — ISO 3166 “Codes for the Representation of Names of Countries”. 
       Этим стандартом за каждой страной закреплен уникальный двухбуквенный код. Для решения
каких-либо специфических задач предлагается трехбуквенный код. В качестве альтернативы
предложен также трехразрядный числовой код — для приложений, которым нужна независимость
от алфавита или экономное расходование памяти. 
       — http://www.iso.org/iso/en/prods-services/popstds/countrynamecodes.html. 
       — ISO 4217:2001 “Codes for the Representation of Currencies and Funds”. 
       — http://www.iso.org/iso/en/prods-services/popstds/currencycodeslist.html. 
       — IBAN: International Standard Bank Number. 
       — http://www.ecbs.org/iban/iban.htm. 
       — ISO 8601 “Data Elements and Interchange Formats — InformationInterchange — Representation
of Dates and Times”. 
       — http://www.iso.org/iso/en/prods-services/popstds/datesandtime.html.

Коды правительства США


       — NAICS: North American Industry Classification System Система кодов, пришедшая на смену
старой системе (SIC). 
       — http://www.census.gov/epcd/www/naics.html.NAPCS: North American Product Classification
System. 
       — http://www.census.gov/eos/www/napcs/napcs.html 
       — TIGER: Topologically Integrated Geographic Encoding and Referencingsystem. Представление
географических данных для переписей. Доступнов электронной форме. 
       DOT: словарь профессий. Система кодов министерства труда США. Некоторые коды
приведены здесь: 
       — http://www.wave.net/upg/immigration/dot_index.html.

Розничная торговля
       — EAN: European Article Number, now combined with the UPC codes 
       — ISO/IEC 15418:1999 “EAN/UCC Application Identifiers and Fact Data Identifiers and
Maintenance”. 
       — ISO/IEC 15420:2000 “Automatic Identification and Data Capture Techniques — Bar Code
Symbology Specification — EAN/UPC”. 
       — Статья Стива Лора (Steve Lohr) “Bar Code Detente: U.S. Finally Adds One MoreDigit” в
выпуске New York Times от 12 июля 2004 Г: http://www.nytimes.com/2004/07/1
2/business/12barcode.html?ex=1090648405&ei=l&en=202cb9baba72e846. 
       — VIN: код идентификации транспортного средства 
       — ISO 3779:1983 Vehicle Identification Number (VIN). 
       — ISO 4030:1983 Vehicle Identification Number (VIN) — расположение и крепление. 
       — ISO/TR 8357:1996 Инструкции по реализации назначения всемирного идентификатора
производителя (world manufacturer identifier, WMI) для системы кодов VIN и для всемирного
идентификатора производителя комплектующих (world parts manufacturer identifier, WPMI) — только
на английском языке. 
       Хорошая статья о грядущих изменениях в VIN: 
       — http://www.cars.com/news/stories/070104_storya_dn.jhtml?page=newsstory&aff=national. 
       Объяснение стандартов ISO для автомобильных покрышек: 
       — http://www.hostelshoppe.com/tech_tires.php. 
       ISBN: International Standard Book Number 
       — http://www.isbn.org/standards/home/index.asp. 
       На сайте имеется программа преобразования для нового 13-разрядного ISBN, основанная на
переходе от 10-разрядных кодов UPC к 13-разрядным кодам EAN.

Оформление кода и правила назначения имен


       Другие мнения вы найдете по следующим адресам: 
       — http: //www. sqlservercentral.com/columnists/sjones/codingstandardspart2formatting.asp. 
       — http://www.sqlservercentral.com/columnists/sjones/codingstandardspart1formatting.asp. 
       — Gulutzan, P. “SQL Naming Conventions”, 
       http://dbazine.com/gulutzan5.shtml. 
       — Bryzek, M. “Constraint Naming Standards”, 
       http://ccm.redhat.com/doc/core-platform/5.0/engineering-standards/eng-standards-constraintnaming-
sect-l.html. 
       — Celko, J. “Ten Things I Hate about You”, 
       http://www.intelligententerprise.com/O01205/celkol_l.jhtml?_requestid=304726. 
       — ISO/IEC. IS 11179-5 Information Technology Specification and Standardization of Data Elements:
PART 5, Naming and Identification Principles forData Elements. 
       — http://metadata-standards.org/Document-library/Draft-standards/l 1179-Part5-
Naming&Identification/. 
       — Jones, S. “Standards Part 1 — Abbreviated Programming”, 
       http://www.databasejournal.com/features/mssql/article.php/1471461. 
       — Karbowski, J.J. “Naming Standards beyond Programming”, http://www.devx.com/tips/Tip/12710. 
       — Koch, G., and K. Loney Oracle8i: The Complete Reference (3rd ed.). Emeryville,CA: Osborne
McGraw Hill, 2000. 
       — Kondreddi, N., Vyas. “Database Object Naming Conventions”, 
       http://vyaskn.tripod.com/object_naming.htm. 
       — Mullins, C. “What's in a Name?”, http://www.tdan.com/i004fe02.htm. 
       — Mullins, C. http://www.craigsmullins com/dbt_0999.htm. 
       — Sheppard, S. “Oracle Naming Conventions”, http://www.ss64.com/orasyntax/naming.html.
ПРИЛОЖЕНИЕ 2
Библиография

       Вудворт (1938) — Woodworth, R.S. Experimental Psychology.  New York: Holt,1938. 


       Фишер (1975) — Fisher, D. “Reading and Visual Search”, Memory and Cognition,  3, 188-196, 1975. 
       Мейсон (1978) — Mason, M. “From Print to Sound in Mature Readers as a Function of Reader
Ability and Two Forms of Orthographic Regularity”, Memory and Cognition,  6, 568-581, 1978. 
       Мейер и Гучера (1975) — Meyer, D. E., and K. D. Gutschera. “Orthographic versus Phonemic
Processing of Printed Words”, Psychonomic Society Presentation, 1975. 
       Полацек, Велл и Шиндаер (1975) — Pollatsek, A, A D. Well, and R M. Schindler. “Effects of
Segmentation and Expectancy on Matching Time for Words and Nonwords”, Journal of Experimental
Psychology: Human Perception and Performance, 1, 328-338, 1975. 
       Сэнгер (1975) — Saenger, P. Space Between Words: The Origins of Silent Reading. Palo Alto, CA:
Stanford University Press, 1975. 
       Кодд (1979) — Codd, E. F. “Extending the Database Relational Model to Capture More
Meaning”, ACM Transactions on Database Systems,  44, 397-434, December 1979. 
       Вайнберг (1978) — Weinberg, G. The Psychology of Computer Programming: Silver Anniversary
Edition. New York: Dorset House, 1998. 
       Миллер (1956) — Miller, G., A. “The Magical Number Seven Plus or Minus Two: Some Limits on Our
Capacity for Processing Information”, The Psyco-logical Review, 1956. 
       Мак-Кабе (1976) — McCabe, Tom. “A Complexity Measure”, IEEE Transactions on Software,  1976. 
       Селко (1999) — Celko, J. Data & Databases.  San Francisco: Morgan-Kaufmann, 1999. 
       Ледгард (1975) — Ledgard, H. Programming Proverbs. Rochelle Park, NJ, Hayden Books, 1975. 
       Селко (2004) — Celko, J. Trees & Hierarchies in SQL  San Francisco: Morgan-Kaufmann, 2004. 
       Селко (2005) — Celko, J. SQLforSmarties  (3rd ed.). San Francisco: Morgan-Kaufmann, 2005.

Об авторе

Джо Селко (Joe Celko) — известный преподаватель и консультант, один из самых читаемых в
мире авторов книг по SQL. Большую известность принесли ему десятилетняя работа в комитете
ANSI по стандартам SQL, колонка в журнале Intelligent Enterprise  (получившая несколько премий
Reader's Choice) и множество реальных историй, которыми он иллюстрирует свои взгляды на SQL-
программирование. Он также является автором бестселлеров SQL for Smarties: Advanced SQL
Programming (второе издание), SQL Puzzles and Answers  и Trees and Hierarchies in SQL for
Smarties.