📝 3行まとめ
- DictSQLiteが高機能化で破綻したので、シンプルで速いNanaSQLiteをゼロから作り直した
- 「とりあえずRustにすれば速くなる」と思っていたら逆に遅くなった失敗をした
- ベンチマーク計測でボトルネックを特定してから改善したら、バッチ処理が19.9倍速になった
🔗 公式サイト: https://nanasqlite.disnana.com/
🐙 GitHub: https://github.com/disnana/nanasqlite
はじめに
「SQLiteをdictで使えるライブラリを作ろう」——そのシンプルな動機から始まったDictSQLiteは、気づけば誰も幸せにしないモンスターになっていました。
そこからNanaSQLiteを作り直す中で、設計の失敗とRustの罠という2つの痛い経験をしました。
この記事は「なぜ作り直したか」「なぜRustで失敗したか」「どう立て直したか」の開発記録です。
1. DictSQLiteが破綻した理由
DictSQLiteの当初のコンセプトはシンプルでした。
db["key"] = "value" # これだけでSQLiteに保存したい
しかし機能追加を重ねるうちに、こうなりました。
db = DictSQLite(
"data.db",
encryption_key="secret",
cache_size=1000,
journal_mode="WAL"
)
with db.transaction():
db.set("key", "value", encrypt=True)
気がついたら「重い・複雑・バグが多い」ライブラリができあがっていた。問題を整理すると以下の通りです。
| 問題 | 具体的な症状 |
|---|---|
| 重い | 暗号化・複雑な最適化処理がオーバーヘッドになる |
| 複雑 | オプションが多すぎて使い方がわからない |
| 不具合 | 機能間の相互作用でバグが頻発 |
| 学習コスト | ドキュメントを読まないと動かせない |
「ただSQLiteをdictで使いたいだけ」なのに、こんなに難しくする必要があったのか?
その問いがNanaSQLiteの出発点でした。
2. 「とりあえずRustで高速化」は失敗した
最初の作り直しでは、「コア部分をRustで書けば解決する」と安直に考えていました。
失敗談:Rustにすれば無条件に速くなるわけではない
ボトルネックを計測せず、コア部分をRustで実装した結果、PythonとRust間のFFI(外部関数インターフェース)のオーバーヘッドが支配的になり、純粋なPython実装より処理速度が悪化しました。「速い言語を使う」だけでは速くならないことを身をもって体験しました。
原因は明快でした。データのやり取りのたびに発生するPython↔Rustの型変換コストが、Rustの高速化メリットを上回っていたのです。
3. 計測駆動で作り直したNanaSQLiteのアーキテクチャ
反省を踏まえ、今回は**「計測→特定→改善→再計測」**のサイクルを徹底しました。
設計の優先順位を変えた
DictSQLite: 高機能 → 複雑 → 重い
NanaSQLite: シンプル → 速い → 使いやすい
あえて削除した機能と、追加した機能を明確に決めました。
削除したもの(意図的に)
- ❌ pickleシリアライズ(セキュリティリスクの排除)
- ❌ 複雑な最適化オプション(設定不要にする)
- ❌ 高度なトランザクション管理
追加したもの
- ✅ JSON自動シリアライズ(安全・他言語との互換性)
- ✅ 即時永続化(書いた瞬間にSQLiteへ保存)
- ✅ スマートキャッシング(遅延ロード・一括ロードの切り替え)
- ✅ async/await完全対応(Discord bot・FastAPIへの組み込みを想定)
- ✅ Pydanticサポート
- ✅ セキュリティ機能(strict_sql_validation, ReDoS対策)
内部の速度を支えるSQLite設定
-- 設定不要で自動適用される最適化
PRAGMA journal_mode=WAL; -- 並行書き込みを可能にする
PRAGMA synchronous=NORMAL; -- 安全性と速度のバランス
PRAGMA mmap_size=268435456; -- 256MBメモリマップで高速化
PRAGMA cache_size=-64000; -- 64MBキャッシュ
PRAGMA temp_store=MEMORY; -- 一時領域はメモリに
これらを自動適用することで、ユーザーは何も意識しなくても最適化された状態で使えます。
標準のsqlite3ではなく、低レベル高速バインディングのapswを採用しているのも速度の要因のひとつです。
Rustを「使う箇所だけ」に絞った
前回の失敗を踏まえ、今回はプロファイラで計測して本当にFFIオーバーヘッドに見合う箇所のみにRustを採用しました。「とりあえず全体をRust化」ではなく、ピンポイントで効かせる設計です。
4. ベンチマーク結果
同一環境(Windows 11、Python 3.11、SSD)でDictSQLiteと比較した結果です。
| 操作 | DictSQLite | NanaSQLite | 倍率 |
|---|---|---|---|
| 初期化 | 84.88ms | 11.00ms | ⚡ 7.7倍速 |
| 単純書き込み(1000件) | 101.08ms | 110.86ms | ほぼ同等 |
| バッチ書き込み(1000件) | 88.88ms | 4.47ms | ⚡ 19.9倍速 |
| 読み取り(1000件) | 31.17ms | 19.25ms | ⚡ 1.6倍速 |
| 一括ロード+読み取り | — | 3.78ms | — |
| ネストデータ(100件) | 10.02ms | 8.84ms | ⚡ 1.1倍速 |
| 混合操作(1500操作) | 101.80ms | 123.62ms | 0.8倍 |
単純書き込みと混合操作はDictSQLiteと同等〜若干遅い
1件ずつの書き込みは、SQLiteのトランザクションオーバーヘッドが支配的なため速度差が出にくいです。大量データにはbatch_update()を必ず使ってください。
スループット比較
| 操作 | DictSQLite | NanaSQLite |
|---|---|---|
| バッチ書き込み | 11,251件/秒 | 223,494件/秒 |
| 読み取り(遅延ロード) | 32,079件/秒 | 51,961件/秒 |
| 読み取り(一括ロード) | — | 4,570,384件/秒 |
なぜバッチ処理でここまで差が出るのか
# DictSQLite — 1件ずつ処理するたびに余分なロジックが走る
for key, value in data.items():
# 暗号化チェック → pickle/JSON判定 → トランザクション管理 → 保存
db[key] = value
# NanaSQLite — JSON一括変換 → SQLite一括INSERTで直行
db.batch_update(data)
余計な判定処理を削ぎ落としたことで、バッチ処理時のオーバーヘッドが激減しました。
「機能を削る」ことがパフォーマンス向上に直結した典型例です。
5. 実装例
基本的な使い方(3行)
from nanasqlite import NanaSQLite
db = NanaSQLite("myapp.db")
db["user"] = {"name": "Nana", "age": 20, "tags": ["admin"]}
print(db["user"]["name"]) # 'Nana'
Pydanticモデルと組み合わせる
from pydantic import BaseModel
from nanasqlite import NanaSQLite
class User(BaseModel):
name: str
age: int
with NanaSQLite("app.db") as db:
db.set_model("user_1", User(name="Nana", age=20))
user = db.get_model("user_1", User)
print(type(user)) # <class 'User'> 型付きで返ってくる
Discord bot / FastAPIでの非同期対応
from nanasqlite import AsyncNanaSQLite
import asyncio
async def main():
async with AsyncNanaSQLite("bot.db", max_workers=10) as db:
await db.aset("guild_123", {"prefix": "!", "lang": "ja"})
config = await db.aget("guild_123")
print(config)
asyncio.run(main())
6. 開発環境
pip install nanasqlite
1日で集中開発し、以下を整備してリリースしました。
- pytest による自動テスト完備
- GitHub Actions によるCI/CD(push → 自動テスト → PyPI自動デプロイ)
- Dependabot によるライブラリの自動更新
実際に自分が運営しているDiscordボット・VPSホスティングサービス「Disnana」の本番環境でも使用中です。
おわりに
DictSQLiteでの設計的な失敗、無計画なRust化での速度悪化、その2つの失敗があったからこそNanaSQLiteは生まれました。
「シンプルにすること」と「機能を削ること」は勇気が要りますが、それが最大のパフォーマンス改善につながるという実感を得られた開発でした。
「SQLiteをdictで使いたいだけ」な方は、ぜひ触ってみてください。
🔗 公式サイト: https://nanasqlite.disnana.com/
🐙 GitHub: https://github.com/disnana/nanasqlite (Starいただけると泣いて喜びます!)