「Javaは左脳、SQLは右脳を使う言語だ」——こう言うと、いかにも脳科学的な裏付けがありそうに聞こえますが、正直に言うと大袈裟な喩えです。正確には「手続き的知識(How)」と「宣言的知識(What)」の違いの話です。
Javaは「どういう手順で処理するか」を1行ずつ命令していく手続き型の言語です。対してSQLは「どういう結果が欲しいか」を宣言するだけで、手順自体はDBMSのオプティマイザに委ねる宣言型の言語です。この違いを、あえて「左脳/右脳」という誰もが知っている比喩に乗せて、体感してもらおうと思います。
JavaやPythonといった手続き型・オブジェクト指向の言語に慣れたエンジニアほど、いざSQLでクエリを設計しようとすると頭を抱えがちです。「for文で回せば一発なのに、SQLだとどう書けばいいんだ」——そんな経験がある方には、特に今回の話が刺さるのではないかと思います。
お題
以下の「所有資格」テーブルから、同一年に複数の資格取得をした社員のデータを取得してください。
「所有資格」テーブル
| 社員ID | 資格ID | 資格取得年月 |
|---|---|---|
| ID00001 | 101 | 201905 |
| ID00001 | 102 | 201704 |
| ID00002 | 201 | 201101 |
| ID00004 | 101 | 201207 |
| ID00004 | 102 | 201105 |
| ID00004 | 201 | 201205 |
| ID00005 | 101 | 201205 |
ID00004が2012年に2つの資格を取っているので、期待結果はこうなります。
| 社員ID | 資格ID | 資格取得年月 |
|---|---|---|
| ID00004 | 101 | 201207 |
| ID00004 | 201 | 201205 |
第1段階:左脳的アプローチ(手続き的思考)
論理的に考えると、まず「同一年に2つ以上資格を持つ人は誰か」を特定しよう、という発想になります。素直に GROUP BY + HAVING で集計します。
SELECT
社員ID,
SUBSTR(資格取得年月, 1, 4) AS 取得年,
COUNT(*) AS 取得数
FROM
所有資格
GROUP BY
社員ID,
SUBSTR(資格取得年月, 1, 4)
HAVING
COUNT(*) >= 2
上記の結果
| 社員ID | 取得年 | 取得数 |
|---|---|---|
| ID00004 | 2012 | 2 |
これで「誰が、いつ、複数取得したか」はわかりました。ただし、これは集計結果であり、求めたいのは元の明細行です。そこでこの結果を「条件」として使い、EXISTSで元テーブルを絞り込みます。
SELECT * FROM 所有資格 A
WHERE EXISTS (
SELECT
'X'
FROM
(
SELECT
社員ID,
SUBSTR(資格取得年月, 1, 4) AS 取得年,
COUNT(*) AS 取得数
FROM 所有資格
GROUP BY
社員ID,
SUBSTR(資格取得年月, 1, 4)
HAVING COUNT(*) >= 2) B
WHERE
A.社員ID = B.社員ID
AND SUBSTR(A.資格取得年月, 1, 4) = B.取得年
)
「まず条件を満たす集合を特定し、それを使って元データを絞り込む」——これは手順を一段ずつ積み上げていく、まさに手続き的な思考の流れです。
第2段階:右脳的アプローチ(宣言的思考)
ここで発想を切り替えます。「複数資格を持っている」ということは、裏を返せば「同じ社員・同じ年の行が、自分以外にも存在する」ということです。つまり、所有資格テーブル同士を「がっちゃんこ」すればいい、という関係的な発想です。
条件を整理すると:
-
社員IDは一致(別人と比べても意味がない)
-
資格取得年月の年は一致(同一年の条件)
-
資格IDは異なる(これがポイント。複数持っていれば、自分とは違う資格IDの行が必ずヒットする)
SELECT A.* FROM 所有資格 A
INNER JOIN 所有資格 B
ON A.社員ID = B.社員ID
AND SUBSTR(A.資格取得年月, 1, 4) = SUBSTR(B.資格取得年月, 1, 4)
AND A.資格ID <> B.資格ID
手順を考える必要はなく、「こういう関係を満たす行」を宣言するだけ。GROUP BYもHAVINGもEXISTSも要らず、一撃で書けます。これが宣言型思考の切れ味です。
落とし穴:宣言的思考の代償
ただし、この自己JOINには罠があります。同一年に3つ以上資格を取っている社員がいると、結果が重複します。
たとえばID00099が2020年に資格101・201・301の3つを取っていたとすると、A=101の行はB=201ともB=301ともマッチするため、A.*が2回出力されてしまいます。
社員ID 資格ID 資格取得年月
ID00099 101 202003 ← B=201とマッチ
ID00099 101 202003 ← B=301とマッチ(重複)
ID00099 201 202003
ID00099 201 202003
ID00099 301 202003
ID00099 301 202003
これは、JOINが「条件を満たす“組み合わせ”の数だけ行を生成する」という性質そのものです。対処は SELECT DISTINCT A.* とするだけですが、見落とすと本番データで初めて発覚しがちな静かなバグになります。
一方でEXISTS版は「存在するかどうか」の真偽判定でしかないため、対象が2件だろうと10件だろうと、この重複は原理的に起こりません。
▼ なぜ資格が3つ(101, 201, 301)あると行が増えてしまうのか?

