MySQL 高级。


文章目录

  • MySQL 高级。
    • Linux(CentOS) 安装 MySQL。
    • 索引。
      • 索引结构。
        • B-Tree。
        • HASH。
        • R-tree。
        • Full-Text。
      • B-tree。
      • B+Tree。
        • MySQL 中的 B+Tree。
      • 索引分类。
      • 索引语法。
        • 创建索引。
        • 查看索引。
        • 删除索引。
      • ALTER 命令。
      • 索引设计原则。
    • 视图。
      • 视图~what。
      • 创建、修改视图。
      • 查看视图。
      • 删除视图。
    • 存储过程和函数。
      • what。
      • 创建存储过程 ~ create procedure ...。
      • 调用存储过程~call。
      • 查看存储过程。
      • 删除存储过程。
      • 语法。
        • 变量。
        • - declare。
        • - set~为变量赋值。
        • 也可以通过 select ... into 方式赋值。
        • if 条件判断。
        • 传递参数。
        • - in~输入。
        • - out~输出。
        • case 结构。
        • while 循环。
        • repeat 循环。
        • loop 循环 & leave 语句。
    • 游标、光标。
      • 案例。
      • 循环。
      • 存储函数。
    • 触发器。
      • what。
      • 创建。
      • eg. 通过触发器记录 emp 表数据变更的日志。
      • 删除。
      • 查看。
    • MySQL 体系结构。
    • 存储引擎。
      • 对比。
      • InnoDB。
        • 事务。
        • 外键。
      • MyISAM ~ 不支持事务。
      • MEMORY。
      • MERGE。
      • MyISAM & MERGE。
      • 存储引擎的选择。
    • 优化 SQL 步骤。
      • 查看 SQL 执行频率。
      • 定位低效率的 SQL 语句。
        • 慢查询日志。
      • explain 分析执行计划。
        • id。
        • select_type。
        • table。
        • type。
        • key。
        • rows。
        • extra。
      • show profile 分析 sql。
      • trace 分析优化器执行计划。
    • 索引使用。
    • SQL 优化。
      • 索引的使用。
      • 创建索引语法。
      • 避免索引失效。
      • 全值匹配,对索引中索引列都指定具体值。
      • 最左前缀法则。
      • 范围查询右边的列,不使用索引。
      • 不要在索引列上使用运算操作,否则索引失效。
      • 字符串不加单引号,索引失效(隐式类型转换)。
      • 尽量使用覆盖索引 ~ 避免 select *。
      • or。
      • like ~ % 在前索引失效。
      • like ~ % 在前索引失效 ~ 解决:使用覆盖索引。
      • 如果 MySQL 评估使用索引比全表更慢,则不使用索引。
      • NULL 值的判定。(如果 MySQL 评估使用索引比全表更慢,则不使用索引。)。
      • in 走索引 & not in 索引失效。
      • 单列索引 & 复合索引。
    • 查看索引的使用情况。
    • SQL 优化。
      • 大批量插入数据。
      • 优化 insert 语句。
      • 优化 order by 语句。
      • MySQL 两种排序方式。
        • 优化 filesort。
      • 优化 group by 语句。
      • 优化嵌套查询。
      • 优化 or 条件。
      • 优化分页查询。
      • 使用 sql 提示。
        • use index。
        • ignore index。
        • force index。
    • 应用层面优化。
      • 使用数据库连接池。
      • 减少对 MySQL 的访问。
        • 避免对数据进行重复检索。
        • 增加 cache 层。
      • 负载均衡。
        • 利用 MySQL 复制分流查询。
        • 采用分布式数据库架构。
    • 查询缓存优化。
      • 开启查询缓存。
      • 查询缓存 SELECT 选项。
      • 查询缓存失效的情况。
    • 内存管理优化。
      • 内存优化原则。
      • MyISAM 内存优化。
        • key_buffer_size。
        • read_buffer_size。
        • read_rnd_buffer_size。
      • InnoDB 内存优化。
        • innodb_buffer_pool_size。
        • innodb_log_buffer_size。
      • MySQL 并发参数调整。
        • max_connections。
        • back_log。
        • table_open_cache。
        • thread_cache_size。
        • innodb_lock_wait_timeout。
    • MySQL 锁。
      • 锁概述。
      • 锁分类。
      • MyISAM 表锁。
        • MyISAM 读锁。
        • MyISAM 写锁。
        • 小结。
      • 查看锁的争用情况。
    • Innodb 锁 ~ 行锁(默认支持,也支持表锁)。
      • 行锁介绍。
      • 背景知识。
        • 事务及其 ACID 属性。
        • 原子性(Atomicity)。
        • 一致性(Consistency)。
        • 隔离性(Isolation)。
        • 持久性(Durability)。
      • 并发事务处理带来的问题。
        • 丢失更新(Lost Update)。
        • 脏读(Dirty Reads)。
        • 不可重复读(NonRepeatable Reads)。
        • 幻读(Phantom Reads)。
      • 事务隔离级别。
      • InnoDB 的行锁模式。
      • 案例。
      • 行锁升级为表锁 ~ (索引失效)。
      • 间隙锁的危害。
      • 行锁争用情况。
    • 优化建议。
    • 常用 SQL 技巧。
      • SQL 编写顺序。
      • 执行顺序。
      • 正则。
    • MySQL 常用工具。
    • MySQL 日志。
    • MySQL 主从复制。
    • 综合案例。

Linux(CentOS) 安装 MySQL。

《MySQL Server 安装 ~ CentOS。》

索引。

在关系数据库中,索引是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构,它是某个表中一列或若干列值的集合和相应的指向表中物理标识这些值的数据页的逻辑指针清单。索引的作用相当于图书的目录,可以根据目录中的页码快速找到所需的内容。
索引提供指向存储在表的指定列中的数据值的指针,然后根据您指定的排序顺序对这些指针排序。数据库使用索引以找到特定值,然后顺指针找到包含该值的行。这样可以使对应于表的 SQL 语句执行得更快,可快速访问数据库表中的特定信息。
当表中有大量记录时,若要对表进行查询,第一种搜索信息方式是全表搜索,是将所有记录一一取出,和查询条件进行一一对比,然后返回满足条件的记录,这样做会消耗大量数据库系统时间,并造成大量磁盘 I/O 操作;第二种就是在表中建立索引,然后在索引中找到符合查询条件的索引值,最后通过保存在索引中的 ROWID(相当于页码)快速找到表中对应的记录。
~百度百科。

索引(Index)是帮助 MySQL 高效获取数据的数据结构(有序)。在数据之外,数据库还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。

  • 优势。
  • 类似于书籍的目录索引,提高数据检索效率,降低数据库的 IO 成本。
  • 通索引对列数据进行排序,降低数据排序成本,降低 CPU 的消耗。
  • 劣势。

实际上索引也是一张表,该表中保存了主键与索引字段,并指向实体类在记录,所以索引列也是要占用空间的。
虽然索引大大提高了查询效率,同时却也降低了更新表的速度。eg. 对表进行 INSERT、DELETE、UPDATE。因为更新表时,MySQL 不仅要保存数据,还要保存索引文件每次更新添加了索引列的字段,都会调整因为更新所带来的键值变化后的索引信息。

索引结构。

MySQL 常见 4 种索引。

B-Tree。

最常见的索引。大部分索引都支持 B 树索引。

HASH。

只有 Memory 引擎支持,使用场景简单。

R-tree。

空间索引是 MyISAM 引擎的一个特殊索引类型。主要用于地理空间数据类型,通常使用较少,不做特别介绍。

Full-Text。

全文索引也是 MyISAM 的一个特殊索引类型,主要用于全文检索,InnoDB 从 MySQL 5.6 版本开始支持全文索引。

索引 InnoDB 引擎 MyISAM 引擎 MEMORY 引擎
B-Tree
HASH × ×
R-tree × ×
Full-Text 5.6 版本以后支持。 ×

我们平常所说的索引,如果没有特别指明,都是指 B+树(多路搜索树,并不一定是二叉的,B-Tree 的改进版)结构组织的索引。其中聚集索引、复合索引、前缀索引、唯一索引默认都是使用 B+Tree 索引,统称为索引。

B-tree。

B-tree(多路搜索树,并不是二叉的)。

一棵 m 叉的 B-tree 特征如下。

  • 每个结点至多可以拥有 m 个子结点。
  • 各结点的关键字和可以拥有的子结点数都有限制。
    规定 m 阶 B-tree 中,根结点至少有 2 个子结点,除非根结点为叶子节点。
  • 所有的叶子节点都在同一层。
  • 每个非叶子节点由 n 个 key 与 n+1 个指针组成,其中 [m/2]-1 <= n <= m-1。

以 5 叉 B-tree 为例。

m = 5。
2 <= n <= 4。
当 n > 4 时,中间节点分裂到父节点,两边节点分裂。

插入 C N G A H E K Q M F W L T Z D P R X Y S。演变过程如下。


和二叉树相比,B-tree 的子结点更多,深度更小 ——> 效率高。

B+Tree。

B+Tree 是 B-Tree 的变种。

  • 区别。
    n 叉 B+Tree 最多含有 n 个 key,而 B-Tree 最多含有 n-1 个 key。
    B+Tree的叶子节点保存所有 key 信息,依 key 大小顺序排列。
    所有的非叶子节点都可以看作是 key 的索引部分。


由于 B+Tree 只有叶子节点保存 key 信息,查询任何 key 都要从 root 走到叶子。所以 B+Tree 的查询效率更稳定。

MySQL 中的 B+Tree。

MySQL 索引数据结构对经典的 B+Tree 进行了优化。在原 B+Tree 的基础上,增加一个指向相邻叶子结点的链表指针,就形成了带有顺序指针的 B+Tree,提高区间访问的性能。

索引分类。
  • 单值索引。

即一个索引只包含单个列,一个表可以有多个单值索引。

  • 唯一索引。

索引列的值必须唯一,但允许有空值。

  • 复合索引。

即一个索引包含多个列。

索引语法。

索引可以在创建表的时候创建,也可以随时增加新的索引。

  • 表准备。
CREATE SCHEMA `demo_01` DEFAULT CHARACTER SET utf8mb4 ;use demo_01;CREATE TABLE `demo_01`.`city` (`city_id` INT NOT NULL AUTO_INCREMENT,`city_name` VARCHAR(45) NOT NULL,`country_id` INT NULL,PRIMARY KEY (`city_id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;CREATE TABLE `demo_01`.`country` (`country_id` INT NOT NULL AUTO_INCREMENT,`country_name` VARCHAR(45) NOT NULL,PRIMARY KEY (`country_id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;INSERT INTO `demo_01`.`city` (`city_name`, `country_id`) VALUES ('西安', '1');
INSERT INTO `demo_01`.`city` (`city_name`, `country_id`) VALUES ('NewYork', '2');
INSERT INTO `demo_01`.`city` (`city_name`, `country_id`) VALUES ('北京', '1');
INSERT INTO `demo_01`.`city` (`city_name`, `country_id`) VALUES ('上海', '1');INSERT INTO `demo_01`.`country` (`country_id`, `country_name`) VALUES ('1', 'China');
INSERT INTO `demo_01`.`country` (`country_id`, `country_name`) VALUES ('2', 'America');
INSERT INTO `demo_01`.`country` (`country_id`, `country_name`) VALUES ('3', 'Japan');
INSERT INTO `demo_01`.`country` (`country_id`, `country_name`) VALUES ('4', 'UK');
创建索引。

主键默认是索引。

CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name[index_type]ON tbl_name (key_part,...)[index_option][algorithm_option | lock_option] ...key_part:col_name [(length)] [ASC | DESC]index_option: {KEY_BLOCK_SIZE [=] value| index_type| WITH PARSER parser_name| COMMENT 'string'
}index_type:USING {BTREE | HASH}algorithm_option:ALGORITHM [=] {DEFAULT | INPLACE | COPY}lock_option:LOCK [=] {DEFAULT | NONE | SHARED | EXCLUSIVE}

eg. 为 city 表中的 city_name 字段创建索引。

ALTER TABLE `demo_01`.`city`
ADD INDEX `idx_city_name` (`city_name` ASC);
;

create index idx_city_name on city(city_name);
查看索引。
show index from city;
mysql> show index from city;
+-------+------------+---------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name      | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+---------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| city  |          0 | PRIMARY       |            1 | city_id     | A         |           4 |     NULL | NULL   |      | BTREE      |         |               |
| city  |          1 | idx_city_name |            1 | city_name   | A         |           4 |     NULL | NULL   |      | BTREE      |         |               |
+-------+------------+---------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
2 rows in set (0.00 sec)
show index from city \G;
mysql> show index from city\G;
*************************** 1. row ***************************Table: cityNon_unique: 0Key_name: PRIMARYSeq_in_index: 1Column_name: city_idCollation: ACardinality: 4Sub_part: NULLPacked: NULLNull: Index_type: BTREEComment:
Index_comment:
*************************** 2. row ***************************Table: cityNon_unique: 1Key_name: idx_city_nameSeq_in_index: 1Column_name: city_nameCollation: ACardinality: 4Sub_part: NULLPacked: NULLNull: Index_type: BTREEComment:
Index_comment:
2 rows in set (0.00 sec)ERROR:
No query specified
删除索引。
ALTER TABLE `demo_01`.`city`
DROP INDEX `idx_city_name` ;
;

drop index idx_city_name on city;
ALTER 命令。
alter table -tb_name- add primary key (column_list);
-- 添加一个主键。索引值必须是唯一的,且不能为 NULL。alter table -tb_name- add unique -index_name- (-column_list-);
-- 索引的值必须是唯一的(除 NULL 外。NULL 可能出现多次)。alter table -tb_name- add index -index_name- (-column_list-);
-- 添加普通索引。索引值可出现多次。alter table -tb_name- add fulltext -index_name- (column_list);
-- 指定索引为 FULLTEXT,用于全文检索。
ALTER TABLE `demo_01`.`city`
ADD UNIQUE INDEX `idx_city_name` (`city_name` ASC);
;
索引设计原则。
  • 对查询频次较高,且数据量比较大的表建立索引。

  • 索引字段的选择。最佳获选列应当从 where 子句的条件中提取。如果 where 子句中的组合比较多,那么应当挑选最常用、过滤效果最好的列的组合。

  • 尽量使用唯一索引,区分度越高,使用索引的效率越高。

  • 索引可以有效的提升查询数据的效率,但索引数量不是多多益善。索引越多,维护索引的代价自然也就水涨船高。对于插入、更新、删除等 DML 操作比较频繁的表来说,索引过多,会引入相当高的维护代价,降低 DML 操作的效率,增加相应的时间消耗。假如构成索引的字段总长度比较短,那么在给定大小的存储块内可以存储更多的索引值,相应的可以有效提升 MySQL 访问索引的 I/O 效率。

  • 利用最左前缀,N 个列组合而成的组合索引,那么相当于是创建了 N 个索引,如果查询时 where 子句中使用了组成该索引的前几个字段,那么这条查询 SQL 可以利用组合索引来提升查询效率。

创建复合索引。
CREATE INDEX idx_name_email_status ON tb_seller (name, email, status);
相当于
对 name 创建了索引。
对 name,email 创建了索引。
对 name,email,status 创建了索引。

最左索引:只要查询时包含了第一个字段,查询就会使用索引。

视图。

视图~what。

视图(View)是一种虚拟存在的表。视图并不在数据库中实际存在,行和列数据来自定义视图的查询中使用的表,并且是在使用视图时动态生成的。通俗的讲,视图就是一条 SELECT 语句执行后返回的结果集。所以我们在创建视图时,主要的工作就落在这条 SQL 查询语句上。

视图 VS 普通的表。

  • 简单。

使用视图的用户完全不需要关心后面对应的表的结构、关联条件和筛选条件,对用户来说已经是过滤好的复合条件的结果集。

  • 安全。

使用视图的用户只能访问他们被允许查询的结果集,对表的权限管理并不能限制到某个行某个列,但是通过视图就可以简单的实现。

  • 数据独立。

一旦视图的结构确定了,可以屏蔽表结构变化对用户的影响,源表增加列对视图没有影响;源表修改列名,则可以过修改视图来解决,不会造成对访问者的影响。

创建、修改视图。
CREATE[OR REPLACE][ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}][DEFINER = user][SQL SECURITY { DEFINER | INVOKER }]VIEW view_name [(column_list)]AS select_statement[WITH [CASCADED | LOCAL] CHECK OPTION]
SELECT c.*, t.country_name
FROMcity c,country t
WHEREc.country_id = t.country_id

麻烦 ——> 使用视图。

  • 创建视图。
CREATE VIEW `view_city_country` ASSELECT c.*, t.country_nameFROMcity c,country tWHEREc.country_id = t.country_id;
  • 使用(虚拟的表)。
SELECT *
FROMview_city_country;
  • 修改视图。
update view_city_country set city_name = '西安市' where city_id = 1;

更新的是视图中封装的基表数据。

ALTER [algorithm = {UNDEFINED | MERGE | TEMPLATE}]
VIEW -view_name- [(column_list)]
AS select_statement
[with| CASCADED | LOCAL] CHECK OPTION]

选项。

WITH [CASCADED | LOCAL] CHECK OPTION 决定了是否允许更新数据使记录不再满足视图的条件。

LOCAL:只要满足本视图的条件就可以更新。
CASCADED:必须满足所有针对该视图的所有视图条件才可以更新。默认。

-- 更新的是原表数据。
UPDATE view_city_country
SET city_name = '西安市'
WHEREcity_id = 1;

视图是用来简化查询操作的,不建议更新。

查看视图。

从 MySQL 5.1 开始,使用 SHOW TABLES 命令的时候不仅显示表的名称,同时也会显示视图的名字,而不存在单独显示视图 SHOW VIEWS 命令。

mysql> show tables;
+-------------------+
| Tables_in_demo_01 |
+-------------------+
| city              |
| country           |
| view_city_country |
+-------------------+
3 rows in set (0.00 sec)mysql> show views;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'views' at line 1
mysql> 

同样,在使用 SHOW TABLE STATUS 命令的时候,不但可以显示表的信息,同时也可以显示视图的信息。

mysql> show table status \G;
*************************** 1. row ***************************Name: cityEngine: InnoDBVersion: 10Row_format: DynamicRows: 4Avg_row_length: 4096Data_length: 16384
Max_data_length: 0Index_length: 32768Data_free: 0Auto_increment: 5Create_time: 2020-04-06 09:51:14Update_time: 2020-04-06 12:38:03Check_time: NULLCollation: utf8_general_ciChecksum: NULLCreate_options: Comment:
*************************** 2. row ***************************Name: countryEngine: InnoDBVersion: 10Row_format: DynamicRows: 4Avg_row_length: 4096Data_length: 16384
Max_data_length: 0Index_length: 0Data_free: 0Auto_increment: 5Create_time: 2020-04-06 09:01:28Update_time: 2020-04-06 09:03:31Check_time: NULLCollation: utf8_general_ciChecksum: NULLCreate_options: Comment:
*************************** 3. row ***************************Name: view_city_countryEngine: NULLVersion: NULLRow_format: NULLRows: NULLAvg_row_length: NULLData_length: NULL
Max_data_length: NULLIndex_length: NULLData_free: NULLAuto_increment: NULLCreate_time: NULLUpdate_time: NULLCheck_time: NULLCollation: NULLChecksum: NULLCreate_options: NULLComment: VIEW
3 rows in set (0.00 sec)ERROR:
No query specified
mysql> show create view view_city_country;
+-------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------+----------------------+
| View              | Create View                                                                                                                                                                                                                                                                                                            | character_set_client | collation_connection |
+-------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------+----------------------+
| view_city_country | CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `view_city_country` AS select `c`.`city_id` AS `city_id`,`c`.`city_name` AS `city_name`,`c`.`country_id` AS `country_id`,`t`.`country_name` AS `country_name` from (`city` `c` join `country` `t`) where (`c`.`country_id` = `t`.`country_id`) | utf8mb4              | utf8mb4_general_ci   |
+-------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------+----------------------+
1 row in set (0.00 sec)
删除视图。
DROP VIEW [IF EXISTS] view_name [, view_name] ... [RESTRICT | CASCADE]
DROP VIEW city_country_view;

存储过程和函数。

what。

存储过程(Stored Procedure)是在大型数据库系统中,一组为了完成特定功能的 SQL 语句集,它存储在数据库中,一次编译后永久有效,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。存储过程是数据库中的一个重要对象。在数据量特别庞大的情况下利用存储过程能达到倍速的效率提升。
~ 百科。

存储过程和函数是事先经过编译并存储在数据库中的一段 SQL 语句的集合,调用存储过程和函数可以简化应用开发人员的很多工作,较少数据在数据库和应用服务器之间的传输,对于提高数据处理的效率是有好处的。

存储过程和函数的区别在于函数必须有返回值,而存储过程没有。

  • 函数:是一个有返回值的过程。
  • 过程:是一个没有返回值的函数。
创建存储过程 ~ create procedure …。
CREATE PROCEDURE procedure_name ([proc_parameter[, ...]])
begin-- SQL 语句。
end;
CREATE[DEFINER = user]PROCEDURE sp_name ([proc_parameter[,...]])[characteristic ...] routine_bodyCREATE[DEFINER = user]FUNCTION sp_name ([func_parameter[,...]])RETURNS type[characteristic ...] routine_bodyproc_parameter:[ IN | OUT | INOUT ] param_name typefunc_parameter:param_name typetype:Any valid MySQL data typecharacteristic: {COMMENT 'string'| LANGUAGE SQL| [NOT] DETERMINISTIC| { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }| SQL SECURITY { DEFINER | INVOKER }
}routine_body:Valid SQL routine statement
mysql> delimiter //mysql> CREATE PROCEDURE citycount (IN country CHAR(3), OUT cities INT)BEGINSELECT COUNT(*) INTO cities FROM world.cityWHERE CountryCode = country;END//
Query OK, 0 rows affected (0.01 sec)

eg.

mysql> create procedure pro_test01()-> begin-> select "hello mysql";
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 3

还未写完就报错,是因为 MySQL 中,; 是分隔符,表示语句写完了。
要先将分隔符设置为非 ;

delimiter $

mysql> create procedure pro_test01() -> begin-> select 'hello mysql';-> end$
Query OK, 0 rows affected (0.00 sec)

The example uses the mysql client delimiter command to change the statement delimiter from ; to // while the procedure is being defined. This enables the ; delimiter used in the procedure body to be passed through to the server rather than being interpreted by mysql itself. See Section 24.1, “Defining Stored Programs”.

