Files
Baradb/clients/nim-allographer/documents/rdb/postgres_efficiency_report.md
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

12 KiB
Raw Blame History

PostgreSQL driver 効率改善レポート

MariaDB driver に対して実施した効率改善と同様の観点で、PostgreSQL driversrc/allographer/query_builder/libs/postgres/ および models/postgres/)を調査した結果をまとめる。


P0: 重大(即時修正推奨)

1. exec / insertId が DML のたびに information_schema.columns を問い合わせている

ファイル: models/postgres/postgres_exec.nim 196-211行, 214-229行

proc exec(self:PostgresQuery, queryString:string) {.async.} =
  ...
  let table = self.query["table"].getStr
  let columnGetQuery = &"SELECT column_name, data_type FROM information_schema.columns WHERE table_name = '{table}'"
  let (columns, _) = postgres_impl.query(..., columnGetQuery, ...).await
  postgres_impl.exec(..., queryString, ..., columns, ...).await

問題:

  • INSERT / UPDATE / DELETE のたびに追加で1回 SELECT が発生し、DML 性能が実質2倍遅くなる
  • '{table}' の文字列補間は SQL インジェクションリスクがある(insertId にも同様の問題)

改善案: MariaDB で実施済みの columnTypeCache: Table[string, seq[seq[string]]]Connections に追加し、テーブルごとに初回のみ取得・キャッシュする。SQL はパラメータ化クエリに変更する。


P1: 中程度

2. waitersseq[Future[void]] で先頭削除が O(n)

ファイル: models/postgres/postgres_types.nim 20行, models/postgres/postgres_exec.nim 28-35行

# postgres_types.nim
waiters*: seq[Future[void]]

# postgres_exec.nim
proc wakeOnePoolWaiter(pools: Connections) =
  while pools.waiters.len > 0:
    let w = pools.waiters[0]
    pools.waiters.delete(0)   # ← O(n) のシフト操作
    ...

問題: seq の先頭削除は全要素シフトを伴う O(n) 操作。waiter が多い高負荷時に性能劣化する。removePoolWaiter も同じくリニアスキャン+シフト。

改善案: std/dequesDeque[Future[void]] に変更。popFirst() が O(1)。MariaDB で実施済み。

3. PgWaitStateref object(ヒープアロケーション)

ファイル: libs/postgres/postgres_impl.nim 14行

type
  PgWaitState = ref object
    cancelled: bool

問題: waitPgReadable / waitPgWritable が呼ばれるたびに PgWaitState のヒープアロケーションが発生する。

改善案: object(値型)に変更。async proc のクロージャ環境に格納されるため、値型でも正しく動作する。MariaDB で実施済み。

4. waitPgReadablewaitPgWritable がほぼ重複している

ファイル: libs/postgres/postgres_impl.nim 37-81行

proc waitPgReadable(db: PPGconn, timeoutMs: int): Future[bool] {.async.} =
  ...
  addRead(fd, readCb)      # ← 違いはここだけ
  ...

proc waitPgWritable(db: PPGconn, timeoutMs: int): Future[bool] {.async.} =
  ...
  addWrite(fd, writeCb)    # ← 違いはここだけ
  ...

問題: 2つの proc の差は addRead vs addWrite だけ。20行以上の完全な重複。

改善案: waitPgIo(db, timeoutMs, forRead: bool) のような統一 proc にまとめる。

5. waitPgReadable / waitPgWritablepqsocket を二重に呼んでいる

ファイル: libs/postgres/postgres_impl.nim 37-58行

proc waitPgReadable(db: PPGconn, timeoutMs: int): Future[bool] {.async.} =
  ...
  ensurePgSocketRegistered(db)   # 内部で pqsocket(db) を呼ぶ
  let sock = pqsocket(db)        # ← 再度 pqsocket を呼んでいる
  if sock < 0:
    dbError(db)
  let fd = AsyncFD(cint(sock))
  ...

