这是另一种方法。
此查询将遇到与此处返回正确结果的其他查询相同的性能问题,因为此查询的执行计划将需要对 stats 表中的每一行执行 SORT 操作。由于时间列上没有谓词(限制),因此将考虑统计表中的每一行。对于一个非常大的stats
表,这将在它可怕的死亡之前耗尽所有可用的临时空间。(下面有更多关于性能的说明。)
SELECT r.*
, IFNULL(s.avg_votes,0)
FROM servers r
LEFT
JOIN ( SELECT t.server
, AVG(t.votes) AS avg_votes
FROM ( SELECT CASE WHEN u.server = @last_server
THEN @i := @i + 1
ELSE @i := 1
END AS i
, @last_server := u.server AS `server`
, u.votes AS votes
FROM (SELECT @i := 0, @last_server := NULL) i
JOIN ( SELECT v.server, v.votes
FROM stats v
ORDER BY v.server DESC, v.time DESC
) u
) t
WHERE t.i <= 24
GROUP BY t.server
) s
ON s.server = r.id
该查询所做的是按服务器和按时间列的降序对统计表进行排序。(内联视图别名为u
。)
使用排序后的结果集,我们为每个服务器的每一行分配一个行号 1、2、3 等。(内联视图别名为t
。)
使用该结果集,我们过滤掉所有行号 > 24 的行,并计算votes
每个服务器的“最新”24 行的列的平均值。(内联视图别名为s
。)
作为最后一步,我们将它加入到服务器表中,以返回请求的结果集。
笔记:
对于表中的大量行,此查询的执行计划将非常昂贵stats
。
为了提高性能,我们可以采取几种方法。
最简单的可能是在查询中包含一个谓词,从表中排除大量行stats
(例如,time
值超过 2 天或超过 2 周的行)。这将显着减少需要排序的行数,以确定“最新”的 24 行。
此外,使用 on 索引stats(server,time)
,MySQL 也有可能对索引进行相对有效的“反向扫描”,从而避免排序操作。
我们还可以考虑在 上的 stats 表上实现索引(server,"reverse_time")
。由于 MySQL 尚不支持降序索引,因此实现实际上将是派生rtime
值上的常规(升序)索引time
(对于 (例如,-1*UNIX_TIMESTAMP(my_timestamp)
或-1*TIMESTAMPDIFF('1970-01-01',my_datetime)
.
另一种提高性能的方法是为每台服务器保留一个包含最近 24 行的影子表。如果我们可以保证不会从stats
表中删除“最新行”,那将是最简单的实现。我们可以使用触发器维护该表。基本上,每当向表中插入一行时stats
,我们检查time
新行上的 是否晚于time
影子表中为服务器存储的最早行,如果是,我们将影子表中最早的行替换为新行,确保在每个服务器的影子表中保留不超过 24 行。
而且,另一种方法是编写一个获得结果的过程或函数。此处的方法是遍历每台服务器,并针对 stats 表运行单独的查询以获得votes
最近 24 行的平均值,然后将所有这些结果收集在一起。(这种方法实际上更像是一种解决方法,以避免对巨大的临时集进行排序,只是为了使结果集能够被返回,而不一定使结果集的返回速度非常快。)
在 LARGE 表上执行此类查询的底线是限制查询考虑的行数并避免对大集合进行排序操作。这就是我们如何执行这样的查询。
附录
要获得“反向索引扫描”操作(stats
使用不使用文件排序操作的索引从排序中获取行),我必须在 ORDER BY 子句中的两个表达式上指定 DESCENDING。上面的查询以前有ORDER BY server ASC, time DESC
,并且 MySQL 总是想做一个文件排序,甚至指定FORCE INDEX FOR ORDER BY (stats_ix1)
提示。
如果要求仅在 stats 表中至少有 24 个关联行时才返回服务器的“平均票数” ,那么我们可以进行更有效的查询,即使它有点混乱。(嵌套 IF() 函数中的大部分混乱是处理 NULL 值,这些值不包含在平均值中。如果我们有一个votes
不为 NULL 的保证,或者如果我们排除任何行,它可以少得多NULL在哪里votes
。)
SELECT r.*
, IFNULL(s.avg_votes,0)
FROM servers r
LEFT
JOIN ( SELECT t.server
, t.tot/NULLIF(t.cnt,0) AS avg_votes
FROM ( SELECT IF(v.server = @last_server, @num := @num + 1, @num := 1) AS num
, @cnt := IF(v.server = @last_server,IF(@num <= 24, @cnt := @cnt + IF(v.votes IS NULL,0,1),@cnt := 0),@cnt := IF(v.votes IS NULL,0,1)) AS cnt
, @tot := IF(v.server = @last_server,IF(@num <= 24, @tot := @tot + IFNULL(v.votes,0) ,@tot := 0),@tot := IFNULL(v.votes,0) ) AS tot
, @last_server := v.server AS SERVER
-- , v.time
-- , v.votes
-- , @tot/NULLIF(@cnt,0) AS avg_sofar
FROM (SELECT @last_server := NULL, @num:= 0, @cnt := 0, @tot := 0) u
JOIN stats v FORCE INDEX FOR ORDER BY (stats_ix1)
ORDER BY v.server DESC, v.time DESC
) t
WHERE t.num = 24
) s
ON s.server = r.id
使用覆盖索引时stats(server,time,votes)
,EXPLAIN 显示 MySQL 避免了文件排序操作,因此它必须使用“反向索引扫描”来按顺序返回行。如果没有覆盖索引和“(server,time) , MySQL used the index if I included an index hint, with the
FORCE INDEX FOR ORDER BY (stats_ix1)”提示上的索引,MySQL 也避免了文件排序。(但由于我的表少于 100 行,我不认为 MySQL 非常重视避免文件排序操作。)
time、votes 和 avg_sofar 表达式被注释掉(在别名为 的内联视图中t
);它们不是必需的,但它们用于调试。
查询的方式是,每个服务器至少需要 24 行统计数据,才能返回平均值。(这可能是可以接受的。)但我在想,一般来说,我们可以返回一个运行总计、到目前为止的总计 (tot) 和一个运行计数 (cnt)。
(如果我们将 替换为WHERE t.num = 24
, WHERE t.num <= 24
我们可以看到运行平均值。)
要返回统计信息中没有至少 24 行的平均值,这实际上是确定 num 最大值 <= 24 的行(对于每个服务器)。