Что не так с обнуляемыми столбцами в составных первичных ключах?

149

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

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

Почему уникальные ограничения могут иметь значения NULL, а первичные ключи - нет? Есть ли фундаментальная логическая причина для этого или это скорее техническое ограничение?

Роман Старков
источник

Ответы:

216

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

По определению, NULL не может быть частью успешного сравнения. Даже сравнение с собой ( NULL = NULL) не удастся. Это означает, что ключ, содержащий NULL, не будет работать.

Кроме того, NULL допускается во внешнем ключе для обозначения необязательного отношения. (*) Разрешение этого в PK также сломало бы это.


(*) Предупреждение: наличие обнуляемых внешних ключей не является чистым дизайном реляционной базы данных.

Если есть две сущности, Aи Bгде они Aмогут быть необязательно связаны B, чистое решение состоит в том, чтобы создать таблицу разрешения (скажем AB). Эта таблица будет связать Aс B: Если есть отношения , то она будет содержать запись, если не то не будет.

Томалак
источник
5
Я изменил принятый ответ на этот. Судя по голосам, этот ответ наиболее понятен большему количеству людей. Я все еще чувствую, что ответ Тони Эндрюса лучше объясняет намерение, стоящее за этим проектом; проверить это тоже!
Роман Старков
2
Q: Когда вы хотите NULL FK вместо отсутствия строки? A: Только в версии схемы, денормализованной для оптимизации. В нетривиальных схемах такие ненормальные проблемы могут вызывать проблемы всякий раз, когда требуются новые функции. Ох, толпа веб-дизайна не волнует. Я бы, по крайней мере, добавил предостережение об этом вместо того, чтобы звучать как хорошая идея дизайна.
zxq9
3
«Наличие обнуляемых внешних ключей - не чистый дизайн реляционной базы данных». - дизайн базы данных без нуля (шестая нормальная форма) неизменно добавляет сложности, полученная экономия места часто перевешивается дополнительной работой программиста, необходимой для реализации этих достижений.
Дай
1
Что делать, если это таблица разрешения ABC? с дополнительным C
Барт Каликсто
1
Я старался не писать «потому что стандарт запрещает это», так как это действительно ничего не объясняет.
Томалак
62

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

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

Тони Эндрюс
источник
10
В Sql Server уникальное ограничение, имеющее столбец, допускающий значение NULL, допускает значение «null» в этом столбце только один раз (с учетом идентичных значений для других столбцов ограничения). Таким образом, такое уникальное ограничение по сути ведет себя как ПК с обнуляемым столбцом.
Джерард
Я подтверждаю то же самое для Oracle (11.2)
Александр Малахов
2
В Oracle (я не знаю о SQL Server) таблица может содержать много строк, где все столбцы в уникальном ограничении равны нулю. Однако, если некоторые столбцы в ограничении уникальности не равны нулю, а некоторые равны нулю, то применяется уникальность.
Тони Эндрюс
Как это относится к композитным УНИКАЛЬНЫМ?
Димс
1
@Dims Как и почти все остальное в базах данных SQL, «это зависит от реализации». В большинстве БД «первичный ключ» фактически является УНИКАЛЬНЫМ ограничением снизу. Идея «первичного ключа» на самом деле не более особенная или мощная, чем концепция UNIQUE. Реальное отличие состоит в том, что если у вас есть два независимых аспекта таблицы, которые могут быть гарантированно УНИКАЛЬНЫМИ, то у вас нет нормализованной базы данных по определению (вы храните два типа данных в одной таблице).
zxq9
46

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

Рассмотрим случай версий модуля / пакета, хранящихся в виде последовательности полей:

CREATE TABLE module
  (name        varchar(20) PRIMARY KEY,
   description text DEFAULT '' NOT NULL);

CREATE TABLE version
  (module      varchar(20) REFERENCES module,
   major       integer NOT NULL,
   minor       integer DEFAULT 0 NOT NULL,
   patch       integer DEFAULT 0 NOT NULL,
   release     integer DEFAULT 1 NOT NULL,
   ext         varchar(20),
   notes       text DEFAULT '' NOT NULL,
   PRIMARY KEY (module, major, minor, patch, release, ext));

