MySQL:优化 JOIN 查询

Posted

技术标签:

【中文标题】MySQL:优化 JOIN 查询【英文标题】:MySQL: optimizing a JOIN query 【发布时间】:2009-10-09 03:35:55 【问题描述】:

假设我有两个 MyISAM 表:

tab_big:   id1, id2, id_a, ord         (5 billion records)
tab_small: id1, id2, id_b              (1 billion records)


CREATE TABLE IF NOT EXISTS `tab_big` (
  `id_a` int(10) unsigned NOT NULL,
  `id1` int(10) unsigned NOT NULL,
  `id2` int(10) unsigned NOT NULL,
  `ord` int(10) unsigned NOT NULL DEFAULT '1',
  PRIMARY KEY (`id_a`,`id1`,`id2`),
  KEY `id1` (`id1`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;


CREATE TABLE IF NOT EXISTS `tab_small` (
  `id_b` int(10) unsigned NOT NULL,
  `id1` int(10) unsigned NOT NULL,
  `id2` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id_b`,`id1`,`id2`),
  KEY `id_b` (`id_b`),
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

所有字段都是 INT。在这两个表中,三个 id 字段(分别为 id1、id2、id_a 和 id1、id2、id_b)值的组合是唯一的,因此我为这三个字段创建了一个主键。

我需要一个高效的查询,从第一个表中获取 id_a 的唯一值,其中:

    第二个表中的 id_b 是一个给定值(将其缩小到大约 10k 个条目) id1/id2 组合在两个表中是相同的 第一个表中的 id_a 与 tab_small 子集中的 id1、id2 字段中的任何一个都不相同(由 id_b 字段缩小);经过一番摆弄之后,似乎在 php 中生成列表(大约 200 个 id)并将其作为文本提供比添加另一个 JOIN 效果更好)。

我认为它不是很容易缓存,因为两个表一直在变化(添加行)。

我当前的查询非常简单:

SELECT tab_big.id_a FROM tab_big, tab_small
    WHERE tab_small.id_b = '$constant'
    AND tab_big.id1 = tab_small.id1 AND tab_big.id2 = tab_small.id2
    AND tab_big.id_a NOT IN (comma delimited list of 200 ids)
    GROUP BY tab_big.id_a
    ORDER BY SUM(tab_big.ord) DESC
    LIMIT 10

它有效,但速度不够快,无法真正使用它。可以用它做什么?

EXPLAIN 表示它首先从 tab_big 获取范围查询,然后将其应用于 tab_small(编辑:添加在下面)。我不知道为什么(解释说查询使用主键),但添加 tab_big.id1 索引似乎有点帮助。此外,尝试使用 STRAIGHT_JOIN 使其相反,首先从(较小的)tab_small 中选择一个 10k 子集,然后使用它在(较大的)tab_big 中搜索比默认值差得多的结果(编辑:使用我的小数据集现在必须进行测试;在生产数据上显然是相反的,EXPLAIN 看起来像第二个)。

+----+-------------+-----------+--------+-----------------+---------+---------+-------------------------------------------+---------+----------------------------------------------+
| id | select_type | table     | type   | possible_keys   | key     | key_len | ref                                       | rows    | Extra                                        |
+----+-------------+-----------+--------+-----------------+---------+---------+-------------------------------------------+---------+----------------------------------------------+
|  1 | SIMPLE      | tab_big   | range  | PRIMARY,id1     | PRIMARY | 4       | NULL                                      | 1374793 | Using where; Using temporary; Using filesort | 
|  1 | SIMPLE      | tab_small | eq_ref | PRIMARY,id_b    | PRIMARY | 12      | const,db.tab_big.id1,db.tab_big.id2       |       1 | Using index                                  | 
+----+-------------+-----------+--------+-----------------+---------+---------+-------------------------------------------+---------+----------------------------------------------+

在较大的数据集上,EXPLAIN 可能看起来更像这样(尽管忽略“行”值 - 它取自较小的数据集):

+----+-------------+-----------+------+---------------------+---------+---------+------------------+-------+----------------------------------------------+
| id | select_type | table     | type | possible_keys       | key     | key_len | ref              | rows  | Extra                                        |
+----+-------------+-----------+------+---------------------+---------+---------+------------------+-------+----------------------------------------------+
|  1 | SIMPLE      | tab_small | ref  | PRIMARY,id_b,id1    | PRIMARY | 4       | const            |   259 | Using index; Using temporary; Using filesort | 
|  1 | SIMPLE      | tab_big   | ref  | PRIMARY,id1         | id1     | 4       | db.tab_small.id1 | 25692 | Using where                                  | 
+----+-------------+-----------+------+---------------------+---------+---------+------------------+-------+----------------------------------------------+

有什么想法吗?

【问题讨论】:

你能把NOT IN去掉,写成IN吗?这通常有助于解决性能问题。 不,不幸的是,我只知道我不想要的东西。 :// 能把表结构贴在SQL里吗? 你能把EXPLAIN的输出也贴出来吗? 两者都添加了;请注意,报告的行数可能比生产中的要少一些(我目前只有一个小数据集)。 【参考方案1】:

创建以下索引:

CREATE INDEX ix_big_1_2_a ON tab_big (id1, id2, id_a)
CREATE UNIQUE INDEX ux_small_b_2_1 ON tab_small (id_b, id2, id1)

试试这个:

SELECT  DISTINCT
        a.id_a
FROM    tab_small b
JOIN    tab_big a
ON      (a.id1, a.id2) = (b.id1, b.id2)
WHERE   b.id_b = 2
        AND a.id_a NOT IN
        (
        SELECT  id1
        FROM    tab_small b1 /* FORCE INDEX (PRIMARY) */
        WHERE   b1.id_b = 2
        )
        AND a.id_a NOT IN
        (
        SELECT  id2
        FROM    tab_small b2 /* FORCE INDEX (ux_small_b_2_1) */
        WHERE   b2.id_b = 2
        )

,它产生这个查询计划:

1, 'PRIMARY', 'b', 'ref', 'PRIMARY,ux_small_b_2_1', 'PRIMARY', '4', 'const', 1, 100.00, 'Using index; Using temporary'
1, 'PRIMARY', 'a', 'ref', 'ix_big_1_2', 'ix_big_1_2', '8', 'test.b.id1,test.b.id2', 2, 100.00, 'Using where'
3, 'DEPENDENT SUBQUERY', 'b2', 'ref', 'ux_small_b_2_1', 'ux_small_b_2_1', '8', 'const,func', 1, 100.00, 'Using index'
2, 'DEPENDENT SUBQUERY', 'b1', 'ref', 'PRIMARY', 'PRIMARY', '8', 'const,func', 1, 100.00, 'Using index'

它没有达到预期的效率,但我仍然希望它比您的查询更快。

我注释掉了FORCE INDEX 语句,但您可能需要取消注释,因为优化器不会选择这些索引。

如果mysql 能够使用MERGE 执行FULL OUTER JOIN,一切都会简单得多,但事实并非如此。

更新:

从你的统计来看,这个查询会更有效率:

SELECT  id_a
FROM    (
        SELECT  DISTINCT id_a
        FROM    tab_big ad
        ) a
WHERE   id_a NOT IN
        (
        SELECT  id1
        FROM    tab_small b1 FORCE INDEX (PRIMARY)
        WHERE   b1.id_b = 2
        )
        AND id_a NOT IN
        (
        SELECT  id2
        FROM    tab_small b2 FORCE INDEX (ux_small_b_2_1)
        WHERE   b2.id_b = 2
        )
        AND EXISTS
        (
        SELECT  NULL
        FROM    tab_small be
        JOIN    tab_big ae
        ON      (ae.id1, ae.id2) = (be.id1, be.id2)
        WHERE   be.id_b = 2
                AND ae.id_a = a.id_a
        )

它的工作原理如下:

构建DISTINCT id_a 列表(100,000 行长) 过滤掉子集中存在的值 对于id_a 的每个值,它会在子集中搜索(id_a, id1, id2) 的存在。这是通过迭代子集来完成的。由于找到该值的概率很高,因此很可能从子集的开头开始在10 行左右搜索会成功,而EXISTS 将在那个时刻返回。

这很可能需要评估大约 1,000,000 记录左右。

确保使用以下计划:

1, 'PRIMARY', '<derived2>', 'ALL', '', '', '', '', 8192, 100.00, 'Using where'
5, 'DEPENDENT SUBQUERY', 'be', 'ref', 'PRIMARY,ux_small_b_2_1', 'PRIMARY', '4', 'const', 1, 100.00, 'Using index'
5, 'DEPENDENT SUBQUERY', 'ae', 'eq_ref', 'PRIMARY,ix_big_1_2', 'PRIMARY', '12', 'a.id_a,test.be.id1,test.be.id2', 1, 100.00, 'Using index'
4, 'DEPENDENT SUBQUERY', 'b2', 'ref', 'ux_small_b_2_1', 'ux_small_b_2_1', '8', 'const,func', 1, 100.00, 'Using index'
3, 'DEPENDENT SUBQUERY', 'b1', 'ref', 'PRIMARY', 'PRIMARY', '8', 'const,func', 1, 100.00, 'Using index'
2, 'DERIVED', 'ad', 'range', '', 'PRIMARY', '4', '', 10, 100.00, 'Using index for group-by'

,最重要的部分是最后一行的Using index for group-by

【讨论】:

我不明白您为什么要按照您的建议定义索引。为了让连接与索引一起工作,连接中使用的所有列是否都必须被索引并且与连接条件中的顺序相同?我的感觉是语句很慢是因为加入...而不是因为子查询! JOIN 中使用的列在ix_big_1_2_a 中编制索引。由于JOIN,该语句可能(或可能不会)很慢,但在我们知道tab_big 中有多少行满足JOIN 条件之前,我们无法判断这是真正的原因。 不错!首先,ix_big_1_2_a 与原始查询有很大不同。其次,您建议的查询效果更好。不幸的是,它丢失了原始查询中的 ORDER BY 部分(应该首先显示最合适的条目),但我可能可以绕开这个问题。非常感谢!对此,我真的非常感激。 :)【参考方案2】:

你试过tab_small LEFT JOIN tab_big吗?您也可以在tab_small.id_btab_big.id_a 字段上创建索引

【讨论】:

为了以防万一,尝试了 LEFT JOIN,实际上效果更差。我实际上有一个 tab_small id_b 索引;添加 tab_big.id_a 索引并没有帮助。【参考方案3】:

我建议在连接的所有四个列上放置一个索引(在 tb.id1、tb.id2、ts.id1 和 ts.id2 列上放置四个单独的索引,或者在 tb.id1 上放置两个/id2 和 ts.id1/id2)。然后看看这是否能给你带来更好的表现。 (我认为确实如此,但除非尝试一下,否则您永远不会知道。)


注意:下面的想法不起作用,但我把它留在了所以 cmets 仍然有意义。

另外,除了使用 PHP 生成的列表之外,你不能在连接条件中(或者如果你愿意,在 where 子句中)表达你的限制 (3) 吗? (类似于 rexem 的建议)

SELECT tb.id_a
  FROM TAB_BIG tb
  JOIN TAB_SMALL ts ON ts.id1 = tb.id1
                 AND ts.id2 = tb.id2
                 AND tb.id1 <> ts.id_a
                 AND tb.id2 <> ts.id_a
 WHERE ts.id_b = ?

但这更多是为了清晰和简单,而不是性能。 (另请注意,附加条件可能需要 id_a 上的另一个索引,并且可能需要 tb.id1 和 tb.id2 上的单独索引。)

【讨论】:

尝试添加 id1,id2 索引,没有帮助(解释仍然说它使用 PRIMARY)。此处的 子句是否仅排除 id1、id2 之一与此特定条目中的 id_a 相同的条目?我需要排除 all 曾出现在具有特定 id_b 的 ts 记录中的 id_a(作为 id1 或 id2)。 好的,那么 rexem 的 EXISTS 是正确的(或 Quassnoi 的陈述)。为了清楚起见,我会在帖子中留下建议。

以上是关于MySQL:优化 JOIN 查询的主要内容,如果未能解决你的问题,请参考以下文章

具有 JOIN 和 GROUP BY 优化的 MySQL 查询。是不是可以?

使用昂贵的 INNER JOIN 优化 MySQL 查询

MySQL 通过semi join 优化子查询

MySQL - 优化查询(LIMIT、OFFSET、JOIN)> 2500 万行

MySQL JOIN LIMIT 动态查询优化

优化 MYSQL 中的 UPDATE JOIN 查询