SQLのパフォーマンスを劇的に改善する書き方|初心者でもできる高速化のコツ

SQL & DB
スポンサーリンク

ある日、自分が書いたSQLの実行を待ちながら、じーっと進捗バーを見つめていました。

1秒…2秒…3秒……。

「え、まだ終わらないの?!」とイライラがふくらみ、気づけば Ctrl + C(データベース操作中に実行中のSQLをキャンセルするために使用されるショートカットキー)をそっと押していました…。

正しくデータを処理できることはもちろん大事です。

でも、「速くてスマートなSQL」って、それだけでなんだかかっこいい。

そんな思いから、私はパフォーマンス改善について本気で学びはじめました。

とはいえ、「SQLの最適化」と聞くと、なにやら難しそうなイメージがつきまとうかもしれません。

でもご安心を。

この記事では、SQLがちょっと遅くてモヤっとしたことがある人に向けて、初心者でも取り組めるパフォーマンス改善のヒントをやさしくご紹介します。

読みやすくて、速くて、美しい——

そんなSQLを書けるようになる第一歩、今日から一緒に踏み出してみませんか?

スポンサーリンク
  1. SQLは“正しく動く”だけで満足ですか?
    1. 処理速度は「やさしさ」でもある
    2. 「性能を意識して書けるSQL」には、実はコツがある
  2. 遅い原因はどこにある?SQLパフォーマンスの“あるある”落とし穴
    1. フルスキャン地獄:インデックスを活かせていない
    2. SELECT * 信者だった頃の私
    3. 無意味なJOINが処理量を爆増させる
    4. 「なんか遅い」の正体は“データ量の無視”かも?
  3. インデックスを味方につける
    1. インデックスって何?ざっくりイメージで理解しよう
    2. どんな条件にインデックスが効くの?
    3. インデックスが効かない“もったいない”書き方
    4. インデックスは使いすぎてもNG?
    5. インデックス活用の第一歩:見直す視点
  4. SELECT句は“本当に必要なカラム”だけを選ぶ
    1. SELECT * がもたらす“3つのムダ”
      1. 無駄その1:データ転送量のムダ
      2. 無駄その2:読みやすさのムダ
      3. 無駄その3:インデックスの活用チャンスを逃す可能性
    2. 必要なカラムを選ぶって、実は“親切”なんです
    3. SELECTするカラムの選び方のコツ
    4. 小さな意識が“データベースにやさしいSQL”を生む
  5. WHERE条件とJOIN順序で“無駄な仕事”を減らす
    1. 絞り込みはできるだけ“早く”やる
    2. INNER JOIN と LEFT JOIN、使い分けできてますか?
    3. 結合順序で“膨らむ中間データ”を減らそう
    4. 絞れる条件があるなら迷わずWHERE句で!
  6. サブクエリ vs CTE(WITH句)|構造を整えて“読みやすさと速さ”を両立しよう
    1. サブクエリが読みづらくなる理由
    2. CTE(WITH句)で分かりやすく分ける!
    3. CTEは読みやすさだけじゃない。再利用も便利!
    4. パフォーマンス的にはどうなの?
    5. 書きやすさと読みやすさを武器にする
  7. サブクエリ vs CTE(WITH句)|読みやすさと効率のバランスを整える
    1. サブクエリは便利、でもネスト地獄に注意
    2. WITH句(CTE)で“読みやすさ”を取り戻す
    3. 再利用性もバツグン◎
    4. ただし、CTEにも落とし穴はある
    5. サブクエリとCTE、どう使い分ければいい?
  8. 実践編:遅かったSQLを“読みやすくて速く”リファクタリングしてみた
    1. ケース①:SELECT * と無駄JOINのダブルパンチ
    2. ケース②:サブクエリが読みにくい上に非効率
    3. ケース③:WHERE句が詰まりすぎて意図不明
    4. ケース④:インデックス無視の書き方になっている
    5. まとめ:ちょっとの工夫が、大きな違いに
  9. 読みやすくて速いSQLは“やさしさと美しさ”の掛け算だった
    1. 書き手にも、読み手にも、未来の自分にもやさしいSQLを
    2. 一気に完璧を目指さなくて大丈夫
    3. 次の一歩:学び続けるSQLライフのために

