この記事は何か
この記事は、経験を積んだプログラマほど使ってしまう、SQLのアンチパターンに関する記事です。
先に結論を言うと 「unionを使うときは、コードが汚くなっても、where句をunion中に置け」 です。
これもDBを担当していると見かけるアンチパターンで、今年も一回、痛い目を見せられました。その供養もかねて記事にします。
「同じレイアウトのテーブルを一括検索」の落とし穴
例えば、売上データに対して、当月の速報値を持つテーブルと、前月以前の確定値を持つテーブルがあり、レイアウトは全く一緒だとします。
で、プログラマとしては、速報か確定かを意識せず、とりあえず「売上」のデータを持ってくる、という処理を書きたくなるわけです
こういうとき、登場するのが SQLの「UNION」 です
select 売上日, 注文ID, 総金額 from 売上_速報 where 注文者ID = 'xxx' and 注文日 >= '2026-01-01'
union all
select 売上日, 注文ID, 総金額 from 売上_確定 where 注文者ID = 'xxx' and 注文日 >= '2026-01-01'
簡単に言うと、同じ列を持つ検索結果を縦に並べるSQLで、詳しくは以下などを参照。
そして、経験を積んだプログラマほど、上のSQLには違和感があるはずです。
多分、以下の方が多少マシな書き方に見えるし、
select *
from (
select 売上日, 注文ID, 総金額 from 売上_速報
union all
select 売上日, 注文ID, 総金額 from 売上_確定
)
where 注文者ID = 'xxx' and 注文日 >= '2026-01-01'
なんだったら、以下のようにやって、
create view 売上_統合 as (
select 売上日, 注文ID, 総金額, ・・・ from 売上_速報
union all
select 売上日, 注文ID, 総金額, ・・・ from 売上_確定
)
以下のようにやりたくなるはずです。
select 売上日, 注文ID, 総金額 from 売上_統合 where 注文者ID = 'xxx' and 注文日 >= '2026-01-01'
最初のSQLよりはるかに「キレイ」になりました。プログラマならそう感じるはずです。
でも、これ、ダメなんです。
なぜダメなのか
DBの実行計画が狂って、インデックスが使えなくなり、レスポンスが落ちるからです。
最近のDBは賢くて、ものすごく長いSQLとかでも、想定通り(時には想定以上)の実行計画を組んでくれるのですが、UNIONを使うと、驚くほど間違えます。
「注文者ID」と「注文日」にインデックスがあっても、以下のようにwhere句をunionの外に出しただけで、本当にあっさり、それを見失ってしまうのです。1
- unionをする前の各テーブルにwhere ⇒ インデックスが使える
select 売上日, 注文ID, 総金額 from 売上_速報 where 注文者ID = 'xxx' and 注文日 >= '2026-01-01'
union all
select 売上日, 注文ID, 総金額 from 売上_確定 where 注文者ID = 'xxx' and 注文日 >= '2026-01-01'
- unionの外にwhere ⇒ インデックスが使えない
select *
from (
select 売上日, 注文ID, 総金額 from 売上_速報
union all
select 売上日, 注文ID, 総金額 from 売上_確定
)
where 注文者ID = 'xxx' and 注文日 >= '2026-01-01'
DRYの原則から言えば、前者のSQLは絶対に間違いですが、システムを安定させる観点では、こう書かざるを得ないのです。
「unionを使うときは、コードが汚くなっても、where句をunion中に置け」
今回はそんなお話でした。
-
と書きましたが、実際には、ここまでシンプルなSQLならインデックスを使ってくれることもあると思います。ですが、unionに対して2026年時点のRDBMSは本当に脆いです。最悪なのは、テスト時はちゃんとインデックスを使えていたのに本番環境で突然失敗するとか、本当に些細な変更(条件をひとつ追加するなど)で突然ダメになるとか、そういう系。「突然」という言葉が好きなSEは、きっと、どこにもいないハズ。 ↩