当加入一个非常小/空的表时,尽管我使用“LIMIT”,为啥 MySQL 会进行全面扫描?

Posted

技术标签:

【中文标题】当加入一个非常小/空的表时,尽管我使用“LIMIT”,为啥 MySQL 会进行全面扫描?【英文标题】:When joining to a very small/empty table why MySQL makes a full scan in spite of I'm using "LIMIT"?当加入一个非常小/空的表时,尽管我使用“LIMIT”,为什么 MySQL 会进行全面扫描? 【发布时间】:2018-03-17 16:20:01 【问题描述】:

编辑:我从示例查询中删除了GROUP BY 子句,但同样的问题显示“当我将表 x 连接到一个空/1 行表 y 时,尽管我使用限制”


原问题: 我试图学习如何优化我的 SQL 查询,但遇到了我无法理解的行为。有这样的架构

SQL fiddle

CREATE TABLE `country` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `name` varchar(45) DEFAULT NULL,   
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB ;


CREATE TABLE `school` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `name` varchar(45) DEFAULT NULL,   
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB ;


CREATE TABLE `users` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `name` varchar(45) DEFAULT NULL,
   `country_id` int(11) DEFAULT NULL,

   PRIMARY KEY (`id`),
   KEY `fk_country_idx` (`country_id`),
   CONSTRAINT `fk_users_country` FOREIGN KEY (`country_id`) REFERENCES `country` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
 ) ENGINE=InnoDB ;


