📌

AI任せのコード、そのパフォーマンスは大丈夫?Prismaの$queryRawを例に

に公開

はじめまして!株式会社モアでエンジニアをしているmoriです!
これからは定期的に自分の学習も込めて記事を投稿していこうと思います。

1投稿目は直近学んだPrismaのお話です。
AIにコードを書かせたら、とりあえず動くものは出てきますよね。

でもそのコード、本当にパフォーマンス的には正しいでしょうか...?

今回、Prismaでの実装を例に、
「AIが書いたコードをそのまま使うとどうなるか」と
「人間が設計としてどう判断すべきか」をまとめてみました。


$queryRaw について

Prisma には $queryRaw という、生のSQLを直接書ける機能があります。

import { Prisma } from '@prisma/client';

const result = await prisma.$queryRaw(Prisma.sql`
  SELECT 〜
`);

COUNT(DISTINCT ...) みたいな重複排除だったりは、
Prismaのクエリビルダー(一般的な書き方)では書けないので、こういう書き方になりますね。


こういう時どっちで書くべき?

以下のテーブルがあるとして、ユーザーごとに購入した商品の種類数を取得したいとします。

purchases
---------
id
user_id
product_id
created_at

こういう時は、$queryRaw を使う書き方、使わない書き方があると思います。

① Prisma で全件取得 → JS で処理

const records = await prisma.purchases.findMany({
  where: {
    userId: { in: userIds },
  },
});

// 重複排除の処理
const result = userIds.map((userId) => {
  const userRecords = records.filter((r) => r.userId === userId);
  const uniqueProductIds = new Set(userRecords.map((r) => r.productId));
  return { userId, count: uniqueProductIds.size };
});

いたってシンプルというか、問題なく動きますね。
$queryRaw を使いたくないならこんな感じになるかと思います。

ただ、商品のデータが大量にあるという前提なら、この書き方は少し微妙です。
全件をメモリ上に乗っけて集計しているので、処理が重くなってしまうのが理由ですね。

こういうときに $queryRaw が使えるとベストです。

$queryRaw でDBに任せる

const result = await prisma.$queryRaw<{ user_id: string; count: bigint }[]>(
  Prisma.sql`
    SELECT user_id, COUNT(DISTINCT product_id) AS count
    FROM purchases
    WHERE user_id IN (${Prisma.join(userIds)})
    GROUP BY user_id
  `
);

これだと重複排除をDBに任せているので、処理が軽くなりますね。
DBはこういった集計が得意なので、はるかに効率的です。

AIが作ったものが必ずしも最適ではない

ちょっと脱線しましたが、今回書きたかった部分はここになります。

今回、やりたいことをAIに伝えて、最初に出来上がったコードは①の方でした。
AIへの指示がよくなかったのか、、、
参考ファイルでは $queryRaw が使われていなかったからなのか、、、原因はわかりません。

ただ今回の例の場合だと、商品の種類数の規模感というのはAIよりも人間の方が正しく把握できているはずです。仮にその数がものすごく多い前提の時、そのことを何もAIに伝えなければ①のコードが生成されてしまう可能性もあります。

AIは「要件を満たすコード」は書けても、
「最適な設計」や「パフォーマンス」を保証してくれるわけではありません。
むしろ今回のように、何も考えずに使うと「非効率な実装」をそれっぽく出してくることも普通にあります。

そのため、ただ単にAIにコード生成させて動けばOK!ではなく、
パフォーマンスやセキュリティ、他にも保守性・拡張性なんかも考慮しながら人間が最終的にチェック必要があるかと思います。

これらを踏まえて、AIに対して適切な指示を出せることや、最適な設計を行うことなどは今のAI時代にすごくもとめられていると感じますね。


補足です

せっかくなので、$queryRaw についても補足・解説します。

他にも $queryRaw が必要になる時

DISTINCT以外にも、Prismaのクエリビルダーでは書けない操作がいくつかあります。

ウィンドウ関数

「グループごとの順位付け」や「累積計算」をしたいときに使う関数です。
たとえば、「ユーザーごとに最新の注文1件だけ取りたい」みたいな時とかです。

ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC)

カラムに関数を適用した集計

DATE(created_at) のように、カラムを関数で加工してから集計したいケースです。
たとえば「日ごとのユニークユーザー数」を取りたいときとかです。

COUNT(DISTINCT DATE(created_at))

複雑なサブクエリ

WHERE id IN (SELECT ...) のネストが深かったり、メインクエリの値を参照する相関サブクエリを書きたいときです。

Prismaでも簡単なサブクエリは表現できるんですが、複雑になってくると書けなくなってくるので、その場合は $queryRaw の出番になります。


なぜPrismaは COUNT(DISTINCT) が書けないのか

Prismaの groupBy で使える _count は、nullでないレコードの件数を返すもので、DISTINCT(重複除外)には対応していません。

// これだと重複ありのカウント
await prisma.purchases.groupBy({
  by: ['userId'],
  _count: { productId: true },
});

$queryRaw のセキュリティ対策

Prisma.sql のテンプレートリテラルを使うと、変数が自動でパラメータ化されて、SQLインジェクション対策にもなっています。

// OK:Prisma.sql が値を安全にパラメータ化してくれる
prisma.$queryRaw(Prisma.sql`SELECT * FROM users WHERE id = ${userId}`)

// NG:$queryRawUnsafe で文字列結合すると、値がそのままSQLに埋め込まれて危険
prisma.$queryRawUnsafe(`SELECT * FROM users WHERE id = ${userId}`)

おわりに

この記事を書く中で、AIがコードを書く時代でもちゃんとした知識や経験が必要ということが改めてよくわかりました。

これからもAIが生成したコードに対して、
びしびし指摘できるくらいにもっと成長していきたいと思います!

そしてAIに任せるだけではなく、設計・パフォーマンスまで含めて考えられるエンジニアの価値は、これからさらに高まっていくと感じています。

そのようなスキルを伸ばすための取り組みとして弊社では CleanArch Master を運営しています。

また、現在採用も行っているので、興味のある方はぜひご連絡ください!


参考

モアテック

Discussion