SQLiteの最適化には注意が必要です。Cアプリケーションの一括挿入のパフォーマンスは、1秒あたり85挿入から1秒あたり96,000挿入を超える場合があります。
背景: SQLiteをデスクトップアプリケーションの一部として使用しています。大量の構成データがXMLファイルに格納されており、アプリケーションが初期化されるときに解析され、SQLiteデータベースにロードされてさらに処理されます。SQLiteは高速であり、特別な構成を必要とせず、データベースは単一のファイルとしてディスクに保存されるため、この状況に最適です。
理論的根拠: 最初は私が見ているパフォーマンスにがっかりしました。SQLiteのパフォーマンスは、データベースの構成方法とAPIの使用方法に応じて(一括挿入と選択の両方で)大きく異なる可能性があることがわかりました。すべてのオプションと手法が何であるかを理解することは簡単なことではなかったので、同じコミュニティのWikiエントリを作成してスタックオーバーフローリーダーと結果を共有し、同じ調査の問題を他の人に知らせるのは賢明だと思いました。
実験:一般的なパフォーマンスのヒント(つまり、「トランザクションを使用する!」)について単に話すのではなく、Cコードを記述して実際にさまざまなオプションの影響を測定するのが最善だと思いました。いくつかの簡単なデータから始めましょう:
- トロント市の完全な輸送スケジュールの28 MBのタブ区切りテキストファイル(約865,000レコード)
- 私のテストマシンは、Windows XPを実行する3.60 GHz P4です。
- コードは、Visual C ++ 2005で「完全最適化」(/ Ox)付きの「リリース」としてコンパイルされ、高速コード(/ Ot)が優先されます。
- 私はテストアプリケーションに直接コンパイルされたSQLite "Amalgamation"を使用しています。私がたまたま持っているSQLiteのバージョンは少し古い(3.6.7)ですが、これらの結果は最新のリリースに匹敵すると思われます(そうでない場合はコメントを残してください)。
コードを書いてみましょう!
コード:テキストファイルを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ステートメントを1回コンパイルしてから、次のコマンドを使用してパラメーターをそのステートメントにバインドします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
and を呼び出すことを忘れないでくださいsqlite3_reset
)が、パフォーマンスを2倍以上にして、毎秒53,000挿入になりました。
PRAGMA同期=オフ
デフォルトでは、SQLiteはOSレベルの書き込みコマンドを発行した後に一時停止します。これにより、データがディスクに書き込まれることが保証されます。を設定することでsynchronous = OFF
、SQLiteにデータをOSにハンドオフして書き込みを行い、続行するように指示しています。データがプラッタに書き込まれる前にコンピュータに壊滅的なクラッシュ(または電源障害)が発生すると、データベースファイルが破損する可能性があります。
/* 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挿入です。
プラグマjournal_mode = MEMORY
評価してロールバックジャーナルをメモリに保存することを検討してください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レコードをインポート
毎秒64,000挿入という以前の最適化よりも少し遅い。
PRAGMA同期= OFF および PRAGMA journal_mode = MEMORY
前の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回の挿入を実行できることは印象的です。
Cコードのリファクタリング
特にSQLiteの改善ではありませんchar*
が、while
ループ内の余分な代入演算は好きではありません。そのコードをすばやくリファクタリングして、の出力を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レコードをインポート
パラメータバインディングで使用される文字列処理コードを少しリファクタリングすることで、毎秒96,700回の挿入を実行できるようになりました。これはかなり高速だと言っても安全だと思います。他の変数(つまり、ページサイズ、インデックスの作成など)の調整を開始すると、これがベンチマークになります。
まとめ(これまで)
あなたが私と一緒にいることを願っています!この道を歩み始めた理由は、SQLiteを使用すると一括挿入のパフォーマンスが非常に大きく変動し、操作を高速化するためにどのような変更を加える必要があるかが必ずしも明確ではないためです。同じコンパイラー(およびコンパイラーオプション)、同じバージョンのSQLite、同じデータを使用してコードを最適化し、SQLiteの使用法を最悪の場合のシナリオである85挿入/秒から96,000挿入/秒以上に変更しました!
CREATE INDEXの次にINSERTとINSERTの次にCREATE INDEX
SELECT
パフォーマンスの測定を始める前に、インデックスを作成することを知っています。以下の回答の1つで、一括挿入を行う場合、データを挿入した後にインデックスを作成する方が(最初にインデックスを作成してからデータを挿入するのではなく)速いことが示唆されています。やってみよう:
インデックスを作成してからデータを挿入する
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クエリの同様のデータをまもなくコンパイルします。
sqlite3_clear_bindings(stmt);
か?毎回十分なバインディングを設定します。最初にsqlite3_step()を呼び出す前、またはsqlite3_reset()の直後に、アプリケーションはsqlite3_bind()インターフェースの1つを呼び出してパラメーターに値をアタッチできます。sqlite3_bind()を呼び出すたびに、同じパラメーターの以前のバインディングがオーバーライドされます(sqlite.org/cintro.htmlを参照)。あなたはそれを呼び出さなければならないというその関数のドキュメントには何もありません。