現在受け入れられている回答に同意しないため、独自の回答を追加しています。操作はスレッドセーフではないと述べていますが、これは明らかに間違っています。SQLite は現在のプラットフォームに適したファイル ロックを使用して、すべてのアクセスがACIDに準拠していることを確認します。
Unix システムでは、これはファイルハンドルごとのロックであるfcntl()
orflock()
ロックになります。その結果、毎回新しい接続を作成する投稿されたコードは、常に新しいファイルハンドルを割り当てるため、SQLite 独自のロックによってデータベースの破損が防止されます。この結果として、NFS 共有などで SQLite を使用することは、通常、特に信頼性の高いロックを提供しないため、お勧めできません (ただし、NFS の実装によって異なります)。
@abernert が既にコメントで指摘しているように、SQLite にはスレッドに関する問題がありましたが、これはスレッド間で単一の接続を共有することに関連していました。彼も言及しているように、これは、アプリケーション全体のプールを使用すると、2 番目のスレッドがプールからリサイクルされた接続を引き出すと、ランタイム エラーが発生することを意味します。これらはまた、テスト (負荷が軽く、使用中のスレッドが 1 つだけ) では気付かないかもしれないが、後で簡単に頭痛の種になる可能性のある厄介なバグの一種です。Martijn Pieters が後に提案したスレッドローカル プールはうまく機能するはずです。
バージョン3.3.1のSQLite FAQで概説されているように、スレッドがロックを保持していない限り、スレッド間で接続を渡すことは実際には安全です。全般的。賢明な接続プーリングの実装では、プール内の接続を置き換える前にすべてがコミットまたはロールバックされていることが常に保証されるため、共有に対する Python チェックがなければ、実際にはアプリケーション グローバル プールは安全である可能性があります。より新しいバージョンの SQLite が使用されたとしても、そのまま維持されると思います。確かに、私の Python 2.7.3 システムには3.7.9を報告するモジュールがありますが、それでもsqlite3
sqlite_version_info
RuntimeError
複数のスレッドからアクセスする場合。
いずれにせよ、チェックが存在する間は、基礎となる SQLite ライブラリがそれをサポートしていても、接続を効果的に共有することはできません。
あなたの元の質問に関しては、確かに毎回新しい接続を作成することは、接続のプールを維持するよりも効率的ではありませんが、これはスレッドローカルプールである必要があるとすでに述べられていますが、これは実装するのが少し面倒です. データベースへの新しい接続を作成する際のオーバーヘッドは、基本的にファイルを開き、ヘッダーを読み取って有効な SQLite ファイルであることを確認することです。ステートメントを実際に実行するオーバーヘッドは、look を取り出してかなりの量のファイル I/O を実行する必要があるため、より高くなります。そのため、作業の大部分は、実際にはステートメントの実行および/またはコミットまで延期されます。
しかし、興味深いことに、少なくとも私が見た Linux システムでは、ステートメントを実行するコードは、ファイル ヘッダーを読み取る手順を繰り返します。その結果、新しい接続を開くことは、最初の読み取り以降、それほど悪くはありません。接続を開くと、ヘッダーがシステムのファイルシステム キャッシュに取り込まれます。つまり、単一のファイルハンドルを開くオーバーヘッドに要約されます。
また、コードが高い同時実行性にスケーリングされることを期待している場合、SQLite は適切な選択ではない可能性があることも付け加えておきます。彼ら自身のウェブサイトが指摘しているように、同時実行スレッドの数が増加するにつれて、単一のグローバルロックを介してすべてのアクセスを圧迫しなければならないというパフォーマンスへの影響が発生し始めるため、高い同時実行にはあまり適していません. 利便性のためにスレッドを使用している場合は問題ありませんが、高度な同時実行性を本当に期待している場合は、SQLite を使用しないでください。
要するに、毎回開くというあなたのアプローチは、実際にはそれほど悪いことではないと思います。スレッド ローカル プールによってパフォーマンスが向上する可能性はありますか? おそらくそうだ。このパフォーマンスの向上は顕著でしょうか? 私の意見では、接続率が非常に高い場合を除き、その時点で多くのスレッドが存在するため、SQLite から離れた方がよいでしょう。使用することに決めた場合は、プールに戻す前に接続をクリーンアップすることを確認してください。SQLAlchemyには、すべての ORM レイヤーを最上位にしたくない場合でも役立つ接続プール機能があります。
編集
非常に合理的に指摘されているように、実際のタイミングを添付する必要があります。これらは、かなり低電力の VPS からのものです。
>>> timeit.timeit("cur = conn.cursor(); cur.execute('UPDATE foo SET name=\"x\"
WHERE id=3'); conn.commit()", setup="import sqlite3;
conn = sqlite3.connect('./testdb')", number=100000)
5.733098030090332
>>> timeit.timeit("conn = sqlite3.connect('./testdb'); cur = conn.cursor();
cur.execute('UPDATE foo SET name=\"x\" WHERE id=3'); conn.commit()",
setup="import sqlite3", number=100000)
16.518677949905396
約 3 倍の違いが見られますが、これは重要ではありません。ただし、絶対時間は依然としてミリ秒未満であるため、リクエストごとに多くのクエリを実行しない限り、最初に最適化する場所が他にある可能性があります。多くのクエリを実行する場合、合理的な妥協点は、要求ごとに新しい接続を作成することです (ただし、プールの複雑さを排除して、毎回再接続するだけです)。
読み取り (つまり、SELECT) の場合、毎回の接続の相対的なオーバーヘッドは高くなりますが、ウォール クロック時間の絶対的なオーバーヘッドは一貫している必要があります。
この質問の他の場所で既に説明したように、実際のクエリでテストする必要があります。結論に至るまでに行ったことを文書化したかっただけです。