記事の目的
SQL の SELECT DISTINCT を使って、検索結果から重複を除外できるようになります。
1列の重複除外に加えて、複数列を組み合わせた重複の扱い、件数を数える COUNT(DISTINCT …) 、よく混同される GROUP BY との違い、そして「テーブルの中の重複行そのものを消す」操作との違いまでを、PostgreSQL・MySQL・SQL Server の製品差に注意しながら整理します。
記事のサンプルテーブルの前提
本記事では、説明用に次のような employees テーブルがある前提で進めます。
- id :自動採番(社員番号)
- name :氏名(文字列)
- department :部署
- position :役職。重複を含むデータが入っている前提
実際の CREATE TABLE 文は別記事で扱います。本文中では次のサンプルデータがすでに入っているものとして説明します。
SELECT * FROM employees;
id | name | department | position
---+------+------------+----------
1 | 佐藤 | 営業 | 課長
2 | 鈴木 | 営業 | 一般
3 | 田中 | 開発 | 課長
4 | 山田 | 開発 | 一般
5 | 高橋 | 人事 | 一般
6 | 伊藤 | 営業 | 一般
サンプルは 6 行です。注目してほしいのは、id=2 の「鈴木」と id=6 の「伊藤」が、department (営業)と position (一般)の組み合わせが同じになっている点です。これを使って「複数列の重複」の挙動を確認します。
最短の答え
employees テーブルから「部署の一覧」を、重複なしで取り出します。
- 主要なRDBMS(PostgreSQL / MySQL / SQL Server)で共通して使える基本形
SELECT DISTINCT department
FROM employees;実行結果は次のようになります。
department
----------
営業
開発
人事
- 複数の列を組み合わせた「重複しない組み合わせ」を取り出す場合は、列をカンマで並べます。
SELECT DISTINCT department, position
FROM employees;- 重複を除いた件数(部署が何種類あるか)を数えたい場合は、
COUNT(DISTINCT …)を使います。
SELECT COUNT(DISTINCT department) FROM employees;ここから先は、それぞれの書き方の意味と、製品ごとに引っかかりやすい点を順に解説します。
DISTINCT 構文 ── 重複行を除外する基本
SELECT DISTINCT は、検索結果の中で「指定した列の値」が同じになる行を 1 つにまとめます。
SELECT DISTINCT 列名
FROM テーブル名;- SELECT の直後に DISTINCT を 1 つだけ書きます( DISTINCT は SELECT 句全体に対する指定です)。
- どの列か、どの並び順かは指定された列の組み合わせで判定されます。
- 元のテーブルは一切変わりません。あくまで検索結果の表示から重複を取り除く操作です。
サンプルの employees から部署の重複を除外すると、次のようになります。
SELECT DISTINCT department FROM employees;department
----------
営業
開発
人事
NULL の扱い
DISTINCT の一覧表示では、複数の NULL を「同じ値」として 1 つにまとめます。これは通常の比較演算( NULL = NULL の結果が UNKNOWN になる)とは別の規則で、DISTINCT 専用の挙動です。3 製品とも同じ動きをします。
ただし、後述の COUNT(DISTINCT 列) では NULL は件数に含めません。「表示は 1 つにまとめるが、件数は数えない」という点で挙動が分かれる点に注意します。
複数列の挙動
SELECT DISTINCT のあとに複数の列を並べると、「列の組み合わせ」が同じになる行を 1 つにまとめます。一部の列だけに DISTINCT を効かせることはできません。
SELECT DISTINCT department, position
FROM employees;department | position
-----------+----------
営業 | 課長
営業 | 一般
開発 | 課長
開発 | 一般
人事 | 一般
サンプルは6行ありますが、結果は5行になりました。「鈴木」(id=2)と「伊藤」(id=6)のdepartment, position の組み合わせがどちらも「営業/一般」で重複していたため、1 行にまとめられています。
「DISTINCT を 1 列だけに効かせたい」はできない
SELECT DISTINCT department, name のように書いても、department だけに DISTINCT がかかるわけではありません。department と name の組み合わせで重複が判定されます。
「部署ごとに代表者 1 名を取りたい」のように「グループごとに代表 1 行」を取りたい場合は、PostgreSQL の DISTINCT ON か、ROW_NUMBER() OVER (PARTITION BY …) のような別の手段を使います(後述)。
COUNT(DISTINCT …) ── 重複を除いた件数
重複を除いた件数を数えたい場合は、COUNT(DISTINCT 列) を使います。
SELECT COUNT(DISTINCT department) FROM employees;-- → 3
employees には 6 行ありますが、部署の種類は「営業・開発・人事」の 3 つです。
NULL は件数に含まれない
COUNT(DISTINCT 列) は、NULL を件数に含めません。DISTINCT の一覧表示では複数の NULL が 1 つにまとまって表示されることがありますが、COUNT(DISTINCT …) では NULL は数えられない、という非対称な挙動です。実務で意外な件数になりやすいので、NULL を含む列をカウントする際には頭に入れておきます。
複数列の COUNT(DISTINCT …) は製品差が大きい
複数列の組み合わせの種類数を数えたい場合、製品ごとに書き方と対応状況が異なります。
| 製品 | COUNT(DISTINCT 1列) | COUNT(DISTINCT 列, 列) の書き方 |
|---|---|---|
| PostgreSQL | 対応 | 対応しない。COUNT(DISTINCT (列, 列)) のようにタプルにする、または下記の代替を使う |
| MySQL | 対応 | 対応する。COUNT(DISTINCT 列1, 列2) がそのまま動く |
| SQL Server | 対応 | 対応しない。下記の代替を使う |
どの製品でも安全に動く代替は、サブクエリで DISTINCT した後に COUNT(*) を取る形です。
-- 部署×役職の組み合わせの種類数(3 製品とも動く)
SELECT COUNT(*) FROM (
SELECT DISTINCT department, position FROM employees
) AS sub;-- → 5
上記の挙動は版や互換モードで変わることがあります。各製品の現行ドキュメントで確認してください。
GROUP BY との違い
SELECT DISTINCT と GROUP BY は、ある条件では同じ結果になりますが、目的が別です。
| 観点 | SELECT DISTINCT | GROUP BY |
|---|---|---|
| 主な目的 | 結果から重複を除く | グループごとに集計する |
集計関数( COUNT /SUM 等) | グループ単位での集計はできない | 使える |
| グループへの条件指定 | DISTINCT 単独では使わない(グループへの条件はGROUP BY + HAVING の役割) | HAVING 句で指定できる |
| 並び順 | 保証されない( ORDER BY が必要) | 保証されない( ORDER BY が必要) |
「単に重複を除きたいだけ」なら DISTINCT 、「グループごとに件数や合計を出したい」なら GROUP BY 、と覚えておくと迷いません。
-- これは GROUP BY でしか書けない(部署ごとの人数を数える)
SELECT department, COUNT(*) AS 人数
FROM employees
GROUP BY department;department | 人数
-----------+------
営業 | 3
開発 | 2
人事 | 1
「重複行を削除する」とは別物
検索でよく混同されますが、SELECT DISTINCT は「検索結果の表示から重複を除く」操作であり、テーブルの中の重複行そのものは消えません。
「テーブルの中に二重登録された行があるので、実体を消したい」場合は、別のテクニックを使います。
GROUP BYで残す行を選び、それ以外をDELETEROW_NUMBER() OVER (PARTITION BY …)で重複行に番号を振り、2 番目以降を削除- PostgreSQL では
ctidを使った方法、BigQuery ではQUALIFYを使った方法もある
製品別の固有機能(PostgreSQL の DISTINCT ON)
PostgreSQL には、SELECT DISTINCT ON (列) という独自の構文があります。これは「指定した列ごとに、ORDER BY で先頭になる行を 1 行だけ取る」という機能で、汎用 DISTINCT とは別物です。
-- PostgreSQL: 部署ごとに id が最も小さい 1 名を取得
SELECT DISTINCT ON (department) id, name, department, position
FROM employees
ORDER BY department, id;DISTINCT ON (department) を使う場合、ORDER BY の先頭にも department を置きます。その後ろにid や日付を指定することで、部署ごとに「どの 1 行を残すか」を決めます。ORDER BY の先頭がDISTINCT ON の列と一致していないと、どの行が残るかが予測できない結果になります。
MySQL や SQL Server には同じ構文はなく、ROW_NUMBER() 等で代替します。詳しくは別記事で扱います。
落とし穴・注意点
SELECT DISTINCT は強力ですが、運用で混乱しがちな点があります。
並び順は保証されない
DISTINCT は重複を除くだけで、結果の並び順は決めません。順序が必要なら、必ず ORDER BY を明示します。
SELECT DISTINCT department
FROM employees
ORDER BY department;SELECT に無い列で ORDER BY できないことがある
SELECT DISTINCT のあとに ORDER BY を書くときは、原則として SELECT に含まれている列でしかソートできません。これは「集約後の結果セットを並べ替える」ためで、含まれていない列で並べると意味が定まらないからです。製品によりエラーになります。
SELECT DISTINCT * は意図しない結果になりやすい
SELECT DISTINCT * は「すべての列の組み合わせ」での重複除外です。主キーが含まれていれば必ず全行ユニークになり、DISTINCT が機能しません。重複を判定したい列だけを明示する方が安全です。
また、JOIN を含むクエリで結果が増えてしまったとき、DISTINCT * で見かけ上の重複を消すのは要注意です。多くの場合、JOIN 条件の誤りで行が膨らんでいるのが原因で、DISTINCT を付けても本質的な問題は解決しません。
まず JOIN 条件そのものを見直します。
パフォーマンスの罠
DISTINCT は内部的にソートまたはハッシュで重複を判定するため、大量データでは重い処理になります。インデックスの効きや実行計画は製品によって異なるため、件数が増えてきたら EXPLAIN で確認します。
集計が目的なら GROUP BY 、本当に重複が問題ない場面では DISTINCT を外す、というのが原則です。
まとめ
SELECT DISTINCT は検索結果から重複を除く基本演算ですが、複数列の挙動・COUNT(DISTINCT …) の製品差とNULL の扱い・GROUP BY との使い分け・「テーブルの重複行削除」との混同、の 4 点を押さえれば、現場のほとんどのケースに対応できます。
