ANTIGRAVITY LABEN
記事一覧/アプリ開発
アプリ開発/2026-07-10上級

エージェントの書いたORMコードでp95が5倍に — クエリ回数をテストで測り、CIで止める

N+1クエリはレビューの目をすり抜けます。クエリ回数を測定可能な値に変え、入力件数に対する傾きで判定するCIゲートの実装を、動くコードと実測値とともにまとめました。

Antigravity319パフォーマンス6ORMCI5テスト設計

プレミアム記事

深夜、ダッシュボードの p95 レイテンシが跳ね上がっているのに気づきました。940ms。前日までは 180ms 台で安定していた一覧APIです。

障害ではありません。エラー率はゼロ、レスポンスの中身も正しい。ただ遅い。本番環境の数字だけが、約 5.2 倍に膨らんでいました。

原因を辿ると、その日の午後にエージェントへ委ねた小さな改修に行き当たりました。一覧に「各アイテムの最新コメント」を足すだけの変更です。差分は 12 行。レビューで私は「読みやすい」とだけ思い、そのまま通していました。

12 行のうちの 1 行が、ループの中でリレーションを引いていました。

正しいコードは、速いコードとは限らない

N+1 クエリの厄介さは、コードとして間違っていないことです。

型は通ります。テストも通ります。返る JSON も期待通りです。レビュアーが目にするのは for ループの中の await getComments(item.id) という、それ自体は何の問題もない一行だけ。呼び出しの回数がリクエストの中身に依存して増えるという事実は、差分のどこにも書かれていません。

エージェントに実装を任せるようになってから、この非対称性が一段と効いてくると感じています。人間なら「ここはループの中だから注意」と身体が反応する場面でも、生成されたコードは常に落ち着いて正しく見えます。私自身、差分の行数が少ないほど油断していました。個人開発で私はこの油断を何度も繰り返しています。

だからレビューの精度を上げる方向で解こうとするのは、筋が悪いと考えるようになりました。人間の注意力に依存しない指標が要ります。

観測できないものは、守れません。

クエリ回数を「テストで読める数値」にする

幸い、主要な ORM はどれもクエリ発行のフックを持っています。ここを使えば、テストの中でクエリ回数を数えられます。

まず Prisma。クライアントを query イベント付きで生成し、カウンタに積むだけです。

// test/support/query-counter.ts
import { PrismaClient } from "@prisma/client";
 
export const prisma = new PrismaClient({
  log: [{ emit: "event", level: "query" }],
});
 
let count = 0;
const seen: string[] = [];
 
prisma.$on("query", (e) => {
  count += 1;
  seen.push(e.query);
});
 
export function resetQueryCount(): void {
  count = 0;
  seen.length = 0;
}
 
export function getQueryCount(): number {
  return count;
}
 
export function getQueries(): readonly string[] {
  return seen;
}

Drizzle では logger を差し替えます。

// test/support/drizzle-counter.ts
import { drizzle } from "drizzle-orm/node-postgres";
import type { Logger } from "drizzle-orm/logger";
 
class CountingLogger implements Logger {
  count = 0;
  logQuery(query: string): void {
    this.count += 1;
  }
}
 
export const logger = new CountingLogger();
export const db = drizzle(pool, { logger });

Python / SQLAlchemy なら before_cursor_execute フックです。

# tests/support/query_counter.py
from contextlib import contextmanager
from sqlalchemy import event
 
@contextmanager
def count_queries(engine):
    counter = {"n": 0}
 
    def _on_execute(conn, cursor, statement, params, context, executemany):
        counter["n"] += 1
 
    event.listen(engine, "before_cursor_execute", _on_execute)
    try:
        yield counter
    finally:
        event.remove(engine, "before_cursor_execute", _on_execute)

ここまでは単なる計測です。問題は、この数値をどう判定に使うかでした。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
クエリ回数をテストで観測し、入力件数に対する傾きで N+1 を機械判定する仕組み
Prisma / Drizzle / SQLAlchemy それぞれのフック実装と、CIに組み込む完全なコード
実測値で見る効果と限界: p95 940ms→190ms、ゲート追加コスト +11秒
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

アプリ開発2026-07-06
端末を熱くしないライブ壁紙 — WallpaperService の描画ループを電力予算から設計する
ライブ壁紙は常時描画のせいで電池を削りがちです。可視性・アイドル・熱状態の3軸で描画を止める WallpaperService.Engine の設計と、適応フレームレート制御による消費電力の実測を、個人開発の壁紙アプリの文脈でまとめました。
アプリ開発2026-06-24
IDE を開かずに、配信前の検証を機械へ預ける — Android CLI を無人パイプラインの検証ゲートに据える設計
Android CLI v1.0 を、IDE を前提にしない無人パイプラインの検証ゲートとして組み込む設計です。三層の検査・終了コードの契約・密度と言語のマトリクスまで、個人開発の身の丈で運用するための考え方をまとめました。
アプリ開発2026-06-23
毎日変わるはずの壁紙ウィジェットが止まった — WidgetKit のリロード予算を実測して設計を組み直した記録
毎日壁紙が切り替わるはずのウィジェットが数日で更新を止めました。WidgetKit のタイムラインとリロード予算、拡張のメモリ上限を実測し、1日分のエントリをまとめて積む設計へ組み直した過程をまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →