SQL初心者がやりがち!知らずに書いてるアンチパターン10選と改善法

こんにちは、ゆうせいです。

「なんとなく動くからこれでいいかな?」
そう思って書いたSQLが、実はパフォーマンスの悪化や将来の不具合の原因になっていることがあります。

今回は、SQL初心者がやりがちな「やってはいけない書き方=アンチパターン」と、すぐに実践できる改善方法を紹介します。


アンチパターン①:SELECT * を使う

■ どんな間違い?

最もよく見かけるパターンです。テーブルのすべてのカラムを取得するために、以下のように書いてしまいます。

SELECT * FROM users;

たしかに楽ですし、テーブル構造を知らなくても動きます。でも…。

■ 何が問題なの?

1. 不必要なカラムまで取得してしまう

→ データ量が増え、ネットワーク転送が遅くなる。とくにJOINやサブクエリの中で使うと顕著です。

2. アプリ側での扱いが不安定になる

→ テーブル構造が変わったとき(カラム追加・順序変更)、プログラム側に副作用が出る可能性があります。

3. 可読性が低い

→ どのカラムが必要なのかがコードを見ただけではわかりません。


■ 改善方法:必要なカラムを明示する

SELECT id, name, email FROM users;

こうすることで、SQLの意図が明確になります。もし何百列もある場合は、本当にすべて必要かをまず考え直しましょう。


■ ちょっとしたコツ

テーブルの構造を確認したいだけなら、以下のようなクエリを使う方が適切です:

SHOW COLUMNS FROM users;


アンチパターン②:サブクエリの多用(特に相関サブクエリ)

■ どんな間違い?

複雑な処理を1行で完結させようと、サブクエリ(とくにSELECT内)を使いすぎるパターンです。

SELECT name,
       (SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id) AS order_count
FROM users;

いかにも「SQLっぽい」書き方ですが…。


■ 何が問題なの?

1. パフォーマンスが極端に悪くなる

users テーブルの行数だけ orders テーブルに対して 繰り返しクエリが実行されるため、N回クエリが走ることになります。

2. インデックスの恩恵が受けにくくなる

→ サブクエリの中で効率的な検索がされず、全件走査になりやすい。


■ 改善方法:JOINで書き直す

SELECT users.name, COUNT(orders.id) AS order_count
FROM users
LEFT JOIN orders ON users.id = orders.user_id
GROUP BY users.id;

このように書けば、1回のスキャンで済む上に、インデックスも効きやすくなります。


■ 相関サブクエリってなに?

サブクエリの中で外側のクエリの値(例:users.id)を参照しているものを「相関サブクエリ」と呼びます。
この相関サブクエリは便利そうに見えて、実行速度のボトルネックになりやすいんです。

こんにちは。ゆうせいです。

それでは、先ほどの続きとして SQLのアンチパターン第3・第4弾 をご紹介します!今回も「やってしまいがちだけど危ない書き方」と「どう直せばいいのか」を、丁寧に解説していきますね。


アンチパターン③:NULLに対する考慮が不十分

■ どんな間違い?

NULL は「空」や「ゼロ」とは違い、「値が存在しない」状態です。ですが、初心者がよくやってしまうのがこのような書き方。

SELECT * FROM users WHERE deleted_flag = NULL;

一見、deleted_flag がNULLのレコードを取りたいように見えますが…これは常に結果が0件になります。


■ 何が問題なの?

1. NULL=で比較できない

SQLにおいて、NULL = NULL という比較は false ではなく unknown(不明) と評価されます。
つまり、どんな値とも等しくないんです。

2. 条件式が正しくても、集計でハマる

COUNT(*) では NULL もカウントしますが、COUNT(column) だと NULLは無視されます


■ 改善方法:IS NULLを使う!

SELECT * FROM users WHERE deleted_flag IS NULL;

逆に、NULLでないレコードを取りたいときは IS NOT NULL を使います。


■ おまけ:NOT NULL + DEFAULT でNULLを避ける設計も◎

たとえば、フラグ的なカラム(例:is_deleted)なら最初から 0 を入れるようにして、NULLが入らないようにするのもおすすめです。

CREATE TABLE users (
  id INT,
  is_deleted TINYINT(1) NOT NULL DEFAULT 0
);


アンチパターン④:LIKE '%xxx%' を多用する

■ どんな間違い?

検索したい文字列を含んでいるレコードを探すため、以下のようなSQLを書きがちです。

SELECT * FROM products WHERE name LIKE '%camera%';

■ 何が問題なの?

1. インデックスが使えない

前方一致(xxx%)ならインデックスが効きますが、%xxx% のようなワイルドカード前置きは、フルスキャン(全件走査)になります。

2. パフォーマンスが著しく低下

とくに対象テーブルに数万〜数百万件ある場合、実行時間が何倍にも膨れ上がることも。


■ 改善方法①:前方一致検索を使う

SELECT * FROM products WHERE name LIKE 'camera%';

名前が「camera」で始まる商品を探すなら、これでインデックスが使えるようになります。


■ 改善方法②:全文検索を導入する(MySQLの場合)

MySQLの FULLTEXT INDEX を使えば、%xxx% のような「部分一致検索」でも高速に検索できるようになります。

ALTER TABLE products ADD FULLTEXT(name);
SELECT * FROM products WHERE MATCH(name) AGAINST('camera');

ただし、MyISAMInnoDBで挙動が違う点や、日本語全文検索では ngramMeCab との連携が必要な場合もあるので、導入には一工夫いります。


■ コラム:インデックスが効いているか確認するには?

EXPLAIN をつけることで、クエリの実行計画をチェックできます。

EXPLAIN SELECT * FROM products WHERE name LIKE '%camera%';

type: ALL(全件スキャン)になっていたら黄色信号!


アンチパターン⑤:OR条件を多用してインデックスを無効化

■ どんな間違い?

複数の条件を OR でつなぐパターン。以下のようなSQLです:

SELECT * FROM users 
WHERE email = 'a@example.com' OR phone = '090-1234-5678';

直感的で書きやすいですが、パフォーマンスの落とし穴が潜んでいます。


■ 何が問題なの?

1. インデックスが効かなくなる

OR は、複数のカラムをまたいで評価される場合インデックスが無効になることがあります。
MySQLは emailphone両方にインデックスがある場合でも、全体として全件スキャンになる可能性が高いです。

2. 複雑な条件になるほど遅くなる

条件が多いほど、評価にかかる計算コストが増えます


■ 改善方法①:UNIONを使う(単一インデックスを活かす)

SELECT * FROM users WHERE email = 'a@example.com'
UNION
SELECT * FROM users WHERE phone = '090-1234-5678';

この書き方であれば、それぞれの条件で別々にインデックスが使えるため、全体として高速に動作することがあります。


■ 改善方法②:IN を使えるケースでは活用

SELECT * FROM users WHERE user_id IN (1, 2, 3);

単一カラムで複数値を探すなら、ORよりもINの方がインデックス効率が良くなります。


■ ワンポイント!

