【问题标题】:Tagging schema - quick when queries run separately, slow when run in one SELECT标记模式 - 查询单独运行时快,在一个 SELECT 中运行时慢
【发布时间】:2011-12-19 15:29:54
【问题描述】:

我有一个奇怪的性能问题,该查询用于为类似 Delicious 的书签 web 应用程序创建“按标签过滤”小部件。如果运行较少的单独查询,则特定的、相对复杂的查询的执行速度要快得多(1000 到 10000 倍)。

我已在以下环境中对其进行了测试:

  • Windows XP / MySQL 5.1.37(服务器和客户端)
  • Ubuntu 11.10 / MySQL 5.1.58(服务器和客户端)

该问题并未出现在小型开发数据库中。我在生产使用过程中发现了它,在数据库中大量增加记录之后(目前在 link_tags 表中大约有 100K 行和 11K 个唯一标签)。

我使用以下数据库架构:

CREATE TABLE IF NOT EXISTS `link_tags` (
  `link_id` int(11) NOT NULL,
  `tag_id` int(11) NOT NULL,
  UNIQUE KEY `link_tag_id` (`link_id`,`tag_id`),
  KEY `tag_id` (`tag_id`),
  KEY `link_id` (`link_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

CREATE TABLE IF NOT EXISTS `tags` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `tag` varchar(255) COLLATE utf8_bin NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `tag` (`tag`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

架构很简单(另见http://www.pui.ch/phred/archives/2005/04/tags-database-schemas.html),因此不需要进一步解释。

从技术上讲,有问题的查询(如下)检索与给定标签集相关的标签(具体而言,所有附加到由指定标签集标记的链接的标签)并计算每个找到的标签和标签集的链接数。

[ORIGINAL QUERY]

SELECT COUNT(*) AS link_count, tag FROM (
    SELECT
        t.tag AS tag,
        CONCAT(lt.tag_id,':',lt.link_id) AS tag_link_hash
    FROM
        link_tags lt, tags t
    WHERE
        t.id = lt.tag_id
        AND lt.link_id IN (
            SELECT
                link_id
            FROM
                link_tags lt2, links l2
            WHERE
                l2.id = lt2.link_id
                AND l2.created_by = ?  <-- user to filter tags for
                AND lt2.tag_id IN (
                   SELECT id FROM tags t2 WHERE tag IN (?)  <-- tags set to filter by
                )
            GROUP BY
                link_id
            HAVING
                COUNT(*) = ?)  <-- number of tags in filter
    GROUP BY
        tag_link_hash) tmp
GROUP BY
    tag
ORDER BY
    link_count DESC,
    tag ASC
[Results in X minutes - up to 4 hours]

在生产数据库中(正如我提到的 - 大约 100K 链接标签和 11K 标签),查询在几分钟到几小时内运行(取决于指定标签的出现频率)。 奇怪的是,如果我把它分成几个查询,一切都会顺利:

1) 为给定的标签名称查找ids。

[REPLACEMENT QUERY 1]

SELECT id FROM tags t2 WHERE tag IN (?)

[Results in 0,0011 seconds]

2) 查找给定标签集的所有link_ids(交集!)。

[REPLACEMENT QUERY 2]

SELECT
    link_id
FROM
    link_tags lt2, links l2
WHERE
    l2.id = lt2.link_id
    AND l2.created_by = 1
    AND lt2.tag_id IN ( ? )  <-- here goes imploded result of query 1
GROUP BY
    link_id
HAVING
    COUNT(*) = ?  <-- number of tags

[Results in 0,0996 seconds]

3) 查找给定link_ids 集合的所有标签,并按链接数对标签进行分组。

[REPLACEMENT QUERY 3]

SELECT COUNT(*) AS link_count, tag FROM (
    SELECT
        t.tag AS tag,
        CONCAT(lt.tag_id,':',lt.link_id) AS tag_link_hash
    FROM
        link_tags lt, tags t
    WHERE
        t.id = lt.tag_id
        AND lt.link_id IN ( ? )  <-- here goes imploded result of query 2
    GROUP BY
        tag_link_hash) tmp
GROUP BY
    tag
ORDER BY
    link_count DESC,
    tag ASC

[Results in 0,0543 seconds]

你知道发生了什么吗? EXPLAIN 显示的大型查询计划与单独查询的总和大致相同。不同之处在于每一步处理的行数(这也很奇怪)。

您能否帮助重写原始查询、提示 MySQL 优化器以高效运行它或指出导致此行为的 MySQL 错误?

解释原始查询的结果:

id  select_type table       type    possible_keys   key         key_len ref                     rows    Extra
1   PRIMARY     <derived2>  ALL     N8LL            N8LL        N8LL    N8LL                    32      Using temporary; Using filesort
2   DERIVED     lt          index   tag_id          link_tag_id 8       N8LL                    78162   Using where; Using index; Using temporary; Using filesort
2   DERIVED     t           eq_ref  PRIMARY         PRIMARY     4       lstack_prod.lt.tag_id   1
3   DEPENDENT   t2          range   PRIMARY,tag     tag         767     N8LL                    2       Using where; Using temporary; Using filesort
    SUBQUERY
3   DEPENDENT   lt2         ref     link_tag_id,    tag_id      4       lstack_prod.t2.id       7
    SUBQUERY                        tag_id,link_id
3   DEPENDENT   l2          eq_ref  PRIMARY,        PRIMARY     4       lstack_prod.lt2.link_id 1       Using where
    SUBQUERY                        created_by

【问题讨论】:

  • 为了完整起见,请同时发布 EXPLAIN 输出。

标签: mysql performance optimization subquery tagging


【解决方案1】:

WHERE IN (select values from table) 在 MySQL 中效率极低,并且会一直触发全表扫描和文件排序。通常,您应该将这些替换为 INNER JOIN。

我认为这应该会有所帮助,但我没有尝试重新创建您的数据库,也没有运行此查询,因此可能存在拼写错误。

SELECT COUNT(*) AS link_count, tag FROM (
    SELECT
        t.tag AS tag,
        CONCAT(lt.tag_id,':',lt.link_id) AS tag_link_hash
    FROM
        link_tags lt
    JOIN tags t on t.id = lt.tag_id
    JOIN (SELECT
                link_id
            FROM
                link_tags lt2
            JOIN links l2 on l2.id = lt2.link_id
            JOIN tags t2 on t2.id = lt2.tag_id                
            WHERE
                AND l2.created_by = ?  <-- user to filter tags for
                AND t2.tag IN (?)  <-- tags set to filter by
            GROUP BY
                link_id
            HAVING
                COUNT(*) = ?) as eligible_links on eligible_links.link_id = lt.link_id
    GROUP BY
        tag_link_hash) tmp
GROUP BY
    tag
ORDER BY
    link_count DESC,
    tag ASC

但是,解释计划会很有帮助。

【讨论】:

  • 我一直在努力将 WHERE IN 替换为 JOINS,但未能达到所需的结果。子查询 2 很难与其余部分合并。
  • 我将其提取为 REPLACEMENT QUERY 2
  • 我已经按照你的要求添加了 EXPLAIN 结果
  • 我不明白 SubQuery 2 中的 count(*) 是什么
  • 从“IN (?)”部分选择only那些有full标签列表的链接。从技术上讲 - 仅从查询结果中获取 X 次出现的那些 link_id,其中 X 是“IN (?)”部分中的标签数。例如:假设我们正在寻找标记为“mysql”和“性能调整”的链接。省略此 COUNT(*) 将导致返回标记为“mysql”或“性能调整”的链接,这不是我们的目标 - 查询所服务的 UI 功能是在标记之后缩小选择标记。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-27
相关资源
最近更新 更多