ここでよく聞かれる質問はアップサートの仕方です。これは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構文の代替手段に関するもので、かなり無関係なものになりました時間の経過とともに詳細。私は最終的な答えに取り組んでいます。
これらの技術は、「存在しない場合は挿入し、そうでなければ何もしない」、すなわち「重複キー無視で挿入...」にも有用である。
PostgreSQL 9.5以降では、INSERT ... ON CONFLICT UPDATE
(およびON CONFLICT DO NOTHING
)、つまりupsertがサポートされます。
簡単な説明 。
使用方法については、 マニュアル -特に構文図のconflict_action句、および 説明テキスト を参照してください。
以下に示す9.4以前のソリューションとは異なり、この機能は競合する複数の行で機能し、排他ロックまたは再試行ループを必要としません。
機能を追加するコミットはこちら および 開発に関する議論はこちら .
9.5を使用していて、下位互換性が必要ない場合は、今すぐ読むのをやめることができます。
PostgreSQLにはUPSERT
(またはMERGE
)機能が組み込まれていないため、同時使用に直面して効率的に実行することは非常に困難です。
一般に、2つのオプションから選択する必要があります。
多数の接続が同時に挿入を実行しようとする場合、再試行ループで個々の行のアップサートを使用するのが妥当なオプションです。
PostgreSQLのドキュメントには、データベース内のループでこれを実行できる便利な手順が含まれています 。ほとんどの素朴なソリューションとは異なり、更新の損失や競合の挿入を防ぎます。 READ COMMITTED
モードでのみ動作し、トランザクションで行うのがそれだけである場合にのみ安全です。トリガーまたはセカンダリ一意キーが一意違反を引き起こす場合、関数は正しく機能しません。
この戦略は非常に非効率的です。実用的な場合は常に、作業をキューに入れ、代わりに以下に説明するように一括アップサートを実行する必要があります。
この問題に対する解決策の多くは、ロールバックを考慮していないため、更新が不完全になります。 2つのトランザクションが互いに競合します。それらのうちの1つは正常にINSERT
s;もう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
MERGE
はどうですか?SQL標準のMERGE
は、実際には並行性のセマンティクスが不十分に定義されており、最初にテーブルをロックせずにアップサートするのには適していません。
これは、データのマージに非常に便利なOLAPステートメントですが、同時実行に対して安全なアップサートには実際には有効なソリューションではありません。他のDBMSを使用してアップサートにMERGE
を使用する人には多くのアドバイスがありますが、実際は間違っています。
INSERT ... ON DUPLICATE KEY UPDATE
MERGE
(ただし、MERGE
の問題については上記を参照)MERGE
(ただし、MERGE
の問題については上記を参照)私は、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 $$;
この解決策は テーブルの行の削除がない場合にのみ適用できることに注意してください 。
私はこの解決策の効率については知りませんが、それは私には十分合理的なようです。
これが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;
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でテスト済み
上記の大きな記事は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)
この質問 は終了したので、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
を超える利点は、挿入時に関係、エラーチェックなどを処理できることです( 一括操作とは異なります。 ).