SQLer 注目 SQLerのベンチマーク概要

SQLite向けの軽量JSON-firstマイクロORM。PydanticモデルをJSONドキュメントとして保存し、直感的なクエリ、FTS5検索、楽観的ロック、84K行/秒のバルク挿入を提供。

役割: メイン開発 @ サイドプロジェクト
期間: 2025年5月

SQLer

SQLite向けの軽量JSON-firstマイクロORM。Pydanticモデル(または軽量なdataclassモデル)をJSONドキュメントとして保存し、直感的なAPIでクエリ。同期・非同期の両方に対応。


なぜSQLer?

SQLiteは世界で最も普及しているデータベースであり、JSON1がドキュメントストアに変える。SQLerはその橋渡し役:Pydanticバリデーションとドキュメント型の柔軟性、ネストしたJSONの中まで探れるクエリビルダー、そして本物のデータ整合性 — 全てが単一ファイル、設定不要のデータベースで動く。

向いている場面: 高速プロトタイピング、組み込みアプリ状態(Electron、CLIツール、モバイル)、Pydanticバリデーション付きのJSON柔軟性、「数時間ではなく数分で動くストレージ」が必要な全ての場面。


クイックスタート

from sqler import SQLerDB, SQLerModel
from sqler.query import SQLerField as F

class User(SQLerModel):
    name: str
    age: int
    tags: list[str] = []

db = SQLerDB.in_memory()
User.set_db(db)

User(name="Alice", age=30, tags=["admin"]).save()
User(name="Bob", age=25, tags=["user"]).save()

admins = User.query().filter(F("tags").contains("admin")).all()
assert admins[0].name == "Alice"

モデルはPydantic — 型ヒント、バリデーション、デフォルト値がそのまま使える。保存は明示的:.save()_id を付けてselfを返す。クエリは F() 演算子でJSON対応のSQLにコンパイルされる。


2つのモデルバリアント

バリアントベースバリデーション用途
SQLerModelPydantic完全な型変換+エラーメッセージ本番、APIレイヤー
SQLerLiteModel@dataclassなし(高速、依存なし)ブラウザ/WASM、軽量スクリプト

LiteモデルはPyodide/WebAssemblyで動作 — Pydantic依存なし、Rustコンパイル不要。クエリAPIは両方で同一。

from dataclasses import dataclass
from sqler import SQLerDB, SQLerLiteModel

@dataclass
class User(SQLerLiteModel):
    __tablename__ = "users"
    name: str
    age: int

パフォーマンス

ベンチマークスイートの実測値(22シナリオ、Python 3.12、SQLite 3.50):

