Qida's Blog

纸上得来终觉浅,绝知此事要躬行。

UNIX 时间戳

unix_timestamp

时间戳问题

Y2K (Year 2000 problem)

Y2K

Y2K 是一个合成词汇:Y = Year,2 = 2,K= Kilo,因此 Y2K 的含义其实就是千禧之年 ——2000 年

https://en.wikipedia.org/wiki/Year_2000_problem

Y2K38 (Year 2038 problem)

2038 年问题又叫 Unix 千年虫或 Y2K38 问题。在时间值以带符号的 32 位整数来存储或计算的数据存储情况下,这个错误就有可能引发问题。

下面这个动画显示了 Y2K38 问题将如何重置日期:

Y2K38

这是因为:用 Unix 带符号的 32 位整数时间格式来表示的最大时间是 2038 年 1 月 19 日 03:14:07UTC(2038-01-19T03:14:07Z),这是自 1970-01-01T00:00:00Z 之后过了 2147483647 秒,值的边界如下:

时间 时间戳 二进制字面量
1970-01-01T00:00:00Z 0 00000000 00000000 00000000 00000000
2038-01-19T03:14:07Z 2^31-1, 2147483647 01111111 11111111 11111111 11111111

测试代码:

1
2
3
4
5
6
7
8
9
// 0
long a = 0;
// 2^31-1, 2147483647
long b = Integer.MAX_VALUE;

// 1970-01-01T00:00:00.000Z
Instant.ofEpochSecond(a).atZone(ZoneOffset.of("-00:00")).toLocalDateTime()
// 2038-01-19T03:14:07.000Z
Instant.ofEpochSecond(b).atZone(ZoneOffset.of("-00:00")).toLocalDateTime()

过了最大时间后,由于整数溢出,时间值将作为负数来存储,系统会将日期读为 1901 年 12 月 13 日,而不是 2038 年 1 月 19 日。

用简单的语言来说,Unix 机器最终将会耗尽存储空间来列举秒数。所以,到那一天,使用标准时间库的 C程序会开始出现日期问题。你可以在维基百科上详细阅读更多的相关内容:

目前,2038年错误没有什么通行的解决方案。如果对用于存储时间值的time_t数据类型的定义进行更改,依赖带符号的32位time_t整数性质的应用程序就会出现一些代码兼容性问题。假设time_t的类型被更改为不带符号的32位整数,那将加大最新的时间限制。但是,这会对由负整数表示的1970年之前的日期造成混乱。

使用64位架构的操作系统和程序使用64位time_t整数。使用带符号的64位值可以将日期延长至今后的2920亿年。

已有人提出了许多建议,包括以带符号的64位整数来存储自某个时间点(1970年1月1日或2000年1月1日)以来的毫秒/微秒,以获得至少30万年的时间范围。其他建议包括用新的库重新编译程序,等等。这方面的工作正在开展之中;据专家们声称,2038年问题解决起来应该不难。

各种开发语言获取当前时间戳

Java

java.time.Instant

1
2
3
4
5
6
7
// 秒时间戳
Instant.now().getEpochSecond()
// 毫秒时间戳
Instant.now().toEpochMilli()

// 毫秒时间戳
System.currentTimeMillis()

Javascript

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date

1
Math.round(new Date() / 1000)

Shell

1
date +%s

MySQL

UNIX_TIMESTAMP([date])

1
SELECT UNIX_TIMESTAMP()

其它语言:…

参考

UNIX时间 - 维基百科

Y2K problem

Y2K38 problem

Y2K22 problem

计算机时间到底是怎么来的?程序员必看的时间知识!

定义

MySQL 原生支持外键(即允许跨表交叉引用相关数据)和外键约束(用于保持数据一致性!)。

外键关系涉及包含初值的父表,以及引用父表值的子表。而外键约束就定义在子表之上。

A foreign key relationship involves a parent table that holds the initial column values, and a child table with column values that reference the parent column values. A foreign key constraint is defined on the child table.

语法

CREATE TABLEALTER TABLE 语句中定义外键约束的基本语法如下:

1
2
3
4
5
6
7
8
[CONSTRAINT [fk_symbol]] FOREIGN KEY
[index_name] (col_name, ...)
REFERENCES tbl_name (col_name,...)
[ON DELETE reference_option]
[ON UPDATE reference_option]

reference_option:
RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT

删除外键约束:

1
ALTER TABLE tbl_name DROP FOREIGN KEY fk_symbol;

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 创建父表
CREATE TABLE `t_parent` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='父表';

