我可以和面试官多聊几句吗?只是想偷点技能过来。MySQL优化篇(基于MySQL8.0测试验证),上部分:优化SQL语句、数据库对象,MyISAM表锁和InnoDB锁问题。

MyISAM表锁和InnoDB锁问题会在第二篇发布:MySQL优化篇,我可以和面试官多聊几句吗?——MyISAM表锁和InnoDB锁问题(二)

你可以将这片博文,当成过度到MySQL8.0的参考资料。注意,经验是用来参考,不是拿来即用。如果你能看到并分享这篇文章,我很荣幸。如果有误导你的地方,我表示抱歉。

接着上一篇MySQL开发篇存储引擎的选择,上一篇用我现在眼光去看是稀烂的,每隔一段时间回顾自己的文章都感觉稀烂。此次带来的是MySQL优化篇,部分内容针对多版本进行说明。在对MySQL进行举例并使用到数据库表,大多数情况使用MySQL官方提供的sakila(模拟电影出租信息管理系统)和world数据库,类似于Oracle的scott用户。

如果没有进行特别说明,一般是基于MySQL8.0.28进行测试验证。官方文档非常具有参考意义。目前市面上针对MySQL8.0书籍还比较少,部分停留在5.6.x和5.7.x版本,但仍然具有借鉴意义。

文中会给出官方文档可以找到的参考内容,基本上在小标题末尾有提及并说明。辅助你快速定位出处,心里更有底气。如果想应对MySQL面试,我想这篇总结还是有一定的参考意义。需要有耐心看完,个人总结时参考书籍和MySQL8.0官方文档也很乏味,纯英文文档更令人头大。不懂的地方可以使用有道,结合实际测试进行理解。英语差,不应该是借口。

MySQL优化篇(一),我可以和面试官多聊几句吗?——SQL优化流程与优化数据库对象-LMLPHP

个人理解有限,难免出现错误偏差。所有测试,仅供参考

如果感觉对你起到作用,有参考意义,想获取原markdown文件。

可以访问我的个人github仓库,定期上传md文件,空余时间会制作目录链接:

目录

MySQL优化篇(一)

MyISAM表锁和InnoDB锁问题会在第二篇:MySQL优化篇(二)进行发布,篇幅太长,不便一次性全部发完。

给出sakila-db数据库包含三个文件,便于大家获取与使用:

  1. sakila-schema.sql:数据库表结构;
  2. sakila-data.sql:数据库示例模拟数据;
  3. sakila.mwb:数据库物理模型,在MySQL workbench中可以打开查看。

world-db数据库,包含三张表:city、country、countrylanguage。

只是用于用于简单测试学习,建议使用world-db

生产前

应用开发初期数据量比较小,开发人员在编写SQL语句时更加注重功能的实现(优先让程序跑起来,有money赚)。

生产后

业务体系逐渐扩张,随着生产数据量的急剧增长,部分SQL语句开始漏出疲态,暴露出性能问题(开始优化,赚更多的money)。

引发的思考

部分有问题的SQL语句成了系统性能的瓶颈,此时需要对SQL语句进行优化。

演示环境

  1. 操作系统:Windows10 and Linux for Centos7.5
  2. 使用工具:MySQL8.0自带字符命令行工具
  3. 数据库:MySQL8.0.28 and MariaDB10.5.6

正文

注意:在某些情况,你自己测试的结果可能与我演示有所不同,我省略了查询结果的部分参数。

本文侧重点在SQL优化流程以及MySQL锁问题(MyISAM和InnoDB存储引擎)。图片可能会挂,演示时尽量使用SQL查询语句返回结果进行示例。篇幅很长,因此使用markdown语法加了目录。

起初,也只是想看MySQL8.0.28有哪些变化,后面索性结合书籍和官方文档总结了一篇。花了将近两周,基本是每天完善一点,因为个人只有晚上和周末有时间总结并测试验证。如果有错别字,也请多多担待。如果你能看到并分享这篇文章,我很荣幸。如果有误导你的地方,我表示抱歉。

如果你是从MySQL5.6或者5.7版本过渡到MySQL8.0。学习之前,建议线看官方文档这一章节:1.3 What Is New MySQL8.0 。在做对比的时候,文档中带有Note标识是你应该注意的地方。比如下面这张截图:

MySQL优化篇(一),我可以和面试官多聊几句吗?——SQL优化流程与优化数据库对象-LMLPHP

与我之前一篇《MySQL8.0.28安装教程全程参考官方文档》是一样的想法,希望大家能认识到自学的重要性,以及阅读官方文档自我成长。结合有道和谷歌翻译以及自己的翻译进行理解,感觉翻译很别扭,可以对单个单词进行分析,结合自己的经验调整并符合阅读习惯。

参考文档:refman-8.0-en.pdf

参考书籍

  • 《深入浅出MySQL 第2版 数据库开发、优化与管理维护》,个人参考优化篇部分。
  • 《MySQL技术内幕InnoDB存储引擎 第2版》,个人参考索引与锁章节描述。

一、SQL优化

01 优化SQL语句流程

登录到mysql字符命令界面

mysql -uroot -p

登录时指定端口和主机地址方式:

mysql -h 192.168.245.147 -uroot -p -P 3307

使用? show帮助命令查询show status用法截取部分语法如下

? show
SHOW [GLOBAL | SESSION] STATUS [like_or_where]

MySQL优化篇(一),我可以和面试官多聊几句吗?——SQL优化流程与优化数据库对象-LMLPHP

1 通过show status查询SQL执行频率

如果不加参数,默认采用session级别,也可以加上global参数进行测试一下。

使用session与global参数区别:

  • session:当前连接统计的结果,默认为session级别;

  • global:上次数据库启动至今统计结果,需要手动那个指定global参数。

下面就列举示例进行说明,分别使用like去查询所有以及匹配CURD操作(select、insert、update、delete):

查询当前session所有统计记录,如果直接在字符命令界面去查询,共有175条记录,大多数情况会采用工具去执行:

show status LIKE 'com_%';
+-------------------------------------+-------+
| Variable_name                       | Value |
+-------------------------------------+-------+
| Com_admin_commands                  | 0     |
| Com_assign_to_keycache              | 0     |
| Com_alter_db                        | 0     |
| Com_commit    					  | 0     |
| Com_rollback              		  | 0     |
+-------------------------------------+-------+
...
175 rows in set (0.00 sec)

MySQL优化篇(一),我可以和面试官多聊几句吗?——SQL优化流程与优化数据库对象-LMLPHP

Com_xx部分参数作用说明

  1. Com_xx:代表某某语句执行次数,一般我们关心的是CURD操作(select、insert、update、delete)。
  2. Com_select:执行select操作次数,每次累加1次。
  3. Com_insert:执行insert操作次数,对于批量执行插入的insert操作只累加1次。
  4. Com_update:执行update操作次数。
  5. Com_delete:执行delete操作次数。

以上这些参数对所有存储引擎表操作均会进行累计。但也有一些参数只针对InnoDB存储引擎,累加算法有些许不同。

查询innodb参数如下,列举部分:

show status LIKE 'innodb_rows%';
+---------------------------------------+--------------------------------------------------+
| Variable_name                         | Value                                            |
+---------------------------------------+--------------------------------------------------+
| Innodb_rows_deleted                   | 0                                                |
| Innodb_rows_inserted                  | 0                                                |
| Innodb_rows_read                      | 0                                                |
| Innodb_rows_updated                   | 0                                                |
+---------------------------------------+--------------------------------------------------+
...
61 rows in set (0.00 sec)
  • InnoDB_rows_read:执行select查询返回行数。
  • InnoDB_rows_inserted:执行insert插入操作返回行数。
  • InnoDB_rows_updated:执行update更新操作返回行数。
  • InnoDB_rows_deleted:执行delete删除操作返回行数。

通过上面几个参数,可以轻松了解当前数据库应用是以插入更新为主还是查询操作为主,以及各种SQL大概执行比例是多少。

对于更新操作执行次数计数,无论是提交还是回滚都会进行累加

对于事务型应用,可以通过Com_commitCom_rollback了解事务提交与回滚情况。对回滚操作非常频繁的数据库,可能存在应用编写问题。

有几个参数便于用户了解数据库情况

show status LIKE 'conn%';
show status LIKE 'upti%';
show status LIKE 'slow_q%';
  • Connections:试图连接MySQL服务器次数。
  • Uptime:服务器工作时间。
  • Slow_queries:慢查询次数。

对优化SQL语句流程就介绍这么多,主要对关心的(CURD以及事务)各个参数熟练操作运用。

2定位执行效率较低的SQL语句

可以通过两种方式定位执行效率较低SQL语句:

  1. 使用参数:--log-slow-queries [=file_name],MySQL会将long_query_time的SQL语句日志写入文件;
  2. 使用参数show processlist:查询MySQL线程状态、是否锁表。

慢查询日志在查询结束以后才记录,在应用反映执行效率问题时查询慢查询慢查询日志并不能定位问题。可以使用show processlist,查看当前MySQL在进行的线程:线程状态、是否锁表,实时查看SQL执行状态。

3使用explain分析执行效率低的SQL语句

参考mysql8.0官方文档explain:

通过上述步骤查询到低效率SQL语句,然后使用explain或者desc命令获取MySQL如何执行查询select语句。

语法explain [SQL语句]

explain [SQL语句]
-- 例如
mysql> explain select * from sakila.city\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: city
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 600
     filtered: 100.00
        Extra: NULL

desc语法desc [SQL语句 & 表名]

world数据库是官方提供,文初有给链接。

-- 示例查询world数据库city表结构
desc world.city;
+-------------+----------+------+-----+---------+----------------+
| Field       | Type     | Null | Key | Default | Extra          |
+-------------+----------+------+-----+---------+----------------+
| ID          | int      | NO   | PRI | NULL    | auto_increment |
| Name        | char(35) | NO   |     |         |                |
| CountryCode | char(3)  | NO   | MUL |         |                |
| District    | char(20) | NO   |     |         |                |
| Population  | int      | NO   |     | 0       |                |
+-------------+----------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

-- 分析查询语句信息
mysql> desc select * from world.city\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: city
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 4046
     filtered: 100.00
        Extra: NULL

以上是对explain与desc语法的介绍,以及简单使用。侧重点不在desc,主要以explain进行说明。

接下来对各个参数进行演示说明

常见访问类型(type)

MySQL优化篇(一),我可以和面试官多聊几句吗?——SQL优化流程与优化数据库对象-LMLPHP

+------+--------+--------+------+---------+---------------+----------+
| ALL  | index  | range  | ref  | eq_ref  | const,system  |   NULL   |
+------+--------+--------+------+---------+---------------+----------+

性能天梯排行榜由左至右,依次递增

3.1、type=ALL:代表全表扫描,MySQL遍历全表匹配行。

