Як UPSERT (MERGE, INSERT… ON DUPLICATE UPDATE) в PostgreSQL?


267

Тут дуже часто задається питанням, як зробити поновлення, яке називає MySQL INSERT ... ON DUPLICATE UPDATEі стандарт підтримує в рамках MERGEоперації.

Зважаючи на те, що PostgreSQL не підтримує його безпосередньо (до сторінки 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. Принципово важливо, що будь-який підхід повинен бути безпечним за наявності кількох транзакцій, що працюють за однією таблицею - або за допомогою явного блокування, або іншим чином захищаючись від результатів гонки.

Ця тема широко обговорюється в Insert, при повторному оновленні в PostgreSQL? , але це стосується альтернатив синтаксису MySQL, і це з часом виросло неабияк непов'язаних деталей. Я працюю над остаточними відповідями.

Ці методи також корисні для "вставки, якщо її немає, інакше нічого не робіть", тобто "вставляти ... при повторному ігноруванні дублюючого ключа".



8
@MichaelHampton метою тут було створити остаточну версію, яку не плутає багато застарілих відповідей - і заблоковано, тому ніхто не може нічого з цим зробити. Я не згоден із близьким голосуванням.
Крейг Рінгер

Чому б тоді це незабаром застаріло - і заблокувало, так що ніхто нічого не міг із цим зробити.
Майкл Хемптон

2
@MichaelHampton Якщо ви стурбовані, можливо, ви можете позначити той, до якого ви пов’язані, і попросити його розблокувати, щоб його можна було почистити, то ми можемо це об'єднати. Мені просто нудно, що я маю єдине очевидне закриття - as-dup для upsert є таким заплутаним і неправильним безладом.
Крейг Рінгер

1
Це питання не заблоковано!
Майкл Хемптон

Відповіді:


396

9.5 і новіші:

Підтримка PostgreSQL 9.5 та новіших версій INSERT ... ON CONFLICT UPDATEON CONFLICT DO NOTHING), тобто upsert.

Порівняння зON DUPLICATE KEY UPDATE .

Швидке пояснення .

Про використання див . Посібник - конкретно застереження про конфлікт у діаграмі синтаксису та пояснювальний текст .

На відміну від рішень для версій 9.4 і старших, які наведені нижче, ця функція працює з декількома конфліктуючими рядками, і вона не вимагає виключного блокування або повторного циклу.

Здійснення функції, що додає функцію, є тут, а дискусія навколо її розвитку - тут .


Якщо у вас 9,5 і вам не потрібно бути сумісними назад, ви можете перестати читати зараз .


9.4 і старші:

PostgreSQL не має жодного вбудованого UPSERT(або MERGE) засобу, і зробити це ефективно в умовах одночасного використання дуже складно.

Ця стаття обговорює проблему корисно докладно .

Загалом ви повинні вибрати один з двох варіантів:

  • Індивідуальні операції вставки / оновлення в циклі повтору; або
  • Блокування столу і виконання пакетного злиття

Індивідуальна петля для повторного введення рядків

Використання окремих наборів рядків у циклі повторної спроби є розумним варіантом, якщо ви хочете, щоб багато з'єднань одночасно намагалися виконувати вставки.

Документація PostgreSQL містить корисну процедуру, яка дозволить вам зробити це в циклі всередині бази даних . Він захищає від втрачених оновлень і вставляє перегони, на відміну від більшості наївних рішень. Він працюватиме лише в READ COMMITTEDрежимі і безпечний лише в тому випадку, якщо це єдине, що ви робите в операції. Функція не буде працювати належним чином, якщо тригери або вторинні унікальні ключі викликають унікальні порушення.

Ця стратегія дуже неефективна. Кожен раз, коли ви практичні, вам слід працювати в черзі та робити об'ємні зміни, як описано нижче.

Багато спроб вирішення цієї проблеми не враховують зворотних наслідків, тому вони призводять до неповних оновлень. Дві транзакції гоняться між собою; один з них успішно INSERTs; інший отримує дублюючу помилку ключа і робить UPDATEзамість цього. У UPDATEблоках чекаючи INSERTвідкат або фіксації. Коли він відкочується назад, UPDATEумова повторної перевірки відповідає нульовим рядкам, тож навіть якщо ці UPDATEзобов'язання насправді не виконали очікувані зміни. Ви повинні перевірити кількість підсумкових рядків і повторно спробувати, де це необхідно.

Деякі спроби рішення також не враховують раси SELECT. Якщо ви спробуєте очевидне і просте:

-- 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випробування, яке відбувається доINSERT того . Обидва отримують нульові ряди, тому обидва роблять INSERT. Не вдалося отримати помилку з повторюваним ключем.

Ось чому вам потрібна повторна спроба циклу. Ви можете подумати, що за допомогою розумного SQL можна попередити повторювані помилки ключа або втрачені оновлення, але ви не можете. Потрібно перевірити кількість рядків або обробити повторювані помилки ключа (залежно від обраного підходу) і повторити спробу.

Будь ласка, не вдавайте для цього власне рішення. Як і в черзі повідомлень, це, мабуть, неправильно.

Об'ємний вгору з замком

Іноді ви хочете зробити об'ємний перегляд, де у вас є новий набір даних, який ви хочете об'єднати в старий існуючий набір даних. Це набагато ефективніше, ніж окремі рядки рядів, і слід віддавати перевагу, коли це можливо.

У цьому випадку ви зазвичай дотримуєтесь наступного процесу:

  • CREATETEMPORARYстіл

  • COPY або вставте нові дані в таблицю темп

  • LOCKцільова таблиця IN EXCLUSIVE MODE. Це дозволяє іншим транзакціям SELECT, але не вносить жодних змін до таблиці.

  • Зробіть UPDATE ... FROMіснуючі записи, використовуючи значення в таблиці темп;

  • Зробіть 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;

Пов'язане читання

Про що MERGE?

Стандарт SQL MERGEнасправді має погано визначену семантику одночасності і не підходить для оновлення без попереднього блокування таблиці.

Це дійсно корисна заява OLAP для об'єднання даних, але насправді це не корисне рішення для безпечного покращення конкуренції. Людям, які використовують інші СУБД, використовуються MERGEдля оновлення, є багато порад , але насправді це неправильно.

Інші БД:


Чи є можливим значення для видалення з newvals, а не фільтрування INSERT? Наприклад, з upd AS (ОНОВЛЕННЯ ... ПОВЕРНЕННЯ newvals.id) ВИДАЛИТИ З newvals ВИКОРИСТОВУВАННЯ upd ДЕ newvals.id = upd.id, а потім голий ВСТАВЛЯЄТЬСЯ в тестовий вибір SELECT * OF newvals? Моя ідея з цього приводу: замість фільтрування двічі в INSERT (для приєднання / де і для унікального обмеження) повторно використовувати результати перевірки існування з UPDATE, які вже є в оперативній пам'яті, і можуть бути набагато меншими. Це може бути виграшним, якщо кілька рядків і / або newvals будуть набагато меншими, ніж тестові.
Gunnlaugur Briem

1
Є ще невирішені питання, а для інших постачальників не зрозуміло, що працює, а що ні. 1. Як зазначалося, циклічне рішення Postgres не працює у випадку декількох унікальних ключів. 2. Ключ on duplicate для mysql також не працює для декількох унікальних ключів. 3. Чи працюють інші рішення для MySQL, SQL Server та Oracle, розміщені вище? Чи можливі винятки в тих випадках, і чи потрібно нам робити цикл?
дан б

@danb Це справді лише про PostgreSQL. Рішення для перехресних постачальників немає. Рішення для PostgreSQL не працює для декількох рядків, на жаль, ви повинні робити одну транзакцію в ряд. "Рішення", які використовуються MERGEдля SQL Server та Oracle, є невірними та схильними до перегонів, як зазначено вище. Вам потрібно буде вивчити кожну СУБД спеціально, щоб дізнатися, як з ними працювати, я дійсно можу запропонувати лише поради щодо PostgreSQL. Єдиним способом зробити безпечний багаторядковий оновлення на PostgreSQL буде, якщо на основний сервер буде додана підтримка нативного оновлення.
Крейг Рінгер

Навіть для PostGresQL рішення не працює в тому випадку, коли в таблиці є кілька унікальних ключів (оновлення лише одного рядка). У цьому випадку вам потрібно вказати, який ключ оновлюється. Можливо, існує рішення між постачальниками, наприклад, використовуючи jdbc.
дан b

2
Postgres тепер підтримує UPSERT - git.postgresql.org/gitweb/…
Кріс

32

Я намагаюся внести ще одне рішення для проблеми єдиної вставки з попередньо 9.5 версіями PostgreSQL. Ідея полягає у тому, щоб спробувати виконати спочатку вставлення, а якщо запис вже є, оновити його:

do $$
begin 
  insert into testtable(id, somedata) values(2,'Joe');
exception when unique_violation then
  update testtable set somedata = 'Joe' where id = 2;
end $$;

Зауважте, що це рішення можна застосовувати лише у тому випадку, якщо немає видалень рядків таблиці .

Я не знаю про ефективність цього рішення, але мені це здається досить розумним.


3
Дякую, саме це я шукав. Не можу зрозуміти, чому це було так важко знайти.
isapir

4
Так. Це спрощення спрацьовує, якщо і тільки якщо немає делетів.
Крейг Рінгер

@CraigRinger Чи можете ви пояснити, що саме відбудеться, якщо були делети?
турбанов

@turbanoff Вставка може не вдатися, оскільки запис вже є, вона одночасно видаляється, а оновлення впливає на нульові рядки, оскільки рядок було видалено.
Крейг Рінгер

@CraigRinger Отже. Видалення відбувається одночасно . Які можливі outways , якщо це є відмінно працює? Якщо видалення працює одночасно - то воно може бути виконане відразу після нашого блоку. Що я намагаюся сказати - якщо у нас є одночасне видалення - тоді цей код прокидається таким же чином, як і власнеinsert on update
turbanoff