# 创建子表
CREATE TABLE `t_child` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`parent_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `fk_parent_id` (`parent_id`),
CONSTRAINT `fk_parent_id` FOREIGN KEY (`parent_id`) REFERENCES `t_parent` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='子表';

可视化界面如下:

foreign_key_constraint

注意:

  • 创建外键约束时,如果主外键之间的数据类型不一致(例如长度、无符号),会报错:1215 - Cannot add the foreign key constraint

  • 创建外键约束后,MySQL 会为子表自动创建普通索引 fk_parent_id,以提升 join 查询性能。

  • 创建外键不一定只能引用父表的主键,也能引用普通列。如果引用普通列,MySQL 则会在父表和子表同时为该列创建普通索引。如果删除该索引会报错:1553 - Cannot drop index '...': needed in a foreign key constraint

  • reference_option 的几种情况总结如下:

    • 操作父表:

      • RESTRICTUPDATE 或者 DELETE 父表记录时,对子表进行一致性检查
      • CASCADEUPDATE 或者 DELETE 父表记录时,对子表进行级联操作
      • SET NULLUPDATE 或者 DELETE 父表记录时,对子表进行 SET NULL 操作
      RESTRICT (NO ACTION) CASCADE SET NULL
      INSERT 正常插入 正常插入 正常插入
      UPDATE 更新父表值,会报错 1451 - Cannot delete or update a parent row: a foreign key constraint fails 更新父表值,子表值级联更新 更新父表值,子表值 SET NULL
      DELETE 删除父表行,会报错 1451 - Cannot delete or update a parent row: a foreign key constraint fails 删除父表行,子表行级联删除 删除父表行,子表值 SET NULL
    • 操作子表:

      • INSERTUPDATE 触发一致性检查。
      RESTRICT (NO ACTION) CASCADE SET NULL
      INSERT 无论哪个 option,插入子表行为父表不存在的值,都会报错 1452 - Cannot add or update a child row: a foreign key constraint fails
      UPDATE 同上
      DELETE 无论哪个 option,删除子表行都 ok

总结

primary_key_and_foreign_key

参考

https://dev.mysql.com/doc/refman/5.7/en/create-table-foreign-keys.html

https://mp.weixin.qq.com/s/jOF1rohb6OvA3Pb5rL6Ilg

OAuth 协议解决了以下问题:

  • 密码泄露风险
  • 无法控制授权范围、有效期

OAuth 协议中,术语“授权类型(Grant Types)”是指应用获取“访问令牌(Access Token)”的方式。OAuth 2.0 定义了以下几种授权类型:

几种授权类型都有其对应的使用场景,各有利弊,但目的都是为了获取访问令牌。访问令牌是一个用于访问已授权资源的临时凭据。

商户在接入认证服务器之前,需要先申请一套专用的 client_idclient_secret,据此再申请 access_token。下表总结了其中三种主流授权类型下,申请 access_token 令牌的前置条件:

授权方式 grant_type 授权的前置条件 描述
授权码模式 authorization_code 授权码 这种模式是最常见、功能最完整、流程最严密的授权模式,第三方应用需要先获取授权码,才能申请到令牌。它的特点就是通过第三方应用的后台服务器,与“服务提供商”的认证服务器进行互动,通过授权码(authorization_code)交换访问令牌(access_token,第三方应用不接触用户密码,安全性高。
密码模式 password 用户的账号、密码 这种模式通常用在用户对该应用高度信任的情况下,或者所有服务都由同一家公司提供。在这种模式下,用户必须把自己的用户名和密码发给应用。应用使用这些信息,再向“服务提供商”索要授权,其风险在于应用获知了密码。但应用无需存储密码,而是存储和使用令牌即可。
密码模式还有一种主流的变种,即使用用户手机号和短信验证码申请令牌,相比密码模式会更安全些。为了区分,可以自定义一个 grant_typesms_verify_code
客户端模式 client_credentials 第三方应用以自己的名义,而不是以用户的名义,向“服务提供商”进行认证,并获取商户类资源,而不是用户类资源。

授权码模式

OAuth 旨在让用户能够对第三方应用授予有限的访问权限。第三方应用首先需要确定所需的权限,然后将用户导向浏览器以获得其授权。简单回顾下:

OAuth2 Authorization Code Flow

要开始授权流程,第三方应用需要先构建 URL。这里附一张流程图,详细总结下授权码模式的整个流程:

OAuth 2.0 授权码模式

报文如下:

OAuth 2.0 授权码模式

交互方式上特别注意浏览器会进行几次 302 重定向。流程总结如下:

  1. 第三方应用首先向服务提供商申请 client_id 应用唯一标识、client_secret 密钥,用于后续获取令牌时提供身份校验;
  2. 获取授权码:此时要提供预分配好的 client_id 标识来源,提供 scope 标识要申请的权限,提供 redirect_uri 标识授权完毕后要回跳的第三方应用的链接。
  3. 第一次 302 重定向:认证服务器展示登录授权页。
  4. 第二次 302 重定向:在用户提交授权,认证服务器认证成功后,会分配授权码 code,并重定向回第三方应用的 redirect_uri
  5. 建议第三方应用要根据当前用户会话生成随机且唯一的 state 参数,并在接收到授权码时先进行校验,避免 CSRF 攻击。
  6. 最后,第三方应用会向认证服务器申请令牌 access_token,此时要提供预分配好的 client_idclient_secretcode以便认证。这一步是在后端之间完成的,对用户不可见。
  7. access_token 是有有效期的,过期后需要刷新。
  8. 拿到令牌 access_token 后,第三方应用就可以访问资源方,获取所需资源。access_token 相当于用户的 session id。

以微信公众平台为例,微信网页授权 就是使用了“授权码模式(grant_type=authorization_code)”。商户在完成接入并获取用户access_token 之后,可用于如下场景:

  • 获取用户 openid
  • 获取用户信息(如昵称、头像、性别、所在地)
  • ……

简化模式

简化了授权码模式,不再需要授权码(Authorization Code)换令牌(Access Token),而是 Authorization Server 直接返回令牌(Access Token)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
+--------+                                           +---------------+
| |--(A)------- Authorization Grant --------->| |
| | | |
| |<-(B)----------- Access Token -------------| |
| | & Refresh Token | |
| | | |
| | +----------+ | |
| |--(C)---- Access Token ---->| | | |
| | | | | |
| |<-(D)- Protected Resource --| Resource | | Authorization |
| Client | | Server | | Server |
| |--(E)---- Access Token ---->| | | |
| | | | | |
| |<-(F)- Invalid Token Error -| | | |
| | +----------+ | |
| | | |
| |--(G)----------- Refresh Token ----------->| |
| | | |
| |<-(H)----------- Access Token -------------| |
+--------+ & Optional Refresh Token +---------------+

密码模式

报文如下:

OAuth 2.0 密码模式

客户端模式

客户端授权模式用于请求商户资源,而不是用户资源,报文如下:

OAuth 2.0 客户端模式

以微信公众平台为例,获取公众号的 access_token 就是使用了“客户端模式(grant_type=client_credential)”。商户在完成接入并获取应用access_token 之后,可用于如下场景:

  • 自定义菜单的配置
  • 消息推送
  • 素材管理
  • 用户管理
  • 帐户管理
  • 数据统计
  • …… 大量服务

参考

https://oauth.net/2/

RFC 6749 - The OAuth 2.0 Authorization Framework - IETF

RFC 6750 - The OAuth 2.0 Authorization Framework: Bearer Token Usage

What the Heck is OAuth?

https://developer.linkedin.com/zh-cn/docs/oauth2

http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html

移花接木:针对OAuth2的CSRF攻击

微信网页授权(授权码模式)

典型技术:基本认证、摘要认证、消息认证码、JWT、单点登录(CAS 流程、OpenID)

基本认证(Basic Access Authentication)

https://en.wikipedia.org/wiki/Basic_access_authentication

在 HTTP 用户代理(如:网页浏览器)请求时,提供用户名和密码的一种方式。

HTTP 请求头会包含 Authorization 字段,形式如下: Authorization: Basic <凭证>,该凭证是 Base64("username:password")

最初,基本认证是定义在 HTTP 1.0 规范(RFC 1945)中,后续的有关安全的信息可以在 HTTP 1.1 规范(RFC 2616)和 HTTP 认证规范(RFC 2617)中找到。于 1999 年 RFC 2617 过期,于 2015 年的 RFC 7617 重新被定义。

Basic_Access_Authentication

摘要认证(Digest Access Authentication)

https://en.wikipedia.org/wiki/Digest_access_authentication

摘要认证是一种比基本认证更安全的认证方式:

It applies a hash function to the username and password before sending them over the network. In contrast, basic access authentication uses the easily reversible Base64 encoding instead of hashing, making it non-secure unless used in conjunction with TLS.

Technically, digest access authentication is an application of MD5 cryptographic hashing with usage of nonce values to prevent replay attacks. It uses the HTTP protocol.

摘要认证最初由 RFC 2069 中被定义。RFC 2069 大致定义了一个传统的由服务器生成随机数(nonce)来维护安全性的摘要认证架构。

RFC 2069 随后被 RFC 2617 取代。RFC 2617 引入了一系列安全增强的选项。

Digest_Access_Authentication

消息认证码(Message Authentication Code)

https://en.wikipedia.org/wiki/Message_authentication_code

In cryptography, a message authentication code (MAC), sometimes known as a tag, is a short piece of information used for authenticating a message. In other words, to confirm that the message came from the stated sender (its authenticity) and has not been changed. The MAC value protects a message’s data integrity, as well as its authenticity, by allowing verifiers (who also possess the secret key) to detect any changes to the message content.

MAC

HMAC

What is an HMAC?

A hash-based message authentication code (HMAC, 散列消息认证码) is a type of message authentication code involving:

HMAC

If any change is made to the data being sent, the resulting HMAC will be completely different from the original. Additionally, since the key is known only to the sender and the receiver, no valid HMAC can be regenerated by anyone else.

HMAC 也是一种摘要认证方式,但相比上述两种认证方式仅保证用户的真实性(Authenticity),HMAC 还能同时保证传输数据的:

  • 完整性(Integrity)
  • 真实性(Authenticity)
  • 不可抵赖性(Non-repudiation)

HMAC 使用场景 —— HTTP 请求参数校验

All you need to do is take the HTTP request body and apply the SHA-256 hash function to it, using the secret key as the hash key. You then compare the resulting HMAC to the one contained in the Signature header:

  • If the HMACs are identical, then the data corresponds to what sender sent.
  • If they are different, this indicates that the data has been intercepted and altered in some way.

Java 使用例子:https://www.baeldung.com/java-hmac

JSON Web Token

https://oauth.net/2/jwt/

https://jwt.io/

JWT 登录认证及 token 自动续期方案解读

一个例子

场景:

  • 订单表数据量:3000 万。
  • 查询最近 7 天的订单,并做分页、分片。

表结构:

1
2
3
4
5
6
7
8
9
CREATE TABLE `t_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_no` varchar(50) NOT NULL,
...
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`) USING BTREE,
KEY `idx_create_time` (`create_time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8

超多分页场景如下:

1
2
3
4
5
6
7
8
9
explain select * from t_order 
where create_time between '2019-10-17' and '2019-10-25'
limit 1000000, 10;

+----+-------------+---------+-------+-----------------+-----------------+---------+------+---------+-----------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+-------+-----------------+-----------------+---------+------+---------+-----------------------+
| 1 | SIMPLE | t_order | range | idx_create_time | idx_create_time | 5 | NULL | 3775048 | Using index condition |
+----+-------------+---------+-------+-----------------+-----------------+---------+------+---------+-----------------------+

虽然走了 idx_create_time 索引,但仍然是个慢查询,扫描行数过多。

超多分页的问题是什么?

随着偏移量 offset 的增加,MySQL 需要花费大量的时间来扫描需要丢弃的数据。本质上就是 offset 过大导致的大量回表 I/O 查询。

如果能减少这种大量的回表查询,就能提升查询性能。

概念介绍

什么是延迟关联优化?

什么是“延迟关联”?

通过使用覆盖索引查询返回需要的主键,再根据主键关联原表获得需要的数据。

参考两个材料:

《高性能 MySQL》P194:

deferred_join_1

《阿里巴巴 Java 开发手册》:

deferred_join_2

什么是覆盖索引?

查询的列被所建的辅助索引所覆盖,无需回表:

1
2
3
4
5
6
7
8
9
explain select id from t_order 
where create_time between '2019-10-17' and '2019-10-25'
limit 1000000, 10;

+----+-------------+---------+-------+-----------------+-----------------+---------+------+---------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+-------+-----------------+-----------------+---------+------+---------+--------------------------+
| 1 | SIMPLE | t_order | range | idx_create_time | idx_create_time | 5 | NULL | 3775048 | Using where; Using index |
+----+-------------+---------+-------+-----------------+-----------------+---------+------+---------+--------------------------+

上述查询字段改为 id 后,执行计划中新增: Extra=Using index,表示走覆盖索引,无需回表,查询速度快了 N 倍。

延迟关联优化

延迟关联优化涉及到了 SQL 优化的两个重要概念:覆盖索引和回表。

  • 通过覆盖索引在辅助索引上完成所有扫描、过滤、排序(利用索引有序)和分页;
  • 最后通过主键回表查询,最大限度减少回表查询的 I/O 次数。

SQL 改造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
explain select * from t_order t 
inner join (
select id from t_order
where create_time between '2019-10-17' and '2019-10-25'
limit 1000000, 10
) e
on t.id = e.id;

+----+-------------+---------------+--------+-----------------+-----------------+---------+------+---------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------------+--------+-----------------+-----------------+---------+------+---------+--------------------------+
| 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 1000010 | NULL |
| 1 | PRIMARY | t | eq_ref | PRIMARY | PRIMARY | 8 | e.id | 1 | NULL |
| 2 | DERIVED | t_order | range | idx_create_time | idx_create_time | 5 | NULL | 3775048 | Using where; Using index |
+----+-------------+---------------+--------+-----------------+-----------------+---------+------+---------+--------------------------+

优化前:

  1. 辅助索引查询,得到 id
  2. id 逐一回表查询(1000000 + 10 次回表)
  3. 查询结果放弃前 offset 行,返回 limit 行

优化后:

  1. 辅助索引覆盖查询,得到 id
  2. 查询结果放弃前 offset 行,返回 limit 行
  3. 只需 10 条 id 回表查询,大大减少回表查询的 I/O 次数

参考

https://dev.mysql.com/doc/refman/5.7/en/derived-tables.html

http://mysql.taobao.org/monthly/2017/03/05/

join

连接语法

join

join

join

join

内连接

1
2
3
4
5
6
7
8
9
# 简单的等值语法创建内联结
SELECT vend_name, prod_name, prod_price
FROM Vendors, Products
WHERE Vendors.vend_id = Products.vend_id;

# ANSI SQL 规范首选 INNER JOIN 语法创建内联结
SELECT vend_name, prod_name, prod_price
FROM Vendors INNER JOIN Products
ON Vendors.vend_id = Products.vend_id;

在内连接两个表时,实际要做的是将第一个表中的每一行与第二个表中的每一行配对,WHEREON 子句作为过滤条件,只包含那些匹配连接条件的行

由没有连接条件的表关系返回的结果为笛卡儿积(cartesian product)。检索出的行的数目将是第一个表中的行数乘以第二个表中的行数。因此应当总是提供连接条件。

外连接(左、右)

许多连接将一个表中的行与另一个表中的行相关联,但有时候需要包含没有关联行的那些行,例如:

  • 对每个顾客下的订单进行计数,包括那些至今尚未下订单的顾客;

  • 列出所有产品以及订购数量,包括没有人订购的产品;

  • 计算平均销售规模,包括那些至今尚未下订单的顾客。

在上述例子中,连接包含了那些在相关表中没有关联行的行。这种连接称为外连接。

例如,要检索出所有顾客+订单,包括那些还未下单的顾客:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT Customers.cust_id, Orders.order_num
FROM Customers LEFT OUTER JOIN Orders
ON Customers.cust_id = Orders.cust_id;

->
cust_id order_num
---------- ---------
1000000001 20005
1000000001 20009
1000000002 NULL
1000000003 20006
1000000004 20007
1000000005 20008

上例如果使用内连接,将不包含 1000000002 顾客,因为他还未下单(即连接条件不匹配)。

作为对比,下例使用内连接 INNER JOIN 和聚集函数 COUNT() 统计出所有顾客的订单数:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT Customers.cust_id, COUNT(Orders.order_num) AS num_ord
FROM Customers INNER JOIN Orders
ON Customers.cust_id = Orders.cust_id
GROUP BY Customers.cust_id;

->
cust_id num_ord
---------- --------
1000000001 2
1000000003 1
1000000004 1
1000000005 1

但如果使用左外连接 LEFT OUTER JOIN 和聚集函数 COUNT() 进行相同统计,将会包括那些还未下单的顾客,例如顾客 1000000002

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT Customers.cust_id, COUNT(Orders.order_num) AS num_ord
FROM Customers LEFT OUTER JOIN Orders
ON Customers.cust_id = Orders.cust_id
GROUP BY Customers.cust_id;

->
cust_id num_ord
---------- -------
1000000001 2
1000000002 0
1000000003 1
1000000004 1
1000000005 1

由于 COUNT(column) 计数会忽略 NULL 值,因此顾客 1000000002 的统计结果为 0。

注意,左、右外连接之间的唯一差别是所关联的表的顺序。换句话说,调整 FROMWHERE 子句中表的顺序,左外连接可以转换为右外连接。因此,这两种外连接可以互换使用,哪个方便就用哪个。

连接算法

MySQL 使用下面两种算法执行表连接:

  • 嵌套循环连接算法(Nested-Loop Join(NLJ)),在被驱动表 join 字段有索引时使用。
  • 基于块的嵌套循环连接算法(Block Nested-Loop Join(BNL)),在被驱动表 join 字段无索引时使用,以减少被驱动表的全表扫描次数。

NLJ

Nested-Loop Join (NLJ) :

A simple nested-loop join (NLJ) algorithm reads rows from the first table in a loop one at a time, passing each row to a nested loop that processes the next table in the join. This process is repeated as many times as there remain tables to be joined.

例如,使用以下 join type 执行 t1t2t3 三个表之间的表连接:

1
2
3
4
Table   Join Type
t1 range
t2 ref
t3 ALL

使用 NLJ 算法,则按以下方式处理连接:

1
2
3
4
5
6
7
for each row in t1 matching range {
for each row in t2 matching reference key {
for each row in t3 {
if row satisfies join conditions, send to client
}
}
}

BNL

BNL 算法将外层循环的检索行缓存到 join_buffer(无序数组)中,以减少内层循环的全表扫描次数。例如,如果外层循环先将 10 行数据读入缓冲区,并将其传递给下一个内层循环,内层循环只需全表扫描一次,即可将读取到的每一行与缓冲区中的所有 10 行在内存中进行比较。这将使得内层循环表的全表扫描次数减少一个数量级。

MySQL join buffer 具有以下特征:

使用 BNL 算法,伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for each row in t1 matching range {
for each row in t2 matching reference key {
store used columns from t1, t2 in join buffer
if buffer is full {
for each row in t3 {
for each t1, t2 combination in join buffer {
if row satisfies join conditions, send to client
}
}
empty join buffer
}
}
}

if buffer is not empty {
for each row in t3 {
for each t1, t2 combination in join buffer {
if row satisfies join conditions, send to client
}
}
}

S 为每行 used columns from t1, t2 的大小, C 为其行数,则 t3 全表扫描的次数为:

1
2
3
4
t3_scanned_count = 
(S * C) % join_buffer_size == 0 ?
(S * C) / join_buffer_size :
(S * C) / join_buffer_size + 1

因此,随着 join_buffer_size 增加,t3 全表扫描的次数反比减少,直到 join_buffer_size=(S * C) 时则无法再优化。

参考

JOIN Syntax

Optimizing SELECT Statements

排序流程

四种排序情况的流程(参考《极客时间》专栏):

order_by_process

order_by_process

总结

排序总结:

order_by

外部排序总结:

  • MySQL 会给每个线程分配一块内存用于排序,称为 sort_buffer。对 sort_buffer 中的数据按照排序字段做快速排序
  • 排序可能在内存中完成,也可能需要使用磁盘排序,取决于排序所需的内存和参数 sort_buffer_size
  • 内存放不下时,使用外部磁盘排序,外部磁盘排序一般使用归并排序算法。可以简单理解,MySQL 将需要排序的数据分成 N 份,每一份单独排序后存在这些临时文件中。然后把这 N 个有序文件再合并成一个有序的大文件。

优化方式:

  • WHEREORDER BY 子句用到的字段,添加联合索引(注意字段顺序);
  • 如果 GROUP BY 结果无需排序,可以加上 ORDER BY NULL

参考

https://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html

索引优化应该是对查询性能优化最有效的手段了。索引能够轻易将查询性能提高几个数量级,“最优”的索引有时比一个“好的”索引性能要好两个数量级。

索引是存储引擎用于快速找到记录的一种数据结构。在 MySQL 中,索引是在存储引擎层而不是服务器层实现的(如下图)。所以,并没有统一的索引标准:不同存储引擎的索引的工作方式并不一样,也不是所有的存储引擎都支持所有类型的索引。即使多个存储引擎支持同一种类型的索引,其底层的实现也可能不同。

MySQL Architecture

常见索引类型

MySQL 支持的常见索引类型:

  • B+Tree 索引
  • Hash index(哈希索引)
  • R-Tree index(空间数据索引)
  • Full-text index(全文索引)
  • ……

本文只探讨最常用的 B-Tree 索引。

B+Tree 数据结构

B+Tree 索引特性

当我们谈论索引的时候,如果没有特别指明类型,那多半说的是 B+Tree 索引。InnoDB 存储引擎默认使用的也是 B+Tree 数据结构来存储数据。
索引可以包含一个或多个列的值。如果包含多个列(称为“联合索引”),那么列的顺序至关重要,因为 MySQL 只能高效地使用索引的 最左前缀列。创建一个包含两个列的索引,和创建两个只包含一列的索引是大不相同的,下面看个例子:

1
2
3
4
5
6
7
CREATE TABLE People (
last_name varchar(50) not null,
first_name varchar(50) not null,
dob date not null,
gender enum('m', 'f') not null,
key(last_name, first_name, dob)
);

可以使用该 B-Tree 索引的查询类型:

全值匹配

全值匹配(Match the full value):

1
2
3
SELECT * 
FROM people
WHERE last_name = 'Wu' AND first_name = 'Qida' AND dob = '2018-01-01';

匹配最左前缀

匹配最左前缀(Match a leftmost prefix):

1
2
3
4
5
6
7
SELECT * 
FROM people
WHERE last_name = 'Wu';

SELECT *
FROM people
WHERE last_name = 'Wu' AND first_name = 'Qida';

匹配列前缀

匹配列前缀(Match a column prefix):

1
2
3
SELECT * 
FROM people
WHERE last_name LIKE 'W%';

注意 MySQL LIKE 的限制:

MySQL can’t perform the LIKE operation in the index. This is a limitation of the low-level storage engine API, which in MySQL 5.5 and earlier allows only simple comparisons (such as equality, inequality, and greater-than) in index operations.

MySQL can perform prefix-match LIKE patterns in the index because it can convert them to simple comparisons, but the leading wildcard in the query makes it impossible for the storage engine to evaluate the match. Thus, the MySQL server itself will have to fetch and match on the row’s values, not the index’s values.

匹配范围值

匹配范围值(Match a range of values):

1
2
3
SELECT * 
FROM people
WHERE last_name BETWEEN 'Wu' AND 'Li';

精确匹配某一列,并范围匹配另外一列

精确匹配某一列,并范围匹配另外一列(Match one part exactly and match a range on another part):

1
2
3
SELECT * 
FROM people
WHERE last_name = 'Wu' And first_name LIKE 'Q%';

覆盖索引

只访问索引列的查询(Index-only queries):

1
2
3
SELECT last_name, first_name, dob
FROM people
WHERE last_name = 'Wu' AND first_name = 'Qida' AND dob = '2018-01-01';

高性能的索引策略

选择合适的索引列顺序

联合索引列按区分度从高到低排列。例如 idx(ctime_status) 而不是 idx(status_ctime)

使用独立的列

“独立的列”是指不在索引列上做任何操作,包括:

例如,下面这个查询无法使用 actor_id 列的索引:

1
2
3
4
5
6
7
8
9
-- 错误示范
SELECT *
FROM people
WHERE id + 1 = 5;

-- 正确示范
SELECT *
FROM people
WHERE id = 4;

凭肉眼很容易看出 WHERE 中的表达式其实等价于 id = 4 ,但是 MySQL 无法自动解析这个方程式。这完全是用户行为。我们应该养成简化 WHERE 条件的习惯,始终将索引列单独放在比较符号的一侧。

下面是另一个常见的错误,将索引列作为函数的参数:

1
2
3
4
5
6
7
-- 错误示范
SELECT ...
WHERE DATE(create_time) = '2000-01-01';

-- 正确示范
SELECT ...
WHERE create_time BETWEEN '2000-01-01 00:00:00' AND '2000-01-01 23:59:59';

另一个常见错误,merchant_no 为 VARCHAR 类型且加了索引,但由于隐式类型转换为数字类型,导致全表扫描:

1
2
3
4
5
6
7
8
9
-- 错误示范
SELECT *
FROM t_order
WHERE merchant_no = 2016;

-- 正确示范
SELECT *
FROM t_order
WHERE merchant_no = '2016';

字符串索引优化

常规方式

  • 直接创建完整索引
    • 优点:可以使用覆盖索引
    • 缺点:比较占用空间
  • 创建前缀索引
    • 优点:节省空间
    • 缺点:
      • 需要计算好区分度,以定义合适的索引长度,否则会增加回表次数
      • 无法使用覆盖索引

区分度计算方法:

1
2
3
4
5
6
select 
count(distinct left(email,4)) / count(distinct email) as L4,
count(distinct left(email,5)) / count(distinct email) as L5,
count(distinct left(email,6)) / count(distinct email) as L6,
count(distinct left(email,7)) / count(distinct email) as L7,
from t1;

其它方式一

  • 倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题。查询时使用 reverse 函数。
  • 创建额外的 hash 字段并创建索引,查询性能稳定(散列冲突的概率更小),有额外的存储和计算消耗,查询时使用 crc32 函数并二次比较。

这两种方式都不支持范围扫描,只支持等值查询。

其它方式二

改为使用更合适的数据类型,例如:

  • 使用日期与时间类型,而不是字符串来存储日期和时间。
  • 使用整型,而不是字符串来存储 IP 地址。
  • 使用定长二进制类型(如 binary),而不是字符串来存储散列值。

索引选择性

了解两个概念:

  • 基数(Cardinality)也称为区分度,是指数据列所包含的不同值的数量。例如,某个数据列包含值:1、2、3、4、5、1,则基数为 5。可以通过 show index 查看。
  • 索引选择性(Index Selectivity)是指基数(Cardinality)和数据表的记录总数(#T)的比值,范围从 1/#T1 之间。

索引的选择性越高则查询效率越高,因为选择性高的索引可以让 MySQL 在查找时过滤掉更多的行。唯一索引的选择性是 1,这是最好的索引选择性,性能也是最好的。

下面显示如何计算某列的平均选择性

1
2
3
4
SELECT COUNT(DISTINCT last_name) / COUNT(*) AS Selectivity
FROM people;

Selectivity: 0.0312

只看平均选择性有时是不够的,需考虑最坏或特殊情况下的选择性(即值的分布,是否有某些值占比过多?)。

如果索引的选择性低(基数/总数的比值),可能会导致优化器生成执行计划时,不走这个索引。

有时 MySQL 会选错索引,解决方案如下:

  • 通过 show index 语句查看索引的“基数”。对于由于索引统计信息不准确导致的问题,可以用 analyze table 来重新统计索引信息。
  • force index 强行选择一个索引。
  • 修改语句,引导 MySQL 使用我们期望的索引。
  • 新建一个更合适的索引,来提供给优化器做选择,或删掉误用的索引。

三星索引评价系统

评估某个索引是否适合某个查询的“三星评价系统”(three-star system):

Lahdenmaki and Leach’s book also introduces a three-star system for grading how suitable an index is for a query:

  • The index earns one star if it places relevant rows adjacent to each other,
  • a second star if its rows are sorted in the order the query needs,
  • and a final star if it contains all the columns needed for the query.
  • 一星:索引列满足查询所需的条件。如果是多个查询条件,则利用联合索引及其最左前缀匹配特性。
  • 二星:索引行排序符合查询所需的排序,没有额外的 ORDER BY(避免 filesort)。
  • 三星:索引列满足查询所需的全部列,不再需要回表查询(即利用覆盖索引 covering index)。

相关命令

下面介绍一些查看索引的常用命令:

DESC

DESC 命令查看表结构时,可以看到索引列 Key ,共有三种类型:

  • PRI 表示主键索引(PRIMARY KEY)。
  • UNI 表示唯一索引(UNIQUE KEY),值不能重复。
  • MUL 表示普通索引(MULTIPLE KEY) ,值可重复。
1
2
3
4
5
6
7
8
9
10
11
$ DESC table_name;

+---------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------+--------------+------+-----+---------+-------+
| FID | int(11) | NO | PRI | NULL | |
| FKEY | varchar(50) | NO | UNI | NULL | |
| FVALUE | varchar(500) | YES | MUL | NULL | |
| FDESC | varchar(50) | YES | MUL | NULL | |
| FCACHED | int(1) | NO | | 1 | |
+---------+--------------+------+-----+---------+-------+

SHOW INDEX

SHOW INDEX 可以以列为单位,查看该表索引的具体信息,例如:

  • 表名 Table
  • 索引唯一性 Non_unique
  • 索引名 Key_name
  • 联合索引中的顺序 Seq_in_index
  • 列名 Column_name
  • 基数 Cardinality
  • 索引类型 Index_type
1
2
3
4
5
6
7
8
9
10
$ SHOW INDEX FROM table_name;

+------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| table_name | 0 | PRIMARY | 1 | FID | A | 105 | NULL | NULL | | BTREE | | |
| table_name | 0 | UK_FKEY | 1 | FKEY | A | 105 | NULL | NULL | | BTREE | | |
| table_name | 1 | IDX_V_D | 1 | FVALUE | A | 105 | NULL | NULL | | BTREE | | |
| table_name | 1 | IDX_V_D | 2 | FDESC | A | 105 | NULL | NULL | | BTREE | | |
+------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+

SHOW CREATE TABLE

SHOW CREATE TABLE 可以查看该表的建表语句,留意最后几行:

1
2
3
4
5
6
7
8
9
10
11
12
SHOW CREATE TABLE table_name;

CREATE TABLE `table_name` (
`FID` int(11) NOT NULL,
`FKEY` varchar(50) NOT NULL,
`FVALUE` varchar(200) NOT NULL,
`FDESC` varchar(50) DEFAULT NULL,
`FCACHED` int(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`FID`),
UNIQUE KEY `UK_FKEY` (`FKEY`),
KEY `IDX_V_D` (`FVALUE`, `FDESC`)
)

参考

《高性能 MySQL》

https://dev.mysql.com/doc/refman/5.7/en/innodb-indexes.html

https://dev.mysql.com/doc/refman/5.7/en/optimization-indexes.html

https://dev.mysql.com/doc/refman/5.7/en/show-columns.html

MySQL索引背后的数据结构及算法原理

慢 SQL 治理经典案例分享 | 阿里技术

EXPLAIN 语句提供有关 MySQL 优化器如何执行语句的信息。能够用于 SELECTDELETEINSERTREPLACEUPDATE 语句。

EXPLAINSELECT 语句中使用到的每张表输出一行信息 。它按照 MySQL 在处理 SELECT 语句时的读取顺序来列出各张表。

MySQL 使用嵌套循环连接算法(NLJ)来解析所有的表连接(MySQL resolves all joins using a nested-loop join method)。详见另一篇。

EXPLAIN 输出列如下:

Column JSON Name Meaning
id SELECT 标识符 The SELECT identifier
select_type SELECT 类型 The SELECT type
table 引用的表名 The table for the output row
partitions 匹配的分区 The matching partitions
type 连接类型 The join type
possible_keys 可选的索引 The possible indexes to choose
key 实际选择的索引 The index actually chosen
key_len 实际所选 key 的长度 The length of the chosen key
ref 与索引比较的列 The columns compared to the index
rows 扫描行数 Estimate of rows to be examined
filtered 按表条件过滤的行百分比 Percentage of rows filtered by table condition
Extra 附加信息 Additional information

id

id 列的编号是 SELECT 的序列号,有几个 SELECT 就有几个 idid 值越大执行优先级越高,id 值相同则从上往下执行,id 值为 NULL 则最后执行。

select_type

表示查询类型是简单查询还是复杂查询。常见 SELECT 类型如下:

select_type Value Meaning
SIMPLE Simple SELECT (not using UNION or subqueries)
PRIMARY Outermost SELECT
UNION Second or later SELECT statement in a UNION
UNION RESULT Result of a UNION
SUBQUERY First SELECT in subquery
DERIVED Derived table
MATERIALIZED Materialized subquery

table

表示输出行所引用的表名,特殊情况如下:

  • <union*M*,*N*>:该行指的是 id 值为 MN 的并集。当有 UNION 时,UNION RESULTtable 列的值为 <union*M*,*N*>,表示参与并集的 id 查询编号为 MN
  • <derived*N*>:当 FROM 子句中有子查询时, table 列为 <derived*N*>,表示当前查询依赖于 id=N 的查询结果,于是先执行 id=N 的查询。
  • <subquery*N*>

type

单表查询的性能对比:const > ref > range > index > ALL。一般来说,得保证查询达到 range 级别,最好达到 ref 级别。

system

该表只有一行。是 const 连接类型的特例。

const

该表最多只有一个匹配行,该行在查询开始时读取。因为只有一行,所以优化器的其余部分可以将这一行中列的值视为常量。const 表非常快,因为它们只能读取一次。

当主键索引(PRIMARY KEY )或唯一索引(UNIQUE KEY)与常量值比较时使用 const 类型。如下:

1
2
3
4
5
6
7
8
9
10
11
SELECT * FROM tbl_name WHERE primary_key = 1;

SELECT * FROM tbl_name WHERE primary_key_part1 = 1 AND primary_key_part2 = 2;

SELECT * FROM tbl_name WHERE unique_key = '001';

SELECT * FROM tbl_name WHERE unique_key_part1 = '001' AND unique_key_part2 = '002';

SELECT * FROM ref_table,other_table
WHERE ref_table.unique_key_column = other_table.unique_key_column
AND other_table.unique_key_column = '001';

eq_ref

对于 other_table 中的每行,仅从 ref_table 中读取唯一一行。eq_ref 类型用于主键索引(PRIMARY KEY )或 NOT NULL 的唯一索引(UNIQUE KEY),且索引被表连接所使用时。除了 systemconst 类型之外,这是最好的连接类型。select_type=SIMPLE 简单查询类型不会出现这种类型。

例子:

1
2
3
4
5
6
SELECT * FROM ref_table,other_table
WHERE ref_table.unique_key_column = other_table.column;

SELECT * FROM ref_table,other_table
WHERE ref_table.unique_key_column_part1 = other_table.column
AND ref_table.unique_key_column_part2 = 1;

ref

对于 other_table 中的每行,从 ref_table 中读取所有匹配行。ref 类型用于普通索引或联合索引的最左前缀列(leftmost prefix of the key),即无法根据键值查询到唯一一行。如果使用的索引仅匹配几行结果,则也是一种很好的连接类型。

例子:

1
2
3
4
5
6
7
8
9
10
SELECT * FROM ref_table WHERE key_column = expr;

SELECT * FROM ref_table WHERE key_column_part1 = expr;

SELECT * FROM ref_table,other_table
WHERE ref_table.key_column = other_table.column;

SELECT * FROM ref_table,other_table
WHERE ref_table.key_column_part1 = other_table.column
AND ref_table.key_column_part2 = 1;

range

使用索引进行范围查询时,例如:=, <>, >, >=, <, <=, <=>, IS NULL, BETWEEN, LIKE, IN()

1
2
3
4
5
6
7
8
9
10
11
SELECT * FROM tbl_name
WHERE key_column = 10;

SELECT * FROM tbl_name
WHERE key_column BETWEEN 10 and 20;

SELECT * FROM tbl_name
WHERE key_column IN (10,20,30);

SELECT * FROM tbl_name
WHERE key_part1 = 10 AND key_part2 IN (10,20,30);

index

索引扫描,类似于 ALL 全表扫描。以下情况发生:

  • 覆盖索引(covering index)。此时 Extra 列显示 Using index。覆盖索引扫描通常比全表扫描速度更快,因为其存储空间更小。例子:

    1
    2
    3
    4
    5
    6
    7
    SELECT primary_key FROM tbl_name;

    SELECT unique_key FROM tbl_name;

    SELECT COUNT(primary_key) FROM tbl_name;

    SELECT COUNT(unique_key) FROM tbl_name;

ALL

全表扫描。此时必须增加索引优化查询。

全表扫描发生的情况如下:

  • 小表,此时全表扫描比二级索引扫描再回表的速度要快;
  • ONWHERE 子句没有可用的索引;
  • 查询的字段虽然使用了索引,但查询条件覆盖的范围太大以至于还不如全表扫描。优化方式详见:Section 8.2.1.1, “WHERE Clause Optimization”
  • 使用了区分度(cardinality)低的索引,索引扫描范围太大以至于还不如全表扫描。如果是统计不准,可以用 ANALYZE TABLE 语句优化:Section 13.7.2.1, “ANALYZE TABLE Syntax”

possible_keys

表示 MySQL 可选的索引。

如果此列为 NULL,表示 MySQL 没有可选的索引。此时,可以检查 WHERE 子句是否引用了某些适合建立索引的列,建立索引以提升查询性能。

key

表示 MySQL 实际选择的索引。

  • 如果此列为 NULL,表示 MySQL 没有找到可用于提高查询性能的索引。
  • 如果 possible_keys NOT NULL,但 key NULL,可能是因为表中数据不多,MySQL 认为索引对此查询帮助不大,选择了全表扫描。

如需强制 MySQL 使用或忽略 possible_keys 中列出的索引,可以在查询中使用 FORCE INDEXUSE INDEXIGNORE INDEX。详见:索引提示

key_len

表示 MySQL 实际选择的索引长度,单位为 Byte。如果该索引为联合索引,可用于判断 MySQL 实际使用了联合索引中的多少个字段。如果 key 列为 NULLkey_len 列也为 NULL

key_len 计算规则如下:

  • 使用 NULL 需要额外增加 1 Byte 记录是否为 NULL。并且进行比较和计算时要对 NULL 值做特别的处理,因此尽可能把所有列定义为 NOT NULL

  • 各个类型:

    • 整数类型
      • TINYINT 1 Byte
      • SMALLINT 2 Bytes
      • MEDIUMINT 3 Bytes
      • INT 4 Bytes
      • BIGINT 8 Bytes
    • 日期与时间类型
      • DATE 3 Bytes
      • TIMESTAMP 4 Bytes
      • DATETIME 8 Bytes
    • 字符串类型,实际字节存储长度取决于使用的字符集
      • 字符集(Character encoding) M L
        latin1 1 Char 1 Byte
        gbk 1 Char 2 Bytes
        utf8 1 Char 3 Bytes
        utf8mb4 1 Char 4 Bytes
      • CHAR(M):如果字符集为 utf8,则长度为 3 * M Bytes

      • VARCHAR(M):如果字符集为 utf8,则长度为 3 * M Bytes + 1 or 2 Bytes。额外 1 or 2 Byte(s) 用于存储长度。

  • 创建索引的时候可以指定索引的长度,例如:alter table test add index idx_username (username(30));。长度 30 指的是字符的个数。

  • InnoDB 索引最大长度为 767 Bytes,引自官方文档

    key_part:
    col_name [(length)] [ASC | DESC]

    index_type:
    USING {BTREE | HASH}

    Prefixes, defined by the length attribute, can be up to 767 bytes long for InnoDB tables or 3072 bytes if the innodb_large_prefix option is enabled. For MyISAM tables, the prefix length limit is 1000 bytes.

举个例子,在字符集为 utf8 的情况下,n 最大只能为 (767 - 2 (存储长度)) / 3 = 765 / 3 = 255 个字符。因此当字符串过长时,MySQL 最多会将开头 255 个字符串截取出来作为索引。一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`username` varchar(256) DEFAULT NULL,
`password` varchar(1) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_username` (`username`(255)) USING BTREE,
KEY `idx_password` (`password`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- key_len: 255 * 3 + 2 + 1 = 768 Bytes (额外增加 1 Byte 记录是否为 NULL)
mysql> explain select username from student where username = 'pete';
+----+-------------+---------+------+---------------+--------------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+------+---------------+--------------+---------+-------+------+-------------+
| 1 | SIMPLE | student | ref | idx_username | idx_username | 768 | const | 1 | Using where |
+----+-------------+---------+------+---------------+--------------+---------+-------+------+-------------+

-- key_len: 1 * 3 + 2 + 1 = 6 Bytes
mysql> explain select password from student where password = 'pete';
+----+-------------+---------+------+---------------+--------------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+------+---------------+--------------+---------+-------+------+--------------------------+
| 1 | SIMPLE | student | ref | idx_password | idx_password | 6 | const | 1 | Using where; Using index |
+----+-------------+---------+------+---------------+--------------+---------+-------+------+--------------------------+

如果使用过长的索引,例如修改了字符串编码类型、增加联合索引列,则报错如下:

1
[Err] 1071 - Specified key was too long; max key length is 767 bytes

ref

ref 显示与 key 列(实际选择的索引)比较的内容,可选值:

  • 列名
  • const:常量值
  • func:值为某些函数的结果
  • NULL:范围查询(type=range

例如联合索引如下,使用三个索引列查询时,执行计划如下(注意 key_lenref):

1
2
3
4
5
6
7
8
9
10
`channel_task_no` varchar(60) NOT NULL,
`reconciliation_code` tinyint(4) unsigned NOT NULL DEFAULT '0',
`reconciliation_status` tinyint(4) unsigned NOT NULL DEFAULT '0',
KEY `idx_taskno_rcode_rstatus` (`channel_task_no`,`reconciliation_code`,`reconciliation_status`) USING BTREE

+----+-------------+-------+------+--------------------------+--------------------------+---------+-------------------+------+-----------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+--------------------------+--------------------------+---------+-------------------+------+-----------------------+
| 1 | SIMPLE | t_xxx | ref | idx_taskno_rcode_rstatus | idx_taskno_rcode_rstatus | 184 | const,const,const | 1 | Using index condition |
+----+-------------+-------+------+--------------------------+--------------------------+---------+-------------------+------+-----------------------+

rows

表示 MySQL 认为执行查询必须扫描的行数。

对于 InnoDB 表,此数字是估计值,可能并不总是准确。

prossible_keys 存在多个可选索引时,优化器会选择一个认为最优的执行方案,以最小的代价去执行语句。其中,这个扫描行数就是影响执行代价的因素之一。扫描的行数越少,意味着访问磁盘数据的 IO 次数越少,消耗的 CPU 资源也越少。

当然,扫描行数并不是唯一的判断标准,优化器还会结合是否使用临时表、是否排序等因素进行综合判断。

所以在实践中,如果你发现 explain 的结果预估的 rows 值跟实际情况差距比较大,可以采用执行 analyze table 重新统计信息。

在数据库的慢查询日志中看到一个 rows_examined 的字段,表示这个语句执行过程中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行的时候累加的。

在有些场景下,执行器调用一次,在引擎内部则扫描了多行,因此引擎扫描行数跟 rows_examined 并不是完全相同的。

Extra

这一列显示的是额外信息。如果想要查询越快越好,需要特别留意 Extra 列是否出现以下情况:

Extra 缓冲区 大小配置 数据结构 备注
Using filesort sort_buffer sort_buffer_size 有序数组 使用了“外部排序”(全字段排序或 rowid 排序)
Using join buffer (Block Nested Loop) join_buffer join_buffer_size 无序数组 使用了“基于块的嵌套循环连接”算法(Block Nested-Loop Join(BNL))
Using temporary 临时表 小于 tmp_table_size 为内存临时表,否则为磁盘临时表(可以使用 SQL_BIG_RESULT 直接指定) 二维表结构(类似于 Map,Key-Value) 如果执行逻辑需要用到二维表特性,就会优先考虑使用临时表。例如:DISTINCTGROUP BYUNION

这三个数据结构都是用来存放 SELECT 语句执行过程中的中间数据,以辅助 SQL 语句的执行的。这些情况通常都能通过索引优化。

各种常见的重要值如下:

Using index

使用了覆盖索引。

Using where

使用 WHERE 条件过滤结果,但查询的列未被索引覆盖。

Using index condition

查询的列不完全被索引覆盖。

例如:索引下推优化(ICP)

EXPLAIN output shows Using index condition in the Extra column when Index Condition Pushdown is used. It does not show Using index because that does not apply when full table rows must be read.

  • ICP is used for the range, ref, eq_ref, and ref_or_null access methods when there is a need to access full table rows.
  • For InnoDB tables, ICP is used only for secondary indexes. The goal of ICP is to reduce the number of full-row reads and thereby reduce I/O operations. For InnoDB clustered indexes, the complete record is already read into the InnoDB buffer. Using ICP in this case does not reduce I/O.

ICP can reduce the number of times the storage engine must access the base table and the number of times the MySQL server must access the storage engine.

查询 ICP 是否开启:SELECT @@GLOBAL.optimizer_switch,注意 index_condition_pushdown 标记:

index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_intersection=on,engine_condition_pushdown=on,index_condition_pushdown=on,mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on,use_index_extensions=on,condition_fanout_filter=on,derived_merge=on

Using temporary

MySQL 需要创建一张临时表来处理查询。通常发生于查询包含 DISTINCTGROUP BYORDER BY 子句等需要数据去重的场景。出现这种情况一般是要进行优化的,首先想到的是用索引进行优化。

Using join buffer

使用 BNL 算法进行表连接。这种情况下一般考虑使用索引对被驱动表的表连接字段进行优化,以使用更高效的 NLJ 算法。

Using filesort

将用外部排序而不是索引排序,数据较少时从内存排序,否则需要在磁盘完成排序。这种情况下一般考虑使用索引进行优化。

优化参考:Section 8.2.1.14, “ORDER BY Optimization”

参考

https://dev.mysql.com/doc/refman/5.7/en/execution-plan-information.html

https://dev.mysql.com/doc/refman/5.7/en/analyze-table.html

MySQL Workbench has a Visual Explain capability that provides a visual representation of EXPLAIN output. See Tutorial: Using Explain to Improve Query Performance.