SQLは“正しく動く”だけで満足ですか?

SQL初心者だった頃、「SELECTで正しいデータが返ってきた!」だけで大喜びしていた自分がいました。

クエリが通った=成功、処理が終わった=正解。 それで十分だと思っていたんです。…最初は。

でもある日、ちょっと大きめのデータに対してSQLを実行したら、 「……あれ、終わらない??」 クエリの完了を待つあいだ、私は静かに、でも確実にイライラしていました。

SQL
-- 結果が出るまで30秒…
SELECT * FROM logs WHERE date >= CURRENT_DATE - INTERVAL '30 days';

そのとき思いました。 正しく動くことは大切。でも、“速く動くこと”はもっと気持ちいい…!

処理速度は「やさしさ」でもある

「速く動くSQL」を書けると、単に自己満足で終わらないんですよね。

  • 画面の表示が速くなる → 利用者にやさしい
  • バッチ処理が速く終わる → サーバ負荷にやさしい
  • メンバーのレビュー時間が短くなる → チームにやさしい

何より、「遅いから見直して」と言われる前に読みやすくて速いSQLが出せると、 ちょっとだけ”一歩先を行けてる感”があって嬉しくなるんです。

「性能を意識して書けるSQL」には、実はコツがある

ありがたいことに、SQLのパフォーマンスは“気をつければ確実に改善できる”領域です。

知らないとやってしまう“もったいない書き方”が多いぶん、 知れば知るほど「わ、こんなに変わるのか!」という発見があっておもしろい。

このあとからの章では、

  • パフォーマンスが悪くなりやすい原因って?
  • インデックスって何に効くの?
  • SELECT * をやめると何が良くなる?
  • JOINの順番に意味ってあるの?

といった具体的なトピックを一つずつ、わかりやすくご紹介していきます。

「正しく動く」だけではない、 「サクサク気持ちよく動くSQL」を目指して。

私がSQLと少しずつ仲良くなれたように、読んでくださるあなたにも「書くのが楽しくなってきた!」と思ってもらえるような内容をお届けできたら嬉しいです。

遅い原因はどこにある?SQLパフォーマンスの“あるある”落とし穴

SQLの処理が遅いと感じたとき、私たちはつい「DBが重いんだな」「データ量が多いから仕方ない」と諦めがちです。

でも実は、ちょっとした書き方のクセや構造の工夫不足が、パフォーマンスの足を引っ張っていることが少なくありません。

私も最初は、何がいけなかったのかすら分からず、「…とにかく遅い!!」と叫びながらEXPLAINに逃げていました。

ここでは、私が実際につまずいた経験も交えながら、ありがちな“遅くなる原因”のパターンをわかりやすくご紹介します!

フルスキャン地獄:インデックスを活かせていない

インデックスが効いていないと、SQLはテーブル全体を上から下までなめることになります。

いわゆる「フルスキャン」です。

SQL
SELECT * FROM users WHERE UPPER(email) = 'TEST@EXAMPLE.COM';

この書き方、email にインデックスがあっても UPPER() のせいで無視されてしまいます。

関数やキャスト、計算が入ると、インデックスが効かなくなるケースが多いんです。

対策のヒント

  • WHERE句では“そのままのカラム”で比較しよう!
  • LIKE や ILIKE の使い方にも注意!

SELECT * 信者だった頃の私

昔の私は、SELECT * の便利さに酔っていました。

「楽だし、全部取れればあとで調整できるし」——

そう思ってたら、カラムが30個以上あるテーブルで大変でした。

SQL
SELECT * FROM orders WHERE status = 'pending';

必要なのは order_id と total だけだったのに、不要なデータを大量に引っ張ってしまい、転送量も実行計画もムダだらけに。

対策のヒント

  • 「何を使うか」だけを意識して、必要なカラムだけ選ぶ
  • *を捨てる覚悟が、軽さと読みやすさを生む!

無意味なJOINが処理量を爆増させる