操作結果コンテキスト
バルク挿入84K行/秒bulk_upsert() 10K行時
インデックス効果470倍9.4ms → 0.02ms(create_index()
キャッシュヒット7,000倍14ms → 0.002ms(@cached_query
FTS検索0.28msデータ規模に関わらずサブミリ秒
バルク vs 単体5.3倍bulk_upsert vs save() ループ(10K行)
Liteモデル1.3倍dataclass版 vs Pydanticオーバーヘッド

インデックス効果: クエリが470倍高速に

最も重要な最適化:クエリ述語にインデックスを張ることdb.create_index("table", "field") でJSONパスに式インデックスを作成。インデックス付きクエリはテーブルサイズに関係なく~0.02msで安定する。


機能

クエリビルダー

F() 演算子でネストしたJSONへの直感的なフィルタ:

# 等値、比較、ソート
User.query().filter(F("age") > 30).order_by("name").all()

# ネストしたフィールドアクセス
User.query().filter(F("address")["city"] == "Kyoto").all()

# 配列操作
Order.query().filter(F(["items"]).any().where(
    (F("sku") == "RamenSet") & (F("qty") >= 2)
)).all()

# 包含、所属、除外
User.query().filter(F("tags").contains("admin")).all()
User.query().filter(F("tier").isin([1, 2])).all()
User.query().exclude(F("name").like("test%")).all()

他にも:betweenis_nullis_not_nullstartswithendswithglobor_filterdistinct_valuespaginate

リレーション

参照は { "_table": "addresses", "_id": 1 } としてJSONに保存され、ロード時にモデルインスタンスへ水和(hydrate)される:

class User(SQLerModel):
    name: str
    address: Address | None = None

loaded = User.from_id(user._id)
assert loaded.address.city == "Kyoto"  # 自動水和

kyoto_users = User.query().filter(
    User.ref("address").field("city") == "Kyoto"
).all()

楽観的ロック

SQLerSafeModel はレコードごとに _version を追跡。競合する更新は StaleVersionError を発生:

from sqler import SQLerSafeModel, StaleVersionError

class Account(SQLerSafeModel):
    owner: str
    balance: int

try:
    account.save()
except StaleVersionError:
    account.refresh()  # DBから再読込して判断

参照整合性

データベース制約なしの削除ポリシー:

  • restrict — 参照されている間は削除を拒否
  • set_null — 参照フィールドをクリアしてから削除
  • cascade — 依存レコードを再帰的に削除
author.delete_with_policy(on_delete="cascade")

validate_references() で漏れた孤立参照を検出可能。

全文検索

FTS5ベースの検索、ランキング、ハイライト対応:

from sqler import FTSIndex

fts = FTSIndex(Article, fields=["title", "content"])
fts.create(db)
fts.rebuild()

results = fts.search("Python")          # データ規模に関わらず0.28ms
ranked = fts.search_ranked("Python")    # BM25関連度スコア付き
snippets = fts.search_highlights("Python")  # ハイライト付き抜粋

同期・非同期

同じセマンティクスの並行API。同期でプロトタイプし、FastAPIへ書き換えなしで移行:

from sqler import AsyncSQLerDB, AsyncSQLerModel

async def main():
    db = AsyncSQLerDB.in_memory()
    await db.connect()
    # 同じクエリAPI、awaitするだけ

プロダクション機能

機能説明
bulk_upsert()84K行/秒のバッチ挿入
@cached_queryTTLベースの結果キャッシュ(LRU回収)
MetricsCollectorPrometheus/StatsD形式のクエリ監視
PooledSQLerDBWAL付きスレッドセーフな接続プール
エクスポート/インポートCSV、JSON、JSONL(同期+非同期)
スキーママイグレーションadd_field()remove_field()rename_field()
変更追跡is_dirty()get_dirty_fields()TrackedModel
トランザクション対応with db.transaction(): 内の model.save() は正しくロールバック

インタラクティブツアー

11本のブラウザベースmarimoノートブック — インストール不要で直接コードを実行・編集:

インタラクティブツアーを起動 →

ツアートピック
01. 基礎モデル、CRUD、クエリ、集計
02. リレーション参照、水和、モデル間クエリ
03. セーフモデル楽観的ロック、競合解決
04. トランザクションアトミック操作、ロールバック
05. Mixinタイムスタンプ、論理削除、ライフサイクルフック
06. 上級バルク操作、インデックス、整合性、生SQL
07. エクスポート/インポートCSV、JSON、JSONL
08. 全文検索FTS5、ブール検索、ランキング
09. 変更追跡ダーティチェック、変更検出
10. DB操作ヘルスチェック、統計、VACUUM、ログ
11. メトリクス・キャッシュPrometheusメトリクス、クエリキャッシュ、プール

全11本がPyodide/WASM上でLiteモデルを使ってブラウザ内で動作。Pydanticフル版はローカルで uv run marimo edit examples/tour_01_fundamentals.py


正直な制限事項

  • シングルライター — SQLiteのアーキテクチャ。bulk_upsert とトランザクションで書き込みをバッチ化。
  • JSONオーバーヘッド — フィールドは json_extract() 経由。ネイティブカラムではない。インデックスで差を埋める。
  • JOINなし — リレーションは参照水和で解決。SQL JOINではない。典型的なドキュメントパターンには十分。
  • メモリ制約 — 大きな結果セットはPython上に展開される。paginate()count() で制限を。

はじめる

pip install sqler                # または: uv add sqler

Python 3.12+ · JSON1付きSQLite(ほとんどのプラットフォームで同梱)。

uv run pytest -q                 # 548テスト
uv run python -m benchmarks run  # 22ベンチマークシナリオ