『SQLの苦手を克服する本』を読んでみて
1. はじめに
- 『SQLの苦手を克服する本』を読了。
- 読むきっかけは、PJ現場で初歩的なアンチパターンに引っ掛かることがあり、SQLの本質部分を事例ベースで学べる書籍を探していた。
- 知識の定着のため、自分が気になった点をピックアップしてまとめる。
2. 本書の前提
- SQLは集合指向の言語であり、手続き型言語とは全く違う概念であること
この前提を踏まえた上で、個人的に刺さったポイントについて書いていきます。
① 表形式のデータ操作イメージを持つ
② 「結合条件」と「抽出条件」の違い
③ CASE式とパラメータテーブルの活用
④ Entity-Attribute-Value手法はやめよう
⑤ インジェクション対策のためにもSQL動的組み立てはやめよう
⑥ SQLのための仕様書は書くだけ無駄
3. 個人的に刺さったポイント
① 表形式のデータ操作イメージを持つ
SQLを学ぶ上で「表形式のデータ操作イメージ」を持つと、直感的に理解しやすくなり、クエリの構造や処理の流れが頭に入りやすくなります。たとえば以下のような図解があると、SQLの処理がイメージしやすくなります。
■SELECT(列を選ぶ)
元のテーブル:
| name | age | gender |
|---|---|---|
| Alice | 25 | female |
| Bob | 35 | male |
| Emma | 28 | female |
クエリ:
SELECT name, age FROM users;
結果:
| name | age |
|---|---|
| Alice | 25 |
| Bob | 35 |
| Emma | 28 |
■WHERE(行を選ぶ)
元のテーブル:
| name | age |
|---|---|
| Alice | 25 |
| Bob | 35 |
| Emma | 28 |
クエリ:
SELECT * FROM users WHERE age > 30;
結果:
| name | age |
|---|---|
| Bob | 35 |
■JOIN(表同士の結合)
usersテーブル:
| id | name |
|---|---|
| 1 | Alice |
| 2 | Bob |
ordersテーブル:
| user_id | item |
|---|---|
| 1 | Laptop |
| 2 | Smartphone |
クエリ:
SELECT * FROM users JOIN orders ON users.id = orders.user_id;
結果:
| id | name | user_id | item |
|---|---|---|---|
| 1 | Alice | 1 | Laptop |
| 2 | Bob | 2 | Smartphone |
■GROUP BY(集計)
元のテーブル:
| gender |
|---|
| male |
| female |
| male |
| female |
| male |
クエリ:
SELECT gender, COUNT(*) FROM users GROUP BY gender;
結果:
| gender | COUNT(*) |
|---|---|
| male | 3 |
| female | 2 |
② 「結合条件」と「抽出条件」の違い
| 書き方 | 特徴 | 落とし穴 |
|---|---|---|
WHERE に条件を書く |
結合後のフィルタ | 欲しい行が消える危険あり |
JOIN ... ON に条件を書く |
結合時に制御できる | 欠落防止・意図が明確 |
③ CASE式とパラメータテーブルの活用
SQL内で条件分岐を行いたい場合、アプリケーション側でif文やfor文を書くのではなく、CASE式を使うことで「集合単位」での処理が可能になります。特に「行持ちデータを列持ちに変換(クロス集計)」する際に強力な武器になります。
また、条件値をクエリ内にハードコーディングせず、「パラメータテーブル(区分値管理テーブル)」に切り出す設計の重要性も本書では語られていました。
■CASE式の活用例(クロス集計)
元のテーブル(monthly_sales):
| year | month | amount |
|---|---|---|
| 2024 | 1 | 100 |
| 2024 | 2 | 150 |
| 2024 | 3 | 200 |
クエリ(月ごとの売上を横持ちに変換):
SELECT
year,
SUM(CASE WHEN month = 1 THEN amount ELSE 0 END) AS jan_sales,
SUM(CASE WHEN month = 2 THEN amount ELSE 0 END) AS feb_sales,
SUM(CASE WHEN month = 3 THEN amount ELSE 0 END) AS mar_sales
FROM monthly_sales
GROUP BY year;
④ Entity-Attribute-Value手法はやめよう
「将来どんなデータ項目が増えても対応できるように」という理由で、EAV(Entity-Attribute-Value)パターンを採用するのは避けるべきです。これは**「なんでも入る箱」を作ってしまうことによる弊害**がメリットを上回るためです。
■EAV(非推奨)のイメージ
| id | entity_id | attribute | value |
|---|---|---|---|
| 1 | 101 | name | Alice |
| 2 | 101 | age | 25 |
| 3 | 101 | city | Tokyo |
■何がダメなのか
-
型安全性が失われる:
valueカラムは通常VARCHAR型になりがちで、数値や日付としての整合性がDB側で担保できない。 - クエリが複雑化する: 「名前」と「年齢」を1行で取得したい場合、何度もテーブルを自己結合(Self Join)する必要があり、可読性もパフォーマンスも悪化する。
-
制約が使えない:
NOT NULL制約などが属性ごとに設定できないため、データ品質が低下する。
正規化されたテーブル設計を行うか、どうしてもスキーマレスな柔軟性が必要な場合は、JSON型の利用やNoSQLの検討が推奨されます。
⑤ インジェクション対策のためにもSQL動的組み立てはやめよう
文字列連結でSQLを組み立てるのは、セキュリティ(SQLインジェクション)の観点からも、パフォーマンス(実行計画のキャッシュ効率)の観点からもNGです。
必ず**プレースホルダ(バインド変数)**を使用する習慣をつける必要があります。
■悪い例(文字列連結)
# 悪意ある入力で意図しないデータ操作が可能になってしまう
sql = "SELECT * FROM users WHERE name = '" + user_input + "'"
■良い例(プレースホルダ)
# DBエンジン側で値を安全にエスケープ・処理してくれる
sql = "SELECT * FROM users WHERE name = ?"
params = [user_input]
また、プレースホルダを使うことで、DB側が「同じ構文のSQL」と認識しやすくなり、実行計画(Access Plan)の再利用率が上がり、パフォーマンス向上にも繋がります。
⑥ SQLのための仕様書は書くだけ無駄
「詳細設計書」として、Excel等で以下のようなドキュメントを作ることがありますが、これはメンテナンスコストの無駄であるという主張です。
仕様書例:
1. usersテーブルからageが20以上のデータを抽出する
2. ordersテーブルと結合する...
■なぜ無駄なのか
- SQL自体が仕様書である: SQLは宣言型言語であり、「何が欲しいか(結果)」を記述しているため、コードそのものが仕様を表している。
- 二重管理のコスト: SQLを修正したのに仕様書を直し忘れると、仕様書が嘘をつくことになる(そして誰も読まなくなる)。
■どうすべきか
「何をしているか(How)」を日本語訳するのではなく、**「なぜそのデータが必要なのか(Why/ビジネスロジック)」**をコメントやドキュメントに残すべきです。
4. 終わりに(感想)
- 新しい発見というより、"強いエンジニア"たちの思考や習慣を学び、納得感が得られた。
- 特に「理解に時間をかける」「伝えることをベースにする」は今後実践していきたい。
Discussion