Вставить, дублировать обновление в 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.
Чтобы уточнить, я хочу вставить несколько вещей, и если они уже существуют для их обновления.
Ответы
Ответ 1
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 для более четкого примера.
Ответ 2
Предупреждение: это небезопасно, если выполняется с нескольких сеансов одновременно (см. ниже).
Другим умным способом выполнить "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);
UPDATE будет успешным, если строка с "id = 3" уже существует, в противном случае она не имеет эффекта.
INSERT будет успешным, только если строка с "id = 3" еще не существует.
Вы можете объединить эти два в одну строку и запустить их как с одним оператором SQL, выполняемым из вашего приложения. Рекомендуется использовать их вместе в одной транзакции.
Это работает очень хорошо, когда выполняется изолированно или в заблокированной таблице, но зависит от условий гонки, которые означают, что он все равно может не работать с дублирующейся ключевой ошибкой, если строка вставлена одновременно или может завершиться без строки, вставленной, когда строка удаляется одновременно. Операция SERIALIZABLE
на PostgreSQL 9.1 или выше будет надежно справляться с ней за счет очень высокой скорости сбоя сериализации, то есть вам придется многократно повторить попытку. См. почему это так сложно, в котором более подробно обсуждается этот случай.
Этот подход также подвергается потерям обновлений в read committed
изоляции, если приложение не проверяет затронутые подсчеты строк и не проверяет, повлияло ли на строку insert
или update
на строку.
Ответ 3
С 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)
Смотрите эти записи в блоге:
Обратите внимание, что это решение не предотвращает уникальное нарушение ключа, но оно не уязвимо для потерянных обновлений.
См. продолжение Craig Ringer на dba.stackexchange.com
Ответ 4
В 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?
Ответ 5
Когда я пришел сюда, я искал то же самое, но отсутствие общей функции "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- производительность /2006-04/msg00557.php
самая высокая стоимость выполнения запроса из вашего кода, на стороне базы данных стоимость выполнения намного меньше
Ответ 6
Для этого нет простой команды.
Самый правильный подход - использовать функцию, такую как функция docs.
Другое решение (хотя это и не так безопасно) - обновлять с возвратом, проверять, какие строки были обновлениями, и вставить остальные из них
Что-то по строкам:
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');
Конечно, он рано или поздно выручит (в параллельной среде), так как здесь есть явное состояние гонки, но обычно это будет работать.
Здесь более длинная и более полная статья по теме.
Ответ 7
Лично я установил "правило", прилагаемое к инструкции insert. Скажем, у вас была таблица "dns", в которой каждый раз записывались клики DNS для каждого клиента:
CREATE TABLE dns (
"time" timestamp without time zone NOT NULL,
customer_id integer NOT NULL,
hits integer
);
Вы хотели иметь возможность повторно вставлять строки с обновленными значениями или создавать их, если они еще не существовали. Ключ к customer_id и времени. Что-то вроде этого:
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));
Обновление. Это может потерпеть неудачу, если происходят одновременные вставки, поскольку он будет генерировать уникальные исключения исключения. Тем не менее, транзакция без прерывания будет продолжаться и преуспеть, и вам просто нужно повторить завершенную транзакцию.
Однако, если количество вложений происходит постоянно, вы захотите установить блокировку таблицы вокруг операторов вставки: блокировка SHARE ROW EXCLUSIVE предотвратит любые операции, которые могут вставлять, удалять или обновлять строки в вашей целевой таблице. Тем не менее, обновления, которые не обновляют уникальный ключ, безопасны, поэтому, если вы не выполняете никаких действий, используйте вместо этого консультативные блокировки.
Кроме того, команда COPY не использует ПРАВИЛА, поэтому, если вы вставляете ее с COPY, вам нужно использовать триггеры вместо этого.
Ответ 8
Я настраиваю функцию "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... $$)
Важно поместить двойную долларовую запятую, чтобы избежать ошибок компилятора
Ответ 9
Похоже на наиболее понравившийся ответ, но работает немного быстрее:
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/)
Ответ 10
У меня такая же проблема для управления настройками учетной записи, как и пары значений имени.
Критерии проектирования заключаются в том, что разные клиенты могут иметь разные настройки.
Мое решение, аналогичное JWP, - это массовое удаление и замена, генерирование записи объединения в вашем приложении.
Это довольно пуленепробиваемая, независимая от платформы, и поскольку на клиента не более 20 настроек, это всего лишь 3 довольно низкого уровня загрузки db-вызовов - возможно, самый быстрый метод.
Альтернатива обновления отдельных строк - проверка исключений, а затем вставка - или некоторая комбинация отвратительного кода, медленная и часто ломающаяся, потому что (как упоминалось выше) обработка нестандартных 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
Ответ 11
Согласно документации PostgreSQL оператора INSERT
, обработка случая ON DUPLICATE KEY
не поддерживается. Эта часть синтаксиса является проприетарным расширением MySQL.
Ответ 12
Для слияния небольших множеств использование вышеуказанной функции в порядке. Однако, если вы объединяете большие объемы данных, я бы предложил посмотреть http://mbk.projects.postgresql.org
Текущая лучшая практика, о которой я знаю, это:
- КОПИРОВАТЬ новые/обновленные данные в таблицу temp (обязательно, или вы можете сделать INSERT, если стоимость в порядке)
- Приобретение блокировки [необязательно] (рекомендуется использовать блокировки таблиц, IMO)
- Merge. (забавная часть)
Ответ 13
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
Ответ 14
UPDATE вернет количество измененных строк. Если вы используете JDBC (Java), вы можете сравнить это значение с 0 и, если строки не были затронуты, вместо этого запустить INSERT. Если вы используете какой-то другой язык программирования, возможно, число измененных строк все еще можно получить, проверьте документацию.
Это может быть не так элегантно, но у вас гораздо более простой SQL, который более тривиально использовать из вызывающего кода. Иными словами, если вы пишете сценарий из десяти строк на PL/PSQL, вам, вероятно, нужно провести unit тест того или иного вида только для него.
Ответ 15
Я использую эту функцию merge
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
Ответ 16
Изменить: Это не работает должным образом. В отличие от принятого ответа, это приводит к уникальным нарушениям ключа, когда два процесса неоднократно вызывают 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');