Вставить, при повторном обновлении в PostgreSQL?

522

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

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

  • 37
    Любой, кто найдет этот вопрос, должен прочесть статью Депеша «Почему так сложно расстроиться , Это объясняет проблему и возможные решения очень хорошо.
  • 7
    UPSERT будет добавлен в Postgres 9.5: wiki.postgresql.org/wiki/…
Показать ещё 1 комментарий
Теги:
upsert
sql-merge

16 ответов

375
Лучший ответ

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 для более четкого примера.

  • 5
    Единственное, что мне не нравится в этом, это то, что это будет намного медленнее, потому что каждый переход будет представлять собой отдельный вызов в базу данных.
  • 0
    @ baash05 Там может быть способ сделать это навалом, см. мой обновленный ответ.
Показать ещё 10 комментариев
385

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


Другим умным способом выполнить "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 на строку.

  • 0
    Вопрос, INSERT терпит неудачу, если запись существует? или он вставляет пустую запись? Будет ли это работать, если я не использую идентификатор (pk) и просто другое уникальное поле?
  • 5
    Краткий ответ: если запись существует, INSERT ничего не делает. Длинный ответ: SELECT в INSERT вернет столько результатов, сколько совпадений в предложении where. Это самое большее один (если номер один не в результате суб-выбора), иначе ноль. Таким образом, INSERT добавит одну или ноль строк.
Показать ещё 10 комментариев
227

С 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

  • 0
    Это лучше, чем хранимая процедура?
  • 1
    @ FrançoisBeausoleil: вероятность возникновения гонки намного меньше, чем при использовании метода «попробуй / обработай исключение»
Показать ещё 13 комментариев
102

В 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?

  • 0
    Я посмотрел на решение PostgreSQL 9.5, как вы описали выше, потому что я испытывал пробелы в поле автоинкремента в то время, когда в MySQL было ON DUPLICATE KEY UPDATE . Я скачал Postgres 9.5 и внедрил ваш код, но, как ни странно, та же проблема возникает в Postgres: поле последовательного ключа первичного ключа не является последовательным (между вставками и обновлениями есть промежутки). Есть идеи, что здесь происходит? Это нормально? Есть идеи, как избежать такого поведения? Спасибо.
  • 0
    @WM Это в значительной степени присуще операции upsert. Вы должны оценить функцию, которая генерирует последовательность, прежде чем пытаться вставить. Поскольку такие последовательности предназначены для одновременной работы, они освобождаются от обычной семантики транзакции, но даже если они не были генерацией, она не вызывается в субтранзакции и откатывается, она завершается нормально и фиксируется с остальной частью операции. Так что это может произойти даже с «последовательными» реализациями последовательностей. Единственный способ, которым БД может этого избежать, - отложить оценку генерации последовательности до проверки ключа.
Показать ещё 5 комментариев
16

Я искал то же самое, когда я пришел сюда, но отсутствие общей функции "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 и зацикливания отдельных обновлений, удар преформации будет очень небольшим, см. http://archives.postgresql.org/pgsql-performance/2006-04/msg00557.php

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

  • 3
    Вам все еще нужно выполнить это в цикле повтора, и он склонен к гонкам с одновременным DELETE если вы не заблокируете таблицу или не находитесь в SERIALIZABLE изоляции транзакции на PostgreSQL 9.1 или более SERIALIZABLE .
12

Для этого нет простой команды.

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

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

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

  • 1
    При использовании этой опции обязательно убедитесь, что идентификатор возвращается, даже если обновление ничего не делает. Я видел запросы по оптимизации баз данных типа «Обновить таблицу foo set bar = 4, где bar = 4».
8

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

7

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

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

  • проверить скорость...
5

Похоже на наиболее понравившийся ответ, но работает немного быстрее:

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

  • 3
    Это не удастся, если запускаться одновременно в двух сеансах, потому что ни одно обновление не увидит существующую строку, поэтому оба обновления попадут в ноль строк, поэтому оба запроса будут выполнять вставку.
5

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

Мое решение, аналогичное 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
  • 0
    Добро пожаловать в ТАК. Хорошее введение! :-)
  • 1
    Это больше похоже на REPLACE INTO чем INSERT INTO ... ON DUPLICATE KEY UPDATE , что может вызвать проблемы при использовании триггеров. В конечном итоге вы будете запускать удаление и вставлять триггеры / правила, а не обновлять их.
4

Я использую эту функцию 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
  • 1
    Более эффективно сначала выполнить update а затем проверить количество обновленных строк. (См ответ Ахмада)
4

UPDATE вернет количество измененных строк. Если вы используете JDBC (Java), вы можете проверить это значение на 0 и, если никакие строки не были затронуты, вместо этого запустите INSERT. Если вы используете какой-либо другой язык программирования, возможно, количество модифицированных строк может быть получено, проверьте документацию.

Это может быть не так элегантно, но у вас гораздо более простой SQL, который более тривиально использовать из вызывающего кода. Иными словами, если вы пишете десять строк script в PL/PSQL, вы, вероятно, должны иметь unit test того или иного вида только для него.

4
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
3

Изменить: Это не работает должным образом. В отличие от принятого ответа, это приводит к уникальным нарушениям ключа, когда два процесса неоднократно вызывают 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');
  • 1
    Вы можете объединить их в одно утверждение, если используете записываемый CTE. Но, как и большинство решений, опубликованных здесь, это неверное решение и оно не будет выполнено при наличии одновременных обновлений.
3

Для слияния небольших множеств использование вышеуказанной функции в порядке. Однако, если вы объединяете большие объемы данных, я бы предложил посмотреть http://mbk.projects.postgresql.org

Текущая лучшая практика, о которой я знаю, это:

  • КОПИРОВАТЬ новые/обновленные данные в таблицу temp (обязательно, или вы можете сделать INSERT, если стоимость в порядке)
  • Приобретение блокировки [необязательно] (рекомендуется использовать блокировки таблиц, IMO)
  • Merge. (забавная часть)
3

Согласно документации PostgreSQL инструкции INSERT, обработка события ON DUPLICATE KEY не поддерживается. Эта часть синтаксиса является проприетарным расширением MySQL.

  • 0
    @Lucian MERGE также действительно больше работы OLAP; см. stackoverflow.com/q/17267417/398670 для объяснения. Он не определяет семантику параллелизма, и большинство людей, которые используют его для upsert, просто создают ошибки.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню