web-dev-qa-db-ja.com

PostgreSQLでUPSERT(MERGE、INSERT ... ON DUPLICATE UPDATE)をするにはどうすればいいですか?

ここでよく聞かれる質問はアップサートの仕方です。これは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');

今度は、タプル(2, 'Joe')(3, 'Alan')を "upsert"したいとします。そのため、新しいテーブルの内容は次のようになります。

(1, 'fred'),
(2, 'Joe'),    -- Changed value of existing Tuple
(3, 'Alan')    -- Added new Tuple

それがupsertについて議論するときに人々が話していることです。決定的に、どんなアプローチも同じテーブルで作業する複数のトランザクションの存在下で安全でなければなりません - 明示的なロックを使用するか、または他の方法で結果の競合条件に対して防御することによって。

このトピックは 挿入、PostgreSQLでの重複更新? で広範囲に説明されていますが、それはMySQL構文の代替手段に関するもので、かなり無関係なものになりました時間の経過とともに詳細。私は最終的な答えに取り組んでいます。

これらの技術は、「存在しない場合は挿入し、そうでなければ何もしない」、すなわち「重複キー無視で挿入...」にも有用である。

228
Craig Ringer

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)機能が組み込まれていないため、同時使用に直面して効率的に実行することは非常に困難です。

この記事では、問題について詳しく説明しています

一般に、2つのオプションから選択する必要があります。

  • 再試行ループでの個々の挿入/更新操作。または
  • テーブルのロックとバッチマージの実行

個々の行の再試行ループ

多数の接続が同時に挿入を実行しようとする場合、再試行ループで個々の行のアップサートを使用するのが妥当なオプションです。

PostgreSQLのドキュメントには、データベース内のループでこれを実行できる便利な手順が含まれています 。ほとんどの素朴なソリューションとは異なり、更新の損失や競合の挿入を防ぎます。 READ COMMITTEDモードでのみ動作し、トランザクションで行うのがそれだけである場合にのみ安全です。トリガーまたはセカンダリ一意キーが一意違反を引き起こす場合、関数は正しく機能しません。

この戦略は非常に非効率的です。実用的な場合は常に、作業をキューに入れ、代わりに以下に説明するように一括アップサートを実行する必要があります。

この問題に対する解決策の多くは、ロールバックを考慮していないため、更新が不完全になります。 2つのトランザクションが互いに競合します。それらのうちの1つは正常にINSERTs;もう1つは重複キーエラーを受け取り、代わりに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;

その後、2つを同時に実行すると、いくつかの障害モードがあります。 1つは、更新の再確認に関する既に説明した問題です。もう1つは、UPDATEが同時に、ゼロ行に一致して継続する場合です。その後、両方ともEXISTSテストを実行します。これはbeforeINSERTで発生します。両方ともゼロ行を取得するため、両方ともINSERTを実行します。 1つは重複キーエラーで失敗します。

これが、再試行ループが必要な理由です。巧妙なSQLで重複キーエラーや更新の喪失を防ぐことができると思うかもしれませんが、できません。行数を確認するか、選択したアプローチに応じて重複キーエラーを処理し、再試行する必要があります。

このための独自のソリューションをロールしないでください。メッセージのキューイングと同様に、おそらく間違っています。

ロック付き一括アップサート

古いデータセットにマージしたい新しいデータセットがある場合、一括アップサートを行いたい場合があります。これはvastly個々の行のアップサートよりも効率的であり、実用的であればいつでも推奨されます。

この場合、通常は次のプロセスに従います。

  • CREATE a TEMPORARYテーブル

  • 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ステートメントですが、同時実行に対して安全なアップサートには実際には有効なソリューションではありません。他のDBMSを使用してアップサートにMERGEを使用する人には多くのアドバイスがありますが、実際は間違っています。

その他のDB:

365
Craig Ringer

私は、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 $$;

この解決策は テーブルの行の削除がない場合にのみ適用できることに注意してください

私はこの解決策の効率については知りませんが、それは私には十分合理的なようです。

28
Renzo

これがinsert ... on conflict ...pg 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;

12
Eric Wang
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でテスト済み

3
aristar

PostgresのSQLAlchemyアップサート> = 9.5

上記の大きな記事はPostgresバージョンに対する多くの異なるSQLアプローチ(質問のように9.5以外のもの)を網羅しているので、Postgres 9.5を使用している場合はSQLAlchemyでそれを行う方法を追加したいと思います。独自のupsertを実装する代わりに、SQLAlchemyの関数(SQLAlchemy 1.1で追加されたもの)を使用することもできます。個人的には、できればこれらを使用することをお勧めします。利便性だけでなく、PostgreSQLがあらゆる競合状態を処理できるようにするためです。

昨日行った別の回答からのクロス投稿( https://stackoverflow.com/a/44395983/2156909

SQLAlchemyはon_conflict_do_update()on_conflict_do_nothing()の2つのメソッドでON CONFLICTをサポートします。

ドキュメントからコピーする:

from sqlalchemy.dialects.postgresql import insert

stmt = insert(my_table).values(user_email='[email protected]', 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/dialections/postgresql.html?highlight=conflict#insert-on-conflict-upsert

3
P.R.

この質問 は終了したので、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

2つのヘルパー関数

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 を超える利点は、挿入時に関係、エラーチェックなどを処理できることです( 一括操作とは異なります。 ).

0
reubano