MySQL高级
基于MySQL版本5.5
MySQL的架构介绍
MySQL简介
概述
-
MySQL是一个关系型数据库管理系统,由瑞典MySQL AB公司开发,目前属于Oracle公司。
-
MySQL是一种关联数据库管理系统,将数据保存在不同的表中,而不是将所有的数据放在一个大仓库内,这样就增加了速度并提高了灵活性。
-
MySQL是开源的,所以并不需要支付额外的费用。
-
MySQL支持大型的数据库,可以处理拥有上千万条记录的大型数据库。
-
MySQL使用标准的SQL数据语言形式。
-
MySQL可以允许于多个系统上,并且支持多种语言。这些编程语言包括C、C++,Python,Java、Perl、PHP、Ruby和TCL等。
-
MySQL对PHP有很好的支持,PHP是目前最流行的Web开发语言。
-
MySQL支持大型数据库,支持5000万条记录的数据仓库,32位系统表文件最大可支持4GB,64位系统支持最大的表文件8TB。
-
MySQL是可以定制的,采用了GPL协议。可以修改源码来开发自己的MySQL系统。
高级MySQL
完整的MySQL优化需要很深的功底,大公司甚至有专门的DBA
- MySQL内核
- SQL优化工程师
- MySQL服务器的优化
- 各种参数常量的设定
- 查询语句优化
- 主从复制
- 软硬件升级
- 容灾备份
- SQL编程
MySQL Linux版本的安装
参照Linux常用软件的安装-MySQL的安装。
修改MySQL的字符集
连接MySQL,输入SQL查看字符集
show variables like 'character%';
show variables like '%char%';
MySQL的配置文件
- 二进制日志log-bin:要用于主从复制
log-bin=mysql-bin
-
错误日志log-error:默认是关闭的,记录严重的警告和错误信息,每次启动和关闭的详细信息等。
-
查询日志log:默认关闭,记录查询的SQL语句,如果开启会降低MySQL的整体性能,因为记录日志也是需要消耗系统资源的
-
数据文件
- 两个系统
- windows:MySQL安装目录\data目录下可以挑选很多库
- linux:
- 查看当前系统中的全部库后再进去
- 默认路径:/var/lib/mysql
- frm文件:存放表结构
- myd文件:存放表数据
- myi文件:存放表索引
- 两个系统
-
配置文件
-
Windows系统文件名:my.ini文件
-
Linux系统文件名:/etc/my.cnf文件
-
MySQL逻辑架构介绍
总体概览
和其他数据相比,MySQL有点域中不同,它的架构可以在多种不同场景中应用并发挥良好作用。主要体现在存储引擎的架构上,插件式的存储引擎架构将查询处理和其它的系统任务以及数据的存储提取相分离 。这种架构可以根据业务的需求和实际需要选择合适的存储引擎。
MySQL逻辑架构图:
- 1.连接层:最上层是一些客户端和连接服务,包含本地sock通信和大多数基于客户端/服务端工具实现的类似于tcp/ip的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程池的概念,为通过认证安全接入的客户端提供线程。同样在该层上可以实现基于SSL的安全链接。服务器也会为安全接入的每个客户端验证它锁具有的操作权限。
- 2.服务层:第二层架构主要完成大多数的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化及部分内置函数的执行。所有跨存储引擎的功能也在这一层实现。如过程、函数等。在该层,服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定查询表的顺序,是否利用索引等,最后生成相应的执行操作。如果是select语句,服务器还会查询内部的缓存。如果缓存空间足够大,这样在解决大量读操作的环境中能够很好的提升系统的性能
- 3.引擎层:存储引擎层,存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API与存储引擎进行通信。不同的存储引擎具有的功能不同,这样我们可以根据自己的实际需要进行选取。后面会介绍MyISAM和InnoDB
- 4.存储层:数据存储层,主要是将数据存储在运行与裸设备的文件系统之上,并完成与存储引擎的交互。
MySQL存储引擎
查看MySQL的存储引擎
-- 查看MySQL已提供的存储引擎
show engines;
-- 查看MySQL当前默认的存储引擎
show variables like '%storage_engine%';
MyISAM和InNoDB对比
对比项 | MyISAM | InNoDB |
---|---|---|
主外键 | 不支持 | 支持 |
事务 | 不支持 | 支持 |
行表锁 | 表锁,即使操作一条记录也会锁住整个表,不适合高并发的操作 | 行锁,操作时只锁某一行,不对其他行有影响,适合高并发的操作 |
缓存 | 只缓存索引,不缓存真实数据 | 不仅缓存索引,还要缓存真实数据,对内存要求较高,而且内存大小对性能有决定性的影响 |
表空间 | 小 | 大 |
关注点 | 性能 | 事务 |
默认安装 | 是 | 是 |
索引优化分析
索引简介
什么是索引
MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。
可以看出索引的本质:索引是数据结构。
索引的目的在于提高查询效率,可以类比字典。例如:
如果要查“mysql”这个单次,肯定需要先定位到m字母,再往下找到y字母,再找剩下的sql。
如果没有索引,那么可能需要从a-z寻找。如果想再找Java开头的单词或者Oracle的单词,这种查询效率非常慢。
**排好序的快速查找数据结构就是索引。**在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。下图就是一种可能的索引方式示例:
左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址。
为了加快Col2的查找,可以维护一个右边所示的二叉查找数,每个节点分别包含索引键值和一个指向对应数据记录物理地址的指针,这样就可以运用二叉查找在一定的复杂度内获取到相应数据,从而快速的检索出符合条件的记录。
一般来说索引本身也很大,不可能全部存储在内存中,因此在索引往往以索引文件的形式存储在硬盘上。
我们平常所说的索引,如果没有特别指明,都是指B树(多路搜索树,并不一定是二叉树)结构组织的索引。其中聚集索引,次要索引,覆盖索引、符合索引、前缀索引、唯一索引默认都是使用B+树索引,统称索引。当然,除了B+树这种类型的索引之外,还有哈希索引(hash index)等。
索引的优势
- 类似大学图书馆建书目索引,提高数据检索的效率,降低数据库的IO成本
- 通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗
索引的劣势
- 实际上索引也是一张表,这张表保存了主键与索引字段,并指向实体表的记录,所以索引列也是要占用空间的
- 虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件。每次更新添加了索引列的字段,都会调整因为更新所带来的键值变化后的索引信息
- 索引只是提高效率的因素,如果MySQL有大数据量的表,就需要花时间研究建立最优秀的索引,或优化查询语句
MySQL索引分类
-
单值索引:即一个索引只包含单个列,一个表可以有多个单列索引
-
唯一索引:索引列的值必须唯一,但允许有空值
-
复合索引:即一个索引包含多个列
-
基本语法:
- 创建索引
-- [] 内的内容可以不填 -- 如果是CHAR、VARCHAR类型,length可以小于字段实际长度 -- 如果是BLOB和TEXT类型,必须指定length CREATE [UNIQUE] INDEX indexName ON tableName(columnname(length))
- 更新索引
ALTER tableName ADD [UNIQUE] INDEX [indexName] On (columnname(length));
- 删除索引
DROP INDEX [indexName] on tableName;
- 查看索引
SHOW INDEX FROM tableName;
- 使用ALTER命令
-- 有以下四种方式来添加数据表的索引 -- 1.该语句添加一个主键,这意味着索引值必须是唯一的,且不能为NULL ALTER TABLE tableName ADD PRIMARY KEY (column_list); -- 2.该语句创建索引的值必须是唯一的(除了NULL外,NULL可能会出现多次) ALTER TABLE tableName ADD UNIQUE indexName (column_list); -- 3.添加普通索引,索引值可以出现多次 ALTER TABLE tableName ADD INDEX indexName (column_list); -- 4.改语句指定了索引为FULLTEXT,用于全文检索 ALTER TABLE tableName ADD FULLTEXT indexName (column_list);
MySQL索引结构
- BTree 索引
检索原理:
【初始化介绍】
一颗B+树,浅蓝色的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据线(深蓝色表示)和指针(黄色所示),如磁盘块1包含数据项17和35,包含指针P1、P2、P3,P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。
真实的数据存在于叶子节点。 即3、5、9、10、13、15、28、29、36、60、75、79、90、99.
非叶子节点只不存储真实的数据,只存储指引搜索方向的数据项 ,如17、35并不真实存在于数据表中。
【查找过程】
如果要查找数据项29,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO,在内存中用二分查找确定29在17和35之间,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘的IO)可以忽略不急,通过磁盘块1的P2指针的磁盘地址把磁盘块3由磁盘加载到内存,发生第二次IO,29在26到30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存,发生第三次IO,同时内存中做二分查找找到29,结束查询,总共三次IO。
真实的情况是,三层的B+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本非常非常高。
- Hash索引
- full-text全文索引
- R-Tree索引
创建索引的情况
- 主键自动建立唯一索引
- 频繁作为查询条件的字段应该创建索引
- 查询中与其他表关联的字段,外键关系建立索引
- 频繁更新的字段不适合创建索引
- 因为每次更新不单单是更新了记录,还会更新索引,加重了IO负担
- Where条件里用不到的字段不创建索引
- 单键/组合索引的选择问题:高并发下倾向创建组合索引
- 查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度
- 查询中统计或分组字段
不要创建索引的情况
- 表记录太少
- 经常增删改的表
- 提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件
- 数据重复且分布平均的表字段,因此应该只为最经常查询和最经常排序的数据列建立索引。
- 注意:如果某个数据列包含许多重复的内容,为它建立索引就没有太大的实际效果。
- 假如一个表有10万行记录,有一个字段A只有T和F两种值,且每个值的分布概率大约为50%,那么对这种表A字段建立索引一般不会提高数据库的查询速度。
- 索引的选择性是指索引列中不同值的数目与表中记录数的比。如果一个表中有2000条就,表索引列有1980个不同的值,那么这个索引的选择性就是1980/2000=0.99.一个索引的选择性越接近于1,这个索引的效率就越高。
性能分析
MySQL Query Optimizer
MySQL中有专门负责优化SELECT语句的优化器模块,主要功能:通过计算分析系统中手机到的统计信息,为客户端请求的Query提供他认为最优的执行计划(它认为最优的数据检索方式,但不见得是DBA认为是最优的,这部分最耗费时间)
当客户端向MySQL请求一条Query,命令解析器模块完成请求分类,区别出是SELECT并转发给MySQL Query Optimizer时,MySQL Query Optimizer首先会对整条Query进行优化,处理掉一些常量表达式的预算,直接换算成常量值。并对Query中的查询条件进行简化和转换,如去掉一些无用或显而易见的条件、结构调整等。然后分析Query中的Hint信息(如果有),看显示Hint信息是否可以完全确定Query的执行计划。如果没有Hint或Hint信息还不足以完全确定执行计划,则会读取所涉及对象的统计信息,根据Query进行写相应的计算分析,然后再得出最后的执行计划
MySQL常见瓶颈
- CPU:CPU在饱和的时候一般发生在数据装入内存或从磁盘上读取数据的时候
- IO:磁盘IO瓶颈发生在装入数据远大于内存容量的时候
- 服务器硬件的性能瓶颈:top,free,iostat和vmstat来查看系统的性能状态
explain(执行计划)
简介
使用EXPLAIN关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理你的SQL语句的。分析你的查询语句或是表结构的性能瓶颈。
作用
- 获取表的读取顺序
- 数据读取操作的操作类型
- 哪些索引可以使用
- 哪些索引被实际使用
- 表之间的引用
- 每张表有多少行被优化器查询
使用
EXPLATIN + SQL语句
执行计划包含的信息:
各名词解释
id
select查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序
三种情况:
- ID相同:执行顺序由上到下
- ID不同:如果是子查询,ID的序号会递增,ID值越大优先级越高,越先被执行
- ID相同不同:同时存在。ID如果相同,可以认为是一组,从上往下顺序执行;在所有组中,ID越大,优先级越高,越先执行。
select_type
查询的类型,主要用于区别普通查询、联合查询、子查询等的复杂查询。类别如下:
- SIMPLE:简单的select查询,查询中不包含子查询或者UNION
- PRIMARY:查询中若包含任何复杂的子部分,最外层查询则被标记为PRIMARY
- SUBQUERY:在SELECT或WHERE列表中包含了子查询
- DERIVED:在FROM列表中包含的子查询被标记为DERIVED(衍生)。MySQL会敌对执行这些子查询,把结果放在临时表里。
- UNION:若第二个SELECT出现在UNION之后,则被标记为UNION;若UNION包含在FROM子句的子查询中,外层SELECT将被标记为:DERIVED
- UNION RESULT:从UNION表获取结果的SELECT
table
显示这一行的数据是关于那张表的
type
type显示的是访问类型,是较为重要的一个指标,结果值从最好到最差依次是:
system>const>eq_ref>ref>fulltext>ref_or_null>index_merge>unique_subquery>index_subquery>range>index>ALL,常见的排序为:system>const>eq_ref>ref>range>index>ALL。一般来说,得保证查询至少达到range级别,最好能达到ref
类型:
- system:表只有一行记录(等于系统表),这是const类型的特烈,平时不会出现,这个也可以忽略不急
- const:表示通过索引一次就找到了,const用于比较primary key或者unique索引。因为只匹配一行数据,所以很快。如将主键置于WHERE列表中,MySQL就能将该查询值转换为一个常量
- eq_ref:唯一性索引扫描,对于每个索引建,表中只有一条记录与之匹配。常见于主键或唯一索引扫描
- ref:非唯一性索引扫描,返回匹配某个单独值的所有行。本质上也是一种索引访问,它返回所有匹配某个单独值的行,然而,它可能会找到多个符合条件的行,所以他应该属于查找和扫描的混合体。
- range:只检索给定范围的行,使用一个索引来选择行。KEY列显示使用了那个索引,一般就是在你的betweeen,<、>、In等的查询,这种范围扫描索引扫描比全表扫描要好,因为它只需要开始于索引的某一点,而结束于另一点,不用扫描全部索引。
- index:Full Index Scan,Index与ALL区别为Index类型值遍历索引数。这通常比ALL快,因为索引文件通常比数据文件小。(也就是说虽然ALL和Index都是读全表,单index是从索引中读取的,而ALL是从硬盘中读取的)。
- all:Full Table Scan全表扫描,将遍历全表以找到匹配的行
- 备注:一般来说,得保证查询至少达到range级别,最好能达到ref
possible_keys
显示可能应用在这张表中的索引,一个或多个。
查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用。
key
实际使用的索引。如果为NULL,则没有使用索引。
查询中若使用了覆盖索引,则该索引和查询的select字段重叠。
key_len
表示索引中使用的字节数,可通过该列计算查询的索引的长度。在不损失精确性的情况下,长度越短越好。
key_len显示的值为索引字段的最大长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的。
ref
显示索引的那一列被使用了,如果可能的话,是一个常数。哪些列或常量被用于查找索引列上的值。
rows
根据表统计信息及索引选用情况,大致估算出找到所需的记录所需要读取的行数。
Extra
包含不适合在其他列中显示但十分重要的额外信息。
- Using firesort:说明MySQL会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。MySQL中无法利用索引完成的排序操作成为“文件排序”
- Using temporary:使用了临时表保存中间结果,MySQL再对查询结果排序时使用临时表。常见于排序 order by
- Using index:表示相应的select操作中使用了覆盖索引(CoveringIndex),避免方位了表的数据行,效率不错!如果同时出现using where,表明索引被用来执行索引键值的查找,如果没有同时出现using where,表明索引用来读取数据而非执行查找动作。
- 覆盖索引:一说为索引覆盖
- 理解方式一:就是select的数据只用从索引中就能够取得,不必读取数据行,MySQL可以利用索引返回select列表中的字段,而不必根据索引再次读取数据文件,换句话说查询列要被所建的索引覆盖。
- 理解方式二:索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整行。毕竟索引叶子节点存储了它们索引的数据,当能通过读取索引就可以得到想要的数据,那就不需要读取行了。一个索引包含了(或覆盖了)满足查询结果的数据就叫做覆盖索引。
- 注意:如果要使用覆盖索引,一定要注意select列表中只取出需要的列,不可select *,因为如果将所有字段一起做索引会导致索引文件过大,查询性能下降。
- 覆盖索引:一说为索引覆盖
- Using where:表明使用了where过滤
- Using join buffer:使用了连接缓存
- impossible where:where子句的值总是false,不能用来获取任何元组
- select tables optimized away:在没有 GROUP BY子句的情况下,基于索引优化MIN/MAX操作或者对于MyISAM存储引擎优化COUNT(*)操作,不必等到执行阶段再进行计算,查询执行计划生成的阶段即完成优化
- distinct:优化distinct操作,在找到第一匹配的元组后即停止找同样值的动作
索引优化
索引分析
单表
建表语句
CREATE TABLE IF NOT EXISTS `article` (
`id` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`author_id` INT(10) UNSIGNED NOT NULL,
`category_id` INT(10) UNSIGNED NOT NULL,
`views` INT(10) UNSIGNED NOT NULL,
`comments` INT(10) UNSIGNED NOT NULL,
`title` VARBINARY(255) NOT NULL,
`content` TEXT NOT NULL);
INSERT INTO `article`(`author_id`,`category_id`,`views`,`comments`,`title`,`content`) VALUES (1,1,1,1,'1','1'), (2,2,2,2,'2','2'), (1,1,3,3,'3','3');
案例
-- 查询 category_id为1,且comments大于1的情况下,views最多的article_id。
EXPLAIN SELECT id,author_id FROM article WHERE category_id = 1 AND comments > 1 ORDER BY views DESC LIMIT 1
-- 结论:很显然,type是ALL,即最坏的情况。Extra里还出现了Using filesort,也是最坏的情况。优化是必须的。
-- 开始优化
-- 1.1 新建索引+删除索引
-- ALTER TABLE `article` ADD INDEX idx_article_ccv(`category_id`,`comments`,`views`);
CREATE INDEX idx_article_ccv ON article(category_id,comments,views);
-- 再次执行后,解决了全表扫描,但还是出现了Using filesort。删除索引
DROP INDEX idx_article_ccv ON article
-- 1.2 第二次EXPLAIN
EXPLAIN SELECT id,author_id FROM article WHERE category_id = 1 AND comments > 1 ORDER BY views DESC LIMIT 1
EXPLAIN SELECT id,author_id FROM article WHERE category_id = 1 AND comments = 3 ORDER BY views DESC LIMIT 1
-- 结论:
-- type变成了range,这是可以接受的。但是extra里使用Using filesort仍是无法接受的。
-- 但是我们已经建立了索引,为什么没用?
-- 这是因为按照BTree索引的工作原理,
-- 先排序category_id,
-- 如果遇到相同的category_id则再排序comments,如果遇到相同的comments则再排序views,
-- 当comments字段在联合索引里处于中间位置时,
-- 因comments > 1条件是一个范围值(所谓range)
-- MySQL无法利用索引再对后面的views部分进行检索,即range类型查询字段后面的索引无效
-- 1.3 第2次新建索引
CREATE INDEX idx_article_cv ON article(category_id,views);
-- 1.4 第三次EXPLAIN
EXPLAIN SELECT id,author_id FROM article WHERE category_id = 1 AND comments > 1 ORDER BY views DESC LIMIT 1
-- 结论:可以看到type变成了ref,Extra中的Using filesort也消失了,结果很理想
DROP INDEX idx_article_cv ON article
双表
建表语句
CREATE TABLE IF NOT EXISTS `class`(
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY(`id`)
);
CREATE TABLE IF NOT EXISTS `book`(
`bookId` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY(`bookId`)
);
INSERT INTO class(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO class(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO class(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO class(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO class(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO class(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO class(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO class(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO class(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO class(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO class(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO class(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO class(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO class(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO class(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO class(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO class(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO class(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO class(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO class(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO book(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO book(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO book(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO book(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO book(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO book(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO book(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO book(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO book(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO book(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO book(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO book(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO book(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO book(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO book(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO book(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO book(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO book(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO book(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO book(card) VALUES (FLOOR(1+(RAND()*20)));
案例
-- 下面开始explain分析
EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card=book.card;
-- 结论:type有All
-- 添加索引优化,左连接的情况下,加在右表
ALTER TABLE `book` ADD INDEX card (`card`);
-- 第2次EXPLAIN,可以看到第二行的type编程了ref,rows也变了,优化比较明细
-- 测试主表的索引,先删除右表的索引
DROP INDEX card on book;
ALTER TABLE `class` ADD INDEX card (`card`);
-- 再次EXPLAIN,可以发现rows没有变化
-- 这是由左连接特性决定的。LEFT JOIN条件用于确定如何从右表搜索行,左边一定都有,所以右边是我们的关键点,一定需要建立索引
-- 同理,如果是右连接,RIGHT JOIN条件用于确定如何从左表搜索行,右边一定都有,所以左边是我们的关键点,一定需要建立索引。
三表
建表语句
CREATE TABLE IF NOT EXISTS `phone`(
`phoneId` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY(`phoneId`)
);
INSERT INTO phone(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO phone(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO phone(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO phone(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO phone(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO phone(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO phone(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO phone(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO phone(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO phone(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO phone(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO phone(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO phone(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO phone(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO phone(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO phone(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO phone(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO phone(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO phone(card) VALUES (FLOOR(1+(RAND()*20)));
INSERT INTO phone(card) VALUES (FLOOR(1+(RAND()*20)));
案例
-- 分析三表联合查询
EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card = book.card LEFT JOIN phone ON book.card = phone.card
-- 添加索引
ALTER TABLE `book` ADD INDEX Y (`card`);
ALTER TABLE `phone` ADD INDEX Z (`card`);
-- 再次分析,后两行的type为ref且总rows优化很好,效果不错。因此索引最好设置在需要经常查询的字段中。
-- 结论:JOIN语句的优化
-- 尽可能减少JOIN语句中的NestedLoop的循环总次数:“永远用小结果集驱动打的结果集”。
-- 优先优化NestedLoop的内存循环
-- 保证JOIN语句中被驱动表上JOIN条件字段已被索引
-- 当无法保证被驱动表的JOIN条件字段被索引且内存资源充足的前提下,不要太吝惜JoinBuffer的设置
索引失效(应该避免)
建表
CREATE TABLE `staffs` (
`id` int(11) NOT NULL,
`name` varchar(24) NOT NULL COMMENT '姓名',
`age` int(11) NOT NULL COMMENT '年龄',
`pos` varchar(24) NOT NULL COMMENT '职位',
`add_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工记录表';
INSERT INTO `staffs` (`name`, `age`, `pos`, `add_time`) VALUES ( '2000', 23, 'dev', NOW());
INSERT INTO `staffs` (`name`, `age`, `pos`, `add_time`) VALUES ( 'july', 23, 'dev', NOW());
INSERT INTO `staffs` (`name`, `age`, `pos`, `add_time`) VALUES ('z3', 22, 'manager', '2023-06-11 20:20:22');
ALTER TABLE staffs ADD INDEX idx_staffs_nameAgePos(name,age,pos)
案例(索引失效)
-
全值匹配我最爱
-
最佳左前缀法则:如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不挑过索引中的列 。
-- 测试语句
EXPLAIN SELECT * FROM staffs WHERE age = 23 AND pos = 'dev'
-- 结果:全表扫描,未使用索引
-- 测试语句
EXPLAIN SELECT * FROM staffs WHERE age = 23
-- 结果:全表扫描,未使用索引
-- 测试语句
EXPLAIN SELECT * FROM staffs WHERE name = 'july'
-- 结果:使用到了索引
-- 测试语句
EXPLAIN SELECT * FROM staffs WHERE name = 'july' and pos = 'dev'
-- 结果:使用了部分索引
-- 总结:最佳左前缀法则,带头大哥不能死,中间部分不能断
- 不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描
- 存储引擎中不能使用索引中范围条件右边的列
- 尽量使用覆盖索引(只访问索引的查询(索引列与查询列一直)),减少select *
- MySQL在使用不等于(!=或者<>)的时候无法使用索引会导致全表扫描
- is null,is not null 也无法使用索引
- like 以通配符开头(‘%abc…’)MySQL索引失效会变成全表扫描的操作
- 如何解决:使用覆盖索引
- 字符串不加单引号索引失效
- 少用or,用它来连接会索引失效
- 总结:示例索引index(a,b,c)
Where语句 | 索引是否被使用 |
---|---|
where a = 3 | Y,使用到a |
where a = 3 and b = 5 | Y,使用到a,b |
where a = 3 and b = 4 and c = 5 | Y,使用到a,b,c |
where b = 3 或者 where b = 3 and c = 4 或者 where c = 4 | N |
where a = 3 and c = 5 | 使用到a,但是c不可以,b中间断了 |
where a = 3 and b > 4 and c = 5 | 使用到a和b,c不能用在范围之后,b断了 |
where a = 3 and b like ‘kk%’ and c = 4 | Y,使用到a,b,c |
where a = 3 and b like ‘%kk’ and c = 4 | Y,只使用到a |
where a = 3 and b like ‘%kk%’ and c = 4 | Y,只使用到a |
where a = 3 and b like ‘k%kk%’ and c = 4 | Y,使用到a,b,c |
面试相关
建表语句
CREATE TABLE `test03` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`c1` char(10) DEFAULT NULL,
`c2` char(10) DEFAULT NULL,
`c3` char(10) DEFAULT NULL,
`c4` char(10) DEFAULT NULL,
`c5` char(10) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `test03` (`c1`, `c2`, `c3`, `c4`, `c5` )
VALUES
( 'a1', 'a2', 'a3', 'a4', 'a5' );
INSERT INTO `test03` (`c1`, `c2`, `c3`, `c4`, `c5` )
VALUES
( 'b1', 'b2', 'b', 'b4', 'b5' );
INSERT INTO `test03` (`c1`, `c2`, `c3`, `c4`, `c5` )
VALUES
( 'c1', 'c2', 'c3', 'c4', 'c5' );
INSERT INTO `test03` (`c1`, `c2`, `c3`, `c4`, `c5` )
VALUES
( 'd1', 'd2', 'd3', 'd4', 'd5' );
INSERT INTO `test03` (`c1`, `c2`, `c3`, `c4`, `c5` )
VALUES
( 'e1', 'e2', 'e3', 'e4', 'e5' );
-- 建立索引
CREATE INDEX idx_test03_c1234 ON test03 ( c1, c2, c3, c4 );
总结
定值、范围还是排序,一般order by是给个范围
group by基本上都需要进行排序,会有临时表产生
一般性建议
- 对于单键索引,尽量选择针对当前query过滤性更好的索引
- 在选择组合索引的时候,当前Query中过滤性最好的字段在索引字段顺序中,位置越靠前越好
- 在选择组合索引的时候,尽量选择可以能够包含当前Query中的Where子句中更多字段的索引
- 尽可能通过分析统计信息和调整Query的写法来达到选择合适索引的目的。
优化总结口诀
全值匹配我最爱,最左前缀要遵守;
带头大哥不能死,中间兄弟不能断;
索引列上少计算,范围之后全失效;
LIKE百分写最右,覆盖索引不写星;
不等空值还有OR,索引失效要少用;
VAR引号不可丢,SQL高级也不难!
查询截取分析
查询优化
永远小表驱动大表
类似嵌套循环 Nested Loop
优化原则:小表驱动大表,即小的数据集驱动大的数据集
-- 原理(RBO)
select * from A where id in (select id in B)
-- 等价于循环
for select id from B
for select * from A where A.id = B.id
-- 当B表的数据集必须小于A表的数据集时,用in优于exists
select * from A where id existss (select id in B)
-- 等价于
for select * from A
for select * from B where B.id = A.id
-- 当A的数据集小于B表的数据集时,用exists优先于in
-- 注意:A表与B表的ID字段应建立索引
- EXISTS
SELECT … FROM table WHERE EXISTS(subquery)
该语法可以理解为:将主查询的数据,放到子查询中做条件验证,根据验证结果(TRUE或FALSE)来决定主查询的数据结果是否得以保留。
- 提示
- EXISTS(subquery) 只返回TRUE或FALSE,因此子查询中的SELECT * 也可以是SELECT 1或其他,官方说法是实际执行时会忽略SELECT清单,实际上没有区别
- EXISTS子查询的实际执行过程可能经过 优化,而不是我们理解上的逐条对比。如果担忧效率问题,可进行实际检验以确定是否有效率问题。
- EXISTS子查询往往也可以用条件表达式、其他子查询或者JOIN来替代,何种最优需要具体问题具体分析
ORDER BY关键字优化
避免FileSort排序
ORDER BY 子句,尽量使用Index方式排序,避免使用FileSort方式排序
建表语句
-- 建表
CREATE TABLE `tbla` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`age` int(11) DEFAULT NULL,
`birth` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 新增测试数据
INSERT INTO tbla(age,birth) VALUES(22,NOW());
INSERT INTO tbla(age,birth) VALUES(23,NOW());
INSERT INTO tbla(age,birth) VALUES(24,NOW());
-- 创建索引
CREATE INDEX idx_A_ageBirth ON tbla(age,birth);
案例
-- 测试案例1
EXPLAIN SELECT * FROM tbla WHERE age > 20 ORDER BY age;
-- 结果:未使用FileSort排序
-- 测试案例2
EXPLAIN SELECT * FROM tbla WHERE age > 20 ORDER BY age,birth;
-- 结果:未使用FileSort排序
-- 测试案例3
EXPLAIN SELECT * FROM tbla WHERE age > 20 ORDER BY birth;
-- 结果:使用FileSort排序
-- 测试案例4
EXPLAIN SELECT * FROM tbla WHERE age > 20 ORDER BY birth,age;
-- 结果:使用FileSort排序
-- 测试案例5
EXPLAIN SELECT * FROM tbla ORDER BY birth;
-- 结果:使用FileSort排序
-- 测试案例6
EXPLAIN SELECT * FROM tbla WHERE birth > '2023-01-01 00:00:00' ORDER BY birth;
-- 结果:使用FileSort排序
-- 测试案例7
EXPLAIN SELECT * FROM tbla WHERE birth > '2023-01-01 00:00:00' ORDER BY age;
-- 结果:未使用FileSort排序
-- 测试案例8
EXPLAIN SELECT * FROM tbla ORDER BY age ASC,birth DESC;
-- 结果:使用FileSort排序
总结
MySQL支持两种方式的排序,FileSort和Index,Index效率高,它指MySQL扫描索引本身完成排序。FileSort方式效率低。
ORDER BY 满足两种情况,会使用Index方式排序:
- ORDER BY语句使用索引最左前列
- 使用WHERE子句与ORDER BY子句条件列组合满足索引最左前列
尽可能在索引列上完成排序操作,遵照索引建的最佳左前缀
FileSort排序
如果不在索引列上,FileSort有两种算法,MySQL就要启动双路排序和单路排序
双路排序
MySQL 4.1之前是使用双路排序,字面意思就是两次扫描磁盘,最终得到数据。
读取行指针和order by列,对他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据输出。
从磁盘读取排序字段,在Buffer进行排序,再从磁盘取其他字段
单路排序
取一批数据,要对磁盘进行两次扫描,众所周知,IO是很耗时的,所以在MySQL4.1之后,出现了第二种改进的算法,就是单路排序。
从磁盘读取查询需要的所有列,按照ORDER BY列在Buffer对它们进行排序,然后扫描排序后的列表进行输出,因为它的效率更快一下,避免了第二次读取数据。并把随机IO变成了顺序IO,但是它会使用更多的空间,因为它把每一行都保存在内存中了。
结论及引申出的问题
由于单路是后出的,总体而言好过双路。
单路的问题:
在sort_buffer中,方法B比方法A要多占用很多空间,因为方法B是把所有字段都取出,所以有可能取出的数据的总大小超出了sort_buffer的容量,导致每次只能读取sort_buffer容量的数据,进行排序(创建tmp文件,多路合并),拍完序后再取sort_buffer容量大小,再排……从而多次IO。
本来想省一次IO操作,结果却导致了大量的IO操作,反而得不偿失。
优化策略:
-
增大sort_buffer_size参数的配置
-
增大max_length_for_sort_data参数的设置
优化原因:
-
ORDER BY 时,select * 是一个大忌,只Query需要的字段,这点非常重要。在这里的影响是:
- 当Query的字段大小总和小于max_length_for_sort_data而且排序字段不是TEXT/BOLB类型时,会用改进后的算法-单路排序,否则用老算法-多路排序
- 两种算法的数据都有可能炒作sort_buffer的容量,超出之后,会创建tmp文件进行合并排序,导致多次IO,但是用单路排序算法的风向更大一些,所以要提高sort_buffer_size。
-
尝试提高 sort_buffer_size:不管用哪种算法,提高这个参数都会提高效率,当然,要根据系统的能力去提高,因为这个参数是针对每个进程的。
-
尝试提高 max_length_for_sort_data:提高这个参数,会增加用改进算法的概率。但是如果设的太高,数据总容量超出sort_buffer_size的概率就增大,明显症状是高的磁盘IO活动和低的处理器使用率。
总结
- MySQL有两种排序方式:文件排序或扫描有序索引排序
- MySQL能为排序与查询使用相同的索引
测试案例:
-- 索引:
KEY a_b_c(a,b,c)
-- ORDER BY能使用索引最左前缀
ORDER By a,
ORDER By a,b
ORDER By a,b,c
ORDER By a DESC,b DESC,c DESC -- 排序顺序需要一致,同升或同降,不一致则不会使用索引排序
-- 如果WHERE使用索引的最左前缀定义为常量,则ORDER BY能使用索引
WHERE a = const ORDER BY b,c
WHERE a = const AND b = const ORDER BY c
WHERE a = const AND b > const ORDER BY b,c
-- 不能使用索引进行排序
ORDER BY a ASC,b DESC,c DESC -- 排序不一致
WHERE g = const ORDER BY b,c -- 丢失a索引
WHERE a = const ORDER BY c -- 丢失b索引
WHERE a = const ORDER BY a,d -- d不是索引的一部分
WHERE a in (...) ORDER BY b,c -- 对于排序来说,多个相等条件也是范围查询
GROUP BY关键字优化
优化与ORDER BY几乎一致
-
GROUP BY实质是先排序后进行分组,遵照索引建的最佳左前缀
-
当无法使用索引列,增大max_length_for_sort_data参数的设置,增大sort_buffer_size参数的设置
-
WHERE高于HAVING,能写在WHERE限定的条件就不要去HAVING限定了
慢查询日志
简介
- MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过阈值的语句
- 具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。long_query_time的默认值是10,意思是运行10秒以上的语句。
- 由他来查看那些SQL超出了我们的最大忍耐时间值,比如一条SQL执行时间超过5秒,就算慢SQL,希望能收集超过5秒的SQL,结合之前EXPLAIN进行全面分析
使用
说明
默认情况下,MySQL数据库没有开启慢查询日志,需要手动来设置这个参数。
当然,如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件。
开启
- 查看是否开启
SHOW VARIABLES LIKE '%slow_query_log%';
-- 默认情况下,slow_query_log的值是OFF,表示慢查询日志是禁用的,可以通过设置slow_query_log的值来开启
- 开启慢查询日志
-- 使用 set global slow_query_log=1 开启慢查询日志只对当前数据库生效,如果MySQL重启后则会失效
set global slow_query_log=1
-- 如果要永久生效,那么就必须修改配置文件my.cnf(其他系统变量也是如此)
-- 修改my.cnf文件,[mysqld]下增加或修改参数:slow_query_log和slow_query_log_file后,然后重启MySQL服务器。
slow_query_log=1
slow_query_log_file=/var/lib/mysql/root-slow.log
-- 关于慢查询的参数slow_query_log_file,它指定慢查询日志文件的存放路径,系统默认会给一个缺省的文件host_name-slow.log(如果没有指定参数slow_query_log_file的话)
慢查询语句记录
这个是由参数long_query_time控制,默认情况下long_query_time的值为10秒,命令:
SHOW VARIABLES LIKE 'long_query_time%';
这个参数可以使用命令修改,也可以在my.cnf参数里修改。
set global long_query_time = 3
-- 设置后再次查询,会发现值没有变化
-- 这时需要重新连接或新开一个会话才能看到修改值
SHOW VARIABLES LIKE 'long_query_time%';
-- 或者使用命令查询全局
SHOW GLOBAL VARIABLES LIKE 'long_query_time%';
-- 测试慢查询语句 4秒的查询语句
SELECT sleep(4);
-- 通过命令查询MySQL中有多少慢查询记录
SHOW GLOBAL STATUS LIKE '%Slow_queries%';
假如运行时间正好等于long_query_time的情况,并不会被记录下来。也就是说,
在MySQL源码里是**判断大于long_query_time,而非大于等于。 **
配置项
[mysqld]
slow_query_log=1,
slow_query_log_file=/var/lib/mysql/root-slow.log
long_query_time=3
log_output=FILE
日志分析工具mysqldumpslow
在生产环境中,如果要手工分析日志,查询、分析SQL,显然是个体力活,MySQL提供了日志分析工具mysqldumpslow
查看帮助信息
mysqldumpslow --help
-
s:是表示按照何种方式排序
-
c:访问次数
-
l:锁定时间
-
r:返回记录
-
t:查询时间
-
al:平均锁定时间
-
ar:平均返回记录数
-
at:平均查询时间
-
t:返回前面多少条的数据
-
g:后边搭配一个正则匹配模式,大小写不敏感
工作常用参考
# 得到返回记录集最多的10个SQL
mysqldumpslow -s r -t 10 /var/lib/mysql/root-slow.log
# 得到访问次数最多的10个SQL
mysqldumpslow -s c -t 10 /var/lib/mysql/root-slow.log
# 得到按照时间排序的前10条里面含有左连接的查询语句
mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/root-slow.log
# 另外建议使用这些命令时结合| 和 more 使用,否则有可能出现爆屏情况
mysqldumpslow -s r -t 10 /var/lib/mysql/root-slow.log | more
批量数据脚本
往表里插入1000W条数据
建表
-- 创建部门表
CREATE TABLE `dept` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`deptno` mediumint(9) DEFAULT NULL,
`dname` varchar(20) DEFAULT NULL,
`loc` varchar(13) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 创建员工表
CREATE TABLE `emp` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`empno` mediumint(9) DEFAULT NULL COMMENT '编号',
`ename` varchar(20) DEFAULT NULL COMMENT '员工姓名',
`job` varchar(9) DEFAULT NULL COMMENT '工作',
`mgr` mediumint(9) DEFAULT NULL COMMENT '上级编号',
`hiredate` date DEFAULT NULL COMMENT '入职时间',
`sal` decimal(7,2) DEFAULT NULL COMMENT '薪水',
`comm` decimal(7,2) DEFAULT NULL COMMENT '红利',
`deptno` mediumint(9) DEFAULT NULL COMMENT '部门编号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
设置参数
创建函数,假如报错:This function has none of DETERMINISTIC……
由于开启过慢查询日志,因为我们开启了bin_log,我们就必须为我们的function指定一个参数。
SHOW VARIABLES LIKE 'log_bin_trust_function_creators';
SET GLOBAL log_bin_trust_function_creators=1;
这样添加参数以后,如果MySQL重启,上述参数又会消失,永久生效的办法,还是修改配置文件。
[mysqld]
log_bin_trust_function_creators=1
创建函数
保证每条数据都不相同
随机产生字符串
DELIMITER $$
CREATE FUNCTION rand_string ( n INT ) RETURNS VARCHAR ( 255 ) BEGIN
DECLARE
chars_str VARCHAR ( 100 ) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
DECLARE
return_str VARCHAR ( 255 ) DEFAULT '';
DECLARE
i INT DEFAULT 0;
WHILE i < n DO
SET return_str = CONCAT(
return_str,
SUBSTRING( chars_str, FLOOR( 1 + RAND()* 52 ), 1 ));
SET i = i + 1;
END WHILE;
RETURN return_str;
END $$
-- 删除函数
DROP FUNCTION rand_string
随机产生部门编号
DELIMITER $$
CREATE FUNCTION rand_num () RETURNS INT ( 5 ) BEGIN
DECLARE
i INT DEFAULT 0;
SET i = FLOOR( 100 + RAND()* 10 );
RETURN i;
END $$
-- 删除函数
DROP FUNCTION rand_num
创建存储过程
创建往emp表中插入数据的存储过程
DELIMITER $$
CREATE PROCEDURE insert_emp (
IN START INT ( 10 ),
IN max_num INT ( 10 )) BEGIN
DECLARE
i INT DEFAULT 0;# 把autocommit 设置为0
SET autocommit = 0;
REPEAT
SET i = i + 1;
INSERT INTO emp ( empno, ename, job, mgr, hiredate, sal, comm, deptno )
VALUES
(
( START + i ),
rand_string ( 6 ),
'SALESMAN',
0001,
CURDATE(),
2000,
400,
rand_num ());
UNTIL i = max_num
END REPEAT;
COMMIT;
END $$
-- 删除存储过程
DROP PROCEDURE insert_emp;
创建往dept表中插入数据的存储过程
DELIMITER $$
CREATE PROCEDURE insert_dept (
IN START INT ( 10 ),
IN max_num INT ( 10 )) BEGIN
DECLARE
i INT DEFAULT 0;# 把autocommit 设置为0
SET autocommit = 0;
REPEAT
SET i = i + 1;
INSERT INTO dept ( deptno, dname, loc )
VALUES
(
( START + i ),
rand_string ( 10 ),
rand_string ( 8 ));
UNTIL i = max_num
END REPEAT;
COMMIT;
END $$
-- 删除存储过程
DROP PROCEDURE insert_dept;
调用存储过程
- 部门表插入
DELIMITER;
CALL insert_dept(100,10);
- 员工表插入
-- 执行存储过程,往emp表添加50万条数据
CALL insert_emp(100001,500000);
ShowProfile
简介
ShowProfile,是MySQL提供可以用来分析当前会话中语句执行的资源消耗情况。可以用于SQL调优的测量
官网地址:https://dev.mysql.com/doc/refman/5.5/en/show-profile.html
默认情况下,参数处于关闭状态,并保存最近15次的运行结果。
分析步骤
- 查看当前MySQL版本是否支持功能
SHOW VARIABLES like 'profiling';
-- 或者
SHOW VARIABLES like 'profiling%';
-- 默认是关闭的,使用前需要开启
- 开启功能,默认是关闭的,使用前需要开启
SET profiling = on;
-- 查看是否开启
SHOW VARIABLES like 'profiling';
- 运行SQL
-- 测试SQL
select * from emp group by id%10 limit 150000;
select * from emp group by id%20 order by 5;
- 查看结果
show profiles;
- 诊断SQL
- 参数备注:
- ALL 显示所有的开销信息
- BLOCK IO 显示所有块IO相关开销
- CONTEXT SWITCHES 上下文切换相关开销
- CPU 显示CPU相关开销信息
- IPC 显示发送和接收相关开销信息
- MEMORY 显示内存相关开销信息
- PAGE FAULTS 显示页面错误相关开销信息
- SOURCE 显示和Source_function,Source_file,Source_line相关的开销信息
- SWAPS 显示交换次数相关开销的信息
- 参数备注:
show profile cpu,block io for query 上一步获取到的Query_ID;
- 日常开发需要注意的步骤
- converting HEAP to MySIAM 查询结果太大,内存都不够用了往磁盘上搬了
- Creating tmp table 创建临时表
- 拷贝数据到临时表
- 用完再删除
- Copying to tmp table on desk 把内存中临时表复制到磁盘,危险!
- locked
全局查询日志
不要在生产环境中启用
- 配置启用
# 在MySQL的my.cnf中,设置如下:
# 开启
general_log=1
# 记录日志文件的路径
general_log_file=/path/logfile
# 输出格式
log_output=FILE
- 编码启用
-- 命令
SET GLOBAL general_log=1;
SET GLOBAL log_output='TABLE';
-- 此后,所有编写的SQL语句,都会记录到MySQL库里的general_log表,可以用下面的名称查看
select * from mysql.general_log;
MySQL锁机制
概述
锁是计算机协调多个进程或线程并发访问某一资源的机制。
在数据库中,除传统的计算资源(如CPU、RAM、IO等)的争用意外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。
锁的分类
从对数据操作的类型分
- 读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响
- 写锁(排他锁):当前写操作没有完成前,它会阻断其他写锁和读锁
从对数据操作的粒度分
- 表锁
- 行锁
表锁(偏读)
特点
偏向MyISAM存储引擎,开销小,加锁快;无死锁;锁定力度大,发生锁冲突的概率最高,并发度最低。
案例分析
建表
-- 使用MyISAM引擎
CREATE TABLE `mylock` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
-- 插入数据
INSERT INTO mylock(name) VALUES ('a');
INSERT INTO mylock(name) VALUES ('b');
INSERT INTO mylock(name) VALUES ('c');
INSERT INTO mylock(name) VALUES ('d');
INSERT INTO mylock(name) VALUES ('e');
-- 手动增加表锁
lock table 表名 read(write),表名2 read(write),其他;
-- 查看表上加过的锁
show open tables;
-- 释放所有锁
unlock tables;
加读锁
-- 添加读锁
lock table mylock read;
-- 读表
select * from mylock;
-- 读表没有问题
-- 尝试更新数据
update mylock set name = 'a2' where id = 1;
ERROR 1099 - Table 'mylock' was locked with a READ lock and can't be updated
-- 当前session插入或者更新锁定的表都会提示错误
-- 其他session插入或者更新锁定表会一直等待获得锁
-- 读其他表
select * from book;
ERROR 1100 - Table 'book' was not locked with LOCK TABLES
-- 当前session不能查询没有锁定的表
-- 其他连接可以查询或者更新未锁定的表
加写锁
-- 当前连接添加写锁
lock table mylock write;
-- 当前连接对锁定表的查询、更新、插入操作都可以执行
-- 其他连接对锁定表的查询被阻塞,需要等待锁被释放
案例结论
MyISAM在执行查询语句(SELECT)前,会自动给涉及到的所有表加读锁,在执行增删改操作前,会自动给设计的表加写锁。
MySQL的表级锁有两种模式:
表共享读锁(Table Read Lock)
表独占写锁(Table Write Lock)
锁类型 | 可否兼容 | 读锁 | 写锁 |
---|---|---|---|
读锁 | 是 | 是 | 否 |
写锁 | 是 | 否 | 否 |
结论:
结合上表,所以对MyISAM表进行操作,会有以下情况:
- 1、对MyISAM表的读操作(加读锁),不会阻塞其他进程对同一表的读请求,但会阻塞对同一表的写请求。只有当读锁释放后,才会执行其他进程的写操作
- 2、对MyISAM表的写操作(加写锁),会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会好执行其他进程的读写操作。
简而言之,就是读锁会阻塞写,但是不会阻塞读。而写锁会把读和写都堵塞
表锁分析
- 查看那些表被加锁了
show open tables;
- 如何分析表锁定
-- 可以通过检查table_locks_waited和table_locks_immediate状态变量来分析系统上的表锁定
show status like 'table%';
这里有两个状态变量记录MySQL内部表级锁定的情况,这两个变量说明如下:
- Table_locks_immediate:产生表级锁定的次数,表示可以立即获取锁的查询次数,每立即获取锁值加1;
- Table_locks_vaited:出现表级锁定争用而发生等待的次数(不能立即获取锁的次数,每等待一次锁值加1),此值高则说明存在较严重的表级锁争用情况。
此外,MyISAM的读写锁调度是写优先,这也是MyISAM不适合做写为主表的引擎。因为写锁后,其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞。
行锁(偏写)
特点
偏向InnoDB存储引擎,开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
InnoDB和MyISAM的最大不同有两点:一是支持事务(TRANSACTION),二是采用了行级锁
事务
事务及其ACID属性
事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性。
- 原子性(Atomicity):事务是一个原子性操作单元,其对数据的修改,要么全都执行,要么全都不执行
- 一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保证数据的完整性;事务结束时,所有的内部数据结构(如B数索引或双向链表)也都必须是正确的
- 隔离性(Isolation):数据库同提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然
- 持久性(Durable):事务完成之后,它对数据的修改是永久性的,即使出现系统故障也能保持
并发事务处理带来的问题
更新丢失(Lost Update)
当两个或多个事务选择同一行,然后基于选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题——最后的更新覆盖了由其他事务所做的更新。
例如,两个程序员修改同一java文件。每个程序员独立的更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。最后保存其更改副本的编辑人员覆盖前一个程序员所做的更改。
如果在一个程序员完成并提交事务之前,另一个程序员不能访问同一文件,则可避免此问题。
脏读(Dirty Reads)
一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态;这时候,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏“数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象并形象的叫做”脏读“。
一句话,事务A读取了事务B已修改但尚未提交 的数据,还在这个数据基础上做了操作。此时,如果事务B回滚,A读取的数据无效,不符合一致性要求。
不可重复读(Non-Repeatable Reads)
一个事务在读取某个数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了。这种现象就叫做“不可重复读”。
一句话,事务A读取了事务B已经提交的修改数据,不符合隔离性。
幻读(Phantom Reads)
一个事务按照相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为”幻读“。
一句话,事务A读取刅了事务B提交的新数据,不符合隔离性。
幻读与脏读有点类似:
- 脏读是事务B里面修改了数据
- 幻读是事务B里面新增了数据
事务隔离级别
脏读、不可重复读和幻读,其实都是数据库一致性问题,必须由数据库提供一定的事务隔离机制来解决。
读数据一致性及允许的并发副作用/隔离级别 | 读数据一致性 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
未提交读(Read uncommitted) | 最低级别,只能保证不读取物理上损坏的数据 | 是 | 是 | 是 |
已提交读(Read committed) | 语句级 | 否 | 是 | 是 |
可重复读(Repeatable read) | 事务级 | 否 | 否 | 是 |
可序列化(Serializable) | 最高级别,事务级 | 否 | 否 | 否 |
数据库的事务隔离越严格,并发副作用越小,但付出的代价越大,因为事务隔离实质上就是使事务在一定程度上“串行化”进行,这显然与“并发”是矛盾的。同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读”和“幻读”并不敏感,可能更关系数据并发访问的能力。
查看当前数据库的事务隔离级别:
SHOW VARIABLES LIKE 'tx_isolation';
案例分析
建表语句
-- 创建表,存储引擎为innodb
CREATE TABLE `innodb_lock` (
`a` int(11) DEFAULT NULL,
`b` varchar(16) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入测试数据
INSERT INTO innodb_lock VALUES(1,'b2');
INSERT INTO innodb_lock VALUES(1,'b1');
INSERT INTO innodb_lock VALUES(3,'3');
INSERT INTO innodb_lock VALUES(4,'4000');
INSERT INTO innodb_lock VALUES(5,'5000');
INSERT INTO innodb_lock VALUES(6,'6000');
INSERT INTO innodb_lock VALUES(7,'7000');
INSERT INTO innodb_lock VALUES(8,'8000');
INSERT INTO innodb_lock VALUES(9,'9000');
-- 新增索引
create index idx_innodb_lock_a_ind on innodb_lock(a);
create index idx_innodb_lock_b_ind on innodb_lock(b);
行锁定基本案例
客户端1 | 客户端2 |
---|---|
关闭自动提交:set autommit=0; | 关闭自动提交:set autommit=0; |
更新但是不提交,没有手写commit:update innodb_lock set b = ‘4001’ where a = 4 | 执行更新语句:更新但是不提交,没有手写commit:update innodb_lock set b = ‘4001’ where a = 1被阻塞,只能等待 |
提交更新:commit; | 解除阻塞,更新下正常进行 |
提交更新:commit; |
无索引行锁升级为表锁
索引失效场景:索引类型为VARCHAR的,必须加上单引号
客户端1 | 客户端2 |
---|---|
关闭自动提交:set autommit=0; | 关闭自动提交:set autommit=0; |
正常案例:更新id=4的语句:update innodb_lock set b = ‘4002’ where a = 4 | 正常案例:更新id=9的语句:update innodb_lock set b = ‘9001’ where a = 0 |
提交更新,正常:commit; | 提交更新,正常:commit; |
已添加索引,b为VARCHAR类型 | |
错误案例:更新b='4002’的语句:update innodb_lock set a = 42 where b = 4002 | 更新id=9的语句:update innodb_lock set b = ‘9002’ where a = 0被阻塞,只能等待 |
提交更新:commit; | 解除阻塞,更新正常进行 |
提交更新:commit; |
间隙锁危害
客户端1 | 客户端2 |
---|---|
关闭自动提交:set autommit=0; | 关闭自动提交:set autommit=0; |
更新语句:update innodb_lock set b=‘0616’ where a > 1 and b < 6 | 新增语句:insert into innodb_lock values(2,‘2000’); 被阻塞,只能等待 |
提交更新:commit; | 解除阻塞,插入正常进行 |
提交更新:commit; |
间隙锁简介
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内并不存在的记录,叫做”间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的“间隙锁(Next-Key锁)”。
危害
因为Query执行过程中通过范围查找的话,会锁定整个范围内所有的索引键值,即使这个键值并不存在。
间隙锁有一个比较致命的弱点,就是当锁定一个范围键值之后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下可能会对性能造成很大的危害。
如何锁定一行
通过select xxx for update锁定某一行后,其他的操作会被阻塞,直到锁定行的会话提交commit
案例结论
InnoDB存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会更高一些,但是在整体并发处理能力方面要远远优于MyISAM的表级锁定的。当系统并发量较高的时候,InnoDB的整体性能和MyISAM相比就会有比较明显的优势了。
但是,InnoDB的行级锁定同样也有其脆弱的一面,当我们使用不当的时候,可能会让InnoDB的整体性能表现不仅不能比MyISAM高,甚至可能会更差。
行锁分析
通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况。
SHOW STATUS LIKE 'innodb_row_lock%';
对各个状态量的说明如下:
- Innodb_row_lock_current_waits:当前正在等待锁定的数量
- Innodb_row_lock_time:从系统启动到现在锁定总时间长度
- Innodb_row_lock_time_avg:每次等待所花平均时间
- Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花时间
- Innodb_row_lock_waits:系统启动后到现在总共等待的次数
对于这5个状态变量,比较重要的主要是Innodb_row_lock_time_avg(等待平均时长)、Innodb_row_lock_waits(等待总次数)、Innodb_row_lock_time(等待总时长)这三项。
优化建议
- 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
- 合理设计索引,尽量缩小锁的范围
- 尽可能减少检索条件,避免间隙锁
- 尽量控制事务大小,减少锁定资源量和时间长度
- 尽可能低级别事务隔离
页锁
了解即可
开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
主从复制
复制的基本原理
Slave会从Master读取binlog来进行数据同步。
MySQL复制过程分为三步:
- 1、mater将改变记录到二进制日志(binary log)。这些记录过程叫做二进制日志文件,binary log events;
- 2、slave将master的binary log events拷贝到它的中继日志(relay log)‘
- 3、slave重做中继日志中的事件,将改变应用到自己的数据库中。MySQL复制是异步的且串行化的。
复制的基本原则
- 每个Slave只有一个master
- 每个slave只能有一个唯一的服务器ID
- 每个master可以有多个slave
复制的最大问题
延时
一主一从常见配置
要求:
- MySQL版本一直且后台以服务运行
- 主从服务器在同一网段,互相之间可以ping通
主机配置
主从配置都是在[mysqld]节点下,都是小写
以Windows为例,修改my.ini配置文件
- *主服务器唯一ID:server-id=1
- *启用二进制日志:log-bin=本地路径/mysqlbin
- 示例:log-bin=D:/Program/MySQL/data/mysqlbin
- 启用错误日志:log-err=本地路径/mysqlerr
- 示例:log-err=D:/Program/MySQL/data/mysqlerr
- 根目录:basedir=“本地路径”
- 示例:basedir=“D:/Program/MySQL/”
- 临时目录:tmpdir=“本地目录”
- 示例:tmpdir=“D:/Program/MySQL/”
- 数据目录:datadir=“本地目录/Data”
- 示例:datadir=“D:/Program/MySQL/Data/”
- read-only=0:主机,读写都可以
- 设置不要复制的数据库:binlog-ignore-db=mysql
- 设置需要复制的数据库:binlog-do-db=需要复制的主数据库名称
从机配置
从机以Linux服务器示例,修改my.cnf配置文件
- *主服务器ID
- 启用二进制日志
重启服务,并关闭防火墙
由于配置文件已经修改,需要重启MySQL服务。
主机与从机都需要关闭防火墙。
在Windows主机上建立账户并授权slave
-- 建立账户并授权
GRANT REPLICATION SLAVE ON *.* TO 'test_sync'@'从机数据库IP' IDENTIFIED BY '123456';
-- 刷新配置
flush privileges;
查询master的状态
SHOW master STATUS;
-- 记录下File和Position的值
执行完此步骤后不要再操作主服务MySQL,防止主服务器状态值变化
在Linux从机上配置需要复制的主机
CHANGE MASTER TO MASTER_HOST=’主机IP’,
MASTER_USER='test_sync',
MASTER_PASSWORD='123456',
MASTER_LOG_FILE='File名字',
MASTER_LOG_POS=Position数字;
从服务器的复制功能
-- 启动
start slave;
-- 停止
stop slave;
查看状态
SHOW SLAVE STATUS\G
-- 查看下面两个参数,都是YES,则说明配置成功
Slave_IO_Running:Yes
Slave_SQL_Running:Yes
主机新建数据库、新建表、新增记录,测试从服务器的复制功能。
g events;
- 2、slave将master的binary log events拷贝到它的中继日志(relay log)‘
- 3、slave重做中继日志中的事件,将改变应用到自己的数据库中。MySQL复制是异步的且串行化的。
复制的基本原则
- 每个Slave只有一个master
- 每个slave只能有一个唯一的服务器ID
- 每个master可以有多个slave
复制的最大问题
延时
一主一从常见配置
要求:
- MySQL版本一直且后台以服务运行
- 主从服务器在同一网段,互相之间可以ping通
主机配置
主从配置都是在[mysqld]节点下,都是小写
以Windows为例,修改my.ini配置文件
- *主服务器唯一ID:server-id=1
- *启用二进制日志:log-bin=本地路径/mysqlbin
- 示例:log-bin=D:/Program/MySQL/data/mysqlbin
- 启用错误日志:log-err=本地路径/mysqlerr
- 示例:log-err=D:/Program/MySQL/data/mysqlerr
- 根目录:basedir=“本地路径”
- 示例:basedir=“D:/Program/MySQL/”
- 临时目录:tmpdir=“本地目录”
- 示例:tmpdir=“D:/Program/MySQL/”
- 数据目录:datadir=“本地目录/Data”
- 示例:datadir=“D:/Program/MySQL/Data/”
- read-only=0:主机,读写都可以
- 设置不要复制的数据库:binlog-ignore-db=mysql
- 设置需要复制的数据库:binlog-do-db=需要复制的主数据库名称
从机配置
从机以Linux服务器示例,修改my.cnf配置文件
- *主服务器ID
- 启用二进制日志
重启服务,并关闭防火墙
由于配置文件已经修改,需要重启MySQL服务。
主机与从机都需要关闭防火墙。
在Windows主机上建立账户并授权slave
-- 建立账户并授权
GRANT REPLICATION SLAVE ON *.* TO 'test_sync'@'从机数据库IP' IDENTIFIED BY '123456';
-- 刷新配置
flush privileges;
查询master的状态
SHOW master STATUS;
-- 记录下File和Position的值
执行完此步骤后不要再操作主服务MySQL,防止主服务器状态值变化
在Linux从机上配置需要复制的主机
CHANGE MASTER TO MASTER_HOST=’主机IP’,
MASTER_USER='test_sync',
MASTER_PASSWORD='123456',
MASTER_LOG_FILE='File名字',
MASTER_LOG_POS=Position数字;
从服务器的复制功能
-- 启动
start slave;
-- 停止
stop slave;
查看状态
SHOW SLAVE STATUS\G
-- 查看下面两个参数,都是YES,则说明配置成功
Slave_IO_Running:Yes
Slave_SQL_Running:Yes
主机新建数据库、新建表、新增记录,测试从服务器的复制功能。