- データベース
- 技術 💻
SQLで結果や速度が変わる8選|ウィンドウ関数・再帰CTE
SQLって、動くものは書けるようになっても、その先を教わる機会が意外と少ないんですよね。
今回は、知っているかどうかで結果や速度が変わるものを8つ集めました。どれも実務で「ほぉー」となるやつです。1つでも知らなかったら儲けものです。異論・追加も大歓迎です。
① NOT IN は、NULLがひとつでもあると結果が空になります
これを知ったときは、けっこう衝撃でした。
WHERE id NOT IN (SELECT user_id FROM orders)
このサブクエリの結果にNULLが1件でも混ざっていると、条件に合う行があっても結果が返ってこないんです。
理由は、SQLが真・偽・不明の三値で動いているからです。NOT IN は内部的にこう展開されます。
id <> 1 AND id <> 2 AND id <> NULL
最後の「NULLと等しくない」は、真でも偽でもなくUNKNOWNになります。ANDでつながっているので、全体が絶対にTRUEにならないんです。WHERE句はTRUEの行しか返さないので、結果が空になる、というわけです。
面白いのが、IN の方では起きないこと。ORでつながるので、どれかがTRUEなら成立するんです。NOT IN だけが危ない。この非対称、覚えておくと事故が減ります。
安全なのは NOT EXISTS です。
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)
こちらはNULLがあっても意図どおりに動きます。
② 条件ごとの件数を、1回で数える
「有料会員は何人、無料会員は何人」を出すとき、クエリを2本書いていませんか。1本で済むんです。
SUM(CASE WHEN plan = 'paid' THEN 1 ELSE 0 END) AS paid,
SUM(CASE WHEN plan = 'free' THEN 1 ELSE 0 END) AS free
COUNTを使う書き方もあります。
COUNT(CASE WHEN plan = 'paid' THEN 1 END)
COUNTはNULLを数えないので、ELSEを書かなくていいんです。ここ、地味に気持ちいいところです。テーブルを1回読むだけで済むので、速度面でも効いてきます。
PostgreSQLなら、もっと素直に書けます。
COUNT(*) FILTER (WHERE plan = 'paid')
③ グループごとの「最新の1件」を取る
ユーザーごとに、いちばん新しい注文を1件だけ取りたい。自己結合で書こうとすると、急に複雑になるんですよね。ウィンドウ関数を使うと、すっきり書けます。
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
これを付けたサブクエリを作って、外側で rn = 1 を絞る。それだけです。
PARTITION BYが「グループ分け」、ORDER BYが「その中での順番」。この2つが分かると、ウィンドウ関数はぐっと身近になります。
同点も全部ほしいときは RANK()、順位を詰めたいときは DENSE_RANK()。使い分けると便利です。
④ 前の行との差を出す
売上の前日比、前月比。これもウィンドウ関数の出番です。
LAG(amount) OVER (ORDER BY date) AS prev_amount
LAGは前の行、LEADは次の行を持ってきてくれます。自己結合で「1日前のレコード」を探しにいく必要がないんです。
移動平均を出したいときは、範囲指定もできます。
AVG(amount) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)
直近7日の平均が、1行で出ます。
ここでひとつ注意点を。範囲の指定には ROWS と RANGE があって、意味が違うんです。ROWSは「行数」、RANGEは「値の範囲」。同じ日付が複数行あるときに結果が変わるので、迷ったらROWSが安全です。
⑤ インデックスが効かなくなる書き方
これはかなり実務的です。
WHERE DATE(created_at) = '2026-08-01'
一見きれいなんですが、列に関数をかけると、多くの場合インデックスが使えなくなるんですよね。範囲で書くと効くようになります。
WHERE created_at >= '2026-08-01' AND created_at < '2026-08-02'
同じ意味なのに、速度が桁で変わることがあります。「列そのものを、そのまま比較する」。これがインデックスを活かすコツです。
ちなみに複合インデックスにも似た話があって、(a, b) の順で作ったインデックスは、bだけで検索しても効きません。左から順に使うので、列の順番は設計時にけっこう重要なんです。
⑥ 深いページングは、OFFSETをやめる
一覧画面の「次のページ」を作るとき、OFFSETを使いますよね。
LIMIT 20 OFFSET 100000
これ、実は10万行を読み飛ばしてから20行返しているんです。ページが深くなるほど遅くなります。かわりに、前のページの最後の値を覚えておいて、そこから続きを取る方法があります。
WHERE id < 前ページの最後のid ORDER BY id DESC LIMIT 20
読み飛ばしが発生しないので、何ページ目でも速度が変わりません。無限スクロールを作るときは、こちらがおすすめです。
⑦ ゼロ除算を、静かに回避する
割り算の分母がゼロになると、エラーで止まってしまいますよね。NULLIF を使うと、きれいに回避できます。
amount / NULLIF(count, 0)
NULLIF(a, b) は「aとbが同じならNULLを返す」関数です。分母がゼロのときだけNULLになるので、エラーにならず結果もNULLになります。表示用に0にしたいなら、COALESCEで包めばOKです。
COALESCE(amount / NULLIF(count, 0), 0)
小技ですが、集計クエリではよく使います。
⑧ 階層構造を、SQLだけで展開する
これがいちばんマニアックかもしれません。組織図やカテゴリツリーのように、親子関係が何階層も続くデータってありますよね。「この部署の配下を、全部取ってきたい」。アプリ側でループを回して、1階層ずつ問い合わせる方法もありますが、SQLだけで一気に取れるんです。
WITH RECURSIVE tree AS (
SELECT id, name, parent_id FROM depts WHERE id = 1
UNION ALL
SELECT d.id, d.name, d.parent_id
FROM depts d JOIN tree t ON d.parent_id = t.id
)
SELECT * FROM tree
上半分が出発点、下半分が「その子どもを取ってくる」処理です。子どもがなくなるまで、自動で繰り返してくれます。初めて見たときは、けっこう感動しました。
ただし、データに循環があると止まらなくなるので、深さの上限を持たせておくと安心です。
番外編:EXPLAINを見る習慣
最後にひとつだけ。遅いクエリに出会ったとき、書き換える前にEXPLAINを見てみてください。どのインデックスを使ったのか。何行読んだのか。それが分かると、勘ではなく根拠で直せるようになります。
「なんとなく速くなった」から「なぜ速くなったか説明できる」へ。この変化が、けっこう大きいんです。
RJCの研修でも、SQLを扱っています
RJCの研修は4ヶ月・12段階で、Lv.07〜09でコレクション・JUnit5・SQL・Web基礎を扱います。未経験の方が「なんとなく動く」から「理由がわかって書ける」ところまで行けるように、という設計です。
そしてTechチームでは、ログや定義書を貼るだけで実行用SQLを生成するツールを自作したりもしています。作った時間は約20分だったそうです。
よくいただくご質問
Q. どれから覚えるのがいいですか?
A. ①かなと思います。知らないと結果そのものが間違うタイプなので、優先度が高いんです。速度の話は、遅くなってからでも間に合います。
Q. ウィンドウ関数、難しそうです…
A. PARTITION BY(グループ分け)とORDER BY(並び順)の2つだけ意識すれば大丈夫です。慣れると自己結合を書かなくなります。
Q. データベースの種類によって違いますか?
A. ③④⑧は主要なDBで使えます。②のFILTER句のように、DBによっては独自の短い書き方もあるので、お使いの環境のドキュメントも見てみてください。
Q. 未経験からでも、こういうことができるようになりますか?
A. なります。社員のほぼ全員が未経験スタートです。研修で土台を作ってから、現場で覚えていきます。
知っているかどうかだけの差なんです
今日の8つ、どれも難しい理屈ではないですよね。知っていれば書ける。知らなければ踏む。それだけの差なんです。
だからこそ、こういう知識はチームで共有する価値があると思っています。RJCでは、Techチームで検証したことを社内で共有しています。一人で全部を追いかけるのは大変ですが、持ち寄れば追いつけます。
ひとつでも「ほぉー」があったら嬉しいです。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。