問題: ensurePgSocketRegistered 内で pqsocket を呼んで fd を得ているのに、その戻り値を使わず直後にもう一度 pqsocket を呼んでいる。FFI 呼び出しの無駄。

改善案: ensurePgSocketRegisteredAsyncFD を返す proc にする(MariaDB 版 ensureMariadbSocketRegistered と同じ設計)。


P2: 軽微

6. getTime().toUnix() によるデッドライン計算(秒精度)

ファイル: libs/postgres/postgres_impl.nim 83-89行, 149-150行 他多数

proc pgRemainingMs(deadline: int64): int =
  let leftSec = deadline - getTime().toUnix()
  ...
  result = int(leftSec * 1000)

# deadline の生成(秒精度)
let calledAt = getTime().toUnix()
let deadline = calledAt + timeout.int64

問題:

  • getTime().toUnix() は秒精度のため、ミリ秒単位のタイムアウト制御ができない
  • NTP 補正によるクロック巻き戻りの影響を受ける
  • timeout = 1 のケースでは、0.9秒経過後に残り 0ms と判定される可能性がある

改善案: std/monotimesMonoTime + Duration に統一する。MariaDB で実施済み。

7. setColumnInfo が行ごとに全カラム分の FFI 呼び出しを行っている

ファイル: libs/postgres/postgres_lib.nim 159-167行, libs/postgres/postgres_impl.nim 160-163行 他

# postgres_lib.nim
proc setColumnInfo*(res: PPGresult; dbRows: var DbRows; line, cols: int32) =
  var columns: DbColumns
  setLen(columns, cols)
  for col in 0'i32..cols-1:
    columns[col].name = $pqfname(res, col)          # 行に依存しない
    columns[col].typ = getColumnType(res, line, col) # NULL判定のみ行依存
    columns[col].tableName = $(pqftable(res, col))   # 行に依存しない
  dbRows.add(columns)

# postgres_impl.nimquery* 内)
for i in 0'i32 .. pqNtuples(pqresult) - 1:
  setRow(pqresult, row, i, cols)
  setColumnInfo(pqresult, dbRows, i, cols)   # ← 毎行で全カラム FFI 呼び出し
  rows.add(row)

問題: pqfname, pqftable, pqftype はカラムの OID・名前・テーブル名を返す関数で、行によって結果が変わらない。行に依存するのは pqgetisnull(res, line, col) による NULL 判定のみ。1000行×10カラムでは、カラム名・テーブル名だけで2万回の不要な FFI 呼び出し+文字列アロケーションが発生する。

改善案: カラムの基本情報(name, tableName, OID)はループ外で1回だけ取得し、行ごとには NULL 判定のみ行う。

# 基本情報を1回だけ取得
var baseColumns: DbColumns
setLen(baseColumns, cols)
for col in 0'i32..cols-1:
  baseColumns[col].name = $pqfname(res, col)
  baseColumns[col].typ = getBaseColumnType(res, col)  # NULL判定なし
  baseColumns[col].tableName = $(pqftable(res, col))

# 行ごとにNULLだけ上書き
for i in 0'i32 .. pqNtuples(pqresult) - 1:
  setRow(pqresult, row, i, cols)
  var rowColumns = baseColumns
  for col in 0'i32..cols-1:
    if pqgetisnull(res, i, col) == 1:
      rowColumns[col].typ = DbType(kind: dbNull, name: "null")
  dbRows.add(rowColumns)
  rows.add(row)

8. dbFormat / questionToDaller の char-by-char 連結

ファイル: libs/postgres/postgres_lib.nim 203-216行, 219-229行

proc dbFormat*(formatstr: string, args: varargs[string]): string =
  result = ""
  ...
  for c in items(formatstr):
    if c == '?':
      add(result, dbQuote(args[a]))
      inc(a)
    else:
      add(result, c)           # ← 1文字ずつ add

proc questionToDaller*(s:string):string =
  var i = 1
  for c in s:
    if c == '?':
      result.add(&"${i}")
      i += 1
    else:
      result.add(c)            # ← 1文字ずつ add

