SQL DISTINCTの使い方|重複除外・複数列・GROUP BYとの違い

記事の目的

SQL の SELECT DISTINCT を使って、検索結果から重複を除外できるようになります。

1列の重複除外に加えて、複数列を組み合わせた重複の扱い、件数を数える COUNT(DISTINCT …) 、よく混同される GROUP BY との違い、そして「テーブルの中の重複行そのものを消す」操作との違いまでを、PostgreSQL・MySQL・SQL Server の製品差に注意しながら整理します。

記事のサンプルテーブルの前提

本記事では、説明用に次のような employees テーブルがある前提で進めます。

  • id :自動採番(社員番号)
  • name :氏名(文字列)
  • department :部署
  • position :役職。重複を含むデータが入っている前提

実際の CREATE TABLE 文は別記事で扱います。本文中では次のサンプルデータがすでに入っているものとして説明します。

サンプルは 6 行です。注目してほしいのは、id=2 の「鈴木」と id=6 の「伊藤」が、department (営業)と position (一般)の組み合わせが同じになっている点です。これを使って「複数列の重複」の挙動を確認します。

目次

最短の答え

employees テーブルから「部署の一覧」を、重複なしで取り出します。

  • 主要なRDBMS(PostgreSQL / MySQL / SQL Server)で共通して使える基本形
SELECT DISTINCT department
FROM employees;

実行結果は次のようになります。

  1. 複数の列を組み合わせた「重複しない組み合わせ」を取り出す場合は、列をカンマで並べます。
SELECT DISTINCT department, position
FROM employees;
  1. 重複を除いた件数(部署が何種類あるか)を数えたい場合は、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;

NULL の扱い

DISTINCT の一覧表示では、複数の NULL を「同じ値」として 1 つにまとめます。これは通常の比較演算( NULL = NULL の結果が UNKNOWN になる)とは別の規則で、DISTINCT 専用の挙動です。3 製品とも同じ動きをします。

ただし、後述の COUNT(DISTINCT 列) では NULL は件数に含めません。「表示は 1 つにまとめるが、件数は数えない」という点で挙動が分かれる点に注意します。

複数列の挙動

SELECT DISTINCT のあとに複数の列を並べると、「列の組み合わせ」が同じになる行を 1 つにまとめます。一部の列だけに DISTINCT を効かせることはできません。

SELECT DISTINCT department, position
FROM employees;

サンプルは6行ありますが、結果は5行になりました。「鈴木」(id=2)と「伊藤」(id=6)のdepartment, position の組み合わせがどちらも「営業/一般」で重複していたため、1 行にまとめられています。

「DISTINCT を 1 列だけに効かせたい」はできない

SELECT DISTINCT department, name のように書いても、department だけに DISTINCT がかかるわけではありません。departmentname の組み合わせで重複が判定されます。

「部署ごとに代表者 1 名を取りたい」のように「グループごとに代表 1 行」を取りたい場合は、PostgreSQL の DISTINCT ON か、ROW_NUMBER() OVER (PARTITION BY …) のような別の手段を使います(後述)。

COUNT(DISTINCT …) ── 重複を除いた件数

重複を除いた件数を数えたい場合は、COUNT(DISTINCT 列) を使います。

SELECT COUNT(DISTINCT department) FROM employees;

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;

上記の挙動は版や互換モードで変わることがあります。各製品の現行ドキュメントで確認してください。

GROUP BY との違い

SELECT DISTINCTGROUP BY は、ある条件では同じ結果になりますが、目的が別です。

観点SELECT DISTINCTGROUP 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;

「重複行を削除する」とは別物

検索でよく混同されますが、SELECT DISTINCT は「検索結果の表示から重複を除く」操作であり、テーブルの中の重複行そのものは消えません。

「テーブルの中に二重登録された行があるので、実体を消したい」場合は、別のテクニックを使います。

  • GROUP BY で残す行を選び、それ以外を DELETE
  • ROW_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 点を押さえれば、現場のほとんどのケースに対応できます。

目次