【问题标题】:Which query should be used? Deducing from MySQL Explain应该使用哪个查询?从 MySQL Explain 推导出来
【发布时间】:2019-10-30 08:18:51
【问题描述】:

O'reilly Optimizing SQL Statments Book 中的Explaining MySQL Explain 一章,最后有这个问题。

以下是在父/子关系中检索孤立父记录的业务需求示例。此 SQL 查询可以用三种不同的方式编写。虽然输出产生相同的结果,但 QEP 显示了三种不同的路径。

mysql> EXPLAIN SELECT p.*
    -> FROM parent p
    -> WHERE p.id NOT IN (SELECT c.parent_id FROM child c)\G
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: p
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 160
        Extra: Using where
*************************** 2. row ***************************
           id: 2
  select_type: DEPENDENT SUBQUERY
        table: c
         type: index_subquery
possible_keys: parent_id
          key: parent_id
      key_len: 4
          ref: func
         rows: 1
        Extra: Using index
2 rows in set (0.00 sec)



mysql> EXPLAIN SELECT p.*
    -> FROM parent p
    -> LEFT JOIN child c ON p.id = c.parent_id
    -> WHERE c.child_id IS NULL\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: p
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 160
        Extra:
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: c
         type: ref
possible_keys: parent_id
          key: parent_id
      key_len: 4
          ref: test.p.id
         rows: 1
        Extra: Using where; Using index; Not exists
2 rows in set (0.00 sec)



mysql> EXPLAIN SELECT p.*
    -> FROM parent p
    -> WHERE NOT EXISTS
    -> SELECT parent_id FROM child c WHERE c.parent_id = p.id)\G
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: p
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 160
        Extra: Using where
*************************** 2. row ***************************
           id: 2
  select_type: DEPENDENT SUBQUERY
        table: c
         type: ref
possible_keys: parent_id
          key: parent_id
      key_len: 4
          ref: test.p.id
         rows: 1
        Extra: Using index
2 rows in set (0.00 sec)

哪个最好?随着时间的推移,数据增长会导致不同的 QEP 表现更好吗?

据我所知,书上或互联网上都没有答案。

【问题讨论】:

  • 请注意,这两个查询是不等价的。如果child.parent_id 不能为 NULL,它们将返回相同的结果。
  • 是的,我想它是隐含的 parent_id 是一个 fk,因为它也是子表中的一个索引并且没有孩子是孤立的,只有父母是。讽刺。
  • FK 可以为 NULL(可选关系)。顺便说一句:哪本书?您的报价来源是什么?
  • 对不起,我刚刚编辑了我的评论。 oreilly.com/library/view/effective-mysql-optimizing/… 是书。

标签: mysql innodb explain


【解决方案1】:

有一个old article from 2009 我在 stackoverflow 上看到过很多次链接。那里的测试表明,NOT EXISTS 查询比其他两个查询(LEFT JOINNOT IN)慢 27%(实际上是 26%)。

但是,优化器已经从一个版本到另一个版本进行了改进。完美的优化器将为所有三个查询创建相同的执行计划。但只要优化器不完美,“哪个查询更快?”的答案。可能取决于实际设置(包括版本、设置和数据)。

我过去曾进行过类似的测试,我只记得LEFT JOIN 从未比任何其他方法慢得多。但出于好奇,我刚刚使用默认设置在 MariaDB 10.3.13 便携式 Windows 版本上创建了一个新测试。

虚拟数据:

set @parents = 1000;

drop table if exists parent;
create table parent(
    parent_id mediumint unsigned primary key
);
insert into parent(parent_id)
    select seq
    from seq_1_to_1000000
    where seq <= @parents
;

drop table if exists child;
create table child(
    child_id mediumint unsigned primary key,
    parent_id mediumint unsigned not null,
    index (parent_id)
);
insert into child(child_id, parent_id)
    select seq as child_id
    , floor(rand(1)*@parents)+1 as parent_id
    from seq_1_to_1000000
;

不在:

set @start = TIME(SYSDATE(6));

select count(*) into @cnt
from parent p
where p.parent_id not in (select parent_id from child c);

select @cnt, TIMEDIFF(TIME(SYSDATE(6)), @start);

左连接:

set @start = TIME(SYSDATE(6));

select count(*) into @cnt
from parent p
left join child c on c.parent_id = p.parent_id
where c.parent_id is null;

select @cnt, TIMEDIFF(TIME(SYSDATE(6)), @start);

不存在:

set @start = TIME(SYSDATE(6));

select count(*) into @cnt
from parent p
where not exists (
    select *
    from child c
    where c.parent_id = p.parent_id
);

select @cnt, TIMEDIFF(TIME(SYSDATE(6)), @start);

以毫秒为单位的执行时间:

@parents   | 1000 | 10000 | 100000 | 1000000
-----------|------|-------|--------|--------
NOT IN     |   21 |    38 |    175 |    4459
LEFT JOIN  |   24 |    40 |    183 |    1508
NOT EXISTS |   26 |    44 |    180 |    4463

我已经多次执行查询并花费最少的时间价值。而SYSDATE 可能不是衡量执行时间的最佳方法——所以不要把这些数字当作准确的。但是,我们可以看到最多 100K 父行,并没有太大的区别,而且NOT IN 方法要快一点。但是对于 1M 父行,LEFT JOIN 的速度要快三倍。

结论

那么答案是什么?我只能说:“LEFT JOIN”获胜。但事实是——这个测试证明不了。答案是(多次):“这取决于”。当性能很重要时,您能做的最好的事情就是使用针对真实数据的真实查询运行您自己的测试。如果您还没有真实数据,您应该创建具有您期望在未来拥有的数量和分布的虚拟数据。

【讨论】:

    【解决方案2】:

    这取决于您使用的 MySQL 版本。在旧版本中,IN ( SELECT ...) 的表现非常糟糕。在最新版本中,它通常与其他变体一样好。此外,MariaDB 也有一些优化差异,可能在这方面。

    EXISTS( SELECT 1 ... ) 可能是最清楚地说明了意图。而且它可能一直(一旦出现)都很快。

    NOT INNOT EXISTS 是不同的动物。

    您的问题中可能会产生影响的一些内容:funcindex_subquery。在类似的查询中,您可能看不到这些,而这种差异可能会导致性能差异。

    或者,重复一遍:

    “自 2009 年以来,优化器已进行了多项改进。

    “致作者 (Quassnoi):请重新运行您的测试,并指定它们正在运行的版本。另请注意 MySQL 和 MariaDB 可能会产生不同的结果。

    “致读者:自己测试变体,不要盲目相信本博客的结论。”

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-09-14
      • 2016-06-11
      • 1970-01-01
      • 2019-10-16
      • 2021-08-09
      • 2012-07-28
      • 1970-01-01
      • 2015-08-19
      相关资源
      最近更新 更多