JOINの書き方ひとつで、SQLの仕事量が何倍にもなることがあります。

とくにJOIN条件が不明瞭だったり、実は結合しなくてもよかったテーブルがあると、実行時間にダメージが…。

SQL
-- 本当は不要な結合
SELECT u.name, o.total
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN regions r ON u.region_id = r.id;

regions を使っていないのにJOINしてしまうの、わりと“やりがち”なんですよね…。

対策のヒント

  • 本当に使っているテーブルか再確認
  • JOINの書きっぱなしに注意!

「なんか遅い」の正体は“データ量の無視”かも?

1万件と100万件、同じように扱えないのは想像しやすいですよね。

でも実際のSQLでは、ついつい全件検索を許してしまいがちです。

SQL
SELECT * FROM logs;  -- ぜんぶ取っちゃうやつ

この書き方が必要なケースって、ほぼありません(本当)。

対策のヒント

  • テスト段階でも LIMIT 100 を習慣化しよう
  • WHERE句で“絞り込み”の意識を!

この章では、「やってしまいがちな遅くなる理由」を整理してきました。

それぞれ、小さな書き方の工夫で確実に改善できることばかりです。

次の章では、それらを解決するカギのひとつ、“インデックスの正しい活かし方”についてやさしく解説していきます!

インデックスを味方につける

「処理が遅い…」と感じたSQLに、インデックスを貼ったら一瞬で終わった!

そんな経験をしたとき、私はちょっと感動しました。

もはや魔法かと思いました。

でも実際は、魔法ではなく“正しく仕組みを使っただけ”なんですよね。

この章では、そんな頼れる味方「インデックス」について、初心者でも理解しやすく、実務で使える視点で解説していきます!

インデックスって何?ざっくりイメージで理解しよう

インデックスは、本の「索引」や辞書の「見出し」のようなものです。

たとえば、500ページある本のなかから「インデックス」という単語を探すとき、1ページ目からめくるより、巻末の索引で調べたほうが早いですよね。

SQLも同じで、インデックスがあると、全件チェック(フルスキャン)をせずに必要な情報へジャンプできるようになります。

どんな条件にインデックスが効くの?

一般的に、以下のようなWHERE句やJOINの条件に対してインデックスが効果を発揮します:

SQL
SELECT * FROM users WHERE email = 'taro@example.com';

この場合、email カラムにインデックスがあれば、 usersテーブル全体を走査するのではなく、emailでピンポイントに探せるようになります。

そのほかにも、

  • JOINのON条件(ON u.id = o.user_id など)
  • ORDER BY / GROUP BY の対象
  • 一部の部分一致(LIKE ‘abc%’)パターン

などでインデックスが活きてきます。

インデックスが効かない“もったいない”書き方

「インデックス貼ってあるのに、なんか速くならない…?」という場合、 その書き方がインデックスの“邪魔”をしている可能性があります。

効きにくい例:

SQL
WHERE LOWER(email) = 'taro@example.com'   -- 関数で覆ってしまっている
WHERE CAST(user_id AS TEXT) = '123'      -- キャストしてしまっている

このように、カラムに関数や変換がかかると、インデックスが無効になることがあります。

インデックスを最大限に活かすには、“そのままの値で比較する”のが基本です!

インデックスは使いすぎてもNG?

「インデックス貼っとけばなんでも速くなるのでは!」と、一時期の私は思ってました。

でも実は、インデックスの貼りすぎは逆効果になることもあります。

  • データ追加・更新のたびにインデックスも更新される
  • 不要なインデックスがあるとストレージが無駄に
  • クエリプランが複雑になる可能性も

つまり、インデックスは“少数精鋭”がおすすめ。

本当に検索・結合・ソートでよく使われるカラムを見極めて付けることが大切です。

インデックス活用の第一歩:見直す視点

この記事を読んだ今、以下のような視点でSQLを見直すだけでも、パフォーマンス改善のヒントが見つかるかもしれません:

  • WHEREやJOINに使っているカラムにインデックスは貼られているか?
  • 関数やキャストでインデックスを殺していないか?
  • そもそもその絞り込み条件は“活きている”のか?

