Aclarar el ON CONFLICT DO UPDATE
comportamiento
Considere el manual aquí :
Para cada fila individual propuesta para la inserción, la inserción continúa o, si conflict_target
se viola una restricción o índice de árbitro especificado por
, conflict_action
se toma la alternativa .
El énfasis audaz es mío. Por lo tanto, no tiene que repetir predicados para columnas incluidas en el índice único en la WHERE
cláusula de UPDATE
(the conflict_action
):
INSERT INTO test_upsert AS tu
(name , status, test_field , identifier, count)
VALUES ('shaun', 1 , 'test value', 'ident' , 1)
ON CONFLICT (name, status, test_field) DO UPDATE
SET count = tu.count + 1;
WHERE tu.name = 'shaun' AND tu.status = 1 AND tu.test_field = 'test value'
La violación única ya establece lo que su WHERE
cláusula adicional aplicaría de forma redundante.
Aclarar índice parcial
Agregue una WHERE
cláusula para convertirlo en un índice parcial real como lo mencionó usted mismo (pero con lógica invertida):
CREATE UNIQUE INDEX test_upsert_partial_idx
ON public.test_upsert (name, status)
WHERE test_field IS NULL; -- not: "is not null"
Para usar este índice parcial en su UPSERT, necesita una coincidencia como @ypercube demuestra :conflict_target
ON CONFLICT (name, status) WHERE test_field IS NULL
Ahora se infiere el índice parcial anterior. Sin embargo , como el manual también señala :
[...] un índice único no parcial (un índice único sin predicado) será inferido (y por lo tanto utilizado por ON CONFLICT
) si dicho índice que satisface todos los demás criterios está disponible.
Si tiene un índice adicional (o solo) solo (name, status)
se usará (también). No se inferiría (name, status, test_field)
explícitamente un índice en . Esto no explica su problema, pero puede haber aumentado la confusión durante las pruebas.
Solución
AIUI, ninguno de los anteriores resuelve su problema , todavía. Con el índice parcial, solo se detectarían casos especiales con valores NULL coincidentes. Y otras filas duplicadas se insertarían si no tiene otros índices / restricciones únicos coincidentes, o generará una excepción si lo tiene. Supongo que eso no es lo que quieres. Usted escribe:
La clave compuesta está compuesta por 20 columnas, 10 de las cuales pueden ser anulables.
¿Qué es exactamente lo que consideras un duplicado? Postgres (según el estándar SQL) no considera que dos valores NULL sean iguales. El manual:
En general, se infringe una restricción única si hay más de una fila en la tabla donde los valores de todas las columnas incluidas en la restricción son iguales. Sin embargo, dos valores nulos nunca se consideran iguales en esta comparación. Eso significa que incluso en presencia de una restricción única, es posible almacenar filas duplicadas que contienen un valor nulo en al menos una de las columnas restringidas. Este comportamiento se ajusta al estándar SQL, pero hemos escuchado que otras bases de datos SQL podrían no seguir esta regla. Por lo tanto, tenga cuidado al desarrollar aplicaciones destinadas a ser portátiles.
Relacionado:
Supongo que desea que losNULL
valores en las 10 columnas anulables se consideren iguales. Es elegante y práctico cubrir una sola columna anulable con un índice parcial adicional como se muestra aquí:
Pero esto se sale de control rápidamente para obtener más columnas anulables. Necesitaría un índice parcial para cada combinación distinta de columnas anulables. Para solo 2 de esos, son 3 índices parciales para (a)
, (b)
y (a,b)
. El número está creciendo exponencialmente con 2^n - 1
. Para sus 10 columnas anulables, para cubrir todas las combinaciones posibles de valores NULL, ya necesitaría 1023 índices parciales. No vayas.
La solución simple: reemplazar los valores NULL y definir las columnas involucradas NOT NULL
, y todo funcionaría bien con una UNIQUE
restricción simple .
Si esa no es una opción, sugiero un índice de expresión con COALESCE
para reemplazar NULL en el índice:
CREATE UNIQUE INDEX test_upsert_solution_idx
ON test_upsert (name, status, COALESCE(test_field, ''));
La cadena vacía ( ''
) es un candidato obvio para los tipos de caracteres, pero puede usar cualquier valor legal que nunca aparezca o que se pueda plegar con NULL de acuerdo con su definición de "único".
Luego usa esta declaración:
INSERT INTO test_upsert as tu(name,status,test_field,identifier, count)
VALUES ('shaun', 1, null , 'ident', 11) -- works with
, ('bob' , 2, 'test value', 'ident', 22) -- and without NULL
ON CONFLICT (name, status, COALESCE(test_field, '')) DO UPDATE -- match expr. index
SET count = COALESCE(tu.count + EXCLUDED.count, EXCLUDED.count, tu.count);
Al igual que @ypercube, supongo que realmente desea agregar count
al recuento existente. Dado que la columna puede ser NULL, agregar NULL establecería la columna NULL. Si define count NOT NULL
, puede simplificar.
Otra idea sería simplemente eliminar el conflict_target de la declaración para cubrir todas las violaciones únicas . Entonces podría definir varios índices únicos para una definición más sofisticada de lo que se supone que es "único". Pero eso no volará con ON CONFLICT DO UPDATE
. El manual una vez más:
Para ON CONFLICT DO NOTHING
, es opcional especificar un conflict_target; cuando se omite, se manejan los conflictos con todas las restricciones utilizables (y los índices únicos). Para ON CONFLICT DO UPDATE
, se debe proporcionar un conflicto_target .
count = CASE WHEN EXCLUDED.count IS NULL THEN tu.count ELSE COALESCE(tu.count, 0) + COALESCE(EXCLUDED.count, 0) END
puede ser simplificado acount = COALESCE(tu.count+EXCLUDED.count, EXCLUDED.count, tu.count)
Creo que el problema es que no tienes un índice parcial y la
ON CONFLICT
sintaxis no coincide con eltest_upsert_upsert_id_idx
índice, sino con la otra restricción única.Si define el índice como parcial (con
WHERE test_field IS NULL
):y estas filas ya en la tabla:
entonces la consulta tendrá éxito:
con los siguientes resultados:
fuente