示例:演示type为ALL执行计划

explain select * from world.city;

MySQL优化篇(一),我可以和面试官多聊几句吗?——SQL优化流程与优化数据库对象-LMLPHP

3.2、type=index:索引全扫描,MySQL遍历整个索引匹配行。

如果不清除哪一个是主键或者是index,使用desc命令查看,desc world.city

示例:演示type为index执行计划

explain select id from world.city;

MySQL优化篇(一),我可以和面试官多聊几句吗?——SQL优化流程与优化数据库对象-LMLPHP

3.3、type=range:索引范围扫描,常见于<、<=、>、>=、between等操作符。

示例:演示type为range执行计划

explain select * from world.city c where c.id<6;

MySQL优化篇(一),我可以和面试官多聊几句吗?——SQL优化流程与优化数据库对象-LMLPHP

3.4、type=ref:使用非唯一索引扫描或唯一索引的前缀扫描,返回某个单独值匹配记录行。

示例:演示type为ref执行计划

explain select * from world.city where countrycode='AFG';

MySQL优化篇(一),我可以和面试官多聊几句吗?——SQL优化流程与优化数据库对象-LMLPHP

ref往往还经常出现在join操作中

示例:演示type为ref执行计划,使用inner join内连接

 explain select * from world.city t1 inner join world.countrylanguage t2 on t1.countrycode=t2.countrycode;

MySQL优化篇(一),我可以和面试官多聊几句吗?——SQL优化流程与优化数据库对象-LMLPHP

3.5、type=eq_ref:与ref类似,区别eq_ref使用唯一索引。每个索引键值,表中只有一条匹配记录行。简单来说,在多表连接查询中使用primary key或者unique index作为关联条件

示例:演示type为eq_ref执行计划

explain select * from sakila.film t1,sakila.film_text t2 where t1.film_id=t2.film_id;

MySQL优化篇(一),我可以和面试官多聊几句吗?——SQL优化流程与优化数据库对象-LMLPHP

3.6、type=const&system:单表中最多有一条匹配行,查询速度很快。这条匹配行中其它列值可以被优化器在当前查询中当做常量来处理。例如,根据主键primary key或者唯一索引unique key进行查询。

示例:演示type为const执行计划

explain select * from world.city t where t.id=7;

MySQL优化篇(一),我可以和面试官多聊几句吗?——SQL优化流程与优化数据库对象-LMLPHP

3.7、type=NULL:MySQL不用访问表或索引,直接得到结果。

示例:演示type为NULL执行计划

explain select 1;

MySQL优化篇(一),我可以和面试官多聊几句吗?——SQL优化流程与优化数据库对象-LMLPHP

以前,只知道统计查询表使用MyISAM存储引擎非常快,但不知其原理。使用explain分析了下,看到访问类型(type)是NULL,瞬间有点明白了。下图是使用InnoDB与MyISAM存储引擎表的对比

MySQL优化篇(一),我可以和面试官多聊几句吗?——SQL优化流程与优化数据库对象-LMLPHP

个人只演示常见的几种。官方示例比较多,比如:ref_or_null、index_merge以及index_subquery等等。

你可以找到参考文档:

tips:在MySQL8.0中移除了explain extended,使用这条命令分析SQL语句会报(1064(42000))。

某种场景下,使用explain并不能满足我们需求,需要更高效定位问题,此时可以配合show profile命令联合分析。

4show profile分析SQL

查看当前MySQL版本对profile是否支持:如果是YES,代表是支持的

mysql> select @@have_profiling;
+------------------+
| @@have_profiling |
+------------------+
| YES              |
+------------------+
1 row in set, 1 warning (0.00 sec)

默认show profiling是关闭的,可以通过set命令设置session级别开启profiling:

select @@profiling;
+-------------+
| @@profiling |
+-------------+
|           0 |
+-------------+
1 row in set, 1 warning (0.00 sec)

开启profiling:设置profiling参数值为1,默认是0。

mysql> set profiling=1;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> select @@profiling;
+-------------+
| @@profiling |
+-------------+
|           1 |
+-------------+
1 row in set, 1 warning (0.00 sec)

示例

  1. 统计查询world数据库city表行记录数;
  2. 执行show profiles命令分析SQL。

统计city表记录

mysql> select count(*) from world.city;
+----------+
| count(*) |
+----------+
|     4079 |
+----------+
1 row in set (0.01 sec)

使用show profiles命令分析

示例

mysql> show profiles;
+----------+------------+---------------------------------+
| Query_ID | Duration   | Query                           |
+----------+------------+---------------------------------+
|        1 | 0.00017800 | select @@profiling              |
|        2 | 0.00115675 | select count(*) from world.city |
+----------+------------+---------------------------------+
2 rows in set, 1 warning (0.00 sec)

使用show profile for query语句可以查询到执行过程中线程更多信息:状态、消耗时间

示例:截取部分参数作为演示。

mysql> show profile for query 2;
+--------------------------------+----------+
| Status                         | Duration |
+--------------------------------+----------+
| starting                       | 0.000059 |
| Executing hook on transaction  | 0.000003 |
...
+--------------------------------+----------+
17 rows in set, 1 warning (0.01 sec)

更具上面查到的参数值,可以进一步分析是哪些影响到查询效率

更多用法请参考官方文档

SHOW PROFILE [type [, type] ... ]
[FOR QUERY n]
[LIMIT row_count [OFFSET offset]]
type: {
ALL
| BLOCK IO | CONTEXT SWITCHES | CPU 	| IPC
| MEMORY   | PAGE FAULTS      | SOURCE 	| SWAPS
}

比如从BLOCK IO(锁输入和输出操作)、CPU(用户系统CPU消耗时间)、内存等等着手分析。

判断用户CPU消耗时间可以统计数据量大一点的表:我统计这张表模拟数据为1kw条。

show profile CPU for query 1;
+--------------------------------+----------+----------+------------+
| Status                         | Duration | CPU_user | CPU_system |
+--------------------------------+----------+----------+------------+
| executing                      | 1.685893 | 5.593750 |   0.375000 |
+--------------------------------+----------+----------+------------+

5使用trace分析优化器如何选择执行计划

查看trace是否开启:OPTIMIZER_TRACE

  • enabled:默认为off。on代表开启,off代表关闭。
  • one_line:json格式显示,是否以一行显示。on代表一行显示,off代表多行显示(格式化)。
select @@OPTIMIZER_TRACE;
+-------------------------+
| @@OPTIMIZER_TRACE       |
+-------------------------+
| enabled=on,one_line=on  |
+-------------------------+

示例:临时开启trace,在字符命令行中使用,测试建议还是使用一行显示比较好。

set OPTIMIZER_TRACE="enabled=on,one_line=on";

示例

  1. 查询world数据库city(城市)表前两行记录。
  2. 然后使用trace(optimizer_trace分析)追踪。
-- 1. 查询world数据库city(城市)表前两行记录。
select * from world.city limit 0,2;
-- 2. 然后使用trace追踪。
select * from information_schema.optimizer_trace\G
*************************** 1. row ***************************
                            QUERY: select * from world.city limit 0,2
                            TRACE: {"steps": [{"join_preparation": {"select#": 1,"steps": [{"expanded_query": "/* select#1 */ select `world`.`city`.`ID` AS `ID`,`world`.`city`.`Name` AS `Name`,`world`.`city`.`CountryCode` AS `CountryCode`,`world`.`city`.`District` AS `District`,`world`.`city`.`Population` AS `Population` from `world`.`city` limit 0,2"}]}},{"join_optimization": {"select#": 1,"steps": [{"table_dependencies": [{"table": "`world`.`city`","row_may_be_null": false,"map_bit": 0,"depends_on_map_bits": []}]},{"rows_estimation": [{"table": "`world`.`city`","table_scan": {"rows": 4046,"cost": 9.375}}]},{"considered_execution_plans": [{"plan_prefix": [],"table": "`world`.`city`","best_access_path": {"considered_access_paths": [{"rows_to_scan": 4046,"access_type": "scan","resulting_rows": 4046,"cost": 413.975,"chosen": true}]},"condition_filtering_pct": 100,"rows_for_plan": 4046,"cost_for_plan": 413.975,"chosen": true}]},{"attaching_conditions_to_tables": {"original_condition": null,"attached_conditions_computation": [],"attached_conditions_summary": [{"table": "`world`.`city`","attached": null}]}},{"finalizing_table_conditions": []},{"refine_plan": [{"table": "`world`.`city`"}]}]}},{"join_execution": {"select#": 1,"steps": []}}]}
MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0
          INSUFFICIENT_PRIVILEGES: 0
1 row in set (0.00 sec)

6定位问题后采取相应优化方法

建立索引:在常用字段上建立,不常用字段(应该考虑是否建立)。

经过上述步骤第3步explain分析SQL查询语句,使用explain执行计划发现使用全表扫描(大量数据)非常耗时间。

在相应字段建立索引,然后进行分析,扫描行数明细减少,大大提高数据库访问速度。

02 索引问题

索引问题,是一个老生常谈的问题。如果是数据库优化场景,职场面试中经常被提到。

索引是在MySQL存储引擎中实现,而不是在服务器层实现。

每种存储引擎索引不一定完全相同,并不是所有存储引擎支持索引类型都一致。

以下列举几种常见索引介绍(索引存储分类)

  1. B-Tree索引:最常见的使索引类型,大部分存储引擎都支持B树索引。
  2. HASH索引:MEMORY、HEAP、NDB支持,使用场景较为简单。
  3. R-Tree索引(空间索引):空间索引是MyISAM存储引擎一个特殊索引类型,主要用于地理空间数据类型,一般使用较少。
  4. Full-text(全文索引):全文索引是MyISAM存储引擎一个特殊索引类型,主要用于全文索引。在MySQL5.6版本开始对InnoDB提供全文索引支持

注意:索引类型子句不能用于FULLTEXT(全文索引)或(在MySQL 8.0.12之前)空间索引规范。全文索引的实现依赖于存储引擎。空间索引实现为R-tree索引。

1 索引分类

几种常见的MySQL存储引擎支持索引类型

以上四种存储引擎支持索引特点对比:Primary key(主键索引),Unique(唯一索引),key(普通索引),FULLTEXT(全文索引),SPATIAL(空间索引)。

InnoDB存储引擎

MyISAM存储引擎

MEMORY存储引擎

NDB存储引擎

关于更多用法介绍,你可以找到参考内容

2MySQL如何使用索引

InnoDB存储引擎Information Schema一些视图脚本名称更新(MySQL8.0.3或者更高版本):

如果你升级到MySQL8.0.3或者更高版本:会发现与MySQL绑定的zlib库版本从版本1.2.3提升到版本1.2.11

2.1使用索引

