EXISTS vs IN - 哪一个在MySQL 5.5和MySQL 5.7中更好?

时间:2018-02-21 13:08:45

标签: mysql sql performance database-performance

我在MySQL 5.7和MySQL上测试了两种不同的SQL方法(EXISTS vs IN)。 5.5从表现的角度来看。 作为关于测试的附注,两个数据库都在同一台机器上,我在每台测试中只激活其中一个。他们每个人都有4GB的内存分配给他们。我在每次测试之前重新启动数据库,以确保没有完成缓存(至少不在数据库级别)。

我在StackOverflow上看到了很多问题,从IN到EXISTS的转换在性能方面很有帮助。在大多数线程中,对于较旧的MySQL版本(ver< 5.6)就是这种情况。所以我的第一个目标是测试这个理论(对于较旧的MySQL版本,EXISTS优于IN)。

另外,我一直在读到在新的MySQL版本中IN可能已得到改进,所以我想亲眼看看。

因此,为了更好地理解哪一个更适合我将来的查询,我运行了以下测试:

定义恒定数量:

SET @quantity = 50;

EXISTS查询:

SELECT SQL_NO_CACHE
    c.c_first_name, c.c_birth_country
FROM
    customer c
WHERE
    EXISTS( SELECT 
            1
        FROM
            store_sales ss
        WHERE
            ss.ss_quantity > @quantity
                AND ss.ss_customer_sk = c.c_customer_sk)
ORDER BY c.c_first_name DESC , c.c_birth_country DESC
LIMIT 1000;

等效的IN查询:

SELECT SQL_NO_CACHE
    c.c_first_name, c.c_birth_country
FROM
    customer c
WHERE
    c.c_customer_sk IN (SELECT 
            ss.ss_customer_sk
        FROM
            store_sales ss
        WHERE
            ss.ss_quantity > @quantity)
ORDER BY c.c_first_name DESC , c.c_birth_country DESC
LIMIT 1000;

结果:

  • MySQL 5.5 - EXISTS - 53秒
  • MySQL 5.5 - IN - 48秒

  • MySQL 5.7 - EXISTS - 46秒

  • MySQL 5.7 - IN - 4秒

