这竟是索引的“锅”!

2019 年 9 月 22 日

这竟是索引的“锅”!

最近在开发异地交易可视化项目中,为了做不同城市系统的兼容,需要获取当前所属的城市编码来区分不同appId,利用框架查询表apps结果,与偶然自己手写sql查询表数据对比,发现两次查询结果竟然不一致。


查询过程


第一条 SQL(框架查询):


select * from apps limit 1;
复制代码



第二条 SQL(自己查询):


select id from apps limit 1;
复制代码



注:后文分别简称以上查询为:SQL1、SQL2


探奥索隐


为了一探究竟,查阅一下表数据及相关索引如下:



索引如下:



表中存在两条数据记录,索引分别为 id 列的主键索引和 company_code 列的唯一索引。


首先,来看看 MySQL 本身是如何解释的?


explain select * from apps limit 1;
复制代码



explain select id from apps limit 1;
复制代码



由此可见,第一条没有用到索引,按主键默认排序,SQL1 取到了第一条;第二条用到了索引 uniq_company_code,按照索引排序,SQL2 取到了第二条。


【总结一下】:根据 select 的字段不同,MySQL 选取的查询策略不同,导致了不同的展示结果。


看起来这个总结可以解释上面的问题了,但是存在几个疑惑点:


为什么 SQL2 中并没有出现 company_code 字段,却会使用索引 uniq_company_code?


为什么 SQL1 中不会使用索引 uniq_company_code?


援疑质理


在回答以上问题之前,我们先了解一下 MySQL 常用表引擎的实现方式。


MySQL 服务器逻辑架构图 如下所示:



最下层的存储引擎负责 MySQL 中数据的存储和提取。


示例表如下所示:



不同表引擎索引的实现:



由此,我们应该可以得到如下结论 :


因为索引uniq_company_code中包含id字段,SQL2 可以从索引uniq_company_code中直接取得数据,所以优化器选择走索引uniq_company_code;

而 SQL1 中select * 选取了在索引uniq_company_code中不包含的列,所以无法使用索引uniq_company_code。


为了验证上面的结论,进一步进行试验:


假如查询字段包含了 company_name(索引 uniq_compamy_code 中不包含的字段),则应该无法使用此索引。


explain select id, company_name from apps limit 1;
复制代码



至此,验证了索引覆盖的问题(compamy_name 不在索引 uniq_compamy_code 索引覆盖范围内,导致无法使用其索引)。


那么,MySQL 为什么要使用索引覆盖呢?MySQL 是下面这么解释的:


It is possible that key will name an index that is not present in the possible_keys value. This can happen if none of the possible_keys indexes are suitable for looking up rows, but all the columns selected by the query are columns of some other index. That is, the named index covers the selected columns, so although it is not used to determine which rows to retrieve, an index scan is more efficient than a data row scan .


主要原因:假如索引覆盖包含了所选取的字段,会优先使用索引覆盖,因为效率更快。


不是所有类型的索引都可以成为覆盖索引,覆盖索引必须要存储索引列的值,而哈希索引、空间索引和全文索引等都不存储索引列的值,所以 MySQL 只能使用 B-Tree 索引做覆盖索引。另外,不同的存储引擎实现覆盖索引的方式也不同,而且不是所有的引擎都支持索引覆盖。


既然主键索引列包含所有的数据列,那么主键索引一样可以做到索引覆盖,那优化器为什么不选择使用主键索引呢?


在 MySQL v5.1.46 的优化器在对 index 选择上做了一点改动,具体描述如下:


Performance: While looking for the shortest index for a covering index scan, the optimizer did not consider the full row length for a clustered primary key, as in InnoDB. Secondary covering indexes will now be preferred, making full table scans less likely。
复制代码


该版本中增加了 find_shortest_key(),该函数的作用是可以认为选择最小的 key_length 的索引来满足我们的查询。


find_shortest_key() 函数注释如下:


As far as clustered primary key entry data set is a set of all record fields (key fields and not key fields) and secondary index entry data is a union of its key fields and primary key fields (at least InnoDB and its derivatives don’t duplicate primary key fields there, even if the primary and the secondary keys have a common subset of key fields), then secondary index entry data is always a subset of primary key entry. Unfortunately, key_info[nr].key_length doesn’t show the length of key/pointer pair but a sum of key field lengths only, thus we can’t estimate index IO volume comparing only this key_length value of secondary keys and clustered PK. So, try secondary keys first, and choose PK only if there are no usable secondary covering keys or found best secondary key include all table fields (i.e. same as PK)
复制代码