一般情况,针对InnoDB存储引擎进行描述索引使用,因为MySQL5.5.5开始默认存储引擎是InnoDB。

InnoDB存储引擎支持索引:

  • B-tree indexs(B+树索引);
  • Full-text search indexes(全文索引):需要在MySQL5.6或者更高的版本中使用。

本不支持HASH indexs(NO Support),但InnoDB内部利用哈希索引来实现自适应哈希索引特性

B+树索引是传统意义上的索引,目前关系型数据库系统中查找最为常用和最为有效地的引。B+树索引构造类似于二叉树,根据键值(Key Value)快速查找数据。

注意:B+树中的B不是代表二叉树(binary),而是平衡树(balance),因为B+树是从平衡二叉树演化而来,但B+树也不是一个二叉树。B+树索引并不能找到一个给定键值的具体行,能找到的是被查找数据行所在页。然后数据库通过将页读到内存,再从内存中进行查找,最后得到要查找的数据。

上面简单介绍了下InnoDB存储引擎支持的索引,以及部分新特性,以及B+树索引。如果想深入理解B+树索引,可以从算法角度去分析,但不是此次内容的重点,可以私下查找文档去了解。接着讨论如何使用索引

2.2MySQL中使用索引典型场景

匹配全值(Match the whole value)。对索引中所有列都指定具体指,即索引所有列都有等值匹配条件。

mysql> explain select * from sakila.rental where rental_date='2005-05-27 07:33:54' and customer_id=134 and inventory_id=360\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: rental
   partitions: NULL
         type: const
possible_keys: uk_rental_date,idx_fk_inventory_id,idx_fk_customer_id
          key: uk_rental_date
      key_len: 10
          ref: const,const,const
         rows: 1
     filtered: 100.00
        Extra: NULL

通过观察explain输出结果,发现type=const。表示常量;字段key值为uk_rental_date,表示优化器使用索引uk_rental_date进行扫描。

匹配范围查询(March range)。对索引值能够进行范围查找。例如,查找租赁表rental中客户编号customer_id在指定范围记录:

mysql> explain select * from sakila.rental where customer_id>=366 and customer_id<=399\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: rental
   partitions: NULL
         type: range
possible_keys: idx_fk_customer_id
          key: idx_fk_customer_id
      key_len: 2
          ref: NULL
         rows: 925
     filtered: 100.00
        Extra: Using index condition

通过explain分析,发现type=range以及Extra: Using index condition。使用到范围性查找,以及索引下放操作。

匹配最左前缀(Matches the leftmost prefix)。仅仅使用到索引中的最左边列进行查找,比如在多个字段(col1、col2、col3)字段上的联合索引能够被包含col1、(col1、col2)、(col1、col2、col3)的等值查询利用到,但是不能被(col2、col3)、col2的等值查询利用到。以sakila数据库中支付(payment)表进行示例。

下面创建组合索引 idx_payment_date便于测示:

mysql> ALTER TABLE sakila.payment add index idx_payment_date(payment_date,amount,last_update);
Query OK, 0 rows affected (0.07 sec)
Records: 0  Duplicates: 0  Warnings: 0

使用explain执行分析:

mysql> explain select * from sakila.payment where payment_date='2005-06-15 21:08:46' and last_update='2005-06-15 21:08:46'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: payment
   partitions: NULL
         type: ref
possible_keys: idx_payment_date
          key: idx_payment_date
      key_len: 5
          ref: const
         rows: 1
     filtered: 10.00
        Extra: Using index condition

通过观察执行结果,发现 type=ref 以及Extra: Using index condition,根据最左匹配原则,你会发现payment_date处于索引1号位,此时扫描利用到组合索引idx_payment_date。

如果使用last_update和amount进行测试分析:

mysql> explain select * from sakila.payment where last_update='2005-06-15 21:08:46' and amount=9.99\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: payment
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 16086
     filtered: 1.00
        Extra: Using where

通过观察查询结果,发现type=ALL走全表扫描,索引没有使用到。

仅仅对索引查询(Only for index queries)。当查询列都在索引字段中,查询效率更高。

mysql> explain select last_update from sakila.payment where payment_date='2005-06-15 21:08:46' and amount=9.99\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: payment
   partitions: NULL
         type: ref
possible_keys: idx_payment_date
          key: idx_payment_date
      key_len: 8
          ref: const,const
         rows: 1
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)

Extra: Using index,意味着现在直接访问索引就足够获取到所有需要的数据,无需索引回表,Using index也是通常所说的覆盖索引扫描。只访问必须访问的数据,一般而言,减少不必要数据访问可以提高效率。

匹配列前缀(Match a column prefix ),仅仅使用索引中的第一列,并且只包含索引第一列开头一部分。例如,查询出标题是AGENT开头的电影信息。

创建列前缀索引:

mysql> create index idx_title_desc_part on sakila.film_text(title(10),description(20));
Query OK, 0 rows affected (0.03 sec)
Records: 0  Duplicates: 0  Warnings: 0

执行explain进行分析,注意:在B-tree索引中使用时,不要以通配符(%),不然索引会失效。

mysql> explain select title from sakila.`film_text` where title like 'AGENT%'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: film_text
   partitions: NULL
         type: range
possible_keys: idx_title_desc_part,idx_title_description
          key: idx_title_desc_part
      key_len: 42
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using where

分析执行计划,看到idx_title_desc_part被利用到,type=range,使用范围性查询。Extra: Using where表示优化器需要通过索引回表查询数据。

索引匹配部分精确,其它部分范围匹配(Match a part)。

mysql> explain select inventory_id from sakila.rental where rental_date='2006-02-14 15:16:03' and customer_id>=300 and customer_id<=400\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: rental
   partitions: NULL
         type: ref
possible_keys: uk_rental_date,idx_fk_customer_id
          key: uk_rental_date
      key_len: 5
          ref: const
         rows: 182
     filtered: 16.86
        Extra: Using where; Using index

上面通过explain分析,查询出出租日期(rental_date)、指定日期的客户编号(customer_id)指定范围的库存。根据type=ref,以及key=uk_rental_date,优化器建议走唯一索引。

如果列名是索引,使用column_name is null就会使用索引。

mysql> explain select * from sakila.payment where rental_id is null\G
         type: ref
possible_keys: fk_payment_rental
          key: fk_payment_rental
      key_len: 5
      	Extra: Using index condition

通过explain执行分析,查询支付表(payment)租赁编号(rental_id)字段为空的记录使用到了索引。

MySQL5.6以及更高版本支持Index Condition Pushdown (ICP)特性,索引条件下放操作,进一步优化了查询。某些情况操作下放到存储引擎。

  1. ICP可以用于InnoDB和MyISAM表,包括分区的InnoDB和MyISAM表。
  2. 当需要访问全表时,ICP用于range、ref、eq_ref和ref或null访问方法。
  3. 对于InnoDB表,ICP仅用于二级索引(次索引、辅助索引)。ICP的目标是减少全行读取的数量,从而减少I/O操作。对于InnoDB聚集索引,完整的记录已经被读取到InnoDB缓冲区。在这种情况下使用ICP不会减少I/O。
  4. 如果在虚拟列上创建二级索引,则不支持ICP。InnoDB支持在虚拟列上建立二级索引。
  5. 引用到子查询条件不能使用操作下放。
  6. 引用存储函数的条件不支持操作下放,存储引擎无法调用存储函数。
  7. 使用触发器(触发的条件),不能使用操作下放。

如下示例,查询支付表,强制使用索引查询内容。

mysql> explain select * from sakila.payment force index(fk_payment_rental) where rental_id > 1\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: payment
   partitions: NULL
         type: range
possible_keys: fk_payment_rental
          key: fk_payment_rental
      key_len: 5
          ref: NULL
         rows: 8043
     filtered: 100.00
        Extra: Using index condition
1 row in set, 1 warning (0.00 sec)

经过explain分析,看到Extra值为Using index condition,表示MySQL使用了ICP进一步优化查询,在检索时,将条件rental_id过滤操作下推到到存储引擎层来完成,可以降低不必要的IO访问。

注意:前缀限制以字节为单位,而CREATE TABLE、ALTER TABLE 和 CREATE INDEX语句中的前缀长度,被解析为非二进制字符串类型(CHAR、VARCHAR、TEXT)的字符数,和二进制字符串类型(binary、VARBINARY、BLOB)的字节数。使用多字节字符集的非二进制字符串列指定前缀长度时,请考虑这一点。

2.3存在但不能使用索引的场景

B-Tree索引可用于使用=、>、>=、<、<=或BETWEEN操作符表达式中的列做比较。如果LIKE的参数是一个不以通配符开头的常量字符串,则索引也可以用于LIKE比较。例如,下面的SELECT语句使用索引场景:

以 % 开头 LIKE 查询不能够利用B-Tree索引,执行计划中Key值为NULL表示没有使用索引。如下示例:

-- 没有利用到索引场景
mysql> explain select * from world.city where countrycode like '%A%'\G
		 type: ALL
possible_keys: NULL
          key: NULL

-- 索引生效场景
mysql> explain select * from world.city where countrycode like 'A%'\G
		 type: range
possible_keys: CountryCode
          key: CountryCode

第一种场景,使用explain执行优化分析后:key=NULL,没有利用到索引。第二种场景,以 % 结束,执行explain优化分析,明显索引起作用了,type=range,属于范围性扫描。

因为B-Tree索引结构特性,以通配符(%)开头的查询自然无法利用到索引,一般建议使用全文索引(fulltext)来解决类似问题。或者考虑利用InnoDB聚簇表特点,采用一种轻量级别解决方式:一般情况,索引比表小,扫描索引比扫描表更快。

数据类型出现隐式转换时不会使用索引,如果列类型是字符串,使用where条件记得将字符常量用引号引起来。MySQL默认将输入的常量值进行转换以后才进行检索。

如下示例

-- 场景一
mysql> explain select * from world.city where countrycode=1\G
         type: ALL
possible_keys: CountryCode
          key: NULL

-- 场景二
mysql> explain select * from world.city where countrycode='1'\G
         type: ref
possible_keys: CountryCode
          key: CountryCode
      key_len: 12

在场景二中,字符串(char)类型将1引起来,通过explain分析使用到索引。场景一中没有加引号,索引没有利用,从而走全表扫描。

复合索引场景下,如果查询条件不包含索引列最左部分,即不满足最左原则(LeftMost),不会利用到符合索引:

mysql> explain select * from sakila.payment where amount=9.99 and last_update='2006-02-15 22:12:30'\G
         type: ALL
possible_keys: NULL
          key: NULL

如果 MySQL 判断使用索引比扫描全表慢,则不会使用索引。比如,返回记录很大,但使用索引扫描更费时间,优化器更倾向于使用全表扫描,这样代价更低,效率更高。(使用Trace可以追踪更多信息,前面也提到过)

