MySQL - 其他表中最近的平均列

fru*_*cup 6 mysql join greatest-n-per-group

我有两个表:"服务器"和"统计数据"

服务器有一个名为"id"的列,可以自动递增.stats有一个名为"server"的列,对应于servers表中的一行,一个名为"time"的列表示它被添加的时间,还有一个名为"votes"的列,我希望得到它的平均值.

我想获取所有服务器(SELECT * FROM servers)以及与每个服务器对应的24个最新行的平均投票.我相信这是一个"每组最大的"问题.

这是我尝试做的,但它总共给了我24行,而不是每组24行:

SELECT servers.*,
       IFNULL(AVG(stats.votes), 0) AS avgvotes
FROM servers
LEFT OUTER JOIN
  (SELECT server,
          votes
   FROM stats
   GROUP BY server
   ORDER BY time DESC LIMIT 24) AS stats ON servers.id = stats.server
GROUP BY servers.id
Run Code Online (Sandbox Code Playgroud)

就像我说的,我想为每个服务器获取最近24行,而不是最近24行.

spe*_*593 1

这是另一种方法。

该查询将遇到与返回正确结果的其他查询相同的性能问题,因为该查询的执行计划将需要对统计表中的每一行进行排序操作。由于时间列上没有谓词(限制),因此将考虑统计表中的每一行。对于一个非常大的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
Run Code Online (Sandbox Code Playgroud)

此查询的作用是按服务器并按时间列的降序对统计表进行排序。(内联视图别名为u。)

对于排序后的结果集,我们为每个服务器的每一行分配行号 1、2、3 等。(内联视图别名为t。)

使用该结果集,我们过滤掉行号 > 24 的所有行,并计算votes每个服务器的“最新”24 行的列平均值。(内联视图别名为s。)

作为最后一步,我们将其连接到服务器表,以返回请求的结果集。


笔记:

对于表中的大量行,此查询的执行计划将是昂贵的stats

为了提高性能,我们可以采取多种方法。

最简单的可能是在查询中包含一个谓词,排除表中的大量行stats(例如,time值超过 2 天或超过 2 周的行)。这将显着减少需要排序的行数,以确定“最新”的 24 行。

另外,对于 上的索引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 行。

并且,另一种方法是编写获取结果的过程或函数。这里的方法是循环遍历每个服务器,并对统计表运行单独的查询以获取votes最新 24 行的平均值,并将所有这些结果收集在一起。(这种方法实际上可能更像是一种解决方法,以避免对巨大的临时集进行排序,只是为了返回结果集,不一定使结果集的返回速度非常快。)

在大型表上执行此类查询的性能的底线是限制查询所考虑的行数并避免对大型集进行排序操作。这就是我们如何执行这样的查询。


附录

为了获得“反向索引扫描”操作(使用stats不使用文件排序操作的索引排序来获取行),我必须在 ORDER BY 子句中的两个表达式上指定 DESCENDING。上面的查询之前有ORDER BY server ASC, time DESC,并且 MySQL 总是想要进行文件排序,甚至指定提示FORCE INDEX FOR ORDER BY (stats_ix1)

如果要求仅当统计表中至少有 24 个关联行时才返回服务器的“平均投票” ,那么我们可以进行更有效的查询,即使它有点混乱。(嵌套 IF() 函数中的大部分混乱是处理 NULL 值,这些值不包含在平均值中。如果我们保证不为votesNULL,或者如果我们排除任何行,那么混乱就会少得多其中votesNULL。)

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
Run Code Online (Sandbox Code Playgroud)

通过覆盖索引stats(server,time,votes),EXPLAIN 显示 MySQL 避免了文件排序操作,因此它必须使用“反向索引扫描”来按顺序返回行。由于没有覆盖索引和“(server,time) , MySQL used the index if I included an index hint, with theFORCE INDEX FOR ORDER BY (stats_ix1)”提示上的索引,MySQL 也避免了文件排序。(但由于我的表少于 100 行,我认为 MySQL 并没有太强调避免文件排序操作。)

时间、投票和 avg_sofar 表达式被注释掉(在内联视图中别名为t);它们不是必需的,但用于调试。

按照该查询的方式,每个服务器至少需要 24 行统计数据,才能返回平均值。(这可能是可以接受的。)但我在想,一般来说,我们可以返回一个运行总计、到目前为止总计 (tot) 和一个运行计数 (cnt)。

(如果我们将 替换WHERE t.num = 24WHERE t.num <= 24,我们可以看到运行平均值。)

要返回统计数据中至少没有 24 行的平均值,实际上需要识别 num 最大值 <= 24 的行(对于每个服务器)。