Skip to content

MySQL复杂查询优化

复杂查询的定义与特征

复杂查询是指结构复杂、执行时间长、资源消耗大的SQL查询。典型特征包括:

  • 包含多个表连接(3个或以上)
  • 嵌套多层子查询
  • 包含复杂的WHERE条件
  • 使用聚合函数和GROUP BY子句
  • 使用ORDER BY和LIMIT
  • 涉及大表查询(百万级以上数据量)
  • 使用复杂的函数或表达式

复杂查询的执行计划分析

1. 使用EXPLAIN分析执行计划

EXPLAIN是分析查询执行计划的核心工具,通过它可以了解:

  • 查询的执行顺序
  • 表的访问方式
  • 连接类型
  • 索引使用情况
  • 估计的行数和成本
sql
EXPLAIN SELECT 
    o.order_id, o.order_date, 
    c.customer_name, c.customer_email,
    SUM(oi.quantity * p.price) AS total_amount
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
WHERE o.order_date BETWEEN '2023-01-01' AND '2023-12-31'
GROUP BY o.order_id, o.order_date, c.customer_name, c.customer_email
ORDER BY total_amount DESC
LIMIT 10;

2. 关键执行计划指标解读

2.1 select_type

  • SIMPLE:简单查询,不包含子查询或UNION
  • PRIMARY:主查询
  • SUBQUERY:子查询
  • DERIVED:派生表(FROM子查询)
  • UNION:UNION中的第二个或后续查询
  • UNION RESULT:UNION的结果集

2.2 type

访问类型,性能从好到差依次为:

  • system:表只有一行记录
  • const:使用主键或唯一索引查询
  • eq_ref:多表连接时使用主键或唯一索引
  • ref:使用非唯一索引查询
  • range:使用索引范围查询
  • index:扫描整个索引
  • ALL:全表扫描

2.3 rows

估计需要扫描的行数,值越小越好。

2.4 filtered

通过条件过滤后剩余的行百分比,值越大越好。

2.5 Extra

额外信息,常见值包括:

  • Using index:使用覆盖索引
  • Using where:使用WHERE条件过滤
  • Using temporary:使用临时表
  • Using filesort:使用文件排序
  • Using join buffer:使用连接缓冲区

3. 使用EXPLAIN ANALYZE(MySQL 8.0+)

MySQL 8.0引入了EXPLAIN ANALYZE,提供实际执行计划和执行时间:

sql
EXPLAIN ANALYZE SELECT 
    o.order_id, o.order_date, 
    c.customer_name, c.customer_email,
    SUM(oi.quantity * p.price) AS total_amount
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
WHERE o.order_date BETWEEN '2023-01-01' AND '2023-12-31'
GROUP BY o.order_id, o.order_date, c.customer_name, c.customer_email
ORDER BY total_amount DESC
LIMIT 10;

复杂查询优化策略

1. 优化表连接

1.1 选择合适的连接顺序

  • 小表驱动大表:将结果集较小的表作为驱动表
  • 使用STRAIGHT_JOIN提示强制连接顺序
  • 让优化器自动选择连接顺序(大多数情况下)

1.2 优化连接条件

  • 确保连接条件使用相同的数据类型
  • 避免在连接条件中使用函数或表达式
  • 使用索引列作为连接条件

1.3 限制连接表的数量

  • 尽量减少连接表的数量(建议不超过5个)
  • 对于复杂查询,考虑拆分为多个简单查询
  • 使用临时表存储中间结果

2. 优化WHERE条件

2.1 避免在WHERE条件中使用函数

优化前

sql
SELECT * FROM orders WHERE YEAR(order_date) = 2023;

优化后

sql
SELECT * FROM orders WHERE order_date BETWEEN '2023-01-01' AND '2023-12-31';

2.2 避免使用LIKE通配符开头

优化前

sql
SELECT * FROM customers WHERE customer_name LIKE '%John%';

优化后

sql
-- 使用全文索引
ALTER TABLE customers ADD FULLTEXT INDEX idx_customer_name(customer_name);
SELECT * FROM customers WHERE MATCH(customer_name) AGAINST('John' IN NATURAL LANGUAGE MODE);

2.3 优化OR条件

优化前

sql
SELECT * FROM orders WHERE order_id = 1001 OR customer_id = 2001;

优化后

sql
SELECT * FROM orders WHERE order_id = 1001
UNION ALL
SELECT * FROM orders WHERE customer_id = 2001;

2.4 使用IN替代OR(当值较多时)

优化前

sql
SELECT * FROM products WHERE category_id = 1 OR category_id = 2 OR category_id = 3;

优化后

sql
SELECT * FROM products WHERE category_id IN (1, 2, 3);

3. 优化GROUP BY和ORDER BY

3.1 避免使用SELECT *配合GROUP BY

优化前