使用 OR 分割开的条件,如果OR条件前列有索引,OR后列没有索引,那么涉及到的索引都不会被利用。

mysql> explain select * from sakila.payment where customer_id=9 or amount=9.99\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: payment
         type: ALL
possible_keys: idx_fk_customer_id
          key: NULL
         rows: 16086
     filtered: 10.15
        Extra: Using where

因为 OR 后列没有索引,那么后继查询需要走全表扫描。存在全表扫描情况下,也没必要多走一次索引扫描增加磁盘I/O访问。如果前面列无索引,后面列有索引,执行结果一样走全表扫描。(在接下来的优化OR查询部分,进行了对比)

3查看索引使用情况

查看 Handler_read_key 值判断索引工作频率,基于键值读取一行的请求数。如果这个值(Handler_read_key)很高,说明您的表在查询时已经建立了适当的索引。读取一行请求数值很低,则表明增加索引改善并不明显,索引没有经常使用。

可以通过show status like 'Handler_read%'查询参数值,分析索引使用状况。

mysql> show status like 'Handler_read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 2     |
| Handler_read_key      | 74    |
| Handler_read_last     | 0     |
| Handler_read_next     | 147   |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 30    |
| Handler_read_rnd_next | 32    |
+-----------------------+-------+

初始时(索引还未工作),上述查询出默认值为零,当你使用索引后,这些参数会有变化。

Handler_read_rnd:基于固定位置读取一行的请求数。如果执行大量需要对结果进行排序的查询,则该值会很高。你可能有大量需要MySQL扫描全表的查询,或者你没有合理地使用键连接。

Handler_read_rnd_next:读取数据文件中下一行的请求数。如果要进行大量的表扫描,这个值就会很高。一般来说,这意味着您的表没有正确索引,或者说是写入查询没有利用到索引。

03 简单优化方法

对于开发人员来说,可能只需掌握简单实用的优化方法。比较复杂的优化,一般交给DBA来管理。

  1. analyze:分析表,analyze table table_name;
  2. check:检查表,check table table_name;
  3. checksum table:检查表;
  4. optimize table:优化表,同时支持MyISAM和InnoDB表。回收删除操作造成的空洞,比如回收索引。
  5. repair table:修复表,支持 MyISAM,ARCHIVE以及CSV 表。

3.1定期分析表和检查表

定期分析与检查主要有两个关键命令:

  1. analyze:分析表,analyze table table_name;
  2. check:检查表,check table table_name;

分析(analyze)表语法

ANALYZE [NO_WRITE_TO_BINLOG | LOCAL]
TABLE tbl_name [, tbl_name] ...

ANALYZE [NO_WRITE_TO_BINLOG | LOCAL]
TABLE tbl_name

UPDATE HISTOGRAM ON col_name [, col_name] ...
[WITH N BUCKETS]

ANALYZE [NO_WRITE_TO_BINLOG | LOCAL]
TABLE tbl_name
DROP HISTOGRAM ON col_name [, col_name] ...

示例分析表:可以使用官方示例库进行分析,个人使用自己创建test数据库进行测试tolove表

analyze table test.tolove;
+-------------+---------+----------+-----------------------------+
| Table       | Op      | Msg_type | Msg_text                    |
+-------------+---------+----------+-----------------------------+
| test.tolove | analyze | status   | Table is already up to date |
+-------------+---------+----------+-----------------------------+
1 row in set (0.01 sec)

总结:analyze语句用于分析存储表关键字分布,分析结果使系统得到更准确的信息,SQL生成预期执行计划。如果你感觉实际执行计划没有达到预期结果,不妨尝试执行一次分析表计划。

检查(check)表语法

CHECK TABLE tbl_name [, tbl_name] ... [option] ...
option: {
FOR UPGRADE
| QUICK	| FAST	| MEDIUM
| EXTENDED	| CHANGED
}

示例检查表:这张tolove表创建后修改为MyISAM存储引擎进行测试,数据量1kw,所以分析起来有点耗时。

tips:同时测试使用InnoDB表,数据量1kw,花了5.21sec,这里就不贴出来了。

mysql> check table test.tolove;
+-------------+-------+----------+----------+
| Table       | Op    | Msg_type | Msg_text |
+-------------+-------+----------+----------+
| test.tolove | check | status   | OK       |
+-------------+-------+----------+----------+
1 row in set (1.63 sec)

check table作用:用于检查一张或多张表是否有错误,前面提到过,同时支持MyISAM和InnoDB表。同样支持检查视图,这里不做示范,可以自行参考文档进行测试验证。

3.2定期优化表

优化(optimize )表语法

OPTIMIZE [NO_WRITE_TO_BINLOG | LOCAL]
TABLE tbl_name [, tbl_name] ...

如果已经删除了表中一大部分数据,或已经对含有可变长度行的表(例如含有:varchar、blob或者txt列的表)进行很多更改,则可以使用optimize table命令 进行优化表。

作用optimize命令可以将表中空间碎片进行合并,消除由于删除或者更新造成的空间浪费。同样支持MyISAM和InnoDB表

示例(optimize)优化表:演示的tolove表前面说过指定MyISAM存储引擎

mysql> optimize table test.tolove\G
*************************** 1. row ***************************
   Table: test.tolove
      Op: optimize
Msg_type: status
Msg_text: Table is already up to date
1 row in set (0.01 sec)

测试test表使用InnoDB存储引擎。对于InnoDB存储引擎,通过设置innodb_file_per_table参数(默认值为1),改为独立表空间模式,每个数据库每张表会生成独立ibd文件,用于存储表和索引,可以在一定程度上减轻 InnoDB表回收空间问题。此外,在删除大量数据后,可以通过alter table命令不修改表引擎方式回收不用的空间:

mysql> optimize table test.test\G
*************************** 1. row ***************************
   Table: test.test
      Op: optimize
Msg_type: note
Msg_text: Table does not support optimize, doing recreate + analyze instead
*************************** 2. row ***************************
   Table: test.test
      Op: optimize
Msg_type: status
Msg_text: OK
2 rows in set (17.85 sec)

mysql> alter table test.test engine=innodb;
Query OK, 0 rows affected (20.30 sec)
Records: 0  Duplicates: 0  Warnings: 0

注意analyzecheckoptimize以及alter table执行期间将对表进行锁定一定要注意在数据库不频繁使用期间,再进行相关操作

提到优化方法,在MySQL8.0文档中你可以找到参考内容:

04 常用SQL优化

在某种场景下,查询使用很频繁,针对查询优化确实很有必要。

但实际开发中,还会面临使用其它常用SQL,比如insert、group by、order by等等。

4.1批量(大量)插入数据

在使用load命令导入数据时,适当进行设置可以提高导入效率。

对于MyISAM表可以通过以下方式快速导入大量数据。

操作命令

ALTER TABLE tbl_name DISABLE KEYS;	-- 禁用MyISAM表非唯一索引更新
ALTER TABLE tbl_name ENABLE KEYS;	-- 开启MyISAM表非唯一索引更新

disable keys和enable keys用于开启和关闭MyISAM表非唯一索引更新。

MyISAM存储引擎默认,导入大量数据至一张空MyISAM表,默认先导入数据,然后创建索引,不用进行设置。

示例导入数据语句

load data infile 'file_name' into table tbl_name;

自行测试时,可以先手动开启非唯一索引,然后关闭非唯一索引进行对比导入时间。

通过测试关闭唯一索引,导入数据效率确实要高很多。这是对MyISAM表进行测试优化,对InnoDB类型表上述方式作用不是很大

InnoDB表导入表同样也有相应优化措施

  1. 导入数据按主键顺序排列,可以提高效率。(InnoDB表是按主键顺序排列
  2. 导入数据前执行set unique_checks=0,关闭唯一性校验;导入完成,再设置set unique_checks=1,恢复唯一性校验。从而提高导入效率。
  3. 如果应用使用自动提交(autocommit),建议导入前执行set autocommit=0,关闭自动提交。导入数据后,再设置set autocommit=1,开启自动提交,同样可以提高导入效率。

MyISAM表和InnoDB表导入数据语句是一样的。以上介绍MyISAM表和InnoDB表导入数据优化方式,可进行参考测试验证。

更多关于MyISAM表插入数据优化方法可以参考如下引用说明: 对于文档理应善于使用搜索Ctrl + f

4.2优化 INSERT、ORDER BY、GROUP BY 语句

你可以找到参考内容:

  • 13.2.6 INSERT Statement
  • 8.2.1.16 ORDER BY Optimization
  • 8.2.1.17 GROUP BY Optimization

4.2.1INSERT语句

当进行数据库INSERT操作时,可以考虑以下几种优化方式。

如果同时从同一用户表插入多行,应尽量使用多个值表的INSERT语句,这种方式大大缩减客户端与数据库之间的连接、关闭等消耗。一般情况下,比单个执行INSERT语句效率要高得多,但也分场景。下面给出一次插入多值示例:

INSERT INTO tbl_name(a,b,c) VALUES(1,2,3), (4,5,6), (7,8,9);

上述演示,指定字段。从安全角度考虑,实际开发过程中也是推荐指定字段,因为这种方式更加安全。多年前,我还是一位菜鸡开发人员,虽然现在也是一名菜鸟。当时不是很理解,为何在DAO中非要在前面指明字段。直到某天翻阅实体书籍时,才意识到。

如果从不同用户插入多行。使用到DELAYED语句,需要注意了,在MySQL5.6之前版本还没被移除,从MySQL5.6开始已经弃用。使用DELAYED之所以快,其实数据被存放在内存队列中,并没有真正写入从磁盘

注意事项DELAYED关键字计划在未来的版本中删除。延迟插入( DELAYED INSERT )和替换在MySQL 5.6中已弃用。在MySQL 8.0中,不支持DELAYED。服务器可以识别,但会忽略DELAYED关键字,将插入处理视为非延迟插入,并生成ER_WARN_LEGACY_SYNTAX_CONVERTED 警告:INSERT DELAYED is no longer supported. The statement was converted to INSERT。

可以将索引文件与数据文件在不同的磁盘上存放,建表时可以选择

如果进行批量插入,可以通过增减bulk_insert_buffer_size变量值的方法来提高速度。对MyISAM表有效,MyISAM使用一种特殊的树状缓存,使批量插入更快。 INSERT ... SELECT,INSERT ... VALUES (...),(...),...,和LOAD DATA在添加数据到非空表时。这个变量以每个线程的字节为单位限制缓存树的大小。将其设置为0将禁用此优化。默认值为8MB。

注意事项:从MySQL 8.0.14开始,设置bulk_insert_buffer_size这个系统变量的会话值是一个受限制的操作。会话用户必须具有设置受限制会话变量的权限。

当从一个文本装载一张表时,使用LOAD DATA INFILE,一般比使用INSERT语句快得多

从MySQL 8.0.19版本开始,你也可以使用INSERT…TABLE在MySQL 8.0.19及以后版本中插入一行,使用TABLE替换SELECT。

mysql> CREATE TABLE tb (i INT);
Query OK, 0 rows affected (0.02 sec)

mysql> INSERT INTO tb TABLE t;
Query OK, 3 rows affected (0.01 sec)
Records: 3  Duplicates: 0  Warnings: 0

以上演示,是将表 t 中所有记录插入到 tb 表中,与之前insert into tb select * from t用法是一样的执行效果。

4.2.2ORDER BY语句

看到ORDER BY语句,可以联想到排序方式。那么,了解一下MySQL中的排序方式。

查看world数据库中city表索引情况:此处省略掉了一些参数值,全部展示篇幅太长。

mysql> show index from city\G
*************************** 1. row ***************************
        Table: city
   Non_unique: 0
     Key_name: PRIMARY
 Seq_in_index: 1
  Column_name: ID
    Collation: A
  Cardinality: 4046
   Index_type: BTREE
      Visible: YES
   Expression: NULL
*************************** 2. row ***************************
        Table: city
   Non_unique: 1
     Key_name: CountryCode
 Seq_in_index: 1
  Column_name: CountryCode
    Collation: A
  Cardinality: 232
   Index_type: BTREE
      Visible: YES
   Expression: NULL
2 rows in set (0.01 sec)

MySQL中有两种排序方式

  1. Use of Indexes to Satisfy ORDER BY,使用using index。
  2. Use of filesort to Satisfy ORDER BY,使用filesort。

在某些情况下,MySQL可能会使用索引来满足ORDER BY子句,从而避免执行filesort操作时涉及的额外排序。第一种通过有序使用顺序扫描直接返回有序数据,这种方式在使用explain分析查询时显示Using index,无需额外排序,操作效率较高,示例如下:

mysql> explain select id from city order by id\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: city
   partitions: NULL
         type: index
possible_keys: NULL
          key: PRIMARY
      key_len: 4
          ref: NULL
         rows: 4046
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)

