1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SQL の考え方 🤔

Last updated at Posted at 2025-08-07

はじめに

最近『達人に学ぶ SQL 徹底指南書 第 2 版』を読みました。

この本を通して SQL には SQL の「考え方」がある ことが分かりました。
SQL は「宣言型」であり「集合指向」の言語のため、普段使い慣れた手続き型言語とは発想の仕方が異なります。
ここでは、その SQL 的な考え方 を整理します。

本題に入る前に...

  • 手続き型言語 : どう処理するかを手順で書く(ex. C, Java, Python)
  • 宣言型言語 : 欲しい結果だけを書く(ex. SQL, Haskell, Lisp)

SQL は「どう計算するか」ではなく「どんなデータが欲しいか」を書くスタイルです。

1. SQL を支える理論

SQL を支えているのは大きくこの 2 つです。

  • 集合論(テーブルは集合だよ、という考え方)
  • 述語論理(条件を「存在する/存在しない」で表現する考え方)

1.1 集合論

1.1.1 テーブルは集合

RDB(リレーショナルデータベース)は「関係モデル」という集合論の発想を採用しています。
つまり テーブルは「行の集まり=集合」 として考えられます。

ちょっと難しいですが、関係(=テーブル)の定義からも集合であることがわかります 🤓

関係の定義

関係: $R$ , 属性(列): $A_i$ , その定義域: $D_i$

$$R \subseteq D_1 \times D_2 \times D_3 \times \cdots \times D_n$$

自己結合

テーブルが集合であることを実感しやすいのが 自己結合 です。
同じテーブルを「別の集合っぽく」扱って比較します。

ex. 同じ値段の商品を探す

Products テーブル
name price
りんご 50
みかん 100
ぶどう 50
スイカ 80
レモン 30
いちご 100
バナナ 100
SELECT DISTINCT P1.name, P1.price
FROM
  Products P1
    INNER JOIN Products P2
      ON P1.price = P2.price
        AND P1.name <> P2.price
ORDER BY P1.price;
結果
name price
りんご 50
ぶどう 50
いちご 100
みかん 100
バナナ 100

手続き型言語なら「for 文で 1 行ずつ比較」しますが、SQL なら 「条件を宣言」するだけ で OK です。

1.1.2 集約(GROUP BY / HAVING)

HAVING 句は 「部分集合に条件をかける」 という集合的な発想です。

ex. 学生が全員レポート提出済みの学部を探す

Students テーブル
student_id dpt sbmt_date
100 理学部 2018-10-10
101 理学部 2018-9-22
102 文学部
103 文学部 2018-9-10
200 文学部 2018-9-22
201 工学部
202 経済学部 2018-9-25
SELECT dpt
FROM Students
GROUP BY dpt
HAVING COUNT(*) = COUNT(sbmt_date);
結果
dpt
理学部
経済学部

HAVING で「提出済みの人数」と「その学部の総人数」を比較しています。
集合にまとめて条件をかけてるのがポイントです。

1.2 述語論理

述語論理は 「条件をつけて対象をしぼり込む考え方」 です。
その中でも SQL に関係するのは「量化子」と呼ばれるものです。

  • 存在量化子(∃):「~するものが存在する」
  • 全称量化子(∀):「すべてのものが~する」

SQL の世界では、この量化子が EXISTS で表現されます。

1.2.1 存在量化

「この条件を満たす行がひとつでもあれば OK」というのが EXISTS です。
サブクエリの結果が 1 行でも返ってきたら真(TRUE)になります。

イメージ

∃x P(x) ⇔ EXISTS (サブクエリ)

ex. 会議に参加していない人を探す

Meetings テーブル
meeting person
第 1 回 伊藤
第 1 回 水嶋
第 1 回 坂東
第 2 回 伊藤
第 2 回 宮田
第 3 回 坂東
第 3 回 水嶋
第 3 回 宮田
SELECT DISTINCT
  M1.meeting,
  M2.person
FROM
  Meetings M1
    CROSS JOIN Meetings M2
WHERE
  NOT EXISTS(
      SELECT *
      FROM Meetings M3
      WHERE
        M1.meeting = M3.meeting
          AND M2.person = M3.person
    );
結果
meeting person
第 1 回 坂東
第 2 回 伊藤
第 2 回 宮田
第 3 回 坂東

やっていることは次の通りです 👇

  1. すべての会議 × すべての人の組み合わせを作る
  2. その会議にその人が「存在しない」場合だけ残す

つまり NOT EXISTS を使って「存在しない」を表現することで、「参加していない人」を洗い出しています。

1.2.2 全称量化

「すべてのものが~する」を SQL で表現するには工夫が必要です。
残念ながら SQL に FORALL 述語なんてありません。

そこで 二重否定を使います

「すべての要素が条件を満たす」
⇔「条件を満たさない要素はひとつも存在しない」

