次の定義のテーブルがあるとします。
CREATE TABLE public.positions
(
id serial,
latitude numeric(18,12),
longitude numeric(18,12),
updated_at timestamp without time zone
)
そして、このテーブルには50,000行あります。テスト目的で、次のような更新を実行します。
update positions
set updated_at = now()
where latitude between 234.12 and 235.00;
このステートメントは、(この特定のデータセットの)50,000から1,000行を更新します。
このようなクエリを30の異なるスレッド*で実行すると、MySQL innodbは成功し、PostgreSQLは多くのデッドロックで失敗します。
どうして?
* MySQL innodbとPostgresの最新バージョンを比較しています。これは同時更新のケースです。生産ケース:5000株が常に入手可能な最新の価格で更新されていると想像してください。
2013年3月13日 berはMySQLからPostgrsSQLに切り替えました 。
驚いたことに、その情事はあまり長続きしませんでした。
2016年6月26日 berはPostgreSQLからMySQLに切り替えました 。
なぜ顔について???
UPDATE
の実行は、PostgreSQLでは驚くほどマイクロマネージドです。
PostgreSQLには2つのDDLシステム識別子があり、正しく理解する必要があります
ctid
:テーブル内の行バージョンの物理的な場所。 ctidを使用して行バージョンをすばやく見つけることができますが、VACUUM FULL
によって行が更新または移動されると、行のctid
が変更されることに注意してください。したがって、ctid
は長期的な行識別子としては役に立ちません。論理行を識別するには、OID、またはさらに優れたユーザー定義のシリアル番号を使用する必要があります。oid
:行のオブジェクト識別子(オブジェクトID)。この列は、WITH OIDSを使用してテーブルが作成された場合、またはdefault_with_oids構成変数がそのときに設定された場合にのみ存在します。この列はtypeoid(列と同じ名前)です。タイプの詳細については オブジェクト識別子タイプに関するPostgreSQLドキュメント を参照してください。Ctidに関しては、UPDATE
(_DELETE
/INSERT
from a physical立場)またはVACUUM FULL
を実行すると、行のctidが変更されることに注意してください。これは、多くのインデックスを持つテーブルの前兆ではありません。どうして ?この事実は、最近(2016年7月26日)、Uberのデータエンジニアによって発見されました。
次の図に注意してください(Uber Engineering提供)
PostgreSQLはPRIMARY KEY
を一意でないインデックスと結合的に結合しません。インデックスはctid
によって行を参照するため、単純なUPDATE
(インデックス付けされていない列でも)はctid
を変更し、その結果、変更された行を参照するテーブル内のすべてのインデックスのctid
を書き換える必要があります。 これは新しいものではありません。 PostgreSQLはこれを常にdesignで行っています。
したがって、1000行でUPDATE
を実行すると、1000 DELETEs
および1000 INSERTs
が実行されます。すでに述べたように、テーブルにアタッチされたすべてのインデックスは、古い行のctid
の値を置き換えて、BTREEインデックスエントリに新しい行のctid
の値を書き込む必要があります。テーブルにインデックスがないため、表示されているデッドロックを生成するDELETEs
およびINSERTs
が発生しているだけです。
誰かがこれについてのディスカッションとそれを回避する方法を投稿したことがあります。他の人は、WHERE句で利用可能なシステム識別子の公開を利用します。
Jun 04, 2002
: ctid&更新(または迅速な更新/削除)Jul 05, 2011
: PostgreSQLで完全に重複する行を削除します(1つだけ保持します)Nov 26, 2012
: ctid列を使用したテーブル書き換えの検出May 27, 2014
: ctidをページ番号と行番号に分解するにはどうすればよいですか?デフォルトのトランザクション分離レベルはREAD COMMITTED
です。移動するインデックスがあると、この分離レベルではデータの整合性が向上しますが、インデックス内のctid
値が常に書き換えられ、MongoDBのように新しい行全体を削除して挿入するという動作が犠牲になります。単純なUPDATE
。他のトランザクション分離レベル(REPEATABLE READ
、READ UNCOMMITTED
、SERIALIZABLE
)も可能ですが、特定のトランザクションでデータの必要なビューを維持するには、ctidとoidの値に対して特別なSQLが必要になる場合があります。このような特殊なクエリは、SELECTs
には役立ちますが、UPDATEs
デーモンが実行されており、DELETEs
が廃止される可能性が非常に高いため、VACUUM
およびctid
にはあまり信頼できません。その場合、oid
ではなくcitd
でクエリの行を参照する方が賢明です。
InnoDBは、PostgreSQLのようなループをジャンプしてUPDATEを実行しません。 InnoDBでは、ROWIDタイプの情報を利用してUPDATEを実行する特殊なSQLコマンドを作成する必要はありません。
InnoDBのデフォルトの分離レベルはREPEATABLE READ
です。これはREAD COMMITTED
よりも保守がはるかに簡単です。また、InnoDBのROWIDが変更されることもありません。
InnoDBがこれと同じ悪い動作をするためには、 REPLACEコマンド(機械的なDELETEおよびINSERT) を実行する必要があります。 PostgreSQLはこの操作を自動的に実行します。ソースコードを取得し、UPDATE
プロセスを修正または改善する勇気がない限り、PostgreSQLに対してできることは何もありません。
Rolandoは、デッドロックが発生する理由をすでにある程度説明しています。 PostgreSQLは、一貫性を維持するために、更新する行をロックする必要があることを付け加えておきます。 (now()
はトランザクションの先頭に固定されています。つまり、2つの同時更新で特定の行を異なるタイムスタンプに更新する必要があるため、何らかの方法で解決する必要があります。行をロックすることはPostgreSQLのソリューションです。 。)
ロックは先着順でトランザクションに割り当てられます。デッドロックは、2つのトランザクションが同じ行を異なる順序でロックしようとしたときに発生します。これを行う方法は、当然のことながら、予測可能な順序でロックを要求することです。これは
SELECT id -- could be any column, even a constant
FROM position
WHERE latitude between 234.12 and 235.00
ORDER BY id
FOR UPDATE;
ここで重要な部分はORDER BY id
およびFOR UPDATE
。後者は、後続のUPDATE
が必要とする同じタイプの 必要なロック を取ります。
SELECT ... FOR UPDATE
ステートメントは、それ以外の場合(特に、選択したデータが不要な場合)は不要に見える可能性があり、実際にデータを変更する前であっても、ディスクへの書き込みを意味します。これを使用するときは、これを考慮する必要があります。
私はこれがPostgresやMySQL(またはUberの やや根拠のない理由 に切り替えるための==)によるUPDATEの処理方法とは何の関係もないと思います。
さらに、指定された設定では、Postgresはインデックス値が変更されないため、更新ごとに新しい行バージョンを作成しません。このケースは、HOT(ヒープのみのタプル)更新として知られています。たとえば、 here 、 here または here (プラス:Uberの批評家の反応として、これに対する最適化は 現在作業中 =)
デッドロックの原因は、常に異なるスレッドから異なる順序でロックを取得することです。これは、 how ロックの取得方法またはバックグラウンドでの変更の物理的な方法に依存しません。
MySQL/InnoDBとPostgresの大きな違いは、MySQLは常にがOracleの「インデックス構成テーブル」と呼ばれるもの(または「クラスター化インデックス」)を使用することです。 SQL Serverで)。したがって、テーブルの行は実際にはインデックス構造に格納されます(これは 常に良い選択ではありません ではありません)。
これは、行が主キーによって物理的に「ソート」されていることを意味し(主キーがない場合、MySQLはそのために非表示の列を追加します)、インデックスのないテーブルでの更新では、すべてのテーブルがすべてwhere句に一致する行が見つかりました。
インデックス構成テーブルによって課される「ソートされた」ストレージは、行が(おそらく)常に各スレッドに対して同じ順序でスキャンされることを意味します-つまり、ロックは取得各スレッドで同じ順序-デッドロックを回避します。
Postgresにはインデックス構成のテーブル(クラスター化インデックス)がないため、ロックが取得される順序は暗黙的な順序によって決定されないため、デッドロックが発生する可能性があります。
Oracleの通常のテーブル(またはクラスター化インデックスのないテーブルのSQL Server)の性質を考えると、OracleまたはSQL ServerがPostgresと同じ動作を示したとしても、私は驚かないでしょう。
私はこれを再現して小さな(Java)テストプログラムを作成しました。
テーブルは次のように作成されました:
create table positions
(
id integer,
value_1 numeric(18,12),
value_2 numeric(18,12),
updated_at timestamp
);
insert into positions (id, value_1, value_2)
select i, random() * 50000 + 1, random() * 50000 + 1
from generate_series(1, 50000) i;
だから私はvalue_1
およびvalue_2
1〜50.000の範囲の列
Javaプログラムは50個のスレッドを作成し、それぞれをデータベースに接続し、すべてが接続されると、実際の更新が並行して開始されました-これは、最初のスレッドが最後のスレッドの前にすでに終了するのを避けるために行われました接続されている1つ)。
update positions
set updated_at = current_timestamp
where value_1 between 5000 and 30000;
つまり、約25.000行(テーブルの半分)を更新しています。
Postgres 9.5でも9.6でも、デッドロックは発生しませんでした。デッドロックタイムアウトを1秒から25ミリ秒に下げた後も、デッドロックが発生しませんでした。 lotのロック警告が次のように表示されますが(log_lock_waits
)
2016-10-12 08:19:53 CEST postgres LOG: process 6252 still waiting for ExclusiveLock on Tuple (9384,1) of relation 1828790 of database 12401 after 31.197 ms
2016-10-12 08:19:53 CEST postgres DETAIL: Process holding the lock: 11036. Wait queue: 1892, 6252, 9836.
更新後、すべてのスレッドを25ミリ秒間スリープさせた後でも、コミットの前にデッドロックが発生しませんでした。
また、行数を500.000に、 updated 行の数を250.000に増やしましたが、デッドロックはありません。
あなたはここでテストプログラムを見ることができます: http://hastebin.com/usuruqolez.Java