Вставить, дублировать обновление в 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

Ответ 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');