「データベースに優しく、SQLに誠実」な書き方こそ、インデックスを活かすコツです。

次の章では、データ転送量や見通しを良くする構文的な工夫について掘り下げていきます。

「速さ」を支えるのはインデックスだけじゃない!一緒に見ていきましょう!

SELECT句は“本当に必要なカラム”だけを選ぶ

SQLを書き始めたばかりの頃、私はこう思っていました。

「SELECT * って楽だし、全部取れればとりあえず安心じゃない?」

…と。

たしかに最初のうちは「どのカラムが必要かまだ分からない」「画面で全部見て決めたい」といった理由で SELECT * を使いたくなります。

私もそうでした。でも気づいてしまったんです。

“便利”のつもりが、“負担”になっていることもある って。

SELECT * がもたらす“3つのムダ”

便利そうに見える SELECT *。でも、パフォーマンスの面では要注意です。

無駄その1:データ転送量のムダ

  • 使わないカラムも全部読み込む → ネットワーク転送&アプリ側の処理量が増える

無駄その2:読みやすさのムダ

  • 何を取得しているのか、一目で分からない
  • レビューや保守のときに「あれ、これ何に使ってるんだっけ?」となりがち

無駄その3:インデックスの活用チャンスを逃す可能性

  • 一部のDBでは、「使われているカラムがインデックスに含まれているか」が最適化の鍵になることも(PostgreSQLはこの点やや柔軟)

必要なカラムを選ぶって、実は“親切”なんです

SQL
-- ❌ これはやりすぎ
SELECT * FROM users WHERE status = 'active';

-- ⭕️ これだけで十分かも?
SELECT id, name, email FROM users WHERE status = 'active';

このように、本当に必要なカラムだけを選ぶと、「このSQLは何を目的としているのか」がパッと伝わるようになります。

SQLはロジックであると同時に、チームへのメッセージでもあると思うんです。

SELECTするカラムの選び方のコツ

  • 画面で表示する項目だけを抽出
  • JOIN先で必要なIDだけを取得(不要な全カラム取得は避ける)
  • GROUP BYやORDER BYで使うカラムも意識的に含める

そして何より、「あとから使うかもしれないし…」という気持ちは、いったん机の引き出しにしまっておきましょう。

あとから必要になったら、そのとき追記すれば大丈夫です!

小さな意識が“データベースにやさしいSQL”を生む

たとえば数百万レコードのテーブルで SELECT * をしてしまうと、それだけでメモリやI/Oに大きな負担をかけてしまいます。

1人1回のクエリなら耐えられても、同時アクセスが増えたときにはシステム全体に影響が出ることも。

「自分ひとりのSQLじゃない」という意識を持つだけで、グッとやさしく、速く、美しいクエリになります。

次の章では、WHERE句やJOIN順序など、SQLの処理構造そのものを見直す視点に入っていきます。

ここまでで「データの量」に目が向いたら、次は「そのデータをどう絞るか」を考えていきましょう!

WHERE条件とJOIN順序で“無駄な仕事”を減らす

SQLは命令一つで大量のデータを引っ張ってきたり、表同士を結びつけてくれたりします。

だからこそ、「どこから処理するか」「何を先に絞るか」がパフォーマンスに大きな差を生むんです。

ここでは、「SQLって並び順ひとつでそんなに変わるの?」と思っていた昔の自分に教えてあげたい、無駄な仕事を減らすための構文上のコツをご紹介します!

絞り込みはできるだけ“早く”やる

以下の2つのSQL、どちらも正しく結果は返りますが、実行効率に差が出ることがあります。

SQL
-- ❌ 絞り込みが遅い(JOIN後にWHEREで絞っている)
SELECT *
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE u.status = 'active';

-- ✅ 絞り込みを先に(JOIN対象を減らしてから結合)
SELECT *
FROM (
    SELECT * FROM users WHERE status = 'active'
) u
JOIN orders o ON o.user_id = u.id;

※PostgreSQLは内部で最適化されることもありますが、処理の流れを意識した書き方は読みやすさにもつながります。

INNER JOIN と LEFT JOIN、使い分けできてますか?

