Проблема PostgreSQL UPSERT зі значеннями NULL


13

У мене виникають проблеми з використанням нової функції UPSERT в Postgres 9.5

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

CREATE TABLE public.test_upsert (
upsert_id serial,
name character varying(32) NOT NULL,
status integer NOT NULL,
test_field text,
identifier character varying(255),
count integer,
CONSTRAINT upsert_id_pkey PRIMARY KEY (upsert_id),
CONSTRAINT test_upsert_name_status_test_field_key UNIQUE (name, status, test_field)
);

Запуск цього запиту працює за необхідності (Спочатку вставляйте, а потім наступні вставки просто збільшуєте кількість):

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

Однак якщо я запускаю цей запит, кожен раз вставляється 1 рядок, а не збільшується кількість початкових рядків:

INSERT INTO test_upsert as tu(name,status,test_field,identifier, count) 
VALUES ('shaun',1,null,'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 = null;

Це моє питання. Мені потрібно просто збільшити значення підрахунку, а не створювати кілька однакових рядків з нульовими значеннями.

Спроба додати частковий унікальний індекс:

CREATE UNIQUE INDEX test_upsert_upsert_id_idx
ON public.test_upsert
USING btree
(name COLLATE pg_catalog."default", status, test_field, identifier);

Однак це дає ті самі результати, або вставлені кілька нульових рядків, або це повідомлення про помилку при спробі вставлення:

ПОМИЛКА: не існує єдиного обмеження або обмеження виключення, що відповідає специфікації ON CONFLICT

Я вже намагався додати додаткові деталі щодо часткового індексу, такого як WHERE test_field is not null OR identifier is not null. Однак при вставці я отримую повідомлення про помилку обмеження.

Відповіді:


15

Уточнити 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 .


1
Приємно. Я пропустив частину 20-10 стовпців, коли я прочитав питання, і не встиг завершити пізніше. 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)
ypercubeᵀᴹ

Подивившись знову, моя "спрощена" версія не така самодокументація.
ypercubeᵀᴹ

@ ypercubeᵀᴹ: Я застосував запропоноване оновлення. Простіше, дякую.
Ервін Брандстеттер

@ErwinBrandstetter ти найкращий
Seamus Abshere

7

Я думаю, що проблема полягає в тому, що у вас немає часткового індексу, а ON CONFLICTсинтаксис не відповідає test_upsert_upsert_id_idxіндексу, а іншому унікальному обмеженню.

Якщо ви визначите індекс як частковий (з WHERE test_field IS NULL):

CREATE UNIQUE INDEX test_upsert_upsert_id_idx
ON public.test_upsert
USING btree
(name COLLATE pg_catalog."default", status)
WHERE test_field IS NULL ;

і ці рядки вже в таблиці:

INSERT INTO test_upsert as tu
    (name, status, test_field, identifier, count) 
VALUES 
    ('shaun', 1, null, 'ident', 1),
    ('maria', 1, null, 'ident', 1) ;

тоді запит буде успішним:

INSERT INTO test_upsert as tu
    (name, status, test_field, identifier, count) 
VALUES 
    ('peter', 1,   17, 'ident', 1),
    ('shaun', 1, null, 'ident', 3),
    ('maria', 1, null, 'ident', 7)
ON CONFLICT 
    (name, status) WHERE test_field IS NULL   -- the conflicting condition
DO UPDATE SET
    count = tu.count + EXCLUDED.count 
WHERE                                         -- when to update
    tu.name = 'shaun' AND tu.status = 1 ;     -- if you don't want all of the
                                              -- updates to happen

з такими результатами:

('peter', 1,   17, 'ident', 1)  -- no conflict: row inserted

('shaun', 1, null, 'ident', 3)  -- conflict: no insert
                           -- matches where: row updated with count = 1+3 = 4

('maria', 1, null, 'ident', 1)  -- conflict: no insert
                     -- doesn't match where: no update

Це пояснює, як використовувати частковий індекс. Але (я думаю) це ще не вирішує проблему.
Ервін Брандстеттер

чи не слід вважати, що кількість "марії" залишається 1, оскільки не відбувається оновлення?
mpprdev

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