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】
- スッキリわかるSQL入門 第4版:練習問題がたくさんある入門書。手を動かして基本を固めたい人向け
※Amazonのアソシエイトとして、当サイトは適格販売により収入を得ています。
参考リンク
- MySQL 8.0 Reference Manual: DISTINCT Optimization
- MySQL 8.0 Reference Manual: GROUP BY Optimization
- MySQL 8.0 Reference Manual: MySQL Handling of GROUP BY
アイキャッチ画像: Photo by Xiangkun ZHU on Unsplash