如您所见,结果令人惊讶:

  1. 由于某些原因,EXISTS替代方案的表现比使用MySQL 5.5的IN方案更差。
  2. 对于MySQL 5.7,IN似乎表现得比EXISTS好得多,即使EXISTS替代品的EXPLAIN看起来更好"比IN替代(参见下面的EXPLAIN输出)。
  3. 你能分享一下你的想法吗?

    当您接近编写新查询时,如何在IN和EXISTS之间进行选择?你会如何指导你的团队?我们每次都可以尝试它们,但这听起来有点嘲笑,并且在编写复杂的查询时可能会浪费很多时间。对于MySQL和它们,应该有一些指导方针来解决每个问题。 5.6和MySQL> 5.6。

    我缺少哪些来自MySQL的文档?

    只需关闭所有相关表格的循环并解释信息 -

    带有索引的表创建脚本(您可能会在这里看到许多非有用或冗余的索引,但请忽略它们,因为这是一个测试环境):

    CREATE TABLE `customer` (
       `c_customer_sk` int(11) NOT NULL,
       `c_customer_id` char(16) NOT NULL,
       `c_current_cdemo_sk` int(11) DEFAULT NULL,
       `c_current_hdemo_sk` int(11) DEFAULT NULL,
       `c_current_addr_sk` int(11) DEFAULT NULL,
       `c_first_shipto_date_sk` int(11) DEFAULT NULL,
       `c_first_sales_date_sk` int(11) DEFAULT NULL,
       `c_salutation` char(10) DEFAULT NULL,
       `c_first_name` char(20) DEFAULT NULL,
       `c_last_name` char(30) DEFAULT NULL,
       `c_preferred_cust_flag` char(1) DEFAULT NULL,
       `c_birth_day` int(11) DEFAULT NULL,
       `c_birth_month` int(11) DEFAULT NULL,
       `c_birth_year` int(11) DEFAULT NULL,
       `c_birth_country` varchar(20) DEFAULT NULL,
       `c_login` char(13) DEFAULT NULL,
       `c_email_address` char(50) DEFAULT NULL,
       `c_last_review_date` char(10) DEFAULT NULL,
       PRIMARY KEY (`c_customer_sk`),
       KEY `c_fsd2` (`c_first_shipto_date_sk`),
       KEY `c_fsd` (`c_first_sales_date_sk`),
       KEY `c_hd` (`c_current_hdemo_sk`),
       KEY `c_cd` (`c_current_cdemo_sk`),
       KEY `c_a` (`c_current_addr_sk`),
       KEY `customer_index_1` (`c_first_name`,`c_birth_country`),
       CONSTRAINT `c_a` FOREIGN KEY (`c_current_addr_sk`) REFERENCES `customer_address` (`ca_address_sk`) ON DELETE NO ACTION ON UPDATE NO ACTION,
       CONSTRAINT `c_cd` FOREIGN KEY (`c_current_cdemo_sk`) REFERENCES `customer_demographics` (`cd_demo_sk`) ON DELETE NO ACTION ON UPDATE NO ACTION,
       CONSTRAINT `c_fsd` FOREIGN KEY (`c_first_sales_date_sk`) REFERENCES `date_dim` (`d_date_sk`) ON DELETE NO ACTION ON UPDATE NO ACTION,
       CONSTRAINT `c_fsd2` FOREIGN KEY (`c_first_shipto_date_sk`) REFERENCES `date_dim` (`d_date_sk`) ON DELETE NO ACTION ON UPDATE NO ACTION,
       CONSTRAINT `c_hd` FOREIGN KEY (`c_current_hdemo_sk`) REFERENCES `household_demographics` (`hd_demo_sk`) ON DELETE NO ACTION ON UPDATE NO ACTION
     ) ENGINE=InnoDB DEFAULT CHARSET=utf8
    
    
    CREATE TABLE `store_sales` (
       `ss_sold_date_sk` int(11) DEFAULT NULL,
       `ss_sold_time_sk` int(11) DEFAULT NULL,
       `ss_item_sk` int(11) NOT NULL,
       `ss_customer_sk` int(11) DEFAULT NULL,
       `ss_cdemo_sk` int(11) DEFAULT NULL,
       `ss_hdemo_sk` int(11) DEFAULT NULL,
       `ss_addr_sk` int(11) DEFAULT NULL,
       `ss_store_sk` int(11) DEFAULT NULL,
       `ss_promo_sk` int(11) DEFAULT NULL,
       `ss_ticket_number` int(11) NOT NULL,
       `ss_quantity` int(11) DEFAULT NULL,
       `ss_wholesale_cost` decimal(7,2) DEFAULT NULL,
       `ss_list_price` decimal(7,2) DEFAULT NULL,
       `ss_sales_price` decimal(7,2) DEFAULT NULL,
       `ss_ext_discount_amt` decimal(7,2) DEFAULT NULL,
       `ss_ext_sales_price` decimal(7,2) DEFAULT NULL,
       `ss_ext_wholesale_cost` decimal(7,2) DEFAULT NULL,
       `ss_ext_list_price` decimal(7,2) DEFAULT NULL,
       `ss_ext_tax` decimal(7,2) DEFAULT NULL,
       `ss_coupon_amt` decimal(7,2) DEFAULT NULL,
       `ss_net_paid` decimal(7,2) DEFAULT NULL,
       `ss_net_paid_inc_tax` decimal(7,2) DEFAULT NULL,
       `ss_net_profit` decimal(7,2) DEFAULT NULL,
       PRIMARY KEY (`ss_item_sk`,`ss_ticket_number`),
       KEY `ss_s` (`ss_store_sk`),
       KEY `ss_t` (`ss_sold_time_sk`),
       KEY `ss_d` (`ss_sold_date_sk`),
       KEY `ss_p` (`ss_promo_sk`),
       KEY `ss_hd` (`ss_hdemo_sk`),
       KEY `ss_c` (`ss_customer_sk`),
       KEY `ss_cd` (`ss_cdemo_sk`),
       KEY `ss_a` (`ss_addr_sk`),
       KEY `store_sales_index_1` (`ss_quantity`,`ss_customer_sk`),
       KEY `store_sales_idx_sk_price` (`ss_item_sk`,`ss_sales_price`),
       KEY `store_sales_idx_price_sk` (`ss_sales_price`,`ss_item_sk`),
       CONSTRAINT `ss_a` FOREIGN KEY (`ss_addr_sk`) REFERENCES `customer_address` (`ca_address_sk`) ON DELETE NO ACTION ON UPDATE NO ACTION,
       CONSTRAINT `ss_c` FOREIGN KEY (`ss_customer_sk`) REFERENCES `customer` (`c_customer_sk`) ON DELETE NO ACTION ON UPDATE NO ACTION,
       CONSTRAINT `ss_cd` FOREIGN KEY (`ss_cdemo_sk`) REFERENCES `customer_demographics` (`cd_demo_sk`) ON DELETE NO ACTION ON UPDATE NO ACTION,
       CONSTRAINT `ss_d` FOREIGN KEY (`ss_sold_date_sk`) REFERENCES `date_dim` (`d_date_sk`) ON DELETE NO ACTION ON UPDATE NO ACTION,
       CONSTRAINT `ss_hd` FOREIGN KEY (`ss_hdemo_sk`) REFERENCES `household_demographics` (`hd_demo_sk`) ON DELETE NO ACTION ON UPDATE NO ACTION,
       CONSTRAINT `ss_i` FOREIGN KEY (`ss_item_sk`) REFERENCES `item` (`i_item_sk`) ON DELETE NO ACTION ON UPDATE NO ACTION,
       CONSTRAINT `ss_p` FOREIGN KEY (`ss_promo_sk`) REFERENCES `promotion` (`p_promo_sk`) ON DELETE NO ACTION ON UPDATE NO ACTION,
       CONSTRAINT `ss_s` FOREIGN KEY (`ss_store_sk`) REFERENCES `store` (`s_store_sk`) ON DELETE NO ACTION ON UPDATE NO ACTION,
       CONSTRAINT `ss_t` FOREIGN KEY (`ss_sold_time_sk`) REFERENCES `time_dim` (`t_time_sk`) ON DELETE NO ACTION ON UPDATE NO ACTION
     ) ENGINE=InnoDB DEFAULT CHARSET=utf8
    

    解释 - MySQL 5.7 - EXISTS替代 -

    1   PRIMARY c       index       customer_index_1    124     1000    100.00  Using where; Using index
    2   DEPENDENT SUBQUERY  ss      ref ss_c,store_sales_index_1    ss_c    5   tpcds.c.c_customer_sk   32  50.00   Using where
    

    解释 - MySQL 5.7 - 替代方案 -

    1   SIMPLE  ss      range   ss_c,store_sales_index_1    store_sales_index_1 5       1395022 100.00  Using where; Using index; Using temporary; Using filesort; Start temporary
    1   SIMPLE  c       eq_ref  PRIMARY PRIMARY 4   tpcds.ss.ss_customer_sk 1   100.00  End temporary
    

    解释 - MySQL 5.5 - EXISTS替代 -

    1   PRIMARY c   index       customer_index_1    124     1000    Using where; Using index
    2   DEPENDENT SUBQUERY  ss  ref ss_c,store_sales_index_1    ss_c    5   tpcds.c.c_customer_sk   14  Using where
    

    解释 - MySQL 5.5 - 替代方案 -

    1   PRIMARY c   index       customer_index_1    124     1000    Using where; Using index
    2   DEPENDENT SUBQUERY  ss  index_subquery  ss_c,store_sales_index_1    ss_c    5   func    14  Using where
    

