文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

SQL优化极简法则,还有谁不会?

2024-12-03 14:07

关注

[[373751]] 

图片来自 Pexels

查询优化是一个复杂的工程,涉及从硬件到参数配置、不同数据库的解析器、优化器实现、SQL 语句的执行顺序、索引以及统计信息的采集等,甚至应用程序和系统的整体架构。

本文介绍几个关键法则,可以帮助我们编写高效的 SQL 查询;尤其是对于初学者而言,这些法则至少可以避免我们写出性能很差的查询语句。

以下法则适用于各种关系型数据库,包括但不限于:MySQL、Oracle、SQL Server、PostgreSQL 以及 SQLite 等。

法则一:只返回需要的结果

一定要为查询语句指定 WHERE 条件,过滤掉不需要的数据行。

通常来说,OLTP 系统每次只需要从大量数据中返回很少的几条记录;指定查询条件可以帮助我们通过索引返回结果,而不是全表扫描。

绝大多数情况下使用索引时的性能更好,因为索引(B-树、B+树、B*树)执行的是二进制搜索,具有对数时间复杂度,而不是线性时间复杂度。

以下是 MySQL 聚簇索引的示意图:

 

举例来说,假设每个索引分支节点可以存储 100 个记录,100 万(1003)条记录只需要 3 层 B- 树即可完成索引。

通过索引查找数据时需要读取 3 次索引数据(每次磁盘 IO 读取整个分支节点),加上 1 次磁盘 IO 读取数据即可得到查询结果。

相反,如果采用全表扫描,需要执行的磁盘 IO 次数可能高出几个数量级。当数据量增加到 1 亿(1004)时,B- 树索引只需要再增加 1 次索引 IO 即可;而全表扫描则需要再增加几个数量级的 IO。

同理,我们应该避免使用 SELECT * FROM, 因为它表示查询表中的所有字段。

这种写法通常导致数据库需要读取更多的数据,同时网络也需要传输更多的数据,从而导致性能的下降。

法则二:确保查询使用了正确的索引

如果缺少合适的索引,即使指定了查询条件也不会通过索引查找数据。因此,我们首先需要确保创建了相应的索引。

一般来说,以下字段需要创建索引:

即使创建了合适的索引,如果 SQL 语句写的有问题,数据库也不会使用索引。

导致索引失效的常见问题包括:

执行计划(execution plan,也叫查询计划或者解释计划)是数据库执行 SQL 语句的具体步骤,例如通过索引还是全表扫描访问表中的数据,连接查询的实现方式和连接的顺序等。

如果 SQL 语句性能不够理想,我们首先应该查看它的执行计划,通过执行计划(EXPLAIN)确保查询使用了正确的索引。

法则三:尽量避免使用子查询