どうしても OR を使いたい場合は、実行計画(EXPLAIN でインデックス使用状況をチェックしましょう!


アンチパターン⑥:インデックスを理解せずにクエリを書く

■ どんな間違い?

初心者に多いのが、「インデックスの存在を意識せずにSQLを書く」ケースです。

たとえば以下のようなクエリ:

SELECT * FROM orders WHERE YEAR(order_date) = 2024;

このクエリ、一見シンプルですが…。


■ 何が問題なの?

1. 関数を使うとインデックスが無効に

WHERE句でカラムに関数(例:YEAR())を使ってしまうと、インデックスがまったく使われません。結果として全件スキャンになり、遅くなります。

2. インデックスの「前方一致」ルールを理解していない

複合インデックス((last_name, first_name)など)では、左から順に条件指定しないと効かないというルールがあります。


■ 改善方法①:関数を使わずに範囲条件で書く

SELECT * FROM orders 
WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01';

■ 改善方法②:必要な列にインデックスを追加する

CREATE INDEX idx_order_date ON orders(order_date);

検索対象になるカラムには適切なインデックスを張ることが重要です。ただし、張りすぎると更新・削除の処理が遅くなるため注意!


■ 数式と説明

インデックスは、B+木(B Plus Tree)という構造で管理されています。
これは、以下のような比較検索が速い構造になっています:

  • order_date = '2024-03-10' → ◎
  • YEAR(order_date) = 2024 → ✕

アンチパターン⑦:暗黙の型変換によるバグやパフォーマンス低下

■ どんな間違い?

異なるデータ型をそのまま比較することによって、SQLエンジンが自動的に型変換を行う例です。

SELECT * FROM users WHERE user_id = '123';

この user_id カラムが INT 型であるにもかかわらず、比較対象を文字列('123')で書いています。


■ 何が問題なの?

1. インデックスが効かなくなる

MySQLは比較前に型を揃えようとしますが、この過程でインデックスが無効化される場合があります。

2. 比較の精度が落ちる・バグの温床

たとえば user_id = '123abc' のように書いた場合、MySQLは '123abc'123 に変換して比較します。
これは一見便利ですが、意図しない一致が発生する危険な仕様です。


■ 改善方法:比較対象は正しいデータ型で書く!

-- INT型には数値で
SELECT * FROM users WHERE user_id = 123;

-- DATE型には日付で
SELECT * FROM logs WHERE created_at >= '2024-01-01';

■ 明示的に変換する場合はCASTやCONVERTを使う

これは例外的なケースですが、意図がある場合だけ使うべきです。


■ 補足:MySQLの型変換ルールは独特

MySQLでは「文字列を数値に変換すると、先頭の数字部分だけを解釈する」という挙動があります。
他のDBMS(PostgreSQLなど)ではエラーになります。移植性にも要注意です!


アンチパターン⑧:トランザクションを使わずに更新処理を行う

■ どんな間違い?

複数の更新系クエリ(UPDATE, INSERT, DELETEなど)を、トランザクションを使わずに順番に実行してしまう例です。

UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

このコード、一見するとただの送金処理ですが…。


■ 何が問題なの?

1. 途中で失敗したら不整合になる

1行目は成功して、2行目が失敗したら…
お金がどこかに消えてしまうことになります!

2. 同時アクセスに弱い(排他制御が効かない)

複数人が同時に同じレコードを更新すると、**競合(Race Condition)**が発生しやすくなります。


■ 改善方法:トランザクション制御を使う!

START TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

COMMIT;

途中でエラーが起きた場合は、こうします:

ROLLBACK;

これですべての処理が一体化されて、成功も失敗もまとめて扱えるようになります。


■ トランザクションとは?

簡単に言えば、複数の処理を「まとめて1つのまとまり(原子的操作)」として扱う機能です。

  • 原子性(Atomicity):全部やるか、全部やらないか
  • 整合性(Consistency):常に一貫した状態を保つ
  • 独立性(Isolation):他の処理と干渉しない
  • 永続性(Durability):一度完了した処理は消えない

この4つをまとめて ACID特性 と呼びます。


■ 注意点

  • トランザクションは InnoDB エンジンでしか使えません(MyISAMは非対応)
  • 明示的に開始しないとオートコミットになることも

アンチパターン⑨:DELETE文やUPDATE文にWHERE句を付け忘れる

■ どんな間違い?

うっかり以下のようなSQLを書いてしまうことがあります。

DELETE FROM users;

UPDATE orders SET status = 'shipped';

この場合、全レコードが対象になります。しかも、気づいたときには手遅れということも…。


■ 何が問題なの?

1. 全件削除・全件更新になり、重大なデータ損失

数万件、数百万件のデータが一瞬で失われる可能性があります。

2. ロールバックできないケースもある

→ トランザクションを使っていないと、完全に取り返しがつかない状態になります。


■ 改善方法:必ずWHERE句を付ける!しかも意図を明確に!

DELETE FROM users WHERE id = 123;


UPDATE orders SET status = 'shipped' WHERE order_id = 1001;

■ ダブルチェックの習慣をつける!

  • 実行前にSELECT文で対象件数を確認する
  • 可能なら、開発環境やステージング環境で試してから本番に反映する

アンチパターン⑩:大量データへのUPDATE/DELETEを一気に実行する

■ どんな間違い?

以下のようなSQLで、数万件の更新・削除を一発で実行すること。

DELETE FROM logs WHERE created_at < '2023-01-01';


■ 何が問題なの?

1. ロック時間が長くなる

→ 他のクエリが待たされ、DB全体のパフォーマンスが著しく低下します。

2. タイムアウト・エラーの原因に

→ 一部のDB設定では、処理時間や行数に上限があるため、途中で落ちて中途半端な状態に。

3. リードレプリカやバックアップに悪影響

→ 大量処理により、スレーブ遅延やログ肥大が起きることも。


■ 改善方法:バッチ処理に分割する

-- 1000件ずつ削除する例
DELETE FROM logs WHERE created_at < '2023-01-01' LIMIT 1000;

これをループで繰り返すことで、小分けに安全に削除できます。

■ トランザクションやジョブ管理ツールとの併用も有効

  • BEGIN / COMMIT を使って安全に処理
  • 定期的にバッチを流すなら、スケジューラー(cronやAirflowなど)との連携も検討

まとめ:SQLのアンチパターンを避けて、安全・高速なデータ操作を!

今回ご紹介した「SQL初心者がやりがちなアンチパターン10選」は以下の通りです:

No.アンチパターン主な問題点
1SELECT * の乱用不必要なデータ取得・可読性低下
2サブクエリ多用パフォーマンス低下
3NULLの誤用意図しない検索・集計ミス
4LIKE '%xxx%'インデックスが効かない
5OR条件の濫用インデックス無効化
6インデックス軽視実行速度の悪化
7暗黙の型変換バグやインデックス無効
8トランザクション未使用データ不整合・同時更新の衝突
9WHERE句忘れ全件削除・全件更新
10一括大量操作ロック、タイムアウト、障害

今後の学習の指針

SQLを「書ける」から「設計できる」「最適化できる」レベルへステップアップするには、以下の観点を意識して学習を進めましょう:

  • 実行計画(EXPLAIN)を読みこなす力をつける
  • インデックス設計と正規化・非正規化のバランスを理解する
  • トランザクション管理とその挙動(ロック、分離レベルなど)を学ぶ
  • 慢性的なパフォーマンス問題を定量的に測定・改善できる視点を持つ

セイ・コンサルティング・グループの新人エンジニア研修のメニューへのリンク

投稿者プロフィール

山崎講師
山崎講師代表取締役
セイ・コンサルティング・グループ株式会社代表取締役。
岐阜県出身。
2000年創業、2004年会社設立。
IT企業向け人材育成研修歴業界歴20年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。