28

Ось кілька прикладів для insert ... on conflict ...( стор. 9.5+ ):

  • Вставте, на конфлікт - нічого не робіть .
    insert into dummy(id, name, size) values(1, 'new_name', 3)
    on conflict do nothing;`  
  • Вставте, при оновленні конфлікту - вкажіть ціль конфлікту за допомогою стовпця .
    insert into dummy(id, name, size) values(1, 'new_name', 3)
    on conflict(id)
    do update set name = 'new_name', size = 3;  
  • Вставте, при оновленні конфлікту - вкажіть ціль конфлікту через ім'я обмеження .
    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;

чудова відповідь - питання: чому або в якій ситуації слід використовувати цільову специфікацію через ім'я стовпця або обмеження? Чи є перевага / недолік для різних випадків використання?
Натан Бентон

1
@NathanBenton Я думаю, що принаймні є дві різниці: (1) ім'я стовпця задається програмістом, тоді як ім'я обмеження може бути або задане програмістом, або сформоване з бази даних відповідно до імен таблиці / стовпців. (2) кожен стовпець може мати кілька обмежень. З огляду на це, саме від вашого випадку вибирати, який саме використовувати.
Ерік Ван

8

SQLAlchemy upsert for Postgres> = 9.5

Оскільки велика публікація вище охоплює багато різних підходів SQL для версій Postgres (не тільки не 9.5, як у питанні), я хотів би додати, як це зробити в SQLAlchemy, якщо ви використовуєте Postgres 9.5. Замість того, щоб реалізувати власну програму, ви також можете використовувати функції 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)

http://docs.sqlalchemy.org/en/latest/dialects/postgresql.html?highlight=conflict#insert-on-conflict-upsert


4
Python та SQLAlchemy не згадуються у питанні.
Олександр Ємельянов

Я часто використовую Python у написаних рішеннях. Але я не заглядав у SQLAlchemy (або знав про це). Це здається елегантним варіантом. Дякую. Якщо він перевіриться, я подару це моїй організації.
Роберт

3
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


@CraigRinger: Ви могли б детальніше зупинитися на цьому? не cte атомний?
parisni

2
@parisni Ні. Кожен термін CTE отримує власний знімок, якщо він виконує запис. Також немає блокування предикатів, що виконується на рядках, які не були знайдені, тому їх все одно можна створити одночасно іншим сеансом. Якщо ви використовували SERIALIZABLEізоляцію, ви отримали б перерву з помилкою серіалізації, інакше ви, мабуть, отримаєте унікальне порушення. Не винаходити верхівку, винахід буде неправильним. Використовуйте INSERT ... ON CONFLICT .... Якщо ваш PostgreSQL занадто старий, оновіть його.
Крейг Рінгер

@CraigRinger INSERT ... ON CLONFLICT ...не призначений для масового завантаження. З вашого посту, LOCK TABLE testtable IN EXCLUSIVE MODE;всередині CTE - це спосіб вирішити питання про отримання атомних речей. Немає ?
parisni

@parisni Це не призначено для масового завантаження? Хто каже? postgresql.org/docs/current/sql-insert.html#SQL-ON-CONFLICT . Звичайно, це набагато повільніше, ніж велике завантаження без поведінки, схоже на прихильність, але це очевидно, і це буде так, незалежно від того, що ви робите. Це набагато швидше, ніж використання субтранзакцій, це точно. Найшвидший підхід полягає в тому, щоб заблокувати цільову таблицю, а потім insert ... where not exists ..., звичайно, зробити чи подібне.
Крейг Рінгер

1

Оскільки це питання було закрито, я публікую тут інформацію про те, як це зробити за допомогою SQLAlchemy. Через рекурсію він витягує об'ємну вставку або оновлення для боротьби з умовами перегонів та помилками валідації.

По-перше, імпорт

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

І нарешті функція втілення

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полягає в тому, що вона може обробляти відносини, перевірку помилок тощо на вкладиші (на відміну від масових операцій ).


На мене це також виглядає неправильно. Що робити, якщо паралельний сеанс вставляє рядок після того, як ви зіберете свій список ідентифікаторів? Або видаляє його?
Крейг Рінгер

хороший момент @CraigRinger Я роблю щось подібне до цього, але виконую лише 1 сеанс. Який найкращий спосіб обробити кілька сеансів? Можливо, транзакція?
reubano

Операції не є магічним рішенням усіх проблем, що стосуються одночасності. Ви можете використовувати SERIALIZABLE транзакції та вирішувати збої серіалізації, але це повільно. Вам потрібна робота з помилками та цикл для повторної спроби. Дивіться мою відповідь та розділ "пов'язане читання" в ній.
Крейг Рінгер

@CraigRinger gotcha. Я фактично реалізував цикл повторного повторення у власному випадку через інші помилки перевірки. Я відповідно оновлю цю відповідь.
reubano
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.