如果索引不能满足ORDER BY子句,MySQL执行一个filesort操作,读取表行并对它们排序。filesort在查询执行中构成一个额外的排序阶段。第二种是通过对返回数据进行排序,也是通常所说的filesort排序,所有不是通过索引直接返回结果的排序都称为filesort排序。

filesort并不代表通过磁盘文件进行排序,只是说明进行了一个排序操作,至于操作是否使用了磁盘文件或者临时表等,则取决于MySQL服务器对排序参数的设置和需要排序数据的大小。

如果结果集太大,无法装入内存,则 filesort 操作将根据需要使用临时磁盘文件。有些类型的查询特别适合于完全在内存中的filesort操作。例如,优化器可以使用filesort在内存中有效地处理,而不需要临时文件。示例:

SELECT ... FROM single_table ... ORDER BY non_index_column [DESC] LIMIT [M,]N;

以下给出使用 Using filesort 情况示例:

mysql> explain select store_id,email,customer_id from sakila.customer order by email\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: customer
   partitions: NULL
         type: index
possible_keys: NULL
          key: idx_storeid_email
      key_len: 204
          ref: NULL
         rows: 599
     filtered: 100.00
        Extra: Using index; Using filesort
1 row in set, 1 warning (0.00 sec)

注意:为了获得 filesort 操作的内存,从MySQL 8.0.12开始,优化器根据需要增量分配内存缓冲区(sort_buffer_size ),直到由排序缓冲区大小系统变量指示的大小,而不是像在MySQL 8.0.12之前那样,预先分配固定数量的排序缓冲区(sort_buffer_size )大小字节。这使用户可以将排序缓冲区大小设置为更大的值,以加快更大的排序,而不用担心小排序会占用过多的内存。(这种好处可能不会出现在Windows上的多个并发排序,因为Windows有一个弱多线程malloc。)

了解MySQL排序方式后,优化目的清晰了:尽量减少额外排序,通过索引直接返回数据

添加组合索引,然后使用explain执行测试:

mysql> ALTER TABLE sakila.customer ADD INDEX idx_storeid_email(store_id,email);
Query OK, 0 rows affected (0.04 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> explain select store_id,email,customer_id from sakila.customer where store_id=1 order by email desc\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: customer
   partitions: NULL
         type: ref
possible_keys: idx_fk_store_id,idx_storeid_email
          key: idx_storeid_email
      key_len: 1
          ref: const
         rows: 326
     filtered: 100.00
        Extra: Backward index scan; Using index
1 row in set, 1 warning (0.00 sec)

依据上面测试演示结果,可以分析出返回索引扫描。如果是在8.0之前显示有所区别,比如在MySQL5.7出现的是Extra: Using where; Using index。

查询商店编号store_id大于等于1小于等于3,按照email进行排序记录主键customer_id时,由于优化器评估使用索引idx_storeid_email进行范围扫描const最低,所以最终对索引进行扫描的结果,进行额外email逆序操作:

mysql> explain select store_id,email,customer_id from sakila.customer where store_id>=1 and store_id<=3 order by email desc\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: customer
   partitions: NULL
         type: index
possible_keys: idx_fk_store_id,idx_storeid_email
          key: idx_storeid_email
      key_len: 204
          ref: NULL
         rows: 599
     filtered: 100.00
        Extra: Using where; Using index; Using filesort
1 row in set, 1 warning (0.00 sec)

优化filesort:通过建立合适的索引减少 filesort 出现,但在某种情况下,条件限制无法让 filesort 消失,可以想办法加快 filesort 操作。如何加快,可以通过控制sort_buffer_sizemax_length_for_sort_data(max_sort_length ) 大小进行优化。

注意:对于没有使用filesort的慢ORDER BY查询,尝试将排序数据系统变量(max_length_for_sort_data)的最大长度降低到适合触发filesort的值。(将此变量值设置过高的一个症状是高磁盘活动和低CPU活动的结合。)这种技术只适用于MySQL 8.0.20之前。从8.0.20开始,排序数据的最大长度已弃用,因为优化器的更改使其过时且无效。

4.2.3GROUP BY语句

满足GROUP BY子句的最常用方法是扫描全表,并创建一个新的临时表,其中每个组中所有行都是连续的,然后使用这个临时表来发现组并应用聚合函数(如果存在的话)。在某些情况下,MySQL能够做得更好,并通过使用索引访问避免创建临时表。

GROUP BY使用索引最重要的前提条件:所有GROUP BY列引用来自同一索引的属性,并且该索引按顺序存储其键(例如,对于BTREE索引是这样,但对于HASH索引则不同)。临时表的使用是否可以被索引访问替代,还取决于查询中使用索引的哪些部分、为这些部分指定的条件以及所选的聚合函数。

访问索引执行 GROUP BY 两种扫描方式

  1. 松散索引扫描(Loose Index Scan)
  2. 密集索引扫描(Tight Index Scan)

默认情况下,MySQL对所有GROUP BY c1,c2,...字段进行排序,与查询中指定ORDER BY c1,c2,...类似。因此,如果显示包括一个相同列的ORDER BY子句,对MySQL实际执行性能没有什么影响。

如果查询包括GROUP BY,但用户想避免排序结果的消耗,则可以指定ORDER BY NULL禁止排序。如下示例:

mysql> explain select payment_date,sum(amount) from sakila.payment group by payment_date\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: payment
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 16086
     filtered: 100.00
        Extra: Using temporary
1 row in set, 1 warning (0.00 sec)

分析查询出来的结果,发现Extra: Using temporary,使用一个临时表。type=ALL,执行全表扫描。

注意:在MySQL5.7或者更低的版本中使用 ORDER BY NULL有显示优化作用,GROUP BY在特定条件下隐式排序。在MySQL8.0中不再出现这种情况,所以在最后指定 ORDER BY NULL 来抑制隐式排序,就没有必要了。但是,查询结果可能与之前的MySQL版本有所不同。要生成给定的排序顺序,请使用 ORDER BY子句。

即使在MySQL8.0中显示使用ORDER BY NULL 来抑制隐式排序,结果并没变化。但在MySQL5.7或者MariaDB10.5.6中使用时有变化,而且你会发现执行结果出现:Extra: Using temporary; Using filesort。对于filesort吗,上面也给出了简单处理方法。

mysql> explain select payment_date,sum(amount) from sakila.payment group by payment_date order by null\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: payment
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 16086
     filtered: 100.00
        Extra: Using temporary
1 row in set, 1 warning (0.00 sec)

4.3优化嵌套查询、分页查询

4.3.1嵌套查询

你可以找到参考内容:8.2.1 Optimizing SELECT Statements

MySQL4.1中开始支持SQL子查询。这个技术可以使用SELECT语句来创建一个单列的查询结果,然后将查询的结果作为过滤条件作用在另一个查询中。使子查询可以一次性完成更多逻辑上需要多个步骤才能完成的SQL操作,同时可以表面事务或者表锁死,编写相对容易。但在某些情况下,使用连接(join)效率更高,可以被替代。

示例:在顾客表查询排除支付表中的所有顾客信息,使用子查询实现。

mysql> EXPLAIN SELECT * FROM sakila.`customer` WHERE customer_id
  NOT IN(SELECT customer_id FROM sakila.`payment`)\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: customer
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 599
     filtered: 100.00
        Extra: NULL
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: payment
   partitions: NULL
         type: ref
possible_keys: idx_fk_customer_id
          key: idx_fk_customer_id
      key_len: 2
          ref: sakila.customer.customer_id
         rows: 26
     filtered: 100.00
        Extra: Using where; Not exists; Using index
2 rows in set, 1 warning (0.00 sec)

可使用join进行改进,我提供思路,用left join进行连接查询,主要以customer表为主,也是以左表为主。

EXPLAIN SELECT * FROM sakila.`customer` a LEFT JOIN sakila.`payment` b ON
a.`customer_id`=b.`customer_id` WHERE b.`customer_id` IS NULL\G

注意:当时还纳闷测试看不出index_subquery。查询后,发现在MySQL8.0.16之前可以看到type由index_subquery变为ref,而在MySQL8.0.16开始优化器调整并做优化(in和exists),与上面子查询得到结果并无区别。

连接(join)之所以效率更高,因为MySQL不需要在内存中创建临时表来完成这个逻辑上需要两个步骤完成的工作。

4.3.2分页查询

你可以找到参考内容:8.2.1.19 LIMIT Query Optimization

一般分页查询时,通过创建覆盖索引能够比较好地提高性能。一个很头痛的分页场景:limit 996,20,此时MySQL排序出前996记录后仅仅只需要返回第996到1016条记录,前996条记录会被抛弃,查询和排序代价非常高。

通过分析上述描述场景,使用explain进行分析:

mysql> explain select * from world.city limit 996,20\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: city
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 4046
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

可以看出,type=ALL,优化分析器走了全表扫描。

第一种优化思路:在索引上完成排序分页操作,最后根据关联原表查询所需要的其它列内容。

通过思考,对上面SQL语句进行调整优化:

mysql> explain select * from world.city c inner join(select id from world.city order by countrycode limit 996,20)a on c.id=a.id\G
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
  ...
         type: ALL
        Extra: NULL
*************************** 2. row ***************************
           id: 1
  select_type: PRIMARY
  ...
         type: eq_ref
        Extra: NULL
*************************** 3. row ***************************
           id: 2
  select_type: DERIVED
        table: city
   partitions: NULL
         type: index
possible_keys: NULL
          key: CountryCode
      key_len: 12
          ref: NULL
         rows: 1016
     filtered: 100.00
        Extra: Using index
3 rows in set, 1 warning (0.00 sec)

上述结果,前两页省略掉了一些内容。这种方式使MySQL扫描尽可能少的页面来提高分页效率,缺点是SQL语句变长了。

第二种优化思路:将limit查询转换成某个位置的查询,实际上是将limit m,n转换为limit n查询,只适合排序字段不会出现重复值的特定环境,能减轻分页翻页压力。如果排序字段现大量重复值,则不适合进行这种优化,因为会丢失部分记录。

注意:对于带有ORDER BY或GROUP BY和LIMIT子句的查询,优化器在默认情况下尝试选择一个有序索引,这样做会加快查询的执行速度。在MySQL 8.0.21之前,即使在使用一些其它优化,可能更快的情况下,没有办法覆盖这种行为。从MySQL 8.0.21开始,可以通过设置优化器开关(optimizer_switch)系统变量的优先排序索引(prefer_ordering_index)标志来关闭这种优化。

默认情况optimizer_switchprefer_ordering_index是开启的:

mysql> SELECT @@optimizer_switch LIKE '%prefer_ordering_index=on%'\G
*************************** 1. row ***************************
@@optimizer_switch LIKE '%prefer_ordering_index=on%': 1
1 row in set (0.00 sec)

4.4优化 OR 条件

你可以查找到参考内容:12.4.3 Logical Operators

在介绍OR条件时,可以先了解MySQL中的逻辑操作符(Logical Operators)。有如下几种:

  • AND, &&:逻辑与、并且,在两个条件中必须都满足。
  • NOT, !:否定、取反。
  • OR, ||:逻辑或、在两个条件中满足一个条件即可。
  • XOR:逻辑XOR。如果是NULL,返回NULL;如果是non-NULL,返回1;如果奇数个非零操作数,则计算结果为1,否则返回0。

示例XOR:

SELECT 1 XOR 1\G		-- return:0
SELECT 1 XOR 0\G		-- return:1
SELECT 1 XOR NULL\G		-- return:NULL
SELECT 1 XOR 1 XOR 1\G	-- return:1

对于含有OR查询的子句,如果要利用索引、则OR之间的每个条件列都必须用到索引,如果没有索引,应该考虑增加索引。

可以使用show index from tal_name语句查看表索引情况:

mysql> show index from city\G
...
Column_name: city_id
Column_name: country_id
...

然后查询存在索引的两列,并使用OR条件联合查询:

mysql> explain select * from city where city_id=6 or country_id=101\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: city
   partitions: NULL
         type: index_merge
possible_keys: PRIMARY,idx_fk_country_id
          key: PRIMARY,idx_fk_country_id
      key_len: 2,2
          ref: NULL
         rows: 4
     filtered: 100.00
        Extra: Using union(PRIMARY,idx_fk_country_id); Using where
1 row in set, 1 warning (0.01 sec)

可以发现查询正确地使用到索引,并且从执行计划描述中,发现MySQL在处理含有OR子句查询时,实际对OR各个字段分别查询后的结果进行了union操作。

在有复合索引的列上做OR操作,却无法使用到索引,查询结果如下:

mysql> explain select * from inventory where inventory_id=6 or store_id=2\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: inventory
   partitions: NULL
         type: ALL
possible_keys: PRIMARY,idx_store_id_film_id
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 4581
     filtered: 50.01
        Extra: Using where
1 row in set, 1 warning (0.01 sec)

4.5使用 SQL 提示

可以找到参考的内容:8.9.4 Index Hints

SQL提示(SQL Hints)是优化数据库的一项重要手段,简单说是在SQL语句中加入一些人为的提示达到优化目的。下面将给出一个使用SQL提示的示例:

SELECT SQL_BUFFER_RESULT FROM t1...

其默认值为0,即是关闭状态,设置为1则启用。如果启用,SQL_BUFFER_RESULT将强制SELECT语句的结果放入临时表中。在需要很长时间向客户端发送结果的情况下,帮助比较大,因为这有助于MySQL尽早释放表锁。

以下介绍一些在MySQL中常用的SQL提示:索引提示(Index Hints)

索引提示语法

tbl_name [[AS] alias] [index_hint_list]

index_hint_list:
		index_hint [index_hint] ...

index_hint:
	USE {INDEX|KEY}
		[FOR {JOIN|ORDER BY|GROUP BY}] ([index_list])
		| {IGNORE|FORCE} {INDEX|KEY}
		[FOR {JOIN|ORDER BY|GROUP BY}] (index_list)

index_list:
		index_name [, index_name] ...

看完提示语法,可以了解到索引提示三种技巧USE INDEX、IGNORE INDEX以及FORCE INDEX。

4.5.1USE INDEX

在查询语句中表名的背后,使用USE INDEX希望MySQL去参考索引列表,此时达到不让MySQL去参考其它可用索引的目的。

示例:使用explain进行分析

mysql> explain select count(*) from countrylanguage use index(CountryCode)\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: countrylanguage
   partitions: NULL
         type: index
possible_keys: NULL
          key: CountryCode
      key_len: 12
          ref: NULL
         rows: 984
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)