問題: ? が出現するまでの連続部分を1文字ずつ add しており、長いクエリ文字列で非効率。

改善案: ? 位置間のサブ文字列を一括 add する。MariaDB の dbFormat で実施済みの手法。

9. query* / rawQuery* / execGetValue* のパラメータ送信パターンが重複

ファイル: libs/postgres/postgres_impl.nim 134-166行, 193-225行, 228-260行

# query*, execGetValue*, rawQuery* の冒頭がほぼ同一:
let status =
  if pgParams.nParams > 0:
    pqsendQueryParams(db, query.cstring, pgParams.nParams, nil, pgParams.values, ...)
  else:
    pqsendQueryParams(db, query.cstring, pgParams.nParams, nil, nil, nil, nil, 0)
defer:
  if pgParams.nParams > 0: pgParams.values.deallocCStringArray()
if status != 1: dbError(db)

問題: 4箇所にわたるクエリ送信ボイラープレート。保守コストが高い。

改善案: pgSendQuery(db, pgParams) のような共通 proc を抽出する。

10. fromObjArray の2つのオーバーロードが大部分重複

ファイル: libs/postgres/postgres_lib.nim 239-291行, 293-337行

問題: columns パラメータの有無のみが異なり、JSON→string 変換ロジックは同一。

改善案: 共通の内部 proc を抽出し、columns が空の場合はバイナリ判定をスキップする。


P3: 微小

11. dbQuote の容量見積もりなし

ファイル: libs/postgres/postgres_lib.nim 182-201行

proc dbQuote(s: string): string =
  if s == "null":
    return "NULL"
  result = "'"
  ...

問題: newStringOfCap による事前容量確保がない。特殊文字が多い文字列で再アロケーションが発生する。

改善案: result = newStringOfCap(s.len + 2) を先頭に追加。


改善優先度まとめ

優先度 # 問題 影響 修正難度
P0 1 exec/insertId が毎回 information_schema を問い合わせ + SQL インジェクション DML ごとに追加1クエリ 中(キャッシュ + パラメータ化)
P1 2 waitersseq.delete(0) が O(n) 高負荷時の性能劣化 低(Deque に変更)
P1 3 PgWaitStateref object 毎回のヒープアロケーション 低(object に変更)
P1 4 waitPgReadable/waitPgWritable が重複 保守コスト 低(統一)
P1 5 pqsocket 二重呼び出し 不要な FFI 呼び出し
P2 6 getTime().toUnix() の秒精度デッドライン タイムアウト精度 低(MonoTime 化)
P2 7 setColumnInfo が行ごとに全カラム FFI 呼び出し 行数×カラム数の不要な FFI + アロケーション (構造変更)
P2 8 dbFormat/questionToDaller の char-by-char 長いクエリで微小な差
P2 9 クエリ送信ボイラープレート重複 保守コスト
P2 10 fromObjArray の重複 保守コスト
P3 11 dbQuote の容量見積もりなし 微小

MariaDB との比較

項目 MariaDB(修正済み) PostgreSQL(現状)
カラム型キャッシュ columnTypeCache 導入済み 毎回 information_schema を問い合わせ
ブロッキング ping 除去済み assert db.status == CONNECTION_OKFFI なし、問題小)
setColumnInfo 位置 ループ外に移動済み ループ内で毎行呼び出し
waiter データ構造 Deque に変更済み seq(先頭削除 O(n)
待機状態オブジェクト 値型(object)に変更済み ref object(ヒープ割当)
デッドライン MonoTime に変更済み getTime().toUnix()(秒精度)
dbFormat セグメント一括 add char-by-char
SQL インジェクション プレースホルダ化済み 文字列補間のまま

注意: PostgreSQL の assert db.status == CONNECTION_OKpqStatus() がローカルのフィールド読み取りであり、MariaDB の mysql_ping() のようなネットワークラウンドトリップは発生しない。そのため、PostgreSQL 側はこの assert の除去は不要。