mysql 查询语句中的 order by 对索引的影响

负载均衡师
• 阅读 3967

Mysql从5.0以后,索引的使用就相当智能了,甚至还支持索引聚合(一次查询使用多条索引),但昨天一次业务查询中,一条where子句(例如 where user_id=1000)明明可以使用联合索引,把扫描行限制在数千范围内的select语句,竟然使用了全主键索引扫描,导致花费将近40s秒才返回结果集。更神奇的是,该where子句只有特定的数据才会导致误用主键,大部分数据(比如 user_id = 18 或其他值)都可以在几百毫秒内完成。

经过explain的初步判断,应该是order by id limit x子句影响了索引的选择。

这里把这个问题复现一下。

建立数据表

首先准备一份不多不少的数据:足以让mysql对索引做出优化,又不至于查询很慢。从Kaggle找到一份现成的图书数据Good read books,创建一张book表,并导入文件"books.csv"。

CREATE TABLE `NewTable` (
`bookID`  int(11) NOT NULL AUTO_INCREMENT ,
`title`  varchar(256) NULL DEFAULT NULL ,
`authors`  varchar(128) NULL DEFAULT NULL ,
`average_rating`  decimal(10,2) NULL DEFAULT NULL ,
`isbn13`  varchar(20) NULL DEFAULT NULL ,
`language_code`  varchar(10) NULL DEFAULT NULL ,
`num_pages`  int(11) NULL DEFAULT NULL ,
`ratings_count`  int(11) NULL DEFAULT NULL ,
`text_reviews_count`  int(11) NULL DEFAULT NULL ,
`publication_date`  date NULL DEFAULT NULL ,
`publisher`  varchar(256) NULL DEFAULT NULL ,
`create_time`  datetime NULL DEFAULT NULL ,
`update_time`  datetime NULL DEFAULT NULL ,
`count`  int(11) NULL DEFAULT NULL ,
PRIMARY KEY (`bookID`),
INDEX `idx_book_title` (`title`) USING BTREE ,
INDEX `idx_book_author` (`authors`) USING BTREE ,
INDEX `idx_book_avg_rating` (`average_rating`) USING BTREE ,
INDEX `idx_book_isbn` (`isbn13`) USING BTREE ,
INDEX `idx_book_num_pages` (`num_pages`) USING BTREE ,
INDEX `idx_book_lan_code_pub_create` (`language_code`, `publisher`, `create_time`) USING BTREE 
);

检查数据

总数

select count(*) from book可以看到全表数据约7500条:
mysql 查询语句中的 order by 对索引的影响

数据记录

数据表前几条大致是如下的样子:
mysql 查询语句中的 order by 对索引的影响

索引

其中有一条 idx_book_lan_code_pub_create的联合索引,是为测试语句准备的,它首列language_code区分度不是很高,而且很不平衡,正好适合测试。
mysql 查询语句中的 order by 对索引的影响

用show index查看一下索引统计信息的话:
mysql 查询语句中的 order by 对索引的影响
可以看到它的基数只有22

测试索引使用

简单首列查询

select * from book where language_code='en-US'

mysql 查询语句中的 order by 对索引的影响
很简单地使用了联合索引第一列,扫描919行即得到全部结果(也是919行)

加order by

select * from book where language_code='en-US' order by bookID 

mysql 查询语句中的 order by 对索引的影响
显然,仅仅order by并不会影响索引的使用,但在Extra信息中,出现了Using filesort,结果集在内存中排序,这通常是影响性能的提示,不过对于900多个结果来说,也不算事儿。

加limit

select * from book where language_code='en-US' order by bookID  limit 5

mysql 查询语句中的 order by 对索引的影响
戏剧性的结果出现了:mysql选择了PRIMARY,也就是主键索引,而抛弃了where子句适用的联合索引,Extra字段中出现Using where,意味着在内存中做筛选,同时Using filesort消失了。

此时mysql优化器应该是看到了:总共需要才返回5条数据,联合索引的扫描数可能不低,那还不如干脆用主键,按顺序依次取数据,取出来再看是否满足where子句要求,满足的就留下,不满足的放弃(就是所谓Using where),果然才取了40条(rows=40)就找到了全部记录。

调整limit数量

那么是不是只要加limit,就会导致使用主键索引呢?也不会。这次我们把限制调整到50:

select * from book where language_code='en-US' order by bookID  limit 50

