Mysql篇:MySQL distinct 与 group by 去重(where/having)
Mysql篇:MySQL distinct 与 group by 去重(where/having)
一、背景与问题
在数据处理场景中,重复数据是常见的问题。MySQL 提供了 DISTINCT 和 GROUP BY 两种去重机制,但二者在实现原理、性能表现和适用场景上有显著差异。本文将从底层原理出发,结合真实开发场景,深入分析两者的使用方式、常见误区和性能优化策略。
二、基本原理
1. DISTINCT 的工作原理
DISTINCT 是 SQL 中用于去重的关键词,其核心机制是:
- 在查询结果集生成阶段,对字段值进行去重处理
- 内部实现依赖于临时表(temporary table)和排序(sort)操作
- 默认按全字段进行比较(包括 NULL 值)
2. GROUP BY 的工作原理
GROUP BY 是聚合操作的核心,其处理流程包括:
- 按指定字段进行分组
- 内部使用哈希表(hash table)或排序(sort)进行分组
- 需配合聚合函数(如 COUNT、SUM、MAX 等)使用
- 可通过
HAVING子句进行分组过滤
3. 两者的本质区别
| 特性 | DISTINCT | GROUP BY |
|---|---|---|
| 去重目的 | 生成唯一值列表 | 生成分组统计结果 |
| 是否需要聚合函数 | 不需要 | 必须配合聚合函数使用 |
| 执行计划 | 使用临时表+排序 | 使用哈希/排序+分组 |
| 性能影响 | 可能产生全表扫描 | 可能产生全表扫描 |
| 灵活性 | 仅能去重,不能做统计 | 可同时做去重和统计 |
三、环境准备
-- 创建测试表
CREATE DATABASE IF NOT EXISTS test_db;
USE test_db;
-- 创建订单表
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT,
product_name VARCHAR(100),
quantity INT,
price DECIMAL(10,2),
order_date DATE
);
-- 插入测试数据
INSERT INTO orders (order_id, customer_id, product_name, quantity, price, order_date)
VALUES
(1, 101, 'Laptop', 2, 1299.99, '2023-01-15'),
(2, 102, 'Monitor', 1, 499.99, '2023-01-15'),
(3, 101, 'Laptop', 1, 1299.99, '2023-01-16'),
(4, 103, 'Keyboard', 3, 89.99, '2023-01-16'),
(5, 102, 'Monitor', 2, 499.99, '2023-01-17'),
(6, 101, 'Laptop', 2, 1299.99, '2023-01-18'),
(7, 104, 'Mouse', 1, 29.99, '2023-01-18'),
(8, 105, 'Speaker', 1, 199.99, '2023-01-19'),
(9, 101, 'Laptop', 1, 1299.99, '2023-01-20');四、核心实现
1. 基础去重:DISTINCT
-- 查询所有不重复的客户ID
SELECT DISTINCT customer_id FROM orders;执行计划分析:
- MySQL 会创建一个临时表,将
customer_id字段去重后存储 - 如果未指定索引,可能进行全表扫描
- 对于大数据量,建议在
customer_id字段建立索引
优化建议:
-- 建立索引
CREATE INDEX idx_customer_id ON orders(customer_id);2. 分组统计:GROUP BY
-- 查询每个客户的总订单金额
SELECT customer_id, SUM(price * quantity) AS total
FROM orders
GROUP BY customer_id;执行计划分析:
- 使用哈希分组(hash group by)或排序分组(sort group by)
- 如果未指定索引,可能进行全表扫描
- 聚合函数会计算每个分组的统计值
优化建议:
-- 建立复合索引
CREATE INDEX idx_customer_product ON orders(customer_id, product_name);3. 条件过滤:WHERE 与 HAVING
-- 查询购买金额超过 5000 的客户
SELECT customer_id, SUM(price * quantity) AS total
FROM orders
GROUP BY customer_id
HAVING SUM(price * quantity) > 5000;关键点:
WHERE用于过滤原始数据行HAVING用于过滤分组后的结果HAVING可以使用聚合函数进行条件判断
五、完整案例
场景:统计每个产品的销售总量
-- 创建产品表
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name VARCHAR(100)
);
-- 插入产品数据
INSERT INTO products (product_id, product_name)
VALUES
(1, 'Laptop'),
(2, 'Monitor'),
(3, 'Keyboard'),
(4, 'Mouse'),
(5, 'Speaker');
-- 统计每个产品的销售总量
SELECT p.product_name, SUM(o.quantity) AS total_sold
FROM orders o
JOIN products p ON o.product_name = p.product_name
GROUP BY p.product_name
ORDER BY total_sold DESC;结果分析:
Laptop销量最高,共 5 个Monitor销量次之,共 3 个- 其他产品销量较低
性能优化:
- 建立联合索引:
CREATE INDEX idx_product ON orders(product_name, quantity) - 使用子查询预处理数据:
SELECT product_name, SUM(quantity) FROM orders GROUP BY product_name
六、源码解析
以 MySQL 8.0 源码为例,GROUP BY 的处理流程如下:
- 解析阶段:
optimizer::optimize()处理GROUP BY语句 - 执行计划生成:
JOIN::make_join_plan()生成分组计划 分组执行:
- 如果使用
GROUP BY列作为索引,使用哈希分组 - 否则进行全表扫描并排序分组
- 如果使用
- 聚合计算:
item_sum::walk()执行聚合函数计算
对于 DISTINCT,其核心逻辑在 sql_select.cc 中,通过 make_distinct() 函数生成临时表并去重。
七、进阶使用
1. 多字段去重
-- 查询不重复的客户和产品组合
SELECT DISTINCT customer_id, product_name
FROM orders
WHERE order_date > '2023-01-15';2. 窗口函数替代方案
-- 使用窗口函数实现去重
SELECT customer_id, product_name
FROM (
SELECT
customer_id,
product_name,
ROW_NUMBER() OVER (PARTITION BY customer_id, product_name ORDER BY order_id) AS rn
FROM orders
) t
WHERE rn = 1;3. 联合去重
-- 联合两个表的去重查询
SELECT DISTINCT customer_id, product_name
FROM orders
UNION
SELECT customer_id, product_name
FROM returns;八、性能与工程实践
1. 性能优化策略
| 场景 | 优化方法 |
|---|---|
| 大表去重 | 使用 DISTINCT + 索引 |
| 分组统计 | 建立复合索引 + 聚合函数优化 |
| 多条件过滤 | 使用 WHERE 预过滤 + HAVING 精确过滤 |
| 联合查询 | 使用 UNION 替代 OR 条件 |
2. 索引设计建议
- 对
GROUP BY字段建立索引 - 对
DISTINCT字段建立索引 - 对
WHERE条件字段建立索引 - 对
JOIN字段建立复合索引
3. 异常处理
-- 处理空值的特殊处理
SELECT customer_id, SUM(price * quantity) AS total
FROM orders
GROUP BY customer_id
HAVING SUM(price * quantity) IS NOT NULL;4. 安全风险
- 避免使用
GROUP BY的SELECT *,可能导致数据泄露 - 对敏感字段使用
HAVING进行过滤 - 使用参数化查询防止 SQL 注入
九、常见问题与踩坑
1. 常见错误示例
-- 错误:GROUP BY 中未使用聚合字段
SELECT customer_id, product_name
FROM orders
GROUP BY customer_id;错误原因:product_name 未被聚合函数处理
2. 常见错误:DISTINCT 与 GROUP BY 混用
-- 错误:同时使用 DISTINCT 和 GROUP BY
SELECT DISTINCT customer_id, product_name
FROM orders
GROUP BY customer_id;错误原因:GROUP BY 已经完成分组,DISTINCT 无实际意义
3. 常见错误:HAVING 使用不当
-- 错误:HAVING 中使用非聚合字段
SELECT customer_id, SUM(price * quantity) AS total
FROM orders
GROUP BY customer_id
HAVING order_date > '2023-01-15';错误原因:order_date 不在 GROUP BY 或聚合函数中
十、最佳实践
1. 使用场景选择指南
| 场景 | 推荐方案 |
|---|---|
| 简单去重 | DISTINCT |
| 分组统计 | GROUP BY + 聚合函数 |
| 条件过滤 | WHERE + HAVING |
| 复杂聚合 | 窗口函数 + 子查询 |
| 多表联合 | UNION + 索引优化 |
2. 索引使用建议
- 对
GROUP BY字段建立索引 - 对
DISTINCT字段建立索引 - 对
WHERE条件字段建立索引 - 对
JOIN字段建立复合索引
3. 性能调优技巧
- 使用
EXPLAIN分析执行计划 - 通过
SHOW PROFILES分析查询耗时 - 使用
SHOW ENGINE INNODB STATUS分析锁问题 - 对大数据量使用分页查询(
LIMIT+OFFSET)
十一、总结
DISTINCT 和 GROUP BY 是 MySQL 中处理重复数据的核心工具,但二者在实现原理、适用场景和性能表现上有显著差异。在实际开发中需要根据具体需求选择合适的方法:
- 使用
DISTINCT进行简单去重时,注意索引优化 - 使用
GROUP BY进行分组统计时,合理使用聚合函数 - 在需要条件过滤时,结合
WHERE和HAVING使用 - 对大数据量查询,注意分页和索引设计
- 避免滥用
SELECT *,防止数据泄露
通过合理使用这些技术,可以有效提升数据处理的效率和准确性,同时确保系统的稳定性和安全性。在实际项目中,建议根据具体业务需求进行充分测试和性能调优,以达到最佳效果。
评论已关闭