※「自分以外の資格」が2つ以上あると、結合(JOIN)の性質上、元の行がその数だけ複製されてしまいます。
まとめ
| 左脳的アプローチ(GROUP BY + EXISTS) | 右脳的アプローチ(自己JOIN) | |
|---|---|---|
| 発想 | 手順を段階的に積み上げる | 満たすべき関係を宣言する |
| コードの見た目 | やや長い・二段構え | 簡潔・一撃 |
| 落とし穴 | ほぼなし | 3件以上でデカルト積的な重複が発生。DISTINCTが必須 |
「左脳/右脳」はあくまで喩えであり、実態は「手続き的知識」と「宣言的知識」の違いです。ですが、この2つのSQLを見比べると、思考の“質感”の違いは確かに体感できるのではないでしょうか。そして宣言的な書き方の切れ味は、集合の性質(今回で言えばJOINのデカルト積)を理解していないと、思わぬ形で牙を剥くという点も、あわせて持ち帰っていただければと思います。
だから私はSQLが好き
昨今はLLMをはじめ、ITエンジニアとして習得しなければならない技術が次から次へと押し寄せてきます。正直、危機感や大変さを感じない日はありません。ですが、それ以上に「ワクワク感」が勝っています。新しい技術に触れるたびに「まだこんな世界があったのか」と思えるのは、ITエンジニアを長く続けてきて本当に良かったと感じる瞬間です。
ただ、白状すると、そのワクワク感の中でもSQLはいまだに別格です。
ここでは紹介していませんが、パフォーマンスチューニングなど、他にも面白いトピックがたくさんあります。
SQLは決して新しい技術ではありません。むしろ古株です。それでも、今回のように「左脳的に解いた後、右脳的に解き直す」という頭の体操ができる技術に、私はSQL以外でまだ出会えていません。20年以上付き合ってきても、「その手があったか!」と膝を打つ瞬間が今でも普通にあります。自己JOINで「がっちゃんこ」する発想も、初めて出会ったときは衝撃でした。
一方で、SQLは「苦手」「地味」というイメージを持たれがちな技術でもあると感じています。多くのITエンジニアが、この頭の体操の面白さにまだ気づいていないのではないか——そんなもどかしさがずっとありました。そこで、自分なりにその魅力を形に残しておこうと思い、Oracle SQLの問題集を2冊書きました。ひたすらクエリを考える構成になっています。興味を持たれた方は、覗いてみてください。
※基本情報技術者〜DBスペシャリストレベルの頭の体操に最適です。
※Noteで同様の記事を挙げています。