CREATE TABLE `user_school_mm` (
   `user_id` int(11) NOT NULL,
   `school_id` int(11) NOT NULL,
   PRIMARY KEY (`user_id`, `school_id`),
   KEY `fk_user_school_mm_user_idx` (`user_id`),
   KEY `fk_user_school_mm_school_idx` (`school_id`),
   CONSTRAINT `fk_user_school_mm_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION,
   CONSTRAINT `fk_user_school_mm_school` FOREIGN KEY (`school_id`) REFERENCES `school` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION
 ) ENGINE=InnoDB ;

INSERT INTO country (name) VALUES ('fooCountry1');
INSERT INTO school (name) VALUES ('fooSchool1'),('fooSchool2'),('fooSchool3');
INSERT INTO users (name, country_id) VALUES
('fooUser1',1),
('fooUser2',1),
('fooUser3',1),
('fooUser4',1),
('fooUser5',1),
('fooUser6',1),
('fooUser7',1),
('fooUser8',1),
('fooUser9',1),
('fooUser10',1)
;
INSERT INTO user_school_mm (user_id, school_id) VALUES
(1,1),(1,2),(1,3),
(2,1),(2,2),(2,3),
(3,1),(3,2),(3,3),
(4,1),(4,2),(4,3),
(5,1),(5,2),(5,3),
(6,1),(6,2),(6,3),
(7,1),(7,2),(7,3),
(8,1),(8,2),(8,3),
(9,1),(9,2),(9,3),
(10,1),(10,2),(10,3)
;

查询 1(快速)

-- GOOD QUERY (mysql uses the limit and skip users table scan after 2 rows )
SELECT * 
FROM 
    users LEFT JOIN
    user_school_mm on users.id = user_school_mm.user_id
ORDER BY users.id ASC
LIMIT 2
-- takes about 100 milliseconds if users table is 3 million records  

解释

+---+-----------+---------------+------+-----------------------------------+----------+---------+---------------+------+-----------+
|id |select_type|table          | type | possible_keys                     | key      | key_len | ref           | rows | Extra     |
+---+-----------+---------------+------+-----------------------------------+----------+---------+---------------+------+-----------+
|1  |SIMPLE     |users          |index |PRIMARY,fk_country_idx             | PRIMARY  |4        |               |2     |           |
|1  |SIMPLE     |user_school_mm |ref   |PRIMARY,fk_user_school_mm_user_idx | PRIMARY  |4        |tests.users.id |1     |Using index|
+---+-----------+---------------+------+-----------------------------------+----------+---------+---------------+------+-----------+ 

QUERY 2(慢)

-- BAD QUERY (MySQL ignores the limit and scanned the entire users table )
SELECT * 
FROM 
    users LEFT JOIN
    country on users.country_id = country.id
ORDER BY users.id ASC
LIMIT 2
-- takes about 9 seconds if users table is 3 million records 

解释

+---+-----------+--------+------+------------------------+-----+---------+-----+------+---------------------------------------------------+
|id |select_type|table   | type | possible_keys          | key | key_len | ref | rows | Extra                                             |
+---+-----------+--------+------+------------------------+-----+---------+-----+------+---------------------------------------------------+
|1  |SIMPLE     |users   |ALL   | PRIMARY,fk_country_idx |     |         |     | 10   | Using temporary; Using filesort                   |
|1  |SIMPLE     |country |ALL   | PRIMARY                |     |         |     | 1    | Using where; Using join buffer (Block Nested Loop)|
+---+-----------+--------+------+------------------------+-----+---------+-----+------+---------------------------------------------------+

我不明白幕后发生了什么,我想如果我使用 users 表的主键进行排序和分组,MySQL 将取 users 表的前 2 行并继续加入,但它似乎没有这样做,而是在查询 2 中扫描了整个表

为什么 MySQL 在 query2 中扫描整个表,而在 query1 中只扫描前 2 行?

MySQL 版本是 5.6.38

【问题讨论】:

我猜它会在找到足够多的行后结束扫描。 @shawnt00 但在查询 2 中,它扫描了整个表(本示例中为 10 行),如果用户表为 300 万行,则扫描 300 万行 您的“好”查询也很糟糕...SELECT * .... GROUP BY users.id 是错误的 ansi GROUP BY SQL。它可能导致不良的无关数据psce.com/en/blog/2012/05/15/… 这些是格式错误的查询,使用 select *group by。也许这解释了为什么 MySQL 不尝试优化它们。这些很容易被更合适的逻辑替换,从而产生正确的执行计划。 很高兴你把它整理好了。我其实只是第一次略过,错过了一个重要的细节。 【参考方案1】:

MySQL 优化器将首先决定连接顺序/方法,然后检查对于选择的连接顺序,是否可以通过使用索引来避免排序。对于本题的慢查询,优化器决定使用 Block-Nested-Loop (BNL) join。

当其中一个表非常小时(并且没有限制)时,BNL 通常比使用索引更快。

但是,对于 BNL,行不一定按照第一个表给出的顺序出现。因此,在应用 LIMIT 之前,需要对连接的结果进行排序。

您可以通过set optimizer_switch = 'block_nested_loop=off';关闭BNL

【讨论】:

你......是......国王。从现在开始,我会读你说的每一个字。非常感谢您的回答,感谢您的MySQL。【参考方案2】:

主要原因是GROUP BY的误用。让我们进行第一个查询。虽然“快”了,但还是“错”了:

SELECT * 
    FROM users
    LEFT JOIN user_school_mm on users.id = user_school_mm.user_id
    GROUP BY users.id
    ORDER BY users.id ASC
    LIMIT 2

一个用户可以去两所学校。使用多:多映射user_school_mm 声称这是一种可能性。因此,在执行JOIN 之后,您会为单个用户获得 2 行。但是,你GROUP BY users.id,把它归结为一行。但是...您应该使用两个 school_id 值中的哪一个??

在您提出有意义的查询之前,我不会尝试解决性能问题。到那时,就更容易指出为什么一个查询比另一个查询执行得更好。

【讨论】:

非常感谢您的关注,我删除了 GROUP BY 并且 MySQL 仍然具有相同的行为。 “如果我将表连接到一个空表,MySQL 将对第一个表进行全扫描”。我在如何重现的答案中提供了详细的步骤。【参考方案3】:

经过一些测试,如果第二张表(user_school_mm)有一些数据,MySQL 不会对第一个表进行全表扫描,如果第二张表(country)没有数据/很少数据(1 或 2 条记录) MySQL 将进行全表扫描。为什么会发生这种情况?我不知道。

如何复制

1- 创建这样的架构

CREATE TABLE `event` (
   `ev_id` int(11) NOT NULL AUTO_INCREMENT,
   `ev_note` varchar(255) DEFAULT NULL,
   PRIMARY KEY (`ev_id`)
 ) ENGINE=InnoDB;

CREATE TABLE `table1` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `name` varchar(45) DEFAULT NULL,   
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB ;

CREATE TABLE `table2` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `name` varchar(45) DEFAULT NULL,   
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB ;

2- 在主表(本例中为event)中插入一些数据(我用 35601000 行填充)

3- 将 table1 留空并在 table2 中插入 15 行

insert into table2 (name) values 
('fooBar'),('fooBar'),('fooBar'),('fooBar'),('fooBar'),
('fooBar'),('fooBar'),('fooBar'),('fooBar'),('fooBar'),
('fooBar'),('fooBar'),('fooBar'),('fooBar'),('fooBar');

4- 现在用 table2 连接主表并用 table1 重新测试相同的查询

查询 1(快速)

select * 
from 
    event left join 
    table2 on event.ev_id = table2.id
order by event.ev_id
limit 2;
-- executed in 300 milliseconds measured by the client

解释

+---+-----------+--------+------+----------------+--------+---------+------------------+------+--------+
|id |select_type|table   | type | possible_keys  | key    | key_len | ref              | rows | Extra  |
+---+-----------+--------+------+----------------+--------+---------+------------------+------+--------+
|1  |SIMPLE     |event   |index |                |PRIMARY |4        |                  | 2    |        |
|1  |SIMPLE     |table2  |eq_ref|PRIMARY         |PRIMARY |4        |tests.event.ev_id | 1    |        |
+---+-----------+--------+------+----------------+--------+---------+------------------+------+--------+

查询 2(慢)

select * 
from 
    event left join 
    table1 on event.ev_id = table1.id
order by event.ev_id
limit 2;
-- executed in 79 seconds measured by the client

解释

+---+-----------+--------+------+----------------+--------+---------+-------+---------+---------------------------------------------------+
|id |select_type|table   | type | possible_keys  | key    | key_len | ref   | rows    | Extra                                             |
+---+-----------+--------+------+----------------+--------+---------+-------+---------+---------------------------------------------------+
|1  |SIMPLE     |event   |ALL   |                |        |         |       |33506704 | Using temporary; Using filesort                   |
|1  |SIMPLE     |table1  |ALL   |PRIMARY         |        |         |       |1        | Using where; Using join buffer (Block Nested Loop)|
+---+-----------+--------+------+----------------+--------+---------+-------+---------+---------------------------------------------------+

MySQL 版本是 5.6.38

【讨论】:

以上是关于当加入一个非常小/空的表时,尽管我使用“LIMIT”,为啥 MySQL 会进行全面扫描?的主要内容,如果未能解决你的问题,请参考以下文章

加入具有重复行数据的表时如何获得正确的 SUM()?

尝试将记录添加到具有先前创建的记录的表时出错

在 PostgreSQL 中删除名称为空的表

在 bigquery 中查询多个数据集中的表时遇到问题

org.hibernate.hql.internal.ast.QuerySyntaxException:意外令牌:FROM,当尝试删除与自身连接的表时

当 LEFT JOINing 两个不同的表时,GROUP_CONCAT 接触了太多的重复值