今日、自分の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
ここでの作業にはいくつかの要因があります:
COMMIT
のコスト、挿入ごとに1つのコミットを行う場合(そうではありません)COPY
固有の最適化サーバーがリモートの場合、ステートメントごとの固定時間の「価格」、たとえば50ミリ秒(1/20秒)を「支払う」可能性があります。一部のクラウドホスト型DBの場合はそれ以上です。次の挿入は最後の挿入が正常に完了するまで開始できないため、挿入の最大挿入率は1000/round-trip-latency-in-ms行/秒です。 50ミリ秒の遅延(「ping時間」)では、20行/秒になります。ローカルサーバーでも、この遅延はゼロではありません。 Wheras COPY
は、TCP送信および受信ウィンドウを埋め、DBが書き込み、ネットワークが転送できるのと同じ速さで行をストリーミングします。レイテンシの影響はあまり受けず、同じネットワークリンクに毎秒数千行を挿入する。
PostgreSQLでのステートメントの解析、計画、実行にはコストもかかります。ロックを取得し、リレーションファイルを開き、インデックスを検索する必要があります。COPY
は、最初に一度これらすべてを実行してから、できるだけ高速にローをロードすることに集中します。
アプリが準備して送信している間、オペレーティングシステムが行を待機しているpostgresを切り替えなければならず、その後、postgresが行を処理している間、アプリがpostgresの応答を待機しているため、さらに時間コストがかかります。一方から他方に切り替えるたびに、少しの時間を無駄にします。プロセスが待機状態に入るときと待機状態から出るときに、さまざまな低レベルのカーネル状態の中断と再開に無駄な時間がかかる可能性があります。
その上、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
がデータの途中でエラーから回復できないことも意味します。一括読み込み全体を元に戻します。
コピーは一括読み込みを使用します。つまり、一度に複数の行を挿入しますが、単純な挿入は一度に1つの挿入を実行しますが、次の構文に従って複数の行を挿入できます。
insert into table_name (column1, .., columnn) values (val1, ..valn), ..., (val1, ..valn)
バルクロードの使用の詳細については、例を参照してください。 Daniel Westermannによるpostgresqlで1m行をロードする最速の方法 。
一度に挿入する必要がある行数の問題は、行の長さに依存します。大まかな目安は、挿入ステートメントごとに100行を挿入することです。
トランザクションを高速化するために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