イメージ

∀x P(x) ⇔ ¬∃x ¬P(x) ⇔ NOT EXISTS (条件を満たさない行)

ex. すべてのテストで 50 点以上の生徒を探す

TestScores テーブル
student_id subject score
100 算数 100
100 国語 80
100 理科 80
200 算数 80
200 国語 95
300 算数 40
300 国語 90
300 社会 55
400 算数 80
SELECT DISTINCT
  student_id
FROM
  TestScores TS1
WHERE
  NOT EXISTS(
      SELECT *
      FROM TestScores TS2
      WHERE
        TS2.student_id = TS1.student_id
          AND TS2.score < 50
    );
結果
student_id
100
200
400

これは「50 点未満の点数が存在しない」=「すべて 50 点以上」を表しています。

2. 手続き型言語との比較

SQL を、慣れ親しんだ手続き型のプログラミング言語と比較してみます。

2.1 順次実行

手続き型なら「上から順番に実行」ですが、SQL では実行順序はこうです。

FROM → WHERE → GROUP BY → HAVING → SELECT (→ ORDER BY)

SELECT 句は、最後に 見た目を整えたり とか 計算した列を追加したり しているだけです。
だから、実行順序どおりに FROM 句から読み書きする と、流れが自然でロジックも追いやすいです。

2.2 条件分岐

SQL の条件分岐は CASE 式を使います。
if 文より関数型っぽいです。

CASE
  WHEN x = 1 THEN 'x は 1 です'
  WHEN x = 2 THEN 'x は 2 です'
  ELSE 'x はそれ以外の数です'
END

2.3 繰り返し

SQL には for 文や while 文はありません。
その代わり「集約(GROUP BY)」と「相関サブクエリ」を使って繰り返し処理を表現します。

2.3.1 GROUP BY を使った繰り返し

繰り返し処理を SQL 的に言い換えると 「集合にまとめて条件を書けること」 になります。

ex. 学生が全員レポート提出済みの学部を探す(再掲)

Students テーブル
student_id dpt sbmt_date
100 理学部 2018-10-10
101 理学部 2018-9-22
102 文学部
103 文学部 2018-9-10
200 文学部 2018-9-22
201 工学部
202 経済学部 2018-9-25
SELECT dpt
FROM Students
GROUP BY dpt
HAVING COUNT(*) = COUNT(sbmt_date);
結果
dpt
理学部
経済学部

集合に対して一気に条件をかけるので、ループは不要です。

2.3.2 相関サブクエリを使った繰り返し

相関サブクエリは「外側の行ごとに内側の検索を繰り返す」ため、ループ的に動作します。

ex. 各商品分類ごとの平均より高い商品を探す

Shohin テーブル
shohin_id shohin_mei shohin_bunrui hanbai_tanka
0001 T シャツ 衣服 1000
0002 穴あけパンチ 事務用品 500
0003 カッターシャツ 衣服 4000
0004 包丁 キッチン用品 3000
0005 圧力鍋 キッチン用品 6800
0006 フォーク キッチン用品 500
0007 おろしがね キッチン用品 880
0008 ボールペン 事務用品 100
SELECT
  shohin_bunrui,
  shohin_mei,
  hanbai_tanka
FROM
  Shohin S1
WHERE
  hanbai_tanka > (
      SELECT AVG(shohin_tanka)
      FROM Shohin S2
      WHERE S1.shohin_bunrui = S2.shohin_bunrui
      GROUP BY shohin_bunrui
    );
結果
shohin_bunrui shohin_mei hanbai_tanka
事務用品 穴あけパンチ 500
衣服 カッターシャツ 4000
キッチン用品 包丁 3000
キッチン用品 圧力鍋 6800
相関サブクエリを避けるなら WINDOW 関数を使う(WinMagic)

相関サブクエリには可読性やパフォーマンスの観点で課題があります 😵‍💫
WINDOW 関数を使うと、効率的に置き換えられる場合が多いです。

相関サブクエリは外側テーブルの行ごとにテーブルスキャンが発生します。
ただし、結合キーにインデックスが貼られている場合は、その限りではなく効率的に処理されます。

SELECT
  shohin_bunrui,
  shohin_mei,
  hanbai_tanka
FROM
  (
    SELECT
      shohin_mei,
      shohin_bunrui,
      hanbai_tanka,
      AVG(hanbai_tanka) OVER(PARTITION BY shohin_bunrui) AS avg_tanka
    FROM
      Shohin
  ) TMP
WHERE
  hanbai_tanka > avg_tanka;

理解しやすく、テーブルスキャンが 1 回で済むためパフォーマンスも良いです。

おわりに

『達人に学ぶ SQL 徹底指南書 第 2 版』を読んで SQL 脳が鍛えられた気がします 🤔
もっと早くに読めばよかった...

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?