3 个答案:

答案 0 :(得分:3)

这个问题没有最终真理。如果in的效果始终低于exists,则优化程序可以采取的第一个简单步骤就是将每个in重写为exists

in允许优化器使用您不能为常规exists子查询执行的several different execution paths。它尤其可以in执行exists(但反之亦然)。因此,如果您想要一个通用指南,您可以尽可能使用in,因为它可以简单地重写为exists,让您选择(和编译器)以任何方式执行此操作。如果测试显示MySQL采用了错误的路径,您可能只需切换到exists,强制优化器也这样做。

如果优化器选择采用其中一个新的可用执行计划,它们可能会变得更快 - 或者不是。对于优化器做出的许多决策来说都是如此:它基本上是基于它对您的数据的一些有限信息而猜测的,并且可能猜错了。告诉优化器探索一些不同路径的直接方法是使用Optimizer Hints。稍微更改查询(例如,将in切换为exists)可以使优化器也选择不同的执行计划(例如,因为其他执行计划不再可用),因此您可能会将其视为间接提示,虽然它比实际提示更难控制。

这些可能会给你一个更快的结果 - 或者,出于同样的原因,相反。它通常取决于您的实际数据和情况。您只需根据具体情况进行测试,然后选择速度更快的测试即可。但请记住,情况可能会发生变化(如果您的数据分发发生变化),因此您可能需要重新测试并在某些时候重写您的查询。

但它通常不适用 - 正如您已经意识到的那样,对于您的具体情况,您认为“EXISTS优于IN的旧MySQL版本”并不成立,而它似乎对你所看到的大多数问题都这样做(这可能是也可能不是有偏见的选择)。

