SQLiteは、アプリケーションに埋め込む人気のあるリレーショナルデータベースです。ただし、避けるべき多くの落とし穴や落とし穴があります。この記事では、ORMの使用、ディスクスペースの再利用方法、クエリ変数の最大数、列のデータ型、大きな整数の処理方法など、いくつかの落とし穴(およびそれらを回避する方法)について説明します。
はじめに
SQLiteは人気のあるリレーショナルデータベース(DB)システムです 。 MySQLなどの兄貴と非常によく似た機能セットを備えています 、クライアント/サーバーベースのシステムです。ただし、SQLiteは埋め込まれています データベース 。静的(または動的)ライブラリとしてプログラムに含めることができます。 これにより、展開が簡素化されます 、個別のサーバープロセスは必要ないため。バインディングとラッパーライブラリを使用すると、ほとんどのプログラミング言語でSQLiteにアクセスできます 。
私は博士論文の一部としてBSyncを開発している間、SQLiteを幅広く使用してきました。 この記事は、開発中に遭遇したトラップと落とし穴の(ランダムな)リストです 。それらがお役に立てば幸いです。また、私がかつて行ったのと同じ間違いをしないようにしてください。
落とし穴と落とし穴
ORMライブラリの使用には注意が必要です
オブジェクトリレーショナルマッピング(ORM)ライブラリは、具体的なデータベースエンジンとその構文(特定のSQLステートメントなど)から高レベルのオブジェクト指向APIに詳細を抽象化します。そこには多くのサードパーティライブラリがあります(ウィキペディアを参照)。 ORMライブラリにはいくつかの利点があります:
- 開発中の時間を節約 、コード/クラスをDB構造にすばやくマッピングするため
- それらは多くの場合クロスプラットフォームです 、つまり、具体的なDBテクノロジー(SQLiteとMySQLなど)の置き換えを許可します。
- スキーマ移行用のヘルパーコードを提供します 。
ただし、いくつかの重大な欠点もあります 次の点に注意してください:
- データベースでの作業を表示します 簡単 。ただし、実際には、DBエンジンには、知っておく必要のある複雑な詳細があります 。何かがうまくいかない場合、例えばORMライブラリが理解できない例外をスローした場合、または実行時のパフォーマンスが低下した場合、 ORMを使用して節約した開発時間は、問題のデバッグに必要な労力によってすぐに使い果たされます 。たとえば、指標がわからない場合 つまり、必要なすべてのインデックスが自動的に作成されなかった場合、ORMによって引き起こされるパフォーマンスのボトルネックのトラブルシューティングに苦労するでしょう。本質的には、フリーランチはありません。
- 具体的なDBベンダーの抽象化により、ベンダー固有の機能にアクセスするのが難しいか、まったくアクセスできない。 。
- 計算のオーバーヘッドがあります SQLクエリを直接記述して実行する場合と比較して。ただし、より高いレベルの抽象化に切り替えるとパフォーマンスが低下するのが一般的であるため、この点は実際には議論の余地があると思います。
結局、ORMライブラリの使用は個人的な好みの問題です。 その場合は、予期しない動作やパフォーマンスのボトルネックが発生した場合に、リレーショナルデータベースの癖(およびベンダー固有の警告)について学習する必要があることを覚悟してください。
そうでない場合 ORMライブラリを使用する場合は、DBのスキーマの移行を処理する必要があります。 。これには、テーブルスキーマを変更し、保存されたデータを何らかの方法で変換する移行コードの記述が含まれます。 「migrations」または「version」と呼ばれるテーブルを作成することをお勧めします。このテーブルには、単一の行と列があり、スキーマのバージョンを格納するだけです。単調に増加する整数を使用します。これにより、移行機能は、どの移行をまだ適用する必要があるかを検出できます。移行ステップが正常に完了すると、移行ツールコードはUPDATE
を介してこのカウンターをインクリメントします SQLステートメント。
テーブルを作成するたびに、SQLiteは自動的にINTEGER
を作成します rowid
という名前の列 あなたのために – WITHOUT ROWID
を指定した場合を除く 条項(ただし、この条項について知らなかった可能性があります)。 rowid
行は主キー列です。このような主キー列も自分で指定する場合(たとえば、構文some_column INTEGER PRIMARY KEY
を使用する場合) )この列は単にエイリアスになります rowid
の場合 。かなり不可解な言葉で同じことを説明している詳細については、ここを参照してください。 SELECT * FROM table
に注意してください ステートメントは rowid
を含める デフォルト– rowid
を要求する必要があります 明示的に列。
そのPRAGMA
を確認します 本当に機能します
とりわけ、 PRAGMA
ステートメントは、データベース設定を構成したり、さまざまな機能を呼び出したりするために使用されます (公式ドキュメント)。ただし、変数を設定しても実際には効果がない場合がある、文書化されていない副作用があります 。つまり、機能せず、サイレントに失敗します。
たとえば、次のステートメントを指定された順序で発行すると、最後 ステートメントはありません 効果があります。変数auto_vacuum
まだ値0
(NONE
)、正当な理由はありません。
PRAGMA journal_mode = WAL
PRAGMA synchronous = NORMAL
PRAGMA auto_vacuum = INCREMENTAL
Code language: SQL (Structured Query Language) (sql)
PRAGMA variableName
を実行すると、変数の値を読み取ることができます。 等号と値を省略します。
上記の例を修正するには、別の順序を使用します。行の順序3、1、2を使用すると、期待どおりに機能します。
このようなチェックをプロダクションに含めることもできます。 これらの副作用は、具体的なSQLiteのバージョンとその構築方法によって異なる可能性があるためです。 本番環境で使用されるライブラリは、開発中に使用したライブラリとは異なる場合があります。
デフォルトでは、SQLiteデータベースファイルのサイズは単調に増加しています 。行を削除すると、特定のページのみが無料としてマークされます 、INSERT
に使用できるようにします 将来のデータ。 実際にディスクスペースを再利用し、パフォーマンスを高速化するには、次の2つのオプションがあります。
-
VACUUM
を実行します ステートメント 。ただし、これにはいくつかの副作用があります。- DB全体をロックします。
VACUUM
の間は、同時操作を実行できません。 操作。 - 内部的に再作成されるため(大規模なデータベースの場合)、長い時間がかかります。 DBを別の一時ファイルに入れ、最後に元のデータベースを削除して、その一時ファイルに置き換えます。
- 一時ファイルは追加を消費します 操作の実行中のディスク容量。したがって、
VACUUM
を実行することはお勧めできません。 ディスク容量が不足している場合に備えて。それでも実行できますが、(freeDiskSpace - currentDbFileSize) > 0
であることを定期的に確認する必要があります。 。
- DB全体をロックします。
-
PRAGMA auto_vacuum = INCREMENTAL
を使用します 作成時 DB。 このPRAGMA
を作成します 最初 ファイル作成後のステートメント!これにより、内部のハウスキーピングが可能になり、PRAGMA incremental_vacuum(N)
を呼び出すたびにデータベースがスペースを再利用できるようになります。 。この呼び出しは、最大N
を再利用します ページ。公式ドキュメントには、詳細と、auto_vacuum
のその他の可能な値が記載されています。 。- 注:
PRAGMA incremental_vacuum(N)
を呼び出すと、どのくらいの空きディスク容量(バイト単位)が得られるかを決定できます。 :PRAGMA freelist_count
によって返される値を乗算しますPRAGMA page_size
を使用 。
- 注:
より良いオプションはあなたの文脈に依存します。 非常に大きなデータベースファイルの場合は、オプション2をお勧めします 、オプション1は、データベースがクリーンアップされるのを数分または数時間待つことでユーザーを苛立たせるためです。 オプション1は小規模なデータベースに適しています 。その追加の利点は、パフォーマンス DBのが向上します (オプション2の場合はそうではありません)。レクリエーションにより、データの断片化による副作用が排除されるためです。
デフォルトでは、クエリで使用できる変数(「ホストパラメータ」)の最大数は999にハードコードされています (ここのセクション単一のSQLステートメント内のホストパラメータの最大数を参照してください。 )。 コンパイル時であるため、この制限は異なる場合があります パラメータ。デフォルト値はあなた(またはSQLiteをコンパイルした人)が変更した可能性があります。
アプリケーションが(任意に大きな)リストをDBエンジンに提供することは珍しくないため、これは実際には問題があります。たとえば、大量に送信する場合-DELETE
(またはSELECT
)たとえば、IDのリストに基づく行。
DELETE FROM some_table WHERE rowid IN (?, ?, ?, ?, <999 times "?, ">, ?)
Code language: SQL (Structured Query Language) (sql)
エラーがスローされ、完了しません。
これを修正するには、次の手順を検討してください。
- リストを分析し、小さなリストに分割します
- 分割が必要な場合は、必ず
BEGIN TRANSACTION
を使用してください およびCOMMIT
単一のステートメントが持っていたであろう原子性をエミュレートするために 。 - 他の
?
も考慮してください。 着信リストに関連しないクエリで使用する可能性のある変数 (例:?
ORDER BY
で使用される変数 条件)、合計 変数の数が制限を超えていません。
別の解決策は、一時テーブルを使用することです。アイデアは、一時テーブルを作成し、クエリ変数を行として挿入してから、その一時テーブルをサブクエリで使用することです。例:
DROP TABLE IF EXISTS temp.input_data
CREATE TABLE temp.input_data (some_column TEXT UNIQUE)
# Insert input data, running the next query multiple times
INSERT INTO temp.input_data (some_column) VALUES (...)
# The above DELETE statement now changes to this one:
DELETE FROM some_table WHERE rowid IN (SELECT some_column from temp.input_data)
Code language: SQL (Structured Query Language) (sql)
SQLiteのタイプアフィニティに注意してください
SQLite列は厳密に型指定されているわけではなく、変換は必ずしも期待どおりに行われるとは限りません。 提供するタイプはヒントです 。 SQLiteは多くの場合任意ののデータを保存します オリジナルを入力します タイプし、変換がロスレスの場合にのみ、データを列のタイプに変換します。たとえば、"hello"
を挿入するだけです。 INTEGER
への文字列 桁。 SQLiteは文句を言ったり、型の不一致について警告したりしません。逆に、SELECT
によって返されるデータを期待できない場合があります INTEGER
のステートメント 列は常にINTEGER
。これらのタイプヒントは、SQLite-speakでは「タイプアフィニティ」と呼ばれます。ここを参照してください。新しいテーブルを作成するときに指定する列タイプの意味をよりよく理解するために、SQLiteマニュアルのこの部分をよく調べてください。
SQLiteは署名済みをサポートしています 64ビット整数 、保存したり、計算を実行したりできます。つまり、-2^63
の数字のみ to (2^63) - 1
符号を表すために1ビットが必要なため、サポートされています!
つまり、より大きな数で作業することを期待している場合、たとえば128ビット(符号付き)整数または符号なし64ビット整数。必要があります データをテキストに変換します 挿入する前に 。
これを無視して、より大きな数値を(整数として)挿入すると、恐怖が始まります。 SQLiteは文句を言わず、丸められた 代わりに番号! たとえば、2 ^ 63(すでにサポートされている範囲外)を挿入すると、SELECT
ed値は9223372036854776000であり、2 ^ 63=9223372036854775808ではありません。ただし、使用するプログラミング言語とバインディングライブラリによっては、動作が異なる場合があります。たとえば、Pythonのsqlite3バインディングは、そのような整数のオーバーフローをチェックします!
REPLACE()
を使用しないでください ファイルパスの場合
相対または絶対ファイルパスをTEXT
に保存するとします。 SQLiteの列(例:実際のファイルシステム上のファイルを追跡します。 3つの行の例を次に示します。
foo/test.txt
foo/bar/
foo/bar/x.y
ディレクトリの名前を「foo」から「xyz」に変更するとします。どのSQLコマンドを使用しますか?これ?
REPLACE(path_column, old_path, new_path)
Code language: SQL (Structured Query Language) (sql)
奇妙なことが起こり始めるまで、これは私がしたことです。 REPLACE()
の問題 すべてを置き換えるということです 発生。パスが「foo/bar / foo /」の行があった場合は、REPLACE(column_name, 'foo/', 'xyz/')
結果は「xyz/bar / foo/」ではなく「xyz/bar / xyz /」になるため、大混乱を引き起こします。
より良い解決策は次のようなものです
UPDATE mytable SET path_column = 'xyz/' || substr(path_column, 4) WHERE path_column GLOB 'foo/*'"
Code language: SQL (Structured Query Language) (sql)
4
古いパス(この場合は「foo /」)の長さを反映します。 GLOB
を使用したことに注意してください LIKE
の代わりに 開始する行のみを更新します 「foo/」を使用します。
結論
SQLiteは素晴らしいデータベースエンジンであり、ほとんどのコマンドが期待どおりに機能します。ただし、先ほど紹介したような特定の複雑さには、開発者の注意が必要です。この記事に加えて、SQLiteの公式警告ドキュメントも必ずお読みください。
過去に他の警告に遭遇したことがありますか?もしそうなら、コメントで知らせてください。