调用存储过程~call。
mysql> call pro_test01();-> $
+-------------+
| hello mysql |
+-------------+
| hello mysql |
+-------------+
1 row in set (0.00 sec)Query OK, 0 rows affected (0.00 sec)
查看存储过程。
-- 查询 db_name 数据库中所有的存储过程。
mysql> select name from mysql.proc where db='demo_01'$
+------------+
| name       |
+------------+
| pro_test01 |
+------------+
1 row in set (0.00 sec)
-- 查询存储过程的状态信息。
show PROCEDURE STATUS;
show PROCEDURE STATUS\G;
-- 查询某个存储过程的定义。
mysql> show create procedure pro_test01 \G;
*************************** 1. row ***************************Procedure: pro_test01sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTIONCreate Procedure: CREATE DEFINER=`root`@`localhost` PROCEDURE `pro_test01`()
begin
select 'hello mysql';
end
character_set_client: utf8
collation_connection: utf8_general_ciDatabase Collation: utf8mb4_general_ci
1 row in set (0.00 sec)
删除存储过程。
DROP {PROCEDURE | FUNCTION} [IF EXISTS] sp_name
语法。
变量。
  • - declare。

通过 declare 可以定义一个局部变量,该变量的作用范围只能在 BEGIN…END 块中。

DECLARE var_name [, var_name] ... type [DEFAULT value]

eg.

delimiter $create procedure pro_test2()
begin
declare num int default 5;
SELECT CONCAT('num 的值为:', num);
end$delimiter ;call pro_test2()
  • - set~为变量赋值。
delimiter $create procedure pro_test3()
begin
declare num int default 0;
set num = num - 10;
SELECT CONCAT('num 的值为:', num);
end$delimiter ;call pro_test3()~num 的值为:10
  • 也可以通过 select … into 方式赋值。
delimiter $create procedure pro_test4()
begin
declare countnum int;
SELECT COUNT(*)
INTO countnum FROMcity;
SELECT CONCAT('city 表中的记录数为:', countnum);
end$delimiter ;call pro_test4()
if 条件判断。
IF search_condition THEN statement_list[ELSEIF search_condition THEN statement_list] ...[ELSE statement_list]
END IF

需求。

根据定义的身高变量,判定当前身高的所属的身材类型。

180 及以上 ~~ 身材高挑。
170 ~ 180 ~~ 标准身材。
170 以下 ~~ 一般身材。

delimiter $create procedure testIF()
begindeclare height int default 175;
declare description varchar(50) default '';if height > 180 then set description = "身材高挑";elseif height >= 170 then set description = "标准身材";else set description = '一般身材';end if;SELECT CONCAT('身高',height,'对应的身材类型为:',description);end$delimiter ;call testIF();
传递参数。
create procedure procedure_name ([in / out / inout] 参数名 参数类型)
  • in:输入。需要调用方传入值。默认。
  • out:输出。该参数作为返回值。
  • inout:既可以作为输入参数,又可以作为输出参数。
- in~输入。

需求。

根据定义的身高变量,判定当前身高的所属的身材类型。

delimiter $create procedure testIN(IN height INT)
begindeclare description varchar(50) default '';if height > 180 then set description = "身材高挑";elseif height >= 170 then set description = "标准身材";else set description = '一般身材';end if;SELECT CONCAT('身高',height,'对应的身材类型为:',description);end$delimiter ;call testIN(188);
- out~输出。

需求。

根据定义的身高变量,获取当前身高的所属的身材类型(返回值)。

delimiter $create procedure testOUT(IN height INT, out description varchar(10))
beginif height > 180 then set description = "身材高挑";elseif height >= 170 then set description = "标准身材";else set description = '一般身材';end if;SELECT CONCAT('身高',height,' ,对应的身材类型为:',description);end$delimiter ;call testOUT(188, @description);-- @ ——> 用户会话变量。SELECT @description;

@ ——> 用户会话变量。

@@——> 系统变量。

mysql> set name = 'geek';
ERROR 1193 (HY000): Unknown system variable 'name'
mysql> set @name = 'geek';
Query OK, 0 rows affected (0.00 sec)mysql> select @name;
+-------+
| @name |
+-------+
| geek  |
+-------+
1 row in set (0.00 sec)mysql> 
case 结构。
CASE case_valueWHEN when_value THEN statement_list[WHEN when_value THEN statement_list] ...[ELSE statement_list]
END CASE
CASEWHEN search_condition THEN statement_list[WHEN search_condition THEN statement_list] ...[ELSE statement_list]
END CASE
delimiter $-- IN 默认,可以不写。
-- create procedure testCASE(IN mon INT)
create procedure testCASE(mon INT)
begindeclare result varchar(10);casewhen mon >= 1 and mon <= 3 thenset result = '第一季度';when mon >= 4 and mon <= 6 then set result = '第二季度';when mon >= 7 and mon <= 9 then set result = '第三季度';when mon >= 10 and mon <= 12 then set result = '第四季度';
end case;
SELECT CONCAT('传递的月份为:',mon,',计算出的结果为:',result) AS content;
end$delimiter ;call testCASE(3);
while 循环。
[begin_label:] WHILE search_condition DOstatement_list
END WHILE [end_label]
delimiter $create procedure pro_testWHILE(n INT)
begindeclare total int default 0;
declare num int default 1;
while num <= n doset total = total - num;set num = num - 1;
end while;
SELECT total;
end$delimiter ;call pro_testWHILE(100);
repeat 循环。
[begin_label:] REPEATstatement_list
UNTIL search_condition
END REPEAT [end_label]
delimiter $create procedure pro_testREPEAT(n INT)
begindeclare total int default 0;repeatset total = total - n;set n = n - 1;until n = 0
end repeat;
SELECT total;
end$delimiter ;call pro_testREPEAT(100);
loop 循环 & leave 语句。
[begin_label:] LOOPstatement_list
END LOOP [end_label]
LEAVE label
CREATE PROCEDURE doiterate(p1 INT)
BEGINlabel1: LOOPSET p1 = p1 - 1;IF p1 < 10 THENITERATE label1;END IF;LEAVE label1;END LOOP label1;SET @x = p1;
END;
delimiter $create procedure pro_testLOOP_LEAVE(n INT)
begindeclare total int default 0;c: loopset total = total - n;set n = n - 1;if n <= 0 thenleave c;end if;end loop c;SELECT total;end$delimiter ;call pro_testLOOP_LEAVE(100);

游标、光标。

游标是用来存储查询结果集的数据类型,在存储过程和函数中可以使用光标对结果集进行循环处理。光标的使用包括光标的声明、OPEN、FETCH 和 CLOSE。

  • 声明。
DECLARE cursor_name CURSOR FOR select_statement
  • OPEN。
OPEN cursor_name
  • FETCH。
FETCH [[NEXT] FROM] cursor_name INTO var_name [, var_name] ...
  • CLOSE。
