Как UPSERT (MERGE, INSERT ... ON DUPLICATE UPDATE) в PostgreSQL?
очень часто задаваемый вопрос здесь-как сделать upsert, который является тем, что MySQL называет INSERT ... ON DUPLICATE UPDATE
и стандартные поддержки как часть MERGE
операции.
учитывая, что PostgreSQL не поддерживает его напрямую (до pg 9.5), как вы это делаете? Рассмотрим следующее:
CREATE TABLE testtable (
id integer PRIMARY KEY,
somedata text NOT NULL
);
INSERT INTO testtable (id, somedata) VALUES
(1, 'fred'),
(2, 'bob');
теперь представьте, что вы хотите "upsert" кортежи (2, 'Joe')
, (3, 'Alan')
, поэтому новое содержание таблицы будет такой:
(1, 'fred'),
(2, 'Joe'), -- Changed value of existing tuple
(3, 'Alan') -- Added new tuple
вот, о чем говорят люди, когда обсуждение upsert
. Важно, что любой подход должен быть безопасно при наличии нескольких транзакций, работающих на одной таблице - либо используя явную блокировку, либо иным образом защищаясь от результирующих условий гонки.
эта тема подробно обсуждается в вставить, при дублировании обновления в PostgreSQL?, но это касается альтернатив синтаксису MySQL, и со временем он вырос довольно много несвязанных деталей. Я работаю над определением ответы.
эти методы также полезны для "вставить, если не существует, в противном случае ничего не делать", т. е. "вставить ... на дубликат ключа игнорировать".
6 ответов
9.5 и новее:
поддержка PostgreSQL 9.5 и новее INSERT ... ON CONFLICT UPDATE
(и ON CONFLICT DO NOTHING
), то есть upsert.
сравнению с ON DUPLICATE KEY UPDATE
.
для использования см. руководство - а конкретно conflict_action предложение в синтаксической диаграмме и пояснительным текстом.
в отличие от решений для 9.4 и старше, которые даются ниже эта функция работает с несколькими конфликтующими строками и не требует исключительной блокировки или цикла повтора.
фиксация добавления функции здесь и дискуссия вокруг его развития здесь.
если вы находитесь на 9.5 и не должны быть обратно-совместимыми вы можете остановить чтение сейчас.
9.4 и старше:
PostgreSQL не имеет встроенного UPSERT
(или MERGE
) объект, и делать это эффективно при одновременном использовании очень сложно.
в этой статье обсуждается проблема в полезных деталях.
в общем, вы должны выбрать один из двух вариантов:
- отдельные операции вставки / обновления в цикле повтора; или
- блокировка таблицы и выполнение пакетного слияния
индивидуальный цикл повтора строк
использование отдельной строке upserts в цикле повтора является разумным вариантом, если вы хотите, чтобы многие соединения одновременно пытались выполнить вставки.
документация PostgreSQL содержит полезную процедуру, которая позволит вам сделать это в цикле внутри базы данных. Он защищает от потерянных обновлений и вставляет гонки, в отличие от большинства наивных решений. Он будет работать только в READ COMMITTED
режим и безопасен только в том случае, если это единственное, что вы делаете в транзакции. Функция не будет работать правильно, если триггеры или вторичные уникальные ключи вызывают уникальные нарушения.
эта стратегия очень неэффективна. Всякий раз, когда это практично, вы должны стоять в очереди и делать массовый upsert, как описано ниже.
многие попытки решения этой проблемы не учитывают откаты, поэтому они приводят к неполным обновлениям. Две сделки соревнуются друг с другом; одна из них успешно INSERT
s; другой получает дубликат ключевой ошибки и делает . The UPDATE
блоки ждем INSERT
откат или фиксация. Когда он откатывается назад,UPDATE
состояние проверить соответствует ноль строк, так хоть UPDATE
commits он на самом деле не сделал upsert, который вы ожидали. Вы должны проверить количество строк результата и повторить попытку, где это необходимо.
некоторые попытки решения также не учитывают избранные расы. Если вы попробуете очевидное и простое:
-- THIS IS WRONG. DO NOT COPY IT. It's an EXAMPLE.
BEGIN;
UPDATE testtable
SET somedata = 'blah'
WHERE id = 2;
-- Remember, this is WRONG. Do NOT COPY IT.
INSERT INTO testtable (id, somedata)
SELECT 2, 'blah'
WHERE NOT EXISTS (SELECT 1 FROM testtable WHERE testtable.id = 2);
COMMIT;
затем, когда два запуска одновременно есть несколько режимов отказа. Одно это уже обсуждалась проблема с повторной проверкой обновлений. Другой-где оба UPDATE
в то же время, соответствие нулевым строкам и продолжение. Тогда они оба делают EXISTS
тест, который происходит до the INSERT
. Оба получают нулевые строки, поэтому оба делают INSERT
. Одна ошибка с дубликатом ключа.
вот почему вам нужен цикл повторной попытки. Вы можете подумать, что вы можете предотвратить дублирование ключевых ошибок или потерянных обновлений с помощью clever SQL, но вы не можете. Вам нужно проверить количество строк или обработать дублируйте ключевые ошибки (в зависимости от выбранного подхода) и повторите попытку.
пожалуйста, не сворачивайте свое собственное решение для этого. Как и с очередью сообщений, это, вероятно, неправильно.
Bulk upsert с замком
иногда вы хотите сделать массовый upsert, где у вас есть новый набор данных, который вы хотите объединить в более старый существующий набор данных. Это vastly более эффективно, чем отдельные строки upserts и должны быть предпочтительными всякий раз, когда практическое.
в этом случае вы обычно выполняете следующий процесс:
CREATE
aTEMPORARY
столCOPY
или bulk-вставьте новые данные в таблицу tempLOCK
целевой таблицеIN EXCLUSIVE MODE
. Это позволяет другим транзакциямSELECT
, но не вносить никаких изменений в таблицу.сделать
UPDATE ... FROM
существующих записей с использованием значений в таблица Temp;сделать
INSERT
строк, которые еще не существуют в целевой таблице;COMMIT
, освободив замок.
например, для примера, приведенного в вопросе, используя многозначное INSERT
чтобы заполнить временную таблицу:
BEGIN;
CREATE TEMPORARY TABLE newvals(id integer, somedata text);
INSERT INTO newvals(id, somedata) VALUES (2, 'Joe'), (3, 'Alan');
LOCK TABLE testtable IN EXCLUSIVE MODE;
UPDATE testtable
SET somedata = newvals.somedata
FROM newvals
WHERE newvals.id = testtable.id;
INSERT INTO testtable
SELECT newvals.id, newvals.somedata
FROM newvals
LEFT OUTER JOIN testtable ON (testtable.id = newvals.id)
WHERE testtable.id IS NULL;
COMMIT;
статьи
- страница Вики UPSERT
- UPSERTisms в Postgres
- вставить, при дублировании обновления в PostgreSQL?
- http://petereisentraut.blogspot.com/2010/05/merge-syntax.html
- Upsert с транзакцией
- является ли SELECT или INSERT функцией, склонной к условиям гонки?
- среда SQL
MERGE
на вики PostgreSQL - самый идиоматический способ реализации UPSERT в Postgresql в настоящее время
а как же MERGE
?
SQL-standard MERGE
на самом деле имеет плохо определенную семантику параллелизма и не подходит для upserting без блокировки таблицы сначала.
это действительно полезный оператор OLAP для слияния данных, но на самом деле это не полезное решение для безопасного параллелизма upsert. Есть много советов людям, использующим другие СУБД для использования MERGE
для вставки, но это на самом деле неправильный.
Другие DBs:
INSERT ... ON DUPLICATE KEY UPDATE
в MySQL-
MERGE
от MS SQL Server (но см. выше оMERGE
проблемы) -
MERGE
от Oracle (но см. выше оMERGE
проблемы)
Я пытаюсь внести свой вклад в другое решение для одной проблемы вставки с версиями PostgreSQL до 9.5. Идея состоит в том, чтобы просто попытаться выполнить сначала вставку, и в случае, если запись уже присутствует, обновить ее:
do $$
begin
insert into testtable(id, somedata) values(2,'Joe');
exception when unique_violation then
update testtable set somedata = 'Joe' where id = 2;
end $$;
обратите внимание, что это решение может быть применено только если нет удаления строк таблицы.
Я не знаю об эффективности этого решения, но мне кажется, что это достаточно разумно.
вот несколько примеров для insert ... on conflict ...
( pg 9.5+):
"вставить", на конфликте - ничего.
insert into dummy(id, name, size) values(1, 'new_name', 3) on conflict do nothing;
"вставить", на конфликте - do update укажите конфликта через колонки.
insert into dummy(id, name, size) values(1, 'new_name', 3) on conflict(id) do update set name = 'new_name', size = 3;
"вставить", на конфликте - do update укажите конфликта через ограничение имя.
insert into dummy(id, name, size) values(1, 'new_name', 3) on conflict on constraint dummy_pkey do update set name = 'new_name', size = 4;
WITH UPD AS (UPDATE TEST_TABLE SET SOME_DATA = 'Joe' WHERE ID = 2
RETURNING ID),
INS AS (SELECT '2', 'Joe' WHERE NOT EXISTS (SELECT * FROM UPD))
INSERT INTO TEST_TABLE(ID, SOME_DATA) SELECT * FROM INS
протестировано на Postgresql 9.3
SQLAlchemy upsert для Postgres >=9.5
поскольку большой пост выше охватывает множество различных подходов SQL для версий Postgres (не только не 9.5, как в вопросе), я хотел бы добавить, как это сделать в SQLAlchemy, если вы используете Postgres 9.5. Вместо реализации собственного upsert вы также можете использовать функции SQLAlchemy (которые были добавлены в SQLAlchemy 1.1). Лично я бы рекомендовал использовать их, если это возможно. Не только из-за удобства, но и потому, что позволяет PostgreSQL обрабатывать любые условия гонки, которые могут возникнуть.
кросс-постинг из другого ответа, который я дал вчера (https://stackoverflow.com/a/44395983/2156909)
SQLAlchemy поддерживает ON CONFLICT
теперь с двумя методами on_conflict_do_update()
и on_conflict_do_nothing()
:
копировать из документации:
from sqlalchemy.dialects.postgresql import insert
stmt = insert(my_table).values(user_email='a@b.com', data='inserted data')
stmt = stmt.on_conflict_do_update(
index_elements=[my_table.c.user_email],
index_where=my_table.c.user_email.like('%@gmail.com'),
set_=dict(data=stmt.excluded.data)
)
conn.execute(stmt)
С этот вопрос был закрыт, я публикую здесь, как вы это делаете с помощью SQLAlchemy. Через рекурсию он повторяет массовую вставку или обновление для combat гонки и ошибки валидации.
сначала импорт
import itertools as it
from functools import partial
from operator import itemgetter
from sqlalchemy.exc import IntegrityError
from app import session
from models import Posts
теперь пара вспомогательных функций
def chunk(content, chunksize=None):
"""Groups data into chunks each with (at most) `chunksize` items.
https://stackoverflow.com/a/22919323/408556
"""
if chunksize:
i = iter(content)
generator = (list(it.islice(i, chunksize)) for _ in it.count())
else:
generator = iter([content])
return it.takewhile(bool, generator)
def gen_resources(records):
"""Yields a dictionary if the record's id already exists, a row object
otherwise.
"""
ids = {item[0] for item in session.query(Posts.id)}
for record in records:
is_row = hasattr(record, 'to_dict')
if is_row and record.id in ids:
# It's a row but the id already exists, so we need to convert it
# to a dict that updates the existing record. Since it is duplicate,
# also yield True
yield record.to_dict(), True
elif is_row:
# It's a row and the id doesn't exist, so no conversion needed.
# Since it's not a duplicate, also yield False
yield record, False
elif record['id'] in ids:
# It's a dict and the id already exists, so no conversion needed.
# Since it is duplicate, also yield True
yield record, True
else:
# It's a dict and the id doesn't exist, so we need to convert it.
# Since it's not a duplicate, also yield False
yield Posts(**record), False
и, наконец, функция upsert
def upsert(data, chunksize=None):
for records in chunk(data, chunksize):
resources = gen_resources(records)
sorted_resources = sorted(resources, key=itemgetter(1))
for dupe, group in it.groupby(sorted_resources, itemgetter(1)):
items = [g[0] for g in group]
if dupe:
_upsert = partial(session.bulk_update_mappings, Posts)
else:
_upsert = session.add_all
try:
_upsert(items)
session.commit()
except IntegrityError:
# A record was added or deleted after we checked, so retry
#
# modify accordingly by adding additional exceptions, e.g.,
# except (IntegrityError, ValidationError, ValueError)
db.session.rollback()
upsert(items)
except Exception as e:
# Some other error occurred so reduce chunksize to isolate the
# offending row(s)
db.session.rollback()
num_items = len(items)
if num_items > 1:
upsert(items, num_items // 2)
else:
print('Error adding record {}'.format(items[0]))
вот как вы его используете
>>> data = [
... {'id': 1, 'text': 'updated post1'},
... {'id': 5, 'text': 'updated post5'},
... {'id': 1000, 'text': 'new post1000'}]
...
>>> upsert(data)
преимущество над bulk_save_objects
это то, что он может обрабатывать отношения, проверку ошибок и т. д. при вставке (в отличие от массовые операции).