我在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 - IN - 48秒
MySQL 5.7 - EXISTS - 46秒
如您所见,结果令人惊讶:
你能分享一下你的想法吗?
当您接近编写新查询时,如何在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
答案 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
,甚至可能找到exists
比in
更快的值。另一个因素是limit
的价值。 limit 1
应该(假设您的查询返回的行数少于几行)执行当前的exists
,因此您可能会找到一些数量和限制的参数in
将会更慢比exists
。如果MySQL确实将in
的执行计划更改为与exists
类似,那么将会有一些限制值(我们至少知道值1000
) 。您可能会发现in
再次慢于exists
的值。
但要再次强调这一点:它通常不适用。这些值将取决于您的数据,并且情况可能会随之改变。如果你是越来越多的客户,1000的限制可能越来越不相关,你可能会在in
变得比exists
更糟糕的情况下达到临界点(没有MySQL实现它),并且可能必须改变你的查询。
答案 1 :(得分:1)
这是一个公平的比较吗?
EXISTS
是"半连接" - 也就是说,它执行类似连接的查找,但在找到单行时停止。
IN
意味着找到所有行。您的IN
测试在SELECT
的{{1}}中至少有一百个值?
IN
和EXISTS
的速度取决于可用的索引。例如,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,因此两种变体都应该执行相同的操作。