web-dev-qa-db-ja.com

COPYはどのように機能し、なぜINSERTよりもはるかに高速なのですか?

今日、自分の1日を費やして、PostgresデータベースにデータをプッシュするPythonスクリプトのパフォーマンスを向上させました。以前は次のようにレコードを挿入していました。

query = "INSERT INTO my_table (a,b,c ... ) VALUES (%s, %s, %s ...)";
for d in data:
    cursor.execute(query, d)

次に、スクリプトを書き直して、PostgresのCOPYコマンドで使用されるものよりもメモリ内のファイルを作成するようにしました。これにより、ファイルからテーブルにデータをコピーできます。

f = StringIO(my_tsv_string)
cursor.copy_expert("COPY my_table FROM STDIN WITH CSV DELIMITER AS E'\t' ENCODING 'utf-8' QUOTE E'\b' NULL ''", f)

COPYメソッドは驚異的に高速でした

METHOD      | TIME (secs)   | # RECORDS
=======================================
COPY_FROM   | 92.998    | 48339
INSERT      | 1011.931  | 48377

しかし、なぜ私は情報を見つけることができませんか?マルチラインINSERTとはどのように異なるので、処理速度が大幅に向上しますか?

このベンチマーク も参照してください:

# original
0.008857011795043945: query_builder_insert
0.0029380321502685547: copy_from_insert

#  10 records
0.00867605209350586: query_builder_insert
0.003248929977416992: copy_from_insert

# 10k records
0.041108131408691406: query_builder_insert
0.010066032409667969: copy_from_insert

# 1M records
3.464181900024414: query_builder_insert
0.47070908546447754: copy_from_insert

# 10M records
38.96936798095703: query_builder_insert
5.955034017562866: copy_from_insert
20
turnip

ここでの作業にはいくつかの要因があります:

  • ネットワーク遅延と往復遅延
  • PostgreSQLのステートメントごとのオーバーヘッド
  • コンテキストの切り替えとスケジューラーの遅延
  • COMMITのコスト、挿入ごとに1つのコミットを行う場合(そうではありません)
  • 一括読み込みのCOPY固有の最適化

ネットワーク待ち時間

サーバーがリモートの場合、ステートメントごとの固定時間の「価格」、たとえば50ミリ秒(1/20秒)を「支払う」可能性があります。一部のクラウドホスト型DBの場合はそれ以上です。次の挿入は最後の挿入が正常に完了するまで開始できないため、挿入の最大挿入率は1000/round-trip-latency-in-ms行/秒です。 50ミリ秒の遅延(「ping時間」)では、20行/秒になります。ローカルサーバーでも、この遅延はゼロではありません。 Wheras COPYは、TCP送信および受信ウィンドウを埋め、DBが書き込み、ネットワークが転送できるのと同じ速さで行をストリーミングします。レイテンシの影響はあまり受けず、同じネットワークリンクに毎秒数千行を挿入する。

PostgreSQLのステートメントごとのコスト

PostgreSQLでのステートメントの解析、計画、実行にはコストもかかります。ロックを取得し、リレーションファイルを開き、インデックスを検索する必要があります。COPYは、最初に一度これらすべてを実行してから、できるだけ高速にローをロードすることに集中します。

タスク/コンテキスト切り替えコスト

アプリが準備して送信している間、オペレーティングシステムが行を待機しているpostgresを切り替えなければならず、その後、postgresが行を処理している間、アプリがpostgresの応答を待機しているため、さらに時間コストがかかります。一方から他方に切り替えるたびに、少しの時間を無駄にします。プロセスが待機状態に入るときと待機状態から出るときに、さまざまな低レベルのカーネル状態の中断と再開に無駄な時間がかかる可能性があります。

COPY最適化を逃している

その上、COPYには、ある種のロードに使用できるいくつかの最適化があります。たとえば、生成されたキーがなく、デフォルト値が定数である場合、それはそれらを事前に計算し、エグゼキュータを完全にバイパスし、PostgreSQLの通常の作業の一部を完全にスキップする低いレベルでテーブルにデータを高速でロードします。 TRUNCATEと同じトランザクションでCREATE TABLEまたはCOPYを使用すると、マルチクライアントデータベースで必要な通常のトランザクションブックキーピングをバイパスすることで、ロードを高速化するためのさらに多くのトリックを実行できます。

これにもかかわらず、PostgreSQLのCOPYは、速度を上げるためにまだ多くのことを実行できます。テーブルの特定の割合を超えて変更している場合は、インデックスの更新を自動的にスキップしてからインデックスを再構築できます。バッチでインデックスの更新を行うことができます。さらに多く。

コミット費用

考慮すべき最後の1つは、コミットコストです。 psycopg2はデフォルトでトランザクションを開き、指示がない限りコミットしないので、おそらく問題にはなりません。オートコミットを使用するように指示しない限り。しかし、多くのDBドライバーでは、自動コミットがデフォルトです。このような場合、INSERTごとに1つのコミットを実行します。つまり、1つのディスクがフラッシュされ、サーバーはメモリ内のすべてのデータをディスクに確実に書き込み、独自のキャッシュを永続ストレージに書き込むようディスクに指示します。これにはlong時間がかかる場合があり、ハードウェアによって大きく異なります。 SSDベースのNVMe BTRFSラップトップは、毎秒200 fsyncしか実行できませんが、毎秒300,000の非同期書き込みを実行できます。したがって、ロードされるのは1秒あたり200行のみです。一部のサーバーは、50 fsync /秒しか実行できません。 20,000人にできる人もいます。したがって、定期的にコミットする必要がある場合は、バッチでロードしてコミットしたり、複数行の挿入を行ったりするなどしてください。COPYは最後に1つのコミットしか行わないため、コミットのコストはごくわずかです。ただし、これはCOPYがデータの途中でエラーから回復できないことも意味します。一括読み込み全体を元に戻します。

16
Craig Ringer

コピーは一括読み込みを使用します。つまり、一度に複数の行を挿入しますが、単純な挿入は一度に1つの挿入を実行しますが、次の構文に従って複数の行を挿入できます。

insert into table_name (column1, .., columnn) values (val1, ..valn), ..., (val1, ..valn)

バルクロードの使用の詳細については、例を参照してください。 Daniel Westermannによるpostgresqlで1m行をロードする最速の方法

一度に挿入する必要がある行数の問題は、行の長さに依存します。大まかな目安は、挿入ステートメントごとに100行を挿入することです。

4

トランザクションを高速化するためにINSERTを実行します。

トランザクションなしのbashでのテスト:

>  time ( for((i=0;i<100000;i++)); do echo 'INSERT INTO testtable (value) VALUES ('$i');'; done ) | psql root | uniq -c
 100000 INSERT 0 1

real    0m15.257s
user    0m2.344s
sys     0m2.102s

そしてトランザクションで:

> time ( echo 'BEGIN;' && for((i=0;i<100000;i++)); do echo 'INSERT INTO testtable (value) VALUES ('$i');'; done && echo 'COMMIT;' ) | psql root | uniq -c
      1 BEGIN
 100000 INSERT 0 1
      1 COMMIT

real    0m7.933s
user    0m2.549s
sys     0m2.118s
2
OBi