JOINには複数の種類がありますが、パフォーマンス的に軽いのは INNER JOIN。

でもつい、「なんとなくLEFT JOINで書いておけば網羅できるし安心…」と思っていませんか?(私は思ってました)

SQL
-- ❌ 不要なLEFT JOINのせいで余計な処理
SELECT o.id, u.name
FROM orders o
LEFT JOIN users u ON o.user_id = u.id;

-- ⭕️ 存在前提ならINNER JOINでOK
SELECT o.id, u.name
FROM orders o
JOIN users u ON o.user_id = u.id;

LEFT JOINは「紐づかない行も全部出す」分、結合の処理コストが重くなりやすいので、 本当に必要な場面だけ使うようにすると、SQLがスリムになります!

結合順序で“膨らむ中間データ”を減らそう

JOINの順番にも意味があります。

SQL
-- ❌ 大きなテーブル2つを先にJOIN
A(100万件) × B(80万件) → その後にC(500件)

-- ⭕️ 小さいテーブルと大きいテーブルを先にJOINして“絞る”
A(100万件) × C(500件) → その後にBとJOIN

これは中間結果が爆発しないように処理順序を最適化するイメージです。

SQLの処理順に正解はありませんが、「どの順で処理すれば結果が少なくなるか」を考えるだけで、 “ムダなJOIN地獄”にハマる確率がグッと減ります。

絞れる条件があるなら迷わずWHERE句で!

JOINのON句で条件を書くのは正解ですが、絞り込み条件はWHERE句にしっかり分けて記述すると、意図が読み取りやすくなります。

SQL
-- ❌ あいまいなJOIN条件+WHEREなし
JOIN orders o ON u.id = o.user_id AND o.total > 1000

-- ⭕️ JOIN条件と絞り込みを分けてスッキリ
JOIN orders o ON u.id = o.user_id
WHERE o.total > 1000

ロジックの役割が明確になるだけで、読みやすさとレビューしやすさが大幅アップします!

この章では、SQLの構文や処理順序を少し意識するだけで、“不要な仕事”を減らせるということをお伝えしました。

次の章では、サブクエリ vs CTE(WITH句)に注目して、「構造を読みやすく整理しながら速くする」方法をお話しします。

ここも見落としがちなポイントなので、ぜひ一緒に整理していきましょう!

サブクエリ vs CTE(WITH句)|構造を整えて“読みやすさと速さ”を両立しよう

「動くけど読めないSQL」、私もたくさん書いてきました。

特にサブクエリが2重3重とネストされると、もはや構造が迷宮入りして「どこから読んだらいいんだ…」と目が泳ぎます。

この章では、そんなサブクエリ地獄から抜け出すための選択肢、CTE(WITH句)についてご紹介します。

サブクエリが読みづらくなる理由

SQL
SELECT user_id, total
FROM (
    SELECT user_id, SUM(price) AS total
    FROM (
        SELECT user_id, price
        FROM orders
        WHERE status = 'completed'
    ) AS t1
    GROUP BY user_id
) AS t2;

上のようなクエリ、一番外側を理解するために、内側から読まないといけないという構造になっていて、非常に読みづらいです。

  • ネストが深くなると、「何をしているか」が把握しづらい
  • AS t1, AS t2 などの別名が抽象的で、意味が見えにくい
  • 再利用や修正もしづらい

CTE(WITH句)で分かりやすく分ける!

CTE(Common Table Expression)は、クエリの途中結果に名前を付けて“分けて書ける”構文です。

先ほどのネストをCTEで書き直すと、こうなります。

SQL
WITH completed_orders AS (
    SELECT user_id, price
    FROM orders
    WHERE status = 'completed'
),
user_totals AS (
    SELECT user_id, SUM(price) AS total
    FROM completed_orders
    GROUP BY user_id
)
SELECT user_id, total
FROM user_totals;

どうでしょう? 「まず完了した注文を取って、そのあとに合計金額を集計」という処理の流れがとっても明快になりましたよね。

CTEは読みやすさだけじゃない。再利用も便利!

