Restricción única de columnas múltiples de PostgreSQL y valores NULL


94

Tengo una tabla como la siguiente:

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)
);

Y quiero (id_A, id_B, id_C)ser distinto en cualquier situación. Por lo tanto, las siguientes dos inserciones deben provocar un error:

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

Pero no se comporta como se esperaba porque, según la documentación, dos NULLvalores no se comparan entre sí, por lo que ambas inserciones pasan sin error.

¿Cómo puedo garantizar mi restricción única, incluso si id_Cpuede ser NULLen este caso? En realidad, la verdadera pregunta es: ¿puedo garantizar este tipo de singularidad en "sql puro" o tengo que implementarlo en un nivel superior (Java en mi caso)?


Entonces, digamos que tiene valores (1,2,1)y (1,2,2)en las (A,B,C)columnas. ¿Debería (1,2,NULL)permitirse que se agregue o no?
ypercubeᵀᴹ

A y B no pueden ser nulos, pero C puede ser nulo o cualquier valor entero positivo. Entonces (1,2,3) y (2,4, nulo) son válidos pero (nulo, 2,3) o (1, nulo, 4) no son válidos. Y [(1,2, nulo), (1,2,3)] no rompe la restricción única, pero [(1,2, nulo), (1,2, nulo)] debe romperla.
Manuel Leduc

2
¿Hay valores que no aparecerán en las columnas (como valores negativos?)
a_horse_with_no_name

No tiene que etiquetar sus restricciones en la pág. Generará automáticamente un nombre. Solo para tu información.
Evan Carroll

Respuestas:


94

Puedes hacerlo en SQL puro . Cree un índice único parcial además del que tiene:

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

De esta manera puede ingresar (a, b, c)en su tabla:

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

Pero ninguno de estos por segunda vez.

O use dosUNIQUE índices parciales y ningún índice completo (o restricción). La mejor solución depende de los detalles de sus requisitos. Comparar:

Si bien esto es elegante y eficiente para una sola columna anulable en el UNIQUEíndice, se descontrola rápidamente por más. Discutiendo esto, y cómo usar UPSERT con índices parciales:

Aparte

No se pueden usar identificadores de mayúsculas y minúsculas sin comillas dobles en PostgreSQL.

Usted puede considerar una serialcolumna como clave principal o una IDENTITYcolumna en Postgres 10 o posterior. Relacionado:

Entonces:

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)
);

Si no espera más de 2 mil millones de filas (> 2147483647) durante la vida útil de su tabla (incluidos los desperdicios y las filas eliminadas), considere integer(4 bytes) en lugar de bigint(8 bytes).


1
Los documentos recomiendan este método. Agregar una restricción única creará automáticamente un índice de árbol B único en la columna o grupo de columnas enumeradas en la restricción. Una restricción de unicidad que cubre solo algunas filas no se puede escribir como una restricción única, pero es posible hacer cumplir dicha restricción creando un índice parcial único.
Evan Carroll

12

Tuve el mismo problema y encontré otra forma de tener un NULL único en la tabla.

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

En mi caso, el campo foreign_key_fieldes un entero positivo y nunca será -1.

Entonces, para responder al Manual Leduc, otra solución podría ser

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

Supongo que los identificadores no serán -1.

¿Cuál es la ventaja de crear un índice parcial?
En caso de que no tenga la cláusula NOT NULL id_a, id_by id_cpueda estar NULL juntos solo una vez.
Con un índice parcial, los 3 campos podrían ser NULL más de una vez.


3
> ¿Cuál es la ventaja de crear un índice parcial? La forma en que lo ha hecho COALESCEpuede ser eficaz para restringir los duplicados, pero el índice no sería muy útil en las consultas, ya que es un índice de expresión que probablemente no coincida con las expresiones de consulta. Es decir, a menos que no estés SELECT COALESCE(col, -1) ...golpeando el índice.
Bo Jeanes

@BoJeanes El índice no se ha creado para un problema de rendimiento. Se ha creado para cumplir con los requisitos comerciales.
Luc M

8

Un nulo puede significar que el valor no se conoce para esa fila en este momento, pero se agregará, cuando se conozca, en el futuro (ejemplo FinishDatepara una carrera Project) o que no se puede aplicar ningún valor para esa fila (ejemplo EscapeVelocitypara un agujero negro Star).

En mi opinión, generalmente es mejor normalizar las tablas eliminando todos los Nulos.

En su caso, desea permitir NULLsen su columna, pero desea que solo se NULLpermita uno . ¿Por qué? ¿Qué tipo de relación es esta entre las dos tablas?

Quizás pueda simplemente cambiar la columna NOT NULLy almacenar, en lugar de NULL, un valor especial (como -1) que se sabe que nunca aparece. Esto resolverá el problema de restricción de unicidad (pero puede tener otros efectos secundarios posiblemente no deseados. Por ejemplo, usar -1para significar "no conocido / no se aplica" sesgará cualquier cálculo de suma o promedio en la columna. O todos esos cálculos tendrán que tomarse tener en cuenta el valor especial e ignorarlo.)


2
En mi caso, NULL es realmente NULL (id_C es una clave foránea para table_c por ejemplo, por lo que no puede tener un valor de -1), significa que no hay relación entre "my_table" y "table_c". Entonces tiene un significado funcional. Por cierto [(1, 1,1, nulo), (2, 1,2, nulo), (3,2,4, nulo)] es una lista válida de datos insertados.
Manuel Leduc

1
No es realmente un Nulo como se usa en SQL porque solo quieres uno en todas las filas. Puede cambiar el esquema de su base de datos agregando -1 a table_c o agregando otra tabla (que sería supertipo a subtipo table_c).
ypercubeᵀᴹ

3
Solo me gustaría señalarle a @Manuel que la opinión sobre nulos en esta respuesta no es universal, y es muy debatida. Muchos, como yo, piensan que nulo puede usarse para cualquier propósito que desee (pero solo debe significar una cosa para cada campo y estar documentado, posiblemente en el nombre del campo o en un comentario de columna)
Jack Douglas

1
No puede usar un valor ficticio cuando su columna es una CLAVE EXTRANJERA.
Luc M

1
+1 Estoy contigo: si queremos que alguna combinación de columnas sea única, entonces debes considerar una entidad en la que esta combinación de columnas sea un PK. El esquema de la base de datos de los OP probablemente debería cambiar a una tabla primaria y una secundaria.
AK
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.