1. SQL优化不只是加索引:打破常规认知
很多开发者一提到SQL优化,第一反应就是"加索引"。确实,索引是提升查询性能最直接的手段之一,但SQL优化远不止于此。我在实际工作中见过太多案例,开发者盲目添加各种索引,结果不仅没提升性能,反而让数据库变得更慢。
SQL优化是一个系统工程,需要从多个维度综合考虑。就像医生看病,不能只盯着症状开药,而要全面诊断找出病因。下面我就分享几个容易被忽视但效果显著的优化手段。
2. 查询重写:SQL语句本身的优化
2.1 避免SELECT * 的陷阱
新手最常犯的错误就是无脑使用SELECT *。这不仅会传输不必要的数据,还会导致索引失效。
-- 不好的写法 SELECT * FROM orders WHERE user_id = 100; -- 好的写法 SELECT order_id, order_date, total_amount FROM orders WHERE user_id = 100;实际案例:某电商平台订单查询页面,从SELECT *改为只查询必要字段后,查询时间从1200ms降到200ms。
2.2 慎用子查询,多用JOIN
子查询虽然写起来方便,但性能往往不如JOIN。特别是关联子查询(DEPENDENT SUBQUERY),会导致外层查询的每行记录都执行一次子查询。
-- 性能较差的子查询写法 SELECT user_id, user_name, (SELECT COUNT(*) FROM orders WHERE orders.user_id = users.user_id) AS order_count FROM users; -- 优化后的JOIN写法 SELECT u.user_id, u.user_name, COUNT(o.order_id) AS order_count FROM users u LEFT JOIN orders o ON u.user_id = o.user_id GROUP BY u.user_id, u.user_name;提示:MySQL 5.6+版本对子查询有优化,但复杂场景下JOIN仍是更好的选择
2.3 合理使用UNION ALL替代OR条件
当WHERE条件中有多个OR条件时,可以考虑用UNION ALL重写:
-- 原始写法(可能不走索引) SELECT * FROM products WHERE category_id = 5 OR price > 1000; -- 优化写法(保证使用索引) SELECT * FROM products WHERE category_id = 5 UNION ALL SELECT * FROM products WHERE price > 1000;3. 执行计划深度解析
3.1 EXPLAIN命令的进阶用法
大多数开发者只关注EXPLAIN的输出表格,其实JSON格式的输出包含更多细节:
EXPLAIN FORMAT=JSON SELECT * FROM orders WHERE user_id = 100;重点关注:
cost_info: 查询的预估成本attached_condition: 实际应用的过滤条件optimized_away: 是否被优化器优化掉
3.2 索引下推(ICP)优化
MySQL 5.6引入的Index Condition Pushdown特性,可以在存储引擎层就过滤数据:
-- 创建测试索引 ALTER TABLE orders ADD INDEX idx_user_status (user_id, status); -- 启用ICP(默认开启) SET optimizer_switch='index_condition_pushdown=on'; -- 查询示例 EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status LIKE 'P%';在Extra列看到"Using index condition"即表示使用了ICP。
4. 高级索引策略
4.1 函数索引的妙用
MySQL 8.0开始支持函数索引,可以解决很多特殊场景的查询优化:
-- 创建函数索引 CREATE INDEX idx_name_lower ON users ((LOWER(user_name))); -- 使用函数索引的查询 SELECT * FROM users WHERE LOWER(user_name) = 'john';4.2 多列索引的最佳实践
多列索引的列顺序至关重要,遵循以下原则:
- 高区分度列在前
- 等值查询列在前,范围查询列在后
- 经常查询的列在前
-- 用户表的多列索引示例 ALTER TABLE users ADD INDEX idx_query (status, register_date, vip_level);4.3 覆盖索引的威力
当查询的所有列都包含在索引中时,可以避免回表操作:
-- 创建覆盖索引 ALTER TABLE orders ADD INDEX idx_cover (user_id, order_date, total_amount); -- 使用覆盖索引的查询 EXPLAIN SELECT user_id, order_date, total_amount FROM orders WHERE user_id = 100;Extra列会显示"Using index"。
5. 数据库设计层面的优化
5.1 合理的数据类型选择
常见问题:
- 用VARCHAR存储IP地址(应用INET_ATON/INET_NTOA函数)
- 用字符串存储枚举值(应用TINYINT)
- 过度使用TEXT/BLOB类型
优化案例:
-- 存储IP地址的优化 -- 不好的写法 CREATE TABLE access_log ( ip_address VARCHAR(15) ); -- 好的写法 CREATE TABLE access_log ( ip_address INT UNSIGNED ); -- 查询时转换 SELECT INET_NTOA(ip_address) FROM access_log;5.2 范式与反范式的平衡
根据查询模式适当反范式化:
- 增加冗余字段减少JOIN
- 使用汇总表预计算统计结果
- 物化视图(MySQL可通过触发器实现)
-- 订单总额冗余示例 ALTER TABLE orders ADD COLUMN total_amount DECIMAL(10,2); -- 通过触发器维护冗余字段 CREATE TRIGGER update_order_total AFTER INSERT ON order_items FOR EACH ROW BEGIN UPDATE orders SET total_amount = ( SELECT SUM(price * quantity) FROM order_items WHERE order_id = NEW.order_id ) WHERE order_id = NEW.order_id; END;6. 服务器配置优化
6.1 关键参数调整
# InnoDB缓冲池大小(建议物理内存的50-70%) innodb_buffer_pool_size = 4G # 日志文件大小(建议256M-2G) innodb_log_file_size = 512M # 并发连接数 max_connections = 2006.2 查询缓存的问题
MySQL 8.0已移除查询缓存,但在早期版本中需要注意:
# 对于频繁更新的表,建议关闭查询缓存 query_cache_type = 07. 实战案例分析
7.1 分页查询优化
常见问题:LIMIT offset, size在大偏移量时性能差
优化方案1:使用覆盖索引+延迟关联
SELECT * FROM products p JOIN ( SELECT product_id FROM products WHERE category_id = 5 ORDER BY create_time DESC LIMIT 10000, 20 ) AS tmp ON p.product_id = tmp.product_id;优化方案2:记住上次查询的位置
-- 第一页 SELECT * FROM products WHERE category_id = 5 ORDER BY create_time DESC, product_id DESC LIMIT 20; -- 获取下一页(假设上一页最后一条记录的create_time和product_id) SELECT * FROM products WHERE category_id = 5 AND (create_time < '2023-01-01' OR (create_time = '2023-01-01' AND product_id < 100)) ORDER BY create_time DESC, product_id DESC LIMIT 20;7.2 大数据量统计优化
替代COUNT(*)的方案:
- 使用汇总表
- 使用Redis等缓存计数
- 使用EXPLAIN估算
-- 使用汇总表 CREATE TABLE user_stats ( user_id INT PRIMARY KEY, order_count INT, last_updated TIMESTAMP ); -- 定期更新 INSERT INTO user_stats (user_id, order_count, last_updated) SELECT user_id, COUNT(*), NOW() FROM orders GROUP BY user_id ON DUPLICATE KEY UPDATE order_count = VALUES(order_count), last_updated = VALUES(last_updated);8. 监控与持续优化
8.1 慢查询日志分析
# 启用慢查询日志 slow_query_log = 1 slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 1 log_queries_not_using_indexes = 1使用pt-query-digest工具分析:
pt-query-digest /var/log/mysql/mysql-slow.log8.2 Performance Schema的使用
-- 查看最耗资源的SQL SELECT * FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 10; -- 查看表访问情况 SELECT * FROM performance_schema.table_io_waits_summary_by_table ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;SQL优化是一门需要不断实践和总结的艺术。我在实际工作中发现,很多性能问题不是简单地加个索引就能解决的,需要从查询语句、索引设计、数据库结构、服务器配置等多个维度综合考虑。最重要的是要理解每种优化手段背后的原理,这样才能在具体场景中做出正确的选择。