MySQL - 其他表中的平均最新列
Posted
技术标签:
【中文标题】MySQL - 其他表中的平均最新列【英文标题】:MySQL - Average most recent columns in other table 【发布时间】:2012-06-21 22:22:04 【问题描述】:我有两个表:“servers”和“stats”
servers 有一个名为“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
就像我说的,我想获取每台服务器的 24 行最近的行,而不是总共 24 行。
【问题讨论】:
我相信this是你表的表结构。对吗? 【参考方案1】:感谢您的精彩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
这将给出每组 24 个最新实体的平均值
编辑2
@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
【讨论】:
我在这里遇到语法错误:'select servers.*, IFNULL(AVG(stats.votes), 0) AS avgvotes from servers lef' at line 2 @DrAgonmoray,在alter table add...
行和set @num...
行之后放置一个;
,因为它们是与实际查询不同的命令。
现在代码可以工作了,但是它似乎给了我每台服务器所有记录的平均值,而不是最后的 24 条。我使用几个不同的服务器对此进行了测试。【参考方案2】:
这是另一种方法。
此查询将遇到与此处返回正确结果的其他查询相同的性能问题,因为此查询的执行计划将需要对 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 的行,并计算每个服务器“最新”24 行的 votes
列的平均值。 (内联视图别名为s
。)
作为最后一步,我们将其连接到服务器表,以返回请求的结果集。
注意:
对于stats
表中的大量行,此查询的执行计划将非常昂贵。
为了提高性能,我们可以采取多种方法。
最简单的可能是在查询中包含一个谓词,该谓词从 stats
表中排除大量行(例如,time
值超过 2 天或超过 2 周的行)。这将显着减少需要排序的行数,以确定“最新”的 24 行。
此外,对于stats(server,time)
上的索引,MySQL 也有可能对索引进行相对有效的“反向扫描”,从而避免排序操作。
我们还可以考虑在(server,"reverse_time")
上的统计表上实施索引。由于 MySQL 尚不支持降序索引,因此实现实际上将是派生的 rtime
值上的常规(升序)索引(一个“反向时间”表达式,对于 time
的降序值(例如, -1*UNIX_TIMESTAMP(my_timestamp)
或 -1*TIMESTAMPDIFF('1970-01-01',my_datetime)
。
另一种提高性能的方法是为每台服务器保留一个包含最近 24 行的影子表。如果我们可以保证不会从stats
表中删除“最新行”,那将是最容易实现的。我们可以使用触发器维护该表。基本上,每当向stats
表中插入一行时,我们检查新行上的time
是否晚于影子表中为服务器存储的最早time
,如果是,我们替换最早的新行在影子表中的行,确保在每个服务器的影子表中保留不超过 24 行。
另外一种方法是编写一个过程或函数来获得结果。此处的方法是遍历每台服务器,并针对统计表运行单独的查询以获得最新 24 行的平均 votes
,并将所有这些结果收集在一起。 (这种方法实际上更像是一种解决方法,可以避免对巨大的临时集进行排序,只是为了能够返回结果集,而不一定要使结果集的返回速度非常快。)
在 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,或者如果我们排除 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 避免了文件排序操作,因此它必须使用“反向索引扫描”来按顺序返回行。如果没有覆盖索引和“(server,time), 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 最大值为
【讨论】:
抱歉回复晚了。此查询有效,并且比以前的答案运行得更快。我也非常感谢您的详细解释和许多提高速度的解决方案。目前有 40,000 行,但有可能增加到数百万行。我现在将使用索引 (stats(server,time)
),如果性能显着下降,我可能会实施您的影子表建议。非常非常感谢!
stats(server,time,votes)
上的覆盖索引会更好地提高性能。我在我的答案中添加了一个附录,另一个查询可能更快。它有一个限制(如其所写),服务器的统计信息表中至少需要 24 行才能返回平均值。【参考方案3】:
试试这个解决方案,在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
【讨论】:
由于某种原因,这个查询非常慢。我执行它并让它静置几分钟,但它没有完成。我不需要极速,但这太长了。 @DrAgonmoray 好的,我明白了。我会尝试更好的解决方案。你的索引结构是什么样的?您是否在time
字段上设置了索引?
不,我没有在时间字段上设置索引,但是我可以根据解决方案的需要添加/删除索引。目前没有为统计数据定义索引。
@DrAgonmoray,您能否发布EXPLAIN
的输出以供我查询?另外,出于好奇,我们在stats
中处理了多少条记录?以上是关于MySQL - 其他表中的平均最新列的主要内容,如果未能解决你的问题,请参考以下文章