sql
SELECT * FROM order_items GROUP BY product_id;

优化后

sql
SELECT product_id, SUM(quantity) FROM order_items GROUP BY product_id;

3.2 使用索引优化GROUP BY

确保GROUP BY的列上有适当的索引,或与ORDER BY的列相同,形成覆盖索引:

sql
-- 创建覆盖索引
CREATE INDEX idx_order_date_customer_id ON orders(order_date, customer_id);

3.3 优化ORDER BY

  • 使用索引排序:确保ORDER BY的列上有适当的索引
  • 避免在ORDER BY中使用函数
  • 考虑使用延迟排序:先分页,再排序

优化前

sql
SELECT * FROM products ORDER BY price DESC LIMIT 10 OFFSET 1000000;

优化后

sql
SELECT p.* FROM products p
JOIN (
    SELECT product_id FROM products 
    ORDER BY price DESC 
    LIMIT 10 OFFSET 1000000
) tmp ON p.product_id = tmp.product_id
ORDER BY p.price DESC;

4. 优化子查询

4.1 将子查询转换为JOIN

优化前

sql
SELECT * FROM products p
WHERE p.product_id IN (
    SELECT oi.product_id FROM order_items oi 
    WHERE oi.quantity > 10
);

优化后

sql
SELECT DISTINCT p.* FROM products p
JOIN order_items oi ON p.product_id = oi.product_id
WHERE oi.quantity > 10;

4.2 使用物化表缓存子查询结果

sql
CREATE TEMPORARY TABLE high_quantity_products AS
SELECT product_id FROM order_items WHERE quantity > 10;

ALTER TABLE high_quantity_products ADD INDEX idx_product_id(product_id);

SELECT * FROM products p
WHERE p.product_id IN (SELECT product_id FROM high_quantity_products);

5. 优化聚合查询

5.1 使用合适的聚合函数

  • COUNT():计数

    • COUNT(*):统计行数,不忽略NULL
    • COUNT(column):统计非NULL值的数量
    • COUNT(DISTINCT column):统计不同值的数量(性能较差)
  • SUM():求和

  • AVG():平均值

  • MAX()/MIN():最大值/最小值

5.2 优化COUNT()查询

优化前

sql
SELECT COUNT(DISTINCT customer_id) FROM orders;

优化后

sql
SELECT COUNT(*) FROM (
    SELECT DISTINCT customer_id FROM orders
) tmp;

5.3 避免在聚合函数中使用子查询

优化前

sql
SELECT 
    product_id,
    (SELECT MAX(price) FROM products WHERE category_id = p.category_id) AS max_price
FROM products p;

优化后

sql
SELECT 
    p.product_id, m.max_price
FROM products p
JOIN (
    SELECT category_id, MAX(price) AS max_price 
    FROM products 
    GROUP BY category_id
) m ON p.category_id = m.category_id;

6. 使用临时表和物化视图

6.1 使用临时表存储中间结果

对于复杂查询,可以将中间结果存储在临时表中,减少重复计算:

sql
-- 创建临时表存储订单数据
CREATE TEMPORARY TABLE temp_orders AS
SELECT 
    o.order_id, o.order_date, o.customer_id,
    SUM(oi.quantity * p.price) AS total_amount
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
WHERE o.order_date BETWEEN '2023-01-01' AND '2023-12-31'
GROUP BY o.order_id, o.order_date, o.customer_id;

-- 添加索引
ALTER TABLE temp_orders ADD INDEX idx_total_amount(total_amount);
ALTER TABLE temp_orders ADD INDEX idx_customer_id(customer_id);

-- 使用临时表查询
SELECT 
    t.order_id, t.order_date, 
    c.customer_name, t.total_amount
FROM temp_orders t
JOIN customers c ON t.customer_id = c.customer_id
ORDER BY t.total_amount DESC
LIMIT 10;

6.2 使用物化视图(MySQL 8.0+)

MySQL 8.0支持物化视图,可以自动刷新或手动刷新:

sql
CREATE MATERIALIZED VIEW mv_top_orders
ENGINE = InnoDB
AS
SELECT 
    o.order_id, o.order_date, 
    c.customer_name, c.customer_email,
    SUM(oi.quantity * p.price) AS total_amount
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
GROUP BY o.order_id, o.order_date, c.customer_name, c.customer_email;

-- 手动刷新物化视图
REFRESH MATERIALIZED VIEW mv_top_orders;

-- 使用物化视图查询
SELECT * FROM mv_top_orders ORDER BY total_amount DESC LIMIT 10;

7. 优化索引

7.1 创建合适的索引

  • 为WHERE条件中的列创建索引
  • 为连接条件中的列创建索引
  • 为ORDER BY和GROUP BY的列创建索引
  • 使用覆盖索引减少回表操作

