Вопрос

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

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

Это было полезно?

Решение

PostgreSQL начиная с версии 9.5 имеет РАССТРОЕННЫЙ синтаксис, с О КОНФЛИКТЕ оговорка. со следующим синтаксисом (аналогично 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.Исключения с ОБНОВЛЕНИЕМ / ВСТАВКОЙ

В этом примере используется обработка исключений для выполнения либо ОБНОВЛЕНИЯ, либо ВСТАВКИ, в зависимости от обстоятельств:

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 от a_horse_with_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" уже существует, в противном случае это не имеет никакого эффекта.

Вставка будет успешной только в том случае, если строка с "id=3" еще не существует.

Вы можете объединить эти два параметра в одну строку и запустить их оба с помощью одной инструкции SQL, выполняемой из вашего приложения.Настоятельно рекомендуется запускать их вместе в одной транзакции.

Это работает очень хорошо при запуске изолированно или в заблокированной таблице, но зависит от условий гонки, которые означают, что при одновременной вставке строки все равно может произойти сбой с ошибкой дублирования ключа или может завершиться без вставки строки при одновременном удалении строки.A SERIALIZABLE транзакция в PostgreSQL 9.1 или выше будет надежно обрабатываться ценой очень высокой частоты сбоев сериализации, что означает, что вам придется много раз повторять попытки.Видишь почему upsert такой сложный, в котором этот случай обсуждается более подробно.

Этот подход также подвержен потере обновлений в 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 см. в разделе Как ОБНОВИТЬ (ОБЪЕДИНИТЬ, ВСТАВИТЬ ...ПРИ ДУБЛИРУЮЩЕМ ОБНОВЛЕНИИ) в 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 и зацикливания отдельных обновлений, предварительный результат будет очень небольшим, см. 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');

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

Вот такой более длинная и всеобъемлющая статья на эту тему.

Лично я настроил "правило", прикрепленное к инструкции insert.Допустим, у вас была таблица "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 .Однако незавершенная транзакция продолжится и завершится успешно, и вам просто нужно повторить завершенную транзакцию.

Однако, если постоянно происходит множество вставок, вам захочется установить блокировку таблицы вокруг инструкций insert:ЭКСКЛЮЗИВНАЯ блокировка ОБЩЕЙ СТРОКИ предотвратит любые операции, которые могут привести к вставке, удалению или обновлению строк в вашей целевой таблице.Однако обновления, которые не обновляют уникальный ключ, безопасны, поэтому, если вы никакими операциями не сможете этого сделать, используйте вместо этого рекомендательные блокировки.

Кроме того, команда 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/)

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

Мое решение, аналогичное JWP, заключается в массовом стирании и замене, генерируя запись слияния в вашем приложении.

Это довольно пуленепробиваемый способ, не зависящий от платформы, и поскольку на каждом клиенте никогда не бывает более 20 настроек, это всего лишь 3 вызова базы данных с довольно низкой нагрузкой - вероятно, самый быстрый метод.

Альтернатива обновлению отдельных строк - проверка на наличие исключений с последующей вставкой - или некоторая комбинация - это отвратительный код, медленный и часто ломающийся, потому что (как упоминалось выше) нестандартная обработка исключений SQL меняется от базы данных к базе данных - или даже от выпуска к выпуску.

 #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

В соответствии с Документация 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

Я использую эту функцию слияния

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

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

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

  1. СКОПИРУЙТЕ новые / обновленные данные во временную таблицу (обязательно, или вы можете сделать вставку, если стоимость приемлема)
  2. Получить блокировку [необязательно] (рекомендация предпочтительнее блокировок таблиц, IMO)
  3. Слияние.(самая забавная часть)

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

Это может быть не так элегантно, но у вас есть гораздо более простой SQL, который более тривиален для использования из вызывающего кода.Другими словами, если вы пишете десятистрочный скрипт на 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');
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top