CTEは名前付きの“仮想テーブル”のような扱いになるため、同じ途中結果を複数の場所で使いたいときにも便利です。

SQL
WITH active_users AS (
    SELECT id FROM users WHERE is_active = true
)
SELECT * FROM active_users;
-- 他のクエリでも active_users を再利用できる

一時的なビューのように使えるこの特性が、複雑なロジックを複数の小さい部品で整理するスタイルにフィットします。

パフォーマンス的にはどうなの?

PostgreSQLでは、CTEは基本的に「最適化の対象外」です。

つまり、通常のサブクエリやJOINであれば実行計画が合体して最適な処理順を選んでくれるところを、CTEは独立して処理されてしまいます。

  • 小規模 or 読みやすさ重視 → CTE でOK!
  • 処理負荷やJOINが大きい → サブクエリで最適化させた方が速くなることも

という風に、使いどころを見極めるのがポイントです。

書きやすさと読みやすさを武器にする

私自身、CTEに出会ったときは感動しました。

「SQLってここまで整理して書けるんだ!」と。

もちろん、CTEを使えばすべて解決!という話ではありませんが、ネスト深めのSQLを「ストーリーのように整える」手段としては本当に心強い存在です。

次の章では、読みづらくてパフォーマンスも悪かったSQLをBefore → Afterで整える実践編に入ります。

ここまで紹介してきた“読みやすくて速いSQL”の要素をどう生かせばいいのか、一緒に手を動かしながら確認してみましょう!

サブクエリ vs CTE(WITH句)|読みやすさと効率のバランスを整える

クエリが複雑になってくると、つい頼ってしまうのがサブクエリのネスト。

「SELECTの中のSELECTの中のSELECT……」と階層が深くなるほど、自分で書いたのに読み返すたびに「これは何をしてるんだっけ…?」と記憶がバグってきます。

私はこの構造で何度も自爆しました。

動くけど読めない。 遅くはないけど、誰にも引き継げない。

そこで救世主として登場したのが、CTE(Common Table Expression / WITH句)でした。

サブクエリは便利、でもネスト地獄に注意

まず、サブクエリの基本構造はこちら:

SQL
SELECT user_id, total
FROM (
    SELECT user_id, SUM(price) AS total
    FROM orders
    WHERE status = 'completed'
    GROUP BY user_id
) AS sub;

このくらいなら大丈夫ですが、実務でよくあるのは「このサブクエリをさらに別のサブクエリで囲む」スタイル。

ネストが増えるほど、処理の流れが下から上にしか読めず、理解がどんどん困難になっていきます。

WITH句(CTE)で“読みやすさ”を取り戻す

同じ処理をCTEで書くと、こうなります:

SQL
WITH completed_orders AS (
    SELECT user_id, price
    FROM orders
    WHERE status = 'completed'
),
user_totals AS (
    SELECT user_id, SUM(price) AS total
    FROM completed_orders
    GROUP BY user_id
)
SELECT user_id, total
FROM user_totals;

このように各処理に名前をつけて上から順に並べることで、処理のストーリーが直感的に伝わるようになります。

「何をして」「その結果をどう使って」「最終的に何を出すのか」が自然に読める構成です。

再利用性もバツグン◎

CTEはただ読みやすいだけでなく、同じ処理を複数の箇所で参照することも可能です。

SQL
WITH active_users AS (
    SELECT id FROM users WHERE is_active = true
)
SELECT * FROM active_users;
-- 別のクエリでも active_users を再利用できる

データ分析や集計系の処理では、「途中の処理を再利用できる」だけでも可読性・保守性がぐっと上がります。

ただし、CTEにも落とし穴はある

CTEは常にパフォーマンスに有利とは限らない、というのが正直なところです。

とくにPostgreSQLなど一部のDBでは、CTEが“最適化されずに一時テーブル化”されるため、 場合によってはサブクエリより遅くなることもあります。

だからこそ、“読みやすさ重視”なのか、“実行速度重視”なのかを場面によって選ぶバランス感覚が大切です!

サブクエリとCTE、どう使い分ければいい?