7.2 避免创建过多索引

  • 每个索引都会增加写操作的成本
  • 索引会占用存储空间
  • 优化器需要选择合适的索引,索引过多会增加优化器的负担

7.3 定期维护索引

  • 使用ANALYZE TABLE更新表统计信息
  • 使用OPTIMIZE TABLE优化表和索引
  • 删除不使用的索引

8. 优化服务器参数

8.1 调整连接缓冲区

ini
join_buffer_size = 256K

8.2 调整排序缓冲区

ini
sort_buffer_size = 2M

8.3 调整临时表大小

ini
tmp_table_size = 64M
max_heap_table_size = 64M

8.4 调整查询缓存(MySQL 5.7及之前)

ini
query_cache_type = 1
query_cache_size = 64M

8.5 调整InnoDB参数

ini
innodb_buffer_pool_size = 8G  # 建议为服务器内存的50-70%
innodb_log_file_size = 1G
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT

复杂查询优化案例

案例1:多表连接优化

问题:查询2023年每个客户的订单总数和总金额

优化前

sql
SELECT 
    c.customer_id, c.customer_name,
    COUNT(o.order_id) AS order_count,
    SUM(oi.quantity * p.price) AS total_amount
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
LEFT JOIN order_items oi ON o.order_id = oi.order_id
LEFT JOIN products p ON oi.product_id = p.product_id
WHERE o.order_date BETWEEN '2023-01-01' AND '2023-12-31'
GROUP BY c.customer_id, c.customer_name;

优化后

sql
-- 创建覆盖索引
CREATE INDEX idx_orders_customer_date ON orders(customer_id, order_date);
CREATE INDEX idx_order_items_order_product ON order_items(order_id, product_id, quantity);
CREATE INDEX idx_products_product_price ON products(product_id, price);

-- 优化查询
SELECT 
    c.customer_id, c.customer_name,
    COALESCE(o.order_count, 0) AS order_count,
    COALESCE(o.total_amount, 0) AS total_amount
FROM customers c
LEFT JOIN (
    SELECT 
        o.customer_id,
        COUNT(o.order_id) AS order_count,
        SUM(oi.quantity * p.price) AS total_amount
    FROM orders o
    JOIN order_items oi ON o.order_id = oi.order_id
    JOIN products p ON oi.product_id = p.product_id
    WHERE o.order_date BETWEEN '2023-01-01' AND '2023-12-31'
    GROUP BY o.customer_id
) o ON c.customer_id = o.customer_id;

案例2:子查询优化

问题:查询购买了所有产品类别的客户

优化前

sql
SELECT c.customer_id, c.customer_name
FROM customers c
WHERE NOT EXISTS (
    SELECT DISTINCT category_id FROM products
    WHERE category_id NOT IN (
        SELECT DISTINCT p.category_id
        FROM orders o
        JOIN order_items oi ON o.order_id = oi.order_id
        JOIN products p ON oi.product_id = p.product_id
        WHERE o.customer_id = c.customer_id
    )
);

优化后

sql
-- 创建临时表存储每个客户购买的产品类别
CREATE TEMPORARY TABLE customer_categories AS
SELECT 
    o.customer_id, 
    COUNT(DISTINCT p.category_id) AS category_count
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
GROUP BY o.customer_id;

-- 获取总产品类别数
SELECT @total_categories := COUNT(DISTINCT category_id) FROM products;

-- 查询购买了所有产品类别的客户
SELECT c.customer_id, c.customer_name
FROM customers c
JOIN customer_categories cc ON c.customer_id = cc.customer_id
WHERE cc.category_count = @total_categories;

案例3:聚合查询优化

问题:查询每个月的订单数、总金额和平均金额

优化前

sql
SELECT 
    YEAR(order_date) AS year,
    MONTH(order_date) AS month,
    COUNT(order_id) AS order_count,
    SUM(total_amount) AS total_amount,
    AVG(total_amount) AS avg_amount
FROM (
    SELECT 
        o.order_id, o.order_date,
        SUM(oi.quantity * p.price) AS total_amount
    FROM orders o
    JOIN order_items oi ON o.order_id = oi.order_id
    JOIN products p ON oi.product_id = p.product_id
    GROUP BY o.order_id, o.order_date
) tmp
GROUP BY YEAR(order_date), MONTH(order_date)
ORDER BY year, month;

优化后

sql
-- 创建覆盖索引
CREATE INDEX idx_orders_date ON orders(order_date);

-- 优化查询
SELECT 
    YEAR(o.order_date) AS year,
    MONTH(o.order_date) AS month,
    COUNT(DISTINCT o.order_id) AS order_count,
    SUM(oi.quantity * p.price) AS total_amount,
    SUM(oi.quantity * p.price) / COUNT(DISTINCT o.order_id) AS avg_amount
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
GROUP BY YEAR(o.order_date), MONTH(o.order_date)
ORDER BY year, month;

