Що не так із нульовими стовпцями у складених первинних ключах?


149

ORACLE не дозволяє NULL значень в жодному зі стовпців, що містять первинний ключ. Схоже, те саме стосується більшості інших систем на рівні підприємства.

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

Чому так унікальні обмеження можуть мати NULL, але первинні ключі не можуть? Чи є в цьому фундаментальна логічна причина, чи це більше технічне обмеження?


Відповіді:


216

Первинні ключі призначені для однозначної ідентифікації рядків. Це робиться шляхом порівняння всіх частин ключа до вводу.

За визначенням, NULL не може бути частиною успішного порівняння. Навіть порівняння з самим собою ( NULL = NULL) не вдасться. Це означає, що ключ, що містить NULL, не працює.

Крім того, NULL дозволено в іноземному ключі, щоб позначити необов'язкові відносини. (*) Якщо дозволити це в ПК, це також порушило б це.


(*) Слово обережності: Мати нульові сторонні ключі - це не чиста конструкція реляційних баз даних.

Якщо є дві сутності, Aі Bякщо Aвони необов'язково можуть бути пов'язані B, чистим рішенням є створення таблиці роздільної здатності (скажімо AB). Ця таблиця посилатиметься Aна B: Якщо є зв'язок, то вона міститиме запис, а якщо немає , то не буде.


5
Я змінив прийняту відповідь на цю. Судячи з голосів, ця відповідь є найбільш зрозумілою для більшості людей. Я все ще відчуваю, що відповідь Тоні Ендрюса краще пояснює намір цього дизайну; перевірити це також!
Роман Старков

2
Q: Коли ви хочете, щоб NULL FK замість відсутності ряду? A: Тільки у версії схеми, денормалізованої для оптимізації. У нетривіальних схемах ненормалізовані проблеми, як це, можуть спричинити проблеми, коли потрібні нові функції. Ото, натовп веб-дизайну не хвилює. Я хотів би додати ноту обережності з цього приводу, замість того, щоб це звучало як гарна дизайнерська ідея.
zxq9

3
"Мати нульові сторонні ключі - це не чистий дизайн реляційних баз даних." - безвідмінна конструкція баз даних (шоста нормальна форма) незмінно додає складності, отримані заощадження місця часто переважують додаткові роботи програміста, необхідні для реалізації цих здобутків.
Дай

1
що робити, якщо це таблиця роздільної здатності ABC? з факультативом C
Bart Calixto

1
Я намагався уникати написання "тому що стандарт забороняє це", оскільки це насправді нічого не пояснює.
Томалак

62

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

Унікальне обмеження не обов'язково визначає кожен ряд; він просто вказує, що якщо рядок має значення у своїх стовпцях, то вони повинні бути унікальними. Цього недостатньо, щоб однозначно ідентифікувати кожен рядок, що і повинен робити первинний ключ.


10
У сервісі Sql унікальне обмеження, яке має нульовий стовпець, дозволяє значення "null" у цьому стовпчику лише один раз (задано однакові значення для інших стовпців обмеження). Тож таке унікальне обмеження по суті поводиться як pk із стовпчиком, що обертається.
Джерард

Я підтверджую те ж саме для Oracle (11.2)
Олександр Малахов

2
У Oracle (я не знаю про SQL Server) таблиця може містити багато рядків, де всі стовпці з унікальним обмеженням є недійсними. Однак якщо деякі стовпці (-і) в унікальному обмеженні не є нульовими, а деякі - нульовими, то унікальність застосовується.
Тоні Ендрюс

Як це стосується складеного UNIQUE?
Дімс

1
@Dims Як і майже все інше в базах даних SQL, "це залежить від реалізації". У більшості dbs "первинний ключ" - це фактично єдине обмеження під ним. Ідея "первинного ключа" насправді не є більш особливою чи потужною, ніж концепція УНІКАЛЬНОГО. Справжня різниця полягає в тому, що якщо у вас є два незалежних аспекти таблиці, які можна гарантувати УНІКАЛЬНІ, то у вас немає нормалізованої бази даних за визначенням (ви зберігаєте два типи даних в одній таблиці).
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" або будь-який інший для когось кому чотири поля недостатньо, можливо, присниться). Якщо пакет не має розширення, то він є НУЛЛ у наведеній вище моделі, і шкоди не було б заподіяно, залишивши речі таким чином.

Але що таке НУЛЬ? Він повинен представляти брак інформації, невідомого. Однак, можливо, це має більше сенсу:

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значень. З цього приводу було б чудово, якби RDBMS включав BOTTOMтип за замовчуванням на додаток до того, щоб NULLзапобігти звичці випадково поєднувати семантику "без значення" з "невідомим значенням". ]


