优化 30 000+ 行表上的 LEFT JOIN

Posted

技术标签:

【中文标题】优化 30 000+ 行表上的 LEFT JOIN【英文标题】:Optimize LEFT JOIN on table with 30 000+ rows 【发布时间】:2010-07-18 09:12:36 【问题描述】:

我有一个网站,访问者可以在其中离开 cmets。我想添加回答 cmets 的能力(即嵌套 cmets)。

起初这个查询很快,但在我用现有的 cmets(大约 30000 个)填充表后,一个简单的查询如下:

SELECT c.id, c2.id
  FROM (SELECT id
         FROM swb_comments
         WHERE pageId = 1411
         ORDER BY id DESC
         LIMIT 10) AS c
  LEFT JOIN swb_comments AS c2 ON c.id = c2.parentId

超过 2 秒,没有 childComments(!)。

如何优化这样的查询?可能的解决方案是http://www.ferdychristant.com/blog//articles/DOMM-7QJPM7(滚动到“正确完成的平面表模型”),但这使得分页相当困难(如何在 1 个查询中限制为 10 个父 cmets?)

该表有 3 个索引,id、pageId 和 ParentId。

提前致谢!

编辑:

添加了表定义。这是与上述 SELECT 查询有些不同的完整定义(即 pageId 而不是 numberId 以避免混淆)

CREATE TABLE `swb_comments` (
    `id` mediumint(9) NOT NULL auto_increment,
    `userId` mediumint(9) unsigned NOT NULL default '0',
    `numberId` mediumint(9) unsigned default NULL,
    `orgId` mediumint(9) unsigned default NULL,
    `author` varchar(100) default NULL,
    `email` varchar(255) NOT NULL,
    `message` text NOT NULL,
    `IP` varchar(40) NOT NULL,
    `timestamp` varchar(25) NOT NULL,
    `editedTimestamp` varchar(25) default NULL COMMENT 'last edited timestamp',
    `status` varchar(20) NOT NULL default 'publish',
    `parentId` mediumint(9) unsigned NOT NULL default '0',
    `locale` varchar(10) NOT NULL,
    PRIMARY KEY  (`id`),
    KEY `userId` (`userId`),
    KEY `numberId` (`numberId`),
    KEY `orgId` (`orgId`),
    KEY `parentId` (`parentId`)
  ) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=34748 ;

【问题讨论】:

您打算无限嵌套吗? (即,您可以再次回复答案?)或者只有一级深度最大值? EXPLAIN 有什么要说的? SELECT id FROM swb_cmets WHERE pageId = 1411 LIMIT 10 也慢吗?您从父 ID 加入获得了多少点击? 如果子选择中没有 ORDER BY,您实际上是在随机选择 cmets。这是本意吗? @Daniel 我只想要一个级别(Facebook 风格)。 @Sam SELECT id FROM swb_cmets WHERE numberId =1411 ORDER BY id DESC LIMIT 10 只有 0.0024 秒,所以没有。目前我只从旧表中导入了所有行,所以根本没有子 cmets。 @Marcelo 感谢您的订购,被切成 0.2 秒,这很棒。我更新了问题以包含它。 【参考方案1】:

问题是,如果 mysql 需要处理派生查询的结果,它就无法应用索引(这就是为什么在 possible_keys 列中有 NULL 的原因)。所以我建议过滤掉你需要的十个cmets:

SELECT * FROM swb_comments WHERE pageId = 1411 ORDER BY id DESC LIMIT 10

然后发送单独的请求以获取每个评论 ID 的答案:

SELECT * FROM swb_comments WHERE parentId IN ($commentId1, $commentId2, ..., $commentId10)

在这种情况下,数据库引擎将能够有效地应用 pageId 和 parentId 索引。

【讨论】:

谢谢!我会试试这个! 通常情况下,我会对此投反对票,但是在使用 MySQL 优化器时遇到了大多数其他供应商没有的无数奇怪的事情,我只需要点头表示同意。【参考方案2】:

如果 Fedorenko 先生是正确的并且子查询导致了优化器的困难,你能不能试试...

SELECT c.id, c2.id
    FROM swb_comments c LEFT JOIN swb_comments c2 ON c.id = c2.parentID
    WHERE c.pageId = 1411
    ORDER BY c.id DESC
    LIMIT 10;

看看有没有改善?

稍后 - 我使用您的定义创建了一个表,用 30,000 个骨架行填充它,并尝试了这两个查询。他们都在太短的时间内完成,以至于无法注意到。解释计划在这里...

mysql> EXPLAIN SELECT c.id, c2.id
               FROM swb_comments c LEFT JOIN swb_comments c2 ON c.id = c2.parentID
               WHERE c.numberId = 1411     ORDER BY c.id DESC     LIMIT 10;
+----+-------------+-------+------+---------------+----------+---------+------------+------+-----------------------------+
| id | select_type | table | type | possible_keys | key      | key_len | ref        | rows | Extra                       |
+----+-------------+-------+------+---------------+----------+---------+------------+------+-----------------------------+
|  1 | SIMPLE      | c     | ref  | numberId      | numberId | 4       | const      |    1 | Using where; Using filesort |
|  1 | SIMPLE      | c2    | ref  | parentId      | parentId | 3       | books.c.id |   14 |                             |
+----+-------------+-------+------+---------------+----------+---------+------------+------+-----------------------------+

mysql> EXPLAIN SELECT c.id, c2.id
                   FROM swb_comments c LEFT JOIN swb_comments c2 ON c.id = c2.parentID
                   WHERE c.numberId = 1411     ORDER BY c.id DESC     LIMIT 10;
+----+-------------+-------+------+---------------+----------+---------+------------+------+-----------------------------+
| id | select_type | table | type | possible_keys | key      | key_len | ref        | rows | Extra                       |
+----+-------------+-------+------+---------------+----------+---------+------------+------+-----------------------------+
|  1 | SIMPLE      | c     | ref  | numberId      | numberId | 4       | const      |    1 | Using where; Using filesort |
|  1 | SIMPLE      | c2    | ref  | parentId      | parentId | 3       | books.c.id |   14 |                             |
+----+-------------+-------+------+---------------+----------+---------+------------+------+-----------------------------+

这正是我所期望的。

这很神秘。

我会再考虑一下,看看是否还有其他可以尝试的方法。

【讨论】:

这是我的原始查询,但它不支持父 cmets 的分页。而且这个查询实际上要慢得多,大约 32 秒。 嗯。这让我很惊讶。 EXPLAIN 对此有何评论? 我已经更新了carl-fredrik.net/explain.html,并为上述查询添加了解释。我认为“在哪里使用;使用临时;使用文件排序”是罪魁祸首。 是的,它真的不太喜欢你的索引,是吗?也许我们可以看看你的表定义。 分析表只说:表已经是最新的。明天给你表格定义。

以上是关于优化 30 000+ 行表上的 LEFT JOIN的主要内容,如果未能解决你的问题,请参考以下文章

Mysql left join with nested select慢,如何优化

MySQL 在同一张表上有 2 个 LEFT JOIN

PostgreSQL 上的 EF Core 批量删除

在 MySQL 中高效查询 15,000,000 行表

SQL Merge - 我该如何优化它?

INNODB FULLTEXT 使用 JOIN 搜索:不同表上的搜索词