DISTINCTとGROUP BYの違い:重複を消すならどちらを使うか

並んだ白いブロック SQL

DISTINCT と GROUP BY は、どちらも同じ値の行を1行にまとめられます。重複を消すだけなら結果は同じで、MySQL の中でもほぼ同じ方法で処理されます。違いが出るのは、件数や合計を一緒に出したいときと、ORDER BY を付けたときです。

注文データを例に、同じ結果になる書き方と違いが出る書き方を並べました。NULL の扱い、EXPLAIN で見た速さ、よく出る2つのエラーまで確かめ、最後にどちらを選ぶかを表にしています。

以下の結果は MySQL 8.0 で実際に実行したものです。

このシナリオで考える

小さなネットショップの注文テーブル orders を使います。お客さんは4人、注文は8件です。同じお客さんが同じ分類の商品を何度も買っているので、そのまま一覧にすると名前が重なります。

クーポンを使っていない注文は、coupon_code が NULL です。この列は NULL の扱いを確かめるときに使います。

CREATE TABLE orders (
  order_id      INT PRIMARY KEY,
  customer_name VARCHAR(20) NOT NULL,
  category      VARCHAR(20) NOT NULL,
  amount        INT NOT NULL,
  coupon_code   VARCHAR(10),
  order_date    DATE NOT NULL
);

INSERT INTO orders VALUES
  (1, '佐藤', '本',     1200, NULL,    '2026-11-01'),
  (2, '佐藤', '本',      800, 'NOV10', '2026-11-03'),
  (3, '佐藤', '文具',    300, NULL,    '2026-11-05'),
  (4, '鈴木', '本',     1500, 'NOV10', '2026-11-02'),
  (5, '鈴木', '食品',    600, NULL,    '2026-11-06'),
  (6, '高橋', '文具',    450, 'WELCOME', '2026-11-04'),
  (7, '高橋', '文具',    450, NULL,    '2026-11-07'),
  (8, '田中', '食品',    900, 'NOV10', '2026-11-08');

重複を消すだけならDISTINCTもGROUP BYも同じ結果

注文したことのあるお客さんの名前を、重複なしで出してみます。DISTINCT は SELECT のすぐあとに書き、GROUP BY はまとめたい列を指定します。

SELECT DISTINCT customer_name
FROM orders
ORDER BY customer_name;

SELECT customer_name
FROM orders
GROUP BY customer_name
ORDER BY customer_name;

2つのクエリの結果はまったく同じでした。

+---------------+
| customer_name |
+---------------+
| 佐藤          |
| 田中          |
| 鈴木          |
| 高橋          |
+---------------+

MySQL の公式リファレンスでも、DISTINCT はほとんどの場合 GROUP BY の特別な形として扱う、と書かれています。GROUP BY 向けの最適化が、DISTINCT にもそのまま使われます。

DISTINCTは選んだ列の組み合わせ全体にかかる

列を2つ選ぶと、DISTINCT は2列の組み合わせで重複を判定します。最初の列だけにかかるわけではありません。

SELECT DISTINCT customer_name, category
FROM orders
ORDER BY customer_name, category;
+---------------+----------+
| customer_name | category |
+---------------+----------+
| 佐藤          | 文具     |
| 佐藤          | 本       |
| 田中          | 食品     |
| 鈴木          | 本       |
| 鈴木          | 食品     |
| 高橋          | 文具     |
+---------------+----------+

佐藤さんは「本」と「文具」の2行に分かれています。よくある勘違いが SELECT DISTINCT(customer_name), category と書けば名前だけで重複を消せる、というものです。このカッコはただのカッコで、実行しても上とまったく同じ6行が返りました。名前ごとに1行にしたいなら、次の GROUP BY と集約関数を使います。

件数や合計も出すならGROUP BY

グループごとに数えたり足したりしたいときは GROUP BY を使います。DISTINCT には、まとめた行ごとに集計する機能がありません。

SELECT customer_name,
       COUNT(*)    AS order_count,
       SUM(amount) AS total_amount
FROM orders
GROUP BY customer_name
ORDER BY total_amount DESC;
+---------------+-------------+--------------+
| customer_name | order_count | total_amount |
+---------------+-------------+--------------+
| 佐藤          |           3 |         2300 |
| 鈴木          |           2 |         2100 |
| 高橋          |           2 |          900 |
| 田中          |           1 |          900 |
+---------------+-------------+--------------+

DISTINCT は集約関数の中でも使えます。「お客さんごとに何種類の分類を買ったか」は、GROUP BY と COUNT(DISTINCT ...) を組み合わせると1文で出せます。

SELECT customer_name,
       COUNT(DISTINCT category) AS category_count
FROM orders
GROUP BY customer_name
ORDER BY customer_name;
+---------------+----------------+
| customer_name | category_count |
+---------------+----------------+
| 佐藤          |              2 |
| 田中          |              1 |
| 鈴木          |              2 |
| 高橋          |              1 |
+---------------+----------------+

集計した結果をさらに絞りこむ HAVING は「GROUP BYとHAVINGの違いとWHERE句との使い分け」で説明しています。

NULLはどちらも1つのグループにまとまる

NULL どうしは = で比べると一致しません。それでも DISTINCT と GROUP BY では、NULL は同じ値として1行にまとまります。

SELECT DISTINCT coupon_code
FROM orders
ORDER BY coupon_code;

