Уточнить 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 обнуляемых столбцах считались равными. Элегантно и практично покрывать один столбец, который может быть пустым, с дополнительным частичным индексом, как показано здесь:
Но это быстро выходит из-под контроля для более обнуляемых столбцов. Вам понадобится частичный индекс для каждой отдельной комбинации столбцов, допускающих значение NULL. Только для 2 из них это 3 частных индекса (a)
, (b)
и (a,b)
. Число растет в геометрической прогрессии с 2^n - 1
. Для того чтобы ваши 10 столбцов, допускающих значение NULL, покрывали все возможные комбинации значений 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
, вы можете упростить.
Другая идея заключается в том, чтобы просто исключить объект конфликта из заявления, чтобы охватить все уникальные нарушения . Затем вы можете определить различные уникальные индексы для более сложного определения того, что должно быть «уникальным». Но это не сработает ON CONFLICT DO UPDATE
. Руководство еще раз:
Для ON CONFLICT DO NOTHING
, это необязательно, чтобы указать конфликта_target; если опущено, обрабатываются конфликты со всеми используемыми ограничениями (и уникальными индексами). Для ON CONFLICT DO UPDATE
, конфликт_ цель должен быть предоставлен.
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)
Я думаю, проблема в том, что у вас нет частичного индекса, а
ON CONFLICT
синтаксис не совпадает сtest_upsert_upsert_id_idx
индексом, а с другим уникальным ограничением.Если вы определите индекс как частичный (с
WHERE test_field IS NULL
):и эти строки уже в таблице:
тогда запрос будет выполнен успешно:
со следующими результатами:
источник