Первые 5 элементов первичного ключа являются регулярно определяемыми частями версии выпуска, но некоторые пакеты имеют настраиваемое расширение, которое обычно не является целым числом (например, «rc-foo», «vanilla» или «beta» или кем-то еще для кому четыре поля недостаточно, может придумать). Если пакет не имеет расширения, то в приведенной выше модели он равен NULL, и если оставить все как есть, никакого вреда не будет.

Но что такое NULL? Предполагается, что это нехватка информации, неизвестность. Тем не менее, возможно, это имеет больше смысла:

CREATE TABLE version
  (module      varchar(20) REFERENCES module,
   major       integer NOT NULL,
   minor       integer DEFAULT 0 NOT NULL,
   patch       integer DEFAULT 0 NOT NULL,
   release     integer DEFAULT 1 NOT NULL,
   ext         varchar(20) DEFAULT '' NOT NULL,
   notes       text DEFAULT '' NOT NULL,
   PRIMARY KEY (module, major, minor, patch, release, ext));

В этой версии «ext» часть кортежа НЕ NULL, но по умолчанию это пустая строка, которая семантически (и практически) отличается от NULL. NULL - это неизвестное, тогда как пустая строка - это преднамеренная запись «чего-то, чего нет». Другими словами, «пустой» и «ноль» - разные вещи. Разница между «у меня нет значения здесь» и «я не знаю, каково значение здесь».

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

Кстати, оба способа действительны в Postgres (поскольку мы обсуждаем «корпоративные» RDMBS), но результаты сравнения могут немного отличаться, когда вы добавляете NULL в смесь - потому что NULL == «не знаю», так что все результаты сравнения, включающего NULL, заканчиваются тем, что NULL, поскольку вы не можете знать то, что неизвестно. ОПАСНОСТЬ! Тщательно подумайте об этом: это означает, что результаты сравнения NULL распространяются через серию сравнений. Это может быть источником незаметных ошибок при сортировке, сравнении и т. Д.

Postgres предполагает, что вы взрослый человек, и можете принять это решение самостоятельно. Oracle и DB2 предполагают, что вы не поняли, что делаете что-то глупое, и выдавали ошибку. Это , как правило , правильно, но не всегда - вы , возможно , на самом деле не знаю , и имеют значение NULL в некоторых случаях и , следовательно , оставляя строку с неизвестным элементом , против которого содержательные сравнения невозможны это правильное поведение.

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

[* ПРИМЕЧАНИЕ. Можно создать пользовательский тип, представляющий собой объединение целых чисел и «нижнего» типа, который будет семантически означать «пустой», а не «неизвестный». К сожалению, это вносит некоторую сложность в операции сравнения, и, как правило, правильность ввода текста на практике не стоит усилий, так как вам вообще не следует разрешать много NULLзначений. Тем не менее, было бы замечательно, если бы СУБД включали BOTTOMтип по умолчанию в дополнение к NULLпредотвращению привычки случайно связывать семантику «нет значения» с «неизвестным значением». ]

zxq9
источник
5
Это очень хороший ответ и многое объясняет о значениях NULL и их последствиях во многих ситуациях. Вы, сэр, теперь мое уважение! Даже в колледже я не получил такого хорошего объяснения значений NULL в базах данных. Спасибо!
Я поддерживаю основную идею этого ответа. Но запись типа «предполагается, что она представляет собой недостаток информации, неизвестен», «семантически (и практически) отличается от NULL», «NULL - это неизвестен», «пустая строка - это преднамеренная запись» того, чего нет "',' NULL ==" не знаю "'и т. Д. Являются расплывчатыми и вводящими в заблуждение и действительно только мнемоникой для отсутствующих утверждений о том, как NULL или какое-либо значение является или может или должно было использоваться - для остальной части поста , (В том числе вдохновляя (плохой) дизайн функций SQL NULL.) Они ничего не оправдывают и не объясняют; они должны быть объяснены и разоблачены.
Philipxy
21

NULL == NULL -> false (по крайней мере, в СУБД)

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