SELECT coupon_code, COUNT(*) AS order_count
FROM orders
GROUP BY coupon_code
ORDER BY coupon_code;
+-------------+
| coupon_code |
+-------------+
| NULL        |
| NOV10       |
| WELCOME     |
+-------------+

+-------------+-------------+
| coupon_code | order_count |
+-------------+-------------+
| NULL        |           4 |
| NOV10       |           3 |
| WELCOME     |           1 |
+-------------+-------------+

気をつけたいのは COUNT(DISTINCT coupon_code) です。こちらは NULL を数えないので、結果は2になります。

SELECT COUNT(DISTINCT coupon_code) AS coupon_kinds
FROM orders;
+--------------+
| coupon_kinds |
+--------------+
|            2 |
+--------------+

使われたクーポンの種類を知りたいなら、2で合っています。クーポンなしも1種類と数えたいなら、DISTINCT の結果の行数を数えます。

速さはほぼ同じ(EXPLAINで確認)

重複を消すだけのクエリなら、DISTINCT と GROUP BY の速さに差はほとんどありません。EXPLAIN(実行計画を表示するコマンド)で比べると、インデックスがないときは2つとも同じ計画でした。

EXPLAIN SELECT DISTINCT customer_name FROM orders;
EXPLAIN SELECT customer_name FROM orders GROUP BY customer_name;
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra           |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-----------------+
|  1 | SIMPLE      | orders | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    8 |   100.00 | Using temporary |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-----------------+

全件を読み、一時テーブル(Using temporary)で重複をまとめています。customer_name にインデックスを作ると、2つとも次の計画に変わりました。

CREATE INDEX idx_customer ON orders (customer_name);
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+------+----------+--------------------------+
| id | select_type | table  | partitions | type  | possible_keys | key          | key_len | ref  | rows | filtered | Extra                    |
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+------+----------+--------------------------+
|  1 | SIMPLE      | orders | NULL       | range | idx_customer  | idx_customer | 82      | NULL |    5 |   100.00 | Using index for group-by |
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+------+----------+--------------------------+

Using index for group-by は、インデックスの中の値の切れ目だけを飛ばし読みする方法です。DISTINCT でも GROUP BY でも同じように使われます。速くしたいときに見直すのは、書き方よりインデックスです。複数の列でまとめるときの列の順番は「MySQLの複合インデックス設計:左端マッチとカラム順の基本」で説明しています。

よくある落とし穴

DISTINCTとORDER BYでエラー3065

DISTINCT のクエリでは、SELECT に書いていない列で並べかえられません。初めて注文した日が早い順にお客さんを並べようとして、次のように書くとエラーになります。

SELECT DISTINCT customer_name
FROM orders
ORDER BY order_date;
ERROR 3065 (HY000): Expression #1 of ORDER BY clause is not in SELECT list, references column 'demo.orders.order_date' which is not in SELECT list; this is incompatible with DISTINCT

佐藤さんには注文日が3つあり、どの日で並べるかが決まらないためです。GROUP BY に書きかえて、MIN(order_date) のようにどの日を使うかを指定すると動きます。

SELECT customer_name
FROM orders
GROUP BY customer_name
ORDER BY MIN(order_date);
+---------------+
| customer_name |
+---------------+
| 佐藤          |
| 鈴木          |
| 高橋          |
| 田中          |
+---------------+

GROUP BYに書いていない列でエラー1055

GROUP BY のクエリで、まとめた列でも集約関数でもない列を SELECT に書くとエラーになります。MySQL 8.0 は ONLY_FULL_GROUP_BY が最初から有効だからです。

SELECT customer_name, category
FROM orders
GROUP BY customer_name;
ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'demo.orders.category' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

組み合わせの一覧がほしいなら GROUP BY customer_name, category にします。名前ごとに1行がよければ MAX(category) などの集約関数で値を1つに決めます。

並び順はORDER BYを書かないと決まらない

MySQL 5.7 までは、GROUP BY を書くと結果がその列で並んでいました。8.0 ではこの並べかえがなくなっています。DISTINCT も GROUP BY も、順番が必要なら ORDER BY を必ず書いてください。

どちらを選ぶか

やりたいこと 選ぶもの 理由
重複を消した一覧がほしいだけ DISTINCT 短く書けて、意図が読み手に伝わる
グループごとの件数・合計・最大値も出す GROUP BY 集約関数はグループごとに計算される
グループごとの種類の数を出す GROUP BY + COUNT(DISTINCT) 両方を組み合わせて1文で書ける
SELECT にない列の値で並べたい GROUP BY + MIN / MAX DISTINCT ではエラー3065になる
集計結果で絞りこむ GROUP BY + HAVING DISTINCT には絞りこむ方法がない

まとめ

重複を消すだけなら、DISTINCT と GROUP BY は結果も速さも同じです。そのときは意図が伝わりやすい DISTINCT を使い、件数や合計を出すときや、SELECT にない列で並べたいときに GROUP BY へ切りかえます。DISTINCT は選んだ列の組み合わせ全体にかかること、COUNT(DISTINCT 列) は NULL を数えないことの2つを覚えておくと、思ったのと違う結果になるのを防げます。

もっと学ぶなら【PR】

※Amazonのアソシエイトとして、当サイトは適格販売により収入を得ています。

参考リンク

アイキャッチ画像: Photo by Xiangkun ZHU on Unsplash

タイトルとURLをコピーしました