【问题标题】:Index probably not used correctly on simple SQL query在简单的 SQL 查询中可能没有正确使用索引
【发布时间】:2020-10-27 22:40:22
【问题描述】:

尺寸:

  • 广告系列:3k 行(200 行,campaign.is_active = 1)
  • 链接:20k 行(4k 行,links.status = 1 // 500 行,links.status = 1 AND campaign.is_active = 1)
  • 点击次数:1000 万行(50k 次创建 > '2020-10-25 00:00:00')

此查询运行 2 秒

SELECT links.id, COUNT(clicks.id)
FROM links 
INNER JOIN campaigns ON campaigns.id = links.campaign_id 
AND campaigns.is_active = 1 
LEFT JOIN clicks ON clicks.link_id = links.id
WHERE links.status = 1 
AND clicks.created > '2020-10-25 00:00:00'
GROUP BY links.id

当我删除以下行时,它只运行 0.13 秒(快 15 倍)

AND campaigns.is_active = 1

campaigns.is_active 上有一个索引。 还尝试在 2 列(campaigns.id + campaign.is_active)上设置索引,但没有帮助。

“campaigns.is_active”只包含 0 或 1。campaigns 表很小,campaigns.is_active 条件实际上减少了行数。所以它应该加快查询速度。

为什么会因为这种情况需要这么长时间?如何解决?

如果我要删除活动的 JOIN,而是将 links.campaign_id 添加到 SELECT 字段,然后在附加查询中查询每个返回的活动 ID,例如“SELECT is_active FROMcampaign WHERE id = ?" 它仍然会更快,因为这样的查询是 0.000x。根据我的经验,当 2 次查询速度更快时,这通常意味着第一次查询没有得到最大程度的优化。

解释-选择

结构

CREATE TABLE `campaigns` (
  `id` int(11) UNSIGNED NOT NULL,
  `is_active` tinyint(4) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE `clicks` (
  `id` int(11) UNSIGNED NOT NULL,
  `link_id` int(11) UNSIGNED NOT NULL,
  `created` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE `links` (
  `id` int(11) UNSIGNED NOT NULL,
  `campaign_id` int(8) UNSIGNED NOT NULL,
  `status` tinyint(4) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


ALTER TABLE `campaigns`
  ADD PRIMARY KEY (`id`),
  ADD UNIQUE KEY `id_isactive` (`id`,`is_active`),
  ADD KEY `is_active` (`is_active`)

ALTER TABLE `clicks`
  ADD PRIMARY KEY (`id`),
  ADD KEY `link_id` (`link_id`),
  ADD KEY `created` (`created`)

ALTER TABLE `links`
  ADD PRIMARY KEY (`id`),
  ADD KEY `campaign_id` (`campaign_id`),

【问题讨论】:

  • 感谢您的 cmets。添加了缺失的信息。还添加了我删除的聚合函数以简化对 stackoverflow 的查询
  • 阅读完此stackoverflow.com/tags/query-performance/infoedit 您的问题提供更多信息。而且,clicks.created 上的过滤器会将您的 LEFT JOIN 变成普通的内部 JOIN。
  • EXPLAIN 暗示数据集比您建议的要小,但也许我遗漏了一些东西。顺便说一句,我认为innodb悄悄地将pk添加到所有索引的末尾,所以(id,is_active)中的id有些多余
  • 索引一个真/假或1/0列是不值得的,因为索引的选择性会很低,mysql无法使用它!
  • @Shadow,视情况而定。如果 90% 的行是 false 并且您使用 true 搜索 10% 的行,那很好。但是,如果您假设这两个值分配得更均匀,那么您是对的,那么它就没有那么有效了。

标签: mysql sql query-performance


【解决方案1】:

这需要多长时间?

SELECT l.id,
       (SELECT COUNT(*)
        FROM clicks cl
        WHERE cl.link_id = l.id AND
              cl.created > '2020-10-25'
       )
FROM links l JOIN
     campaigns ca
     ca.id = l.campaign_id 
WHERE l.status = 1 AND ca.is_active = 1;

编辑:

嗯,order by,你可以试试:

SELECT l.id,
       (SELECT COUNT(*)
        FROM clicks cl
        WHERE cl.link_id = l.id AND
              cl.created > '2020-10-25'
       )
FROM links l 
WHERE EXISTS (SELECT 1
              FROM campaigns ca
              WHERE ca.id = l.campaign_id AND ca.is_active = 1
             )
WHERE l.status = 1 
ORDER BY l.id;

为此,您需要在links(status, id)campaigns(campaign_id, is_active) 上建立索引。

【讨论】:

  • 我在第 3 行添加了“FROM links l”,在第 10 行添加了“ON ca.id”,最后添加了“GROUP BY l.id”。它运行了 0.10 秒,但结果是错误的,计数列始终包含自 2020-10-25 以来的总点击量
  • @DeveloperJano 。 . .我确定了答案。
  • 谢谢!它运行 0.10 秒,但是当添加 ORDER BY l.id ASC 时,它再次运行 1.8 秒。这很奇怪,因为最终(分组)结果大约有 500 行,mysql 怎么会花这么多时间来订购?
  • “编辑”后的新查询再次运行 1.8 秒
  • @DeveloperJano 。 . .你有推荐的索引吗?
【解决方案2】:

问题...如果一个活动当前不活跃,你不希望它有任何输出,对吗?此外,不活动的广告系列不会有任何点击,对吗?那为什么还要检查is_active

即使我的分析是错误的,在计数完成之前忽略 is_active 可能会更快。

当它不起作用时,请不要使用LEFT。你有一个简单的JOIN

使用COUNT(*); COUNT(x) 测试 x 不为空。

SELECT  links.id, COUNT(*)
    FROM  links
    JOIN  clicks  ON clicks.link_id = links.id
    WHERE  links.status = 1
      AND  clicks.created > '2020-10-25 00:00:00'
    GROUP BY  links.id

这是多余的:

ADD UNIQUE KEY `id_isactive` (`id`,`is_active`),

因为PRIMARY KEY(id) 声明id 是一个索引并且是唯一的。

【讨论】:

    【解决方案3】:

    我不喜欢和数据库引擎优化器作斗争。

    SELECT links.id, campaigns.is_active, COUNT(clicks.id)
    FROM links 
    INNER JOIN campaigns ON campaigns.id = links.campaign_id 
    LEFT JOIN clicks ON clicks.link_id = links.id
    WHERE links.status = 1 
    AND clicks.created > '2020-10-25 00:00:00'
    GROUP BY links.id, campaigns.is_active
    HAVING campaigns.is_active = 1;
    

    第二个变种!

    -- Second Variant
    EXPLAIN
    SELECT links.id AS LinksId
          , COUNT(clicks.id) AS ClickCount
    FROM links 
    LEFT JOIN clicks 
        ON links.id = clicks.link_id
    WHERE links.status = 1
    AND clicks.created > '2020-10-25 00:00:00'
    AND links.campaign_id IN (SELECT campaign_id 
                                FROM campaigns
                                WHERE is_active = 1)
    GROUP BY links.id;
    

    第三次是魅力!由于已发布的基数而使用 CTE。

    -- Third time is the charm
    WITH ActiveCampaigns
    AS
        (SELECT *
        FROM campaigns
        WHERE is_active = 1)
    SELECT links.id, COUNT(clicks.id)
    FROM links 
    INNER JOIN ActiveCampaigns 
        ON ActiveCampaigns.id = links.campaign_id 
    LEFT JOIN clicks 
        ON clicks.link_id = links.id
    WHERE links.status = 1 
    AND clicks.created > '2020-10-25 00:00:00'
    GROUP BY links.id;
    

    【讨论】:

    • 这需要 0.14 秒,正是我想要的
    • 仍然很奇怪为什么引擎优化器会这样做,是否有任何可以理解的原因或任何方式来预测这一点?我想知道我们有多少查询,这样的优化会有所帮助,但没有人会考虑它
    • 我没有足够的数据来深入挖掘,但可能值得对链接、活动和链接运行分析表以更新统计信息。可以使用目录表查看最近更新的统计信息mysql.innodb_table_stats 和 mysql.innodb_index_stats。过时的统计数据会产生低效的计划。顺便说一句,已发布的架构缺少 SQL 查询中使用的列。
    • 当我检查时,这些表最近已经更新,分析表没有帮助。我将缺少的列添加到架构中。
    • 我现在遇到了问题,当我添加另一个与活动表相关的连接时,使用您提供的查询连接会变慢。这是有道理的,因为从活动表中查询的数据实际上使用 HAVING 比使用 WHERE 大得多。我还能做什么?
    猜你喜欢
    • 2013-12-02
    • 1970-01-01
    • 2012-07-09
    • 1970-01-01
    • 2021-07-09
    • 1970-01-01
    • 2014-07-20
    • 2021-11-25
    • 1970-01-01
    相关资源
    最近更新 更多