mysql IS NULL使用索引案例讲解

简介

mysql的sql查询语句中使用is null、is not null、!=对索引并没有任何影响,并不会因为where条件中使用了is null、is not null、!=这些判断条件导致索引失效而全表扫描。

mysql官方文档也已经明确说明is null并不会影响索引的使用。

MySQL can perform the same optimization on col_name IS NULL that it can use for col_name = constant_value. For example, MySQL can use indexes and ranges to search for NULL with IS NULL.

事实上,导致索引失效而全表扫描的通常是因为一次查询中回表数量太多。mysql计算认为使用索引的时间成本高于全表扫描,于是mysql宁可全表扫描也不愿意使用索引。

案例

CREATE TABLE `user_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(11) DEFAULT NULL,
  `age` int(4) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_name` (`name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `user_info` (`id`, `name`, `age`) VALUES ('1', 'tom', '18');
INSERT INTO `user_info` (`id`, `name`, `age`) VALUES ('2', null, '19');
INSERT INTO `user_info` (`id`, `name`, `age`) VALUES ('3', 'cat', '20');

执行sql查询时使用is null、is not null,发现依然使用的索引查询,并没有出现索引失效的问题。

分析

分析上述现象,则需要详细了解mysql索引的工作原理以及索引数据结构。下面,分别通过工具解析和直接查看二进制文件两种方式分别分析mysql索引数据结构。

工具解析

innodb_ruby是一个非常强大的mysql分析工具,可以用来轻松解析mysql的.ibd文件进而深入理解mysql的数据结构。

首先安装innodb_ruby工具:

yum install -y rubygems ruby-deve
gem install innodb_ruby

innodb_ruby的功能很多,此处我们只需要用来解析mysql的索引结构,因此只需要如下的命令即可。更多的功能和命令详见wiki

innodb_space -s ibdata1 -T sakila/film -I PRIMARY index-recurse

解析主键索引:

$ innodb_space -s /usr/soft/mysql-5.6.31/data -T test/user_info -I PRIMARY index-recurse
ROOT NODE #3: 3 records, 89 bytes
  RECORD: (id=1) → (name="tom", age=18)
  RECORD: (id=2) → (name=:NULL, age=19)
  RECORD: (id=3) → (name="cat", age=20)

解析普通索引index_name:

$ innodb_space -s /usr/soft/mysql-5.6.31/data -T test/user_info -I index_name index-recurse
ROOT NODE #4: 3 records, 38 bytes
  RECORD: (name=:NULL) → (id=2)
  RECORD: (name="cat") → (id=3)
  RECORD: (name="tom") → (id=1)

通过解析工具数据mysql的索引结构可以发现,null值也被储存到了索引树中,并且null值被处理成最小的值放在index_name索引树的最左侧。

二进制文件

找到user_info表对应的物理文件user_info.ibd,通过软件例如UltraEdit打开,直接定位到第5个数据页(mysql默认一个数据页占用16KB)。

如图,这些二进制数据就是index_name索引对应的索引页数据,只挑选其中的索引记录,展开如下:

最小记录0x00010063

01 B2 01 00 02 00 29 	记录头信息
69 6E 66 69 6D 75 6D 	最小记录(固定值infimum)

最大记录0x00010070

00 04 00 0B 00 00 		记录头信息
73 75 70 72 65 6D 75 6D 最大记录(固定值supremum)

ID为1的索引0x0001007f

03 00 00 00 10 FF F1 	记录头信息
74 6F 6D 				字段name的值:tom
80 00 00 01 			RowID:主键id的值为1

ID为2的索引0x0001008c

01 00 00 18 00 0B 		记录头信息
						字段name的值:null
80 00 00 02				RowID:主键id的值为2

ID为3的索引0x00010097

03 00 00 00 20 FF E8 	记录头信息
63 61 74 				字段name的值:cat
80 00 00 03 			RowID:主键id的值为3

最小记录的记录头信息最后2字节00 29 -> 0x00010063偏移0x0029 -> 0x0001008C,即ID为2的索引位置;

ID为2的记录头信息最后2字节00 0B -> 0x0001008C偏移0x000B -> 0x00010097,即ID为3的索引位置;

ID为3的记录头信息最后2字节FF E8 -> 0x00010097偏移0xFFE8 -> 0x0001007F,即ID为1的索引位置;

ID为1的记录头信息最后2字节FF F1 -> 0x0001007F偏移0xFFF1 -> 0x00010070,最大记录的记录位置;

由此可见索引记录是通过单向链表并以索引值排序串联在一起,而null值被处理成最小的值放在了索引链表的最开始位置,也就是索引树的最左侧。与innodb_ruby工具解析出来的结果一致。

误解原因

为何大众误解认为is null、is not null、!=这些判断条件会导致索引失效而全表扫描呢?

导致索引失效而全表扫描的通常是因为一次查询中回表数量太多。mysql计算认为使用索引的时间成本高于全表扫描,于是mysql宁可全表扫描也不愿意使用索引。使用索引的时间成本高于全表扫描的临界值可以简单得记忆为20%左右。

详细的分析过程可以见笔者的另一篇博客:mysql回表致索引失效。

也就是如果一条查询语句导致的回表范围超过全部记录的20%,则会出现索引失效的问题。而is null、is not null、!=这些判断条件经常会出现在这些回表范围很大的场景,然后被人误解为是这些判断条件导致的索引失效。

复现索引失效

复现索引失效,只需要回表范围超过全部记录的20%,如下插入1000条非null记录。

delimiter  //
CREATE PROCEDURE init_user_info()
BEGIN
	DECLARE indexNo INT;
	SET indexNo = 0;
	WHILE indexNo < 1000 DO
		START TRANSACTION;
			insert into user_info(name,age) values (concat(floor(rand()*1000000000)),floor(rand()*100));
			SET indexNo = indexNo + 1;
		COMMIT;
	END WHILE;
END //
delimiter ;
call init_user_info();

此时user_info表中一共有1003条记录,其中只有1条记录的name值为null。那么is null判断语句导致的回表记录只有1/1003不会超过临界值,而is not null判断语句导致的回表记录有1002/1003远远超过临界值,将出现索引失效的现象。

由下两图也可以见,is null依然正常使用索引,而is not null如预期由于回表率太高而宁可全表扫描也不使用索引。

使用mysql的optimizer tracing(mysql5.6版本开始支持)功能来分析sql的执行计划:

SET optimizer_trace="enabled=on";
explain select * from user_info where name is not null;
SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE;

optimizer tracing输出的执行计划可见,该查询下,使用全表扫描所需要的时间成本为206.9;而使用索引所需要的时间成本为1203.4,远远高于全表扫描。因此mysql最终选择全表扫描而出现索引失效的现象。

{
    "rows_estimation": [
        {
            "table": "`user_info`",
            "range_analysis": {
                "table_scan": {
                    "rows": 1004,   // 全表扫描需要扫描1004条记录
                    "cost": 206.9   // 全表扫描需要的成本为206.9
                },
                "potential_range_indices": [
                    {
                        "index": "PRIMARY",
                        "usable": false,
                        "cause": "not_applicable"
                    },
                    {
                        "index": "index_name",
                        "usable": true,
                        "key_parts": [
                            "name",
                            "id"
                        ]
                    }
                ],
                "setup_range_conditions": [],
                "group_index_range": {
                    "chosen": false,
                    "cause": "not_group_by_or_distinct"
                },
                "analyzing_range_alternatives": {
                    "range_scan_alternatives": [
                        {
                            "index": "index_name",
                            "ranges": [
                                "NULL < name"
                            ],
                            "index_dives_for_eq_ranges": true,
                            "rowid_ordered": false,
                            "using_mrr": false,
                            "index_only": false,
                            "rows": 1002,   // 索引需要扫描1002条记录
                            "cost": 1203.4, // 索引需要的成本为1203.4
                            "chosen": false,
                            "cause": "cost"
                        }
                    ],
                    "analyzing_roworder_intersect": {
                        "usable": false,
                        "cause": "too_few_roworder_scans"
                    }
                }
            }
        }
    ]
}

到此这篇关于mysql IS NULL使用索引案例讲解的文章就介绍到这了,更多相关mysql IS NULL使用内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • MySQL中IF()、IFNULL()、NULLIF()、ISNULL()函数的使用详解

    在MySQL中可以使用IF().IFNULL().NULLIF().ISNULL()函数进行流程的控制. 1.IF()函数的使用 IF(expr1,expr2,expr3),如果expr1的值为true,则返回expr2的值,如果expr1的值为false,则返回expr3的值. SELECT IF(TRUE,'A','B'); -- 输出结果:A SELECT IF(FALSE,'A','B'); -- 输出结果:B 2.IFNULL()函数的使用 IFNULL(expr1,expr2),如果

  • 浅谈SQLServer的ISNULL函数与Mysql的IFNULL函数用法详解

    SQL Serve的ISNULL函数: ISNULL(check_expression,replacement_value) 1.check_expression与replacement_value的数据类型必须一致. 2.如果check_expression为NULL,则返回replacement_value. 3.如果check_expression为NULL,则返回check_expression. Mysql的IFNULL函数: IFNULL(expr1,expr2) 如果expr1不是

  • MySql中的IFNULL、NULLIF和ISNULL用法详解

    今天用到了MySql里的isnull才发现他和MSSQL里的还是有点区别,现在简单总结一下: mysql中isnull,ifnull,nullif的用法如下: isnull(expr) 的用法: 如expr 为null,那么isnull() 的返回值为 1,否则返回值为 0. mysql> select isnull(1+1); -> 0 mysql> select isnull(1/0); -> 1 使用= 的null 值对比通常是错误的. isnull() 函数同 is nul

  • mysql IS NULL使用索引案例讲解

    简介 mysql的sql查询语句中使用is null.is not null.!=对索引并没有任何影响,并不会因为where条件中使用了is null.is not null.!=这些判断条件导致索引失效而全表扫描. mysql官方文档也已经明确说明is null并不会影响索引的使用. MySQL can perform the same optimization on col_name IS NULL that it can use for col_name = constant_value.

  • MySQL中NULL对索引的影响深入讲解

    前言 看了很多博客,也听过很多人说,包括我们公司的DBA,说MySql中如果某一列中含有null,那么包含该列的索引就无效了. 翻了下<高性能MySQL第二版>和<MySQL技术内幕--InnoDB存储引擎第2版>,并没有看到关于这个的介绍.但在本地试了下,null列是可以用到索引的,不管是单列索引还是联合索引,但仅限于is null,is not null是不走索引的. 后来在官方文档中找到了说明,如果某列字段中包含null,确实是可以使用索引的,地址:https://dev.m

  • mysql回表致索引失效案例讲解

    简介 mysql的innodb引擎查询记录时在无法使用索引覆盖的场景下,需要做回表操作获取记录的所需字段. mysql执行sql前会执行sql优化.索引选择等操作,mysql会预估各个索引所需要的查询代价以及不走索引所需要的查询代价,从中选择一个mysql认为代价最小的方式进行sql查询操作.而在回表数据量比较大时,经常会出现mysql对回表操作查询代价预估代价过大而导致索引使用错误的情况. 案例 示例如下,在5.6版本的mysql.1CPU2G内存的Linux环境下,新建一个测试表,并创建将近

  • MySQL非空约束(not null)案例讲解

    目录 在创建表时设置非空约束 在修改表时添加非空约束 删除非空约束 MySQL 非空约束(NOT NULL)指字段的值不能为空.对于使用了非空约束的字段,如果用户在添加数据时没有指定值,数据库系统就会报错.可以通过 CREATE TABLE 或 ALTER TABLE 语句实现.在表中某个列的定义后加上关键字 NOT NULL 作为限定词,来约束该列的取值不能为空. 比如,在用户信息表中,如果不添加用户名,那么这条用户信息就是无效的,这时就可以为用户名字段设置非空约束. 在创建表时设置非空约束

  • PHP之mysql位运算案例讲解

    位运算,赋值状态时异或对应位数1的整形,判断状态则与运算对应位数1的整形.最大用处就是同时判断32位状态,节省存储空间,便于扩展,  如果你不知道什么是位运算的话, 那么请你先去看看基础的C语言教程吧. 与运算 a & b  , 或运算 a | b ,  异或运算 a ^ b , 或者 你也可以将 与运算理解为 + 法  例如 1|2 = 3   (1+2 = 3) 1|2|4 = 7 (1+2+4 = 7) 将 异或运算理解为 - 法 例如 3^2 = 1 (3-2 = 1) 3^1 = 2

  • MySQL外键约束(FOREIGN KEY)案例讲解

    MySQL 外键约束(FOREIGN KEY)是表的一个特殊字段,经常与主键约束一起使用.对于两个具有关联关系的表而言,相关联字段中主键所在的表就是主表(父表),外键所在的表就是从表(子表). 外键用来建立主表与从表的关联关系,为两个表的数据建立连接,约束两个表中数据的一致性和完整性.比如,一个水果摊,只有苹果.桃子.李子.西瓜等 4 种水果,那么,你来到水果摊要买水果就只能选择苹果.桃子.李子和西瓜,其它的水果都是不能购买的. 主表删除某条记录时,从表中与之对应的记录也必须有相应的改变.一个表

  • MySQL之权限以及设计数据库案例讲解

    权限及设计数据库 用户管理 使用SQLyog 创建用户,并授予权限演示 基本命令 /* 用户和权限管理 */ ------------------ 用户信息表:mysql.user -- 刷新权限 FLUSH PRIVILEGES -- 增加用户 CREATE USER kuangshen IDENTIFIED BY '123456' CREATE USER 用户名 IDENTIFIED BY [PASSWORD] 密码(字符串) - 必须拥有mysql数据库的全局CREATE USER权限,或

  • Mysql简易索引方案讲解

    目录 Mysql简易索引 一.没有索引的时候如何查找 在一个页中查找 在很多页中查找 二.一个简易索引 1. 下一页用户记录的主键值必须大于上一页的 2. 给所有的页建立一个目录项 三.简易索引暴露出的问题 Mysql简易索引 一.没有索引的时候如何查找 先忽略掉索引这个概念,如果现在直接要查某条记录,要如何查找呢? 在一个页中查找 如果表中的记录很少,一个页就够放,那么这时候有 2 种情况: 用主键为搜索条件:这时就是之前文章提过的方式,页面目录中用二分法快速定位到槽,然后遍历该槽对应分组的记

  • Mysql表的约束超详细讲解

    目录 约束的概念 空属性 默认值 列描述 zerofill 主键 自增长 唯一键 外键 约束的概念 约束:通过限制用户操作的方式,来达到维护数据本身安全,完整性的一套方案. 为什么要有约束? Mysql是一套整体的数据存储解决方案,除了解决数据存储功能,还要保证数据的安全,减少用户的误操作. 表的约束有很多,主要介绍:null/not null,default, comment, zerofill,primary key, auto_increment,unique key . 空属性 数据库默

  • mysql中null(IFNULL,COALESCE和NULLIF)相关知识点总结

    本文实例讲述了mysql中null(IFNULL,COALESCE和NULLIF)相关知识点.分享给大家供大家参考,具体如下: 在MySQL中,NULL值表示一个未知值,它不同于0或空字符串'',并且不等于它自身. 我们如果将NULL值与另一个NULL值或任何其他值进行比较,则结果为NULL,因为一个不知道是什么的值(NULL值)与另一个不知道是什么的值(NULL值)比较,其值当然也是一个不知道是什么的值(NULL值). 然而我们通常,使用NULL值来表示数据丢失,未知或不适用的情况. 例如,潜

随机推荐