Статьи

Математика и SQL, часть 2: функции и первая нормальная форма

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

I: Определение первой нормальной формы

В предыдущем посте мы говорили об определении функции и отношения. Это ключ к пониманию первой нормальной формы.

Первая нормальная форма требует двух вещей, как определено Коддом:

  1. Каждое отношение является правильно сформированным отношением (то есть правильно сформированным набором фактических корреляций). 
  2. Ни один элемент отношения (т.е. коррелированный факт) сам по себе не является множеством. Поэтому наборы внутри наборов не допускаются.

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

Четкий математический взгляд на первую нормальную форму:

  1. Каждое отношение представляет собой набор кортежей.
  2. Ни один кортеж не содержит набора.

II: множества и ординальность

Далее отметим, что одной из определяющих особенностей набора является то, что он не имеет порядка. Более формально мы бы сказали, что множества не имеют порядков. {1, 2, 3} = {2, 1, 3}. По этой причине нельзя разрешать повторяющиеся строки, так как это означает, что отношение не является правильно сформированным набором.  

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

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

III: массивы SQL, ординальность и 1NF

Массив SQL на самом деле является математической матрицей, несущей в себе все основные свойства этого. В частности, существуют два основных ограничения:

  1. Массивы SQL являются порядковыми, поэтому [1, 2, 3]! = [2, 1, 3]
  2. Многомерные массивы должны быть правильно сформированными матрицами. Т.е. [[2, 1, 3], [3, 2, 1]] в порядке, а [1, 2, [3, 2]] — нет.

Массивы могут быть использованы различными способами. Некоторые из них нарушают первую нормальную форму. Другие нет. Например, следующее не нарушает 1NF (хотя я намеренно избегаю встроенных типов PostgreSQL для работы в сети):

 CREATE TABLE network_addrs (
    host_id int primary key,
    hostname text,
    ip_as_octets int[]
);
INSERT INTO network_addrs(1, 'myhost', ARRAY[192,168,1,124]);

Поскольку [192,168,1,124] и [124,168,1,192] не эквивалентны, первая нормальная форма не нарушается.

С другой стороны, рассмотрим:
 CREATE TABLE web_page (
      file_name text primary key,
      content text,
      tags text[]
);
INSERT INTO web_page ('home.html', '<html></html>', ARRAY['foo', 'bar']);

Поскольку [foo, bar] эквивалентно [bar, foo], мы имеем нарушение 1NF.
Теперь, в PostgreSQL, для этого могут быть веские причины, но в любой СУБД также есть значительные затраты.

Основной компромисс в PostgreSQL в последней версии заключается в том, что вы получаете возможность оптимизировать запросы на выборку для тегов, используя индексы GIN. Однако удаление тега из системы становится очень болезненным. Разница между тем, который нарушает 1NF с использованием тегов, и тем, который правильно использует массивы для IP-адресов, состоит в том, что я собираюсь управлять только тем IP-адресом, который существует сразу. Я никогда не буду изменять или удалять октеты по отдельности. Однако для веб-страниц может потребоваться доступ к массиву с помощью SQL для обновления или удаления элементов.  

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

IV: 1NF, ключи-кандидаты и функциональные зависимости


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

Интересно, что PostgreSQL поддерживает запросы, отмечающие столбцы как функции отношений.
Хотя обычно мы можем запустить что-то вроде:
 select proname, nspname 
  from pg_proc p 
  join pg_namespace n ON p.pronamespace = n.oid;

Мы также можем написать это как:
 select proname, nspname 
  from pg_proc p 
  join pg_namespace n ON pronamespace(p) = oid(n);

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

V: Заметки о кортежах в кортежах


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

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

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

VI: когда сломать 1NF


Есть много раз, когда 1NF должен быть сломан.  
In the real world we don’t often get to verify that our data conforms to rigid mathematical definitions before we load it.  For this reason, the non-duplication rule often must be ignored for data import workflows.  In these cases, one loads the data with duplicates, and filters them out with subsequent operations.  This is acceptable.
Additionally there are cases where the workloads are such that, with PostgreSQL, GIN indexes can give you major performance boosts when you need it.  This comes at a cost, in that deleting a valid element from the system can no longer perform well, but sometimes this cost is worth it for relatively unimportant and freeform data.

VII:  Perl modules and Related Concepts

In writing this I came across a number of Perl modules which are closely related to this post.
The first is
Set::Relation, which implements basic relational logic in Perl.  This is worth looking at if you are trying to see how to implement set logic without an RDBMS.  A very nice feature is that by default, relations are immutable, which means you have an ability to use them in a fully Functional Programming environment.
The second is
Math::Matrix, which gives you a library for matrix mathematics.  For integer and float arrays, this allows you to incorporate this into the database back-end using PL/PerlU procedures, and would be particularly useful when combined with SQL language arrays (either in the back-end or middle-ware).

VIII:  Final Thoughts

First Normal Form is often treated as a given in relational theory.  However since most books aimed at database professionals don’t cover the mathematical basis for the model, it is generally poorly understood.  Understanding First Normal Form however is the beginning of understanding relational logic and relational database systems.  It therefore deserves much more exploration than the brief space it is usually given.

Understanding 1NF is particularly important when one decides to use a non-1NF database design.  These have significant tradeoffs which are not evident to beginners.  However, this is only the starting point.  In future discussions we will cover related aspects which give a more complete understanding of the basics of the system.

Next Week:  Immutability, MVCC, and Functional Programming