mysql 查询语句中的 order by 对索引的影响
果然又退回到使用联合索引,并且Using filesort了。

经过测试,临界点在35,小于这个数,才使用主键索引——说明优化器内部有个估算:先通过where子句获得结果集再排序,或者从排序结果逐个检索结果集,那种更值得?当然这种估算未必准确,比如这里limit 50的情况,如果我们强制使用PRIMARY索引,其实效果更好:

select * from book where language_code='en-US' order by bookID  limit 50

mysql 查询语句中的 order by 对索引的影响
可以看到只需要50行扫描,而且真正执行的话,使用主键索引只需3ms,而缺省的联合索引需要5ms。

调整language_code值

如果把language_code从总数第二的'en-US',换成最多的'eng'会怎样呢?

索引切换的临界点会变为145,说明mysql优化器会先从联合索引先大致确认给定数据的检索量,再决定用哪种索引,这也解释了为什么在实际业务环境中,user_id的不同取值,会影响查询计划。

通常优化器的选择是正确的,但也会出现文章一开始提到的误判,这有可能是索引基数(cardinality)更新不及时造成的。

解决方案

强制指定索引

使用force index强制指定,无论如何limit都不会用主键索引了:

select * from book force index(idx_book_lan_code_pub_create) where language_code='en-US' order by bookID limit 5

mysql 查询语句中的 order by 对索引的影响
优点是可靠:指定的索引一定能用上;缺点是死板:将来SQL语句变了,可能影响优化,指定的索引也不能改名,而且用了数据库方言

改用非主键字段排序

这里用bookID排序,基本相当于按插入顺序,那么跟create_time是一致的,改成order by create_time,肯定也不会用主键索引了:

select * from book where language_code='en-US' order by create_time limit 5

mysql 查询语句中的 order by 对索引的影响
这个方法的优点很明显:还是普通SQL,没有数据库依赖性,将来SQL的改变,不影响优化器;唯一的缺点是:对于那些真的用主键索引更快的数据,就享受不到优化的好处了。

尝试优化索引基数

前面说过,索引基数不是实时更新的,可能导致优化器误判,那么可以尝试用analyze命令重新计算索引统计信息,看看是否会影响优化器的选择:

analyze table book
点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Wesley13 Wesley13
4年前
MySQL千万级别优化·中
MySQL千万级别的查询优化手段·中单列索引(假设在v\_record表中存在id列的索引)1、WHERE条件使用​EXPLAINSELECT\FROMv\_recordWHEREid2​结论:利用索引进行回表查询2、SELECT字段使用
Wesley13 Wesley13
4年前
mysql千万级大数据SQL查询优化
1.对查询进行优化,应尽量避免全表扫描,首先应考虑在where及orderby涉及的列上建立索引。2.应尽量避免在where子句中对字段进行null值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:selectidfromtwherenumisnull可以在num上设置默认值0,确保表中num列没有
Easter79 Easter79
4年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
4年前
mysql5.6 分页查询优化
mysql5.6分页查询优化场景:表结构:主键(非自增)contentCode(varchar),过滤条件列为updateTime(timeStamp),已经为timestamp建立索引。搜索sql为:SELECTFROMmy_hello_tableWHEREupdat
Stella981 Stella981
4年前
SQLite里的正反向索引index使用问题记录
在SQLite里面似乎只要使用了正向或者反向索引关键字,查找的时候就无法使用索引了,比如:CREATEINDEX"idx_items"ON"items"("category_id"ASC);在使用如下语句查询的时候就会扫描全表SELECTid,titleFROMitemsWHEREcategory_id5;
Easter79 Easter79
4年前
SQLite里的正反向索引index使用问题记录
在SQLite里面似乎只要使用了正向或者反向索引关键字,查找的时候就无法使用索引了,比如:CREATEINDEX"idx_items"ON"items"("category_id"ASC);在使用如下语句查询的时候就会扫描全表SELECTid,titleFROMitemsWHEREcategory_id5;
Stella981 Stella981
4年前
ELK学习笔记之ElasticSearch的索引详解
0x00ElasticSearch的索引和MySQL的索引方式对比Elasticsearch是通过Lucene的倒排索引技术实现比关系型数据库更快的过滤。特别是它对多条件的过滤支持非常好,比如年龄在18和30之间,性别为女性这样的组合查询。倒排索引很多地方都有介绍,但是其比关系型