根据上面分析结果,可以看出type=index,走索引扫描;Extra内容是Using index,达到我们预期要求。

4.5.1IGNORE INDEX

如果用户只是单纯地想让MySQL忽略某一个或多个索引,则可以使用IGNORE INDEX作为索引提示(HINTS)。

下面使用IGNORE INDEX进行演示:

mysql> explain select count(*) from countrylanguage ignore index(CountryCode)\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: countrylanguage
   partitions: NULL
         type: index
possible_keys: NULL
          key: PRIMARY
      key_len: 132
          ref: NULL
         rows: 984
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)

通过上述执行分析,放弃了默认索引,此时走的索引是PRIMARY。

4.5.1FORCE INDEX

强制MySQL使用一个特定索引,可以在查询中使用FORCE INDEX作为HINTS。

例如,不强制使用索引时,此时支付表中大部分rental_id都是大于1的,因此MySQL默认会全表扫描,而不使用索引。如下所示:

mysql> explain select * from sakila.payment where rental_id > 1\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: payment
   partitions: NULL
         type: ALL
possible_keys: fk_payment_rental
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 16086
     filtered: 50.00
        Extra: Using where
1 row in set, 1 warning (0.01 sec)

此时,尝试指定使用索引fk_payment_rental,发现MySQL依旧走全表扫描。

mysql> explain select * from sakila.payment use index(fk_payment_rental) where rental_id > 1\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: payment
   partitions: NULL
         type: ALL
possible_keys: fk_payment_rental
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 16086
     filtered: 50.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

当使用FORCE INDEX进行提示时,即使使用索引效率不是最高,MySQL还是选择使用索引,这是MySQL将选择执行计划的权利交给了用户。加入FORCE INDEX进行测试索引提示:

mysql> explain select * from sakila.payment force index(fk_payment_rental) where rental_id > 1\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: payment
   partitions: NULL
         type: range
possible_keys: fk_payment_rental
          key: fk_payment_rental
      key_len: 5
          ref: NULL
         rows: 8043
     filtered: 100.00
        Extra: Using index condition
1 row in set, 1 warning (0.00 sec)

通过测试验证,发现MySQL确实强制走了索引,印证了MySQL将选择计划使用索引提示权利交给了用户。

注意:在MySQL8.0.20版本,此时服务支持index-level分析优化提示这些索引:JOIN_INDEX,GROUP_INDEX,ORDER_INDEX以及 INDEX。它们相当于取代了FORCE INDEX提示,同样地NO JOIN INDEX、NO GROUP INDEX、NO ORDER INDEX和NO INDEX优化器提示,它们相当于并打算取代IGNORE INDEX索引提示。因此,你应该预料到使用USE INDEX、FORCE INDEX和IGNORE INDEX会在未来的MySQL版本中被弃用,并且在以后的某个时候会被完全删除。

05 常用 SQL 技巧

常用SQL技巧主要介绍有:正则表达式。正则表达式泛用性比较广,无论在数据库SQL中还是Java语言以及Linux操作系统grep搜索匹配都用得上,甚至网页爬虫也很实用。提取随机行函数RAND()。WITH ROLLUP子句。Bit GROUP Functions 做统计。数据库库名、表名大小写注意事项。使用外键注意事项。

5.1使用正则表达式

在MySQL8.0文档中,你可以找到参考使用方法:12.8.2 Regular Expression Function and Operator Descriptions

正则表达式(Regular Expression)是指用来描述或匹配一系列符合某个句法规则的字符串的单个字符串。在多数文本编辑器或其它工具里,正则表达式通常被用来检索或替换哪些符合某个模式的文本内容。许多程序语言都支持利用正则表达式进行字符串操作。例如,在Perl中就内建了一个功能强大的正则表达式引擎。正则表达式最初是由UNIX中的工具软件(例如SED和GREP)普及开来,通常写成REGEX 或者 REGEXP。

在linux操作系统中输入pcretest即可进入练习使用正则表达式(新版本pcre2test):

$ wget https://download.fastgit.org/PhilipHazel/pcre2/releases/download/pcre2-10.39/pcre2-10.39.tar.gz
$ tar -zxvf pcre2-10.39.tar.gz
$ cd pcre2-10.39
$ ./configure
$ make && make install
$ pcre2test

MySQL利用REGEXP命令提供给用户扩展正则表达式功能,REGEXP实现的功能类似于UNIX上GREP和SED功能,并且REGEXP在进行模式匹配是是区分大小写的。熟悉掌握REGEXP用法,可以使模式匹配事半功倍。接下来将介绍一些在MySQL中的用法。

正则表达式函数和操作符如下表格所示:

在MySQL中一些正则表达式匹配符号含义:

下面将带来实际示例REGEXP用法:

SELECT 'Michael!' REGEXP '.*';
+------------------------+
| 'Michael!' REGEXP '.*' |
+------------------------+
|                      1 |
+------------------------+

SELECT 'new*\n*line' REGEXP 'new\\*.\\*line';
+---------------------------------------+
| 'new*\n*line' REGEXP 'new\\*.\\*line' |
+---------------------------------------+
|                                     0 |
+---------------------------------------+