以 MySQL 为例,以下查询返回月薪大于部门平均月薪的员工信息:

  1. EXPLAIN ANALYZE 
  2.  SELECT emp_id, emp_name 
  3.    FROM employee e 
  4.    WHERE salary > ( 
  5.      SELECT AVG(salary) 
  6.        FROM employee 
  7.        WHERE dept_id = e.dept_id); 
  8. -> Filter: (e.salary > (select #2)) (cost=2.75 rows=25) (actual time=0.232..4.401 rows=6 loops=1) 
  9.     -> Table scan on e  (cost=2.75 rows=25) (actual time=0.099..0.190 rows=25 loops=1) 
  10.     -> Select #2 (subquery in condition; dependent) 
  11.         -> Aggregate: avg(employee.salary) (actual time=0.147..0.149 rows=1 loops=25) 
  12.             -> Index lookup on employee using idx_emp_dept (dept_id=e.dept_id) (cost=1.12 rows=5) (actual time=0.068..0.104 rows=7 loops=25) 

从执行计划可以看出,MySQL 中采用的是类似 Nested Loop Join 实现方式;子查询循环了 25 次,而实际上可以通过一次扫描计算并缓存每个部门的平均月薪。

以下语句将该子查询替换为等价的 JOIN 语句,实现了子查询的展开(Subquery Unnest):

  1. EXPLAIN ANALYZE 
  2.  SELECT e.emp_id, e.emp_name 
  3.    FROM employee e 
  4.    JOIN (SELECT dept_id, AVG(salary) AS dept_average 
  5.            FROM employee 
  6.           GROUP BY dept_id) t 
  7.      ON e.dept_id = t.dept_id 
  8.   WHERE e.salary > t.dept_average; 
  9. -> Nested loop inner join (actual time=0.722..2.354 rows=6 loops=1) 
  10.     -> Table scan on e (cost=2.75 rows=25) (actual time=0.096..0.205 rows=25 loops=1) 
  11.     -> Filter: (e.salary > t.dept_average)  (actual time=0.068..0.076 rows=0 loops=25) 
  12.         -> Index lookup on t using  (dept_id=e.dept_id) (actual time=0.011..0.015 rows=1 loops=25) 
  13.             -> Materialize (actual time=0.048..0.057 rows=1 loops=25) 
  14.                 -> Group aggregate: avg(employee.salary) (actual time=0.228..0.510 rows=5 loops=1) 
  15.                     -> Index scan on employee using idx_emp_dept (cost=2.75 rows=25) (actual time=0.181..0.348 rows=25 loops=1) 

改写之后的查询利用了物化(Materialization)技术,将子查询的结果生成一个内存临时表;然后与 employee 表进行连接。通过实际执行时间可以看出这种方式更快。

以上示例在 Oracle 和 SQL Server 中会自动执行子查询展开,两种写法效果相同;在 PostgreSQL 中与 MySQL 类似,第一个语句使用 Nested Loop Join,改写为 JOIN 之后使用 Hash Join 实现,性能更好。

另外,对于 IN 和 EXISTS 子查询也可以得出类似的结论。由于不同数据库的优化器能力有所差异,我们应该尽量避免使用子查询,考虑使用 JOIN 进行重写。

法则四:不要使用 OFFSET 实现分页

分页查询的原理就是先跳过指定的行数,再返回 Top-N 记录。分页查询的示意图如下:

 

数据库一般支持 FETCH/LIMIT 以及 OFFSET 实现 Top-N 排行榜和分页查询。当表中的数据量很大时,这种方式的分页查询可能会导致性能问题。

以 MySQL 为例:

  1. -- MySQL 
  2. SELECT * 
  3.   FROM large_table 
  4.  ORDER BY id 
  5.  LIMIT 10 OFFSET N; 

以上查询随着 OFFSET 的增加,速度会越来越慢;因为即使我们只需要返回 10 条记录,数据库仍然需要访问并且过滤掉 N(比如 1000000)行记录,即使通过索引也会涉及不必要的扫描操作。

对于以上分页查询,更好的方法是记住上一次获取到的最大 id,然后在下一次查询中作为条件传入:

  1. -- MySQL 
  2. SELECT * 
  3.   FROM large_table 
  4.  WHERE id > last_id 
  5.  ORDER BY id 
  6.  LIMIT 10; 

如果 id 字段上存在索引,这种分页查询的方式可以基本不受数据量的影响。

法则五:了解 SQL 子句的逻辑执行顺序

以下是 SQL 中各个子句的语法顺序,前面括号内的数字代表了它们的逻辑执行顺序:

  1. (6)SELECT [DISTINCT | ALL] col1, col2, agg_func(col3) AS alias 
  2. (1) FROM t1 JOIN t2 
  3. (2) ON (join_conditions) 
  4. (3) WHERE where_conditions 
  5. (4) GROUP BY col1, col2 
  6. (5)HAVING having_condition 
  7. (7) UNION [ALL
  8.    ... 
  9. (8) ORDER BY col1 ASC,col2 DESC 
  10. (9)OFFSET m ROWS FETCH NEXT num_rows ROWS ONLY

也就是说,SQL 并不是按照编写顺序先执行 SELECT,然后再执行 FROM 子句。

从逻辑上讲,SQL 语句的执行顺序如下:

注意逻辑执行顺序并不代表物理执行顺序,实际上数据库在获取表中的数据之前会使用 ON 和 WHERE 过滤条件进行优化访问。

了解 SQL 逻辑执行顺序可以帮助我们进行 SQL 优化。例如 WHERE 子句在 HAVING 子句之前执行,因此我们应该尽量使用 WHERE 进行数据过滤,避免无谓的操作;除非业务需要针对聚合函数的结果进行过滤。

除此之外,理解 SQL 的逻辑执行顺序还可以帮助我们避免一些常见的错误,例如以下语句:

  1. -- 错误示例 
  2. SELECT emp_name AS empname 
  3.   FROM employee 
  4.  WHERE empname ='张飞'

该语句的错误在于 WHERE 条件中引用了列别名;从上面的逻辑顺序可以看出,执行 WHERE 条件时还没有执行 SELECT 子句,也就没有生成字段的别名。

另外一个需要注意的操作就是 GROUP BY,例如:

  1. -- GROUP BY 错误示例 
  2. SELECT dept_id, emp_name, AVG(salary) 
  3.   FROM employee 
  4.  GROUP BY dept_id; 

由于经过 GROUP BY 处理之后结果集只保留了分组字段和聚合函数的结果,示例中的 emp_name 字段已经不存在。

从业务逻辑上来说,按照部门分组统计之后再显示某个员工的姓名没有意义。如果需要同时显示员工信息和所在部门的汇总,可以使用窗口函数。

如果使用了 GROUP BY 分组,之后的 SELECT、ORDER BY 等只能引用分组字段或者聚合函数;否则,可以引用 FROM 和 JOIN 表中的任何字段。

还有一些逻辑问题可能不会直接导致查询出错,但是会返回不正确的结果;例如外连接查询中的 ON 和 WHERE 条件。

以下是一个左外连接查询的示例:

  1. SELECT e.emp_name, d.dept_name 
  2.   FROM employee e 
  3.   LEFT JOIN department d ON (e.dept_id = d.dept_id) 
  4.  WHERE e.emp_name ='张飞'
  5. emp_name|dept_name| 
  6. --------|---------| 
  7. 张飞 |行政管理部| 
  8.  
  9. SELECT e.emp_name, d.dept_name 
  10.   FROM employee e 
  11.   LEFT JOIN department d ON (e.dept_id = d.dept_id AND e.emp_name ='张飞'); 
  12. emp_name|dept_name| 
  13. --------|---------| 
  14. 刘备 | [NULL]| 
  15. 关羽 | [NULL]| 
  16. 张飞 |行政管理部| 
  17. 诸葛亮 | [NULL]| 
  18. ... 

第一个查询在 ON 子句中指定了连接的条件,同时通过 WHERE 子句找出了“张飞”的信息。

第二个查询将所有的过滤条件都放在 ON 子句中,结果返回了所有的员工信息。

这是因为左外连接会返回左表中的全部数据,即使 ON 子句中指定了员工姓名也不会生效;而 WHERE 条件在逻辑上是对连接操作之后的结果进行过滤。

总结

SQL 优化本质上是了解优化器的的工作原理,并且为此创建合适的索引和正确的语句;同时,当优化器不够智能的时候,手动让它智能。

作者:不剪发的Tony老师

编辑:陶家龙

出处:http://002ii.cn/YknfGU

 

来源:002ii内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