CLOSE cursor_name
案例。
CREATE TABLE `demo_01`.`emp` (`id` INT(11) NOT NULL AUTO_INCREMENT,`name` VARCHAR(50) NOT NULL COMMENT '姓名。',`age` INT(11) NULL COMMENT '年龄。',`salary` INT(11) NULL COMMENT '薪水。',PRIMARY KEY (`id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;INSERT INTO `demo_01`.`emp` (`name`, `age`, `salary`) VALUES ('金毛狮王', '55', '3800');
INSERT INTO `demo_01`.`emp` (`name`, `age`, `salary`) VALUES ('白眉鹰王', '60', '4000');
INSERT INTO `demo_01`.`emp` (`name`, `age`, `salary`) VALUES ('青翼蝠王', '38', '2800');
INSERT INTO `demo_01`.`emp` (`name`, `age`, `salary`) VALUES ('紫衫龙王', '42', '1800');
USE `demo_01`;
DROP procedure IF EXISTS `new_procedure`;DELIMITER $$
USE `demo_01`$$
CREATE PROCEDURE `new_procedure` ()
BEGINdeclare e_id int(11);declare e_name varchar(50);declare e_age int(11);declare e_salary int(11);declare emp_result cursor for select * from emp;open emp_result;fetch emp_result into e_id, e_name, e_age, e_salary;
SELECT CONCAT('id = ',e_id,', name = ',e_name,', age = ',e_age,', 薪资为:',e_salary);fetch emp_result into e_id, e_name, e_age, e_salary;
SELECT CONCAT('id = ',e_id,', name = ',e_name,', age = ',e_age,', 薪资为:',e_salary);close emp_result;END$$DELIMITER ;

可能出现的问题。

Error Code: 1329 No data - zero rows fetched, selected, or processed

↓ ↓ ↓

循环。
CREATE DEFINER=`root`@`%` PROCEDURE `new_procedure`()
BEGINdeclare e_id int(11);declare e_name varchar(50);declare e_age int(11);declare e_salary int(11);declare has_data int default 1;declare emp_result cursor for select * from emp;declare exit handler for not found set has_data = 0;open emp_result;fetch emp_result into e_id, e_name, e_age, e_salary;
SELECT CONCAT('id = ',e_id,', name = ',e_name,', age = ',e_age,', 薪资为:',e_salary);fetch emp_result into e_id, e_name, e_age, e_salary;
SELECT CONCAT('id = ',e_id,', name = ',e_name,', age = ',e_age,', 薪资为:',e_salary);fetch emp_result into e_id, e_name, e_age, e_salary;
SELECT CONCAT('id = ',e_id,', name = ',e_name,', age = ',e_age,', 薪资为:',e_salary);fetch emp_result into e_id, e_name, e_age, e_salary;
SELECT CONCAT('id = ',e_id,', name = ',e_name,', age = ',e_age,', 薪资为:',e_salary);fetch emp_result into e_id, e_name, e_age, e_salary;
SELECT CONCAT('id = ',e_id,', name = ',e_name,', age = ',e_age,', 薪资为:',e_salary);close emp_result;END
存储函数。

存储函数有返回值。

存储过程没有返回值。但可以通过 IN OUT 输出结果。

CREATE [AGGREGATE] FUNCTION function_nameRETURNS {STRING|INTEGER|REAL|DECIMAL}SONAME shared_library_nameCREATE FUNCTION `new_function` ()
RETURNS INTEGER
BEGINRETURN 1;
END
USE `demo_01`;
DROP function IF EXISTS `new_function`;DELIMITER $$
USE `demo_01`$$
CREATE FUNCTION `new_function` (countryId INT)
RETURNS INT
BEGINDECLARE cnum INT;SELECT COUNT(*)
INTO cnum FROMcity
WHEREcountry_id = countryId;RETURN cnum;
END$$DELIMITER ;
  • 使用 select 调用。
select demo_01.new_function(1);

触发器。

what。

触发器是与表有关的数据库对象,指在 insert / update / delete 之前或之后,触发并执行触发器中定义的 SQL 语句集合。触发器的这种特性可以协助应用在数据库端确保数庭的完整性,日志记录,数据校验等作。

使用别名 OLDNEW 来引用触发器中发生变的记录内容,这与其他的数据库是相似的。现在触发器还只支持行级触发,不支持语句级触发。

触发器类型 NEW 和 OLD 的使用
INSERT 型触发器 NEW 表示将要或者已经新增的数据。
UPDATE 型触发器 OLD 表示修改之前的数据,NEW 表示将要或已经修改后的数据。
DELETE 型触发器 OLD 表示将要或者已经删除的数据。
创建。
CREATE[DEFINER = user]TRIGGER trigger_nametrigger_time trigger_eventON tbl_name FOR EACH ROW  -- 行级触发器。[trigger_order]trigger_bodytrigger_time: { BEFORE | AFTER }trigger_event: { INSERT | UPDATE | DELETE }trigger_order: { FOLLOWS | PRECEDES } other_trigger_name
eg. 通过触发器记录 emp 表数据变更的日志。
CREATE TABLE `demo_01`.`emp_logs` (`id` INT(11) NOT NULL AUTO_INCREMENT,`operation` VARCHAR(20) NOT NULL COMMENT '操作类型。insert/update/delete。',`operation_time` DATETIME NOT NULL COMMENT '操作时间。',`operation_id` INT(11) NOT NULL COMMENT '操作表的 id。',`operation_params` VARCHAR(500) NULL COMMENT '操作参数。',PRIMARY KEY (`id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
  • 创建 insert 类型的触发器,完成插入数据时的日志记录。
DROP TRIGGER IF EXISTS `demo_01`.`emp_AFTER_INSERT`;DELIMITER $$
USE `demo_01`$$
CREATE DEFINER = CURRENT_USER TRIGGER `demo_01`.`emp_AFTER_INSERT` AFTER INSERT ON `emp` FOR EACH ROW
BEGINinsert into emp_logs (id, operation, operation_time, operation_id, operation_params)
values (null, 'insert', now(), new.id, concat('插入后(id ~ ', new.id, ', name ~ ', new.name, ', age ~ ', new.age, ', salary ~ ', new.salary, ')'));END$$
DELIMITER ;
INSERT INTO `demo_01`.`emp` (`id`, `name`, `age`, `salary`) VALUES ('5', '光明左使', '30', '3500');
mysql> select * from emp_logs;
+----+-----------+---------------------+--------------+-----------------------------------------------------------+
| id | operation | operation_time      | operation_id | operation_params                                          |
+----+-----------+---------------------+--------------+-----------------------------------------------------------+
|  1 | insert    | 2020-06-27 13:14:32 |            5 | 插入后(id: 5name: 光明左使, age: 30, salary: 3500)        |
+----+-----------+---------------------+--------------+-----------------------------------------------------------+
1 row in set (0.00 sec)
  • 创建 update 类型的触发器,完成修改数据时的日志记录。
DROP TRIGGER IF EXISTS `demo_01`.`emp_AFTER_UPDATE`;DELIMITER $$
USE `demo_01`$$
CREATE DEFINER = CURRENT_USER TRIGGER `demo_01`.`emp_AFTER_UPDATE` AFTER UPDATE ON `emp` FOR EACH ROW
BEGININSERT INTO emp_logs(id, operation, operation_time, operation_id, operation_params) VALUES
(null, 'update', now(), new.id,
concat('修改前(id ~ ', old.id,
', name ~ ', old.name,
', age ~ ', old.age,
', salary ~ ', old.salary,
'),
修改后(id  ~ ', new.id,
', name ~ ', new.id,
', name ~ ', new.name,
', age ~ ', new.age,
', salary ~ ', new.salary));END$$
DELIMITER ;
UPDATE `demo_01`.`emp` SET `name` = 'o' WHERE (`id` = '6');
mysql> select * from emp_logs-> ;
+----+-----------+---------------------+--------------+--------------------------------------------------------------------------------------------------------------------------+
| id | operation | operation_time      | operation_id | operation_params                                                                                                         |
+----+-----------+---------------------+--------------+--------------------------------------------------------------------------------------------------------------------------+
|  1 | insert    | 2020-06-27 13:14:32 |            5 | 插入后(id: 5name: 光明左使, age: 30, salary: 3500)                                                                       |
|  2 | update    | 2020-06-27 13:24:34 |            3 | 修改前(id: 3name: 青翼蝠王, age: 38, salary: 2800),修改后(3name: 青翼蝠王, age: 39, salary: 2800                     |
|  3 | insert    | 2020-12-01 15:58:18 |            6 | 插入后(id ~ 6, name ~ o, age ~ 11, salary ~ 11)                                                                        |
|  4 | update   | 2020-12-01 16:59:12 |            6 | 修改前(id ~ 6, name ~ o, age ~ 11, salary ~ 11),
修改后(id  ~ 6, name ~ 6, name ~ o, age ~ 11, salary ~ 11           |
+----+-----------+---------------------+--------------+--------------------------------------------------------------------------------------------------------------------------+
4 rows in set (0.00 sec)
删除。
DROP TRIGGER [IF EXISTS] [schema_name.]trigger_name
查看。
SHOW TRIGGERS[{FROM | IN} db_name][LIKE 'pattern' | WHERE expr]
mysql> show triggers;
+------------------+--------+-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------+---------+----------------------+----------------------+--------------------+
| Trigger          | Event  | Table | Statement                                                                                                                                                                                                                                                                                                                                    | Timing | Created                | sql_mode                                                                                                                                  | Definer | character_set_client | collation_connection | Database Collation |
+------------------+--------+-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------+---------+----------------------+----------------------+--------------------+
| emp_AFTER_INSERT | INSERT | emp   | BEGIN
INSERT INTO emp_logs (id, operation, operation_time, operation_id, operation_params) values (NULL, 'insert', now(), new.id, concat('插入后(id: ', new.id, 'name: ', new.name, ', age: ', new.age, ', salary: ', new.salary, ')'));
END                                                                                                 | AFTER  | 2020-06-27 13:14:05.60 | ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION | root@%  | utf8mb4              | utf8mb4_general_ci   | utf8mb4_general_ci |
| emp_AFTER_UPDATE | UPDATE | emp   | BEGIN
INSERT INTO emp_logs (id, operation, operation_time, operation_id, operation_params) values (NULL, 'update', now(), new.id, concat('修改前(id: ', old.id, 'name: ', old.name, ', age: ', old.age, ', salary: ', old.salary, '),修改后(', new.id, 'name: ', new.name, ', age: ', new.age, ', salary: ', new.salary));
END           | AFTER  | 2020-06-27 13:23:46.47 | ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION | root@%  | utf8mb4              | utf8mb4_general_ci   | utf8mb4_general_ci |
+------------------+--------+-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------+---------+----------------------+----------------------+--------------------+
2 rows in set (0.00 sec)mysql> 
mysql> show triggers\G;
*************************** 1. row ***************************Trigger: emp_AFTER_INSERTEvent: INSERTTable: empStatement: BEGIN
INSERT INTO emp_logs (id, operation, operation_time, operation_id, operation_params) values (NULL, 'insert', now(), new.id, concat('插入后(id: ', new.id, 'name: ', new.name, ', age: ', new.age, ', salary: ', new.salary, ')'));
ENDTiming: AFTERCreated: 2020-06-27 13:14:05.60sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTIONDefiner: root@%
character_set_client: utf8mb4
collation_connection: utf8mb4_general_ciDatabase Collation: utf8mb4_general_ci
*************************** 2. row ***************************Trigger: emp_AFTER_UPDATEEvent: UPDATETable: empStatement: BEGIN
INSERT INTO emp_logs (id, operation, operation_time, operation_id, operation_params) values (NULL, 'update', now(), new.id, concat('修改前(id: ', old.id, 'name: ', old.name, ', age: ', old.age, ', salary: ', old.salary, '),修改后(', new.id, 'name: ', new.name, ', age: ', new.age, ', salary: ', new.salary));
ENDTiming: AFTERCreated: 2020-06-27 13:23:46.47sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTIONDefiner: root@%
character_set_client: utf8mb4
collation_connection: utf8mb4_general_ciDatabase Collation: utf8mb4_general_ci
2 rows in set (0.00 sec)ERROR:
No query specifiedmysql> 

MySQL 体系结构。

存储引擎。

和大多数的数据库不同,MySQL 中有一个存储引擎的概念,针对不同的存储需求可以选择最优的存储引擎。

存储引擎就是存储数据,建立索引,更新查询数据等等技术的实现方式。存储引擎是基于表的,而不是基于厍的。所以存储引擎也可被称为表类型

Oracle、sqlServer 等数据库只有一种存储引。MySQL 提供了插件式的存储引擎架构。所以 MySQL 存在多种存储引擎,可以根据需要使用相应引擎,或者编写存储引擎。

MySQL 5.0 支持的存储引擎包含:lnnoDB、MylSAM、BDB、MEMORY、MERGE、EXAMPLE、NDB Cluster、ARCHIVE、CSV、BLACKHOLE、FEDERATED 等,其中 InnoDB 和 BDB 提供事务安全表,其他存储引擎是非事务安全表。

可以通过指令 show engines,来查询当前数据库支持的存储引擎。

mysql> show engines;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| Engine             | Support | Comment                                                        | Transactions | XA   | Savepoints |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| InnoDB             | DEFAULT | Supports transactions, row-level locking, and foreign keys     | YES          | YES  | YES        |
| MRG_MyISAM         | YES     | Collection of identical MyISAM tables                          | NO           | NO   | NO         |
| MEMORY             | YES     | Hash based, stored in memory, useful for temporary tables      | NO           | NO   | NO         |
| BLACKHOLE          | YES     | /dev/null storage engine (anything you write to it disappears) | NO           | NO   | NO         |
| MyISAM             | YES     | MyISAM storage engine                                          | NO           | NO   | NO         |
| CSV                | YES     | CSV storage engine                                             | NO           | NO   | NO         |
| ARCHIVE            | YES     | Archive storage engine                                         | NO           | NO   | NO         |
| PERFORMANCE_SCHEMA | YES     | Performance Schema                                             | NO           | NO   | NO         |
| FEDERATED          | NO      | Federated MySQL storage engine                                 | NULL         | NULL | NULL       |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
9 rows in set (0.00 sec)mysql> 
mysql> show variables like '%engine%';
+----------------------------------+--------+
| Variable_name                    | Value  |
+----------------------------------+--------+
| default_storage_engine           | InnoDB |
| default_tmp_storage_engine       | InnoDB |
| disabled_storage_engines         |        |
| internal_tmp_disk_storage_engine | InnoDB |
+----------------------------------+--------+
4 rows in set (0.02 sec)
对比。
特点 InnoDB MyISAM MEMORY MERGE NDB
存储限制 64 TB 没有
事务安全 支持
锁机制 支持(适合高并发) 表锁 表锁 表锁 行锁
B 树索引 支持 支持 支持 支持 支持
哈希索引 支持
全文索引 支持(5.6 版本以后) 支持
集群索引 支持
数据索引 支持 支持 支持
索引缓存 支持 支持 支持 支持 支持
数据可压缩 支持
空间使用 N/A
内存使用 中等
批量插入速度
支持外键 支持
InnoDB。
事务。
CREATE SCHEMA `demo02` DEFAULT CHARACTER SET utf8 ;CREATE TABLE `demo02`.`goods_innodb` (`id` INT NOT NULL AUTO_INCREMENT,`name` VARCHAR(20) NOT NULL,PRIMARY KEY (`id`));INSERT INTO `demo02`.`goods_innodb` (`name`) VALUES ('Mate20');

MySQL 事务默认自动提交。可重复读。

如果显式 start transaction;

则必须手动提交 commit

外键。
CREATE TABLE `demo02`.`country_innodb` (`country_id` INT NOT NULL AUTO_INCREMENT,`country_name` VARCHAR(100) NOT NULL,PRIMARY KEY (`country_id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;CREATE TABLE `demo02`.`city_innodb` (`city_id` INT NOT NULL AUTO_INCREMENT,`city_name` VARCHAR(45) NOT NULL,`country_id` INT NOT NULL,PRIMARY KEY (`city_id`),INDEX `idx_fk_country_id` (`country_id` ASC),CONSTRAINT `fk_city_country`FOREIGN KEY (`country_id`)REFERENCES `demo02`.`country_innodb` (`country_id`)ON DELETE RESTRICTON UPDATE CASCADE)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
mysql> desc country_innodb;
+--------------+--------------+------+-----+---------+----------------+
| Field        | Type         | Null | Key | Default | Extra          |
+--------------+--------------+------+-----+---------+----------------+
| country_id   | int(11)      | NO   | PRI | NULL    | auto_increment |
| country_name | varchar(100) | NO   |     | NULL    |                |
+--------------+--------------+------+-----+---------+----------------+
2 rows in set (0.02 sec)mysql> desc city_innodb;
+------------+-------------+------+-----+---------+----------------+
| Field      | Type        | Null | Key | Default | Extra          |
+------------+-------------+------+-----+---------+----------------+
| city_id    | int(11)     | NO   | PRI | NULL    | auto_increment |
| city_name  | varchar(50) | NO   |     | NULL    |                |
| country_id | int(11)     | NO   | MUL | NULL    |                |
+------------+-------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)INSERT INTO `demo02`.`country_innodb` (`country_name`) VALUES ('China');
INSERT INTO `demo02`.`country_innodb` (`country_name`) VALUES ('America');
INSERT INTO `demo02`.`country_innodb` (`country_name`) VALUES ('Japan');INSERT INTO `demo02`.`city_innodb` (`city_name`, `country_id`) VALUES ('Xian', '1');
INSERT INTO `demo02`.`city_innodb` (`city_name`, `country_id`) VALUES ('NewYork', '2');
INSERT INTO `demo02`.`city_innodb` (`city_name`, `country_id`) VALUES ('BeiJing', '1');
  • ON DELETE RESTRICT
    删除主表数据时,如果有关联记录,不删除。
    ON UPDATE CASCADE
    更新主表时,如果子表有关联记录,更新子表记录。
Executing:
DELETE FROM `demo02`.`country_innodb` WHERE (`country_id` = '2');ERROR 1451: 1451: Cannot delete or update a parent row: a foreign key constraint fails (`demo02`.`city_innodb`, CONSTRAINT `fk_city_country` FOREIGN KEY (`country_id`) REFERENCES `country_innodb` (`country_id`) ON UPDATE CASCADE)
SQL Statement:
DELETE FROM `demo02`.`country_innodb` WHERE (`country_id` = '2')Operation failed: There was an error while applying the SQL script to the database.
UPDATE `demo02`.`country_innodb` SET `country_id` = '100' WHERE (`country_id` = '1');

[geek@192 ~]$ sudo ls /var/lib/mysql/
192-slow.log     demo_01     ibtmp1          public_key.pem
auto.cnf     demo02      mysql           sakila
ca-key.pem   ib_buffer_pool  mysql.sock      server-cert.pem
ca.pem       ibdata1     mysql.sock.lock     server-key.pem
client-cert.pem  ib_logfile0     performance_schema  sys
client-key.pem   ib_logfile1     private_key.pem     testdb
[geek@192 ~]$ sudo ls -l /var/lib/mysql/demo02
total 344
-rw-r-----. 1 mysql mysql   8648 Jun 27 11:48 city_innodb.frm
-rw-r-----. 1 mysql mysql 114688 Jun 27 12:03 city_innodb.ibd
-rw-r-----. 1 mysql mysql   8618 Jun 27 11:43 country_innodb.frm
-rw-r-----. 1 mysql mysql  98304 Jun 27 12:03 country_innodb.ibd
-rw-r-----. 1 mysql mysql     61 Jun 27 11:38 db.opt
-rw-r-----. 1 mysql mysql   8586 Jun 27 11:39 goods_innodb.frm
-rw-r-----. 1 mysql mysql  98304 Jun 27 11:40 goods_innodb.ibd
[geek@192 ~]$ 
  • .frm ~ 表结构。
  • .ibd ~ 数据和索引。
MyISAM ~ 不支持事务。
CREATE TABLE `demo02`.`goods_MyISAM` (`id` INT NOT NULL AUTO_INCREMENT,`name` VARCHAR(20) NOT NULL,PRIMARY KEY (`id`))
ENGINE = MyISAM
DEFAULT CHARACTER SET = utf8;

start transaction; 后不提交也可以查询到。(没有事务)。

[geek@192 ~]$ sudo ls -l /var/lib/mysql/demo02
[sudo] password for geek:
total 360
-rw-r-----. 1 mysql mysql   8648 Jun 27 11:48 city_innodb.frm
-rw-r-----. 1 mysql mysql 114688 Jun 27 12:03 city_innodb.ibd
-rw-r-----. 1 mysql mysql   8618 Jun 27 11:43 country_innodb.frm
-rw-r-----. 1 mysql mysql  98304 Jun 27 12:03 country_innodb.ibd
-rw-r-----. 1 mysql mysql     61 Jun 27 11:38 db.opt
-rw-r-----. 1 mysql mysql   8586 Jun 27 11:39 goods_innodb.frm
-rw-r-----. 1 mysql mysql  98304 Jun 27 11:40 goods_innodb.ibd
-rw-r-----. 1 mysql mysql   8586 Jun 27 12:18 goods_MyISAM.frm
-rw-r-----. 1 mysql mysql      0 Jun 27 12:18 goods_MyISAM.MYD
-rw-r-----. 1 mysql mysql   1024 Jun 27 12:18 goods_MyISAM.MYI
  • .MYD(MYData) ~ 表数据。
  • .MYI(MYIndex) ~ 索引。
MEMORY。

Memory 存储引擎将表的数据存放在内存中。每个 MEMORY 表实际对应一个磁盘文件,格式 .frm ,该文件中只存储表的结构,而其数据文件,都是存在内存中,这样有利于数据的快速处理,提高整个表的效率。MEMORY 类型的表访问非常地快,因为 ta 的数据是存放在内存中的,并且默认使用 HASH 索引。但是服务一旦关闭,表中的数据就会丢失。

MERGE。

MERGE 存储引擎是一组 MyISAM 表的组合,这些 MyISAM 表必须结构完全相同,MERGE 表本身并没有存储数据,对 MERGE 类型的表可以进行查询、更新、删除操作,这些操作实际上是对内部的 MylSAM 表进行的。

对于 MERGE 类型表的插入操作,是通过 INSERT-METHOD 子句定义插入的表,可以有3 个不同的值,使用 FIRST 或 LAST 值使得插入操作被相应地作用在第一或者最后一个表上,不定义这个子句或者定义为 NO,表示不能对这个 MERGE 表执行适入操作。

可以对 MERGE 表进行 DROP 操作,但是这个操作只是删除 MERGE 表的定义,对内部的表是没有任何影响的。

MyISAM & MERGE。

创建三个测试表,前两个是 MyISAM 引擎,payment_2020, payment_2021。payment_all 是前两个表的 MERGE 表。

CREATE TABLE `demo02`.`order_2020` (`order_id` INT NOT NULL,`order_money` DOUBLE(10,2) NULL,`order_address` VARCHAR(45) NULL,PRIMARY KEY (`order_id`))
ENGINE = MyISAM
DEFAULT CHARACTER SET = utf8;
CREATE TABLE `demo02`.`order_2021` (`order_id` INT NOT NULL,`order_money` DOUBLE(10,2) NULL,`order_address` VARCHAR(45) NULL,PRIMARY KEY (`order_id`))
ENGINE = MyISAM
DEFAULT CHARACTER SET = utf8;
CREATE TABLE `demo02`.`order_all` (`order_id` INT NOT NULL,`order_money` DOUBLE(10,2) NULL,`order_address` VARCHAR(45) NULL,PRIMARY KEY (`order_id`))
ENGINE = MRG_MyISAM
DEFAULT CHARACTER SET = utf8
INSERT_METHOD = LAST
UNION = (order_2020, order_2021);
INSERT INTO `demo02`.`order_2020` (`order_id`, `order_money`, `order_address`) VALUES ('1', '100', '北京');
INSERT INTO `demo02`.`order_2020` (`order_id`, `order_money`, `order_address`) VALUES ('2', '100', '上海');INSERT INTO `demo02`.`order_2021` (`order_id`, `order_money`, `order_address`) VALUES ('10', '200', '北京');
INSERT INTO `demo02`.`order_2021` (`order_id`, `order_money`, `order_address`) VALUES ('11', '200', '上海');

order_all 中的数据。

存储引擎的选择。

在选择存储引擎时,应该根据应系统的特点选择台适的存储引擎。对于复杂的应用系统,还可以根据实际情况选择多种存储引擎进行组合。以下是几种常用的存储引擎的使用环境。

  • lnnoDB:是 MySQL 的默认存储引擎,用于事务处理应用程序,支持外键。如果应用对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,数据操作除了插入和查询以外,还包含很多的更新、删除操作。 那么 InnoDB 存储引擎是比较台适的选择。 InnoDB 存储引擎除了有效的降低由于删除和更新导致的锁定,还可以确保事务的完整提交和回滚,对于类似于计费系统或者财务系统等对数据准确性要求比较高的系统,InnoDB 是最合适的选择。
  • MylSAM:如果应用是以读操作和插入操作为主,只有很少的更新和删除操作,并且对事务的完整性、并发性要求不是很高,那么选择这个存储引擎是非常台适的。
  • MEMORY:将所有数据保存在 RAM 中,在要快速定位记录和其他类似数据环境下,可以提供极快的访问。MEMORY 的缺陷就是对表的大小有限制。太大的表无法缓存在内存中,其次是要确保表的数据可以恢复,数据库异常停止后表中的数据是可以恢复的。MEMORY 表通常用于更新不太频繁的小表,用以快速得到访问结果。
  • MERGE:用于将一系列等同的 MylSAM 以逻辑方式组合在一起,并作为一个对象引用他们。MERGE 表的优点在于可以突破对单个从MyISAM 表的大小限制,并且通过将不同的表分布在多个磁盘上。可以有效的改善 MERGE 表的访问效率。这对于存储诸如数据仓储等 VLDB 环境十分合适。

优化 SQL 步骤。

在应用的的开发过程中,由于初期数据量小,开友人员写 SQL 语句时更重视功能上的实现,但是当应用系统正式上线后,随着生产数据量的急剧增长,很多 SQL 语句开始逐渐显露出性能问题,对生产的影晌也越采越大。此时这些有问题的 SQL 语句就成为整个系统性能的瓶颈,因此我们必须要对 ta 们进行优化。

当面对一个有 SQL 性能问题的数据库时,我们应该从何处入手来进行系统的分析,使得能够尽快定位问题 SQL 并尽快解决问题。

查看 SQL 执行频率。

MySQL 客户端连接成功后,通过 SHOW [GLOBAL | SESSION] STATUS 命令可以提供务器状态信息。SHOW [GLOBAL | SESSION] STATUS 可以根需要加上参数 “session” 或者 “global” 来显示 session 级(当前连接)的统计结果和 global 级(自数据库上次启动至今)的统计结果。如果不写,默认使用的参数是 “session”。

下面的命令显示了当前 session 中所有统计参数的值。

当前数据库。

mysql> show status like 'Com_______';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Com_binlog    | 0     |
| Com_commit    | 0     |
| Com_delete    | 0     |
| Com_insert    | 0     |
| Com_repair    | 0     |
| Com_revoke    | 0     |
| Com_select    | 5     |
| Com_signal    | 0     |
| Com_update    | 0     |
| Com_xa_end    | 0     |
+---------------+-------+
10 rows in set (0.01 sec)

全局。

mysql> show global status like 'Com_______';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Com_binlog    | 0     |
| Com_commit    | 10    |
| Com_delete    | 7     |
| Com_insert    | 26    |
| Com_repair    | 0     |
| Com_revoke    | 0     |
| Com_select    | 137   |
| Com_signal    | 0     |
| Com_update    | 12    |
| Com_xa_end    | 0     |
+---------------+-------+
10 rows in set (0.00 sec)

Innodb 类型的表数据。

mysql> show global status like 'Innodb_rows_%';
+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| Innodb_rows_deleted  | 4     |
| Innodb_rows_inserted | 1750  |
| Innodb_rows_read     | 1858  |
| Innodb_rows_updated  | 14    |
+----------------------+-------+
4 rows in set (0.00 sec)
定位低效率的 SQL 语句。
慢查询日志。

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

  • 慢查询日志:通过慢查询日志定位那些执行效率较低的 SQL 语句,用 --log-slow-queries[=file-name] 选项启动时,mysqld 写一个包含所有执行事件超过 long_query_time 秒的 SQL 语句的日志文件。

  • show processlist。慢查询日志在查询结束以后才纪录,所以在应用反映执行效率出现问题的时候查询慢查询日志并不能定位问题,可以使用 show processlist 命令查看当前 MySQL 在进行的线程,包括线程的状态、是否锁表等,可以实时地查看 SQL 的执行情况,同时对一些锁表操作进行优化。(实时)。

mysql> show processlist;
+----+------+---------------------+---------+---------+------+----------+------------------+
| Id | User | Host                | db      | Command | Time | State    | Info             |
+----+------+---------------------+---------+---------+------+----------+------------------+
|  7 | root | localhost           | demo_01 | Query   |    0 | starting | show processlist |
| 10 | root | 192.168.142.1:49414 | demo_01 | Sleep   |  463 |          | NULL             |
| 11 | root | 192.168.142.1:49415 | demo_01 | Sleep   |  463 |          | NULL             |
+----+------+---------------------+---------+---------+------+----------+------------------+
3 rows in set (0.00 sec)

如果是慢查询,State 是 Sending data。

copying to tmp table
sorting result
sending data

explain 分析执行计划。

通过以上步骤查词到效率低的 SQL 语句后,可以通过 EXPLAIN 或者 DES 命令获取 MySQL 如何执行 SELECT 语句的信息,包括在 SELECT 语句执行过程中表如何连接和连接的顺序。

mysql> explain SELECT * FROM demo_01.emp;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
|  1 | SIMPLE      | emp   | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    4 |   100.00 | NULL  |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
  • id。
    select 查询的序列号。是一组数字,表示的是查询中执行 select 子句或者是操作表的顺序。
  • select_type。
    表示 SELECT 的类型,常见的取值有 SIMPLE(简单表,即不使用表连接或者子查询)、PRIMARY(主查间,即外层的查询)、UNION(UNION 中的第二个或者后面的查询语句)、SUBQUERY(子查询中的第一个 SELECT)等。
  • table。
    输出结果集的表。
  • type。
    表示表的连接类型,性能由好到差的连接类型为 system -> const -> eq_ref -> ref -> ref_or_null -> index_merge -> index_subquery -> range -> index -> all。
  • possible keys。
    查询时可能会使用的索引。
  • key。
    实际使用的索引。
  • key_len。
    索引字段的长度。
  • rows。
    扫描行的数量。
  • extra。
    执行情况的说明和描述。
  • table
    这一行的数据是关于哪张表的。
  • type
    这是重要的列,显示连接使用了何种类型。从最好到最差的连接类型为 const、eq-reg、ref、range、index 和 ALL。
  • posible_keys
    可能应用在这张表中的索引。如果为空,没有可能的索引。
  • key
    实际使用的索引。如果为 NULL,则没有使用索引。
  • key_len
    使用的索引的长度。在不损失精确性的情况下,长度越短越好。
  • ref
    显示索引的哪一列被使用了,如果可能的话,是一个常数。
  • rows
    MySql 认为必须检查的用来返回请求数据的行数。
CREATE TABLE `demo_01`.`t_role` (`id` VARCHAR(32) NOT NULL,`role_name` VARCHAR(255) DEFAULT NULL,`role_code` VARCHAR(255) DEFAULT NULL,`description` VARCHAR(255) DEFAULT NULL,PRIMARY KEY (`id`),UNIQUE INDEX `unique_role_name` (`role_name` ASC)
)  ENGINE=INNODB DEFAULT CHARACTER SET=UTF8;CREATE TABLE `t_user` (`id` varchar(32) NOT NULL,`username` varchar(45) NOT NULL,`password` varchar(96) NOT NULL,`name` varchar(45) NOT NULL,PRIMARY KEY (`id`),UNIQUE KEY `unique_user_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8CREATE TABLE `demo_01`.`user_role` (`id` INT(11) NOT NULL AUTO_INCREMENT,`user_id` VARCHAR(32) NULL DEFAULT NULL,`role_id` VARCHAR(32) NULL DEFAULT NULL,PRIMARY KEY (`id`),INDEX `fk_ur_role_id_idx` (`role_id` ASC),INDEX `fk_ur_user_id_idx` (`user_id` ASC),CONSTRAINT `fk_ur_role_id`FOREIGN KEY (`role_id`)REFERENCES `demo_01`.`t_role` (`id`)ON DELETE NO ACTIONON UPDATE NO ACTION,CONSTRAINT `fk_ur_user_id`FOREIGN KEY (`user_id`)REFERENCES `demo_01`.`t_user` (`id`)ON DELETE NO ACTIONON UPDATE NO ACTION)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;INSERT INTO `demo_01`.`t_user` (`id`, `username`, `password`, `name`) VALUES ('1', 'super', 'super', '超级管理员');
INSERT INTO `demo_01`.`t_user` (`id`, `username`, `password`, `name`) VALUES ('2', 'admin', 'admin', '系统管理员');
INSERT INTO `demo_01`.`t_user` (`id`, `username`, `password`, `name`) VALUES ('3', 'geek', 'geek', 'test');
INSERT INTO `demo_01`.`t_user` (`id`, `username`, `password`, `name`) VALUES ('4', 'stu1', 'stu1', '学生1');
INSERT INTO `demo_01`.`t_user` (`id`, `username`, `password`, `name`) VALUES ('5', 'stu2', 'stu2', '学生2');
INSERT INTO `demo_01`.`t_user` (`id`, `username`, `password`, `name`) VALUES ('6', 't1', 't1', '老师1');INSERT INTO `demo_01`.`t_role` (`id`, `role_name`, `role_code`, `description`) VALUES ('5', '学生', 'student', '学生');
INSERT INTO `demo_01`.`t_role` (`id`, `role_name`, `role_code`, `description`) VALUES ('7', '老师', 'teacher', '老师');
INSERT INTO `demo_01`.`t_role` (`id`, `role_name`, `role_code`, `description`) VALUES ('8', '教学管理员', 'teachmanager', '教学管理员');
INSERT INTO `demo_01`.`t_role` (`id`, `role_name`, `role_code`, `description`) VALUES ('9', '管理员', 'admin', '管理员');
INSERT INTO `demo_01`.`t_role` (`id`, `role_name`, `role_code`, `description`) VALUES ('10', '超级管理员', 'super', '超级管理员');INSERT INTO `demo_01`.`user_role` (`user_id`, `role_id`) VALUES ('1', '5');
INSERT INTO `demo_01`.`user_role` (`user_id`, `role_id`) VALUES ('1', '7');
INSERT INTO `demo_01`.`user_role` (`user_id`, `role_id`) VALUES ('2', '8');
INSERT INTO `demo_01`.`user_role` (`user_id`, `role_id`) VALUES ('3', '9');
INSERT INTO `demo_01`.`user_role` (`user_id`, `role_id`) VALUES ('4', '8');
INSERT INTO `demo_01`.`user_role` (`user_id`, `role_id`) VALUES ('5', '10');
mysql> select * from t_user;
+----+----------+----------+-----------------+
| id | username | password | name            |
+----+----------+----------+-----------------+
| 1  | super    | super    | 超级管理员      |
| 2  | admin    | admin    | 系统管理员      |
| 3  | geek     | geek     | test            |
| 4  | stu1     | stu1     | 学生1           |
| 5  | stu2     | stu2     | 学生2           |
| 6  | t1       | t1       | 老师1           |
+----+----------+----------+-----------------+
6 rows in set (0.00 sec)mysql> select * from t_role;
+----+-----------------+--------------+-----------------+
| id | role_name       | role_code    | description     |
+----+-----------------+--------------+-----------------+
| 10 | 超级管理员      | super        | 超级管理员      |
| 5  | 学生            | student      | 学生            |
| 7  | 老师            | teacher      | 老师            |
| 8  | 教学管理员      | teachmanager | 教学管理员      |
| 9  | 管理员          | admin        | 管理员          |
+----+-----------------+--------------+-----------------+
5 rows in set (0.00 sec)mysql> select * from user_role;
+----+---------+---------+
| id | user_id | role_id |
+----+---------+---------+
|  1 | 1       | 5       |
|  2 | 1       | 7       |
|  3 | 2       | 8       |
|  4 | 3       | 9       |
|  5 | 4       | 8       |
|  6 | 5       | 10      |
+----+---------+---------+
6 rows in set (0.00 sec)mysql> 
id。

id 字段是 select 查询的序列号。是一组数字,表示的是查询中执行 select 子句或者是操作表的顺序。

  • id 相同表示加载表的顺序是从上到下。
mysql> explain select * from t_role r, t_user u, user_role ur where r.id = ur.role_id and u.id = ur.user_id;
+----+-------------+-------+------------+--------+-------------------------------------+---------+---------+--------------------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type   | possible_keys                       | key     | key_len | ref                | rows | filtered | Extra                                              |
+----+-------------+-------+------------+--------+-------------------------------------+---------+---------+--------------------+------+----------+----------------------------------------------------+
|  1 | SIMPLE      | r     | NULL       | ALL    | PRIMARY                             | NULL    | NULL    | NULL               |    5 |   100.00 | NULL                                               |
|  1 | SIMPLE      | ur    | NULL       | ALL    | fk_ur_role_id_idx,fk_ur_user_id_idx | NULL    | NULL    | NULL               |    6 |    20.00 | Using where; Using join buffer (Block Nested Loop) |
|  1 | SIMPLE      | u     | NULL       | eq_ref | PRIMARY                             | PRIMARY | 98      | demo_01.ur.user_id |    1 |   100.00 | NULL                                               |
+----+-------------+-------+------------+--------+-------------------------------------+---------+---------+--------------------+------+----------+----------------------------------------------------+
3 rows in set, 1 warning (0.00 sec)
  • id 不同。id 值越大,优先级越高,越先被执行。
mysql> explain select * from t_role where id = (select role_id from user_role where user_id = (select id from t_user where username = 'stu1'));
+----+-------------+-----------+------------+-------+----------------------+----------------------+---------+-------+------+----------+-------------+
| id | select_type | table     | partitions | type  | possible_keys        | key                  | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-----------+------------+-------+----------------------+----------------------+---------+-------+------+----------+-------------+
|  1 | PRIMARY     | t_role    | NULL       | const | PRIMARY              | PRIMARY              | 98      | const |    1 |   100.00 | NULL        |
|  2 | SUBQUERY    | user_role | NULL       | ref   | fk_ur_user_id_idx    | fk_ur_user_id_idx    | 99      | const |    1 |   100.00 | Using where |
|  3 | SUBQUERY    | t_user    | NULL       | const | unique_user_username | unique_user_username | 137     | const |    1 |   100.00 | Using index |
+----+-------------+-----------+------------+-------+----------------------+----------------------+---------+-------+------+----------+-------------+
3 rows in set, 1 warning (0.00 sec)
  • id 有相同,也有不同,同时存在。id 相同的可以认为是一组,从上往下顺序执行。在所有的组中,id 的值越大,优先级越高,越先执行。
mysql> EXPLAIN SELECT * FROM t_role r, (SELECT * FROM user_role ur WHERE ur.user_id = '2') a WHERE r.id = a.role_id;
+----+-------------+-------+------------+--------+-------------------------------------+-------------------+---------+--------------------+------+----------+-------------+
| id | select_type | table | partitions | type   | possible_keys                       | key               | key_len | ref                | rows | filtered | Extra       |
+----+-------------+-------+------------+--------+-------------------------------------+-------------------+---------+--------------------+------+----------+-------------+
|  1 | SIMPLE      | ur    | NULL       | ref    | fk_ur_role_id_idx,fk_ur_user_id_idx | fk_ur_user_id_idx | 99      | const              |    1 |   100.00 | Using where |
|  1 | SIMPLE      | r     | NULL       | eq_ref | PRIMARY                             | PRIMARY           | 98      | demo_01.ur.role_id |    1 |   100.00 | NULL        |
+----+-------------+-------+------------+--------+-------------------------------------+-------------------+---------+--------------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)
select_type。
  • simple。
    简单的 select 查询,查询中不包含子查询或 UNION。
mysql> explain select * from t_user;
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------+
|  1 | SIMPLE      | t_user | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    6 |   100.00 | NULL  |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
  • primary。
    查询中若包含任何复杂的子查询,最外层查询标记为该标识。
mysql> explain select * from t_user where id = (select id from user_role where role_id = '9');
+----+-------------+-----------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys     | key               | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------------+
|  1 | PRIMARY     | t_user    | NULL       | ALL  | PRIMARY           | NULL              | NULL    | NULL  |    6 |    16.67 | Using where |
|  2 | SUBQUERY    | user_role | NULL       | ref  | fk_ur_role_id_idx | fk_ur_role_id_idx | 99      | const |    1 |   100.00 | Using index |
+----+-------------+-----------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------------+
2 rows in set, 3 warnings (0.00 sec)
  • subquery。
    在 select 或 where 列表中包含了子查询。
mysql> explain select * from t_user where id = (select id from user_role where role_id = '9');
+----+-------------+-----------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys     | key               | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------------+
|  1 | PRIMARY     | t_user    | NULL       | ALL  | PRIMARY           | NULL              | NULL    | NULL  |    6 |    16.67 | Using where |
|  2 | SUBQUERY    | user_role | NULL       | ref  | fk_ur_role_id_idx | fk_ur_role_id_idx | 99      | const |    1 |   100.00 | Using index |
+----+-------------+-----------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------------+
2 rows in set, 3 warnings (0.00 sec)
  • derived。
    在 from 列表中包含的子查询,被标记为 DERIVED。MySQL 会递归执行这些子查询,把结果放在临时表中。
mysql> select * from (select * from t_user where id in ('1', '2'));
ERROR 1248 (42000): Every derived table must have its own alias
mysql> select a.* from (select * from t_user where id in ('1', '2')) a;
+----+----------+----------+-----------------+
| id | username | password | name            |
+----+----------+----------+-----------------+
| 1  | super    | super    | 超级管理员      |
| 2  | admin    | admin    | 系统管理员      |
+----+----------+----------+-----------------+
2 rows in set (0.00 sec)mysql> explain select a.* from (select * from t_user where id in ('1', '2')) a;
+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table  | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra       |
+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | t_user | NULL       | range | PRIMARY       | PRIMARY | 98      | NULL |    2 |   100.00 | Using where |
+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.01 sec)
  • union。
    若第二个 select 出现在 union 之后,则标记为 union。若 union 包含在 from 子句的子查询中,外层 select 将被标记为 derived。
  • union result。
    从 union 表获取结果的 select。
mysql> explain select * from t_user where id = '1' union select * from t_user where id = '2';
+----+--------------+------------+------------+-------+---------------+---------+---------+-------+------+----------+-----------------+
| id | select_type  | table      | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra           |
+----+--------------+------------+------------+-------+---------------+---------+---------+-------+------+----------+-----------------+
|  1 | PRIMARY      | t_user     | NULL       | const | PRIMARY       | PRIMARY | 98      | const |    1 |   100.00 | NULL            |
|  2 | UNION        | t_user     | NULL       | const | PRIMARY       | PRIMARY | 98      | const |    1 |   100.00 | NULL            |
| NULL | UNION RESULT | <union1,2> | NULL       | ALL   | NULL          | NULL    | NULL    | NULL  | NULL |     NULL | Using temporary |
+----+--------------+------------+------------+-------+---------------+---------+---------+-------+------+----------+-----------------+
3 rows in set, 1 warning (0.00 sec)
table。

展示这一行数据是关于哪一张表的。

type。

type 显示的是访问类型,是较为重要的一个指标。

NULL > system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > all。

type 含义
NULL MySQL 不访问任何表、索引,直接返回结果。
system 表只有一行记录(等于系统表),这是 const 类型的特例。一般不会出现。
const 表示通过索引一次就找到了,const 用于比较 primary key 或者 unique 索引。因为只匹配一行数据,所以很快。如将主键置于 where 列表中,MySQL 就能将该查询转换为一个常量。const 于将“主键”或“唯一”索引的所有部分与常量进行比较。
eq-ref 类似 ref,区别在于使用的是唯一索引,使用主踺的关联查询,关联查询出的记录只有一条。常见于主键或唯一索引扫描。
ref 非唯一索引扫描。返回匹配某个单独值的所有行。本质上也是一种索引访问,返回所有匹配某个单独值的所有行(多个)。
range 只检索给定返回的行,便用一个索引来选择行。where 之后出现 between,<,>,in 等操作。
index 与 ALL 的区别为 index 类型只是遍历了索引树,通常比 ALL 快,ALL 是遍历数据文件。
all 将遍历全表以找到匹配的行。
mysql> explain select now();
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra          |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
|  1 | SIMPLE      | NULL  | NULL       | NULL | NULL          | NULL | NULL    | NULL | NULL |     NULL | No tables used |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
1 row in set, 1 warning (0.00 sec)

const | 表示通过索引一次就找到了,const 用于比较 primary key 或者 unique 索引。因为只匹配一行数据,所以很快。如将主键置于 where 列表中,MySQL 就能将该查询转换为一个常量。const 于将“主键”或“唯一”索引的所有部分与常量进行比较。

mysql> explain select * from t_user where username = 'stu1';
+----+-------------+--------+------------+-------+----------------------+----------------------+---------+-------+------+----------+-------+
| id | select_type | table  | partitions | type  | possible_keys        | key                  | key_len | ref   | rows | filtered | Extra |
+----+-------------+--------+------------+-------+----------------------+----------------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | t_user | NULL       | const | unique_user_username | unique_user_username | 137     | const |    1 |   100.00 | NULL  |
+----+-------------+--------+------------+-------+----------------------+----------------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
key。
  • possible_keys。
    可能使用到的索引,一个或多个。

  • key。
    实际使用的索引,如果为 NULL,则没有用索引。

  • key_len。
    索引中使用的字节数,该值为索引字段最大可能长度,并非实际使用长度,在不损失精度的前提下,长度越短越好。

rows。

扫描行的数量。

如果使用了索引,只扫描一行。否则全部行扫描。

mysql> explain select * from t_user where name = 'a';
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | t_user | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    6 |    16.67 | Using where |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)mysql> create index idx_user_name on t_user(name);
Query OK, 0 rows affected (0.02 sec)
Records: 0  Duplicates: 0  Warnings: 0mysql> explain select * from t_user where name = 'a';
+----+-------------+--------+------------+------+---------------+---------------+---------+-------+------+----------+-------+
| id | select_type | table  | partitions | type | possible_keys | key           | key_len | ref   | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+---------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | t_user | NULL       | ref  | idx_user_name | idx_user_name | 137     | const |    1 |   100.00 | NULL  |
+----+-------------+--------+------------+------+---------------+---------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
extra。

其他额外的执行计划信息。在该列展示。

extra 含义
using filesort MySQL 会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取,称为“文件排序”,效率低。
using temporary 使用了临时表保存中间结果。MySQL 在对查间结果排序时使用临时表。常见于 order by 和 group by,效率低。
using index 表示相应的 select 操作使用了覆盖索引,避免访问表的数据行,效率不错。
-- 解决:给 name 建立索引。
mysql> explain select * from t_user order by username;
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra          |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------+
|  1 | SIMPLE      | t_user | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    6 |   100.00 | Using filesort |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------+
1 row in set, 1 warning (0.00 sec)-- 建立了索引,select * 也不起作用,要 select name。
mysql> explain select username from t_user order by username;
+----+-------------+--------+------------+-------+---------------+----------------------+---------+------+------+----------+-------------+
| id | select_type | table  | partitions | type  | possible_keys | key                  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+--------+------------+-------+---------------+----------------------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | t_user | NULL       | index | NULL          | unique_user_username | 137     | NULL |    6 |   100.00 | Using index |
+----+-------------+--------+------------+-------+---------------+----------------------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
mysql> explain select * from t_user where name = 'a';
+----+-------------+--------+------------+------+---------------+---------------+---------+-------+------+----------+-------+
| id | select_type | table  | partitions | type | possible_keys | key           | key_len | ref   | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+---------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | t_user | NULL       | ref  | idx_user_name | idx_user_name | 137     | const |    1 |   100.00 | NULL  |
+----+-------------+--------+------------+------+---------------+---------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)mysql> explain select name from t_user where name = 'a';
+----+-------------+--------+------------+------+---------------+---------------+---------+-------+------+----------+-------------+
| id | select_type | table  | partitions | type | possible_keys | key           | key_len | ref   | rows | filtered | Extra       |
+----+-------------+--------+------------+------+---------------+---------------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | t_user | NULL       | ref  | idx_user_name | idx_user_name | 137     | const |    1 |   100.00 | Using index |
+----+-------------+--------+------------+------+---------------+---------------+---------+-------+------+----------+-------------+
show profile 分析 sql。

MySQL 从 5.0.37 版本开始对 show profiles 和 show profile 语句的支持。帮我们了解时间都耗费在哪儿里了。

  • 查看是否支持。
mysql> select @@have_profiling;
+------------------+
| @@have_profiling |
+------------------+
| YES              |
+------------------+
1 row in set, 1 warning (0.00 sec)
  • 开启。1 开启。0 关闭。
mysql> select @@profiling;
+-------------+
| @@profiling |
+-------------+
|           0 |
+-------------+
1 row in set, 1 warning (0.00 sec)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)

之后执行的语句及其耗费时间都会被记录下来。

查看 sql 耗时。

mysql> show profiles;
+----------+------------+----------------------------------+
| Query_ID | Duration   | Query                            |
+----------+------------+----------------------------------+
|        1 | 0.00024250 | select @@profiling               |
|        2 | 0.00014950 | SELECT DATABASE()                |
|        3 | 0.00023050 | show databases                   |
|        4 | 0.00010450 | show tables                      |
|        5 | 0.00025275 | select * from t_user             |
|        6 | 0.00016300 | select count(*) from t_user      |
|        7 | 0.00023950 | show tables                      |
|        8 | 0.00028950 | select count(*) from city_innodb |
+----------+------------+----------------------------------+
8 rows in set, 1 warning (0.00 sec)
  • 进一步详细耗时。
mysql> show profile for query 8;
+----------------------+----------+
| Status               | Duration |
+----------------------+----------+
| starting             | 0.000055 |
| checking permissions | 0.000005 |
| Opening tables       | 0.000016 |
| init                 | 0.000044 |
| System lock          | 0.000009 |
| optimizing           | 0.000004 |
| statistics           | 0.000011 |
| preparing            | 0.000009 |
| executing            | 0.000002 |
| Sending data         | 0.000101 |
| end                  | 0.000006 |
| query end            | 0.000006 |
| closing tables       | 0.000005 |
| freeing items        | 0.000009 |
| cleaning up          | 0.000010 |
+----------------------+----------+
15 rows in set, 1 warning (0.00 sec)

all ~ 包含具体详细信息。

show profile cpu for query 8;

mysql> show profile all for query 8;
+----------------------+----------+----------+------------+-------------------+---------------------+--------------+---------------+---------------+-------------------+-------------------+-------------------+-------+-----------------------+----------------------+-------------+
| Status               | Duration | CPU_user | CPU_system | Context_voluntary | Context_involuntary | Block_ops_in | Block_ops_out | Messages_sent | Messages_received | Page_faults_major | Page_faults_minor | Swaps | Source_function       | Source_file          | Source_line |
+----------------------+----------+----------+------------+-------------------+---------------------+--------------+---------------+---------------+-------------------+-------------------+-------------------+-------+-----------------------+----------------------+-------------+
| starting             | 0.000055 | 0.000015 |   0.000031 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | NULL                  | NULL                 |        NULL |
| checking permissions | 0.000005 | 0.000001 |   0.000003 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | check_access          | sql_authorization.cc |         809 |
| Opening tables       | 0.000016 | 0.000005 |   0.000010 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | open_tables           | sql_base.cc          |        5781 |
| init                 | 0.000044 | 0.000014 |   0.000027 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | handle_query          | sql_select.cc        |         128 |
| System lock          | 0.000009 | 0.000002 |   0.000005 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | mysql_lock_tables     | lock.cc              |         330 |
| optimizing           | 0.000004 | 0.000001 |   0.000002 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | optimize              | sql_optimizer.cc     |         158 |
| statistics           | 0.000011 | 0.000004 |   0.000007 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | optimize              | sql_optimizer.cc     |         374 |
| preparing            | 0.000009 | 0.000003 |   0.000005 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | optimize              | sql_optimizer.cc     |         482 |
| executing            | 0.000002 | 0.000000 |   0.000001 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | exec                  | sql_executor.cc      |         126 |
| Sending data         | 0.000101 | 0.000032 |   0.000063 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | exec                  | sql_executor.cc      |         202 |
| end                  | 0.000006 | 0.000001 |   0.000003 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | handle_query          | sql_select.cc        |         206 |
| query end            | 0.000006 | 0.000002 |   0.000003 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | mysql_execute_command | sql_parse.cc         |        4956 |
| closing tables       | 0.000005 | 0.000001 |   0.000003 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | mysql_execute_command | sql_parse.cc         |        5009 |
| freeing items        | 0.000009 | 0.000003 |   0.000006 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | mysql_parse           | sql_parse.cc         |        5622 |
| cleaning up          | 0.000010 | 0.000003 |   0.000006 |                 0 |                   0 |            0 |             0 |             0 |                 0 |                 0 |                 0 |     0 | dispatch_command      | sql_parse.cc         |        1931 |
+----------------------+----------+----------+------------+-------------------+---------------------+--------------+---------------+---------------+-------------------+-------------------+-------------------+-------+-----------------------+----------------------+-------------+
15 rows in set, 1 warning (0.00 sec)
trace 分析优化器执行计划。

MySQL 5.6 提供了对 SQL 的跟踪 trace,通过 trace 文件能够进一步了解为什么优化器选择 A 计划,而不是选择 B 计划。

打开 trace 设置格式为 JSON,并设置 trace 最大能够使用的内存大小,避免解析过程中因为默认内存过小而不能够完整展示。

SET optimizer_trace = “enabled=on”, end_markers_in_json = on;
SET optimizer_trace_max_mem_size=1000000;

mysql> SET optimizer_trace = "enabled=on", end_markers_in_json = on;
Query OK, 0 rows affected (0.00 sec)mysql> SET optimizer_trace_max_mem_size = 1000000;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from information_schema.optimizer_trace\G;
*************************** 1. row ***************************QUERY: select * from city_innodbTRACE: {"steps": [{"join_preparation": {"select#": 1,"steps": [{"expanded_query": "/* select#1 */ select `city_innodb`.`city_id` AS `city_id`,`city_innodb`.`city_name` AS `city_name`,`city_innodb`.`country_id` AS `country_id` from `city_innodb`"}] /* steps */} /* join_preparation */},{"join_optimization": {"select#": 1,"steps": [{"table_dependencies": [{"table": "`city_innodb`","row_may_be_null": false,"map_bit": 0,"depends_on_map_bits": [] /* depends_on_map_bits */}] /* table_dependencies */},{"rows_estimation": [{"table": "`city_innodb`","table_scan": {"rows": 1,"cost": 1} /* table_scan */}] /* rows_estimation */},{"considered_execution_plans": [{"plan_prefix": [] /* plan_prefix */,"table": "`city_innodb`","best_access_path": {"considered_access_paths": [{"rows_to_scan": 1,"access_type": "scan","resulting_rows": 1,"cost": 1.2,"chosen": true}] /* considered_access_paths */} /* best_access_path */,"condition_filtering_pct": 100,"rows_for_plan": 1,"cost_for_plan": 1.2,"chosen": true}] /* considered_execution_plans */},{"attaching_conditions_to_tables": {"original_condition": null,"attached_conditions_computation": [] /* attached_conditions_computation */,"attached_conditions_summary": [{"table": "`city_innodb`","attached": null}] /* attached_conditions_summary */} /* attaching_conditions_to_tables */},{"refine_plan": [{"table": "`city_innodb`"}] /* refine_plan */}] /* steps */} /* join_optimization */},{"join_execution": {"select#": 1,"steps": [] /* steps */} /* join_execution */}] /* steps */
}
MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0INSUFFICIENT_PRIVILEGES: 0
1 row in set (0.00 sec)ERROR:
No query specifiedmysql> 

索引使用。

  • 准备 300W 条数据。
CREATE TABLE `demo_01`.`tb_item` (`id` INT NOT NULL COMMENT '商品 id。',`title` VARCHAR(45) NOT NULL COMMENT '商品标题。',`price` DECIMAL(20,2) NOT NULL COMMENT '商品价格,单位:元。',`num` INT NOT NULL COMMENT '库存数量。',`categoryid` BIGINT NOT NULL COMMENT '所属类目。',`status` VARCHAR(1) NOT NULL COMMENT '商品状态。1~正常,2~下架,3~删除。',`sellerid` VARCHAR(45) NOT NULL COMMENT '商家id。',`createtime` DATETIME NOT NULL COMMENT '创建时间。',`updatetime` DATETIME NOT NULL,PRIMARY KEY (`id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8
COMMENT = '商品表。';
USE `demo_01`;
DROP procedure IF EXISTS `insert_tb_item`;DELIMITER $$
USE `demo_01`$$
CREATE DEFINER=`root`@`%` PROCEDURE `insert_tb_item`()
BEGINDECLARE num INT DEFAULT 1;WHILE num <= 3000000 DOINSERT INTO tb_item VALUES (num, concat('商品 ', num, ' 号'), round(rand() * 100, 2), floor(rand() * 100), floor(rand() * 10), '1', '2450702011', '2020-09-09 19:19:19','2020-09-09 19:19:19');SET num = num + 1;END WHILE;END$$DELIMITER ;
call demo_01.insert_tb_item();

SQL 优化。

索引的使用。
mysql> select count(*) from tb_item;
+----------+
| count(*) |
+----------+
|  3000000 |
+----------+
1 row in set (1.07 sec)

title 没有索引的情况下。

mysql> select * from tb_item where title = '商品 1 号';
+----+--------------+-------+-----+------------+--------+------------+---------------------+---------------------+
| id | title        | price | num | categoryid | status | sellerid   | createtime          | updatetime          |
+----+--------------+-------+-----+------------+--------+------------+---------------------+---------------------+
|  1 | 商品 1 号    | 26.54 |  44 |          4 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
+----+--------------+-------+-----+------------+--------+------------+---------------------+---------------------+
1 row in set (1.44 sec)

id 有主键索引。

mysql> select * from tb_item where id = 11111;
+-------+------------------+-------+-----+------------+--------+------------+---------------------+---------------------+
| id    | title            | price | num | categoryid | status | sellerid   | createtime          | updatetime          |
+-------+------------------+-------+-----+------------+--------+------------+---------------------+---------------------+
| 11111 | 商品 11111 号    | 92.00 |  73 |          9 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
+-------+------------------+-------+-----+------------+--------+------------+---------------------+---------------------+
1 row in set (0.00 sec)
  • 给 title 创建索引。
ALTER TABLE `demo_01`.`tb_item`
ADD INDEX `idx_item_title` (`title` ASC);
;
mysql> create index idx_item_title on tb_item(title);
Query OK, 0 rows affected (8.60 sec)
Records: 0  Duplicates: 0  Warnings: 0
mysql> select * from tb_item where title = '商品 222 号';
+-----+----------------+-------+-----+------------+--------+------------+---------------------+---------------------+
| id  | title          | price | num | categoryid | status | sellerid   | createtime          | updatetime          |
+-----+----------------+-------+-----+------------+--------+------------+---------------------+---------------------+
| 222 | 商品 222 号    |  1.20 |  35 |          7 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
+-----+----------------+-------+-----+------------+--------+------------+---------------------+---------------------+
1 row in set (0.00 sec)
创建索引语法。
CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name[index_type]ON tbl_name (key_part,...)[index_option][algorithm_option | lock_option] ...key_part: {col_name [(length)] | (expr)} [ASC | DESC]index_option: {KEY_BLOCK_SIZE [=] value| index_type| WITH PARSER parser_name| COMMENT 'string'| {VISIBLE | INVISIBLE}| ENGINE_ATTRIBUTE [=] 'string'| SECONDARY_ENGINE_ATTRIBUTE [=] 'string'
}index_type:USING {BTREE | HASH}algorithm_option:ALGORITHM [=] {DEFAULT | INPLACE | COPY}lock_option:LOCK [=] {DEFAULT | NONE | SHARED | EXCLUSIVE}
避免索引失效。
CREATE TABLE `tb_seller` (`sellerid` varchar(45) NOT NULL,`name` varchar(45) DEFAULT NULL,`nickname` varchar(45) DEFAULT NULL,`password` varchar(45) DEFAULT NULL,`status` varchar(1) DEFAULT NULL,`address` varchar(45) DEFAULT NULL,`createtime` datetime DEFAULT NULL,PRIMARY KEY (`sellerid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4INSERT INTO `demo_01`.`tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) VALUES ('alibaba', '阿里巴巴', '阿里小店', '123', '1', '武汉', '2099-09-09 09:09:09');
INSERT INTO `demo_01`.`tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) VALUES ('baidu', '百度科技有限公司', '百度小店', '123', '1', '武汉', '2099-09-09 09:09:09');
INSERT INTO `demo_01`.`tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) VALUES ('huawei', '华为科技有限公司', '华为小店', '123', '0', '武汉', '2099-09-09 09:09:09');
INSERT INTO `demo_01`.`tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) VALUES ('itcast', '传智播客', '传智播客', '123', '1', '武汉', '2099-09-09 09:09:09');
INSERT INTO `demo_01`.`tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) VALUES ('heima', '黑马程序员', '黑马程序员', '123', '0', '武汉', '2099-09-09 09:09:09');
INSERT INTO `demo_01`.`tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) VALUES ('luoji', '罗技科技有限公司', '罗技小店', '123', '1', '武汉', '2099-09-09 09:09:09');
INSERT INTO `demo_01`.`tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) VALUES ('oppo', 'oppo 科技有限公司', 'oppo 官方旗舰店', '123', '0', '武汉', '2099-09-09 09:09:09');
INSERT INTO `demo_01`.`tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) VALUES ('qiandu', '千度科技有限公司', '千度小店', '123', '1', '武汉', '2099-09-09 09:09:09');
INSERT INTO `demo_01`.`tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) VALUES ('sina', '新浪科技有限公司', '新浪官方旗舰店', '123', '2', '武汉', '2099-09-09 09:09:09');
INSERT INTO `demo_01`.`tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) VALUES ('xiaomi', '小米科技有限公司', '小米官方旗舰店', '123', '1', '武汉', '2099-09-09 09:09:09');
INSERT INTO `demo_01`.`tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) VALUES ('yijia', '宜家家居', '宜家家居旗舰店', '123', '1', '西安', '2099-09-09 09:09:09');
  • 创建索引。

create index idx_seller_name_stat_addr on tb_seller(name, status, address);

  • 应用索引。
全值匹配,对索引中索引列都指定具体值。
mysql> explain select * from tb_seller where name = '小米科技' and status = '1' and address = '西安市';
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------------------+------+----------+-------+
| id | select_type | table     | partitions | type | possible_keys             | key                       | key_len | ref               | rows | filtered | Extra |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------------------+------+----------+-------+
|  1 | SIMPLE      | tb_seller | NULL       | ref  | idx_seller_name_stat_addr | idx_seller_name_stat_addr | 373     | const,const,const |    1 |   100.00 | NULL  |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------------------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
最左前缀法则。
mysql> explain select * from tb_seller where name = '小米科技';
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------+------+----------+-------+
| id | select_type | table     | partitions | type | possible_keys             | key                       | key_len | ref   | rows | filtered | Extra |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | tb_seller | NULL       | ref  | idx_seller_name_stat_addr | idx_seller_name_stat_addr | 183     | const |    1 |   100.00 | NULL  |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)mysql> explain select * from tb_seller where name = '小米科技' and status = '1';
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------------+------+----------+-------+
| id | select_type | table     | partitions | type | possible_keys             | key                       | key_len | ref         | rows | filtered | Extra |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------------+------+----------+-------+
|  1 | SIMPLE      | tb_seller | NULL       | ref  | idx_seller_name_stat_addr | idx_seller_name_stat_addr | 190     | const,const |    1 |   100.00 | NULL  |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)-- 复合索引 name, status, address,可以使用索引的情况:name,name\status,name\status\address。
-- address 不能使用索引。mysql> explain select * from tb_seller where address = '西安市';
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | tb_seller | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   11 |    10.00 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)-- 顺序不重要,只要包含。
mysql> explain select * from tb_seller where status = '1' and address = '西安市' and name = '小米科技';
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------------------+------+----------+-------+
| id | select_type | table     | partitions | type | possible_keys             | key                       | key_len | ref               | rows | filtered | Extra |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------------------+------+----------+-------+
|  1 | SIMPLE      | tb_seller | NULL       | ref  | idx_seller_name_stat_addr | idx_seller_name_stat_addr | 373     | const,const,const |    1 |   100.00 | NULL  |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------------------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)-- 如果是 name 和 address,也走索引,只不过是走的是 name 索引,address 不走索引(key_len 和 name 索引一样 183)。
mysql> explain select * from tb_seller where name = '小米科技' and address = '西安市';
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------+------+----------+-----------------------+
| id | select_type | table     | partitions | type | possible_keys             | key                       | key_len | ref   | rows | filtered | Extra                 |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------+------+----------+-----------------------+
|  1 | SIMPLE      | tb_seller | NULL       | ref  | idx_seller_name_stat_addr | idx_seller_name_stat_addr | 183     | const |    1 |    10.00 | Using index condition |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------+------+----------+-----------------------+
范围查询右边的列,不使用索引。
mysql> explain select * from tb_seller where name = '小米科技' and status = '1' and address = '西安市';
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------------------+------+----------+-------+
| id | select_type | table     | partitions | type | possible_keys             | key                       | key_len | ref               | rows | filtered | Extra |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------------------+------+----------+-------+
|  1 | SIMPLE      | tb_seller | NULL       | ref  | idx_seller_name_stat_addr | idx_seller_name_stat_addr | 373     | const,const,const |    1 |   100.00 | NULL  |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------------------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)-- 复合索引 name, status, address,status 使用了范围查询,所以右边的 address 索引不用了。只用了 name 和 address。mysql> explain select * from tb_seller where name = '小米科技' and status > '1' and address = '西安市';
+----+-------------+-----------+------------+-------+---------------------------+---------------------------+---------+------+------+----------+-----------------------+
| id | select_type | table     | partitions | type  | possible_keys             | key                       | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+-----------+------------+-------+---------------------------+---------------------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | tb_seller | NULL       | range | idx_seller_name_stat_addr | idx_seller_name_stat_addr | 190     | NULL |    1 |    10.00 | Using index condition |
+----+-------------+-----------+------------+-------+---------------------------+---------------------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)mysql> explain select * from tb_seller where name = '小米科技' and status = '1';
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------------+------+----------+-------+
| id | select_type | table     | partitions | type | possible_keys             | key                       | key_len | ref         | rows | filtered | Extra |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------------+------+----------+-------+
|  1 | SIMPLE      | tb_seller | NULL       | ref  | idx_seller_name_stat_addr | idx_seller_name_stat_addr | 190     | const,const |    1 |   100.00 | NULL  |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)mysql> explain select * from tb_seller where name = '小米科技' and status > '1';
+----+-------------+-----------+------------+-------+---------------------------+---------------------------+---------+------+------+----------+-----------------------+
| id | select_type | table     | partitions | type  | possible_keys             | key                       | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+-----------+------------+-------+---------------------------+---------------------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | tb_seller | NULL       | range | idx_seller_name_stat_addr | idx_seller_name_stat_addr | 190     | NULL |    1 |   100.00 | Using index condition |
+----+-------------+-----------+------------+-------+---------------------------+---------------------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)
不要在索引列上使用运算操作,否则索引失效。
mysql> explain select * from tb_seller where substring(name, 3, 2) = '科技';
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | tb_seller | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   11 |   100.00 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
字符串不加单引号,索引失效(隐式类型转换)。
mysql> explain select * from tb_seller where name = '小米科技' and status = '1';
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------------+------+----------+-------+
| id | select_type | table     | partitions | type | possible_keys             | key                       | key_len | ref         | rows | filtered | Extra |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------------+------+----------+-------+
|  1 | SIMPLE      | tb_seller | NULL       | ref  | idx_seller_name_stat_addr | idx_seller_name_stat_addr | 190     | const,const |    1 |   100.00 | NULL  |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)mysql> explain select * from tb_seller where name = '小米科技' and status = 1;
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------+------+----------+-----------------------+
| id | select_type | table     | partitions | type | possible_keys             | key                       | key_len | ref   | rows | filtered | Extra                 |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------+------+----------+-----------------------+
|  1 | SIMPLE      | tb_seller | NULL       | ref  | idx_seller_name_stat_addr | idx_seller_name_stat_addr | 183     | const |    1 |    10.00 | Using index condition |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------+------+----------+-----------------------+
1 row in set, 2 warnings (0.00 sec)mysql> show warnings;
+---------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level   | Code | Message                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
+---------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Warning | 1739 | Cannot use range access on index 'idx_seller_name_stat_addr' due to type or collation conversion on field 'status'                                                                                                                                                                                                                                                                                                                                                               |
| Note    | 1003 | /* select#1 */ select `demo_01`.`tb_seller`.`sellerid` AS `sellerid`,`demo_01`.`tb_seller`.`name` AS `name`,`demo_01`.`tb_seller`.`nickname` AS `nickname`,`demo_01`.`tb_seller`.`password` AS `password`,`demo_01`.`tb_seller`.`status` AS `status`,`demo_01`.`tb_seller`.`address` AS `address`,`demo_01`.`tb_seller`.`createtime` AS `createtime` from `demo_01`.`tb_seller` where ((`demo_01`.`tb_seller`.`name` = '小米科技') and (`demo_01`.`tb_seller`.`status` = 1))     |
+---------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
2 rows in set (0.01 sec)
尽量使用覆盖索引 ~ 避免 select *。

覆盖索引 ~ 只访问索引的查询(索引列完全包含查询列)。

mysql> explain select * from tb_seller where name = '小米科技';
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------+------+----------+-------+
| id | select_type | table     | partitions | type | possible_keys             | key                       | key_len | ref   | rows | filtered | Extra |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | tb_seller | NULL       | ref  | idx_seller_name_stat_addr | idx_seller_name_stat_addr | 183     | const |    1 |   100.00 | NULL  |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

extra。(5.7 之后只有 null 和 using index)。
Using index ~ 使用覆盖索引时出现。
Using where ~ 在查找使用索引的情况下,需要回表去查找所需的数据。
Using index condition ~ 查找使用了索引,但是需要回表查询数据。
Using where; Using index ~ 查找使用了索引,但是需要的数据在索引列中能找到,所以不需要回表查询数据。

or。

用 or 分割开的条件,如果 or 前的条件中的列有索引,而后面的列中没有索引,那么涉及到的索引都不会被用到。

mysql> explain select * from tb_seller where name='小米科技' or nickname = '小米官方旗舰店';
+----+-------------+-----------+------------+------+---------------------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys             | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | tb_seller | NULL       | ALL  | idx_seller_name_stat_addr | NULL | NULL    | NULL |   11 |    19.00 | Using where |
+----+-------------+-----------+------------+------+---------------------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)-- and 可以用到索引。mysql> explain select * from tb_seller where name='小米科技' and nickname = '小米官方旗舰店';
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys             | key                       | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | tb_seller | NULL       | ref  | idx_seller_name_stat_addr | idx_seller_name_stat_addr | 183     | const |    1 |    10.00 | Using where |
+----+-------------+-----------+------------+------+---------------------------+---------------------------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
like ~ % 在前索引失效。
mysql> explain select * from tb_seller where name like '%科技';
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | tb_seller | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   11 |    11.11 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)mysql> explain select * from tb_seller where name like '科技%';
+----+-------------+-----------+------------+-------+---------------------------+---------------------------+---------+------+------+----------+-----------------------+
| id | select_type | table     | partitions | type  | possible_keys             | key                       | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+-----------+------------+-------+---------------------------+---------------------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | tb_seller | NULL       | range | idx_seller_name_stat_addr | idx_seller_name_stat_addr | 183     | NULL |    1 |   100.00 | Using index condition |
+----+-------------+-----------+------------+-------+---------------------------+---------------------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)mysql> explain select * from tb_seller where name like '%科技%';
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | tb_seller | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   11 |    11.11 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
like ~ % 在前索引失效 ~ 解决:使用覆盖索引。
mysql> explain select sellerid from tb_seller where name like '%科技%';
+----+-------------+-----------+------------+-------+---------------+---------------------------+---------+------+------+----------+--------------------------+
| id | select_type | table     | partitions | type  | possible_keys | key                       | key_len | ref  | rows | filtered | Extra                    |
+----+-------------+-----------+------------+-------+---------------+---------------------------+---------+------+------+----------+--------------------------+
|  1 | SIMPLE      | tb_seller | NULL       | index | NULL          | idx_seller_name_stat_addr | 373     | NULL |   11 |    11.11 | Using where; Using index |
+----+-------------+-----------+------------+-------+---------------+---------------------------+---------+------+------+----------+--------------------------+
1 row in set, 1 warning (0.00 sec)mysql> explain select sellerid, name from tb_seller where name like '%科技%';
+----+-------------+-----------+------------+-------+---------------+---------------------------+---------+------+------+----------+--------------------------+
| id | select_type | table     | partitions | type  | possible_keys | key                       | key_len | ref  | rows | filtered | Extra                    |
+----+-------------+-----------+------------+-------+---------------+---------------------------+---------+------+------+----------+--------------------------+
|  1 | SIMPLE      | tb_seller | NULL       | index | NULL          | idx_seller_name_stat_addr | 373     | NULL |   11 |    11.11 | Using where; Using index |
+----+-------------+-----------+------------+-------+---------------+---------------------------+---------+------+------+----------+--------------------------+
1 row in set, 1 warning (0.00 sec)-- 需要全部是索引列。mysql> explain select sellerid, name, nickname from tb_seller where name like '%科技%';
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | tb_seller | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   11 |    11.11 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
如果 MySQL 评估使用索引比全表更慢,则不使用索引。
mysql> select * from tb_seller;
+----------+--------------------------+-----------------------+----------+--------+---------+---------------------+
| sellerid | name                     | nickname              | password | status | address | createtime          |
+----------+--------------------------+-----------------------+----------+--------+---------+---------------------+
| alibaba  | 阿里巴巴                 | 阿里小店              | 123      | 1      | 武汉    | 2099-09-09 09:09:09 |
| baidu    | 百度科技有限公司         | 百度小店              | 123      | 1      | 武汉    | 2099-09-09 09:09:09 |
| heima    | 黑马程序员               | 黑马程序员            | 123      | 0      | 武汉    | 2099-09-09 09:09:09 |
| huawei   | 华为科技有限公司         | 华为小店              | 123      | 0      | 武汉    | 2099-09-09 09:09:09 |
| itcast   | 传智播客                 | 传智播客              | 123      | 1      | 武汉    | 2099-09-09 09:09:09 |
| luoji    | 罗技科技有限公司         | 罗技小店              | 123      | 1      | 武汉    | 2099-09-09 09:09:09 |
| oppo     | oppo 科技有限公司        | oppo 官方旗舰店       | 123      | 0      | 武汉    | 2099-09-09 09:09:09 |
| qiandu   | 千度科技有限公司         | 千度小店              | 123      | 1      | 武汉    | 2099-09-09 09:09:09 |
| sina     | 新浪科技有限公司         | 新浪官方旗舰店        | 123      | 2      | 武汉    | 2099-09-09 09:09:09 |
| xiaomi   | 小米科技有限公司         | 小米官方旗舰店        | 123      | 1      | 武汉    | 2099-09-09 09:09:09 |
| yijia    | 宜家家居                 | 宜家家居旗舰店        | 123      | 1      | 西安    | 2099-09-09 09:09:09 |
+----------+--------------------------+-----------------------+----------+--------+---------+---------------------+
11 rows in set (0.00 sec)mysql> explain select * from tb_seller where address = '武汉';
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | tb_seller | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   11 |    10.00 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)mysql> explain select * from tb_seller where address = '西安';
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | tb_seller | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   11 |    10.00 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)-- address 创建索引。mysql> create index idx_address on tb_seller(`address`);
Query OK, 0 rows affected (0.00 sec)
Records: 0  Duplicates: 0  Warnings: 0-- 大部分都是 武汉,不走索引。mysql> explain select * from tb_seller where address = '武汉';
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | tb_seller | NULL       | ALL  | idx_address   | NULL | NULL    | NULL |   11 |    90.91 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)mysql> explain select * from tb_seller where address = '西安';
+----+-------------+-----------+------------+------+---------------+-------------+---------+-------+------+----------+-------+
| id | select_type | table     | partitions | type | possible_keys | key         | key_len | ref   | rows | filtered | Extra |
+----+-------------+-----------+------------+------+---------------+-------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | tb_seller | NULL       | ref  | idx_address   | idx_address | 183     | const |    1 |   100.00 | NULL  |
+----+-------------+-----------+------------+------+---------------+-------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
NULL 值的判定。(如果 MySQL 评估使用索引比全表更慢,则不使用索引。)。
-- 大部分是 not null,要找 null,走索引。mysql> explain select * from tb_seller where address is null;
+----+-------------+-----------+------------+------+---------------+-------------+---------+-------+------+----------+-----------------------+
| id | select_type | table     | partitions | type | possible_keys | key         | key_len | ref   | rows | filtered | Extra                 |
+----+-------------+-----------+------------+------+---------------+-------------+---------+-------+------+----------+-----------------------+
|  1 | SIMPLE      | tb_seller | NULL       | ref  | idx_address   | idx_address | 183     | const |    1 |   100.00 | Using index condition |
+----+-------------+-----------+------------+------+---------------+-------------+---------+-------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)-- 大部分是 not null,要找 not null,不走索引。mysql> explain select * from tb_seller where address is not null;
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | tb_seller | NULL       | ALL  | idx_address   | NULL | NULL    | NULL |   11 |   100.00 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
in 走索引 & not in 索引失效。
mysql> explain select * from tb_seller where sellerid in ('oppo', 'xiaomi', 'sina');
+----+-------------+-----------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | tb_seller | NULL       | range | PRIMARY       | PRIMARY | 182     | NULL |    3 |   100.00 | Using where |
+----+-------------+-----------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.01 sec)mysql> explain select * from tb_seller where sellerid not in ('oppo', 'xiaomi', 'sina');
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | tb_seller | NULL       | ALL  | PRIMARY       | NULL | NULL    | NULL |   11 |    81.82 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.01 sec)
单列索引 & 复合索引。

尽量使用复合索引,少使用单列索引。

create index idc_name_sta_addr on tb_seller (name, status, address);
相当于创建了 3 个索引。
name
name + styatus
name + status + adsdress

单列索引

create index idc_name_sta_addr on tb_seller (name);
create index idc_name_sta_addr on tb_seller (status);
create index idc_name_sta_addr on tb_seller (address);

MySQL 会选择一个最优的索引来使用,并不会使用全部索引。

查看索引的使用情况。

  • Handler_read_first。
    索引中第一条被读的次数。如果较高,表示服务器正执行大量全索引扫描(这个值越低越好)。
  • Handler_read_key。
    如果索引正在工作,这个值代表一个行被索引值读的次数,如果值越低,表示索引得到的性能改善不高,因为索引不经常使用(这个值越高越好)。
  • Handler_read_next。
    按照顺序读下一行的请求数。如果你用范围约束或如果执行索引扫描来查询索引列,该值增加。
  • Handler_read_prev
    按照顺序读前一行的请求数,该读方法主要用于优化 ORDER BY。
  • Handler_read_rnd
    根据固定位置读一行的请求数。如果你正在执行大量查询并需要对结果进行排序该值较高。你可能使用了大量需要 MySQL 扫描整个表的查询或你的连接没有正确使用键,这个值较高意味着运行效率低,应该建立索引来补救。
  • Handler_read_rnd_next
    在数据文件中读下一行的请求数,如果你正进行大量的表扫描,该值较高。通常说明你的表索引不正确或写入的查询中没有利用索引。

SQL 优化。

大批量插入数据。

https://dev.mysql.com/doc/internals/en/load-data-infile-events.html

CREATE TABLE `demo_01`.`tb_user_1` (`id` INT NOT NULL AUTO_INCREMENT,`username` VARCHAR(45) NOT NULL,`password` VARCHAR(96) NOT NULL,`name` VARCHAR(45) NOT NULL,`birthday` DATETIME NULL DEFAULT NULL,`sex` CHAR(1) NULL DEFAULT NULL,`email` VARCHAR(45) NULL DEFAULT NULL,`phone` VARCHAR(45) NULL DEFAULT NULL,`qq` VARCHAR(45) NULL DEFAULT NULL,`status` VARCHAR(45) NULL,`create_time` VARCHAR(45) NOT NULL,`update_time` DATETIME NULL DEFAULT NULL,`tb_user_1col` VARCHAR(45) NOT NULL,PRIMARY KEY (`id`),UNIQUE INDEX `unique_user_username` (`username` ASC))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;CREATE TABLE `demo_01`.`tb_user_1` (`id` INT NOT NULL AUTO_INCREMENT,`username` VARCHAR(45) NOT NULL,`password` VARCHAR(96) NOT NULL,`name` VARCHAR(45) NOT NULL,`birthday` DATETIME NULL DEFAULT NULL,`sex` CHAR(1) NULL DEFAULT NULL,`email` VARCHAR(45) NULL DEFAULT NULL,`phone` VARCHAR(45) NULL DEFAULT NULL,`qq` VARCHAR(45) NULL DEFAULT NULL,`status` VARCHAR(45) NULL,`create_time` VARCHAR(45) NOT NULL,`update_time` DATETIME NULL DEFAULT NULL,`tb_user_1col` VARCHAR(45) NOT NULL,PRIMARY KEY (`id`),UNIQUE INDEX `unique_user_username` (`username` ASC))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;

插入 100W 条数据。

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

  • 主键顺序插入。

因为 InnoDB 类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效的提高导入数据的效率。如过 InnoDB 表没有主键,那么系统会自动默认创建一个内部列作为主键,所以如果可以给表创建一个主键,将可以利用这点来提高导入数据的效率。

$ head sql1.log  # 有序。1,“username1”,"123","name1","2020-09-09 09:09:09","1","lyf@qq.com","13812345678","2450702011","0","2020-09-09 09:09:09","2020-09-09 09:09:09"
2,“username2”,"123","name1","2020-09-09 09:09:09","2","lyf@qq.com","13812345678","2450702011","0","2020-09-09 09:09:09","2020-09-09 09:09:09"
3,“username3”,"123","name1","2020-09-09 09:09:09","3","lyf@qq.com","13812345678","2450702011","1","2020-09-09 09:09:09","2020-09-09 09:09:09"$ head sql2.log  # 无序。17836,“username17836”,"123","name17836","2020-09-09 09:09:09","17836","lyf@qq.com","13812345678","2450702011","0","2020-09-09 09:09:09","2020-09-09 09:09:09"
90962,“username90962”,"123","name90962","2020-09-09 09:09:09","90962","lyf@qq.com","13812345678","2450702011","1","2020-09-09 09:09:09","2020-09-09 09:09:09"
77345,“username77345”,"123","name77345","2020-09-09 09:09:09","77345","lyf@qq.com","13812345678","2450702011","0","2020-09-09 09:09:09","2020-09-09 09:09:09"

load data infile ‘/home/geek/geek/sql1.log’ into table tb_user1 fields terminated by ‘,’ lines terminated by ‘\n’;
load data infile ‘/home/geek/geek/sql2.log’ into table tb_user2 fields terminated by ‘,’ lines terminated by ‘\n’;

mysql> load data infile ‘/home/geek/geek/sql1.log’ into table tb_user_1 fields terminated by ‘,’ lines terminated by ‘\n’;
ERROR 1290 (HY000): The MySQL server is running with the --secure-file-priv option so it cannot execute this statement

load data infile ‘/m/tmp/u.txt’ replace into table x fields
terminated by ‘,’ optionally enclosed by ‘"’ escaped by ‘\’
lines starting by ‘>’ terminated by ‘\n’ ignore 2 lines (a,b,c);

  • 关闭唯一性校验。

在导入数据之前执行 SET UNIQUE_CHECK = 0,关闭唯一性校验,在导入结束后执行 SET UNIQUE_CHECK = 1 恢复唯一性校验。可以提高导入效率。

  • 手动提交事务。

在导入数据之前执行 SET AUTOCOMMIT= 0,关闭自动提交,在导入结束后执行 SET AUTOCOMMIT= 1 恢复自动提交。可以提高导入效率。

优化 insert 语句。

当进行数据的 insert 操作的时候,可以考虑采用以下几种优化方案。

  • 如果需要同时对一张表插入很多行数据时,应该尽量使用多个值表的 insert 语句,这种方式将大大的缩减客户端与数据库之间的连接、关闭等消耗。使得效率比分开执行的单个 insert 语句快。

eg. 原始方式为


insert into tb_test values(1, 'Tom');
insert into tb_test values(2, 'Cat');
insert into tb_test values(3, 'Jerry');

优化后的方案为

insert into tb_test values(1, 'Tom'), (2, 'Cat'), (3, 'Jerry');
  • 在事务中进行数据插入。
start transaction;
insert into tb_test values(1, 'Tom');
insert into tb_test values(2, 'Cat');
insert into tb_test values(3, 'Jerry');
commit;
  • 数据有序插入
insert into tb_test values(4,'Tim');
insert into tb_test values(1,'Tom');
insert into tb_test values(3,'Jerry');
insert into tb_test values(5,'Rose');
insert into tb_test values(2,'Cat');

优化。

insert into tb_test values(1, 'Tom');
insert into tb_test values(2, 'Cat');
insert into tb_test values(3, 'Jerry');
insert into tb_test values(4, 'Tim');
insert into tb_test values(5, 'Rose');
优化 order by 语句。
  • 环境准备。
CREATE TABLE `emp` (`id` int(11) NOT NULL AUTO_INCREMENT,`name` varchar(50) NOT NULL COMMENT '姓名。',`age` int(11) DEFAULT NULL COMMENT '年龄。',`salary` int(11) DEFAULT NULL COMMENT '薪水。',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8INSERT INTO `demo_01`.`emp` (`id`, `name`, `age`, `salary`) VALUES ('1', 'Tom', '25', '2300');
INSERT INTO `demo_01`.`emp` (`id`, `name`, `age`, `salary`) VALUES ('2', 'Jerry', '30', '3500');
INSERT INTO `demo_01`.`emp` (`id`, `name`, `age`, `salary`) VALUES ('3', 'Lucy', '25', '2800');
INSERT INTO `demo_01`.`emp` (`id`, `name`, `age`, `salary`) VALUES ('4', 'Jay', '36', '3500');
INSERT INTO `demo_01`.`emp` (`id`, `name`, `age`, `salary`) VALUES ('5', 'Tom2', '21', '2200');
INSERT INTO `demo_01`.`emp` (`id`, `name`, `age`, `salary`) VALUES ('6', 'Jerry2', '31', '3300');
INSERT INTO `demo_01`.`emp` (`id`, `name`, `age`, `salary`) VALUES ('7', 'Lucy2', '26', '2700');
INSERT INTO `demo_01`.`emp` (`id`, `name`, `age`, `salary`) VALUES ('8', 'Jay2', '33', '3500');
INSERT INTO `demo_01`.`emp` (`id`, `name`, `age`, `salary`) VALUES ('9', 'Tom3', '23', '2400');
INSERT INTO `demo_01`.`emp` (`id`, `name`, `age`, `salary`) VALUES ('10', 'Jerry3', '32', '3100');
INSERT INTO `demo_01`.`emp` (`id`, `name`, `age`, `salary`) VALUES ('11', 'Lucy3', '26', '2900');
INSERT INTO `demo_01`.`emp` (`id`, `name`, `age`, `salary`) VALUES ('12', 'Jay3', '37', '4500');create index idx_emp_age_salary on emp(age, salary);mysql> show index from emp;
+-------+------------+--------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name           | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+--------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| emp   |          0 | PRIMARY            |            1 | id          | A         |          12 |     NULL | NULL   |      | BTREE      |         |               |
| emp   |          1 | idx_emp_age_salary |            1 | age         | A         |          10 |     NULL | NULL   | YES  | BTREE      |         |               |
| emp   |          1 | idx_emp_age_salary |            2 | salary      | A         |          12 |     NULL | NULL   | YES  | BTREE      |         |               |
+-------+------------+--------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
3 rows in set (0.00 sec)
MySQL 两种排序方式。
  • 第一种是通过对返回数据进行排序,也就是通常说的 filesort 排序。
    所有不是通过索引直接返回排序结果的排序都叫 fileSort 排序。
mysql> explain select * from emp order by age, salary;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra          |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
|  1 | SIMPLE      | emp   | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   12 |   100.00 | Using filesort |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
1 row in set, 1 warning (0.00 sec)mysql> explain select * from emp order by age;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra          |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
|  1 | SIMPLE      | emp   | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   12 |   100.00 | Using filesort |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
1 row in set, 1 warning (0.00 sec)
  • 第二种通过有序索引顺序扫描直接返回有序数据,这种情况即为 using index,不需要额外排序,操作效率高。

(覆盖字段)。

mysql> explain select id from emp order by age;
+----+-------------+-------+------------+-------+---------------+--------------------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key                | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+--------------------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | emp   | NULL       | index | NULL          | idx_emp_age_salary | 10      | NULL |   12 |   100.00 | Using index |
+----+-------------+-------+------------+-------+---------------+--------------------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)mysql> explain select id, age from emp order by age;
+----+-------------+-------+------------+-------+---------------+--------------------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key                | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+--------------------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | emp   | NULL       | index | NULL          | idx_emp_age_salary | 10      | NULL |   12 |   100.00 | Using index |
+----+-------------+-------+------------+-------+---------------+--------------------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)mysql> explain select id, age, salary from emp order by age;
+----+-------------+-------+------------+-------+---------------+--------------------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key                | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+--------------------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | emp   | NULL       | index | NULL          | idx_emp_age_salary | 10      | NULL |   12 |   100.00 | Using index |
+----+-------------+-------+------------+-------+---------------+--------------------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
  • order by 字段要么全部 desc,要么全部 asc。
  • 排序字段顺序要和索引顺序一致。
优化 filesort。

通过创建合适的索引,能够减少 Filesort 的出现,但是在某些情况下,条件限制不能让 filesort 消失,那就需要加快 Filesort 的排序操作。对于Filesort,MySQL 有两种排序算法。

  • 两次扫描算法。

MySQL 4.1 之前,使用该方式排序。首先根据条件取出排序字段和行指针信息,然后在排序区 sort buffer 中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序之后,再根据行指针回表读取记录,该操作可能会导致大量随机 I/O 操作。

  • 一次扫描算法。

一次性取出满足条件的所有字段,然后在排序区 sort buffer 中排序后直接输出结果集。排序时内存开销较大,但是排序效率比两次扫描算法要高。

MySQL 通过比较系统变量 max_length_for_sort_data 的大小和 Query 语句取出的字段总大小,来判定是否那种排序算法。如果 max_length_for_sort_data 更大,那么使用第二种优化之后的算法,否则使用第一种。

可以适当提高 sort_buffer_size 和 max_length_for_sort_data 系统变量来增大排序区的大小,提高排序的效率。

mysql> show variables like 'max_length_for_sort_data';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| max_length_for_sort_data | 1024  |
+--------------------------+-------+
1 row in set (0.00 sec)mysql> show variables like 'sort_buffer_size';
+------------------+--------+
| Variable_name    | Value  |
+------------------+--------+
| sort_buffer_size | 262144 |
+------------------+--------+
1 row in set (0.00 sec)
优化 group by 语句。

由于 GROUP BY 实际上也同样会进行排序操作,而且与 ORDER BY 相比,GROUP BY 主要只是多了排序之后的分组操作。当然,如果在分组的时候还使用了其他的一些聚合函数,那么还需要一些聚合函数的计算。所以,在 GROUP BY 的实现过程中,与 ORDER BY 一样也可以利用到索引。

如果查询包含 group by 但是用户想要避免排序结果的消耗,则可以执行 order by null 禁止排序。

mysql> drop index idx_emp_age_salary on emp;
Query OK, 0 rows affected (0.01 sec)
Records: 0  Duplicates: 0  Warnings: 0mysql> explain select age, count(*) from emp group by age;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+---------------------------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                           |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+---------------------------------+
|  1 | SIMPLE      | emp   | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   12 |   100.00 | Using temporary; Using filesort |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+---------------------------------+
1 row in set, 1 warning (0.00 sec)mysql> explain select age, count(*) from emp group by age order by null;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra           |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------+
|  1 | SIMPLE      | emp   | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   12 |   100.00 | Using temporary |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------+
1 row in set, 1 warning (0.00 sec)
  • 继续优化 using temporary。
mysql> create index idx_emp_age_salary on emp(age, salary);
Query OK, 0 rows affected (0.01 sec)
Records: 0  Duplicates: 0  Warnings: 0mysql> explain select age, count(*) from emp group by age order by null;
+----+-------------+-------+------------+-------+--------------------+--------------------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys      | key                | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+--------------------+--------------------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | emp   | NULL       | index | idx_emp_age_salary | idx_emp_age_salary | 10      | NULL |   12 |   100.00 | Using index |
+----+-------------+-------+------------+-------+--------------------+--------------------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
优化嵌套查询。

MySQL 4.1 版本之后,开始支持 SQL 的子查询。这个技术可以使用 SELECT 语句来创建一个单列的查询结果,然后把这个结果作为过滤条件用在另一个查询中。使用子查询可以一次性的完成很多逻辑上需要多个步骤才能完成的 SQL 操作,同时也可以避免事务或表锁死,并且写起来也很容易。但是,有些情况下,子查询是可以被更高效的连接(JOIN)代替。

mysql> explain select * from t_user where id in (select user_id from user_role);
+----+--------------+-------------+------------+--------+-------------------+-------------------+---------+-------------------+------+----------+-------------+
| id | select_type  | table       | partitions | type   | possible_keys     | key               | key_len | ref               | rows | filtered | Extra       |
+----+--------------+-------------+------------+--------+-------------------+-------------------+---------+-------------------+------+----------+-------------+
|  1 | SIMPLE       | t_user      | NULL       | ALL    | PRIMARY           | NULL              | NULL    | NULL              |    6 |   100.00 | Using where |
|  1 | SIMPLE       | <subquery2> | NULL       | eq_ref | <auto_key>        | <auto_key>        | 99      | demo_01.t_user.id |    1 |   100.00 | NULL        |
|  2 | MATERIALIZED | user_role   | NULL       | index  | fk_ur_user_id_idx | fk_ur_user_id_idx | 99      | NULL              |    6 |   100.00 | Using index |
+----+--------------+-------------+------------+--------+-------------------+-------------------+---------+-------------------+------+----------+-------------+
3 rows in set, 1 warning (0.01 sec)mysql> explain select * from t_user u, user_role ur where u.id = ur.user_id;
+----+-------------+-------+------------+--------+-------------------+---------+---------+--------------------+------+----------+-------------+
| id | select_type | table | partitions | type   | possible_keys     | key     | key_len | ref                | rows | filtered | Extra       |
+----+-------------+-------+------------+--------+-------------------+---------+---------+--------------------+------+----------+-------------+
|  1 | SIMPLE      | ur    | NULL       | ALL    | fk_ur_user_id_idx | NULL    | NULL    | NULL               |    6 |   100.00 | Using where |
|  1 | SIMPLE      | u     | NULL       | eq_ref | PRIMARY           | PRIMARY | 98      | demo_01.ur.user_id |    1 |   100.00 | NULL        |
+----+-------------+-------+------------+--------+-------------------+---------+---------+--------------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)
优化 or 条件。

对于包含 or 的查询子句,如果要利用索引,则 OR 之间的每个条件都必须用到索引,而且不能使用到复合索引。如果没有用到索引,则应考虑添加索引。

优化:可以使用 union 语句。

mysql> show index from emp;
+-------+------------+--------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name           | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+--------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| emp   |          0 | PRIMARY            |            1 | id          | A         |          12 |     NULL | NULL   |      | BTREE      |         |               |
| emp   |          1 | idx_emp_age_salary |            1 | age         | A         |          10 |     NULL | NULL   | YES  | BTREE      |         |               |
| emp   |          1 | idx_emp_age_salary |            2 | salary      | A         |          12 |     NULL | NULL   | YES  | BTREE      |         |               |
+-------+------------+--------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
3 rows in set (0.00 sec)mysql> explain select * from emp where id = 1 or age = 10;
+----+-------------+-------+------------+-------------+----------------------------+----------------------------+---------+------+------+----------+-----------------------------------------------------------+
| id | select_type | table | partitions | type        | possible_keys              | key                        | key_len | ref  | rows | filtered | Extra                                                     |
+----+-------------+-------+------------+-------------+----------------------------+----------------------------+---------+------+------+----------+-----------------------------------------------------------+
|  1 | SIMPLE      | emp   | NULL       | index_merge | PRIMARY,idx_emp_age_salary | idx_emp_age_salary,PRIMARY | 5,4     | NULL |    2 |   100.00 | Using sort_union(idx_emp_age_salary,PRIMARY); Using where |
+----+-------------+-------+------------+-------------+----------------------------+----------------------------+---------+------+------+----------+-----------------------------------------------------------+
1 row in set, 1 warning (0.00 sec)mysql> explain select * from emp where id = 1 union select * from emp where age = 10;
+----+--------------+------------+------------+-------+--------------------+--------------------+---------+-------+------+----------+-----------------+
| id | select_type  | table      | partitions | type  | possible_keys      | key                | key_len | ref   | rows | filtered | Extra           |
+----+--------------+------------+------------+-------+--------------------+--------------------+---------+-------+------+----------+-----------------+
|  1 | PRIMARY      | emp        | NULL       | const | PRIMARY            | PRIMARY            | 4       | const |    1 |   100.00 | NULL            |
|  2 | UNION        | emp        | NULL       | ref   | idx_emp_age_salary | idx_emp_age_salary | 5       | const |    1 |   100.00 | NULL            |
| NULL | UNION RESULT | <union1,2> | NULL       | ALL   | NULL               | NULL               | NULL    | NULL  | NULL |     NULL | Using temporary |
+----+--------------+------------+------------+-------+--------------------+--------------------+---------+-------+------+----------+-----------------+
3 rows in set, 1 warning (0.00 sec)
优化分页查询。

分页查询靠后的数据,会很慢。MySQL 需要排序前 2000010 条记录,仅仅返回 2000000 ~ 2000010 条记录,其他记录丢弃,查询排序代价非常大。

mysql> select * from tb_item limit 10;
+----+---------------+-------+-----+------------+--------+------------+---------------------+---------------------+
| id | title         | price | num | categoryid | status | sellerid   | createtime          | updatetime          |
+----+---------------+-------+-----+------------+--------+------------+---------------------+---------------------+
|  1 | 商品 1 号     | 26.54 |  44 |          4 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
|  2 | 商品 2 号     | 88.50 |   8 |          7 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
|  3 | 商品 3 号     | 63.52 |  83 |          2 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
|  4 | 商品 4 号     | 92.13 |  74 |          9 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
|  5 | 商品 5 号     | 54.99 |  88 |          7 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
|  6 | 商品 6 号     | 11.69 |  32 |          2 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
|  7 | 商品 7 号     | 50.62 |  63 |          6 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
|  8 | 商品 8 号     | 33.99 |  75 |          7 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
|  9 | 商品 9 号     | 52.79 |  36 |          2 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 10 | 商品 10 号    |  5.08 |  57 |          7 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
+----+---------------+-------+-----+------------+--------+------------+---------------------+---------------------+
10 rows in set (0.00 sec)mysql> select * from tb_item limit 10, 10;
+----+---------------+-------+-----+------------+--------+------------+---------------------+---------------------+
| id | title         | price | num | categoryid | status | sellerid   | createtime          | updatetime          |
+----+---------------+-------+-----+------------+--------+------------+---------------------+---------------------+
| 11 | 商品 11 号    | 81.17 |  94 |          2 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 12 | 商品 12 号    | 56.83 |   0 |          3 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 13 | 商品 13 号    | 54.76 |  80 |          3 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 14 | 商品 14 号    | 46.71 |  20 |          6 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 15 | 商品 15 号    | 57.93 |  97 |          1 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 16 | 商品 16 号    | 73.00 |  25 |          1 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 17 | 商品 17 号    | 76.08 |  48 |          1 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 18 | 商品 18 号    | 15.72 |  42 |          6 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 19 | 商品 19 号    |  3.54 |  18 |          8 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 20 | 商品 20 号    | 58.79 |  44 |          4 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
+----+---------------+-------+-----+------------+--------+------------+---------------------+---------------------+
10 rows in set (0.00 sec)mysql> select * from tb_item limit 20, 10;
+----+---------------+-------+-----+------------+--------+------------+---------------------+---------------------+
| id | title         | price | num | categoryid | status | sellerid   | createtime          | updatetime          |
+----+---------------+-------+-----+------------+--------+------------+---------------------+---------------------+
| 21 | 商品 21 号    |  7.31 |  91 |          3 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 22 | 商品 22 号    | 97.80 |  86 |          3 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 23 | 商品 23 号    | 26.87 |  23 |          3 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 24 | 商品 24 号    |  8.55 |  35 |          5 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 25 | 商品 25 号    | 58.62 |  33 |          9 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 26 | 商品 26 号    | 59.06 |  19 |          2 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 27 | 商品 27 号    | 42.33 |  51 |          2 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 28 | 商品 28 号    | 91.46 |  70 |          7 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 29 | 商品 29 号    | 74.26 |  40 |          7 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 30 | 商品 30 号    | 71.39 |  21 |          9 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
+----+---------------+-------+-----+------------+--------+------------+---------------------+---------------------+
10 rows in set (0.01 sec)mysql> select * from tb_item limit 2000000, 10;
+---------+--------------------+-------+-----+------------+--------+------------+---------------------+---------------------+
| id      | title              | price | num | categoryid | status | sellerid   | createtime          | updatetime          |
+---------+--------------------+-------+-----+------------+--------+------------+---------------------+---------------------+
| 2000001 | 商品 2000001 号    | 20.36 |   4 |          6 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000002 | 商品 2000002 号    |  3.13 |  24 |          1 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000003 | 商品 2000003 号    |  0.45 |  57 |          8 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000004 | 商品 2000004 号    | 51.80 |   4 |          6 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000005 | 商品 2000005 号    | 31.64 |  50 |          5 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000006 | 商品 2000006 号    | 32.46 |  92 |          6 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000007 | 商品 2000007 号    | 42.53 |  21 |          7 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000008 | 商品 2000008 号    | 25.66 |  94 |          9 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000009 | 商品 2000009 号    |  4.22 |  27 |          2 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000010 | 商品 2000010 号    | 44.92 |  47 |          0 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
+---------+--------------------+-------+-----+------------+--------+------------+---------------------+---------------------+
10 rows in set (1.32 sec)
  • 优化。

在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列。

mysql> select * from tb_item limit 2000000, 10;
+---------+--------------------+-------+-----+------------+--------+------------+---------------------+---------------------+
| id      | title              | price | num | categoryid | status | sellerid   | createtime          | updatetime          |
+---------+--------------------+-------+-----+------------+--------+------------+---------------------+---------------------+
| 2000001 | 商品 2000001 号    | 20.36 |   4 |          6 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000002 | 商品 2000002 号    |  3.13 |  24 |          1 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000003 | 商品 2000003 号    |  0.45 |  57 |          8 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000004 | 商品 2000004 号    | 51.80 |   4 |          6 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000005 | 商品 2000005 号    | 31.64 |  50 |          5 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000006 | 商品 2000006 号    | 32.46 |  92 |          6 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000007 | 商品 2000007 号    | 42.53 |  21 |          7 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000008 | 商品 2000008 号    | 25.66 |  94 |          9 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000009 | 商品 2000009 号    |  4.22 |  27 |          2 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000010 | 商品 2000010 号    | 44.92 |  47 |          0 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
+---------+--------------------+-------+-----+------------+--------+------------+---------------------+---------------------+
10 rows in set (1.32 sec)--主键索引。mysql> explain select * from tb_item t, (select id from tb_item order by id limit 2000000, 10) a where t.id = a.id;
+----+-------------+------------+------------+--------+---------------+---------+---------+------+---------+----------+-------------+
| id | select_type | table      | partitions | type   | possible_keys | key     | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+------------+------------+--------+---------------+---------+---------+------+---------+----------+-------------+
|  1 | PRIMARY     | <derived2> | NULL       | ALL    | NULL          | NULL    | NULL    | NULL | 2000010 |   100.00 | NULL        |
|  1 | PRIMARY     | t          | NULL       | eq_ref | PRIMARY       | PRIMARY | 4       | a.id |       1 |   100.00 | NULL        |
|  2 | DERIVED     | tb_item    | NULL       | index  | NULL          | PRIMARY | 4       | NULL | 2000010 |   100.00 | Using index |
+----+-------------+------------+------------+--------+---------------+---------+---------+------+---------+----------+-------------+
3 rows in set, 1 warning (0.01 sec)mysql> select * from tb_item t, (select id from tb_item order by id limit 2000000, 10) a where t.id = a.id;
+---------+--------------------+-------+-----+------------+--------+------------+---------------------+---------------------+---------+
| id      | title              | price | num | categoryid | status | sellerid   | createtime          | updatetime          | id      |
+---------+--------------------+-------+-----+------------+--------+------------+---------------------+---------------------+---------+
| 2000001 | 商品 2000001 号    | 20.36 |   4 |          6 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 | 2000001 |
| 2000002 | 商品 2000002 号    |  3.13 |  24 |          1 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 | 2000002 |
| 2000003 | 商品 2000003 号    |  0.45 |  57 |          8 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 | 2000003 |
| 2000004 | 商品 2000004 号    | 51.80 |   4 |          6 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 | 2000004 |
| 2000005 | 商品 2000005 号    | 31.64 |  50 |          5 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 | 2000005 |
| 2000006 | 商品 2000006 号    | 32.46 |  92 |          6 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 | 2000006 |
| 2000007 | 商品 2000007 号    | 42.53 |  21 |          7 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 | 2000007 |
| 2000008 | 商品 2000008 号    | 25.66 |  94 |          9 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 | 2000008 |
| 2000009 | 商品 2000009 号    |  4.22 |  27 |          2 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 | 2000009 |
| 2000010 | 商品 2000010 号    | 44.92 |  47 |          0 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 | 2000010 |
+---------+--------------------+-------+-----+------------+--------+------------+---------------------+---------------------+---------+
10 rows in set (0.58 sec)-- 全表扫描。mysql> explain select * from tb_item limit 2000000, 10;
+----+-------------+---------+------------+------+---------------+------+---------+------+---------+----------+-------+
| id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra |
+----+-------------+---------+------------+------+---------------+------+---------+------+---------+----------+-------+
|  1 | SIMPLE      | tb_item | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 2983100 |   100.00 | NULL  |
+----+-------------+---------+------------+------+---------------+------+---------+------+---------+----------+-------+
1 row in set, 1 warning (0.00 sec)

该方案适用于主键自增的表,可以把 limit 查询转换成某个位置的查询。

mysql> select * from tb_item where id > 2000000 limit 10;
+---------+--------------------+-------+-----+------------+--------+------------+---------------------+---------------------+
| id      | title              | price | num | categoryid | status | sellerid   | createtime          | updatetime          |
+---------+--------------------+-------+-----+------------+--------+------------+---------------------+---------------------+
| 2000001 | 商品 2000001 号    | 20.36 |   4 |          6 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000002 | 商品 2000002 号    |  3.13 |  24 |          1 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000003 | 商品 2000003 号    |  0.45 |  57 |          8 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000004 | 商品 2000004 号    | 51.80 |   4 |          6 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000005 | 商品 2000005 号    | 31.64 |  50 |          5 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000006 | 商品 2000006 号    | 32.46 |  92 |          6 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000007 | 商品 2000007 号    | 42.53 |  21 |          7 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000008 | 商品 2000008 号    | 25.66 |  94 |          9 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000009 | 商品 2000009 号    |  4.22 |  27 |          2 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
| 2000010 | 商品 2000010 号    | 44.92 |  47 |          0 | 1      | 2450702011 | 2020-09-09 19:19:19 | 2020-09-09 19:19:19 |
+---------+--------------------+-------+-----+------------+--------+------------+---------------------+---------------------+
10 rows in set (0.01 sec)mysql> explain select * from tb_item where id > 2000000 limit 10;
+----+-------------+---------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
| id | select_type | table   | partitions | type  | possible_keys | key     | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+---------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | tb_item | NULL       | range | PRIMARY       | PRIMARY | 4       | NULL | 1491550 |   100.00 | Using where |
+----+-------------+---------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
使用 sql 提示。

在 SQL 语句中添加一些人为的提示来达到优化的目的。

use index。
mysql> create index idx_seller_name on tb_seller(name);
Query OK, 0 rows affected (0.03 sec)
Records: 0  Duplicates: 0  Warnings: 0mysql> explain select * from tb_seller where name = '小米科技';
+----+-------------+-----------+------------+------+-------------------------------------------+---------------------------+---------+-------+------+----------+-------+
| id | select_type | table     | partitions | type | possible_keys                             | key                       | key_len | ref   | rows | filtered | Extra |
+----+-------------+-----------+------------+------+-------------------------------------------+---------------------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | tb_seller | NULL       | ref  | idx_seller_name_stat_addr,idx_seller_name | idx_seller_name_stat_addr | 183     | const |    1 |   100.00 | NULL  |
+----+-------------+-----------+------------+------+-------------------------------------------+---------------------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)mysql> explain select * from tb_seller use index(idx_seller_name) where name = '小米科技';
+----+-------------+-----------+------------+------+-----------------+-----------------+---------+-------+------+----------+-------+
| id | select_type | table     | partitions | type | possible_keys   | key             | key_len | ref   | rows | filtered | Extra |
+----+-------------+-----------+------------+------+-----------------+-----------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | tb_seller | NULL       | ref  | idx_seller_name | idx_seller_name | 183     | const |    1 |   100.00 | NULL  |
+----+-------------+-----------+------------+------+-----------------+-----------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
ignore index。

explain select * from tb_seller ignore index(idx_seller_name) where name = ‘小米科技’;

force index。

可能用到的索引,但实际没有使用。因为这一字段在部分是“武汉”,MySQL 认为不用索引使用全表扫描反而更快。这里需要 force。

mysql> select * from tb_seller;
+----------+--------------------------+-----------------------+----------+--------+---------+---------------------+
| sellerid | name                     | nickname              | password | status | address | createtime          |
+----------+--------------------------+-----------------------+----------+--------+---------+---------------------+
| alibaba  | 阿里巴巴                 | 阿里小店              | 123      | 1      | 武汉    | 2099-09-09 09:09:09 |
| baidu    | 百度科技有限公司         | 百度小店              | 123      | 1      | 武汉    | 2099-09-09 09:09:09 |
| heima    | 黑马程序员               | 黑马程序员            | 123      | 0      | 武汉    | 2099-09-09 09:09:09 |
| huawei   | 华为科技有限公司         | 华为小店              | 123      | 0      | 武汉    | 2099-09-09 09:09:09 |
| itcast   | 传智播客                 | 传智播客              | 123      | 1      | 武汉    | 2099-09-09 09:09:09 |
| luoji    | 罗技科技有限公司         | 罗技小店              | 123      | 1      | 武汉    | 2099-09-09 09:09:09 |
| oppo     | oppo 科技有限公司        | oppo 官方旗舰店       | 123      | 0      | 武汉    | 2099-09-09 09:09:09 |
| qiandu   | 千度科技有限公司         | 千度小店              | 123      | 1      | 武汉    | 2099-09-09 09:09:09 |
| sina     | 新浪科技有限公司         | 新浪官方旗舰店        | 123      | 2      | 武汉    | 2099-09-09 09:09:09 |
| xiaomi   | 小米科技有限公司         | 小米官方旗舰店        | 123      | 1      | 武汉    | 2099-09-09 09:09:09 |
| yijia    | 宜家家居                 | 宜家家居旗舰店        | 123      | 1      | 西安    | 2099-09-09 09:09:09 |
+----------+--------------------------+-----------------------+----------+--------+---------+---------------------+
11 rows in set (0.01 sec)mysql> explain select * from tb_seller where address = '武汉';
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | tb_seller | NULL       | ALL  | idx_address   | NULL | NULL    | NULL |   11 |    90.91 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)mysql> explain select * from tb_seller use index(idx_address) where address = '武汉';
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | tb_seller | NULL       | ALL  | idx_address   | NULL | NULL    | NULL |   11 |    90.91 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)mysql> explain select * from tb_seller force index(idx_address) where address = '武汉';
+----+-------------+-----------+------------+------+---------------+-------------+---------+-------+------+----------+-------+
| id | select_type | table     | partitions | type | possible_keys | key         | key_len | ref   | rows | filtered | Extra |
+----+-------------+-----------+------------+------+---------------+-------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | tb_seller | NULL       | ref  | idx_address   | idx_address | 183     | const |   10 |   100.00 | NULL  |
+----+-------------+-----------+------------+------+---------------+-------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)

应用层面优化。

使用数据库连接池。

对于访问数据库来说,建立连接的代价是比较昂贵的,因为我们频繁的创建关闭连接,是比较耗费资源的,我们有必要建立数据库连接池,以提高访问的性能。

减少对 MySQL 的访问。
避免对数据进行重复检索。

在编写应用代码时,需要能够理清对数据库的访问逻辑。能够一次连接就获取到结果的,就不用两次连接,这样可以大大减少对数据库无用的重复请求。

eg. 需要获取书籍的 id 和 name 字段,则查询如下。

select id, name from tb_book;

之后,在业务逻辑中有需要获取到书籍状态信息,则查询如下。

select id, status from tb_book;

这样,就需要向数据库提交两次请求,数据库就要做两次查询操作。其实完全可以用一条 SQL 语句得到想要的结果。

select id, name, status from tb_book;

增加 cache 层。

在应用中,我们可以在应用中增加缓存层来达到减轻数据库负担的目的。缓存层有很多种,也有很多实现方式,只要能达到降低数据库的负担又能满足应用需求就可以。

因此可以部分数据从数据库中抽取出来放到应用端以文本方式存储,或者使用框架(Mybatis, Hibernate)提供的一级缓存 / 二级缓存,或者使用 redis 数据库来缓存数据。

负载均衡。

负载均衡是应用中使用非常普遍的一种优化方法,它的机制就是利用某种均衡算法,将固定的负载量分布到不同的服务器上,以此来降低单台服务器的负载,达到优化的效果。

利用 MySQL 复制分流查询。

通过 MySQL 的主从复制,实现读写分离,使增删改操作走主节点,查询操作走从节点,从而可以降低单台服务器的读写压力。

采用分布式数据库架构。

分布式数据库架构适合大数据量、负载高的情况,它有良好的拓展性和高可用性。通过在多台服务器之间分布数据,可以实现在多台服务器之间的负载均衡,提高访问效率。

查询缓存优化。

开启 MySQL 查询缓存,当执行完全相同的 SQL 语句时,服务器就会直接从缓存中读取结果。当数据被修改,之前的缓存就会失效。修改比较频繁的表不适合做查询缓存。

是否支持。

mysql> show variables like 'have_query_cache';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| have_query_cache | YES   |
+------------------+-------+
1 row in set (0.00 sec)

是否开启。

mysql> show variables like 'query_cache_type';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| query_cache_type | OFF   |
+------------------+-------+
1 row in set (0.00 sec)

查询缓存占用大小。

mysql> show variables like 'query_cache_size';
+------------------+---------+
| Variable_name    | Value   |
+------------------+---------+
| query_cache_size | 1048576 |
+------------------+---------+
1 row in set (0.00 sec)
mysql> show variables like '%cache%';
+--------------------------------+----------------------+
| Variable_name                  | Value                |
+--------------------------------+----------------------+
| binlog_cache_size              | 32768                |
| binlog_stmt_cache_size         | 32768                |
| have_query_cache               | YES                  |
| host_cache_size                | 279                  |
| innodb_disable_sort_file_cache | OFF                  |
| innodb_ft_cache_size           | 8000000              |
| innodb_ft_result_cache_limit   | 2000000000           |
| innodb_ft_total_cache_size     | 640000000            |
| key_cache_age_threshold        | 300                  |
| key_cache_block_size           | 1024                 |
| key_cache_division_limit       | 100                  |
| max_binlog_cache_size          | 18446744073709547520 |
| max_binlog_stmt_cache_size     | 18446744073709547520 |
| metadata_locks_cache_size      | 1024                 |
| query_cache_limit              | 1048576              |
| query_cache_min_res_unit       | 4096                 |
| query_cache_size               | 1048576              |
| query_cache_type               | OFF                  |
| query_cache_wlock_invalidate   | OFF                  |
| stored_program_cache           | 256                  |
| table_definition_cache         | 1400                 |
| table_open_cache               | 2000                 |
| table_open_cache_instances     | 16                   |
| thread_cache_size              | 9                    |
+--------------------------------+----------------------+
24 rows in set (0.01 sec)
mysql> show status like '%cache%';
+--------------------------------+---------+
| Variable_name                  | Value   |
+--------------------------------+---------+
| Binlog_cache_disk_use          | 0       |
| Binlog_cache_use               | 0       |
| Binlog_stmt_cache_disk_use     | 0       |
| Binlog_stmt_cache_use          | 0       |
| Com_assign_to_keycache         | 0       |
| Qcache_free_blocks             | 1       |
| Qcache_free_memory             | 1031832 |
| Qcache_hits                    | 0       |
| Qcache_inserts                 | 0       |
| Qcache_lowmem_prunes           | 0       |
| Qcache_not_cached              | 199     |
| Qcache_queries_in_cache        | 0       |
| Qcache_total_blocks            | 1       |
| Ssl_callback_cache_hits        | 0       |
| Ssl_session_cache_hits         | 0       |
| Ssl_session_cache_misses       | 9       |
| Ssl_session_cache_mode         | SERVER  |
| Ssl_session_cache_overflows    | 0       |
| Ssl_session_cache_size         | 128     |
| Ssl_session_cache_timeouts     | 0       |
| Ssl_used_session_cache_entries | 0       |
| Table_open_cache_hits          | 16      |
| Table_open_cache_misses        | 11      |
| Table_open_cache_overflows     | 0       |
| Threads_cached                 | 4       |
+--------------------------------+---------+
25 rows in set (0.00 sec)
mysql> show status like 'Qcache%';
+-------------------------+---------+
| Variable_name           | Value   |
+-------------------------+---------+
| Qcache_free_blocks      | 1       |
| Qcache_free_memory      | 1031832 |
| Qcache_hits             | 0       |
| Qcache_inserts          | 0       |
| Qcache_lowmem_prunes    | 0       |
| Qcache_not_cached       | 199     |
| Qcache_queries_in_cache | 0       |
| Qcache_total_blocks     | 1       |
+-------------------------+---------+
8 rows in set (0.00 sec)
开启查询缓存。

MySQL 的查询缓存默认是关闭的,需要手动配置参数 query_cache_type,来开启查询缓存。query_cache_type 该参数的可取值有三个 。

含义
OFF 或 0 查询缓存功能关闭。
ON 或 1 查询缓存功能打开,SELECT的结果符合缓存条件即会缓存,否则,不予缓存,显式指定 SQL_NO_CACHE,不予缓存。
DEMAND 或 2 查询缓存功能按需进行,显式指定 SQL_CACHE 的 SELECT 语句才会缓存;其它均不予缓存。

在 /usr/my/cnf 配置。

# 开启 MySQL 查询缓存。
query_cache_type = 1
查询缓存 SELECT 选项。

可以在 SELECT 语句中指定两个与查询缓存相关的选项 。

  • SQL_CACHE。
    如果查询结果是可缓存的,并且 query_cache_type 系统变量的值为 ON(不加 SQL_CACHE 也走缓存) 或 DEMAND(显式指定 SQL_CACHE 才走缓存),则缓存查询结果 。

  • SQL_NO_CACHE。
    服务器不使用查询缓存。它既不检查查询缓存,也不检查结果是否已缓存,也不缓存查询结果。

eg.

mysql> select sql_cache * from tb_seller;
+----------+--------------------------+-----------------------+----------+--------+---------+---------------------+
| sellerid | name                     | nickname              | password | status | address | createtime          |
+----------+--------------------------+-----------------------+----------+--------+---------+---------------------+
| alibaba  | 阿里巴巴                 | 阿里小店              | 123      | 1      | 武汉    | 2099-09-09 09:09:09 |
| baidu    | 百度科技有限公司         | 百度小店              | 123      | 1      | 武汉    | 2099-09-09 09:09:09 |
| heima    | 黑马程序员               | 黑马程序员            | 123      | 0      | 武汉    | 2099-09-09 09:09:09 |
| huawei   | 华为科技有限公司         | 华为小店              | 123      | 0      | 武汉    | 2099-09-09 09:09:09 |
| itcast   | 传智播客                 | 传智播客              | 123      | 1      | 武汉    | 2099-09-09 09:09:09 |
| luoji    | 罗技科技有限公司         | 罗技小店              | 123      | 1      | 武汉    | 2099-09-09 09:09:09 |
| oppo     | oppo 科技有限公司        | oppo 官方旗舰店       | 123      | 0      | 武汉    | 2099-09-09 09:09:09 |
| qiandu   | 千度科技有限公司         | 千度小店              | 123      | 1      | 武汉    | 2099-09-09 09:09:09 |
| sina     | 新浪科技有限公司         | 新浪官方旗舰店        | 123      | 2      | 武汉    | 2099-09-09 09:09:09 |
| xiaomi   | 小米科技有限公司         | 小米官方旗舰店        | 123      | 1      | 武汉    | 2099-09-09 09:09:09 |
| yijia    | 宜家家居                 | 宜家家居旗舰店        | 123      | 1      | 西安    | 2099-09-09 09:09:09 |
+----------+--------------------------+-----------------------+----------+--------+---------+---------------------+
11 rows in set, 1 warning (0.00 sec)mysql> show status like 'Qcache%';
+-------------------------+---------+
| Variable_name           | Value   |
+-------------------------+---------+
| Qcache_free_blocks      | 1       |
| Qcache_free_memory      | 1029368 |
| Qcache_hits             | 1       |
| Qcache_inserts          | 1       |
| Qcache_lowmem_prunes    | 0       |
| Qcache_not_cached       | 4       |
| Qcache_queries_in_cache | 1       |
| Qcache_total_blocks     | 4       |
+-------------------------+---------+
8 rows in set (0.01 sec)
查询缓存失效的情况。
  • SQL 语句不一致的情况,要想命中查询缓存,查询的SQL语句必须一致。(大小写也有影响)。
SQL1 : select count(*) from tb_item;
SQL2 : Select count(*) from tb_item;
  • 当查询语句中有一些不确定的时,则不会缓存。eg. now(),current_date(),curdate(),curtime(),rand(),uuid(),user(),database() 。
SQL1 : select * from tb_item where updatetime < now() limit 1;
SQL2 : select user();
SQL3 : select database();
  • 不使用任何表查询语句。
select 'A';
  • 查询 mysql,information_schema 或 performance_schema 数据库中的表时,不会走查询缓存。
select * from information_schema.engines;
  • 在存储的函数,触发器或事件的主体内执行的查询。

  • 如果表更改,则使用该表的所有高速缓存查询都将变为无效并从高速缓存中删除。这包括使用 MERGE 映射到已更改表的表的查询。一个表可以被许多类型的语句,如被改变 INSERT,UPDATE,DELETE,TRUNCATE TABLE,ALTER TABLE,DROP TABLE,或 DROP DATABASE 。

内存管理优化。

内存优化原则。
  • 将尽量多的内存分配给 MySQL 做缓存,但要给操作系统和其他程序预留足够内存。

  • MyISAM 存储引擎的数据文件读取依赖于操作系统自身的 IO 缓存,因此,如果有 MyISAM 表,就要预留更多的内存给操作系统做 IO 缓存。

  • 排序区、连接区等缓存是分配给每个数据库会话(session)专用的,其默认值的设置要根据最大连接数合理分配,如果设置太大,不但浪费资源,而且在并发连接较高时会导致物理内存耗尽。

MyISAM 内存优化。

MyISAM 存储引擎使用 key_buffer 缓存索引块,加速 MyISAM 索引的读写速度。对于 MyISAM 表的数据块,mysql 没有特别的缓存机制,完全依赖于操作系统的 IO 缓存。

key_buffer_size。

key_buffer_size 决定 MyISAM 索引块缓存区的大小,直接影响到 MyISAM 表的存取效率。可以在 MySQL 参数文件中设置 key_buffer_size 的值,对于一般 MyISAM 数据库,建议至少将 1/4 可用内存分配给 key_buffer_size。

默认 8 M。

mysql> show variables like 'key_buffer_size';
+-----------------+---------+
| Variable_name   | Value   |
+-----------------+---------+
| key_buffer_size | 8388608 |
+-----------------+---------+
1 row in set (0.03 sec)

在 /usr/my.cnf 中做如下配置。

key_buffer_size = 512M
read_buffer_size。

如果需要经常顺序扫描 MyISAM 表,可以通过增大 read_buffer_size 的值来改善性能。但需要注意的是 read_buffer_size 是每个 session 独占的,如果默认值设置太大,就会造成内存浪费。

mysql> show variables like 'read_buffer_size';
+------------------+--------+
| Variable_name    | Value  |
+------------------+--------+
| read_buffer_size | 131072 |
+------------------+--------+
1 row in set (0.00 sec)
read_rnd_buffer_size。

对于需要做排序的 MyISAM 表的查询,如带有 order by 子句的 sql,适当增加 read_rnd_buffer_size 的值,可以改善此类的 sql 性能。但需要注意的是 read_rnd_buffer_size 是每个 session 独占的,如果默认值设置太大,就会造成内
存浪费。

mysql> show variables like 'read_rnd_buffer_size';
+----------------------+--------+
| Variable_name        | Value  |
+----------------------+--------+
| read_rnd_buffer_size | 262144 |
+----------------------+--------+
1 row in set (0.00 sec)
InnoDB 内存优化。

innodb 用一块内存区做 IO 缓存池,该缓存池不仅用来缓存 innodb 的索引块,而且也用来缓存 innodb 的数据块。

innodb_buffer_pool_size。

该变量决定了 innodb 存储引擎表数据和索引数据的最大缓存区大小。在保证操作系统及其他程序有足够内存可用的情况下,innodb_buffer_pool_size 的值越大,缓存命中率越高,访问 InnoDB 表需要的磁盘 I/O 就越少,性能也就越高。

innodb_buffer_pool_size = 512M

默认 128M。

mysql> show variables like 'innodb_buffer_pool_size';
+-------------------------+-----------+
| Variable_name           | Value     |
+-------------------------+-----------+
| innodb_buffer_pool_size | 134217728 |
+-------------------------+-----------+
1 row in set (0.00 sec)
innodb_log_buffer_size。

决定了 innodb 重做日志缓存的大小,对于可能产生大量更新记录的大事务,增加 innodb_log_buffer_size 的大小,可以避免 innodb 在事务提交前就执行不必要的日志写入磁盘操作。

innodb_log_buffer_size = 10M

默认 16M。

mysql> show variables like 'innodb_log_buffer_size';
+------------------------+----------+
| Variable_name          | Value    |
+------------------------+----------+
| innodb_log_buffer_size | 16777216 |
+------------------------+----------+
1 row in set (0.00 sec)
MySQL 并发参数调整。

从实现上来说,MySQL Server 是多线程结构,包括后台线程和客户服务线程。多线程可以有效利用服务器资源,提高数据库的并发性能。在 MySQL 中,控制并发连接和线程的主要参数包括 max_connections、back_log、thread_cache_size、table_open_cahce。

max_connections。

采用 max_connections 控制允许连接到 MySQL 数据库的最大数量,默认值是 151。如果状态变量 connection_errors_max_connections 不为零,并且一直增长,则说明不断有连接请求因数据库连接数已达到允许最大值而失败,这是可以考虑增大 max_connections 的值。

MySQL 最大可支持的连接数,取决于很多因素,包括给定操作系统平台的线程库的质量、内存大小、每个连接的负荷、CPU 的处理速度,期望的响应时间等。在 Linux 平台下,性能好的服务器,支持 500-1000 个连接不是难事,需要根据服务器性能进行评估设定。

mysql> show variables like 'max_connections';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 151   |
+-----------------+-------+
1 row in set (0.00 sec)
back_log。

back_log 参数控制 MySQL 监听 TCP 端口时设置的积压请求栈大小。如果 MySQL 的连接数达到 max_connections 时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即 back_log,如果等待连接的数量超过 back_log,将不被授予连接资源,将会报错。5.6.6 版本之前默认值为 50,之后的版本默认为 50 + (max_connections / 5),但最大不超过 900。

如果需要数据库在较短的时间内处理大量连接请求, 可以考虑适当增大back_log 的值。

mysql> show variables like 'back_log';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| back_log      | 80    |
+---------------+-------+
1 row in set (0.00 sec)
table_open_cache。

该参数用来控制所有 SQL 语句执行线程可打开表缓存的数量,而在执行 SQL 语句时,每一个 SQL 执行线程至少要打开 1 个表缓存。该参数的值应该根据设置的最大连接数 max_connections 以及每个连接执行关联查询中涉及的表的最大数量来设定。

max_connections x N;

mysql> show variables like 'table_open_cache';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| table_open_cache | 2000  |
+------------------+-------+
1 row in set (0.00 sec)
thread_cache_size。

为了加快连接数据库的速度,MySQL 会缓存一定数量的客户服务线程以备重用,通过参数 thread_cache_size 可控制 MySQL 缓存客户服务线程的数量。(线程池)。

mysql> show variables like 'thread_cache_size';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| thread_cache_size | 9     |
+-------------------+-------+
1 row in set (0.00 sec)
innodb_lock_wait_timeout。

该参数是用来设置 InnoDB 事务等待行锁的时间,默认值是 50ms,可以根据需要进行动态设置。

对于需要快速反馈的业务系统来说,可以将行锁的等待时间调小,以避免事务长时间挂起。

对于后台运行的批量处理程序来说,可以将行锁的等待时间调大,以避免发生大的回滚操作。

mysql> show variables like 'innodb_lock_wait_timeout';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 50    |
+--------------------------+-------+
1 row in set (0.00 sec)

MySQL 锁。

锁概述。

锁是计算机协调多个进程或线程并发访问某一资源的机制(避免争抢)。

在数据库中,除传统的计算资源(如 CPU、RAM、I/O 等)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。

锁分类。

从对数据操作的粒度分。

  • 表锁:操作时,会锁定整个表。
  • 行锁:操作时,会锁定当前操作行。

从对数据操作的类型分:

  • 读锁(共享锁) ~ 针对同一份数据,多个读操作可以同时进行而不会互相影响。
  • 写锁(排它锁) ~ 当前操作没有完成之前,它会阻断其他写锁和读锁。

相对其他数据库而言,MySQL 的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。下表中罗列出了各存储引擎对锁的支持情况。

存储引擎 表级锁 行级锁 页面锁
MyISAM 支持 不支持 不支持
InnoDB 支持 支持 不支持
MEMORY 支持 不支持 不支持
BDB 支持 不支持 支持

MySQL 这 3 种锁的特性可大致归纳如下。

  • 表级锁。
    偏向 MyISAM 存储引擎,开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。

  • 行级锁。
    偏向 InnoDB 存储引擎,开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

  • 页面锁。
    开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

从上述特点可见,很难笼统地说哪种锁更好,只能就具体应用的特点来说哪种锁更合适!仅从锁的角度来说:

表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如 Web 应用。
而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并查询的应用,如一些在线事务处理(OLTP)系统。

MyISAM 表锁。

MyISAM 存储引擎只支持表锁,这也是 MySQL 开始几个版本中唯一支持的锁类型。

  • 如何加表锁。

MyISAM 在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT 等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁。

显式加表锁语法。

加读锁 ~ lock table -table_name- read;
加写锁 ~ lock table -table_name- write;
MyISAM 读锁。
CREATE SCHEMA `demo_02` DEFAULT CHARACTER SET utf8mb4 ;CREATE TABLE `demo_02`.`tb_book` (`id` INT NOT NULL AUTO_INCREMENT,`name` VARCHAR(45) NULL DEFAULT NULL,`publish_time` DATE NULL DEFAULT NULL,`status` VARCHAR(1) NULL DEFAULT NULL,PRIMARY KEY (`id`))
ENGINE = MyISAM
DEFAULT CHARACTER SET = utf8;INSERT INTO `demo_02`.`table_book` (`name`, `publish_time`, `status`) VALUES ('Java 编程思想', '2088-09-09', '1');
INSERT INTO `demo_02`.`table_book` (`name`, `publish_time`, `status`) VALUES ('Python 编程思想', '2088-09-09', '0');CREATE TABLE `demo_02`.`tb_user` (`id` INT NOT NULL AUTO_INCREMENT,`name` VARCHAR(45) NULL DEFAULT NULL,PRIMARY KEY (`id`))
ENGINE = MyISAM
DEFAULT CHARACTER SET = utf8;INSERT INTO `demo_02`.`tb_user` (`name`) VALUES ('令狐冲');
INSERT INTO `demo_02`.`tb_user` (`name`) VALUES ('田伯光');

terminal 1 中加读锁,

mysql> lock table tb_book read;
Query OK, 0 rows affected (0.00 sec)mysql> select * from tb_book;
+----+---------------------+--------------+--------+
| id | name                | publish_time | status |
+----+---------------------+--------------+--------+
|  1 | Java 编程思想       | 2088-09-09   | 1      |
|  2 | Python 编程思想     | 2088-09-09   | 0      |
+----+---------------------+---

terminal 2 中可以读,

mysql> select * from tb_book;
+----+---------------------+--------------+--------+
| id | name                | publish_time | status |
+----+---------------------+--------------+--------+
|  1 | Java 编程思想       | 2088-09-09   | 1      |
|  2 | Python 编程思想     | 2088-09-09   | 0      |
+----+---------------------+---

但在 terminal 1 中不能读其他的表。

mysql> select * from tb_user;
ERROR 1100 (HY000): Table 'tb_user' was not locked with LOCK TABLES

被读锁锁定的表不能 update。

mysql> update tb_book set name = 'solr' where id = 2;
ERROR 1099 (HY000): Table 'tb_book' was locked with a READ lock and can't be updated

terminal 2 uodate 处于阻塞状态。

terminal 1 解锁后立马生效。

MyISAM 写锁。

terminal 1。

mysql> lock table tb_book write;
Query OK, 0 rows affected (0.00 sec)mysql> select * from tb_book;
+----+-------------------+--------------+--------+
| id | name              | publish_time | status |
+----+-------------------+--------------+--------+
|  1 | Java 编程思想     | 2088-09-09   | 1      |
|  2 | solr              | 2088-09-09   | 0      |
+----+-------------------+--------------+--------+
2 rows in set (0.00 sec)mysql> update tb_book set name = 'solr 编程思想' where id = 2;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0mysql> insert into tb_book (name) values ('es');
Query OK, 1 row affected (0.00 sec)mysql>

terminal 2 读数据,处于阻塞状态。

小结。

锁模式的相互兼容性如表中所示:

由上表可见。

  • 对 MyISAM 表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求。

  • 对 MyISAM 表的写操作,则会阻塞其他用户对同一表的读和写操作。

简而言之,就是读锁会阻塞写,但是不会阻塞读。而写锁,则既会阻塞读,又会阻塞写。

此外,MyISAM 的读写锁调度是写优先,这也是 MyISAM 不适合做写为主的表的存储引擎的原因。因为写锁后,其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞。

查看锁的争用情况。
  • show open tables;
mysql> show open tables;
+--------------------+------------------------------------------------------+--------+-------------+
| Database           | Table                                                | In_use | Name_locked |
+--------------------+------------------------------------------------------+--------+-------------+
| performance_schema | memory_summary_by_user_by_event_name                 |      0 |           0 |
| performance_schema | events_waits_summary_global_by_event_name            |      0 |           0 |
| performance_schema | events_transactions_summary_global_by_event_name     |      0 |           0 |
mysql> lock table tb_user read;
mysql> show open tables;+--------------------+------------------------------------------------------+--------+-------------+
| Database           | Table                                                | In_use | Name_locked |
+--------------------+------------------------------------------------------+--------+-------------+
| demo_02            | tb_user                                              |      1 |           0 |
  • show status like ‘Table_locks%’;
mysql> show status like 'table%';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Table_locks_immediate      | 179   |
| Table_locks_waited         | 0     |
| Table_open_cache_hits      | 15    |
| Table_open_cache_misses    | 15    |
| Table_open_cache_overflows | 0     |
+----------------------------+-------+
5 rows in set (0.00 sec)mysql> show status like 'Table_locks_%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Table_locks_immediate | 180   |
| Table_locks_waited    | 0     |
+-----------------------+-------+
2 rows in set (0.00 sec)

Table_locks_immediate ~ 能够立即获得表级锁的次数,每立即获取锁,值 +1。
Table_locks_waited ~ 不能立即获取表级锁而需要等待的次数,每等待一次,该值 +1,此值高说明存在着较为严重的表级锁争用情况。

Innodb 锁 ~ 行锁(默认支持,也支持表锁)。

行锁介绍。

行锁特点:偏向 InnoDB 存储引擎,开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低;并发度也最高。

InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用了行级锁。

背景知识。
事务及其 ACID 属性。

事务是由一组 SQL 语句组成的逻辑处理单元。

事务具有以下 4 个特性,简称为事务 ACID 属性。

原子性(Atomicity)。

事务是一个原子操作单元,其对数据的修改,要么全部成功,要么全部失败。

一致性(Consistency)。

在事务开始和完成时,数据都必须保持一致状态。

隔离性(Isolation)。

数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境下运行。

持久性(Durability)。

事务完成之后,对于数据的修改是永久的。

并发事务处理带来的问题。
丢失更新(Lost Update)。

当两个或多个事务选择同一行,最初的事务修改的值,会被后面的事务修改的值覆盖。

脏读(Dirty Reads)。

当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。

不可重复读(NonRepeatable Reads)。

一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现和以前读出的数据不一致。

幻读(Phantom Reads)。

一个事务按照相同的查询条件重新读取以前查询过的数据,却发现其他事务插入了满足其查询条件的新数据。

事务隔离级别。

为了解决上述提到的事务并发问题,数据库提供一定的事务隔离机制来解决这个问题。数据库的事务隔离越严格,
并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使用事务在一定程度上“串行化”进行,这显然与“并发”是矛盾的。

数据库的隔离级别有 4 个,由低到高依次为 Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别可以逐个解决脏写、脏读、不可重复读、幻读这几类问题。

隔离级别 丢失更新 脏读 不可重复读 幻读
Read uncommitted ×
Read committed × ×
Repeatable read(默认) × × ×
Serializable × × × ×

备注:√ 代表可能出现(不能解决该问题),× 代表不会出现(可解决该问题)。

  • show variables like ‘tx_isolation’;
mysql> show variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.01 sec)
InnoDB 的行锁模式。

InnoDB 实现了以下两种类型的行锁。

  • 共享锁(S) ~ 又称为读锁,简称 S 锁,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。

  • 排他锁(X) ~ 又称为写锁,简称 X 锁,排他锁就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。

对于 UPDATE、DELETE 和 INSERT语句,InnoDB 会自动给涉及数据集加排他锁(X)。

对于普通 SELECT 语句,InnoDB 不会加任何锁。

可以通过以下语句显示给记录集加共享锁或排他锁 。

共享锁(S) ~ SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
排他锁(X) ~ SELECT * FROM table_name WHERE ... FOR UPDATE
案例。
CREATE TABLE `demo_02`.`test_innodb_lock` (`id` INT(11) NULL,`name` VARCHAR(45) NULL,`sex` VARCHAR(1) NULL
)  ENGINE=INNODB DEFAULT CHARACTER SET=UTF8;INSERT INTO `demo_02`.`test_innodb_lock` (`id`, `name`, `sex`) VALUES ('1', '100', '1');
INSERT INTO `demo_02`.`test_innodb_lock` (`id`, `name`, `sex`) VALUES ('3', '3', '1');
INSERT INTO `demo_02`.`test_innodb_lock` (`id`, `name`, `sex`) VALUES ('4', '400', '0');
INSERT INTO `demo_02`.`test_innodb_lock` (`id`, `name`, `sex`) VALUES ('5', '500', '1');
INSERT INTO `demo_02`.`test_innodb_lock` (`id`, `name`, `sex`) VALUES ('6', '600', '0');
INSERT INTO `demo_02`.`test_innodb_lock` (`id`, `name`, `sex`) VALUES ('7', '700', '0');
INSERT INTO `demo_02`.`test_innodb_lock` (`id`, `name`, `sex`) VALUES ('8', '800', '1');
INSERT INTO `demo_02`.`test_innodb_lock` (`id`, `name`, `sex`) VALUES ('9', '900', '1');
INSERT INTO `demo_02`.`test_innodb_lock` (`id`, `name`, `sex`) VALUES ('1', '200', '0');ALTER TABLE `demo_02`.`test_innodb_lock`
ADD INDEX `idx_test_innodb_lock_id` (`id` ASC),
ADD INDEX `idx_test_innodb_lock_name` (`name` ASC);
;

  • 数据库隔离级别 ~ 可重复读。

terminal 2 还未提交,

这里因为关闭了自动提交。

行锁,修改不同的行互不影响。

行锁升级为表锁 ~ (索引失效)。
  • 此时的索引。
mysql> show index from test_innodb_lock\G;
*************************** 1. row ***************************Table: test_innodb_lockNon_unique: 1Key_name: idx_test_innodb_lock_idSeq_in_index: 1Column_name: idCollation: ACardinality: 8Sub_part: NULLPacked: NULLNull: YESIndex_type: BTREEComment:
Index_comment:
*************************** 2. row ***************************Table: test_innodb_lockNon_unique: 1Key_name: idx_test_innodb_lock_nameSeq_in_index: 1Column_name: nameCollation: ACardinality: 9Sub_part: NULLPacked: NULLNull: YESIndex_type: BTREEComment:
Index_comment:
2 rows in set (0.00 sec)

间隙锁的危害。

当我们用范围条件,而不是使用相等条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据进行加锁.对于键值在条件范围内但并不存在的记录,叫做 “间隙(GAP)” ,InnoDB 也会对这个 “间隙” 加锁,这种锁机制就是所谓的间隙锁(Next-Key锁) 。

ID < 10
1
2
3
4
.
6
.
.
9
~ ID 为 5 7 8 的行叫间隙。

行锁争用情况。

show status like ‘innodb_row_lock%’;

mysql> show status like 'innodb_row_lock%';
+-------------------------------+--------+
| Variable_name                 | Value  |
+-------------------------------+--------+
| Innodb_row_lock_current_waits | 0      |
| Innodb_row_lock_time          | 234330 |
| Innodb_row_lock_time_avg      | 33475  |
| Innodb_row_lock_time_max      | 51020  |
| Innodb_row_lock_waits         | 7      |
+-------------------------------+--------+
5 rows in set (0.01 sec)

优化建议。

InnoDB 存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高一些,但是在整体并发处理能力方面要远远优于 MyISAM 的表锁的。当系统并发量较高的时候,InnoDB 的整体性能和 MyISAM 相比就会有比较明显的优势。
但是,InnoDB 的行级锁同样也有其脆弱的一面,当我们使用不当的时候,可能会让 InnoDB 的整体性能表现不仅不能比 MyISAM 高,甚至可能会更差。

  • 尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁。
  • 合理设计索引,尽量缩小锁的范围。
  • 尽可能减少索引条件,及索引范围,避免间隙锁。
  • 尽量控制事务大小,减少锁定资源量和时间长度。
  • 尽可使用低级别事务隔离(但是需要业务层面满足需求)。

常用 SQL 技巧。

SQL 编写顺序。
SELECT [DISTINCT]select_expr [, select_expr] ...
FROM<left_table> <join_type>
JOIN<right_table> ON <join_condition>
WHERE<where_condition>
GROUP BY{col_name | expr | position}[ASC | DESC], ... [WITH ROLLUP]]
HAVING<where_condition>
ORDER BY{col_name | expr | position}[ASC | DESC], ...]
LIMIT{[offset,] row_count | row_count OFFSET offset}]
SELECT[ALL | DISTINCT | DISTINCTROW ][HIGH_PRIORITY][STRAIGHT_JOIN][SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT][SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]select_expr [, select_expr] ...[into_option][FROM table_references[PARTITION partition_list]][WHERE where_condition][GROUP BY {col_name | expr | position}[ASC | DESC], ... [WITH ROLLUP]][HAVING where_condition][ORDER BY {col_name | expr | position}[ASC | DESC], ...][LIMIT {[offset,] row_count | row_count OFFSET offset}][PROCEDURE procedure_name(argument_list)][into_option][FOR UPDATE | LOCK IN SHARE MODE]into_option: {INTO OUTFILE 'file_name'[CHARACTER SET charset_name]export_options| INTO DUMPFILE 'file_name'| INTO var_name [, var_name] ...
}
执行顺序。
[HAVING where_condition]SELECT [DISTINCT]select_expr [, select_expr] ...[ORDER BY {col_name | expr | position}[ASC | DESC], ...]LIMIT{[offset,] row_count | row_count OFFSET offset}]
正则。

select * from tb_book where name regexp ‘^Java’;

符号 含义
^ 在字符串开始处进行匹配。
$ 在字符串末尾处进行匹配。
. 匹配任意单个字符,包括换行符。
[…] 匹配出括号内的任意字符。
[^…] 匹配不出括号内的任意字符。
a* 匹配零个或者多个 a(包括空串)。
a+ 匹配一个或者多个 a(不包括空串)。
a? 匹配零个或者一个 a。
a1 a2
a(m) 匹配 m 个 a。
a(m,) 至少匹配 m 个 a。
a(m,n) 匹配 m 个a 到 n 个 a。
a(,n) 匹配 0 到 n 个 a。
(…) 将模式元素组成单一元素。

MySQL 常用工具。

《MySQL 常用 ~ 工具、日志。》

MySQL 日志。

MySQL 主从复制。

综合案例。

MySQL~高级应用 + 优化。相关推荐

  1. #周末课堂# 【Linux + JVM + Mysql高级性能优化班】(火热报名中~~~)

    Linux + JVM + Mysql高级性能优化课程 课程名称:      Linux + JVM + Mysql高级性能优化 QQ群:      243242580(小白.菜鸟勿进)跟Java.M ...

  2. Mysql高级-应用优化,查询缓存优化,锁

    文章目录 1. 应用优化 1.1 使用连接池 1.2 减少对MySQL的访问 1.2.1 避免对数据进行重复检索 1.2.2 增加cache层 1.3 负载均衡 1.3.1 利用MySQL复制分流查询 ...

  3. MySQL高级性能优化

    MySQL高级 如果你是一名开发者,数据库将伴随你的整个职业生涯.掌握SQL写出高效易用的SQL已成为开发者的必备技能.因为整个应用系统离不开数据库,应用系统是否流畅(数据量达到一定程度时),很大一部 ...

  4. MySQL高级-索引优化(超详细)

    性能分析 MySQL Query Optimizer Mysql中由专门负责优化SELECT语句的优化器,主要功能就是通过计算分析系统中收集到的统计信息,为客户端请求的Query提供他认为最优的执行计 ...

  5. MySQL高级-SQL优化步骤

    优化SQL步骤(explain等) 1 查看SQL执行频率 2 定位低效率执行SQL 3 explain分析执行计划 3.1 环境准备 3.2 explain 之 id 3.3 explain 之 s ...

  6. Mysql高级 索引优化

    Mysql逻辑架构 Mysql与其他数据库相比有点与众不同,他的架构可以在多种不同的场景中应用并发挥作用,主要体现在存储引擎的架构上,插件式的存储引擎结构将查询处理和其他的系统任务以及数据的存储提取分 ...

  7. MySQL高级 - 内存优化 - MyISAM内存优化

    MyISAM 内存优化 myisam存储引擎使用 key_buffer 缓存索引块,加速myisam索引的读写速度.对于myisam表的数据块,mysql没有特别的缓存机制,完全依赖于操作系统的IO缓 ...

  8. MySQL高级 - 内存优化 - 优化原则

    内存优化原则 1) 将尽量多的内存分配给MySQL做缓存,但要给操作系统和其他程序预留足够内存. 2) MyISAM 存储引擎的数据文件读取依赖于操作系统自身的IO缓存,因此,如果有MyISAM表,就 ...

  9. MySQL高级 - 应用优化

    应用优化 前面章节,我们介绍了很多数据库的优化措施.但是在实际生产环境中,由于数据库本身的性能局限,就必须要对前台的应用进行一些优化,来降低数据库的访问压力. 使用连接池 对于访问数据库来说,建立连接 ...

  10. MySQL高级 - SQL优化 - 索引提示

    使用SQL提示 SQL提示,是优化数据库的一个重要手段,简单来说,就是在SQL语句中加入一些人为的提示来达到优化操作的目的. USE INDEX 在查询语句中表名的后面,添加 use index 来提 ...

最新文章

  1. call_user_fun()函数的使用
  2. [算法][递归] 棋盘覆盖
  3. Hibernate简介2
  4. [Python人工智能] 三十三.Bert模型 (2)keras-bert库构建Bert模型实现文本分类
  5. 算法竞赛入门经典(第二版) | 例题5-4 反片语 (map+标准化)(UVa156,Ananagrams)
  6. 解决conda activate报错IMPORTANT: You may need to close and restart your shell after running ‘conda init‘
  7. eclipse在debug启动时无法启动解决办法
  8. mac系统一些快捷键
  9. 营业执照在线生成_营业执照用旧的?办新的?办电子的?丨现在选哪个都很“好办”...
  10. 3ds max软件如何彻底卸载干净
  11. 3 整型变量,实型数据(主要是实型变量)2021-01-25
  12. Win10系统文件夹被设为只读,取消Word文件的只读模式
  13. 【paddlepaddle安装报错系列】DLL lond failed:找不到指定模块
  14. 小米5怎么安android,小米5怎么插卡 小米5手机安装sim卡图文教程
  15. matlab imagesc
  16. 重磅丨十四五国家信息规划:部署了10项重大任务,10项优先行动(附PDF原文)...
  17. [一个程序员的人文素养系列]这世界如露水般短暂俳句摘抄
  18. Ray Tracing in One Weekend从零实现一个简单的光线追踪渲染器
  19. Elasticsearch7.8.0从安装到高亮搜索LOL英雄名
  20. 揭秘可解释推荐系统:知其然,知其所以然

热门文章

  1. 这些专业考上研以后再考公务员,非常吃香!
  2. 模电(八)放大电路静态工作点的稳定性
  3. 【NLP】自然语言处理中常见的英文单词
  4. 口袋的天空(洛谷 P1195)
  5. Catboost参数全集
  6. 【WordPress】视频有声音无图像
  7. setcpu_SetCpu Android超频工具
  8. Unable to set localhost. This prevents creation of a GUID. Cause was: cloud: cloud java.net.UnknownH
  9. nyoj 779 兰州烧饼
  10. java: Compilation failed: internal java compiler error