问题描述
我有一个“游戏”表,其中一些游戏(但不是全部)按广告系列 ID 分组到“广告系列”中。
我正在尝试编写一个 sql 查询,该查询将获取包含有关游戏的各种信息的数据集,但特别是:如果给定的游戏是广告系列的一部分,则该广告系列中总共有多少游戏(我有这个工作) 以及战役中该游戏的索引(例如,战役中最早的游戏是索引“1”,下一个是“2”,依此类推)。
我已经做到了这一点,但执行计划看起来很糟糕,而且显而易见的解决方法也行不通,但我超前了。
g1.`id` AS `game_id`,(SELECT
COUNT(*)
FROM `games` g3
WHERE g3.`campaign` = g1.`campaign`
) AS `campaign_length`,ca2.`ri` AS `campaign_index`,ca1.`id` AS `campaign_id`,ca1.`name` AS `campaign_name`
FROM `games` g1
LEFT JOIN `campaigns` ca1 ON ca1.`id` = g1.`campaign`
LEFT JOIN (
SELECT
g4.`id` AS `id`,ROW_NUMBER() OVER (
PARTITION BY g4.`campaign`
ORDER BY g4.`start` ASC) AS `ri`
FROM `games` g4
) AS ca2 ON ca2.`id` = g1.`id`
WHERE g1.`end` > CURRENT_TIMESTAMP()
AND g1.`gamemaster` = 25
ORDER BY g1.`start` ASC
;
这个版本的问题是,对于表 g4,执行计划列出了一个全表扫描——目前这很好,因为只有几百条记录,但长期来看性能会很糟糕,尤其是这个查询(或非常相似的)将在我网站的许多不同页面上执行。我相信这是因为 ROW_NUMBER() 函数需要对所有行进行编号,然后 LEFT JOIN 的 ON 语句才能将它们过滤到我真正需要的行。
我试图无济于事的显而易见的解决方案是添加
WHERE g4.`campaign` = g1.`campaign`
之后的 FROM `games` g4
;
这样 ROW_NUMBER() 只需要对那些有机会在数据集中返回的记录进行编号。但是这不起作用,因为 g1.`campaign`
不在范围内。
我可以执行 WHERE g4.`campaign` IS NOT NULL
,这至少可以将执行计划缩减为条件索引而不是全表扫描,但随着广告系列中游戏数量的增加,它仍然无法很好地扩展。>
我知道由于范围问题,我的“显而易见的解决方案”将不起作用,但是有人建议我如何在没有糟糕的执行计划的情况下实现我想要做的事情吗?
解决方法
根据您的评论,campaign_index
必须在应用WHERE
子句之前计算。这意味着 campaign_index
的计算将总是需要全表扫描,因为 WHERE
子句不能 减少正在计算的行。
但是,您可以使用窗口函数而不是自连接和相关子查询...
WITH
games AS
(
SELECT
*,COUNT(*)
OVER (
PARTITION BY `campaign`
)
AS `campaign_length`,ROW_NUMBER()
OVER (
PARTITION BY `campaign`
ORDER BY `start`
)
AS `campaign_index`
FROM
games
)
SELECT
games.*,campaigns.`name` AS `campaign_name`
FROM
games
LEFT JOIN
campaigns
ON campaigns.`id` = games.`campaign`
WHERE
games.`end` > CURRENT_TIMESTAMP()
AND games.`gamemaster` = 25
ORDER BY
games.`start`
;
,
将该表复制到具有新 AUTO_INCREMENT
id 的新表中。这将快速添加行号。
CREATE TABLE new_list (
row_num INT AUTO_INCREMENT NOT NULL,INDEX(row_num) ) ENGINE=InnoDB
SELECT ... FROM ...
ORDER BY ... -- this will do the sorting before numbering