比較観点サブクエリCTE(WITH句)
読みやすさ△ ネストが深いと読みにくい◎ 各処理が分かれていて把握しやすい
パフォーマンス(最適化)○ 自動で最適化されやすい△ DBによっては最適化されないことも
再利用性× 使いまわし不可◎ 複数の参照OK
デバッグしやすさ△ ネスト構造で検証しづらい◎ 各処理単位で実行・検証が可能

私の経験では、「読みやすさ」が必要とされるクエリ(他の人に引き継ぐ、レビューされる、将来も読む予定がある)ではCTE、 「パフォーマンスが第一・CTEでは最適化が効かなかった」という場面ではサブクエリを選ぶようにしています。

SQLにおいては、“どっちが正解”ではなく、「書き手の意図が伝わるか」「その場に適しているか」が大事なんだなと学びました。

次の章では、実際に「読みづらくて遅かったSQL」をBefore→Afterで改善する実践編に入っていきます。

ここまで学んだことが、どのようにSQLの見た目と速度に変化をもたらすか、ぜひ一緒に見ていきましょう!

実践編:遅かったSQLを“読みやすくて速く”リファクタリングしてみた

ここまで、「読みやすくて速いSQL」にするためのアイデアやテクニックをいろいろ紹介してきました。

でも、やっぱり一番しっくりくるのはBefore → Afterで変化を見ること。

この章では、実務でありがちな“モヤッと重いSQL”を例に、どのように改善できるかを見ていきましょう!

ケース①:SELECT * と無駄JOINのダブルパンチ

Before

SQL
SELECT * 
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
LEFT JOIN campaigns c ON o.campaign_id = c.id
WHERE o.status = 'completed';
  • SELECT * → 不要なカラムも大量取得
  • LEFT JOIN → 本当はINNER JOINで良いケース
  • 可読性もイマイチ

After

SQL
SELECT 
    o.id AS order_id,
    o.total_price,
    u.name AS user_name,
    c.name AS campaign_name
FROM orders o
JOIN users u ON o.user_id = u.id
LEFT JOIN campaigns c ON o.campaign_id = c.id
WHERE o.status = 'completed';
  • 必要なカラムだけを選択
  • 結合の種類を見直し(users は必須 → JOIN)
  • 各カラムに明確な別名を付与 → レスポンスもコードもスッキリ!

ケース②:サブクエリが読みにくい上に非効率

Before

SQL
SELECT user_id, total
FROM (
  SELECT user_id, SUM(price) AS total
  FROM (
    SELECT user_id, price
    FROM orders
    WHERE status = 'active'
  ) AS inner_sub
  GROUP BY user_id
) AS outer_sub;
  • ネストが深く処理の意図が読みにくい
  • サブクエリの中でSELECT→GROUP BY→外側でさらにSELECT…

After(CTEで整理)

SQL
WITH active_orders AS (
    SELECT user_id, price
    FROM orders
    WHERE status = 'active'
),
user_totals AS (
    SELECT user_id, SUM(price) AS total
    FROM active_orders
    GROUP BY user_id
)
SELECT user_id, total
FROM user_totals;
  • 処理ステップごとに名前をつけて整理
  • 読み手の頭の中で構造がスッと入るように!

ケース③:WHERE句が詰まりすぎて意図不明

Before

SQL
SELECT *
FROM users
WHERE is_active = true AND (last_login > CURRENT_DATE - INTERVAL '30 days' OR role = 'admin') AND region = 'JP';

After

SQL
SELECT 
    id, name, role
FROM users
WHERE 
    is_active = true
    AND (
        last_login > CURRENT_DATE - INTERVAL '30 days'
        OR role = 'admin'
    )
    AND region = 'JP';
  • 条件ごとにインデント&改行 → 論理構造が見える化
  • SELECT * をやめて必要なカラムだけ選ぶ
  • 読みやすさとパフォーマンスのW改善!

ケース④:インデックス無視の書き方になっている

Before

SQL
SELECT * 
FROM users 
WHERE LOWER(email) = 'taro@example.com';

After

SQL
-- あらかじめ email を小文字で保存しておく方式に変更
SELECT * 
FROM users 
WHERE email = 'taro@example.com';

