Files
Baradb/clients/nim-allographer/.cursor/rules/branch/333-prepared-statement.mdc
T
dimgigov 6bfc5b3a3c
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
feat(clients): add nim-allographer client with BaraDB driver support
- Pure Nim wire protocol client (async + sync, no C FFI)
- Query Builder: fluent API, SQL generator, execution, transactions
- Schema Builder: create/alter/drop tables, column operations
- Connection pool with aging and timeout handling
- ORM integration via compile-time DB_BARADB switch
- Tests: test_open, test_query
- Documentation: README.md, PLAN_BARADB.md
2026-05-21 15:49:11 +03:00

168 lines
6.7 KiB
Plaintext

---
description: 333-prepared-statementブランチでの開発時に読み込む
alwaysApply: false
---
333-prepared-statement ブランチでの高速化内容
===
このブランチで実装することは以下の通りです。
- prepared statement を、高頻度な同一 SQL 実行で通常経路より速くなるように改善する
- とくに PostgreSQL で、prepare/deallocate の無駄と接続拡散を減らす
- benchmark が prepared statement の warm 状態を正しく測れるようにする
## 進捗
- [x] `example/benchmark.nim` の PostgreSQL 計測で prepared update が遅く見える理由を調査した
- [x] prepared statement 高速化の設計方針を整理した
- [ ] PostgreSQL にプール単位の prepared statement cache を追加する
- [ ] `close()` を論理 close 化し、物理破棄は別 API に分離する
- [ ] 接続固定で prepared statement を連続実行できる API を追加する
- [ ] `example/benchmark.nim` を cold/warm 計測に分離し、warm 経路で同一 statement を再利用する
- [ ] prepared cache と接続固定 API のテストを追加する
## 参考資料
- PostgreSQL libpq prepared statements: https://www.postgresql.jp/document/12/html/libpq-exec.html
- `PQsendPrepare`
- `PQsendQueryPrepared`
- `DEALLOCATE`
- PostgreSQL 非同期 libpq: https://www.postgresql.jp/document/12/html/libpq-async.html
- 対象実装
- `src/allographer/query_builder/models/postgres/postgres_exec.nim`
- `src/allographer/query_builder/models/postgres/postgres_types.nim`
- `src/allographer/query_builder/libs/postgres/postgres_impl.nim`
- `example/benchmark.nim`
## 調査結果・設計まとめ
### 現状の問題
PostgreSQL で prepared statement が遅く見える主因は、prepared statement 自体の性質ではなく、現在の実装と benchmark の計測条件にある。
- `benchmark.nim` は計測のたびに `prepare` と `close` を行っており、steady-state の再利用性能ではなくライフサイクル全体を測っている
- PostgreSQL 実装は prepared statement をコネクションごとに lazy prepare するため、プールが大きいと `PREPARE` が広く拡散する
- `first()` と `exec()` が毎回 `getFreeConn()` するため、1 つの業務操作でも接続が変わり、prepared state の局所性が弱い
- 通常経路も `PQsendQueryParams` による bind 実行なので、prepared の利得は parse/plan 再利用ぶんに限られる
### 高速化の基本方針
prepared statement を速くするには、単発 API を追加するだけでは足りない。以下をまとめて入れる必要がある。
- prepare/deallocate の回数を減らす
- 同じ logical operation を同じ接続に寄せる
- warm 済み prepared state をプール内で継続利用する
### 設計方針 1: プール単位の prepared cache
`PreparedStatement` ハンドルごとに物理 state を持つのではなく、`Connections` が SQL ごとの prepared state を保持する。
概念設計:
```nim
type PostgresPreparedEntry = ref object
sql*: string
nArgs*: int
stmtBaseName*: string
stmtNames*: seq[string] ## connI ごとの物理 prepared 名
refCount*: int
lastUsedAt*: int64
type Connections* = ref object
conns*: seq[Connection]
timeout*: int
waiters*: Deque[Future[void]]
columnTypeCache*: Table[string, seq[Row]]
preparedCache*: Table[string, PostgresPreparedEntry]
```
期待効果:
- 同じ SQL に対する `prepare()` の重複を避けられる
- 同一プール内では physical prepare を接続ごとに 1 回へ寄せられる
- 短命な prepared handle を繰り返し作っても warm state を失いにくくなる
### 設計方針 2: `close()` の論理 close 化
現在の `close()` は、その handle が使った接続すべてに `DEALLOCATE` を送る。高速化の観点ではこれは不利なので、意味論を分ける。
- `close()`: handle の参照を閉じるだけ
- `clearPreparedCache()` または `flushPrepared(sql)`: 物理 `DEALLOCATE`
- 接続 close 時: その接続に紐づく prepared state を一括破棄
期待効果:
- 利用者が毎回 `prepare` / `close` しても、プール側は hot SQL を保持できる
- benchmark の warm 計測でも、毎回の物理破棄コストを避けられる
### 設計方針 3: 接続固定 API の追加
prepared statement の locality を高めるため、同じ接続上で複数回の prepared 実行を束ねる API を追加する。
候補 API:
```nim
proc withPreparedConnection*(
self: PostgresConnections,
body: proc (ctx: PostgresPreparedContext): Future[void]
): Future[void]
```
```nim
type PostgresPreparedContext* = ref object
owner*: PostgresConnections
connI*: int
```
prepared statement 側には `ctx` 付き overload を足す。
```nim
proc first*(self: PostgresPreparedStatement, ctx: PostgresPreparedContext, args: seq[string]): Future[Option[JsonNode]]
proc exec*(self: PostgresPreparedStatement, ctx: PostgresPreparedContext, args: seq[string]): Future[void]
```
期待効果:
- `SELECT` と `UPDATE` が別接続に飛ぶのを防げる
- lazy prepare の発生先が必要な接続に限定される
- transaction 中の `transactionConn` と整合しやすい
### benchmark の変更方針
`example/benchmark.nim` は prepared statement の高速化効果を正しく測れるように変更する。
- `selectStmt` / `updateStmt` を `timeProcess` の外で 1 回だけ作る
- 繰り返し計測では同じ prepared statement を再利用する
- `close()` は全計測の最後に 1 回だけ呼ぶ
- benchmark 名を `cold` と `warm` に分ける
- warm 計測では `withPreparedConnection` を使って接続固定経路を測る
### 実装順序
#### Phase 1
- `Connections.preparedCache` を追加する
- `prepare()` を cache lookup ベースに変更する
- `close()` を論理 close 化する
- `clearPreparedCache()` を追加する
#### Phase 2
- `PostgresPreparedContext` を追加する
- `withPreparedConnection` を追加する
- prepared `get/first/exec` に `ctx` overload を追加する
- `benchmark.nim` を warm/cold 計測へ更新する
#### Phase 3
- `preparedCacheMaxEntries` を追加する
- `refCount == 0` の entry を対象に LRU eviction を入れる
- eviction 時に `DEALLOCATE` を発行する
### テスト方針
- 同じ SQL を複数回 `prepare()` しても、同一接続で physical prepare が 1 回に収まること
- `close()` 後に再 `prepare()` しても、cache が残る設計なら physical prepare が増えないこと
- `withPreparedConnection` の block 内で同じ `connI` が使われること
- transaction 中は `transactionConn` が優先されること
- cache eviction 時に `DEALLOCATE` が正しく走ること