web-dev-qa-db-ja.com

MySQLが複数の更新を同時に処理でき、PostgreSQLが処理できないのはなぜですか?

次の定義のテーブルがあるとします。

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株が常に入手可能な最新の価格で更新されていると想像してください。

1
SDReyes

歴史レッスン

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提供)

Index ctid Management

PostgreSQLはPRIMARY KEYを一意でないインデックスと結合的に結合しません。インデックスはctidによって行を参照するため、単純なUPDATE(インデックス付けされていない列でも)はctidを変更し、その結果、変更された行を参照するテーブル内のすべてのインデックスのctidを書き換える必要があります。 これは新しいものではありません。 PostgreSQLはこれを常にdesignで行っています。

したがって、1000行でUPDATEを実行すると、1000 DELETEsおよび1000 INSERTsが実行されます。すでに述べたように、テーブルにアタッチされたすべてのインデックスは、古い行のctidの値を置き換えて、BTREEインデックスエントリに新しい行のctidの値を書き込む必要があります。テーブルにインデックスがないため、表示されているデッドロックを生成するDELETEsおよびINSERTsが発生しているだけです。

補足情報

誰かがこれについてのディスカッションとそれを回避する方法を投稿したことがあります。他の人は、WHERE句で利用可能なシステム識別子の公開を利用します。

デフォルトのトランザクション分離レベルはREAD COMMITTEDです。移動するインデックスがあると、この分離レベルではデータの整合性が向上しますが、インデックス内のctid値が常に書き換えられ、MongoDBのように新しい行全体を削除して挿入するという動作が犠牲になります。単純なUPDATE。他のトランザクション分離レベル(REPEATABLE READREAD UNCOMMITTEDSERIALIZABLE)も可能ですが、特定のトランザクションでデータの必要なビューを維持するには、ctidとoidの値に対して特別なSQLが必要になる場合があります。このような特殊なクエリは、SELECTsには役立ちますが、UPDATEsデーモンが実行されており、DELETEsが廃止される可能性が非常に高いため、VACUUMおよびctidにはあまり信頼できません。その場合、oidではなくcitdでクエリの行を参照する方が賢明です。

INNODBとは???

InnoDBは、PostgreSQLのようなループをジャンプしてUPDATEを実行しません。 InnoDBでは、ROWIDタイプの情報を利用してUPDATEを実行する特殊なSQLコマンドを作成する必要はありません。

InnoDBのデフォルトの分離レベルはREPEATABLE READです。これはREAD COMMITTEDよりも保守がはるかに簡単です。また、InnoDBのROWIDが変更されることもありません。

InnoDBがこれと同じ悪い動作をするためには、 REPLACEコマンド(機械的なDELETEおよびINSERT) を実行する必要があります。 PostgreSQLはこの操作を自動的に実行します。ソースコードを取得し、UPDATEプロセスを修正または改善する勇気がない限り、PostgreSQLに対してできることは何もありません。

6
RolandoMySQLDBA

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ステートメントは、それ以外の場合(特に、選択したデータが不要な場合)は不要に見える可能性があり、実際にデータを変更する前であっても、ディスクへの書き込みを意味します。これを使用するときは、これを考慮する必要があります。

9
dezso

私はこれがPostgresやMySQL(またはUberの やや根拠のない理由 に切り替えるための==)によるUPDATEの処理方法とは何の関係もないと思います。

さらに、指定された設定では、Postgresはインデックス値が変更されないため、更新ごとに新しい行バージョンを作成しません。このケースは、HOT(ヒープのみのタプル)更新として知られています。たとえば、 herehere または 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