5
Це ДУЖЕ НІШКА відповідь і багато що пояснює про значення NULL і це впливає на багато ситуацій. Ви, пане, майте мою повагу! Навіть у коледжі я не отримав такого хорошого пояснення щодо значень NULL всередині баз даних. Дякую!

Я підтримую основну думку цієї відповіді. Але написання на кшталт "повинно представляти брак інформації, невідомого", "семантично (і практично) відрізняється від NULL", "NULL є невідомим", "порожня рядок - це свідомий запис про те, що" чогось немає "',' NULL ==" не знаю "'і т. Д. Є невиразними та оманливими і насправді лише мнемоніками для відсутніх висловлювань про те, як NULL або будь-яке значення використовується, або може, або призначене для використання - за рештою публікації . (У тому числі надихає (поганий) дизайн функцій SQL NULL.) Вони нічого не виправдовують і не пояснюють; їх слід пояснити та розкрити.
філіпсі

21

NULL == NULL -> false (принаймні, у СУБД)

Таким чином, ви не зможете отримати жодних зв’язків, використовуючи значення NULL, навіть з додатковими стовпцями з реальними значеннями.


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 знищує всю можливість для порівняння.

Запустіть у вашій базі запит ВИБІРТИ * З ЦІННІВ (НУЛЬ) ВИБІР СПІЛЬНОСТІ * З ЦЕНЬ (НУЛЛ)

Що ви бачите, це лише один кортеж з одним атрибутом, який має значення NULL. Тож об'єднання визнало тут два значення NULL як рівні.

Якщо порівнювати складений ключ, який містить 3 компоненти до кортежу з 3 атрибутами (1, 3, NULL) = (1, 3, NULL) <=> 1 = 1 І 3 = 3 І NULL = NULL Результат цього невідомий .

Але ми могли б визначити новий вид оператора порівняння, наприклад. ==. X == Y <=> X = Y АБО (X - NULL, Y Y NULL)

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


1
Ні, Союз визнав два NULL як невідрізні. Що не те саме, що "рівний". Спробуйте UNION ALL замість цього, ви отримаєте два ряди. А що стосується "нового виду операторів порівняння", у SQL він вже є. НЕ ВІДДІЛИТИ. Але цього само по собі недостатньо. Використання цього в конструкціях SQL, таких як NATURAL JOIN або пункт REFERENCES іноземного ключа, потребуватиме додаткових параметрів для цих конструкцій.
Ервін Смоут

Ага, Ервін Смоут. Воістину приємно познайомитися з вами також на цьому форумі! Мені не було відомо про те, що SQL "НЕ РІЗНЕ". Дуже цікаво! Але здається, що це саме те, що я мав на увазі зі своїм оператором make-up ==. Чи можете ви пояснити мені, чому ви говорите так: "що само по собі недостатньо"?
Рамі Оджарес

Стаття ПОСИЛАННЯ спирається на рівність за визначенням. Вигляд СПОСОБІВ, який відповідає дочірньому кортежу / рядку з батьківським кортежем / рядком, виходячи з відповідних значень атрибутів, БУДІ НЕ ДИСТАНТИМИ замість (суворіших) ЕКВАЛІВ, вимагатиме можливості вказати цей параметр, але синтаксис не відповідає дозволити це. Дітто для ПРИРОДНОГО ПРИЄДНАННЯ.
Ервін Смоут

Для того, щоб зовнішній ключ працював, посилання має бути унікальним (тобто всі значення повинні бути чіткими). Що означає, що воно може мати єдине нульове значення. Усі нульові значення можуть потім посилатися на цей єдиний нуль, якщо СПОСОБИ будуть визначені оператором NOT DISTINCT. Я думаю, що це було б краще (в сенсі корисніше). З ПРИЄДНАЙМОЮ (як зовнішньою, так і внутрішньою), я думаю, що суворі рівні рівні, тому що "НУЛЬНІ МАТЧИ" помножиться, коли нулі з лівого боку будуть відповідати всім нулям на правій стороні.
Рамі Оджарес,

1

Я все ще вважаю, що це фундаментальний / функціональний недолік, спричинений технікою. Якщо у вас є необов'язкове поле, за допомогою якого ви можете ідентифікувати клієнта, вам зараз доведеться зламати в нього фіктивне значення, просто тому, що NULL! = NULL, не особливо елегантний, але це "галузевий стандарт"

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.