C/C++環境でsqliteを使用する際の覚書です。
環境
- Ubuntu18.04 (4.15.0-36-generic, 64bit)
- gcc 7.3.0 (Ubuntu 7.3.0-27ubuntu1~18.04)
- sqlite3 3.22
C/C++ API使い方
基本的な使い方の覚書です。
DB接続と切断
sqlite3_open()もしくはsqlite3_open_v2()でデータベースに接続します。ファイル名がUTF-16の場合はsqlite3_open16()を使用します。使い終わったらsqlite_close_v2()で切断。
#include <sqlite3.h> int main() { sqlite3* db; // open db connection int rtn = sqlite3_open_v2( "test_bench.db", &db, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, nullptr); if(rtn != SQLITE_OK){ cerr << sqlite3_errmsg(db) << endl; } /* exec query */ // close db connection sqlite_close_v2(db); return 0; }
詳細は以下のドキュメントを参照してください。
- SQLite - Opening A New Database Connection
https://www.sqlite.org/c3ref/open.html
クエリの実行
sqlite3_exec()を使う場合
sqlite3_exec()は内部的にはprepared_statementを生成して実行しています(sqlite3_prepare,sqlite3_bind_xxx,sqlite3_stepのWrapper)。”CREATE TABLE”のような初回しか実行しないようなクエリを実行するのに便利です。
int ret = sqlite3_exec(db, "CREATE TABLE nodes_tbl(id INTEGER PRIMARY KEY, lon REAL, lat REAL);", nullptr, nullptr, nullptr);
この関数は毎回内部でクエリの変換が行われます。そのため、繰り返し何度も実行するようなクエリではprepared_statementを使う方がパフォーマンスが向上します。
詳細は以下のドキュメントを参照してください。
- SQLite - One-Step Query Execution Interface
https://www.sqlite.org/c3ref/exec.html
prepared_statementを使う
prepared_statementを生成してクエリを実行します。値はsqlite3_bind_xxxx()を使ってbind出来ます(IN等の可変個となるものは不可)。
// 乱数生成 std::random_device rd; std::mt19937_64 mt(rd()); std::uniform_real_distribution<double> random_lon(0,359.999); std::uniform_real_distribution<double> random_lat(-90.0, 90.0); std::uniform_int_distribution<uint64_t> random_id(0, 100000000); // INSERT sqlite3_stmt* pStmt; sqlite3_prepare_v2(db, "INSERT INTO nodes_tbl(id, lon, lat) VALUES(?, ?, ?);", -1, &pStmt, nullptr); for(int64_t i=0; i<100000000; i++) { sqlite3_bind_int64(pStmt, 1, i); sqlite3_bind_double(pStmt, 2, random_lon(mt)); sqlite3_bind_double(pStmt, 3, random_lat(mt)); while(sqlite3_step(pStmt) == SQLITE_BUSY){} sqlite3_reset(pStmt); sqlite3_clear_bindings(pStmt); } sqlite3_finalize(pStmt); // SELECT sqlite3_prepare_v2(db, "SELECT id, lon, lat FROM nodes_tbl WHERE id=?;", -1, &pStmt, nullptr); for(uint64_t i=0; i<100000; i++) { sqlite3_bind_int64(pStmt, 1, random_id(mt)); while(sqlite3_step(pStmt) == SQLITE_ROW) { int64_t id = sqlite3_column_int64(pStmt, 0); double lon = sqlite3_column_double(pStmt, 1); double lat = sqlite3_column_double(pStmt, 2); /* some process */ } sqlite3_reset(pStmt); sqlite3_clear_bindings(pStmt); } sqlite3_finalize(pStmt);
prepared_statementは1回実行するたびに初期化する必要があります。sqlite3_reset()はprepared_statementを実行可能な状態にリセットしますが、bindしたデータはそのまま保持されます。sqlite3_clear_bindings()を実行するとbindしたデータがクリアされます。また、sqlite3_bind_xxxx()は他と違ってインデックスが1から始まるので注意。
詳細は以下のドキュメントを参照してください。
- SQLite - An Introduction To The SQLite C/C++ Interface
https://www.sqlite.org/cintro.html
blobの扱い
SQLiteのblob型はバイナリデータをそのまま格納出来ます。操作方法も他とほとんど同じですが、sqlite3_bind_blob()の第5引数に注意が必要です。第5引数はbindするデータが不変(いわゆるconstな変数)なのか一時変数なのなのかを指定します。設定可能な値は以下のとおりです。
- SQLITE_STATIC
- SQLITE_TRANSIENT
SQLITE_TRANSIENTを指定すると一時変数として判断され、変数内容がすぐにコピーされます。それに対してSQLITE_STATICを指定するとコピーされず、sqliteがデータベースに書き出す際に変数を直接参照します。もし変数がスコープを外れるなどして破棄されると正常に書き込めません。大抵は一時変数だと思いますので、SQLITE_TRANSIENTを指定するのが無難です。
// insert sqlite3_bind_int(pStmtInsertData, 1, id); sqlite3_bind_blob(pStmtInsertData, 2, pData, dataSize, SQLITE_TRANSIENT); while(sqlite3_step(m_pStmtInsertData) == SQLITE_BUSY); // select sqlite3_stmt *pStmt; sqlite3_prepare_v2(db, querySelectData, -1, &pStmt, nullptr); sqlite3_bind_int(pStmt, 1, m_FileID); while(sqlite3_step(pStmt) == SQLITE_ROW){ int size = sqlite3_column_bytes(pStmt, 0); const char* buf = static_cast<const char*>(sqlite3_column_blob(pStmt, 0)); }
詳細は以下のドキュメントを参照してください。
- SQLite - Constants Defining Special Destructor Behavior
https://www.sqlite.org/c3ref/c_static.html
エラーハンドリング
SQLiteのエラー処理についてです。エラーは関数の戻り値で判定出来ます。また、以下の関数を呼ぶと直近のエラーに関するエラーコード及びエラーメッセージが取得出来ます。
- int sqlite3_errorcode(sqlite3 *db)
- const char *sqlite3_errmsg(sqlite3 *db)
int rtn = sqlite3_open("test_bench.db", &amp;db); if(rtn != SQLITE_OK){ cerr << “[ERR] Failed to open db:” << sqlite3_errmsg(db) << endl; }
また、関数によっては引数にエラーメッセージを返すものがあります。
char *errmsg = nullptr; int ret = sqlite3_exec(db, “SELECT * FROM nodes_tbl”, nullptr, nullptr, &errmsg) if(errmsg != nullptr) { cerr << “[ERR]:” << errmsg << endl; sqlite3_free(errmsg); }
sqlite3_errmsg()で取得した内容はsqliteモジュール内部で管理されているので、何もする必要はありません。逆に、sqlite3_exec()など引数にセットされたエラーメッセージは不要になった段階でsqlite3_free()で開放する必要があります。
詳細は以下のドキュメントを参照してください。
- SQLite - Error Codes And Messages
https://www.sqlite.org/capi3ref.html#sqlite3_errcode - SQLite - The Error And Warning Log
https://www.sqlite.org/errlog.html
sqliteの動作設定について
マルチスレッド関連の動作モード
概要
sqliteにはマルチスレッドに関連する動作モードが3つあります。
- Single-threadモード
シングルスレッド用。全ての排他制御が無効となる。 - Multi-threadモード
マルチスレッド用。
同じDBに対して複数のconnectionを作って操作する場合に使用する。connectionとprepared_statementの排他制御が無効となるので、同じコネクションを別スレッドで使いまわすSigleton的な使い方は不可(同時アクセスすると壊れる可能性がある)。 - Serializedモード
マルチスレッド用。全ての排他制御が有効となる。connection等を複数のスレッドで使いまわすような使い方でも動作が保証される。
C/C++での使い方
C/C++ではsqlite3_config()で動作モードを設定します。設定しなかった場合のデフォルト値はsqliteのコンパイルオプションに依存します。
- SQLITE_CONFIG_SINGLETHREAD
- SQLITE_CONFIG_MULTITHREAD
- SQLITE_CONFIG_SERIALIZED
// set Serialized mode sqlite3_config(SQLITE_CONFIG_SERIALIZED);
データベース毎に動作モードを設定したいときはsqlite3_open_v2()の第三引数にフラグを指定します。
- Multi-threadモード:SQLITE_OPEN_NOMUTEX
- Serializedモード:SQLITE_OPEN_FULLMUTEX
sqlite3 *db; int rtn = sqlite3_open_v2( “./test.db”, &db, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX, nullptr);
詳細は以下を参照してください。
- SQLite - Using SQLite In Multi-Threaded Applications
https://www.sqlite.org/threadsafe.html - SQLite - SQLite C Interface > Configuration Options
https://www.sqlite.org/c3ref/c_config_covering_index_scan.html
各動作モードのパフォーマンス
各動作モードでどれぐらいパフォーマンスが変化するのかベンチマークを取ってみました。内容は単純でINSERTやSELECTを規定回数繰り返すプログラムを走らせ、完了までの時間をtimeで測定しています。結果は以下のようになりました。
- real:プログラムの呼び出しから終了までにかかった実時間
- user:プログラム自体の処理時間
- sys:kernelの処理時間
- others:I/O待ちなどの時間
INSERTでは予想通り排他制御を全く行わないSingle-threadモードが一番速いようです。SELECTだけだと排他制御は関係ないので変わりありません(SELECTは処理時間のほとんどがI/O待ちで埋まってますね)。
Memory-Mapped I/O
概要
sqliteはデフォルトではread()/write()を使ってデータベース(ファイル)にアクセスします。より高速にアクセスしたい場合はmmapを使用するように設定出来ます。ただし、必ずしも速くなるわけではないこと、何らかの原因でメモリ上にマップされたデータが破壊されるとデータベース本体(ファイル)も破損する可能性があることに注意が必要です。
C/C++での使い方
sqlite3_config()でSQLITE_CONFIG_MMAP_SIZEを設定するとmmapアクセスが有効になります。第1引数はmmapバッファのデフォルトサイズ、第2引数はmmapバッファの上限値です。0を指定するとmmap無効(デフォルト)、-1などの負数を指定するとsqlite3のデフォルト値(コンパイルオプションに依存)が設定されます。
int64_t default_mmap_size = 256 * 1024 * 1024; int64_t max_mmap_size = 512 * 1024 * 1024; sqlite3_config(SQLITE_CONFIG_MMAP_SIZE, default_mmap_size, max_mmap_size);
コネクションごとに設定したい場合は、PRAGMA mmap_sizeを使用します。設定値はmmapバッファのデフォルトサイズを指定します。
// enable mmap sqlite3_exec(db, “PRAGMA mmap_size=268435456”, null); // disable mmap sqlite3_exec(db, “PRAGMA mmap_size=0”, null);
詳細は以下のドキュメントを参照してください。
- SQLite - Memory-Mapped I/O
https://www.sqlite.org/mmap.html - SQLite - SQLite C Interface > Configuration Options
https://www.sqlite.org/c3ref/c_config_covering_index_scan.html
mmapのパフォーマンス
mmapを無効/有効にしてベンチマーク測定を行い結果を比較してみました。条件は以下のとおりです。
-
- 動作モード:Serializedモード
- レコード数100,000,000件(サイズ:2.5GByte)のデータベースからランダムに100,000件をSELECTする
- MMAPバッファサイズ:次の4通りで試行
- 0MByte(mmap無効)
- 256MByte
- 512MByte
- 1024MByte
結果は以下のようになりました。ベンチマークに使ったプログラム/クエリはIDをランダムに生成して1件毎にselectしているせいか処理時間のほとんどがI/O待ちです。mmapを有効にしてバッファサイズを増やすとI/O待ち時間が改善しているのが分かりますね。
cmakeでライブラリにsqliteを追加する方法
以下の行をCMakeLists.txtに追記するだけです。
- target_link_libraries(xxxx sqlite3)
参考
この記事は以下の内容を参考にさせていただきました。
- SQLite - Documentation
https://www.sqlite.org/docs.html - Qiita @staatsschreiber - sqlite3のblob操作API
https://qiita.com/staatsschreiber/items/bc43c9b5c9cda2d7a5ea - Qiita @TomK - SQLiteで日付時刻を扱う際のポイント
https://qiita.com/TomK/items/132831ab45e2aba822a8 - MASATOの開発日記 - SQLite性能評価その3
http://www.sutosoft.com/room/archives/000454.html - askyb.com - C++ SQLite Example with Atomic Transaction
http://www.askyb.com/cpp/c-sqlite-example-with-atomic-transaction/ - きままにブログ - SQLite3のテストだよ!
http://staryoshi.hatenablog.com/entry/2015/10/28/142925 - stackoverflow - sqlite3 preparing, binding and resetting statements
https://stackoverflow.com/questions/3822115/sqlite3-preparing-binding-and-resetting-statements - 技術は熱いうちに打て! - 【cocos2d-x】sqlite3のドキュメントを訳してみた
http://blog.dalt.me/898 - MONOist - Android Tips(21): SQLiteデータベースのチューニング (1/2)
http://monoist.atmarkit.co.jp/mn/articles/1209/26/news004.html - Qt wiki - How to Store and Retrieve Image on SQLite
https://wiki.qt.io/How_to_Store_and_Retrieve_Image_on_SQLite - 作っても動かない - データベースを使う(sqlite3)
https://blogs.yahoo.co.jp/hmfjm910/10815002.html - ぴょぴょぴょ? - kernelの cache を強制開放する方法
http://d.hatena.ne.jp/pyopyopyo/20110906/p1 - Siv3D 開発ブログ - C++11 の乱数ライブラリ <random>
http://siv3d.hateblo.jp/entry/2013/02/17/231829
SQLite を始めましたが,中断してしまっています。マルチスレッドは皆目,チンプンカンプンです。いろいろと教えてください。
こんにちは。
私も同じような感じなのです(汗)。とあるプログラムを改造するのに必要になって触りだしたところです。
理解が進んだらこの記事を充実させていきたいですね。