Вставить, при повторном обновлении в PostgreSQL?
Несколько месяцев назад я узнал из ответа на Stack Overflow, как выполнять несколько обновлений одновременно в MySQL, используя следующий синтаксис:
INSERT INTO table (id, field, field2) VALUES (1, A, X), (2, B, Y), (3, C, Z)
ON DUPLICATE KEY UPDATE field=VALUES(Col1), field2=VALUES(Col2);
Я сейчас переключился на PostgreSQL, и, видимо, это не правильно. Это относится ко всем правильным таблицам, поэтому я предполагаю, что это вопрос использования разных ключевых слов, но я не уверен, где в документации PostgreSQL это рассматривается.
Чтобы уточнить, я хочу вставить несколько вещей, и если они уже существуют, чтобы обновить их.
18 ответов
PostgreSQL начиная с версии 9.5 имеет синтаксис UPSERT с предложением ON CONFLICT. со следующим синтаксисом (аналогично MySQL)
INSERT INTO the_table (id, column_1, column_2)
VALUES (1, 'A', 'X'), (2, 'B', 'Y'), (3, 'C', 'Z')
ON CONFLICT (id) DO UPDATE
SET column_1 = excluded.column_1,
column_2 = excluded.column_2;
Поиск в почтовых архивах postgresql "upsert" приводит к тому, что в руководстве вы найдете пример того, что вы, возможно, захотите сделать:
Пример 38-2. Исключения с UPDATE/INSERT
В этом примере используется обработка исключений для выполнения UPDATE или INSERT, в зависимости от ситуации:
CREATE TABLE db (a INT PRIMARY KEY, b TEXT);
CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS
$$
BEGIN
LOOP
-- first try to update the key
-- note that "a" must be unique
UPDATE db SET b = data WHERE a = key;
IF found THEN
RETURN;
END IF;
-- not there, so try to insert the key
-- if someone else inserts the same key concurrently,
-- we could get a unique-key failure
BEGIN
INSERT INTO db(a,b) VALUES (key, data);
RETURN;
EXCEPTION WHEN unique_violation THEN
-- do nothing, and loop to try the UPDATE again
END;
END LOOP;
END;
$$
LANGUAGE plpgsql;
SELECT merge_db(1, 'david');
SELECT merge_db(1, 'dennis');
В списке рассылки хакеров, возможно, есть пример того, как сделать это массово, используя CTE в 9.1 и выше.
WITH foos AS (SELECT (UNNEST(%foo[])).*)
updated as (UPDATE foo SET foo.a = foos.a ... RETURNING foo.id)
INSERT INTO foo SELECT foos.* FROM foos LEFT JOIN updated USING(id)
WHERE updated.id IS NULL;
Смотрите ответ a_horse_with_no_name для более ясного примера.
Предупреждение: это не безопасно, если выполняется из нескольких сеансов одновременно (см. Предостережения ниже).
Еще один умный способ сделать "UPSERT" в postgresql - это сделать два последовательных оператора UPDATE/INSERT, каждый из которых предназначен для успеха или не имеет никакого эффекта.
UPDATE table SET field='C', field2='Z' WHERE id=3;
INSERT INTO table (id, field, field2)
SELECT 3, 'C', 'Z'
WHERE NOT EXISTS (SELECT 1 FROM table WHERE id=3);
ОБНОВЛЕНИЕ будет успешным, если строка с "id=3" уже существует, в противном случае это не имеет никакого эффекта.
INSERT будет успешным, только если строка с "id=3" еще не существует.
Вы можете объединить эти два в одну строку и запустить их оба с помощью одного оператора SQL, выполняемого из вашего приложения. Настоятельно рекомендуется запускать их вместе в одной транзакции.
Это работает очень хорошо, когда выполняется изолированно или на заблокированной таблице, но в зависимости от условий гонки, которые могут означать, что он все равно может потерпеть неудачу с ошибкой повторяющегося ключа, если строка вставлена одновременно, или может завершиться, если строка не будет вставлена, если строка удалена одновременно, SERIALIZABLE
Транзакция на PostgreSQL 9.1 или выше будет надежно обрабатывать ее за счет очень высокой частоты отказов сериализации, что означает, что вам придется много повторяться. Посмотрите, почему так сложно, что обсуждает этот случай более подробно.
Этот подход также может быть потерян обновления в read committed
изоляция, если приложение не проверяет число строк, подверженных влиянию, и проверяет, что либо insert
или update
повлиял на ряд.
В PostgreSQL 9.1 этого можно достичь с помощью записываемого CTE ( общее табличное выражение):
WITH new_values (id, field1, field2) as (
values
(1, 'A', 'X'),
(2, 'B', 'Y'),
(3, 'C', 'Z')
),
upsert as
(
update mytable m
set field1 = nv.field1,
field2 = nv.field2
FROM new_values nv
WHERE m.id = nv.id
RETURNING m.*
)
INSERT INTO mytable (id, field1, field2)
SELECT id, field1, field2
FROM new_values
WHERE NOT EXISTS (SELECT 1
FROM upsert up
WHERE up.id = new_values.id)
Смотрите эти записи в блоге:
Обратите внимание, что это решение не предотвращает нарушение уникального ключа, но оно не уязвимо для потерянных обновлений.
Смотрите продолжение Крейг Рингер на dba.stackexchange.com
В PostgreSQL 9.5 и новее вы можете использовать INSERT ... ON CONFLICT UPDATE
,
Смотрите документацию.
MySQL INSERT ... ON DUPLICATE KEY UPDATE
может быть непосредственно перефразировано в ON CONFLICT UPDATE
, Синтаксис стандарта SQL также не является, они оба являются специфичными для базы данных расширениями. Есть веские причины MERGE
не был использован для этого, новый синтаксис не был создан просто для удовольствия. (Синтаксис MySQL также имеет проблемы, которые означают, что он не был принят напрямую).
например, данная настройка:
CREATE TABLE tablename (a integer primary key, b integer, c integer);
INSERT INTO tablename (a, b, c) values (1, 2, 3);
запрос MySQL:
INSERT INTO tablename (a,b,c) VALUES (1,2,3)
ON DUPLICATE KEY UPDATE c=c+1;
будет выглядеть так:
INSERT INTO tablename (a, b, c) values (1, 2, 10)
ON CONFLICT (a) DO UPDATE SET c = tablename.c + 1;
Отличия:
Необходимо указать имя столбца (или уникальное имя ограничения), которое будет использоваться для проверки уникальности. Это
ON CONFLICT (columnname) DO
Ключевое слово
SET
должны быть использованы, как если бы это было нормальноUPDATE
заявление
Он также имеет несколько приятных особенностей:
Вы можете иметь
WHERE
пункт на вашемUPDATE
(позволяя вам эффективно повернутьON CONFLICT UPDATE
вON CONFLICT IGNORE
для определенных значений)Предложенные для вставки значения доступны как переменная строки
EXCLUDED
, которая имеет ту же структуру, что и таблица назначения. Вы можете получить исходные значения в таблице, используя имя таблицы. Так что в этом случаеEXCLUDED.c
будет10
(потому что это то, что мы пытались вставить) и"table".c
будет3
потому что это текущее значение в таблице. Вы можете использовать один или оба вSET
выражения иWHERE
пункт.
Для справки по upsert смотрите Как сделать UPSERT (MERGE, INSERT ... ON DUPLICATE UPDATE) в PostgreSQL?
Я искал то же самое, когда пришел сюда, но отсутствие общей функции "upsert" беспокоило меня немного, поэтому я подумал, что вы можете просто пропустить обновление и вставить sql в качестве аргументов этой функции из руководства
это будет выглядеть так:
CREATE FUNCTION upsert (sql_update TEXT, sql_insert TEXT)
RETURNS VOID
LANGUAGE plpgsql
AS $$
BEGIN
LOOP
-- first try to update
EXECUTE sql_update;
-- check if the row is found
IF FOUND THEN
RETURN;
END IF;
-- not found so insert the row
BEGIN
EXECUTE sql_insert;
RETURN;
EXCEPTION WHEN unique_violation THEN
-- do nothing and loop
END;
END LOOP;
END;
$$;
и, возможно, чтобы сделать то, что вы изначально хотели сделать, пакетный "upsert", вы можете использовать Tcl, чтобы разделить sql_update и зациклить отдельные обновления, попадание в preformance будет очень маленьким, см. http://archives.postgresql.org/pgsql-performance/2006-04/msg00557.php
самая высокая стоимость выполнения запроса из вашего кода, на стороне базы данных стоимость выполнения намного меньше
Нет простой команды, чтобы сделать это.
Самый правильный подход - использовать функцию, подобную той, что есть в документации.
Другое решение (хотя и не столь безопасное) - обновить с возвратом, проверить, какие строки были обновлениями, и вставить остальные
Что-то вроде:
update table
set column = x.column
from (values (1,'aa'),(2,'bb'),(3,'cc')) as x (id, column)
where table.id = x.id
returning id;
при условии, что id:2 был возвращен:
insert into table (id, column) values (1, 'aa'), (3, 'cc');
Конечно, это рано или поздно выручит (в параллельной среде), поскольку здесь есть ясное условие гонки, но обычно это будет работать.
Вот более длинная и более полная статья по теме.
Я использую эту функцию слияния
CREATE OR REPLACE FUNCTION merge_tabla(key INT, data TEXT)
RETURNS void AS
$BODY$
BEGIN
IF EXISTS(SELECT a FROM tabla WHERE a = key)
THEN
UPDATE tabla SET b = data WHERE a = key;
RETURN;
ELSE
INSERT INTO tabla(a,b) VALUES (key, data);
RETURN;
END IF;
END;
$BODY$
LANGUAGE plpgsql
Лично я установил "правило", прикрепленное к оператору вставки. Допустим, у вас есть таблица "dns", в которой записано количество посещений DNS для каждого клиента в отдельности:
CREATE TABLE dns (
"time" timestamp without time zone NOT NULL,
customer_id integer NOT NULL,
hits integer
);
Вы хотели иметь возможность повторно вставлять строки с обновленными значениями или создавать их, если они еще не существовали. Введите идентификатор клиента и время. Что-то вроде этого:
CREATE RULE replace_dns AS
ON INSERT TO dns
WHERE (EXISTS (SELECT 1 FROM dns WHERE ((dns."time" = new."time")
AND (dns.customer_id = new.customer_id))))
DO INSTEAD UPDATE dns
SET hits = new.hits
WHERE ((dns."time" = new."time") AND (dns.customer_id = new.customer_id));
Обновление: это может привести к сбою при одновременной вставке, так как это приведет к исключениям unique_violation. Однако незавершенная транзакция будет продолжена и завершится успешно, и вам просто нужно повторить прерванную транзакцию.
Тем не менее, если все время происходит множество операций вставки, вам нужно установить блокировку таблицы вокруг операторов вставки: блокировка SHARE ROW EXCLUSIVE предотвратит любые операции, которые могут вставлять, удалять или обновлять строки в вашей целевой таблице. Однако обновления, которые не обновляют уникальный ключ, безопасны, поэтому, если вы не выполняете никаких действий, используйте вместо этого рекомендательные блокировки.
Кроме того, команда COPY не использует ПРАВИЛА, поэтому, если вы вставляете с помощью COPY, вам нужно будет использовать триггеры.
Я настраиваю функцию "upsert" выше, если вы хотите вставить и заменить:
`
CREATE OR REPLACE FUNCTION upsert(sql_insert text, sql_update text)
RETURNS void AS
$BODY$
BEGIN
-- first try to insert and after to update. Note : insert has pk and update not...
EXECUTE sql_insert;
RETURN;
EXCEPTION WHEN unique_violation THEN
EXECUTE sql_update;
IF FOUND THEN
RETURN;
END IF;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION upsert(text, text)
OWNER TO postgres;`
А после выполнения сделайте что-то вроде этого:
SELECT upsert($$INSERT INTO ...$$,$$UPDATE... $$)
Важно ставить двойную запятую, чтобы избежать ошибок компилятора
- проверь скорость...
Похож на наиболее понравившийся ответ, но работает немного быстрее:
WITH upsert AS (UPDATE spider_count SET tally=1 WHERE date='today' RETURNING *)
INSERT INTO spider_count (spider, tally) SELECT 'Googlebot', 1 WHERE NOT EXISTS (SELECT * FROM upsert)
(источник: http://www.the-art-of-web.com/sql/upsert/)
Согласно документации PostgreSQL INSERT
заявление, обработка ON DUPLICATE KEY
дело не поддерживается. Эта часть синтаксиса является проприетарным расширением MySQL.
CREATE OR REPLACE FUNCTION save_user(_id integer, _name character varying)
RETURNS boolean AS
$BODY$
BEGIN
UPDATE users SET name = _name WHERE id = _id;
IF FOUND THEN
RETURN true;
END IF;
BEGIN
INSERT INTO users (id, name) VALUES (_id, _name);
EXCEPTION WHEN OTHERS THEN
UPDATE users SET name = _name WHERE id = _id;
END;
RETURN TRUE;
END;
$BODY$
LANGUAGE plpgsql VOLATILE STRICT
У меня та же проблема для управления настройками учетной записи, что и для пар имя-значение. Критерии дизайна таковы, что разные клиенты могут иметь разные наборы настроек.
Мое решение, похожее на JWP, заключается в массовом удалении и замене, генерируя запись слияния в вашем приложении.
Это довольно пуленепробиваемый, независимый от платформы, и поскольку на клиента не может быть более 20 настроек, это всего лишь 3 вызова с достаточно низкой нагрузкой - вероятно, самый быстрый метод.
Альтернативой обновления отдельных строк - проверки исключений, а затем вставки - или какой-либо комбинации является отвратительный код, медленный и часто ломающийся, потому что (как упомянуто выше) нестандартная обработка исключений SQL изменяется с db на db - или даже выпуск для выпуска.
#This is pseudo-code - within the application:
BEGIN TRANSACTION - get transaction lock
SELECT all current name value pairs where id = $id into a hash record
create a merge record from the current and update record
(set intersection where shared keys in new win, and empty values in new are deleted).
DELETE all name value pairs where id = $id
COPY/INSERT merged records
END TRANSACTION
Для объединения небольших наборов хорошо использовать вышеуказанную функцию. Однако, если вы объединяете большие объемы данных, я бы посоветовал заглянуть в http://mbk.projects.postgresql.org/
В настоящее время я знаю о наилучшей практике:
- КОПИРОВАНИЕ новых / обновленных данных во временную таблицу (конечно, или вы можете сделать INSERT, если стоимость в порядке)
- Получить блокировку [необязательно] (рекомендуется использовать для блокировки таблиц, IMO)
- Слияние. (забавная часть)
PostgreSQL >= v15
Большие новости по этой теме, так как в PostgreSQL v15 можно использоватьMERGE
команда. Фактически, эта долгожданная функция была указана первым из улучшений версии v15.
Это похоже на, но более ориентировано на пакетную обработку. Он имеет мощныйWHEN MATCHED
противWHEN NOT MATCHED
структура, дающая возможностьINSERT
,UPDATE
илиDELETE
на таких условиях.
Это не только облегчает массовые изменения, но даже добавляет больше контроля над традицией иINSERT ... ON CONFLICT
Взгляните на этот очень полный образец с официальной страницы:
MERGE INTO wines w
USING wine_stock_changes s
ON s.winename = w.winename
WHEN NOT MATCHED AND s.stock_delta > 0 THEN
INSERT VALUES(s.winename, s.stock_delta)
WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
UPDATE SET stock = w.stock + s.stock_delta
WHEN MATCHED THEN
DELETE;
PostgreSQL v9, v10, v11, v12, v13, v14
Если версия ниже v15 и выше v9.5, вероятно, лучше всего использоватьUPSERT
синтаксис, сON CONFLICT
пункт
UPDATE вернет количество измененных строк. Если вы используете JDBC (Java), вы можете сравнить это значение с 0 и, если строки не были затронуты, вместо этого запустить INSERT. Если вы используете какой-то другой язык программирования, возможно, число измененных строк все еще можно получить, проверьте документацию.
Это может быть не так элегантно, но у вас гораздо более простой SQL, который более тривиально использовать из вызывающего кода. Иными словами, если вы пишете 10-строчный скрипт на PL/PSQL, вам, вероятно, нужно иметь модульный тест того или иного вида только для него.
Изменить: это не работает, как ожидалось. В отличие от принятого ответа, это приводит к нарушениям уникального ключа, когда два процесса повторно вызывают upsert_foo
одновременно.
Эврика! Я нашел способ сделать это в одном запросе: использовать UPDATE ... RETURNING
проверить, были ли затронуты какие-либо строки:
CREATE TABLE foo (k INT PRIMARY KEY, v TEXT);
CREATE FUNCTION update_foo(k INT, v TEXT)
RETURNS SETOF INT AS $$
UPDATE foo SET v = $2 WHERE k = $1 RETURNING $1
$$ LANGUAGE sql;
CREATE FUNCTION upsert_foo(k INT, v TEXT)
RETURNS VOID AS $$
INSERT INTO foo
SELECT $1, $2
WHERE NOT EXISTS (SELECT update_foo($1, $2))
$$ LANGUAGE sql;
UPDATE
должно быть сделано в отдельной процедуре, потому что, к сожалению, это синтаксическая ошибка:
... WHERE NOT EXISTS (UPDATE ...)
Теперь все работает как нужно:
SELECT upsert_foo(1, 'hi');
SELECT upsert_foo(1, 'bye');
SELECT upsert_foo(3, 'hi');
SELECT upsert_foo(3, 'bye');
Вот пример, как сделать upsert с параметрами и без специальных конструкций sql, если у вас есть особое условие (иногда вы не можете использовать «при конфликте», потому что вы не можете создать ограничение)
WITH upd AS
(
update view_layer set metadata=:metadata where layer_id = :layer_id and view_id = :view_id returning id
)
insert into view_layer (layer_id, view_id, metadata)
(select :layer_id layer_id, :view_id view_id, :metadata metadata FROM view_layer l
where NOT EXISTS(select id FROM upd WHERE id IS NOT NULL) limit 1)
returning id
может быть, это будет полезно