翻译自https://mode.com/sql-tutorial/sql-performance-tuning
从这儿开始吗?这篇是使用SQL进行数据分析教程的一部分,查看教程开始。
子查询那一课让我们意识到可以通过运行更快到语句得到同样到结果集。在这节课中,你会学习识别查询优化点,以及如何优化。
查询时间背后的理论
数据库也是一个运行在计算机上的软件,像所有的软件一样,运行速度囿于相同的“天花板”——它的硬件所能处理的最大信息量也就是它所能处理的最大信息量。使一个查询运行更快的方法就是减少软件(也即硬件)所必须处理的计算的数量。要减少必须的计算量你需要理解SQL是怎样进行计算的。首先,让我们说一些影响计算的数量进而影响查询时间的表面因素:
- 表大小:如果你的查询设计一张或多张有百万行或者更多行的表,那可能影响性能。
- 合并:如果你合并两张表,合并后使结果集的行数大大增加,那么你的查询可能就会变慢。在子查询课程中就有一个这样的例子。
- 聚合:聚合多行去产生一个结果比简单的获得这些行需要更多的计算。
查询时间也依赖于一些跟数据库本身相关的东西,这些东西是不可控的:
- 其他用户运行的查询:数据库中并发查询越多,在给定的时间内数据库所要处理的就越多,所有的查询也就会运行的越慢。尤其别人在运行满足上面一些情况的需要耗费大量资源的查询,查询速度会特别差。
- 数据库软件和优化:这也许是你不能控制的,但是如果你熟悉你在用的软件系统,你可以最大限度的利用它让你的查询更有效率。
现在,让我们忽略你不能控制的,关注你可以控制的。
减少表的行数
筛选出你仅需要的数据可以大大提高查询的速度。如何筛选数据完全取决于你要解决的问题。举例,如果有时间字段数据,限制一个小的时间窗口可以使你的查询运行快的多:
SELECT *
FROM benn.sample_event_table
WHERE event_date >= ‘2014-03-01‘ AND event_date < ‘2014-04-01‘
记住你可以在数据子集上先完成探索性分析,然后去掉限制条件,在整个数据集上运行,以完成最终的查询。最终的查询可能要运行很长一段时间,但是至少你可以很快的运行中间的步骤。
这就是为什么Mode默认添加Limit子句——100行足够你分析决定下一步怎样进行的了,而且返回结果速度快。
当LIMIT跟聚合函数一起使用时,聚合函数先执行完成,然后结果集被限制返回指定的行数,效果跟上面的例子不一样,如果这样的话使用LIMIT将没有任何意义。所有如果你像下面这样,使用聚合函数得到一行结果时,LIMIT 100没有任何作用,因此查询速度也不会加快。
SELECT COUNT(*) FROM benn.sample_event_table LIMIT 100
如果你想要在执行count函数前限制数据集(以加速查询),试着在一个子查询中进行限制:
SELECT COUNT(*) FROM ( SELECT * FROM benn.sample_event_table LIMIT 100 ) sub
注:这样使用LIMIT将会彻底改变你的结果,所以你应该用它去测试你的查询逻辑,要得到真正的结果不要这样用。
一般的,当使用子查询时,你应该确认在限制数据量时,要在最开始执行的语句中去限制。意思就是将LIMIT放在子查询中,而不是外部查询。再一次强调,这只是为了使查询速度,这样你就可以先测试逻辑,而不是为了获得最终的结果。
使联合查询简单化
在某种程度上,这是前一条建议的扩展。就像要在先执行的语句中限制数据量一样,在合并之前限制表的大小会更好。看下面的例子,在这个例子中,将大学运动队的信息表和运动员表用大学名字字段进行联合:
SELECT teams.conference AS conference, players.school_name, COUNT(1) AS players FROM benn.college_football_players players JOIN benn.college_football_teams teams ON teams.school_name = players.school_name GROUP BY 1,2
在benn.college_football_players表中有26,298行。那就意味着对于另一张表的每一行,26,298行都要进行计算比较进行匹配。但是如果事先对benn.college_football_players这张表进行聚合,你可以减少需要匹配的行数。首先,让我们看一下这个聚合操作:
SELECT players.school_name,
COUNT(*) AS players
FROM benn.college_football_players players GROUP BY 1
上面的查询返回252个结果。所以将上面的查询放入一个子查询中,然后在外层查询再进行匹配会大大减少匹配的消耗:
SELECT teams.conference,
sub.*
FROM ( SELECT players.school_name, COUNT(*) AS players FROM benn.college_football_players players GROUP BY 1 ) sub JOIN benn.college_football_teams teams ON teams.school_name = sub.school_name
在这个案例中,你不会发觉有很大的不同,因为30,000行对于数据库处理起来并不是很难。但是如果是成百上千万,甚至更多行时,在匹配前进行聚合操作后,你会看到一个显著的提升。当你这样运用子查询时,确保你的查询逻辑上的合理性——先考虑工作的准确性再考虑运行速度。
EXPLAIN
你可以在任何(有效)的查询前面加上EXPLAIN,可以得到查询所消耗时间的一个估计值。它不是绝对准确的,但它是一个有用的工具。试着运行下面的语句:
EXPLAIN SELECT * FROM benn.sample_event_table WHERE event_date >= ‘2014-03-01‘ AND event_date < ‘2014-04-01‘ LIMIT 100
你会得到这样的输出。它叫做查询计划,它展示了你的查询中执行的先后顺序(用的其他的表,语句一样):
在列表底下的一条是最先被执行的。所以,上面的计划展示出限制日期范围的WHERE子句将会最先执行。然后,数据库将会扫描600行(这是个近似的数字)。你可以看到在行数旁边的消耗——越大的数字意味着更长的运行时间。这些只是参考,而不是绝对准确的结果。这是EXPLAIN的正确打开方式:运行EXPLAIN,然后修改耗费昂贵的步骤,再次运行EXPLAIN,观察消耗是否减少了。最后,LIMIT语句最后被执行,而且消耗非常小。
更多的细节参考Postgres Documentation。