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

时间:2012-06-19 19:56:36

标签: mysql join greatest-n-per-group

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

服务器有一个名为“id”的列,可自动递增。 stats有一个名为“server”的列,对应于服务器表中的一行,一个名为“time”的列表示添加的时间,还有一个名为“votes”的列,我想得到它的平均值。 / p>

我想获取所有服务器(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

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

3 个答案:

答案 0 :(得分:2)

感谢您出色的 post

alter table add index(server, time)
 set @num:=0, @server:='';
select servers.*, IFNULL(AVG(stats.votes), 0) AS avgvotes
from servers left outer join (
select server, 
       time,votes, 
       @num := if(@server = server, @num + 1, 1) as row_number, 
       @server:= server as dummy 
from stats force index(server) 
group by server, time 
having row_number < 25) as stats 
on servers.id = stats.server
group by servers.id

编辑1

我刚注意到上面的查询给出了每组最早的24条记录。

 set @num:=0, @server:='';
select servers.*, IFNULL(AVG(stats.votes), 0) AS avgvotes
from servers left outer join (
select server, 
       time,votes, 
       @num := if(@server = server, @num + 1, 1) as row_number, 
       @server:= server as dummy 
from (select * from stats order by server, time desc)  as t
group by server, time 
having row_number < 25) as stats 
on servers.id = stats.server
group by servers.id

将给出每组最新实体的平均值

EDIT2

@DrAgonmoray 您可以先尝试内部查询部分,看看它是否返回每个组的最新24条记录。在我的mysql 5.5中,它可以正常工作。

select server, 
       time,votes, 
       @num := if(@server = server, @num + 1, 1) as row_number, 
       @server:= server as dummy 
from (select * from stats order by server, time desc)  as t
group by server, time 
having row_number < 25

答案 1 :(得分:1)

这是另一种方法。

此查询将遇到与此处返回正确结果的其他查询相同的性能问题,因为此查询的执行计划将要求对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。)

使用该结果集,我们使用rownumber&gt;过滤掉所有行。 24,我们计算&#34;最新&#34;的votes列的平均值。每个服务器24行。 (内联视图别名为s。)

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


注意:

对于stats表中的大量行,此查询的执行计划将为COSTLY。

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

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

此外,对于stats(server,time)上的索引,MySQL也可以进行相对有效的反向扫描&#34;在索引上,避免排序操作。

我们还可以考虑在(server,"reverse_time")上的统计表上实施索引。由于MySQL还不支持降序索引,因此实现实际上是派生rtime值的常规(升序)索引(&#34;反向时间&#34;表达式,以降序递增)值time(例如,-1*UNIX_TIMESTAMP(my_timestamp)-1*TIMESTAMPDIFF('1970-01-01',my_datetime)

另一种提高性能的方法是保留一个包含每个服务器最近24行的影子表。如果我们能保证&#34;最新的行&#34;那么这将是最简单的实施。不会从stats表中删除。我们可以用触发器维护该表。基本上,只要在stats表中插入一行,我们就会检查新行上的time是否晚于影子表中为服务器存储的最早time,如果它是的,我们用新行替换影子表中最早的行,确保每个服务器的影子表中不超过24行。

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

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


附录

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

如果要求返回平均票数&#39;对于服务器如果统计信息表中至少有24个关联行,那么我们可以进行更有效的查询,即使它有点混乱。 (嵌套的IF()函数中的大多数混乱是处理NULL值,这些值不会包含在平均值中。如果我们保证votes不是NULL,或者保证{NULL}不是NULL,那么它可能会更加混乱。如果我们排除votes为NULL的任何行。)

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避免了文件输出操作,所以它必须使用&#34;反向索引扫描&#34;按顺序返回行。缺少覆盖索引和索引(服务器,时间), MySQL used the index if I included an index hint, with the FORCE INDEX FOR ORDER BY(stats_ix1)`提示,MySQL也避免了文件排序。 (但由于我的表格少于100行,我不认为MySQL非常重视避免文件操作。)

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

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

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

要返回统计数据中至少有24行的平均值,这实际上是识别具有最大值num的行(对于每个服务器)&lt; = 24

答案 2 :(得分:0)

尝试此解决方案,将INNER JOIN子选择中的 top-n-per-group 技术记入Bill Karwin及其关于here的帖子。

SELECT 
    a.*,
    AVG(b.votes) AS avgvotes
FROM
    servers a
INNER JOIN
    (
        SELECT 
            aa.server, 
            aa.votes
        FROM 
            stats aa
        LEFT JOIN stats bb ON 
            aa.server = bb.server AND
            aa.time < bb.time
        GROUP BY
            aa.time
        HAVING
            COUNT(*) < 24
    ) b ON a.id = b.server
GROUP BY
    a.id