主要原因:辅助索引总是主键的子集,从节约 IO 的角度,优先选择辅助索引。


为了验证一下主键索引同样可以做索引覆盖,我们将索引 uniq_company_code 删除,然后看看 SQL2 如何解释:


explain select id from apps limit 1;
复制代码



总结


1、没有查询条件时全表扫描,如果可以用到索引,则索引顺序决定结果顺序


SQL2 用到了索引 uniq_company_code,结果按字段 company_code 顺序返回结果;SQL1 没有用到索引,按默认主键 id 顺序返回结果。


2、辅助索引包含主键索引字段


索引 uniq_company_code 为辅助索引,叶子节点包含 company_code 和 id 字段,所以 SQL2 可以使用这个索引,但 SQL1 不能。


3、索引覆盖


如果索引的叶子节点中已经包含要查询的数据,那么查询只需要在索引文件上进行,不需要再进行回表查询。


4、主键索引和辅助索引都可以覆盖查询列,为何优先使用辅助索引(辅助索引占用空间少,优先使用辅助索引可以节约 IO)


主键索引 id 和辅助索引 uniq_company_code 两个索引都可以覆盖查询列,优先使用辅助索引 uniq_company_code.


凡事讲究个实锤,这竟是索引的“锅”,哈哈


作者介绍:


张欢,交易研发部,17 年 9 月加入链家,任职 PHP 资深研发工程师,目前负责交易 C 端 &知识库 &指标数据方向的研发工作。


本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。


原文链接:


https://mp.weixin.qq.com/s/3xjEfTnmOZL1qdnst1PXyg


2019 年 9 月 22 日 22:43148

评论

发布
暂无评论
发现更多内容

深入计算机底层,从几本靠谱的书开始

HackMSF

计算机工作原理

ARTS-1

你当像鸟飞往你的山

ARTS 打卡计划

像孩子一样认识新事物 —— 读《终身幼儿园》

YoungZY

学习 读书笔记 读书

我常用的浏览器插件

彭宏豪95

chrome 效率工具 浏览器 插件

Flutter开发环境配置

玉龙BB

flutter android vscode

线程池续:你必须要知道的线程池submit()实现原理之FutureTask!

一枝花算不算浪漫

源码分析 并发编程

奈学:传授“带权重的负载均衡实现算法”独家设计思路

奈学教育

分布式

ARTS-01

NIMO

ARTS 打卡计划 ARTS活动

那些会阻碍程序员成长的细节[2]

码闻强

程序员 程序人生

ARTS week 3

刘昱

5G时代下应用的安全防御研究

Nick

5G 5G网络安全 5G安全

# LeetCode 863. All Nodes Distance K in Binary Tree

liu_liu

算法 LeetCode

你不知道的SSD那些事

焱融科技

分布式 存储 SSD nvme

眼中有码,心中无码

小眼睛聊技术

学习 深度思考 程序员 最佳实践 算法

数据产品经理实战-数据门户搭建(上)

第519区

数据中台 开发数据

我为什么开始技术写作?

flyer0126

技术创作

# LeetCode 215. Kth Largest Element in an Array

liu_liu

算法 LeetCode

美团可能会强势涉足 ToB

罗小布

创业 互联网巨头 深度思考 互联网

Mysql索引不会怎么办?6000字长文教会你

Super~琪琪

MySQL 数据库 sql 索引

重学 Java 设计模式:实战单例模式

小傅哥

设计模式 编程思维 重构 优化代码

关爱孩子的心理建设

Neco.W

人生 感悟 教育

如何做好Code Review?

flyer0126

Code Review

ARTS - Week Two

shepherd

js algorithm

DDD 中的那些模式 — 使用 Specification 管理业务规则

Joshua

设计模式 领域驱动设计 DDD 架构模式

Vue生态篇(一)

shirley

Java Vue

每个人都是领导者的工程团队

hongfei

知识也会生宝宝?

史方远

个人成长 随笔杂谈

爬虫框架Scrapy应用实践-淘宝保险频道数据抓取【2】-抓包分析

hadesxiong

Python 爬虫 保险 Scrapy

MySQL 可重复读,差点就我背上了一个 P0 事故!

楼下小黑哥

Java MySQL

我的编程之路 -6(新时代)

顿晓

android 编程之路 时代

【Java 25周年有奖征文获奖名单公布!!!】关于Java,你最想赞扬、吐槽、期待的变化是什么?

InfoQ写作平台

写作平台 Java25周年 活动专区

这竟是索引的“锅”!-InfoQ