复杂查询的最佳实践

1. 拆分复杂查询

  • 将复杂查询拆分为多个简单查询
  • 使用应用程序处理中间结果
  • 避免一次性查询所有数据

2. 限制结果集大小

  • 使用LIMIT限制返回的行数
  • 只查询需要的列,避免SELECT *
  • 分页查询时使用合适的分页策略

3. 使用缓存

  • 缓存查询结果
  • 使用Redis等缓存系统
  • 利用MySQL的查询缓存(MySQL 5.7及之前)

4. 定期优化表和索引

  • 定期更新表统计信息
  • 定期优化表和索引
  • 删除不使用的索引

5. 监控查询性能

  • 使用慢查询日志记录慢查询
  • 使用Performance Schema监控查询性能
  • 使用EXPLAIN分析执行计划
  • 使用第三方监控工具(如Percona Monitoring and Management)

6. 升级MySQL版本

  • 新版本的MySQL在查询优化方面有显著改进
  • MySQL 8.0引入了许多优化特性,如EXPLAIN ANALYZE、物化视图等

常见问题(FAQ)

Q1: 如何判断查询是否需要优化?

A1: 可以通过以下方式判断:

  • 查询执行时间超过预期
  • 查询消耗大量资源(CPU、内存、磁盘I/O)
  • 查询导致服务器负载过高
  • 查询阻塞其他查询
  • 通过EXPLAIN发现全表扫描或其他低效操作

Q2: 如何优化包含多个OR条件的查询?

A2: 可以考虑以下优化方案:

  • 将OR条件转换为UNION ALL
  • 使用IN替代多个OR条件
  • 为OR条件中的列创建索引
  • 考虑使用全文索引(针对字符串匹配)

Q3: 如何优化ORDER BY和LIMIT的组合查询?

A3: 可以考虑以下优化方案:

  • 为ORDER BY的列创建索引
  • 使用延迟排序:先分页,再排序
  • 避免在ORDER BY中使用函数
  • 考虑使用覆盖索引

Q4: 如何优化GROUP BY查询?

A4: 可以考虑以下优化方案:

  • 为GROUP BY的列创建索引
  • 避免在GROUP BY中使用函数
  • 限制GROUP BY查询的结果集大小
  • 考虑使用物化视图

Q5: 如何优化子查询?

A5: 可以考虑以下优化方案:

  • 将子查询转换为JOIN
  • 使用物化表缓存子查询结果
  • 避免使用相关子查询
  • 升级到MySQL 5.6+版本,利用优化器的子查询优化功能

Q6: 如何优化多表连接查询?

A6: 可以考虑以下优化方案:

  • 选择合适的连接顺序
  • 优化连接条件
  • 限制连接表的数量
  • 为连接条件中的列创建索引
  • 考虑使用临时表存储中间结果

Q7: 如何处理慢查询?

A7: 处理慢查询的步骤:

  1. 使用EXPLAIN分析执行计划
  2. 查看是否使用了合适的索引
  3. 优化WHERE条件
  4. 优化表连接
  5. 考虑拆分查询
  6. 调整服务器参数
  7. 升级硬件

Q8: 如何监控查询性能?

A8: 可以使用以下工具和方法:

  • 慢查询日志
  • Performance Schema
  • EXPLAIN和EXPLAIN ANALYZE
  • MySQL Enterprise Monitor
  • Percona Monitoring and Management
  • Grafana + Prometheus

Q9: 如何优化大数据量查询?

A9: 可以考虑以下优化方案:

  • 使用分区表
  • 限制返回的行数
  • 只查询需要的列
  • 使用索引
  • 考虑使用数据仓库或分析数据库
  • 定期归档历史数据

Q10: 如何选择合适的索引?

A10: 选择合适索引的原则:

  • 为WHERE条件中的列创建索引
  • 为连接条件中的列创建索引
  • 为ORDER BY和GROUP BY的列创建索引
  • 使用覆盖索引减少回表操作
  • 避免创建过多索引
  • 考虑索引的选择性

版本差异与兼容性

1. MySQL 5.5及之前

  • 子查询优化有限
  • 不支持物化视图
  • 不支持EXPLAIN ANALYZE
  • 查询优化器相对简单

2. MySQL 5.6

  • 引入了子查询优化(半连接优化、物化等)
  • 改进了查询优化器
  • 支持EXPLAIN FORMAT=JSON

3. MySQL 5.7

  • 进一步改进了查询优化器
  • 支持JSON数据类型
  • 支持生成列
  • 改进了性能_schema

4. MySQL 8.0

  • 支持EXPLAIN ANALYZE
  • 支持物化视图
  • 支持CTE(公共表表达式)
  • 支持窗口函数
  • 改进了查询优化器
  • 支持降序索引
  • 支持直方图统计信息