SELECT 'a' REGEXP '^[a-d]';
+---------------------+
| 'a' REGEXP '^[a-d]' |
+---------------------+
|                   1 |
+---------------------+

REGEXP_INSTR用法:

SELECT REGEXP_INSTR('dog cat dog', 'dog');		-- 返回结果: 1
SELECT REGEXP_INSTR('dog cat dog', 'dog', 2);	-- 返回结果: 9
SELECT REGEXP_INSTR('aa aaa aaaa', 'a{2}');		-- 返回结果: 1
SELECT REGEXP_INSTR('aa aaa aaaa', 'a{4}');		-- 返回结果: 8

REGEXP_LIKE用法:

SELECT REGEXP_LIKE('CamelCase', 'CAMELCASE');	-- 返回结果:1

不做过多演示,使用比较容易上手,可以参考官方文档。

5.2RAND() 提取随机行

大多数数据库都会提供产生随机数的包或者函数,通过这些包或者函数可以产生用户需要的随机数。也可以从数据表中抽取随机产生的记录,这对抽样分析有一定的帮助。个人在MySQL开发篇进行测试生成1kw条数据,就用到了随机数RAND()函数。在Oracle数据库中可以使用DBMS_RANDOM包产生随机数。例如在Oracle中学表随机生成1kw条数据:

-- 创建表
CREATE TABLE test.student
(
    ID NUMBER not null primary key,
    STU_NAME VARCHAR2(60) not null,
    STU_AGE NUMBER(4,0) NOT NULL,
    STU_SEX VARCHAR2(2) not null
)

-- 学生表随机生成1kw数据
insert into test.student
select rownum,dbms_random.string('*',dbms_random.value(6,10)),dbms_random.value(14,16),
'女' from dual
connect by level<=10000000

上面只是提一下穿插一点Oracle中的用法,主要介绍重点是MySQL。在MySQL中,产生随机数是RAND() 函数。

创建表 t 以及插入测试数据。提示:不用创建表,你也可以直接在RAND后面圆括号中加入数字进行测试。

mysql> CREATE TABLE t (i INT);
Query OK, 0 rows affected (0.02 sec)

mysql> INSERT INTO t VALUES(1),(2),(3);
Query OK, 3 rows affected (0.01 sec)
Records: 3  Duplicates: 0  Warnings: 0

演示查询RAND():最普通的用法

mysql> SELECT i, RAND() FROM t;
+------+---------------------+
| i    | RAND()              |
+------+---------------------+
|    1 |  0.9726958740248306 |
|    2 |  0.2550815932965666 |
|    3 | 0.35732037514198606 |
+------+---------------------+
3 rows in set (0.00 sec)

演示查询 RAND(X) 加入参数:X=3

mysql> SELECT i, RAND(3) FROM t;
+------+---------------------+
| i    | RAND(3)             |
+------+---------------------+
|    1 |  0.9057697559760601 |
|    2 | 0.37307905813034536 |
|    3 | 0.14808605345719125 |
+------+---------------------+
3 rows in set (0.00 sec)

RAND()函数用法有好几种,如下:

  1. RAND():最原始用法,不加参数;
  2. RAND(X):加入一个X参数,比如RAND(3);
  3. RAND(X,D):加入两个参数表示范围,比如RAND(1,2);

示例:RAND(X,D)用法

SELECT ROUND(1.298, 1);
*************************** 1. row ***************************
ROUND(1.298, 1): 1.3
1 row in set (0.00 sec)

补充一点RAND() 可以配合 ORDER BYGROUP BY 以及 LIMIT 进行使用。

SELECT * FROM tbl_name ORDER BY RAND();
SELECT * FROM table1, table2 WHERE a=b AND c<d ORDER BY RAND() LIMIT 1000;

5.3GROUP BY 与 WITH ROLLUP 子句

在SQL语句中,使用GROUP BY配合WITH ROLLUP 子句可以检索出更多分组聚合信息,不仅仅局限于GROUP BY检索出各组聚合信息,而且还能检索出本组类的整体聚合信息,创建实例如下所示。

创建一张某产品销售利润统计表进行演示:

CREATE TABLE sales
(
year INT,
country VARCHAR(20),
product VARCHAR(32),
profit INT
);

根据年度进行分组,然后查询统计年度某产品利润:

mysql> SELECT year, SUM(profit) AS profit FROM sales GROUP BY year;
+------+--------+
| year | profit |
+------+--------+
| 2022 |    666 |
| 2021 |    555 |
| 2020 |    455 |
+------+--------+

使用ROLLUP检索出更多信息。显示每年的总利润,要确定所有年份的总利润,必须自己加起来,或者运行一个额外的查询。或者您可以使用ROLLUP,它提供两种级别的分析。

mysql> SELECT year, SUM(profit) AS profit FROM sales GROUP BY year WITH ROLLUP;
+------+--------+
| year | profit |
+------+--------+
| 2020 |    455 |
| 2021 |    555 |
| 2022 |    666 |
| NULL |   1676 |
+------+--------+

配合WITH ROLLUP使用GROUP BY分组后面可以接多个字段使用,以及使用IF条件加入GROUPING进行统计,这里不做演示。

