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

94

У меня есть таблица, подобная следующей:

create table my_table (
    id   int8 not null,
    id_A int8 not null,
    id_B int8 not null,
    id_C int8 null,
    constraint pk_my_table primary key (id),
    constraint u_constrainte unique (id_A, id_B, id_C)
);

И я хочу (id_A, id_B, id_C)быть отличным в любой ситуации. Поэтому следующие две вставки должны привести к ошибке:

INSERT INTO my_table VALUES (1, 1, 2, NULL);
INSERT INTO my_table VALUES (2, 1, 2, NULL);

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

Как я могу гарантировать свое уникальное ограничение, даже если id_Cможет быть NULLв этом случае? На самом деле, реальный вопрос заключается в следующем: могу ли я гарантировать такую ​​уникальность в «чистом sql» или мне нужно реализовать ее на более высоком уровне (в моем случае java)?

Мануэль Ледук
источник
Итак, скажем, у вас есть значения (1,2,1)и (1,2,2)в (A,B,C)столбцах. Должно ли (1,2,NULL)быть разрешено добавить или нет?
ypercubeᵀᴹ
A и B не могут быть нулевыми, но C может быть нулевым или любым положительным целочисленным значением. Таким образом, (1,2,3) и (2,4, ноль) действительны, но (ноль, 2,3) или (1, ноль, 4) недействительны. И [(1,2, null), (1,2,3)] не нарушает уникальное ограничение, но [(1,2, null), (1,2, null)] должно нарушить его.
Мануэль Ледук
2
Существуют ли какие-либо значения, которые никогда не появятся в этих столбцах (например, отрицательные значения?)
a_horse_with_no_name
Вы не должны маркировать свои ограничения в pg. Это автоматически сгенерирует имя. Просто к вашему сведению.
Эван Кэрролл

Ответы:

94

Вы можете сделать это в чистом SQL . Создайте частичный уникальный индекс в дополнение к тому, который у вас есть:

CREATE UNIQUE INDEX ab_c_null_idx ON my_table (id_A, id_B) WHERE id_C IS NULL;

Таким образом, вы можете ввести для (a, b, c)в вашей таблице:

(1, 2, 1)
(1, 2, 2)
(1, 2, NULL)

Но ни один из них во второй раз.

Или используйте два частичных UNIQUEиндекса и не полный индекс (или ограничение). Лучшее решение зависит от деталей ваших требований. Для сравнения:

Хотя это элегантно эффективно для одного столбца с нулем в UNIQUEиндексе, он быстро выходит из-под контроля. Обсуждаем это - и как использовать UPSERT с частичными индексами:

Asides

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

Вы можете рассматривать serialстолбец как первичный ключ или IDENTITYстолбец в Postgres 10 или более поздней версии. Связанный:

Так:

CREATE TABLE my_table (
   my_table_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY  -- for pg 10+
-- my_table_id bigserial PRIMARY KEY  -- for pg 9.6 or older
 , id_a int8 NOT NULL
 , id_b int8 NOT NULL
 , id_c int8
 , CONSTRAINT u_constraint UNIQUE (id_a, id_b, id_c)
);

Если вы не ожидаете более 2 миллиардов строк (> 2147483647) за время жизни вашей таблицы (включая пустые и удаленные строки), рассмотрите integer(4 байта) вместо bigint(8 байтов).

Эрвин Брандштеттер
источник
1
Документы поддерживают этот метод. Добавление уникального ограничения автоматически создаст уникальный индекс B-дерева для столбца или группы столбцов, перечисленных в ограничении. Ограничение уникальности, охватывающее только некоторые строки, не может быть записано как уникальное ограничение, но можно применить такое ограничение, создав уникальный частичный индекс.
Эван Кэрролл
12

У меня была та же проблема, и я нашел другой способ добавить уникальный NULL в таблицу.

CREATE UNIQUE INDEX index_name ON table_name( COALESCE( foreign_key_field, -1) )

В моем случае поле foreign_key_fieldявляется положительным целым числом и никогда не будет равно -1.

Таким образом, чтобы ответить на руководство Leduc, другое решение может быть

CREATE UNIQUE INDEX  u_constrainte (COALESCE(id_a, -1), COALESCE(id_b,-1),COALESCE(id_c, -1) )

Я предполагаю, что идентификаторы не будут -1.

В чем преимущество создания частичного индекса?
В случае, если у вас нет предложения NOT NULL id_a, id_bи вы id_cможете быть NULL вместе только один раз.
С частичным индексом 3 поля могут быть NULL более одного раза.

Люк М
источник
3
> В чем преимущество создания частичного индекса? Способ, которым вы сделали это, COALESCEможет быть эффективным для ограничения дубликатов, но индекс не будет очень полезен при запросах, поскольку это индекс выражения, который, вероятно, не будет соответствовать выражениям запроса. То есть, если бы SELECT COALESCE(col, -1) ...вы не попали в указатель.
Бо Джинс
@BoJeanes Индекс не был создан для проблемы производительности. Он был создан для удовлетворения бизнес-требований.
Люк М
8

Нулевое значение может означать, что значение неизвестно для этой строки в данный момент, но будет добавлено, когда станет известно, в будущем (например, FinishDateдля бега Project) или что никакое значение не может быть применено для этой строки (например, EscapeVelocityдля черной дыры Star).

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

В вашем случае вы хотите разрешить NULLsв своей колонке, но вы хотите, чтобы только один NULLбыл разрешен. Почему? Какие отношения это между двумя таблицами?

Возможно, вы можете просто изменить столбец NOT NULLи сохранить вместо NULLнего специальное значение (например -1), которое, как известно, никогда не появляется. Это решит проблему ограничения уникальности (но может иметь и другие, возможно, нежелательные побочные эффекты. Например, использование -1в значении «неизвестно / не применимо» приведет к искажению любых сумм или усредненных вычислений в столбце. учитывать специальное значение и игнорировать его.)

ypercubeᵀᴹ
источник
2
В моем случае NULL - это действительно NULL (id_C является внешним ключом для table_c для примера, поэтому он не может иметь значение -1), это означает, что они не связаны между «my_table» и «table_c». Так что это имеет функциональное значение. Кстати, [(1, 1,1, ноль), (2, 1,2, ноль), (3,2,4, ноль)] является допустимым списком вставленных данных.
Мануэль Ледук
1
Это не совсем Null, используемый в SQL, потому что вы хотите только один во всех строках. Вы можете изменить схему базы данных, добавив -1 к table_c или добавив другую таблицу (которая будет супертипом к подтипу table_c).
ypercubeᵀᴹ
3
Я просто хотел бы указать @Manuel, что мнение о пустых значениях в этом ответе не является общепринятым и широко обсуждается. Многие, как и я, думают, что null можно использовать для любых целей, которые вы пожелаете (но они должны означать только одну вещь для каждого поля и быть задокументированы, возможно, в названии поля или в комментариях к колонке)
Джек Дуглас
1
Вы не можете использовать фиктивное значение, когда ваш столбец является FOREIGN KEY.
Люк М
1
+1 Я с вами: если мы хотим, чтобы какая-то комбинация столбцов была уникальной, вам нужно рассмотреть сущность, в которой эта комбинация столбцов является PK. Схема базы данных OP, вероятно, должна измениться на родительскую и дочернюю.
АК