Уточнити ON CONFLICT DO UPDATE
поведінку
Розгляньте посібник тут :
Для кожного окремого рядка, запропонованого для вставки, або вставка триває, або, якщо арбітражне обмеження або індекс, визначений
conflict_target
символом, порушено, conflict_action
приймається альтернатива .
Сміливий акцент мій. Тож вам не доведеться повторювати предикати для стовпців, включених до унікального індексу в WHERE
пункті до 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'
Унікальне порушення вже встановлює те, що додане вами додане WHERE
застереження буде виконувати надмірно.
Уточнити частковий індекс
Додайте WHERE
пункт, щоб він став фактичним частковим індексом, як ви згадали про себе (але з перевернутою логікою):
CREATE UNIQUE INDEX test_upsert_partial_idx
ON public.test_upsert (name, status)
WHERE test_field IS NULL; -- not: "is not null"
Щоб використовувати цей частковий індекс у своєму UPSERT, вам потрібна відповідність на зразок @ypercube демонструє :conflict_target
ON CONFLICT (name, status) WHERE test_field IS NULL
Тепер наведено вище частковий індекс. Однак , як зазначає також посібник :
[...] не частковий унікальний індекс (унікальний індекс без присудка) буде зроблений (і таким чином використаний ON CONFLICT
), якщо такий індекс, що відповідає всім іншим критеріям, є.
Якщо у вас є додатковий (або лише) індекс, (name, status)
він буде (також) використаний. Індекс на (name, status, test_field)
явно не буде робиться. Це не пояснює вашу проблему, але, можливо, це додало плутанини під час тестування.
Рішення
AIUI, жодне з перерахованого вище не вирішує вашу проблему . З частковим індексом можуть бути зафіксовані лише спеціальні випадки, які відповідають значенням NULL. А інші повторювані рядки будуть або вставлені, якщо у вас немає інших відповідних унікальних індексів / обмежень, або підняти виняток, якщо це зробити. Я гадаю, що це не те, чого ти хочеш. Ви пишете:
Складовий ключ складається з 20 стовпців, 10 з яких можуть бути нульовими.
Що саме ви вважаєте дублікатом? Postgres (згідно стандарту SQL) не вважає два значення NULL рівними. Посібник:
Загалом, унікальне обмеження порушується, якщо в таблиці є більше одного рядка, де значення всіх стовпців, що входять до обмеження, рівні. Однак два нульові значення ніколи не вважаються рівними в цьому порівнянні. Це означає, що навіть за наявності унікального обмеження можна зберігати повторювані рядки, що містять нульове значення, принаймні в одному з обмежених стовпців. Така поведінка відповідає стандарту SQL, але ми чули, що інші бази даних SQL можуть не дотримуватися цього правила. Тому будьте обережні, розробляючи додатки, призначені для переносу.
Пов'язані:
Я припускаю, що ви хочете, щобNULL
значення у всіх 10 нульових стовпцях вважалися рівними. Елегантно та практично покривати один нульовий стовпчик додатковим частковим індексом, як показано тут:
Але це швидко виходить з ладу для більш зворотних стовпців. Вам знадобиться частковий індекс для кожної виразної комбінації змінних стовпців. Для тільки 2 з тих , що на 3 -х часткових індексів для (a)
, (b)
і (a,b)
. Число зростає з експоненціально2^n - 1
. Для ваших 10 обнулених стовпців, щоб охопити всі можливі комбінації значень NULL, вам уже знадобляться 1023 часткові індекси. Не йдіть.
Просте рішення: замініть значення NULL та визначте задіяні стовпці NOT NULL
, і все буде добре працювати з простим UNIQUE
обмеженням.
Якщо це не варіант, я пропоную індекс вираження COALESCE
замінити NULL в індексі:
CREATE UNIQUE INDEX test_upsert_solution_idx
ON test_upsert (name, status, COALESCE(test_field, ''));
Порожній рядок ( ''
) є очевидним кандидатом для типів символів, але ви можете використовувати будь-яке юридичне значення, яке ніколи не з'являється, або може бути складене NULL відповідно до вашого визначення "унікальним".
Потім скористайтеся цим твердженням:
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);
Як і @ypercube, я припускаю, що ви насправді хочете додати count
до існуючого рахунку. Оскільки стовпець може бути NULL, додавання NULL буде встановлювати стовпець NULL. Якщо ви визначитеся count NOT NULL
, ви можете спростити.
Іншою ідеєю було б просто скинути конфлікт_target із заяви, щоб охопити всі унікальні порушення . Тоді ви могли б визначити різні унікальні індекси для більш складного визначення того, що повинно бути "унікальним". Але це не злетить ON CONFLICT DO UPDATE
. Посібник ще раз:
Бо ON CONFLICT DO NOTHING
необов’язково вказувати конфлікт_target; при пропущенні обробляються конфлікти з усіма корисними обмеженнями (і унікальними індексами). Для ON CONFLICT DO UPDATE
цього слід вказати конфлікт_target .
count = CASE WHEN EXCLUDED.count IS NULL THEN tu.count ELSE COALESCE(tu.count, 0) + COALESCE(EXCLUDED.count, 0) END
Може бути спрощенаcount = COALESCE(tu.count+EXCLUDED.count, EXCLUDED.count, tu.count)