在一般介绍之后(你想听到一些想法,所以你得到了一些):

5.7 in表现良好的原因是因为MySQL在可能的执行计划中找到了一种非常适合您的特定数据分发的方式。

假设您只有1位客户ss_quantity > @quantity。由于您在ss_quantity上有索引,因此查询的最快答案就是使用此索引,使用该数量查找客户并完成。拥有该数量值的客户越多,其获得的效果就越差。例如。假设每个客户都满足数量条件,那么支持您的order by(因此limit)的索引更可取 - 这是MySQL 5.5通过选择利用索引的执行计划计划决定做的事情customer_index_1

exists更改为in使MySQL找到了该路径。优化器在5.5之间变得更好。和5.7。,所以这不仅仅是随机的运气。但是,如果您的数据分布超出临界点并且MySQL仍然采用该路径,那么它将变得更慢。将达到盈亏平衡点的客户数量惊人。你显然处于这一点的优秀方面。

测试此方法的方法是将@quantity设置为较低的值。您可能会找到in将执行的值exists,甚至可能找到existsin更快的值。另一个因素是limit的价值。 limit 1应该(假设您的查询返回的行数少于几行)执行当前的exists,因此您可能会找到一些数量和限制的参数in将会更慢比exists。如果MySQL确实将in的执行计划更改为与exists类似,那么将会有一些限制值(我们至少知道值1000) 。您可能会发现in再次慢于exists的值。

但要再次强调这一点:它通常不适用。这些值将取决于您的数据,并且情况可能会随之改变。如果你是越来越多的客户,1000的限制可能越来越不相关,你可能会在in变得比exists更糟糕的情况下达到临界点(没有MySQL实现它),并且可能必须改变你的查询。

答案 1 :(得分:1)

这是一个公平的比较吗?

EXISTS是"半连接" - 也就是说,它执行类似连接的查找,但在找到单行时停止。

IN意味着找到所有行。您的IN测试在SELECT的{​​{1}}中至少有一百个值?

INEXISTS的速度取决于可用的索引。例如,IN按此顺序需要WHERE ss.ss_quantity > @quantity AND ss.ss_customer_sk = c.c_customer_sk) 。没有任何索引,INDEX(ss_customer_sk, ss_quantity)的速度取决于表中第一个匹配行的位置!如果它在最后,它将与EXISTS一样慢!由于您的索引较差(顺序相反),IN的效率取决于此线程中不可见的某些分布。

EXISTS - 如果您没有ORDER BY c.c_first_name DESC , c.c_birth_country DESC LIMIT 1000;,则会进行排序和表扫描 - 这些是基准测试的云。也就是说,您计算的时间超过INDEX(c_first_name, c_birth_country) vs EXISTS

由于上述差异,

IN 本身EXISTS更快。您可以从IN看到EXPLAIN是否已安全转为IN。没有基准。

好的,所以EXISTS的5.7更快。请注意IN它正在使用创建临时表的优化。同样,我怀疑临时表的大小/结构/等可能有助于基准是否合理。

使用不同的EXPLAIN这样的临时表可能比有益的更昂贵。我怀疑你找到了LIMIT优化优于“半连接”的情况,但可能存在相反的情况。

我刚发现另一个不公平 - 一个需要IN,另一个需要相反的顺序。您有一个受益INDEX(customer, quantity)(store_sales_index_1)的人。我怀疑这就是为什么IN没有在5.5中失败EXISTS的原因!请使用两个索引重新运行您的基准。

答案 2 :(得分:0)

我猜想IN-variant在5.7中表现得更好的主要原因是你只会看到满足数量条件的销售,并在找到1000个不同的客户时停止。使用EXISTS,您将不得不为许多不满足此条件的客户进行所有销售。此外,IN的查询计划在store_sales表上使用覆盖索引,并将使用PRIMARY键查找客户表,而EXISTS的计划将使用二级索引。特别是当需要从磁盘读取数据时,二级索引的效率会降低。

对于5.6及更高版本,您应该更喜欢IN,因为优化器可以使用半连接。它是半连接,可以颠倒两个表的顺序。 (在5.5中,外部查询的表总是需要在子查询的表之前处理。)semijoin有几种替代执行策略;其中一个模仿EXISTS的执行方式。因此,只要优化器的成本估算正确,IN应该至少与EXISTS一样好。

在5.6之前,IN被转换为EXISTS,因此两种变体都应该执行相同的操作。