Вставить, при повторном обновлении в PostgreSQL? - PullRequest
587 голосов
/ 10 июля 2009

Несколько месяцев назад я узнал из ответа на 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 это рассматривается.

Чтобы уточнить, я хочу вставить несколько вещей и, если они уже существуют, обновить их.

Ответы [ 16 ]

437 голосов
/ 10 июля 2009

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

Возможно, в разделе рассылки hackers :

есть пример того, как сделать это массово, используя 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's answer для более ясного примера.

419 голосов
/ 30 июня 2011

Предупреждение: это не безопасно, если выполняется из нескольких сеансов одновременно (см. Предостережения ниже).


Еще один умный способ сделать «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 или выше будет надежно обрабатывать ее за счет очень высокой частоты отказов сериализации, что означает, что вам придется много повторять. См. , почему upsert такой сложный , в котором этот случай рассматривается более подробно.

Этот подход также подвержен потерянным обновлениям в read committed изоляции, если только приложение не проверяет число затронутых строк и не проверяет, что insert или update затронули строку .

222 голосов
/ 02 января 2012

В 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

117 голосов
/ 08 мая 2015

В 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ОБНОВЛЕНИЕ) в PostgreSQL?

16 голосов
/ 16 сентября 2010

Когда я пришел сюда, я искал то же самое, но отсутствие общей функции "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

самая высокая стоимость выполнения запроса из вашего кода, на стороне базы данных стоимость выполнения намного меньше

13 голосов
/ 10 июля 2009

Нет простой команды сделать это.

Самый правильный подход - использовать функцию, подобную той, которая 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');

Конечно, он рано или поздно выйдет из-под контроля (в параллельной среде), так как здесь есть явные условия гонки, но обычно это будет работать.

Вот более длинная и более полная статья по теме .

9 голосов
/ 11 мая 2012

Лично я установил «правило», прикрепленное к оператору вставки. Скажем, у вас есть таблица «днс», в которой записано количество посещений днс для каждого клиента в отдельности:

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, вам нужно использовать триггеры.

8 голосов
/ 22 сентября 2011

Я настраивал функцию "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... $$)

Важно ставить двойную запятую, чтобы избежать ошибок компилятора

  • проверить скорость ...
7 голосов
/ 29 июля 2014

Аналогично наиболее популярному ответу, но работает немного быстрее:

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 / )

6 голосов
/ 21 октября 2011

У меня та же проблема для управления настройками учетной записи, что и для пар имя-значение. Критерии проектирования таковы, что разные клиенты могут иметь разные наборы настроек.

Мое решение, аналогичное 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
...