1年ほど前に手がけた仕事では、C#+Windows.Forms+Entity Framework+System.Data.SQLite という組み合わせでアプリケーションを作成しました。
次に始まる開発でもこの組み合わせにするつもりですが、いろいろとやりにくかった点をまとめておきたいと思います。まずは、SQLiteの日時カラムについて。
SQLite3には日時を表す内部データ表現が存在せず、代わりにTEXTやNumericなどのカラムによくある日付の表現(unixepoch, julianday, iso8601, C#のTicksなどなんでもいい)で入力します。取り出すときは組み込みのdatetime関数などを使ったり、アプリケーションで変換してDateTimeオブジェクトや人の読めるStringにフォーマットしたりして使います。
System.Data.SQLite.dll を使うときは、DATETIME型で定義したカラムについては接続文字列で正しいDateTimeFormatを指定することで、自動変換機能が使えるので、C#からはデータベースにどういうフォーマットで保存されているかを意識しなくてよくなります。
さて、どうせ内部表現が隠蔽できるのなら、精度が高く軽量な内部データにしたほうが速くて効率もいいだろうと思います。それで前の案件ではC#のTicks形式で保存するようにしていました。実際どれほど速くなったのかその時はよく調べませんでしたが、それよりもデバッグ時にTkSQLiteなどのGUIツールを使ってSQLiteデータベースを直接見ようとしたときに、Ticksから"yyyy-DD-mm HH:MM:SS"などのフォーマットに変換する手間が面倒くさくて、Ticksなんかにしなければよかったと後悔しました。実際の計算は以下のようになります。
SELECT datetime(m.created_at/pow(10,7) - 62135629200, 'unixepoch', 'localtime') AS created_at FROM ...; |
秒以下の精度なんて必要ないので、最初から人間が読める形式にしたい。
それには DateTimeFormatを指定しないか、ISO8601 を使います。
それで、クエリが遅くなるのか調べてみました。
まず、テーブルを作成します。
CREATE TABLE log ( id INTEGER PRIMARY KEY AUTOINCREMENT, msg TEXT, dt DATETIME, created_at DATETIME ); |
インデックスなしの場合とありの場合でやります。
CREATE INDEX log_ix ON log (dt); |
次に100万件INSERTします。
for (int cnt = 0; cnt < 1000000; cnt++) { p_msg.Value = cnt; p_dt.Value = DateTime.Now; p_ca.Value = DateTime.Now; com.CommandText = "INSERT INTO log (msg,dt,created_at) VALUES (?, ?, ?)"; com.ExecuteNonQuery(); } |
最後にSELECTします。いったんid=50000の日時dtを取得し、その値にマッチするレコードを1件だけ取得しています。まあ、大体49000から50000件目の範囲のレコードがヒットしました。
INSERTとSELECTの時間をインデックスの有無、DateTimeFormatごとに計測します。
結果は以下の通り。
With index? | Query | Ticks | JulianDay | ISO8601 |
no | INSERT(100万件) | 16.70 s | 17.24 s | 27.32 s |
no | SELECT | 182 ms | 221 ms | 303 ms |
yes | INSERT(100万件) | 22.91 s | 25.11 s | 37.16 s |
yes | SELECT | <1ms | <1ms | <1ms |
INSERTに関しては Ticks < JulianDay << ISO8601 となっていて、データ長が大きくなるISO8601は不利なようです。
SELECTに関しても同様ですが、インデックスありの場合の結果はいずれも1ms以下で十分速いようです。
私の用途の場合はいちどに大量のINSERTは必要なく、SELECTが高速なら事足りるので、次からはISO8601を使おうと思います。
VisualStudio2008 ソリューション
SQLite3BulkInsertTimestamp.zip
追加: データ型の話はもうちょっと複雑みたいです。
SQLiteのデータ型
自分のテーブルについても調べてみました。
SELECT typeof(id), typeof(msg), typeof(dt), typeof(created_at) FROM log LIMIT 1; |
columns | Ticks | JulianDay | ISO8601 |
id | integer | integer | integer |
msg | text | text | text |
dt | integer | real | text |
updated_at | integer | real | text |
DATETIMEと宣言したものは実は全部TEXTに変換されているんじゃ、とか思いましたが、それぞれ異なっていますね。INSERTの値はADOのParameter経由で渡しているので、それぞれ適切なリテラルでSQL文を構成してくれているのだと思われます。ISO8601だけTEXT型で時間がかかるというのは納得いきました。
これも参考にした。
http://www.tamandua-webtools.net/sqlite3-date.html