广告位 · 页头(后台-模块管理-新建模块后替换此区域)

如何在SQL中优化查询?提高数据库性能的实用建议

拿到一条执行超过两秒的SQL,第一反应不该是立刻加索引,而是先搞清楚时间花在哪里。数据库慢查询的原因很多,可能是全表扫描、排序代价过高、锁等待,也可能是返回列过多导致大量回表。如果连执行计划都没看就盲目建索引,很容易出现索引加了一堆、性能反而没有提升的情况。优化SQL应该遵循定位、调整、验证的循环,每一步都以执行计划为依据。

如何在SQL中优化查询?提高数据库性能的实用建议

先看执行计划,定位真实瓶颈

数据库在执行SQL前会生成执行计划,决定走哪个索引、以什么顺序连接表、是否需要额外排序或临时表。不同数据库查看方式略有差异:MySQL用EXPLAIN,PostgreSQL用EXPLAIN ANALYZE,SQL Server可以查看图形化执行计划。不要只盯着慢日志里的耗时,要把计划中的关键字段读出来。

以一条订单查询为例,可以先执行下面语句查看计划:

EXPLAIN
SELECT *
FROM orders
WHERE customer_id = 1001
  AND status = 'paid';

重点看几个字段。如果type为ALL,说明走了全表扫描;如果rows很大,表示需要扫描的行数过多。再看key,它表示实际使用的索引,如果为NULL,大概率没有合适的索引可用。最后看Extra,出现Using filesort或Using temporary时,说明查询触发了额外排序或临时表,这两个操作在数据量上去后会明显拖慢响应速度。

很多时候慢查询并不是因为没有索引,而是索引失效。比如隐式类型转换、在索引列上使用函数、复合索引列顺序不对,都会导致优化器放弃索引。先读执行计划,再判断是加索引、换写法还是改表结构,能避免大量无意义的调整。

索引策略:不是越多越好,而是越准越好

索引能加速查询,但每次插入、更新、删除都要维护索引,写多读少的表索引过多反而拖慢写入。设计索引时优先考虑选择性高的列,选择性可以用不重复值除以总行数来估算。性别、状态这类基数很小的列单独建索引意义不大,优化器可能认为全表扫描代价更低。复合索引要遵循最左前缀原则,比如为(customer_id, status)建复合索引,查询只按status过滤时,索引利用率会很低。

另一个高频问题是索引列被函数或类型转换包裹。例如WHERE DATE(create_time) = '2024-01-01'会导致索引失效,应改写为范围条件:

SELECT *
FROM orders
WHERE create_time >= '2024-01-01 00:00:00'
  AND create_time < '2024-01-02 00:00:00';

这样优化器可以利用create_time上的索引进行范围扫描。类似的还有字符串类型的隐式转换:phone列是varchar,查询时传整数,如WHERE phone = 13800000000,MySQL可能对列做类型转换,索引不生效。尽量保持比较两侧类型一致,能有效避免这类问题。

覆盖索引能减少回表。如果查询只需要customer_id、status和amount,可以建立包含返回列的复合索引:

CREATE INDEX idx_orders_customer_status_amount
ON orders (customer_id, status, amount);

但不要无脑把所有列塞进索引,索引变宽会增加存储空间和写放大,实际收益需要结合查询频率判断。频繁被查询的少数字段适合覆盖,偶尔查询的宽表不建议在索引中包含过多大字段。

改写SQL,减少不必要的数据处理

数据库优化器会尝试选择代价最低的执行路径,但SQL写法会限制它的选择。最典型的是SELECT *。如果只需要订单号和时间,就不要把大字段全部查出来。返回列越多,内存占用越高,网络传输也慢,尤其存在TEXT或BLOB时更明显。列少还更容易命中覆盖索引,这是一项几乎零成本的优化。

分页深翻页是慢查询重灾区。LIMIT 100000, 10需要数据库先扫描并丢弃前10万行,数据量越大越慢。可以改成延迟关联:先只查主键,再回表取完整行。

SELECT o.id, o.order_no, o.amount, o.status
FROM orders o
JOIN (
    SELECT id
    FROM orders
    ORDER BY id
    LIMIT 100000, 10
) t ON o.id = t.id;

子查询只取主键,走索引扫描代价小,再做10次主键回表,整体速度能提升数倍。如果主键连续且排序稳定,还可以使用游标分页,比如记住上一页最后一条ID,用WHERE id > 100000 LIMIT 10,避免扫描前10万行。具体用哪种方式,要看业务是否允许跳页。

另外,能用UNION ALL就不要用UNION,因为后者会去重,引入不必要的排序和临时表。子查询和JOIN的选择也要看执行计划,有些场景NOT EXISTS比NOT IN更安全,特别是子查询结果可能包含NULL时。不要用DISTINCT掩盖数据重复问题,先搞清楚为什么重复,从源头消除。

控制事务与锁,减少等待和阻塞

慢查询不只是读得慢,写操作和事务持有锁时间过长,也会让数据库看起来整体卡顿。事务应该尽可能短,不要在开启事务后调用远程接口、发消息或等待用户输入。把无关逻辑移到事务外,只保留必要的数据库操作。批量更新时避免一次更新几十万行,可以按主键范围分批提交,比如每次处理1000行,减少锁持有时间和undo压力。

更新时如果WHERE条件命中主键或唯一索引,数据库只锁少量行;如果条件没有索引,例如UPDATE orders SET status = 'expired' WHERE create_time < '2024-01-01',可能扫描大量行并加锁,阻塞其他事务。尽量让更新条件走索引,必要时先查询出主键列表再分批更新。下面是一个带条件判断的更新,避免余额扣成负数:

START TRANSACTION;
UPDATE accounts
SET balance = balance - 100
WHERE id = 1
  AND balance >= 100;
COMMIT;

这里的balance >= 100作为行级条件,能防止负余额。类似的乐观锁写法可以在WHERE中带上版本号或原始值,减少锁冲突。隔离级别也要根据业务选择,读多写少的报表场景可以用读提交或快照隔离,不要一上来用可串行化。事务短、锁范围小,数据库的并发吞吐自然就上去了。

SQL优化不是一次性的工作,而是一个持续观察和调整的过程。每次调整后重新查看执行计划,确认扫描行数、排序操作和锁等待是否下降。索引不要过度,SQL要可读,事务要短。把这些基础点做好,很多情况下不需要升级硬件,也能获得明显可感知的性能提升。

广告位 · 文章底部(后台-模块管理替换此区域)