注意事项:以前,MySQL不允许在带有WITH ROLLUP选项的查询中使用DISTINCTORDER BY。这个限制在MySQL 8.0.12及更高版本中被取消(Bug #87450,Bug #86311,Bug #26640100,Bug #26073513)。此外,LIMIT在ROLLUP后面。

我所展示版本是MySQL8.0.28,支持WITH ROLLUP选项的查询中使用DISTINCTORDER BY

mysql> SELECT * FROM(SELECT year, SUM(profit) AS profit
       FROM sales GROUP BY year WITH ROLLUP) AS dt ORDER BY year ASC;
+------+--------+
| year | profit |
+------+--------+
| NULL |   1676 |
| 2020 |    455 |
| 2021 |    555 |
| 2022 |    666 |
+------+--------+

5.4Bit GROUP Functions 做统计

你可以找到参考文档:12.13 Bit Functions and Operators

此处,不做详细解释,只展示具体使用。

以下演示GROUP BY语句和BIT_OR、BIT_AND函数完成统计工作。这两个函数一般用于做数值间的逻辑运算,当将它们与GROUP BY子句联合使用时可以做一些其它的任务。

以下是创建一张示例表t2并插入6条测试数据:

CREATE TABLE t2 (
year YEAR,
month INT UNSIGNED,
day INT UNSIGNED
);

INSERT INTO t2 VALUES(2000,1,1),(2000,1,20),(2000,1,30),(2000,2,2),(2000,2,23),(2000,2,23);

使用BIT_COUNT以及BIT_OR、BIT_AND进行查询:

mysql> SELECT year,month,BIT_COUNT(BIT_OR(1<<day)) AS days FROM t2 GROUP BY year,month;
+------+-------+------+
| year | month | days |
+------+-------+------+
| 2000 |     1 |    3 |
| 2000 |     2 |    2 |
+------+-------+------+

mysql> SELECT year,month,BIT_AND(day) AS days FROM t2 GROUP BY year,month;
+------+-------+------+
| year | month | days |
+------+-------+------+
| 2000 |     1 |    0 |
| 2000 |     2 |    2 |
+------+-------+------+

5.5数据库库名、表名大小写问题

MySQL数据库对应操作系统下的数据目录。数据库中每张表至少对应数据库目录中一个文件(也可能是多个,存储引擎类型不同,有所差异)。因此,使用的操作系统大小写敏感性决定了数据库名、表名大小写的敏感性。在Unix操作系统中,操作系统对大小敏感,导致数据库名、表名对大小写敏感。而Windows平台MySQL数据库对大小写不敏感,因为操作系统本身对大小写不敏感。

列、索引、存储子程序和触发器名在任何平台上对大小写不敏感。默认情况,表别名在Unix中对对大小敏感,但在Windows平台对大小写并不敏感。如下在Linux平台进行演示,由于区分大小写,所以抛出错误提示

mysql> select * from girl;
ERROR 1146 (42S02): Table 'test.girl' doesn't exist

正常情况,使用大写表名进行查找

mysql> select * from GIRL;
+------+-----------+----------+----------+
| ID   | GIRE_NAME | GIRL_AGE | CUP_SIZE |
+------+-----------+----------+----------+
| 1001 | 梦梦       | 14       | C        |
+------+-----------+----------+----------+
1 row in set (0.02 sec)

如上报错以及正常返结果查询操作在Windows平台都可以正常执行。如果想尽可能避免出现差错,统一规范,例如创建时统一使用小写创建库名、表名。

MySQL数据库中,如何在硬盘中保存使用表名、数据库名是由lower_case_table_names系统变量决定的,用户可以在启动MySQL服务之前设置系统变量值(由于Dynamic=no,非动态)。具体设置对应操作系统、以及含义如下表格:

例如:Windows平台使用如下SQL语句进行查询系统默认设置lower_case_table_names值,返回结果是1

mysql> select @@lower_case_table_names;
+--------------------------+
| @@lower_case_table_names |
+--------------------------+
|                        1 |
+--------------------------+
1 row in set (0.00 sec)

设置--lower-case-table-names[=#]参数值,在Windows平台直接编辑my.ini文件,在Linux操作系统可以使用vim编辑/etc/my.cnf中新增如下设置:

# my.cnf or my.ini
[mysqld]
## --lower-case-table-names[=#]	#命令行格式:参数值可以为0 1 2,根据系统平台设定
#example
lower-case-table-names=1 	# Windows平台默认值
lower-case-table-names=0	# Linux默认值为0,设置0和1都可以成功启动

tips:如果在单个平台使用,影响不是很大。使用时尽可能在同一查询中使用相同大小写来引用数据库名或表名,养成一个良好习惯。

5.6使用外键注意事项

在MySQL中,InnoDB存储引擎支持对外部关键字约束条件检查。对于其它类型存储引擎的表,当使用REFERENCES tbl_name(col_name,...)子句定义列时可以使用外部关键字,但该子句没有实际效果,可以作为注释提醒用户目前定义的列指向另一表中的一个列。具体语法在此处,不做演示,在第三章节锁问题(InnoDB锁问题:外键与锁有说明)。

接下来,演示不同类型存储引擎使用外键效果,具体也只演示MyISAM和InnoDB存储引擎使用外键创建父(parent)表和子(child)表。示例如下:

5.6.1MyISAM存储引擎建立有外键父表与子表

CREATE TABLE parent (
id INT NOT NULL,
PRIMARY KEY (id)
) ENGINE=MyISAM;

-- 创建子表child,并加入给update与delete条件加入CASCADE
CREATE TABLE child (
id INT,
parent_id INT,
INDEX par_ind (parent_id),
FOREIGN KEY (parent_id)
REFERENCES parent(id)
ON UPDATE CASCADE
ON DELETE CASCADE
) ENGINE=MyISAM;

测试插入数据:父表(parent)插入一条演示数据,子表(child)插入3条演示数据,有级联关系。如果父表内容被修改,子表三条关联外键内容也应该修改过来。实际上MyISAM存储引擎并不支持外键,所以不生效。

INSERT INTO parent (id) VALUES (1);
INSERT INTO child (id,parent_id) VALUES(1,1),(2,1),(3,1);
UPDATE parent SET id = 2 WHERE id = 1;

最后验证查询有关联的子表,数据并没有变化:

mysql> select * from child;
+------+-----------+
| id   | parent_id |
+------+-----------+
|    1 |         1 |
|    2 |         1 |
|    3 |         1 |
+------+-----------+
3 rows in set (0.00 sec)

你还可以使用语句show create table tbl_name命令查看创建的表child并没有显示外键信息,而InnoDB存储引擎会显示外键信息。

mysql> show create table child\G
*************************** 1. row ***************************
       Table: child
Create Table: CREATE TABLE `child` (
  `id` int DEFAULT NULL,
  `parent_id` int DEFAULT NULL,
  KEY `par_ind` (`parent_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

mysql> show create table test.child\G
*************************** 1. row ***************************
       Table: child
Create Table: CREATE TABLE `child` (
  `id` int DEFAULT NULL,
  `parent_id` int DEFAULT NULL,
  KEY `par_ind` (`parent_id`),
  CONSTRAINT `child_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `parent` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

5.6.2InnoDB存储引擎建立有外键父表与子表

DROP TABLE parent;-- 删除原有创建子表parent
CREATE TABLE parent (
id INT NOT NULL,
PRIMARY KEY (id)
) ENGINE=INNODB;

DROP TABLE child;-- 删除原有创建子表child
-- 重新创建子表child,并加入给update与delete条件加入CASCADE
CREATE TABLE child (
id INT,
parent_id INT,
INDEX par_ind (parent_id),
FOREIGN KEY (parent_id)
REFERENCES parent(id)
ON UPDATE CASCADE
ON DELETE CASCADE
) ENGINE=INNODB;

测试插入数据:父表(parent)插入一条演示数据,子表(child)插入3条演示数据,有级联关系。如果父表内容被修改,子表三条关联外键内容也应该修改过来。InnoDB存储引擎支持外键,所以级联修改起作用,parent_id值被集体修改为2。

INSERT INTO parent (id) VALUES (1);
INSERT INTO child (id,parent_id) VALUES(1,1),(2,1),(3,1);
UPDATE parent SET id = 2 WHERE id = 1;

最后验证查询有关联的子表,查询演示数据:

mysql> SELECT * FROM child;
+------+-----------+
| id   | parent_id |
+------+-----------+
|    1 |         2 |
|    2 |         2 |
|    3 |         2 |
+------+-----------+
3 rows in set (0.00 sec)

更加详细演示,在下面外键与锁章节描述比较详细。也可以参考官方文档示例:product_order、product、customer这三张表之间使用外键进行操作。模拟产品(product)、顾客(customer)、订单(product_order)三张表关联关系,订单表设置级联(CASCADE)关系,并且同时引用产品与顾客相应字段作为外键引用。

CREATE TABLE product (
category INT NOT NULL,
id INT NOT NULL,
price DECIMAL,
PRIMARY KEY(category, id)
) ENGINE=INNODB;

CREATE TABLE customer (
id INT NOT NULL,
PRIMARY KEY (id)
) ENGINE=INNODB;

CREATE TABLE product_order (
no INT NOT NULL AUTO_INCREMENT,
product_category INT NOT NULL,
product_id INT NOT NULL,
customer_id INT NOT NULL,
PRIMARY KEY(no),
INDEX (product_category, product_id),
INDEX (customer_id),
FOREIGN KEY (product_category, product_id)
REFERENCES product(category, id)
ON UPDATE CASCADE ON DELETE RESTRICT,
FOREIGN KEY (customer_id)
REFERENCES customer(id)
) ENGINE=INNODB;

二、优化数据库对象

第二部分,优化数据库对象。看看就行,因为没做过多示例介绍,以理论知识居多。

面对数据库设计过程,用户可能会遇到这类问题。是否完全遵循数据库设计三范式设计表结构?表的字段值大小到底设置为多长合适?这些问题看似很小,但设计不当则可能会给将来的应用带来很多性能问题。

01 优化表数据类型

设计表的时候,需要给定字段类型。

表需要使用何种数据类型应该依据实际应用来判断。当然,考虑到应用字段留有冗余是一个不错的选择。但并不推荐所有字段留有大量的冗余,因为浪费磁盘存储空间,同时在操作应用时也浪费物理内存。

在MySQL中,可以使用函数procedure analyse()对当前应用的表进行分析。该函数可以对数据表中列的数据类型提出优化建议,可以根据实际情况进行优化。

示例:MariaDB 10.5.6中使用procedure analyse()

MariaDB [test]> select * from student procedure analyse()\G
*************************** 1. row ***************************
             Field_name: test.student.ID
              Min_value: 1
              Max_value: 1000000
             Min_length: 1
             Max_length: 7
       Empties_or_zeros: 0
                  Nulls: 0
Avg_value_or_avg_length: 500000.5000
                    Std: 577357.8230
      Optimal_fieldtype: MEDIUMINT(7) UNSIGNED NOT NULL

最终给出的优化建议Optimal_fieldtype:MEDIUMINT(7) UNSIGNED NOT NULL,字段类型MEDIUMINT(7) 。

注意:在MySQL 5.x版本和MariaDB 10.5.6还可以使用。但在MySQL8.0.x版本已经被移除了,暂时没看到替代的方式。

02 拆分表提高访问效率

看小标题已经描述很清晰,通过对数据表进行拆分。

假如针对MyISAM类型表进行,有如下两种方式:

  1. 垂直拆分:将主列和一些列存放至一张表中,然后将主列和另外的列存放到另一张表中。如果不好理解,可以想象一下垂直平分线的方式。如果一张表某些列常用,而另一些列不常用,则可以采取垂直拆分。

    垂直拆分可以使数据行变小,一个数据页可以存放更多数据,查询时会减少I/O次数。缺点在于需要管理冗余列,查询所有数据需要联合(union)操作。

  2. 水平拆分:根据一列或多列数据的值将数据行放入两张独立的表中。
    水平拆分通常在以下几种场景下使用:

    表很大,分割后可以降低在查询时需要读取的数据和索引页数。同时降低索引层数,提高查询速度。

    表中数据本就有独立性。比如,表中数据记录着不同地区的数据或者不同时间段的数据。区分常用数据和不常用数据,需要将数据存放在多个介质上。

水平拆分会给应用增加复杂度,查询时通常需要联结多个表,查询所有数据需要使用UNION操作。考虑是否进行水平拆分,可以依据应用实际数据增长速率进行酌情处理。

03 逆规范

谈到逆规范,第一时间会想到规范,其次想到表中加入冗余字段便于操作。

从我们学习数据库知识起,已经深入到脑海里并理解满足规范设计的重要性。

是不是满足数据设计规范越高越好呢?以前数据库没那么多范式,最多满足3范式,现在到了N范式。个人理解,应该根据实际需求定,不应一概而论。规范越高,关系相对越复杂,表之间联结操作越频繁。如果是查询统计较多的应用,则大大影响查询性能。

设计逆规范时,我们想达到的目的是啥?降低联结操作需求、减少索引数目,也许还会减少表数目。如果带来数据完整性问题,如何处理。做逆规范,理应权衡利弊;弊大于利,则适得其反。如果优质索引可以解决,则不必增加逆规范。

使用逆规范前的思考

  • 数据存储需求;
  • 常用表大小;
  • 特殊计算(比如合计);
  • 物理存储位置。

常用逆规范技术手段:增加冗余列派生列重新组表和分割表

使用逆规范操作,往往有一种比较友好的方式来应对处理,那就是触发器。对数据任何修改立即出发对复制列或派生列的相应修改。触发器是实时的,相应处理逻辑只在一个地方出现,易于维护。

04 中间表提高统计查询效率

曾几何时,你在面试时遇到是否有海量数据处理经验。如果是你来应对,如何处理,思考过如何回答么?

仔细想想,其实可以从单表存储数据过多,会带来哪些缺点进行思考。

对于数据量较大的表,进行统计查询通常效率会降低,并且还要考虑统计查询是否影响在线应用(负面影响)。通常在这种情况下,使用中间表可以提高查询效率。考虑前提,对转移(复制)当前表时间进行忽略。

使用方法进行示例:只需两步完成操作

1、创建新表使用源表数据结构(你也可以适当优化,比如常用字段加单独索引)。当时考虑Oracle中分批次生成1kw数据想到这种方法。

create table test.student01 as select * from test.student;

2、然后插入源表数据,这样做确实很方便。

insert into test.student01 select * from test.student;

做完之后,数据转移到中间表上进行统计,得到结果。既不影响在线应用,也可以快速查询统计。

中间表做统计查询优点

  1. 复制源表部分数据,与源表隔离,中间表做统计查询不影响在线应用使用。
  2. 灵活添加索引,增加临时字段,最终达到提高统计查询效率。

参考资料&鸣谢

  • 《深入浅出MySQL 第2版 数据库开发、优化与管理维护》,个人参考优化篇部分。
  • 《MySQL技术内幕InnoDB存储引擎 第2版》,个人参考索引与锁章节描述。
  • MySQL8.0官网文档:refman-8.0-en.pdf,要学习新版本,官方文档是非常不错的选择。

虽然书籍年份比较久远(停留在MySQL5.6.x版本),但仍然具有借鉴意义。

最后,对以上书籍和官方文档所有作者表示衷心感谢。让我充分体会到:前人栽树,后人乘凉。

莫问收获,但问耕耘

只停留在看上面,提升效果甚微。应该带着思考去测试佐证,或者使用(同类书籍)新版本进行对比,这样带来的效果更好。最重要的一环,养成阅读官方文档,是一个良好的习惯。能编写官方文档,至少证明他们在这个领域是有很高的造诣,对用法足够熟练。

能看到这里的,都是帅哥靓妹。以上是本次MySQL优化篇(上部分)全部内容,希望能对你的工作与学习有所帮助。感觉写的好,就拿出你的一键三连。如果感觉总结的不到位,也希望能留下您宝贵的意见,我会在文章中定期进行调整优化。好记性不如烂笔头,多实践多积累你会发现,自己的知识宝库越来越丰富。原创不易,转载也请标明出处和作者,尊重原创。

一般情况下,会优先在公众号发布:龙腾万里sky。

不定期上传到github仓库:

养得胸中一种恬静
04-01 00:56