または、PostgreSQLなら「関数インデックス」の活用も手段のひとつです:

SQL
-- インデックスの作成例
CREATE INDEX idx_email_lower ON users (LOWER(email));

-- クエリ側は変えずにインデックスが効くようになる

まとめ:ちょっとの工夫が、大きな違いに

この記事で紹介したテクニックは、どれも「やれば確実に良くなる」ものばかり。

そして嬉しいことに、見た目の整え=読みやすさの向上 → 結果としてパフォーマンスにも好影響を与えるケースがほとんどなんです。

  • 不要なカラムは取らない
  • ネストはCTEでスッキリ整理
  • 条件は見やすく分ける
  • インデックスの恩恵を受けられる書き方に整える

これらの積み重ねが、未来の自分やチームの生産性につながっていきます。

次章では、これまでの内容を振り返りながら、「SQLのパフォーマンス改善とどう付き合っていくか?」をやさしくまとめていきます!

読みやすくて速いSQLは“やさしさと美しさ”の掛け算だった

これまでご紹介してきたSQL改善のアイデアは、どれも「ちょっとした意識」で始められるものばかりでした。

  • SELECT * を卒業して、必要なカラムだけを選ぶ
  • インデックスがちゃんと効く書き方にする
  • WHERE句やJOINの順序を意識して、ムダな処理を減らす
  • CTEでネストを整理して、読みやすさと構造美を手に入れる

これらの改善が積み重なると、SQLがスッと読めて、サッと動いて、安心して使えるようになります。 それはもう、ちょっとした“技術の品格”みたいなものかもしれません。

書き手にも、読み手にも、未来の自分にもやさしいSQLを

「このクエリ書いたの、誰だよ……あ、私か」 私もかつて、自作のSQLを見て苦笑いしたことが何度もあります。

でもそこから改善を繰り返す中で、“読みやすくて速いSQLは、相手への思いやり”でもあると気づくようになりました。

  • チームメンバーが読みやすいクエリ
  • バグ調査しやすいクエリ
  • サーバにやさしいクエリ
  • そして、未来の自分が「ありがとう」と言いたくなるクエリ

それを支えているのが、今日紹介したような基本的な書き方の工夫なのです。

一気に完璧を目指さなくて大丈夫

「パフォーマンスを意識しながら、読みやすく美しく書く」 それはたしかに理想ですが、最初から全部できる必要なんてありません。

  • まずはSELECT * を減らしてみる
  • 書いたあとに「インデックス効いてるかな?」と1回調べてみる
  • 見直したSQLが前より読みやすくなったら、自分を褒めてあげる

そんな小さな一歩が、確実に“よりよいSQL”を書く習慣につながっていきます。

次の一歩:学び続けるSQLライフのために

今回紹介したのはあくまで“はじめの一歩”です。

さらに深掘りしていくと、ビューやマテリアライズドビュー、統計情報や実行計画のチューニングなど、いろんなテーマが待っています。

でもまずは今日、「ただ動くだけじゃなく、読みやすくて速いSQLを書いてみたい」と思ってもらえたなら、それが何よりうれしいです。

というわけで、次にSQLを書くときは、ほんの少しだけ “そのクエリ、未来の自分が喜ぶかな?” と心の中で問いかけてみてください。

あなたのSQLライフがもっと快適に、もっと楽しくなりますように。

decopon
decopon

この記事は「SQL、動くけどなんか遅いし読みにくい…」と頭を抱えていた、少し前の自分に向けた手紙のようなつもりで書きました。

正しく動くことももちろん大事。 でも、“読みやすくて速いSQL”は、未来の自分にも、チームにも、データベースにもやさしいんだなと気づいてから、SQLに対する見方が変わりました。

この記事が、あなたのクエリ改善のヒントになったり、 「次はもうちょっと整えてみようかな」と思えるきっかけになっていたら、こんなに嬉しいことはありません。

私もまだまだ学びの途中です。 これからも一緒に、軽やかでやさしいSQLを書いていきましょう!

コメント

タイトルとURLをコピーしました