Cogsy
источник
1
Это звучит как лучший ответ, но я до сих пор не понимаю, почему это запрещено при создании первичного ключа. Если бы это была просто проблема с поиском, вы могли бы использовать where pk_1 = 'a' and pk_2 = 'b'обычные значения и переключаться, where pk_1 is null and pk_2 = 'b'когда есть нули.
EoghanM
Или даже более надежно, where (a.pk1 = b.pk1 or (a.pk1 is null and b.pk1 is null)) and (a.pk2 = b.pk2 or (a.pk2 is null and b.pk2 is null))/
Джордан Ригер
8
Неправильный ответ. NULL == NULL -> НЕИЗВЕСТНО. Не ложь Уловка в том, что ограничение не считается нарушенным, если результат теста НЕИЗВЕСТЕН. Это часто приводит к тому, что SEEM выглядит так, как будто сравнение дает ложь, но на самом деле это не так.
Эрвин Смут
4

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

Сравнение чего-либо со значением NULL приводит к НЕИЗВЕСТНОМУ (третье значение истинности). Таким образом, как было предложено с нулями, вся традиционная мудрость относительно равенства уходит в окно. Ну вот как это кажется на первый взгляд.

Но я не думаю, что это обязательно так, и даже базы данных SQL не считают, что NULL уничтожает все возможности для сравнения.

Запустите в вашей базе данных запрос SELECT * FROM VALUES (NULL) UNION SELECT * FROM VALUES (NULL)

Вы видите только один кортеж с одним атрибутом, который имеет значение NULL. Таким образом, объединение признало здесь два значения NULL равными.

При сравнении составного ключа, который имеет 3 компонента, с кортежем с 3 атрибутами (1, 3, NULL) = (1, 3, NULL) <=> 1 = 1 AND 3 = 3 AND NULL = NULL Результатом этого является UNKNOWN ,

Но мы могли бы определить новый тип оператора сравнения, например. ==. X == Y <=> X = Y ИЛИ (X НЕДЕЙСТВИТЕЛЬНО, И Y НУЛЬ)

Наличие такого типа оператора равенства сделает составные ключи с нулевыми компонентами или несоставные ключи с нулевым значением беспроблемными.

Рами Охарес
источник
1
Нет, UNION признал два значения NULL неразличимыми. Что не то же самое, что "равный". Вместо этого попробуйте UNION ALL, и вы получите два ряда. Что касается «оператора сравнения нового типа», то в SQL он уже есть. НЕ ОТЛИЧАЕТСЯ ОТ. Но этого само по себе недостаточно. Использование этого в SQL-конструкциях, таких как NATURAL JOIN или предложение REFERENCES внешнего ключа, потребует дополнительных опций для этих конструкций.
Эрвин Смут
Ага, Эрвин Смут. Поистине приятно познакомиться и на этом форуме! Я не знал о SQL "НЕ ОТЛИЧАЕТСЯ". Очень интересно! Но, похоже, это именно то, что я имел в виду с моим оператором make ==. Не могли бы вы объяснить, почему вы говорите, что «этого недостаточно»?
Рами Охарес
Предложение REFERENCES основывается на равенстве по определению. Для вида ССЫЛКИ, который сопоставляет дочерний кортеж / строку с родительским кортежем / строкой на основе соответствующих значений атрибута NOT DISTINCT вместо (более строгого) EQUAL, потребовалась бы возможность указать эту опцию, но синтаксис не разрешить это. То же самое для ЕСТЕСТВЕННОГО СОЕДИНЕНИЯ.
Эрвин Смут
Чтобы внешний ключ работал, ссылочный должен быть уникальным (т. Е. Все значения должны быть разными). Это означает, что оно может иметь одно нулевое значение. Все нулевые значения могут затем ссылаться на этот единственный нуль, если REFERENCES будут определены с помощью оператора NOT DISTINCT. Я думаю, что было бы лучше (в смысле более полезным). С JOIN (как внешними, так и внутренними) я думаю, что строгое равенство лучше, потому что «NULL MATCHES» будет умножаться, когда нули на левой стороне будут совпадать со всеми нулями на правой стороне.
Рами Охарес
1

Я все еще верю, что это фундаментальный / функциональный недостаток, вызванный техническими особенностями. Если у вас есть необязательное поле, с помощью которого вы можете идентифицировать клиента, теперь вам нужно ввести в него фиктивное значение только потому, что NULL! = NULL, не особенно элегантно, хотя и является «отраслевым стандартом».

Адриан Давел
источник