SQLiteの最適化には注意が必要です。 Cアプリケーションの一括挿入のパフォーマンスは、1秒あたり85個の挿入から1秒あたり96,000個を超える挿入までさまざまです。
Background:デスクトップアプリケーションの一部としてSQLiteを使用しています。 XMLファイルに保存された大量の構成データが解析され、アプリケーションの初期化時にさらに処理するためにSQLiteデータベースにロードされます。 SQLiteは高速で、特別な構成を必要とせず、データベースが単一のファイルとしてディスクに保存されるため、この状況に最適です。
理論的根拠:最初、私が見ていたパフォーマンスに失望しました。 -inserts and select)データベースの設定方法とAPIの使用方法に応じて。すべてのオプションとテクニックが何であるかを理解することは簡単なことではありませんでしたので、同じ調査の問題を他の人に保存するために、このコミュニティwikiエントリを作成してStack Overflowリーダーと結果を共有するのが賢明だと思いました。
実験:一般的な意味でのパフォーマンスのヒント(つまり "Use a transaction!" )について単に話すのではなく、いくつかのCコードを記述し、実際に測定さまざまなオプションの影響。いくつかの簡単なデータから始めます。
コードを書きましょう!
The Code:テキストファイルを1行ずつ読み取り、文字列を値に分割してから、データをSQLiteデータベースに挿入する単純なCプログラム。この「ベースライン」バージョンのコードでは、データベースが作成されますが、実際にはデータを挿入しません。
/*************************************************************
Baseline code to experiment with SQLite performance.
Input data is a 28 MB TAB-delimited text file of the
complete Toronto Transit System schedule/route info
from http://www.toronto.ca/open/datasets/ttc-routes/
**************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include "sqlite3.h"
#define INPUTDATA "C:\\TTC_schedule_scheduleitem_10-27-2009.txt"
#define DATABASE "c:\\TTC_schedule_scheduleitem_10-27-2009.sqlite"
#define TABLE "CREATE TABLE IF NOT EXISTS TTC (id INTEGER PRIMARY KEY, Route_ID TEXT, Branch_Code TEXT, Version INTEGER, Stop INTEGER, Vehicle_Index INTEGER, Day Integer, Time TEXT)"
#define BUFFER_SIZE 256
int main(int argc, char **argv) {
sqlite3 * db;
sqlite3_stmt * stmt;
char * sErrMsg = 0;
char * tail = 0;
int nRetCode;
int n = 0;
clock_t cStartClock;
FILE * pFile;
char sInputBuf [BUFFER_SIZE] = "\0";
char * sRT = 0; /* Route */
char * sBR = 0; /* Branch */
char * sVR = 0; /* Version */
char * sST = 0; /* Stop Number */
char * sVI = 0; /* Vehicle */
char * sDT = 0; /* Date */
char * sTM = 0; /* Time */
char sSQL [BUFFER_SIZE] = "\0";
/*********************************************/
/* Open the Database and create the Schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
/*********************************************/
/* Open input file and import into Database*/
cStartClock = clock();
pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {
fgets (sInputBuf, BUFFER_SIZE, pFile);
sRT = strtok (sInputBuf, "\t"); /* Get Route */
sBR = strtok (NULL, "\t"); /* Get Branch */
sVR = strtok (NULL, "\t"); /* Get Version */
sST = strtok (NULL, "\t"); /* Get Stop Number */
sVI = strtok (NULL, "\t"); /* Get Vehicle */
sDT = strtok (NULL, "\t"); /* Get Date */
sTM = strtok (NULL, "\t"); /* Get Time */
/* ACTUAL INSERT WILL GO HERE */
n++;
}
fclose (pFile);
printf("Imported %d records in %4.2f seconds\n", n, (clock() - cStartClock) / (double)CLOCKS_PER_SEC);
sqlite3_close(db);
return 0;
}
コードをそのまま実行しても、実際にはデータベース操作は実行されませんが、生のCファイルI/Oおよび文字列処理操作がどれだけ速いかがわかります。
0.94秒で864913レコードをインポートしました
すばらしいです!実際に挿入を行わない限り、毎秒920,000の挿入を実行できます:-)
ファイルから読み取った値を使用してSQL文字列を生成し、sqlite3_execを使用してそのSQL操作を呼び出します。
sprintf(sSQL, "INSERT INTO TTC VALUES (NULL, '%s', '%s', '%s', '%s', '%s', '%s', '%s')", sRT, sBR, sVR, sST, sVI, sDT, sTM);
sqlite3_exec(db, sSQL, NULL, NULL, &sErrMsg);
SQLは挿入ごとにVDBEコードにコンパイルされ、すべての挿入は独自のトランザクションで行われるため、これは遅くなります。 どのくらい遅い?
9933.61秒で864913レコードをインポートしました
いいね! 2時間45分! 1秒あたりの挿入数は85回のみです
デフォルトでは、SQLiteは一意のトランザクション内のすべてのINSERT/UPDATEステートメントを評価します。多数の挿入を実行する場合は、操作をトランザクションでラップすることをお勧めします。
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {
...
}
fclose (pFile);
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
38.03秒で864913レコードをインポートしました
それは良いです。単一のトランザクションですべての挿入をラップするだけで、パフォーマンスが1秒あたり23,000挿入に改善されました。
トランザクションの使用は大幅に改善されましたが、同じSQLを繰り返し使用する場合、挿入ごとにSQLステートメントを再コンパイルすることは意味がありません。 sqlite3_prepare_v2
を使用してSQLステートメントを一度コンパイルし、sqlite3_bind_text
を使用してパラメーターをそのステートメントにバインドします。
/* Open input file and import into the database */
cStartClock = clock();
sprintf(sSQL, "INSERT INTO TTC VALUES (NULL, @RT, @BR, @VR, @ST, @VI, @DT, @TM)");
sqlite3_prepare_v2(db, sSQL, BUFFER_SIZE, &stmt, &tail);
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {
fgets (sInputBuf, BUFFER_SIZE, pFile);
sRT = strtok (sInputBuf, "\t"); /* Get Route */
sBR = strtok (NULL, "\t"); /* Get Branch */
sVR = strtok (NULL, "\t"); /* Get Version */
sST = strtok (NULL, "\t"); /* Get Stop Number */
sVI = strtok (NULL, "\t"); /* Get Vehicle */
sDT = strtok (NULL, "\t"); /* Get Date */
sTM = strtok (NULL, "\t"); /* Get Time */
sqlite3_bind_text(stmt, 1, sRT, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, sBR, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, sVR, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 4, sST, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 5, sVI, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 6, sDT, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 7, sTM, -1, SQLITE_TRANSIENT);
sqlite3_step(stmt);
sqlite3_clear_bindings(stmt);
sqlite3_reset(stmt);
n++;
}
fclose (pFile);
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
printf("Imported %d records in %4.2f seconds\n", n, (clock() - cStartClock) / (double)CLOCKS_PER_SEC);
sqlite3_finalize(stmt);
sqlite3_close(db);
return 0;
16.27秒で864913レコードをインポートしました
いいね!コードはもう少しありますが(sqlite3_clear_bindings
およびsqlite3_reset
を呼び出すことを忘れないでください)、パフォーマンスは2倍以上になり、1秒あたり53,000の挿入になりました。
デフォルトでは、SQLiteはOSレベルの書き込みコマンドを発行した後に一時停止します。これにより、データがディスクに書き込まれることが保証されます。 synchronous = OFF
を設定することにより、書き込みのためにOSにデータをハンドオフしてから続行するようにSQLiteに指示しています。データがプラッターに書き込まれる前にコンピューターに壊滅的なクラッシュ(または電源障害)が発生すると、データベースファイルが破損する可能性があります。
/* Open the database and create the schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA synchronous = OFF", NULL, NULL, &sErrMsg);
12.41秒で864913レコードをインポートしました
改善点は今では小さくなっていますが、最大で毎秒69,600回の挿入が可能です。
PRAGMA journal_mode = MEMORY
を評価して、ロールバックジャーナルをメモリに保存することを検討してください。トランザクションは高速になりますが、トランザクション中に電源が切れたり、プログラムがクラッシュした場合、データベースは部分的に完了したトランザクションで破損した状態のままになる可能性があります。
/* Open the database and create the schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA journal_mode = MEMORY", NULL, NULL, &sErrMsg);
13.50秒で864913レコードをインポートしました
1秒あたり64,000の挿入での以前の最適化よりも少し遅い
前の2つの最適化を組み合わせてみましょう。少し危険です(クラッシュの場合)が、データをインポートするだけです(銀行を経営していない):
/* Open the database and create the schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA synchronous = OFF", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA journal_mode = MEMORY", NULL, NULL, &sErrMsg);
12.00秒で864913レコードをインポートしました
素晴らしい! 1秒間に72,000の挿入を行うことができます。
キックのために、以前のすべての最適化に基づいてデータベースのファイル名を再定義し、RAMで完全に作業できるようにします。
#define DATABASE ":memory:"
10.94秒で864913レコードをインポートしました
データベースをRAMに保存するのはあまり実用的ではありませんが、1秒あたり79,000の挿入を実行できることは印象的です。
特にSQLiteの改善ではありませんが、while
ループでの余分なchar*
割り当て操作は好きではありません。そのコードをすばやくリファクタリングして、strtok()
の出力をsqlite3_bind_text()
に直接渡し、コンパイラーが速度を上げられるようにします。
pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {
fgets (sInputBuf, BUFFER_SIZE, pFile);
sqlite3_bind_text(stmt, 1, strtok (sInputBuf, "\t"), -1, SQLITE_TRANSIENT); /* Get Route */
sqlite3_bind_text(stmt, 2, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Branch */
sqlite3_bind_text(stmt, 3, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Version */
sqlite3_bind_text(stmt, 4, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Stop Number */
sqlite3_bind_text(stmt, 5, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Vehicle */
sqlite3_bind_text(stmt, 6, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Date */
sqlite3_bind_text(stmt, 7, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Time */
sqlite3_step(stmt); /* Execute the SQL Statement */
sqlite3_clear_bindings(stmt); /* Clear bindings */
sqlite3_reset(stmt); /* Reset VDBE */
n++;
}
fclose (pFile);
注:実際のデータベースファイルの使用に戻りました。インメモリデータベースは高速ですが、必ずしも実用的ではありません
8.94秒で864913レコードをインポートしました
パラメーターバインドで使用される文字列処理コードをわずかにリファクタリングすることで、1秒あたり96,700の挿入を実行できるようになりました。これはかなり高速だと言っても安全だと思います。他の変数(ページサイズ、インデックス作成など)の調整を開始すると、これがベンチマークになります。
あなたが私と一緒にいることを望みます!この道を始めたのは、SQLiteによって一括挿入のパフォーマンスが非常に大きく変化するためであり、どのような変更が必要かは必ずしも明らかではありません操作をスピードアップします。同じコンパイラー(およびコンパイラーオプション)、同じバージョンのSQLiteおよび同じデータを使用して、コードを最適化し、SQLiteの使用を1秒あたり85回の挿入という最悪のシナリオから 1秒あたり96,000の挿入
SELECT
パフォーマンスの測定を開始する前に、インデックスを作成することを知っています。以下の回答のいずれかで、一括挿入を行う場合、データを挿入した後にインデックスを作成する方が高速であることが示唆されています(最初にインデックスを作成してからデータを挿入するよりも)。やってみよう:
インデックスを作成してからデータを挿入
sqlite3_exec(db, "CREATE INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
...
18.13秒で864913レコードをインポートしました
データを挿入してからインデックスを作成
...
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "CREATE INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
13.66秒で864913レコードをインポートしました
予想どおり、1つの列にインデックスが付けられている場合、一括挿入は遅くなりますが、データの挿入後にインデックスが作成されると違いが生じます。インデックスなしのベースラインは、1秒あたり96,000の挿入です。 最初にインデックスを作成してからデータを挿入すると、1秒あたり47,700回の挿入が行われますが、最初にデータを挿入してからインデックスを作成すると、1秒あたり63,300回の挿入が行われます。
私は喜んで他のシナリオを試してみてください...そして、SELECTクエリの同様のデータをまもなくコンパイルします。
いくつかのヒント:
pragma journal_mode
)を考えてください。 NORMAL
があり、それからOFF
があります。OSがクラッシュした場合にデータベースが壊れる可能性があることをあまり心配していなければ、挿入速度が大幅に向上します。アプリケーションがクラッシュした場合、データは問題ないはずです。新しいバージョンでは、OFF/MEMORY
設定はアプリケーションレベルのクラッシュに対して安全ではありません。PRAGMA page_size
)。大きなページサイズを持つと、大きなページがメモリに保持されるため、読み書きが少し速くなります。データベースにより多くのメモリが使用されることに注意してください。CREATE INDEX
を呼び出すことを検討してください。これは、インデックスを作成してから挿入を行うよりもはるかに高速です。INTEGER PRIMARY KEY
にしてみてください。これによって、テーブル内の暗黙の一意の行番号列が置き換えられます。!feof(file)
!を使わないでくださいこれらの挿入には、SQLITE_STATIC
の代わりにSQLITE_TRANSIENT
を使用してみてください。
SQLITE_TRANSIENT
はSQLiteに戻る前に文字列データをコピーさせるでしょう。
SQLITE_STATIC
はあなたがそれに与えたメモリアドレスが問い合わせが実行されるまで有効であることを伝えます(このループではいつもそうです)。これにより、ループごとにいくつかの割り当て、コピー、割り当て解除の操作を省くことができます。おそらく大きな改善です。
sqlite3_clear_bindingsを避けます(stmt);
テストのコードは毎回バインディングを設定し、それを通して十分なはずです。
SQLiteのドキュメントからのC APIイントロによると
初めてsqlite3_step()を呼び出す前、またはsqlite3_reset()の直後に、アプリケーションはsqlite3_bind()インターフェースの1つを呼び出して、パラメーターに値を付加することができます。 sqlite3_bind()を呼び出すたびに、同じパラメータに対する以前のバインディングが上書きされます。
(参照: sqlite.org/cintro.html )。 その関数 についての文書には、単にバインディングを設定することに加えてそれを呼び出さなければならないということは何もありません。
この投稿とここで私を導いてくれたStack Overflowの質問にインスパイアされました - SQLiteデータベースに一度に複数の行を挿入することは可能ですか? - 私は自分の投稿を投稿しましたfirst Git リポジトリ:
https://github.com/rdpoor/CreateOrUpdate
これは MySQL 、SQLite、または PostgreSQL データベースにActiveRecordの配列を一括ロードします。既存のレコードを無視する、上書きする、またはエラーを発生させるオプションがあります。私の初歩的なベンチマークは、シーケンシャル書き込み - YMMVと比較して10倍のスピード向上を示しています。
大規模なデータセットを頻繁にインポートする必要があるプロダクションコードでそれを使用しています、そしてそれには満足しています。
一括インポートは、INSERT/UPDATE文をまとめることができれば最もパフォーマンスが良いようです。数行程度のテーブルでは、YMMVで10,000程度の値がうまく機能しました。
あなたが読むことだけを考えているなら、多少速い(しかし古いデータを読むかもしれない)バージョンは複数のスレッドからの複数の接続から読むことです(スレッド毎の接続)。
まず表から項目を見つけます。
SELECT COUNT(*) FROM table
それからページを読んでください(LIMIT/OFFSET):
SELECT * FROM table ORDER BY _ROWID_ LIMIT <limit> OFFSET <offset>
ここで、とは、スレッドごとに計算されます。
int limit = (count + n_threads - 1)/n_threads;
スレッドごとに:
int offset = thread_index * limit
私達の小さい(200mb)dbのためにこれは50-75%のスピードアップ(Windows 7で3.8.0.2 64ビット)をしました。私たちのテーブルは非常に正規化されていません(1000-1500列、およそ10万行以上)。
あまりにも多くのまたは少なすぎるスレッドではうまくいかない場合は、自分でベンチマークをとり、プロファイルを作成する必要があります。
私たちにとっても、SHAREDCACHEはパフォーマンスを低下させたので、私は手動でPRIVATECACHEを設定しました。
Cache_sizeをもっと高い値、つまりPRAGMA cache_size=10000;
に引き上げるまで、トランザクションから何の利益も得られません。
このチュートリアルを読んだ後、私は自分のプログラムにそれを実装しようとしました。
住所を含む4〜5個のファイルがあります。各ファイルには約3000万レコードがあります。私はあなたが提案しているのと同じ設定を使っていますが、私の1秒あたりのINSERT数はかなり低いです(1秒あたり〜10.000レコード)。
これはあなたの提案が失敗するところです。すべてのレコードに対して単一のトランザクションを使用し、エラーや失敗のない単一の挿入を使用します。各レコードを異なるテーブルの複数の挿入に分割しているとしましょう。レコードが壊れているとどうなりますか?
ON CONFLICTコマンドは適用されません。レコード内に10個の要素があり、各要素を異なるテーブルに挿入する必要がある場合、要素5にCONSTRAINTエラーが発生した場合は、それまでの4回の挿入もすべて実行する必要があります。
だからここでロールバックが来るのです。ロールバックの唯一の問題はあなたがすべてのあなたの挿入物を失いそして上から始めているということです。どうすればこれを解決できますか?
私の解決策は、複数のトランザクションを使用することでした。私は10.000レコードごとにトランザクションを開始し、終了します(なぜその数を尋ねないでください、それは私がテストした最も速いものでした)。私は10.000サイズの配列を作成し、そこに成功したレコードを挿入します。エラーが発生したら、ロールバックしてトランザクションを開始し、配列からレコードを挿入してコミットし、破損したレコードの後で新しいトランザクションを開始します。
この解決策は、私が悪い/重複したレコードを含むファイルを扱うときに抱えていた問題(私はほぼ4%の悪いレコードを持っていた)を回避するのに役立ちました。
私が作成したアルゴリズムは、私のプロセスを2時間短縮するのに役立ちました。ファイル1hr 30mの最終ロード処理はまだ遅いですが、最初に行った4時間とは比較になりません。私はインサートを10.000/sから〜14.000/sにスピードアップすることに成功しました
スピードを上げる方法について他のアイデアがある人がいるなら、私は提案を受け入れます。
UPDATE:
上記の私の答えに加えて、あなたはあなたが使用しているハードドライブにもよりますが毎秒インサートがあることを心に留めておくべきです。私は、異なるハードドライブを備えた3つの異なるPCでそれをテストしました、そして時の大きな違いを得ました。 PC1(1時間30分)、PC2(6時間)、PC3(14時間)なので、なぜそうなるのか疑問に思い始めました。
2週間の調査と複数のリソース(ハードドライブ、RAM、キャッシュ)のチェックの後、ハードドライブの設定によってはI/Oレートに影響を与える可能性があることがわかりました。目的の出力ドライブのプロパティをクリックすると、[全般]タブに2つのオプションが表示されます。 Opt1:このドライブを圧縮し、Opt2:このドライブのファイルにインデックスを付けます。
これら2つのオプションを無効にすることで、3台のPCすべてが終了するまでにほぼ同じ時間がかかります(1時間と20〜40分)。挿入が遅い場合は、ハードドライブがこれらのオプションで設定されているかどうかを確認してください。それはあなたに多くの時間と解決策を見つけることを試みる頭痛を救うでしょう
あなたの質問に対する答えは、新しいsqlite3のパフォーマンスが向上したということです。それを使用してください。
この答え SqlAlchemyによるSQLAlchemyの挿入が、sqlite3を直接使用するよりも25倍遅いのはなぜですか? by SqlAlchemy Orm Authorによると、0.5秒で100kの挿入があります。 python-sqliteとSqlAlchemyによる結果。これは私にパフォーマンスがsqlite3で向上したと信じるように導きます