赞
踩
2020年暑假期间我开始接触 MySQL,通过阅读了几本相关书籍以及网上的一些教程,我逐渐了解了 MySQL 的入门级内容。在暑假期间学完 MySQL 的基础知识后,我便将其搁置在一旁,没有再进行进一步的探索。
如今,我想要再次拾起 MySQL 时,却发现其中的一些语法和规则已经差不多遗忘。对 MySQL 进阶的需求,再加上以前学过的每本书籍(或每个教程)的知识侧重点偶尔不同,因此我打算用这篇博客来记录我所学习到的 MySQL 重要知识点(包括基础知识与进阶内容)。
这一部分内容将介绍数据库与 SQL 的基本理论。
数据库(Database,DB)是指将大量数据保存起来,通过计算机加工而成的可以进行高效访问的数据集合。
而用来管理数据库的计算机系统称为数据库管理系统(Database Management System,DBMS)。
数据库管理系统的种类有:
很明显,MySQL 属于关系数据库中的内容。
MySQL语句的种类主要分为以下 3 类:
在这里给出 MySQL 语句的书写顺序与执行顺序。关于语句中的各子句含义与用法,我们会在后续逐个解释。
①SELECT →②FROM →③WHERE →④GROUP BY →⑤HAVING →⑥ORDER BY →⑦LIMIT
①FROM →②WHERE →③GROUP BY →④HAVING →⑤SELECT →⑥ORDER BY →⑦LIMIT
这一部分内容将介绍如何创建和删除数据库、表,同时对创建表的语法中的约束与数据类型进行详细解释,还会介绍如何对表定义进行更新。
①创建&使用数据库
-- 创建数据库
CREATE DATABASE <数据库名称>;
-- 可以在创建时指定默认编码
CREATE DATABASE <数据库名称> DEFAULT CHARACTER SET utf8;
-- 选择要操作的数据库
USE <数据库名称>;
②创建表
CREATE TABLE <表名>
(<列名1> <数据类型> <该列所需的约束>,
<列名2> <数据类型> <该列所需的约束>,
<列名3> <数据类型> <该列所需的约束>,
...
<该表的约束1>, <该表的约束2>, ...
)ENGINE=<数据库引擎> COMMENT='<注释信息>';
1> 命名规范
只能使用半角英文字母、数字、下划线(_)作为数据库、表和列的名称, 名称必须以半角英文字母开头且不能重复。
2> 数据类型
所有的列必须指明数据类型,数据类型主要分为:数字型、字符型和日期型。细分来说,有以下数据类型:
名称 | 类型 | 说明 |
---|---|---|
TINYINT | 短整型 | 范围为0~255 |
INT | 整型 | 4字节整数类型,范围约+/-21亿 |
BIGINT | 长整型 | 8字节整数类型,范围约+/-922亿亿 |
INTEGER | 整数 | 不能用来存放小数 |
REAL(FLOAT(24)) | 浮点型 | 4字节浮点数,范围约+/-1038 |
DOUBLE | 浮点型 | 8字节浮点数,范围约+/-10308 |
DECIMAL(M,N)(NUMERIC(M,N)) | 高精度小数 | 由用户指定精度的小数,例如,DECIMAL(20,10)表示一共20位,其中小数10位,通常用于财务计算 |
CHAR(N) | 定长字符串 | 存储指定长度的字符串,例如,CHAR(100)总是存储100个字符的字符串,不足字符数要求的话使用半角空格补足,若未指定则默认为char(1) |
VARCHAR(N) | 变长字符串 | 存储可变长度的字符串,例如,VARCHAR(100)可以存储0~100个字符的字符串 |
BOOLEAN | 布尔类型 | 存储True或者False |
DATE | 日期类型 | 存储日期,例如,2018-06-22 |
TIME | 时间类型 | 存储时间,例如,12:20:59 |
DATETIME | 日期和时间类型 | 存储日期+时间,例如,2018-06-22 12:20:59 |
既然变长数据类型这样灵活,为什么还要使用定长数据类型?回答是因为性能,MySQL处理定长列远比处理变长列快得多。
3> 约束
在定义每一列的时候,我们可以在最后为该列加上相关的约束,例如:
约束 | 含义 |
---|---|
AUTO_INCREMENT | 编号从1开始,并1为基数递增,每个表只允许一个 AUTO_INCREMENT 列,而且它必须被索引(如使它成为主键) |
DEFAULT <数值> | 为某列设置默认值,MySQL不允许使用函数作为默认值,它只支持常量 |
PRIMARY KEY | 将某一列或某几列设置为主键 |
NOT NULL | 该列不能插入空数据 |
COMMENT ‘<注释信息>’ | 这不算是约束,用在列后对列进行注释说明 |
注意:
PRIMARY KEY(<列名1>, <列名2>, ...)
将设置用作主键的列,FULLTEXT(<列名1>, <列名2>, ...)
对相关列建立全文本搜索的索引。''
(两个单引号,其间没有字符),这在 NOT NULL 列中是允许的。空串是一个有效值,它不是无值。 NULL 值用关键字 NULL 而不是空串指定。SELECT last_insert_id();
,此语句返回最后一个 AUTO_INCREMENT 值。CREATE TABLE
和表名之间添加IF NOT EXISTS
。该语句在建表时会首先查看表名是否存在,并且仅在表名不存在时建表。①删除数据库
DROP DATABASE <数据库名称>;
②删除表
DROP TABLE <表名称>;
还有一种删除表的方法,先判断表是否存在,如果存在便删除:
DROP TABLE IF EXISTS <表名>;
③删除视图
DROP VIEW <视图名称>;
注意,一旦删除便无法恢复。
当你定义了一个表之后,你可能会发现刚定义的表存在以下问题:少定义了一列、多定义了一列、某一列的列名写错了等,那这个时候怎么办?
我们可能会想到删除原来的表再重新定义,但其实不必这么大费周章,只需要运用 ALTER TABLE 语句即可解决以上问题。我们并没有删除原来表的定义,只是对它进行了更新,因此我们称之为表的定义更新。
添加列
ALTER TABLE <表名> ADD [COLUMN] <列的定义> ;
这里列的定义就是像创建表时列的定义一样。
删除列
ALTER TABLE <表名> DROP COLUMN <列名> ;
重命名列名
ALTER TABLE <表名> CHANGE COLUMN <旧列名> <新列的定义>;
定义外键
ALTER TABLE <表名1> ADD CONSTRAINT <约束名称> FOREIGN KEY (<表1列名>) REFERENCES <表名2> <表2列名>;
如果对单个表进行多个更改,可以使用单条 ALTER TABLE 语句,每个更改用逗号分隔。
重命名表名
RENAME TABLE <旧表名> TO <新表名>;
可以同时对多个表进行重命名:
RENAME TABLE <旧表名1> TO <新表名1>, <旧表名2> TO <新表名2>, <旧表名3> TO <新表名3>, ...;
注意:表定义变更后无法恢复。
① 查看MySQL版本号
SELECT VERSION();
② 显示可用数据库&表&列信息
-- 显示可用数据库
SHOW DATABASES;
-- 显示可用表
SHOW TABLES;
-- 显示某个表中的列信息
-- 对每个字段返回一行,行中包含字段名、数据类型、是否允许NULL、键信息、默认值以及其他信息
SHOW COLUMNS FROM <表名>; or DESCRIBE <表名>;
③ 其他显示信息语句
-- 用于显示广泛的服务器状态信息
SHOW STATUS;
-- 用来显示创建特定数据库的MySQL语句
SHOW CREATE DATABASE <数据库名>;
-- 显示创建特定表的MySQL语句
SHOW CREATE TABLE <表名>;
-- 显示授予用户(所有用户或特定用户)的安全权限
SHOW GRANTS;
-- 显示服务器错误消息
SHOW ERRORS;
-- 显示服务器警告消息
SHOW WARNINGS;
这一部分主要介绍 SELECT 语句中最基本的语法,以及三大运算符——算术运算符、比较运算符和逻辑运算符。
SQL 语句中最基础的语句就是 SELECT 语句,通过 SELECT 语句查询并选取出必要数据的过程称为匹配查询或查询(query)。
SELECT 语句的基本语法如下:
-- 这是一行注释,注意半角空格
/*多行注释
这是多行注释*/
SELECT <列名1>, <列名2>, ...
FROM <表名>
WHERE <条件表达式>
LIMIT m, n;
子句(clause)说明:
<表名>.<列名>
,这对于FROM子句中存在不止一个表时非常有用。<数据库名>.<表名>
,这有利于更容易观察到某个表属于哪个数据库,而且还可以跨库对表进行联结。LIMIT m, n
可以写成LIMIT n OFFSET m
。--
之后,只能写在同一行。注意,MySQL中需要在--
之后加入半角空格(如果不加的话就不会被认为是注释)。/*
和*/
之间,可以跨多行。运算符就是使用其两边的值进行四则运算或者字符串拼接、数值大小比较等运算,并返回结果的符号。
算术运算符
含义 | 运算符 |
---|---|
加法运算 | + |
减法运算 | - |
乘法运算 | * |
除法运算 | / |
比较运算符
运算符 | 含义 |
---|---|
= | 等于 |
<> or != | 不等于 |
>= | 大于等于 |
> | 大于 |
<= | 小于等于 |
< | 小于 |
IS NULL 和 IS NOT NULL
运算符。逻辑运算符
运算符 | 含义 |
---|---|
NOT | 否定某一条件,MySQL支持使用 NOT 对 IN 、 BETWEEN 和EXISTS子句取反 |
AND | 在其两侧的查询条件都成立时整个查询条件才成立 |
OR | 在其两侧的查询条件有一个成立时整个查询条件都成立 |
关于NULL
这一部分主要介绍使用 SQL 语句进行汇总操作以及排序操作的方法,主要包含:聚合函数、GROUP BY 子句、HAVING子句和 ORDER BY 子句。
聚合函数(也称为“聚集函数”)主要用来对表中的数据进行某种汇总操作或计算。所谓聚合,就是将多行汇总为一行。SQL 中常见的聚合函数如下表所示:
函数 | 含义 |
---|---|
COUNT | 计算表中的记录数(行数) |
SUM | 计算表中数值列中数据的合计值 |
AVG | 计算表中数值列中数据的平均值 |
MAX | 计算表中任意列中数据的最大值 |
MIN | 计算表中任意列中数据的最小值 |
COUNT (*)
会得到包含 NULL 的数据行数,而COUNT (< 列名 >)
会得到 NULL 之外的数据行数。COUNT(DISTINCT <列名>)
用来计算某一列的种类,依旧会把 NULL 排除在外;使用 GROUP BY 子句可以先把表分成几组,然后再进行汇总处理。基本语法如下:
SELECT <列名1>, <列名2>, ...
FROM <表名>
WHERE <条件表达式>
GROUP BY <聚合键1>, <聚合键2>, ...;
使用 GROUP BY 子句,可以得到将表分组后的结果,而 HAVING 子句可以通过指定条件来选取特定组。基本语法如下:
SELECT <列名1>, <列名2>, ...
FROM <表名>
WHERE <条件表达式>
GROUP BY <聚合键1>, <聚合键2>, ...
HAVING <分组结果对应的条件>;
通常在 SELECT 语句末尾(这是因为对数据行进行排序的操作必须在结果即将返回时执行)添加 ORDER BY 子句来明确指定记录的排列顺序。基本语法如下:
SELECT <列名1>, <列名2>, ...
FROM <表名>
WHERE <条件表达式>
GROUP BY <聚合键1>, <聚合键2>, ...
HAVING <分组结果对应的条件>
ORDER BY <排序键>;
这一部分将会介绍更新表中数据的方法。数据的更新处理大体可以分为插入( INSERT )、删除( DELETE )和更新( UPDATE )三类。此外,还会介绍数据库中用来管理数据更新的重要概念——事务。
SQL 中的 INSERT 语句用来向表中插入数据。基本语法如下:
INSERT INTO <表名> (列1, 列2, 列3, ...) VALUES (值1, 值2, 值3, ...);
-- 如果主键已经存在,就执行更新操作;否则执行插入操作
INSERT INTO <表名> (列1, 列2, 列3, ...) VALUES (值1, 值2, 值3, ...) ON DUPLICATE KEY UPDATE 列1 = 值1, 列2 = 值2, 列3 = 值3, ...;
INSERT LOW_PRIORITY INTO ...
,这也适用于 UPDATE 和 DELETE 语句。从其他表中复制数据插入,基本语法如下:
INSERT INTO <表名> (列1, 列2, 列3, ...)
SELECT <列名> FROM <表名>;
DELETE 语句用于删除表中的数据。基本语法如下:
DELETE FROM <表名>;
DELETE 语句和 DROP 语句的区别:
想要删除部分数据行时,可以像 SELECT 语句那样使用 WHERE 子句指定删除条件。这种指定了删除对象的 DELETE 语句称为搜索型 DELETE 。它的基本语法如下:
DELETE FROM <表名>
WHERE <条件>;
DELETE 语句中不能使用 GROUP BY 、HAVING 和 ORDER BY 三类子句,而只能使用 WHERE 子句,因为在删除表中数据时它们都起不到什么作用。
MySQL中支持一种名为 TRUNCATE 语句的数据删除语句,它的功能与简单型 DELETE 语句相同,那就是只能删除整张表的数据。正是因为它不能具体地控制删除对象,所以其处理速度比 DELETE 要快得多。基本语法如下:
TRUNCATE <表名>;
TRUNCATE 实际是删除原来的表并重新创建一个表,而不是逐行删除表中的数据。
直接使用 UPDATE 语句就可以更新表中的数据。基本语法如下:
UPDATE <表名>
SET <列名> = <表达式>;
该语句会将表中的该列均更新为表达式所对应的值。
更新数据时也可以像 DELETE 语句那样使用 WHERE 子句,这种指定更新对象的 UPDATE 语句称为搜索型 UPDATE 语句。基本语法如下:
UPDATE <表名>
SET <列名> = <表达式>
WHERE <条件>;
就像 MySQL 支持多列插入一样,它也支持多列更新,基本语法如下:
UPDATE <表名>
SET <列名1> = <表达式1>,
<列名2> = <表达式2>,
...
WHERE <条件>;
UPDATE IGNORE <表名> ...
事务是需要在同一个处理单元中执行的一系列更新处理的集合。使用事务开始语句和事务结束语句,将一系列 DML 语句( INSERT / UPDATE / DELETE 语句)括起来,就实现了一个事务处理。
事务并没有标准的开始指令存在,而是根据 DBMS 的不同而不同。实际上,几乎所有的数据库产品的事务都无需开始指令。这是因为大部分情况下,事务在数据库连接建立时就已经悄悄开始了,并不需要用户再明确发出开始指令。
事务处理用来管理 INSERT 、 UPDATE 和 DELETE 语句,不能回退 SELECT 语句,也不能回退 CREATE 或 DROP 操作。事务处理块中可以使用这两条语句,但如果你执行回退,它们不会被撤销。
像这样不使用指令而悄悄开始事务的情况下,应该如何区分各个事务呢?通常会有如下两种情况:
事务基本的语法如下:
START TRANSACTION; -- 事务开始语句
DML语句1(INSERT DELETE UPDATE);
DML语句2;
...
COMMIT 或者 ROLLBACK; -- 事务结束语句
事务中存在保留点,它是指事务处理中设置的临时占位符(placeholder),可以对它发布部分回退(与回退整个事务处理不同)。
为了支持回退部分事务处理,必须能在事务处理块中合适的位置放置占位符。这样,如果需要回退,可以回退到某个占位符,这些占位符称为保留点。为了创建保留点,可使用 SAVEPOINT 语句:
SAVEPOINT <保留点名称>;
每个保留点都取标识它的唯一名字,以便在回退时,MySQL知道要回退到何处。为了回退到指定保留点,可使用如下语句:
ROLLBACK TO <保留点名称>;
保留点在事务处理完成(执行一条 ROLLBACK 或 COMMIT)后自动释放,也可以用RELEASE SAVEPOINT <保留点名称>;
明确地释放指定保留点。
默认的 MySQL 行为是自动提交所有更改。换句话说,任何时候执行一条MySQL语句,该语句实际上都是针对表执行的,而且所做的更改立即生效,为指示 MySQL 不自动提交更改,需要使用以下语句:SET autocommit=0;
。
autocommit 标志决定是否自动提交更改,不管有没有 COMMIT 语句。设置 autocommit 为 0 (假)指示MySQL不自动提交更改(直到 autocommit 被设置为真为止)。autocommit 标志是针对每个连接而不是服务器的。
事务的四种特性——ACID特性:
这一部分主要介绍嵌套在 SELECT 语句中的视图和子查询等技术。
从SQL的角度来看,视图和表是相同的,两者的区别在于表中保存的是实际数据,而视图中保存的是从表中取出数据所使用的 SELECT 语句(视图本身并不存储数据)。
数据库中的数据实际上会被保存到计算机的存储设备(通常是硬盘)中,但是使用视图时并不会将数据保存到存储设备之中,而且也不会将数据保存到其他任何地方。实际上视图保存的是 SELECT 语句,我们从视图中读取数据时,视图会在内部执行该 SELECT 语句并创建出一张临时表。视图有以下几大优点:
视图的语法如下:
CREATE VIEW <视图名称> (<视图列名1>, <视图列名2>, ...)
AS
<SELECT 语句 >
SHOW CREATE VIEW <视图名称>
来查看创建视图的语句CREATE OR REPLACE VIEW
。如果要更新的视图不存在,则第 2 条更新语句会创建一个视图;如果要更新的视图存在,则第 2 条更新语句会替换原有视图。视图存在以下限制:
定义视图时不能使用 ORDER BY 子句:因为视图和表一样,数据行都是没有顺序的。但是使用视图时可以使用 ORDER BY 子句,但如果从该视图检索数据 SELECT 中也含有 ORDER BY ,那么该视图中的 ORDER BY 将被覆盖。(这段话好像前后矛盾)
如果定义视图的 SELECT 语句能够满足某些条件,那么这个视图就可以被更新。举例说明一些具有代表性的条件:
存在该限制的原因是:视图和表需要同时进行更新,因此通过汇总等操作得到的视图无法进行更新,因为无法将视图的更改反映到原表。
假设某视图是根据where 列1 = A
的条件创建出来的,经测试MySQL中依旧可以向视图中插入列1 <> A
的数据,但是该数据仅仅是通过该视图插入到了对应的数据表中,该视图由于限制条件依旧不包含该数据。
删除视图的语法如下:
DROP VIEW <视图名称>;
概括来说,子查询就是一张一次性视图(SELECT 语句)。与视图不同,子查询在 SELECT 语句执行完毕之后就会消失。
子查询就是将用来定义视图的 SELECT 语句直接用于 FROM 子句当中,作为内查询,它会首先执行。基本语法如下:
SELECT <列名1>, <列名2>, ...
FROM (
SELECT <列名1>, <列名2>, ...
FROM <表名>
WHERE <条件>
GROUP BY <聚合键>
) AS <子查询别名>;
标量子查询就是返回单一值的子查询,必须而且只能返回 1 行 1 列的结果,绝对不能返回多行结果:
关联子查询在子查询中添加的 WHERE 子句。基本语法如下:
SELECT <列名1>, <列名2>, ...
FROM <表名1> AS <表别名1>
WHERE <列名3> > (SELECT <对列名3进行聚合>
FROM <表名2> AS <表别名2>
WHERE <表别名1>.<列名1> = <表别名2>.<列名4>);
单看关联子查询的基本语法有些不容易理解,举例来说明:
SELECT product _type , product_name, sale_price
FROM Product AS P1
WHERE sale_price > (SELECT AVG(sale_price)
FROM Product AS P2
WHERE P1.product_type = P2.product_type);
对以上代码我的理解如下:
注意:
这一部分将介绍具有代表性的函数以及特殊版本的函数(谓词和CASE 表达式)的使用方法。
SQL中的函数主要分为以下几种:
种类 | 功能 |
---|---|
算术函数 | 用来进行数值计算的函数 |
字符串函数 | 用来进行字符串操作的函数 |
日期函数 | 用来进行日期操作的函数 |
转换函数 | 用来转换数据类型和值的函数 |
聚合函数 | 用来进行数据聚合的函数 |
算术函数
函数 | 含义 |
---|---|
+ - * / | 加减乘除四则运算 |
ABS(数值) | 对数值求绝对值,NULL 的绝对值仍然是 NULL |
COS(数值)、SIN(数值)、TAN(数值) | 计算参数的余弦值、正弦值、正切值,其中的参数值是以弧度给出的余弦值 |
EXP(数值) | 返回自然对数e的数值次方 |
SQRT(数值) | 返回参数的平方根 |
PI() | 返回圆周率 |
RAND() | 返回0到1内的随机小数 |
MOD(被除数,除数) | 求余,任一参数为 NULL 则结果为 NULL |
ROUND(对象数值,保留的小数位数值) | 对数值进行四舍五入,任一参数为 NULL 则结果为 NULL。 第二个参数可以省略,此时为对对象数值四舍五入取整 |
字符串函数
函数 | 含义 |
---|---|
CONCAT(列1, 列2, 列3, …) | 字符串拼接函数,任一参数为NULL则结果为NULL |
LENGTH(字符串) CHAR_LENGTH(字符串) | 计算字符串的字节长度 计算字符串的字符长度 |
LOWER(字符串) UPPER(字符串) | 对英文字母小写 对英文字母大写 |
REPLACE(对象字符串,替换前的字符串,替换后的字符串) | 字符串的替换,任一参数为NULL则结果为NULL |
SUBSTRING(对象字符串 FROM 截取的起始位置 FOR 截取的字节数) | 字符串的截取,从字符串最左侧开始计算,1为起始值 |
RTrim(列1) LTrim(列1) Trim(列1) | 去掉值右边的所有空格 去掉值左边的所有空格 去掉值两边的所有空格 |
Left(<列1>, len) Right(<列1>, len) | 从字符串左侧提取len长度的子字符串 从字符串右侧提取len长度的子字符串 |
locate(str1,str2) | 判断str2中是否包含str1,如果包含则返回str1在str2中出现的起始位置(从1开始),如果不包含则返回0 |
SOUNDEX(<列名>) | 从str返回一个同音字符串 |
GREATEST(列1, 列2, 列3, …) LEAST(列1, 列2, 列3, …) | 返回所给列在行方向的最大值 / 最小值,任一参数为NULL则结果为NULL |
WHERE SOUNDEX(<列名>) = SOUNDEX(<字符串>)
。日期函数
函数 | 含义 |
---|---|
DATE_ADD(日期, INTERVAL < expr > < type >) | 日期运算函数,其中 INTERVAL 为关键字,expr 为数值,type 为时间单位,具体见后表 |
DateDiff(日期1, 日期2) | 计算日期1-日期2天数之差 |
Date(日期)、Time(日期)、Month(日期)、Day(日期)、Year(日期)、Hour(日期)、Minute(日期)、Second(日期) | 返回日期时间的日期、时间、年、月、日、时、分、秒部分 |
DayOfWeek(日期) | 返回日期对应的星期几,注意周日对应数字1 |
DATE_FORMAT(日期, format) | 以不同的格式显示日期/时间数据,具体格式见后表 |
ADDDATE(日期, < expr >) ADDDATE(日期, INTERVAL < expr > < type >) | 日期运算函数,有两种写法,不加时间单位时默认为天,添加 INTERVAL 关键字和单位时,用法与 DATE_ADD() 函数相同 |
AddTime(日期, < expr >) | 日期运算函数,expr 可以写作1:1:1:1.12581 ,当 expr 为整数时默认为秒 |
CURRENT_DATE | CURRENT_DATE() | CURDATE() | 返回当前日期,也就是该函数执行时的日期 |
CURRENT_TIME | CURRENT_TIME() | CURTIME() | 返回当前时间,也就是该函数执行时的时间 |
CURRENT_TIMESTAMP 或 NOW() | 返回当前日期和时间,也就是该函数执行时的日期和时间 SELECT CURRENT_TIMESTAMP; |
EXTRACT( 日期元素 FROM 日期 ) | 截取出日期数据中的一部分 注意,返回值是数值类型而不是日期类型 日期元素有:YEAR、MONTH、DAY、HOUR、MINUTE、SECOND |
type 取值 | 说明 |
---|---|
YEAR、QUARTER、MONTH、WEEK、DAY、HOUR、MINUTE、SECOND、MICROSECOND | 年、季、月、周、日、时、分、秒、毫秒 |
YEAR_MONTH、DAY_HOUR、DAY_MINUTE、DAY_SECOND、DAY_MICROSECOND、HOUR_MINUTE、HOUR_SECOND、HOUR_MICROSECOND、MINUTE_SECOND、MINUTE_MICROSECOND、SECOND_MICROSECOND | 复合型,此时 < expr > 参数需要使用引号,中间使用任何非数字字符(且可以同时使用多种)作为间隔即可,并且不能使用负数。 时间间隔只指定了一个值也能正常工作,但是对应XXX_ YYY使用的单位为YYY,相当于单一单位的type,此时可以使用负数。 注意,XXX与YYY之间的单位量级必须逐层表示,不能跨级,例如 DAY_SECOND:1:1:1:1 |
expr 取值 | 说明 |
---|---|
[60, 99] | 计算结果为 NULL |
100 | 增加60秒 |
110 | 增加1分10秒 |
[160, 199] | 计算结果为 NULL |
210 | 增加2分10秒 |
格式 | 描述 |
---|---|
%a | 缩写星期名 |
%b | 缩写月名 |
%c | 月,数值 |
%D | 带有英文后缀的月中的天 |
%d | 月的天,数值(00-31) |
%e | 月的天,数值(0-31) |
%f | 微秒 |
%H | 小时 (00-23) |
%h | 小时 (01-12) |
%I | 小时 (01-12) |
%i | 分钟,数值(00-59) |
%j | 年的天 (001-366) |
%k | 小时 (0-23) |
%l | 小时 (1-12) |
%M | 月名 |
%m | 月,数值(00-12) |
%p | AM 或 PM |
%r | 时间,12-小时(hh:mm:ss AM 或 PM) |
%S | 秒(00-59) |
%s | 秒(00-59) |
%T | 时间, 24-小时 (hh:mm:ss) |
%U | 周 (00-53) 星期日是一周的第一天 |
%u | 周 (00-53) 星期一是一周的第一天 |
%V | 周 (01-53) 星期日是一周的第一天,与 %X 使用 |
%v | 周 (01-53) 星期一是一周的第一天,与 %x 使用 |
%W | 星期名 |
%w | 周的天 (0=星期日, 6=星期六) |
%X | 年,其中的星期日是周的第一天,4 位,与 %V 使用 |
%x | 年,其中的星期一是周的第一天,4 位,与 %v 使用 |
%Y | 年,4 位 |
%y | 年,2 位 |
转换函数
“转换”在 SQL 中主要有两层意思:一是数据类型的转换,简称为类型转换,在英语中称为 cast;另一层意思是值的转换。
函数 | 含义 |
---|---|
CAST(转换前的值 AS 想要转换的数据类型) | 数据类型的转换,例如SELECT CAST(‘0001’ AS SIGNED INTEGER) AS int_col; |
COALESCE( 数据 1, 数据 2, 数据 3, …) | 该函数将 NULL 值转换为其他值,返回可变参数中左侧开始第1个不是 NULL 的值 |
谓词就是返回值为真值(TRUE / FALSE / UNKNOWN)的函数。
LIKE谓词
当需要进行字符串的部分一致查询时需要使用该谓词。部分一致大体可以分为前方一致、中间一致和后方一致三种类型。
部分一致类型 | 含义 |
---|---|
前方一致 | 选取出作为查询条件的字符串与查询对象字符串起始部分相同的记录 |
中间一致 | 选取出查询对象字符串中含有作为查询条件的字符串的记录 |
后方一致 | 选取出作为查询条件的字符串与查询对象字符串的末尾部分相同的记录 |
仅中间一致 | 选取出查询对象字符串中以作为查询条件的字符串作为首尾的记录 |
基本语法如下:
<列名> LIKE '<查询条件字符串>%'; -- 前方一致
<列名> LIKE '%<查询条件字符串>%'; -- 中间一致
<列名> LIKE '%<查询条件字符串>'; -- 后方一致
<列名> LIKE '<查询条件字符串>%<查询条件字符串'; -- 仅中间一致
对于 LIKE 谓词有两个特殊符号使用的比较频繁:
符号 | 含义 |
---|---|
% | 0字符以上的任意字符串,包括0 |
_ | 任意一个字符 |
注意,即使在创建表时将某一列的数据类型设定为定长字符串char(n)时,自动填充的半角空格依旧无法利用下划线符号_
识别出来,表显示什么就是什么。(可以类比等号模式进行思考)
BETWEEN谓词
使用 BETWEEN 可以进行范围查询。基本语法如下:
-- 该范围包含上下限的值
<列名> BETWEEN <下限值> AND <上限值>;
IS NULL、IS NOT NULL 谓词
为了选取出某些值为 NULL 的列的数据,不能使用 = ,而只能使用特定的谓词 IS NULL、IS NOT NULL。基本语法如下:
<列名> IS NULL;
<列名> IS NOT NULL;
IN、NOT IN 谓词
IN 谓词是 OR 的简便用法,例如以下 SQL 语句:
<列名> = <值1> OR <列名> = <值2> OR <列名> = <值3>;
可以写成:
<列名> IN (值1, 值2, 值3);
EXISTS谓词
该谓词的作用就是“判断是否存在满足某种条件的记录”。如果存在这样的记录返回 TRUE,否则返回 FALSE:
正则表达式的作用是匹配文本,将一个正则表达式与一个文本串进行比较。MySQL 用 WHERE 子句对正则表达式提供了初步的支持,允许指定正则表达式,过滤 SELECT 检索出的数据。基本语法如下:
SELECT *
FROM <表名>
WHERE <列1> REGEXP <正则表达式>;
MySQL 中正则表达式常见的字符如下:
字符 | 说明 |
---|---|
. | 匹配任意一个字符 |
| | OR操作,匹配其中之一 |
[] | 匹配任何单一字符,如[0-9]、[a-z]、[A-Z]、[0-5]、[a-dx] |
[^] | 匹配除指定字符外的任何东西 |
\\ | 匹配转义字符,如 . 、 | 、 []、\ |
^ | 文本开始 |
$ | 文本结尾 |
[[:<m:n>:]] | 以m开头n结尾的单词,m、n可以省略 |
注意:
正则表达式无需与列值完全匹配,列值中只要包含正则表达式中的内容即认为匹配。
MySQL中的正则表达式匹配不区分大小写,为区分大小写,可使用 BINARY 关键字,如 WHERE <列1> REGEXP BINARY <正则表达式>
。
MySQL要求使用两个反斜杠转义特殊字符,\\ 也用来引用元字符(即具有特殊含义的字符),空白元字符如下:
空白元字符 | 说明 |
---|---|
\\f | 换页 |
\\n | 换行 |
\\r | 回车 |
\\t | 制表 |
\\v | 纵向制表 |
重复元字符如下:
空白元字符 | 说明 |
---|---|
* | 0个或多个匹配 |
+ | 1个或多个匹配(等于{1,}) |
? | 0个或1个匹配(等于{0,1}) |
{n} | 指定数目的匹配 |
{n,} | 不少于指定数目的匹配 |
{n,m} | 匹配数目的范围(m不超过255) |
MySQL可以使用预定义的字符集(字符类),字符类如下:
字符类 | 说明 |
---|---|
[:alnum:] | 任意字母和数字(同a-zA-Z0-9) |
[:alpha:] | 任意字符(同a-zA-Z) |
[:blank:] | 空格和制表(同 \\t) |
[:cntrl:] | ASCII控制字符(ASCII 0到31和127) |
[:digit:] | 任意数字(同0-9) |
[:graph:] | 与[:print:]相同,但不包括空格 |
[:lower:] | 任意小写字母(同a-z) |
[:print:] | 任意可打印字符 |
[:punct:] | 既不在[:alnum:]又不在[:cntrl:]中的任意字符 |
[:space:] | 包括空格在内的任意空白字符(同\\f\\n\\r\\t\\v) |
[:upper:] | 任意大写字母(同A-Z) |
[:xdigit:] | 任意十六进制数字(同a-fA-F0-9) |
可以在不使用数据库表的情况下用 SELECT 来测试正则表达式。 REGEXP 检查总是返回 0(没有匹配)或 1(匹配)。可以用带文字串的 REGEXP 来测试表达式,并试验它们,语法为:SELECT <文本字符串> REGEXP <正则表达式>;
。
CASE 表达式是在区分情况时使用的,这种情况的区分在编程中通常称为条件分支。CASE 表达式分为简单 CASE 表达式和搜索 CASE 表达式两种。
简单 CASE 表达式的基本语法如下:
CASE < 表达式 >
WHEN < 表达式 > THEN < 表达式 >
WHEN < 表达式 > THEN < 表达式 >
WHEN < 表达式 > THEN < 表达式 >
...
ELSE < 表达式 >
END
搜索 CASE 表达式的基本语法如下:
CASE
WHEN < 求值表达式 > THEN < 表达式 >
WHEN < 求值表达式 > THEN < 表达式 >
WHEN < 求值表达式 > THEN < 表达式 >
...
ELSE < 表达式 >
END
这一部分将会介绍使用 2 张以上的表的 SQL 语句,包括以行方向(竖)为单位的集合运算符和以列方向(横)为单位的联结。
集合运算,就是对满足同一规则的记录进行的加减等四则运算,它以行方向(纵向) 为单位进行操作。用来进行集合运算的运算符称为集合运算符。基本语法如下:
SELECT <列1>, <列2>, <列3>
FROM <表1>
<集合运算符>
SELECT <列A>, <列B>, <列C>
FROM <表2>
常见的集合运算符如下表所示:
集合运算符 | 含义 |
---|---|
UNION | 并集 |
UNION ALL | 包含重复行的并集 |
INTERSECT | 选取表中公共部分的交集 |
EXCEPT | 差集 |
联结(JOIN) 就是将其他表中的列添加过来,进行“添加列”的集合运算,它以**列方向(横向)**为单位进行操作。联结大体上可分为内联结、外联结和交叉联结。
内联结——INNER JOIN
内联结是只包含表内信息的联结,只能选取出同时存在于两张表中的数据。基本语法如下:
FROM <表名A> AS <别名A> INNER JOIN <表名B> AS <别名B>
ON <别名A>.<列1> = <别名B>.<列2>
②外联结——OUTTER JOIN
外联结中包含原表中不存在(在原表之外)的信息,只要数据存在于某一张表中就能用方法读取出来。MySQL中外联结有左联结(LEFT OUTER JOIN,下方第一个图)和右联结(RIGHT OUTER JOIN,下方第二个图)。
基本语法如下:
-- 左联结
FROM <表名A> AS <别名A> LEFT OUTER JOIN <表名B> AS <别名B>
ON <别名A>.<列1> = <别名B>.<列2>
-- 右联结
FROM <表名A> AS <别名A> RIGHT OUTER JOIN <表名B> AS <别名B>
ON <别名A>.<列1> = <别名B>.<列2>
注意:
进阶:
1> A独有或B独有
我们可以利用外联结来实现只存在与表 A 或者表 B 的行,这在某种意义上也实现了表的差集运算。
基本语法如下:
-- A表独有
FROM <表名A> AS <别名A> LEFT OUTER JOIN <表名B> AS <别名B>
ON <别名A>.<列1> = <别名B>.<列2>
WHERE <别名B>.<列2> IS NULL;
-- B表独有
FROM <表名A> AS <别名A> RIGHT OUTER JOIN <表名B> AS <别名B>
ON <别名A>.<列1> = <别名B>.<列2>
WHERE <别名A>.<列1> IS NULL;
2> AB全有
MySQL 不支持外联结(FULL OUTER JOIN),但可以通过左联结 + UNION + 右联结实现。
基本语法如下:
-- AB全有
FROM <表名A> AS <别名A> LEFT OUTER JOIN <表名B> AS <别名B>
ON <别名A>.<列1> = <别名B>.<列2>
UNION
FROM <表名A> AS <别名A> RIGHT OUTER JOIN <表名B> AS <别名B>
ON <别名A>.<列1> = <别名B>.<列2>
3> A独有+B独有
A、B独有并集,相当于A、B全有去掉AB的共有(交集)。
基本语法如下:
-- A表独有+B表独有
FROM <表名A> AS <别名A> LEFT OUTER JOIN <表名B> AS <别名B>
ON <别名A>.<列1> = <别名B>.<列2>
WHERE <别名B>.<列2> IS NULL;
UNION
FROM <表名A> AS <别名A> RIGHT OUTER JOIN <表名B> AS <别名B>
ON <别名A>.<列1> = <别名B>.<列2>
WHERE <别名A>.<列1> IS NULL;
交叉联结——CROSS JOIN
FROM <表1>, <表2>
即为交叉联结。这一部分介绍的是 SQL 中的高级用法,包括窗口函数的用法、GROUPING 运算符等。
窗口函数也称为 OLAP 函数,OLAP 是 OnLine Analytical Processing 的简称,意思是对数据库数据进行实时分析处理。基本语法如下:
<窗口函数> OVER ([PARTITION BY <列清单>] ORDER BY <排序列清单>)
-- []表示可选的意思
窗口函数大体可以分为以下两种:
种类 | 举例 |
---|---|
聚合函数 | SUM、AVG、COUNT、MAX、MIN |
专用窗口函数 | RANK 、 DENSE _ RANK 、 ROW _ NUMBER |
关于 PARTITION BY 子句:
关于 ORDER BY 子句:
常用的专用窗口函数含义如下:
函数 | 含义 |
---|---|
RANK() | 计算排序时,如果存在相同位次的记录,则会跳过之后的位次。 例如,有 3 条记录排在第 1 位时:1 位、1 位、1 位、4 位…… |
DENSE_RANK() | 同样是计算排序,即使存在相同位次的记录,也不会跳过之后的位次。 例如,有 3 条记录排在第 1 位时:1 位、1 位、1 位、2 位…… |
ROW_NUMBER() | 赋予唯一的连续位次。 例如,有 3 条记录排在第 1 位时:1 位、2 位、3 位、4 位…… |
根据函数名称,我们可以轻易记住每个函数的功能。例如,RANK(排序)就是正常的排序,DENSE_RANK(密集排序)说明排名紧凑不会跳位,ROW_NUMBER就是单纯的行编号12345。
注意:
聚合函数
计算移动平均
窗口函数就是将表以窗口为单位进行分割,并在其中进行排序的函数。其实其中还包含在窗口中指定更加详细的汇总范围的备选功能,该备选功能中的汇总范围称为框架。其语法需要在 ORDER BY 子句之后使用指定范围的关键字:
ORDER BY <列名> ROWS <N行> PRECEDING;
将框架指定为“截止到之前 N 行”,也就是将作为汇总对象的记录限定为“最靠近的 (N+1) 行”(包括自身行)。
-- 将框架指定为“截止到之后 N 行”
ORDER BY <列名> ROWS <N行> FOLLOWING;
-- 将当前记录的前 M 行后 N 行作为汇总对象
ORDER BY <列名> ROWS <M行> PRECEDING AND <N行> FOLLOWING;
容易混淆的一点
我在做题的时候碰见一种情况: 在使用窗口函数时,利用 ORDER BY 对注册日期进行排序并利用 SUM 函数求销售单价的累加值,SQL 语句如下:
SUM(sale_price) OVER(ORDER BY regist_date)
但是,regist_date 中有个重复值重复了三次,那么 SQL 会以怎样的顺序对该重复值进行排序呢?
我原本以为会以某种规则进行排序,然后逐渐累加。而实际运行发现,当遇到了这个重复值时,它会将三个重复值对应的汇总列加起来再与前面的值进行累加,也就是说三个重复值对应的累加值是一样的。
只使用 GROUP BY 子句和聚合函数是无法同时得出小计和合计的。如果想要同时得到,可以使用 GROUPING 运算符。
GROUPING 运算符包含以下几种:ROLLUP、GROUPING 函数、CUBE、GROUPING SETS。除 ROLLUP 外,MySQL 目前不支持其他运算符。
ROLLUP基本语法如下:
GROUP BY <列1>, <列2>, <列3>, ... WITH ROLLUP
该运算符可以一次计算出不同聚合键组合的结果,组合的个数为 n+1(n 是聚合键的个数)。在上述语法中就是一次计算出了如下组合的汇总结果:
GROUP BY () 表示没有聚合键,也就相当于没有 GROUP BY子句(这时会得到全部数据的合计行的记录),该合计行记录称为超级分组记录,超级分组记录默认使用 NULL 作为聚合键。因此,ROLLUP能够同时得到小计和合计。
GROUPING 函数基本语法如下:
-- 直接对列名使用即可
GROUPING(<列名>);
GROUPING 函数是用来判断超级分组记录的 NULL 的特定函数,该函数在其参数列的值为超级分组记录所产生的 NULL 时返回 1 ,其他情况返回 0。因此,使用 GROUPING 函数能够简单地分辨出原始数据中的 NULL 和超级分组记录中的 NULL 。
CUBE(MySQL目前不支持)基本语法如下:
GROUP BY CUBE(<列1>, <列2>, ...)
将 GROUP BY 子句中聚合键的“所有可能的组合”的汇总结果集中到一个结果中。因此,组合的个数就是 2 n (n 是聚合键的个数)。在上述语法中就是一次计算出了如下组合的汇总结果:
注意:
GROUPING SETS(MySQL目前不支持)基本语法如下:
GROUP BY GROUPING SETS(<列1>, <列2>, ...)
该运算符可以用于从 ROLLUP 或者 CUBE 的结果中取出部分记录,与 ROLLUP 或者 CUBE 能够得到规定的结果相对, GROUPING SETS 用于从中取出个别条件对应的不固定的结果。在上述语法中就是一次计算出了如下组合的汇总结果:
数据库中两个最常使用的引擎为 MyISAM 和 InnoDB ,前者支持全文本搜索,而后者不支持。
启用全文本搜索支持(创建相关索引):
为了进行全文本搜索,必须索引被搜索的列,而且要随着数据的改变不断地重新索引。常见的索引创建方法有两种:
FULLTEXT(<列名1>, <列名2>, ...)
,注意数据库引擎应该为 MyISAM。在定义之后,MySQL自动维护该索引。在增加、更新或删除行时,索引随之自动更新。ALTER TABLE <表名> ADD FULLTEXT [INDEX] <自定义索引名> (<列名1>, <列名2>, ...)
或CREATE FULLTEXT INDEX <自定义索引名> ON <表名> (<列名1>, <列名2>, ...)
。如果正在导入数据到一个新表,此时不应该启用 FULLTEXT 索引,应该首先导入所有数据,然后再修改表定义 FULLTEXT,这样有助于更快地导入数据(而且使索引数据的总时间小于在导入每行时分别进行索引所需的总时间)。进行全文本搜索:
全文本搜索的基本语法如下:
SELECT *
FROM <表名>
WHERE Match(<FULLTEXT索引列>) Against('<搜索表达式>');
Match(<FULLTEXT索引列>) Against('<搜索表达式>')
没有固定的位置要求(必须放在哪一子句中),其得到的是一个 全文本搜索计算出的等级值,等级由MySQL根据行中词的数目、唯一词的数目、整个索引中词的总数以及包含该词的行的数目计算出来。查询扩展:
查询扩展用来设法放宽所返回的全文本搜索结果的范围,增加找到相关匹配的机会。全文本搜索的基本语法如下:
SELECT *
FROM <表名>
WHERE Match(<FULLTEXT索引列>) Against('<搜索表达式>' WITH QUERY EXPANSION);
在使用查询扩展时,MySQL对数据和索引进行两遍扫描来完成搜索:
查询扩展极大地增加了返回的行数,但这样做也增加了实际上并不想要的行的数目。表中的行越多,这些行中的文本就越多,使用查询扩展返回的结果越好。
布尔文本搜索:
布尔文本搜索是 MySQL 所支持的全文本搜索的另外一种形式,即布尔方式(boolean mode)。以布尔方式,可以提供关于如下内容的细节:
基本语法如下:
SELECT *
FROM <表名>
WHERE Match(<FULLTEXT索引列>) Against('<搜索表达式>' IN BOOLEAN MODE);
注意:
布尔操作符 | 说明 |
---|---|
+ | 包含,词必须存在 |
- | 排除,词必须不出现 |
> | 包含,而且增加等级值 |
< | 包含,且减少等级值 |
() | 把词组成子表达式(允许这些子表达式作为一个组被包含、排除、排列等) |
~ | 取消一个词的排序值 |
* | 词尾的通配符 |
“” | 定义一个短语(与单个词的列表不一样,它匹配整个短语以便包含或排除这个短语) |
存储过程
存储过程是指为以后的使用而保存的一条或多条MySQL语句的集合。使用存储过程比单独使用其中的SQL语句要快。
创建存储过程
基本语法如下:
CREATE PROCEDURE <存储过程名称>([参数1], [参数2], ...)
COMMENT '<注释内容>'
BEGIN
<存储过程体>;
END;
@
开始。在使用变量传递相关参数时,调用存储过程不再显示任何数据,可以通过使用```SELECT @<变量名>`;``显示相关返回结果。;
,如果命令行实用程序要解释存储过程自身内的;
字符,则它们最终不会成为存储过程的成分,这会使存储过程中的SQL出现句法错误。解决办法是临时更改命令行实用程序的语句分隔符,如下所示:DELIMITER //
CREATE PROCEDURE <存储过程名称>([参数1], [参数2], ...)
BEGIN
<存储过程体>;
END //
DELIMITER ;
DELIMITER //
告诉命令行实用程序使用//
作为新的语句结束分隔符,可以看到标志存储过程结束的 END 定义为END //
而不是 END ;
。这样,存储过程体内的;
仍然保持不动,并且正确地传递给数据库引擎。最后,使用DELIMITER ;
恢复为原来的语句分隔符。除\
符号外,任何字符都可以用作语句分隔符。SHOW CREATE PROCEDURE <存储过程名称>;
来显示创建一个存储过程的 CREATE 语句,为了获得包括何时、由谁创建、注释内容等详细信息的存储过程列表,可以使用SHOW PROCEDURE STATUS;
列出所有存储过程,也可以使用SHOW PROCEDURE STATUS LIKE '<过滤模式>';
指定一个过滤模式。执行存储过程
MySQL称存储过程的执行为调用,因此 MySQL 执行存储过程的语句为 CALL 。 CALL 接受存储过程的名字以及需要传递给它的任意参数。基本语法如下:
CALL <存储过程名称>([参数1], [参数2], ...)
删除存储过程
基本语法如下:
DROP PROCEDURE <存储过程名称>;
DROP PROCEDURE
将产生一个错误。当过程存在想删除它时(如果过程不存在也不产生错误),可使用DROP PROCEDURE IF EXISTS <存储过程名称>;
存储过程示例
考虑如下场景:计算某一顾客的订单合计金额,并只针对某些顾客增加营业税。sql 语句如下:
CREATE PROCEDURE ordertotal( IN onumber INT, IN taxable BOOLEAN, OUT ototal DECIMAL(8,2) ) COMMENT 'Obtain order total, optionally adding tax' BEGIN -- Declare variable for total DECLARE tota1 DECIMAL(8,2); -- Declare tax percentage DECLARE taxrate INT DEFAULT 6; -- Get the order tota1 SELECT Sum(item_ price*quantity) FROM orderitems WHERE order_num = onumber INTO total; -- Is this taxable? IF taxable THEN -- Yes,so add taxrate to the tota 1 SELECT total+(tota1/100*taxrate) INTO total; END IF; -- And finally, save to out variable SELECT total INTO ototal; END;
游标
游标(cursor)是一个存储在MySQL服务器上的数据库查询,它不是一条 SELECT 语句,而是被该语句检索出来的结果集。在存储了游标之后,应用程序可以根据需要滚动或浏览其中的数据。游标主要用于交互式应用,其中用户需要滚动屏幕上的数据,并对数据进行浏览或做出更改。
使用游标具有以下几个步骤:
CREATE PROCEDURE <存储过程名称>()
BEGIN
DECLARE <游标名称> CURSOR
FOR
<SELECT语句>;
END;
OPEN <游标名称>;
,在处理 OPEN 语句时执行查询,存储检索出的数据以供浏览和滚动。FETCH <游标名称> INTO <变量名称>;
。CLOSE <游标名称>;
注意:
游标使用示例
CREATE PROCEDURE processorders() BEGIN -- Declare 1ocal variables DECLARE done BOOLEAN DEFAULT 0; DECLARE o INT; -- Declare the cursor DECLARE ordernumbers CURSOR FOR SELECT order_num FROM orders; -- Declare conti nue hand ler DECLARE CONTINUE HANDLER FOR SQLSTATE ‘02000' SET done=1; -- Open the cursor OPEN ordernumbers; -- Loop through a11 rows REPEAT -- Get order number FEICH ordernumbers INIO o; -- End of 1oop UNTIL done END REPEAT; -- Close the cursor CLOSE ordernumbers; END;
这个例子中的 FETCH 是在 REPEAT 内,因此它反复执行直到 done 为真(由UNTIL done END REPEAT;
规定)。为使它起作用,定义变量 done 的初始值为 0,那么 done 怎样才能在结束时被设置为真呢?DECLARE CONTINUE HANDLER FOR SQLSTATE ‘02000' SET done=1;
语句中定义了一个CONTINUE HANDLER
,它是在条件出现时被执行的代码。该语句中,它指出当SQLSTATE '02000'
出现时,设置done=1
。SQLSTATE '02000'
是一个未找到条件,当 REPEAT 由于没有更多的行供循环而不能继续时,出现这个条件。
DECLARE 语句的发布存在特定的次序。用 DECLARE 语句定义的局部变量必须在定义任意游标或句柄之前定义,而句柄必须在游标之后定义,不遵守此顺序将产生错误消息。(从广义上,能够从一个数值拎起一大堆数据的东西都可以叫做句柄(Handle)。)
触发器是 MySQL 响应 DELETE、INSERT 或 UPDATE 语句而自动执行的一条 MySQL 语句(或位于 BEGIN 和 END 语句之间的一组语句)。
创建触发器
在创建触发器时,需要给出以下4条信息:(1) 唯一的触发器名;(2) 触发器关联的表;(3) 触发器应该响应的活动(DELETE、 INSERT 或 UPDATE);(4) 触发器何时执行(处理之前或之后)。基本语法如下:
CREATE TRIGGER <触发器名称> AFTER/BEFORE INSERT/DELETE/UPDATE ON <表名>
FOR EACH ROW <单条 SELECT 语句>;
or
CREATE TRIGGER <触发器名称> AFTER/BEFORE INSERT/DELETE/UPDATE ON <表名>
FOR EACH ROW
BEGIN
<单条或多条 SELECT 语句>;
END;
注意:
删除触发器
基本语法如下:
DROP TRIGGER <触发器名称>;
触发器不能更新或覆盖。为了修改一个触发器,必须先删除它,然后再重新创建。
触发器种类
SELECT NEW.<列名>
;MySQL触发器中不支持 CALL 语句,这表示不能从触发器内调用存储过程,所需的存储过程代码需要复制到触发器内。
数据库表被用来存储和检索数据,不同的语言和字符集需要以不同的方式存储和检索。因此,MySQL需要适应不同的字符集(不同的字母和字符),适应不同的排序和检索数据的方法。
在讨论多种语言和字符集时,会遇到以下术语:① 字符集:字母和符号的集合;② 编码:某个字符集成员的内部表示;③ 校对:规定字符如何比较的指令。
MySQL 支持众多的字符集。使用SHOW CHARACTER SET;
语句显示所有可用的字符集以及每个字符集的描述和默认校对;使用SHOW COLLATION'
语句显示所有可用的校对及其适用的字符集,部分字符集具有不止一种校对,而且许多校对出现两次,一次区分大小写(由 _cs 表示),一次不区分大小写(由 _ci 表示)。
通常系统管理在安装时定义一个默认的字符集和校对,也可以在创建数据库时,指定默认的字符集和校对。为了确定所用的字符集和校对,可以使用语句SHOW VARIABLES LIKE 'character%'; or SHOW VARIABLES LIKE 'collation%';
为了给表指定字符集和校对,可使用带子句的 CREATE TABLE(若未指定,则使用默认):
CREATE TABLE <表名>
(
<列的定义>
) DEFAULT CHARACTER SET <指定字符集>
COLLATE <指定校对>;
MySQL还允许对每个列设置指定字符集和校对:
CREATE TABLE <表名>
(
col1 varchar(10) DEFAULT CHARACTER SET <指定字符集> COLLATE <指定校对>,
col2...
) DEFAULT CHARACTER SET <指定字符集>
COLLATE <指定校对>;
校对在对用 ORDER BY子句检索出来的数据排序时起重要的作用。如果需要用与创建表时不同的校对顺序排序特定的 SELECT 语句,可以在 SELECT 语句自身中进行:
SELECT *
FROM <表名>
ORDER BY <列1>, <列2> COLLATE <指定校对>;
除了在 ORDER BY 子句中使用以外, COLLATE 还可以用于 GROUP BY 、 HAVING 、聚集函数、别名等。此外,如果绝对需要,串可以在字符集之间进行转换。为此,使用Cast(<列名> AS <指定字符集>)
或Convert(<列名> USING <指定字符集>)
函数。
需要给用户提供所需的访问权,且仅提供所需的访问权。这就是所谓的访问控制,管理访问控制需要创建和管理用户账号。
可使用如下代码获得所有用户账号列表:
USE mysql;
SELECT user FROM user;
可使用 CREATE USER 语句创建一个新用户账号:CREATE USER <用户名> IDENTIFIED BY '<口令>'
,IDENTIFIED BY 指定的口令为纯文本,MySQL将在保存到 user 表之前对其进行加密,在创建用户账号时不一定需要口令。
使用RENAME USER <旧用户名称> TO <新用户名称>;
语句重新命名一个用户账号,使用DROP USER <用户名>:
语句删除一个用户账号和所有相关的账号权限
使用SHOW GRANTS FOR <用户名>;
查看赋予用户账号的权限,如果输出结果显示用户只有一个权限GRANT USAGE ON *.* TO '<用户名>'@'%';
,表示用户在任意数据库和任意表上对任何东西没有权限。MySQL的权限用用户名和主机名结合定义。如果不指定主机名,则使用默认的主机名%
(授予用户访问权限而不管主机名)。
为设置权限,使用 GRANT 语句。 GRANT 要求至少给出以下信息:
例如使用GRANT SELECT ON <数据库>.* TO <用户名>;
语句允许用户在指定数据库的所有表上使用 SELECT,通过只授予 SELECT 访问权限,用户对指定数据库中的所有数据具有只读访问权限。每条 GRANT 语句添加(或更新)用户的一个权限,也可通过列出各权限并用逗号分隔,将多条 GRANT 语句串在一起。
GRANT 的反操作为 REVOKE ,用它来撤销特定的权限:REVOKE SELECT ON <数据库>.* FROM <用户名>;
,被撤销的访问权限必须存在,否则会出错。
GRANT 和 REVOKE 可以在以下几个层次上控制访问权限:
GRANT ALL 和 REVOKE ALL
;ON database.*
;ON database.table
;下表列出了可以授予或撤销的每个权限:
权 限 | 说 明 |
---|---|
ALL | 除GRANT OPTION外的所有权限 |
ALTER | 使用ALTER TABLE |
ALTER ROUTINE | 使用ALTER PROCEDURE和DROP PROCEDURE |
CREATE | 使用CREATE TABLE |
CREATE ROUTINE | 使用CREATE PROCEDURE |
CREATE TEMPORARY TABLES | 使用CREATE TEMPORARY TABLE |
CREATE USER | 使用CREATE USER、DROP USER、RENAME USER和REVOKE ALL PRIVILEGES |
CREATE VIEW | 使用CREATE VIEW |
DELETE | 使用DELETE |
DROP | 使用DROP TABLE |
EXECUTE | 使用CALL和存储过程 |
FILE | 使用SELECT INTO OUTFILE和LOAD DATA INFILE |
GRANT OPTION | 使用GRANT和REVOKE |
INDEX | 使用CREATE INDEX和DROP INDEX |
INSERT | 使用INSERT |
LOCK TABLES | 使用LOCK TABLES |
PROCESS | 使用SHOW FULL PROCESSLIST |
RELOAD | 使用FLUSH |
REPLICATION CLIENT | 服务器位置的访问 |
REPLICATION SLAVE | 由复制从属使用 |
SELECT | 使用SELECT |
SHOW DATABASES | 使用SHOW DATABASES |
SHOW VIEW | 使用SHOW CREATE VIEW |
SHUTDOWN | 使用mysqladmin shutdown(用来关闭MySQL) |
SUPER | 使用CHANGE MASTER、KILL、LOGS、PURGE、MASTER 和 SET GLOBAL。还允许mysqladmin调试登录 |
UPDATE | 使用UPDATE |
USAGE | 无访问权限 |
在使用 GRANT 和 REVOKE 时,用户账号必须存在,但对所涉及的对象(数据库和表)没有这个要求。这允许管理员在创建数据库和表之前设计和实现安全措施。这样做的副作用是,当某个数据库或表被删除时(用 DROP 语句),相关的访问权限仍然存在。而且,如果将来重新创建该数据库或表,这些权限仍然起作用。
使用SET PASSWORD FOR <用户名> = Password('<新口令>')
语句更新用户口令,新口令必须传递到 Password() 函数进行加密。还可以通过SET PASSWORD = Password('<新口令>')
设置当前登录用户口令。
数据备份
由于MySQL数据库是基于磁盘的文件,普通的备份系统和例程就能备份MySQL的数据。但是,由于这些文件总是处于打开和使用状态,普通的文件副本备份不一定总是有效。下面列出了这个问题的可能解决方案:
使用命令行实用程序 mysqldump 转储所有数据库内容到某个外部文件。在进行常规备份前这个实用程序应该正常运行,以便能正确地备份转储文件。
[root@]> mysqldump -help Usage: mysqldump [OPTIONS] database_name [tables] OR mysqldump [OPTIONS] --databases [OPTIONS] DB1 [DB2 DB3...] OR mysqldump [OPTIONS] --all-databases [OPTIONS] For more options, use mysqldump --help 1.登录选项: -u user:指定用户 -h host:指定主机 -p:表示要使用密码 -E, --events: 备份事件调度器 -R, --routines: 备份存储过程和存储函数 2.备份选项: --all-databases:备份所有数据库 --databases db1 db2:备份指定的数据库 --single-transaction:对事务引擎执行热备 --flush-logs:更新二进制日志文件 --master-data=2 1:每备份一个库就生成一个新的二进制文件(默认) 2:只生成一个新的二进制文件 --quick:在备份大表时指定该选项
可用命令行实用程序 mysqlhotcopy 从一个数据库复制所有数据,mysqlhotcopy 只是简单的缓存写入和文件复制的过程,备份速度比 mysqldump 快很多很多,特别适合大的数据库,但 mysqlhotcopy 只支持 MyISAM 引擎。
mysqlhotcopy [option] db_name1 db_name2 <备份目标目录>
可以使用MySQL的BACKUP TABLE <表名> TO '<文件存储路径>';
或SELECT * INTO '<文件存储路径或表名>' FROM <表名>;
转储所有数据到某个外部文件。这两条语句都接受将要创建的系统文件名,此系统文件必须不存在,否则会出错。数据可以用RESTORE TABLE <表名1>, <表名2>, ... FROM '<文件路径>';
语句来复原。
为了保证所有数据被写到磁盘(包括索引数据),可能需要在进行备份前使用FLUSH TABLES
语句关闭所有的表(包括正在使用的表)。
数据库维护
ANALYZE TABLE <表名>;
用来检查表键是否正确,CHECK TABLE <表名1>, <表名2>, ... [option] ;
用来针对许多问题对表进行检查(在 MyISAM 表上还对索引进行检查)。
CHECK TABLE
中的option
选项支持一系列的用于 MyISAM 表的方式:CHANGED 检查自最后一次检查以来改动过的表。 EXTENDED 执行最彻底的检查, FAST 只检查未正常关闭的表, MEDIUM 检查所有被删除的链接并进行键检验, QUICK 只进行快速扫描。
如果 MyISAM 表访问产生不正确和不一致的结果,可能需要用REPAIR TABLE <表名>;
来修复相应的表;如果从一个表中删除大量数据,应该使用OPTIMIZE TABLE <表名>;
来收回所用的空间,从而优化表的性能。
诊断启动
MySQL 服务器自身通过在命令行上执行 mysqld 启动。下面是几个重要的 mysqld 命令行选项:
建议
SHOW PROCESSLIST
显示所有活动进程(以及它们的线程ID和执行时间),还可以用 KILL 命令终结某个特定的进程。MySQL 中有以下主要配置文件:
注意:
FLUSH LOGS
语句来刷新和重新开始所有日志文件。MySQL的逻辑框架主要分为四层:
可以通过 "show engines"命令查看MySQL支持的存储引擎:
也可以通过 “show variables like ‘%storage_engine%’” 查看MySQL的当前默认存储引擎:
这里主要对MyISAM和InnoDB进行比较,主要区别如下表:
索引在 sql 调优部分占据着重要的位置,了解并深入索引对我们来说也是非常重要的。
MySQL官方对索引的定义如下:索引(Index)是帮助 MySQL 高效获取数据的数据结构。因此索引的本质就是数据结构。索引的目的在于提高查询效率,可类比字典、书籍的目录等这种形式。
简单来说,索引是关系数据库中对某一列或多个列的值进行预排序的快速查找数据结构。通过使用索引,可以让数据库系统不必扫描整个表,而是直接定位到符合条件的记录,这样就大大加快了查询速度。
在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。
一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储在磁盘上。
平常所说的索引,如果没有特别指明,都是B树索引。其中聚集索引、次要索引、覆盖索引、前缀索引、唯一索引默认都是用B树。
通过 “show index from <表名>” 可以查看表的索引情况:
优点:
缺点:
索引主要分为以下三类:
索引的结构:
①创建索引
create index <索引名称> on <表名> (<列名>(长度));
alter table <表名> add index <索引名称> (<列名>(长度));
注意:
②删除索引
drop index <索引名称> on <表名>;
③查看索引
show index from <表名>;
④其他索引的创建方式
1> 添加主键索引
ALTER TABLE <表名> ADD PRIMARY KEY (<列名>);
2> 添加唯一索引
ALTER TABLE <表名> ADD UNIQUE (<列名>);
也可以只对某一列添加一个唯一约束而不创建唯一索引:
ALTER TABLE <表名> ADD CONSTRAINT <约束名称> UNIQUE (<列名>);
这种情况下,列并没有索引,但仍然具有唯一性保证。
3> 添加全文索引
ALTER TABLE <表名> ADD FULLTEXT (<列名>);
4> 添加普通索引
ALTER TABLE <表名> ADD INDEX <索引名称> (<列名>);
5> 添加组合索引
ALTER TABLE <表名> ADD INDEX <索引名称> (<列名1>, <列名2>, <列名3>, ...);
需建立索引的情况:
不需要创建索引的情况:
使用 EXPLAIN(执行计划)关键字可以模拟优化器执行sql查询语句,从而知道MySQL是如何处理sql语句。explain主要用于分析查询语句或表结构的性能瓶颈。
EXPLAIN 的基本语法很简单:
EXPLAIN + sql 语句;
通过 EXPLAIN + sql 语句可以知道如下内容:
我们假设创建了如下两个表:
我们执行如下 sql 语句:
explain select * from tb_emp;
得到的结果如下所示:
① id
id 代表 select 查询的序列号,包含一组数字,表示查询中执行 select 子句或操作表的顺序,该字段通常与 table 字段搭配来分析。
1> id相同,执行顺序从上到下
2> id 不同,如果是子查询 id 的序号会递增,id 值越大执行优先级越高
3> id 同时存在相同和不同
id 如果相同,可以认为是同一组,执行顺序从上到下。在所有组中,id 值越大执行优先级越高。所以执行顺序为:t3 -> derived2(衍生表,也叫临时表) -> t2。
总结:id 的值表示 select 子句或表的执行顺序。id相同,执行顺序从上到下,id不同,值越大的执行优先级越高。
② select_type
select_type 代表查询的类型,主要用于区别普通查询、联合查询、子查询等复杂的查询。其值主要有六个:
1> SIMPLE
简单的 select 查询,查询中不包含子查询或 union 查询。
2> PRIMARY
查询中若包含任何复杂的子部分,最外层查询为 PRIMARY,也就是最后加载的就是 PRIMARY。
3> SUBQUERY
在 select 或 where 子句中包含了子查询,就为被标记为 SUBQUERY。
4> DERIVED
在 from 子句中包含的子查询会被标记为 DERIVED(衍生),MySQL会递归执行这些子查询,将结果放在临时表中。
5> UNION
若第二个 select 出现在 union 后,则被标记为 UNION。
若 union 包含在 from 子句的子查询中,union 中的前一个 select 将被标记为 DERIVED。
6> UNION RESULT
从 union 表获取结果的 select。
③ table
显示 sql 操作属于哪张表。
④ partitions
官方定义为 The matching partitions(匹配的分区),该字段应该是看 table 所在的分区(NULL 表示表未被分区)。
⑤ type
表示查询所使用的访问类型,我的理解是:type 字段表示你使用的 select 语句在对表进行检索行(筛选行)操作时使用了哪种类型的访问方式。
type 的值表示使用的查询 sql 语句的好坏,从最好到最差依次为:system-> const -> eq_ref -> ref -> range -> index -> ALL。
有一些不常用的 type 值没有给出。一般来说,需保证查询至少达到 range 级别,最好能达到 ref。
1> system
表只有一行记录(等于系统表),是 const 的特例类型,平时不会出现,可以忽略不计。在 MySQL 8.0 版本时,不会出现该字段值,只能出现 const,但是在 MySQL 5.5.48 版本可以出现该情况。猜测 MySQL 8.0 版本可能是进行了优化。
5.5.48:
8.0:
注:两个引擎的执行信息不一样,5.5.4 8执行过程中产生了临时表(DERIVED),8.0 为简单查询。
2> const
表示通过一次索引就找到了结果,常出现于 primary key 或 unique 索引。因为只匹配一行数据,所以查询非常快。如将主键置于 where 条件中,MySQL 就能将查询转换为一个常量。
注意:
3> eq_ref
唯一索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见主键或唯一索引扫描。
4> ref
非唯一性索引扫描,返回匹配某个单独值的所有行。本质上也是一种索引访问,返回匹配某个条件的多行值,属于查找和扫描的混合体。由于是非唯一性索引扫描,所以我们在这里对 tb_emp 表的 deptid 字段创建索引:
create index idx_tb_emp_deptid on tb_emp(deptid);
5> range
只检索给定范围的行,使用一个索引(必须是索引)来检索行,一般出现在 where 语句的条件中,例如使用 between、>、<、in、and、or 等查询。这种索引的范围扫描比全索引扫描要好,因为索引的开始点和结束点都固定,范围相对较小。
6> index
全索引扫描,index 和 ALL 的区别:index 只遍历索引树,通常比 ALL 快,因为索引文件通常比数据文件小。虽说 index 和 ALL 都是全表扫描,但是 index 是从索引中读取,ALL 是从磁盘中读取。
7> ALL
全表扫描。
⑥ possible_keys、key 和 key_len
possible_keys:显示可能应用在表中的索引,可能一个或多个。查询涉及到的字段若存在索引(如下图所示,deptid 列和 name 列均设置了单值索引),则该索引将被列出,但不一定被查询实际使用。
key:实际中使用的索引,如果为 NULL,则表示未使用索引。若查询中使用了覆盖索引,则该索引和查询的 select 字段重叠。覆盖索引的定义:select 的数据列只从索引中就能取得数据,不必读取数据行。
key_len:表示索引中所使用的字节数,可通过该列计算查询中使用的索引长度。在不损失精确性的情况下,长度越短越好。key_len 显示的值为索引字段的最大可能长度,并非实际使用长度,即 key_len 是根据表定义计算而得,并不是通过表内检索得出的。
简单来说,possible_keys 表示理论上可能用到的索引,key 表示实际中使用的索引。
possible_keys 为 NULL 表示可能未用到索引,但 key=idx_tb_emp_deptid_name 表示在实际查询的过程中进行了索引的全扫描。
在使用索引查询时,当条件越精确,key_len 的长度可能会越长,所以在不影响结果的情况下,key_len的值越短越好。
⑦ ref
显示关联的字段。如果使用常数等值查询,则显示 const;如果是连接查询,则会显示关联的字段。如果使用的列不是索引列,那么 ref 会显示为 NULL。
注意:
⑧ rows
根据表统计信息及索引选用情况大致估算出找到所需记录所要读取的行数,即每张表有多少行被优化器查询,当然该值越小越好。
⑨ filtered
百分比值,表示存储引擎返回的数据经过滤后,剩下多少满足查询条件记录数量的比例。
⑩ Extra
显示十分重要的额外信息。其取值有以下几个:
1> Using filesort
Using filesort 表明 mysql 会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。mysql 中无法利用索引完成的排序操作称为“文件排序”。
出现 Using filesort 就非常危险了,在数据量非常大的时候几乎“九死一生”。出现Using filesort 尽快优化 sql 语句。
比如 deptname 字段未建索引的情况:
为 deptname 字段创建索引后:
2> Using temporary
使用了临时表保存中间结果,常见于排序 order by 和分组查询 group by。非常危险,“十死无生”,急需优化。
例如,将 tb_emp 中 name 的索引先删除,出现如下图 Using temporary 结果,非常烂,“十死无生”:
为 name 字段创建索引后:
3> Using index
表明相应的 select 操作中使用了覆盖索引,避免访问表的额外数据行,效率不错。如果同时出现了 Using where,表明存在索引被用来执行索引键值的范围查询(where id>1);如果没有同时出现 Using where,表明索引未用来执行范围查询动作。
例如,删除 tb_emp 表中 name 和 deptid 字段的单独索引,创建复合索引。
上面的例子中,我们创建了(name, deptid)的复合索引,查询的时候也使用复合索引或部分,这就形成了覆盖索引。简记:查询使用复合索引,并且查询的列就是索引列,不能多,个数需对应。
使用优先级 Using index > Using filesort > Using temporary,也就说出现后面两项表明 sql 语句是非常烂的,急需优化!!!
EXPLAIN(执行计划)包含的信息十分的丰富,着重关注以下几个字段信息:
着重关注上述五个字段信息,对日常生产过程中调优十分有用。
我们假设创建了如下三个表:
①首先对员工表和描述表执行 LEFT JOIN 操作:
select e.id, e.username, d.empid from tb_emp e left join tb_desc d on e.id=d.empid;
执行结果如下:
②对上述语句通过explain进行分析:
explain select e.id, e.username, d.empid from tb_emp e left join tb_desc d on e.id=d.empid;
结果如下:
从 explain 执行结果可以看出对两表都是用了全表扫描(ALL),并且在 tb_desc 表中还使用了 join 连接缓存,需要进行优化。但是如何优化?是在左表建立索引还是右表建立索引呢?
因为左连接左表是全有,所以左表的每一行数据都会包含,因此建立索引没有太大意义,而右表不一定包含所有的数据行,因此应该在右表建立索引实现快速查找。
③右表创建索引:
create index idx_empid on tb_desc(empid);
通过 explain 执行可以看到,在创建索引后,获得了比较不错的结果(type=ref,Extra=Using index)。
结论:left join(左连接)情况下,应该在右表创建索引。
④ RIGHT JOIN
我们直接交换两表位置,并将 left join 改变成 right join:
explain select e.id, e.username, d.empid from tb_desc d right join tb_emp e on e.id=d.empid;
结果如下:
与 left join 进行对比,可以得到如下结论:
索引优化的目的主要是让索引不失效。
我们假设创建了如下表格:
①最佳左前缀法则
法则:在利用多列创建了复合索引的情况下,查询从索引的最左列开始且不能跳过索引中的列。
简单来说,如果利用多个列创建了复合索引,在使用索引时要按照创建索引的顺序来使用,不能缺少或跳过,当然只依次使用左边的索引列是可以的。通俗理解:“带头大哥不能死,中间兄弟不能断”。要点:“头不能掉”。
Case 1
下面创建组合索引,并执行 explain:
create index idx_nameagegender on tb_emp (name,age, gender);
explain select * from tb_emp where name= 'Tom' ;
explain select * from tb_emp where name='Tom' and email="1@qq.com";
分析:
Case 2
explain select * from tb_emp where age=22;
分析:
Case 3
explain select * from tb_emp where name='Tom' and age=22;
explain select * from tb_emp where name='Tom' and gender= 'male';
分析:
explain select * from tb_emp where age=22 and name='Tom';
Case 4
explain select * from tb_emp where name='Tom'and age=22 and gender= 'male';
分析:
②不要在索引列上做任何操作
在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效从而转向全表扫描。
Case 1
explain select * from tb_emp where left(name,3)='Tom';
结果如下:
分析:
Case 2
explain select * from tb_emp where name=123;
结果如下:
分析:
③范围右边全失效
存储引擎不能使用索引中范围右边的列,也就是说范围右边的索引列会全部失效。这里的右边指的是创建复合索引时的顺序,而不是 where 子句中的顺序。
Case 1
explain select * from tb_emp where name='Jack';
结果如下:
Case 2
explain select * from tb_emp where name='Jack' and age=27;
结果如下:
Case 3
explain select * from tb_emp where name='Jack' and age=27 and gender='male';
结果如下:
Case 4
explain select * from tb_emp where name='Jack' and age>27 and gender='male';
结果如下:
对以上4个case进行分析:
④尽量使用覆盖索引
尽量使用覆盖索引(查询列和索引列尽量一致,通俗说就是对 A、B 列创建了索引,然后查询中也使用 A、B 列),减少 select * 的使用。
Case 1
explain select * from tb_emp where name='Jack' and age=27 and gender='male';
结果如下:
Case 2
explain select name, age, gender from tb_emp where name='Jack' and age=27 and gender='male';
结果如下:
分析:
⑤使用不等于(!=或<>)会使索引失效
Case 1
explain select * from tb_emp where name !='Jack';
结果如下:
分析:
⑥ is null 或 is not null也无法使用索引
Case 1
explain select * from tb_emp where name is null;
结果如下:
Case 2
explain select * from tb_emp where name is not null;
结果如下:
分析:
⑦ like 谓词以 % 开头会使索引失效
Case 1
explain select * from tb_emp where name like '%JACK%';
结果如下:
Case 2
explain select * from tb_emp where name like '%JACK';
结果如下:
Case 3
explain select * from tb_emp where name like 'JACK%';
结果如下:
分析:
在实际生产环境中,% 仅出现在右边可能不能够解决我们的问题,所以解决 % 出现在左边导致索引失效的方法是使用覆盖索引:
Case 4
explain select name from tb_emp where name like '%JACK%';
结果如下:
分析:
Case 5
explain select id from tb_emp where name like '%JACK%';
结果如下:
分析
Case 6
explain select age from tb_emp where name like '%Jack%';
explain select name, age from tb_emp where name like '%Jack%';
explain select name, age, gender from tb_emp where name like '%Jack%';
explain select id, name, age, gender from tb_emp where name like '%Jack%';
结果如下:
分析:
Case 7
explain select id, name, age, gender, email from tb_emp where name like '%Jack%';
结果如下:
分析:
⑧字符串不加单引号导致索引失效
varchar类型的字段,在查询的时候不加单引号会导致索引失效,转向全表扫描。其实这种情况我们在前面情况②中已经讨论过了,相当于在进行比较时索引列发生了类型的自动转换,导致索引失效。
⑨少用 or,因此用 or 连接会使索引失效
Case 1
explain select * from tb_emp where name='Jack'or name='Mary ';
结果如下:
分析:
我们假设创建了如下表格:
我们对表的 c1、c2、c3 和 c4 字段创建复合索引:
create index idx_test_c1234 on test(c1,c2,c3,c4);
Case 1:常量等值查询索引列的顺序
explain select * from test where c1='a1' and c2='a2' and c3='a3' and c4='a4';
explain select * from test where c1='a1' and c3='a3' and c2='a2' and c4='a4';
explain select * from test where c1='a1' and c4='a4' and c3='a3' and c2='a2';
explain select * from test where c4='a4' and c3='a3' and c2='a2' and c1='a1';
结果如下:
分析:
结论:
Case 2:范围右边索引列
explain select * from test where c1='a1' and c2='a2';
结果如下:
explain select * from test where c1='a1' and c2='a2' and c3>'a3' and c4='a4';
结果如下:
分析:
结论:
explain select * from test where c1='a1' and c2='a2' and c4>'a4' and c3='a3';
结果如下:
分析:
结论:
Case 3:order by 中的索引列
explain select * from test where c1='a1' and c2='a2' and c4='a4' order by c3;
结果如下:
分析:
explain select * from test where c1='a1' and c2='a2' order by c3;
结果如下:
分析:
explain select * from test where c1='a1' and c2='a2' order by c4;
结果如下:
分析:
explain select * from test where c1='a1' and c5='a5' order by c2,c3;
结果如下:
分析:
explain select * from test where c1='a1' and c5='a5' order by c3,c2;
结果如下:
分析:
explain select * from test where c1='a1' and c2='a2' order by c2,c3;
explain select * from test where c1='a1' and c2='a2' and c5='a5' order by c2,c3;
结果如下:
分析:
explain select * from test where c1='a1' and c2='a2' order by c3,c2;
结果如下:
分析:
Case 4:group by 中的索引列
explain select * from test where c1='a1' and c4='a4' group by c2,c3;
结果如下:
分析:
explain select * from test where c1='a1' and c4='a4' group by c3,c2;
结果如下:
分析:
通过以上 Case 的分析,进行如下总结:
在使用 order by 时,经常出现 Using filesort,因此对于此类 sql 语句需尽力优化,使其尽量使用 Using index。
我们假设创建了如下表格:
我们对表的 c1、c2、c3 和 c4 字段创建复合索引:
create index idx_c1234 on test(c1,c2,c3,c4);
Case 1
explain select * from test where c1>'a1' order by c1;
结果如下:
分析:
explain select * from test where c1>'a1' order by c2;
结果如下:
分析:
explain select * from test where c1 in ('a1','a2','a3') order by c2,c3;
结果如下:
分析:
c1 in ('a1','a2','a3')
使用了范围查询,这相当于 c1 使用了范围,因此 c2 索引失效,Extra 中出现了 Using filesort。explain select * from test where c1='a1' and c2>'a2' order by c2;
结果如下:
分析:
Case 2
explain select * from test where c1>'a1' order by c1,c2;
结果如下:
分析:
explain select * from test where c1>'a1' order by c2,c1;
结果如下:
分析:
explain select * from test where c2>'a2' order by c1,c2;
结果如下:
分析:
explain select c2 from test where c2>'a2' order by c1,c2;
explain select * from test where c1='a1' and c2>'a2' order by c2,c5;
分析:
Case 3
explain select * from test order by c2;
explain select c1 from test order by c2;
explain select c1 from test where c2>'a2' order by c2;
结果如下:
分析:
explain select c1 from test order by c1 asc,c2 desc;
结果如下:
分析:
explain select c3 from test order by c1 desc,c2 desc;
结果如下:
分析:
Case 4
explain select * from test order by c1,c2;
explain select c3 from test order by c1,c2;
结果如下:
分析:
具体来说,filesort 有两种排序算法,分别是双路排序和单路排序。
双路排序
在 MySQL4.1 之前使用双路排序,就是两次磁盘扫描,得到最终数据。从磁盘中读取行指针(rowid)和 order by 排序列,对它们进行排序,然后扫描已经排好序的列表,按照列表中的值重新从磁盘中读取对应的数据输出。即从磁盘读取排序字段,在 buffer 进行排序,再从磁盘取其他字段。
双路排序步骤如下:
如果使用双路排序,取一批数据要对磁盘进行两次扫描,众所周知,I/O 操作是很耗时的,因此在 MySQL4.1 以后,出现了改进的算法:单路排序。
单路排序
从磁盘中查询所需的列,按照 order by 列在 buffer 中对它们进行排序,然后扫描排序后的列表进行输出。它的效率更高一些,避免了第二次读取数据,并且把随机 I/O 变成了顺序 I/O,但是会使用更多的空间,因为它把每一行都保存在内存中了。
单路排序步骤如下:
两种排序算法的区别:
MySQL根据 max_length_for_sort_data 变量来确定使用哪种算法,默认值是1024字节,如果需要返回的列的总长度大于 max_length_for_sort_data,使用第一种算法,否则使用第二种算法。
解决方式有:
提升 order by 速度的方式有:
group by 与 order by 很类似,其实质是先排序后分组,遵照索引创建顺序的最佳左前缀法则。当无法使用索引列的时候,也要对 sort_buffer_size 和 max_length_for_sort_data 参数进行调整。注意 where 高于 having,能写在 where 中的限定条件就不要去 having 限定了。
在使用 group by 时,经常出现 Using temporary,因此对于此类 sql 语句需尽力优化,使其尽量使用 Using index。
我们假设创建了如下表格:
我们对表的 c1、c2、c3 和 c4 字段创建复合索引:
create index idx_c1234 on test(c1,c2,c3,c4);
Case 1
explain select * from test where c1>'a1' group by c1;
结果如下:
分析:
explain select * from test where c1>'a1' group by c2;
结果如下:
分析:
explain select * from test where c1='a1' and c2>'a2' group by c2;
结果如下:
分析:
Case 2
explain select * from test where c1>'a1' group by c1,c2;
结果如下:
分析:
explain select * from test where c1>'a1' group by c2,c1;
结果如下:
分析:
explain select * from test where c2>'a2' group by c1,c2;
结果如下:
分析:
explain select * from test where c1='a1' and c2>'a2' group by c2,c5;
分析:
Case 3
explain select * from test group by c2;
explain select c1 from test group by c2;
explain select c1 from test where c2>'a2' group by c2;
结果如下:
分析:
Case 4
explain select * from test group by c1,c2;
explain select c3 from test group by c1,c2;
结果如下:
分析:
慢查询日志是 MySQL 提供的一种日志记录,它记录 MySQL 中响应时间超过阈值的语句,具体指运行时间超过 long_query_time 值的sql语句,该 sql 语句会被记录到慢查询日志中。慢查询日志主要与 explain 进行联合分析。
默认情况下,MySQL 数据库没有开启慢查询日志,需要我们手动来设置这个参数。如果不是调优需要,一般不建议开启该参数,因为开启慢查询日志或多或少会带来一定的性能影响。
① 首先查看慢查询日志是否开启
show variables like '%slow_query_log%';
结果如下:
② 使用命令开启慢查询日志
当 slow_query_log 的 Value 为 OFF 时,代表慢查询日志处于关闭状态,可用以下命令来开启:
set global slow_query_log=1;
注意:
① 查看慢查询的阈值时间
该值由 long_query_time 控制。默认情况下为 10 秒。
show variables like '%long_query_time%';
注意:
② 设置 long_query_time 的值
set global long_query_time=8;
当设置 long_query_time 值后,查看其值并没有变化,解决方式:
show global variables like '%long_query_time%';
③ 查看慢查询 sql 的数目
-- 在MySQL中执行select sleep(N)可以让此语句运行N秒钟
SELECT sleep(9);
SELECT sleep(9);
show global status like '%Slow_queries%';
查看慢查询日志文件:
从文件中可看到两条 select sleep(9) 语句。
因为直接分析日志文件是个体力活,因此 mysql 为我们提供了相关工具 mysqldumpslow 来对慢查询日志文件进行分析。具体使用方式可用 mysqldumpslow --help 命令查看具体参数。
我们假设创建了如下表格:
字段 | 含义 |
---|---|
id | 主键 |
deptno | 部门编号 |
dname | 部门名称 |
loc | 备注 |
字段 | 含义 |
---|---|
id | 主键 |
empno | 员工编号 |
empname | 员工名字 |
job | 工作 |
mgr | 上级编号 |
hiredate | 入职时间 |
sal | 薪水 |
comm | 红利 |
deptno | 部门编号 |
由于在创建函数时,可能会报错:This function has none of DETERMINISTIC.....
。因此我们需开启函数创建的信任功能,这一功能由 log_bin_trust_function_creators 参数来开启:
show variables like '%log_bin_trust_function_creators%';
Value 值为 OFF,说明未开启:
可通过 set global log_bin_trust_function_creators=1
的形式开启该功能,也可通过在 my.cnf 中永久配置的方式开启该功能,在 [mysqld] 下配置 log_bin_trust_function_creators=1
。
MySQL 中函数编写的基本语法如下:
create function <函数名称>([参数1 参数1类型],[参数2 参数2类型],...)
returns <返回值类型>
begin
<语句;>
return 返回值;
end;
注意:
delimiter $$
将结束标志改成 “$$”,但是在创建完函数后记得将命令结束标志改回分号。我们接下来创建两个函数:随机生成字符串的函数和随机生成编号的函数,以满足后续向表中插入不同的数据。
①创建随机生成字符串的函数
delimiter $$
drop function if exists rand_string; -- 如果函数存在就先删除
create function rand_string(n int)
returns varchar(255)
begin
declare chars_str varchar(52) 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 $$
注意:
②创建随机生成编号的函数
delimiter $$
drop function if exists rand_num;
create function rand_num()
returns int(5)
begin
declare i int default 0;
set i=floor(100+rand()*100);
return i;
end $$
delimiter ;
注意:
show function status;
MySQL 中存储过程编写的基本语法如下:
create procedure <存储过程名称>(<IN 或 OUT 或 INOUT> 参数1 参数1类型, <IN 或 OUT 或 INOUT> 参数2 参数2类型, ...)
begin
<语句>; -- 过程体
end
注意:
我们接下来创建两个存储过程,分别用于向部门表和员工表中批量插入数据。
①创建往 tb_dept_bigdata 表中插入数据的存储过程
delimiter $$
drop procedure if exists insert_dept;
create procedure insert_dept(in max_num int(10))
begin
declare i int default 0;
set autocommit=0;
repeat
set i=i+1;
insert into tb_dept_bigdata (deptno,dname,loc) values(rand_num(),rand_string(10),rand_string(8));
until i=max_num
end repeat;
commit;
end $$
delimiter ;
注意:
set autocommit=0;
将禁止自动提交,只有当出现 commit;
时才会提交更改,保证了原子性(在事务结束时,其中所包含的更新处理要么全部执行,要么完全不执行);②创建往 tb_emp_bigdata 表中插入数据的存储过程
delimiter $$
drop procedure if exists insert_emp;
create procedure insert_emp(in start int(10),in max_num int(10))
begin
declare i int default 0;
set autocommit=0;
repeat
set i=i+1;
insert into tb_emp_bigdata (empno,empname,job,mgr,hiredate,sal,comm,deptno) values((start+i),rand_string(6),'developer',0001,curdate(),2000,400,rand_num());
until i=max_num
end repeat;
commit;
end $$
delimiter ;
注意:
show procedure status;
执行创建随机生成字符串的函数的 sql 语句。
执行创建随机生成编号的函数的 sql 语句。
查看函数是否创建成功。
执行创建往 tb_dept_bigdata 表中插入数据的存储过程的 sql 语句。
执行创建往 tb_emp_bigdata 表中插入数据的存储过程的 sql 语句。
查看存储过程是否创建成功。
执行存储过程,插入数据:
①首先执行 insert_dept 存储过程
call insert_dept(100);
select count(*) from tb_dept_bigdata;
说明:deptno 的范围为 [100, 200),因为 deptno 的值使用了 rand_num() 函数。
②然后执行 insert_emp 存储过程
call insert_emp(100,300);
select count(*) from tb_emp_bigdata;
注:对于部门表的 deptno 和员工表中 deptno 的数据都使用了 rand_num() 函数进行赋值,确保两边的值能对应。
删除函数与存储过程
-- 删除函数
drop function rand_num;
drop function rand_string;
-- 删除存储过程
drop procedure insert_dept;
drop procedure insert_emp;
在学习了数据批量插入后,我们接下来介绍小表驱动大表。首先给出结论:在查询的优化中,永远小表驱动大表,即小的数据集驱动大的数据集。
类似循环嵌套:
for(int i=5;.......)
{
for(int j=1000;......)
{}
}
如果小的循环在外层,对于数据库连接来说就只连接5次,进行5000次操作,如果1000在外,则需要进行1000次数据库连接,从而浪费资源,增加消耗。这就是为什么要小表驱动大表。
在 tb_dept_bigdata 表中插入100条数据,在 tb_emp_bigdata 表中插入5000条数据:
call insert_dept(100);
call insert_emp(100,5000);
select count(*) as count_dept from tb_dept_bigdata;
select count(*) as count_emp from tb_emp_bigdata;
注意:
①当 B 表的数据集小于 A 表数据集时,用 in 优于 exists:
select * from tb_emp_bigdata A where A.deptno in (select B.deptno from tb_dept_bigdata B);
B 表为 tb_dept_bigdata:100条数据,A 表 tb_emp_bigdata:5000条数据。
用 in 的查询时间为:
将上面 sql 转换成 exists:
select * from tb_emp_bigdata A where exists(select B.deptno from tb_dept_bigdata B where B.deptno=A.deptno);
用exists的查询时间:
经对比可看到,在B表数据集小于A表的时候,用 in 要优于exists,当前的数据集并不大,所以查询时间相差并不多。
②当 A 表的数据集小于 B 表的数据集时,用 exists 优于 in:
select * from tb_dept_bigdata A where A.deptno in (select B.deptno from tb_emp_bigdata B);
用 in 的查询时间为:
将上面 sql 转换成 exists:
select * from tb_dept_bigdata A where exists(select 1 from tb_emp_bigdata B where B.deptno=A.deptno);
用 exists 的查询时间:
由于数据量并不是很大,因此对比并不是那么强烈。
select * from A where id in (select id from B)
等价于:
for select id from B
for select * from A where A.id = B.id
当 B 表的数据集必须小于 A 表的数据集时,用 in 优于 exists。
select * from A where exists (select 1 from B where B.id = A.id)
等价于
for select * from A
for select * from B where B.id = A.id
对于 exists:
select ... from table where exists(subquery);
可以理解为:将主查询的数据放入子查询中做条件验证,根据验证结果(True 或 False)来决定主查询的数据是否得以保留。
Show Profile 是 MySQL 提供的可以用来分析当前会话中 sql 语句执行的资源消耗情况的工具,可用于 sql 调优的测量。默认情况下处于关闭状态,开启后默认保存最近15次的运行结果。Show Profile 的分析步骤如下:
开启 Show Profile 功能,默认该功能是关闭的,使用前需开启。开启后只存活于当前会话,也就说每次使用前都需要开启。
show variables like 'profiling' ;
set profiling=on ;
show variables like 'profiling' ;
向十七章中创建的 tb_emp_bigdata 表中插入 50w 条数据。然后执行如下查询语句:
select * from tb_emp_bigdata group by id%10 limit 150000;
select * from tb_emp_bigdata group by id%20 order by 5;
通过 show profiles 查看结果:
使用 show profile 对 sql 语句进行诊断:
-- cpu、block io 为查询参数
-- Query_ID 为 show profiles 列表中的 Query_ID
show profile cpu,block io for query <Query_ID>;
比如执行show profile cpu,block io for query 15;
,可得到:
因此,通过 show profiles 查看 sql 语句的耗时时间,然后通过 show profile 命令对耗时时间长的 sql 语句进行诊断。
Show Profile 语句中的常用查询参数有:
参数 | 含义 |
---|---|
ALL | 显示所有的开销信息 |
BLOCK IO | 显示块IO开销 |
CONTEXT SWITCHES | 上下文切换开销 |
CPU | 显示CPU开销信息 |
IPC | 显示发送和接收开销信息 |
MEMORY | 显示内存开销信息 |
PAGE FAULTS | 显示页面错误开销信息 |
SOURCE | 显示和 Source_function,Source_file,Source_line 相关的开销信息 |
SWAPS | 显示交换次数开销信息 |
结论 | 含义 |
---|---|
converting HEAP to MyISAM | 查询结果太大,内存不够,数据往磁盘上搬了 |
Creating tmp table | 创建临时表。先拷贝数据到临时表,用完后再删除临时表 |
Copying to tmp table on disk | 把内存中临时表复制到磁盘上,危险!!! |
locked | 出现了锁 |
如果在 show profile 诊断结果中出现了以上4条结果中的任何一条,则 sql 语句需要优化。
全局查询日志用于保存所有的 sql 执行记录,该功能主要用于测试环境,在生产环境中永远不要开启该功能。
开启全局查询日志的方法有两种:
show variables like '%general%';
set global general_log=1;
set global log_output='TABLE';
我们在第十一章介绍 MySQL 存储引擎时,曾经对两大常用的存储引擎 MyISAM 和 InnoDB 进行了如下表的比较:
在上表中有一个比较项叫行表锁,我们当时简单地列出了 MyISAM 和 InnoDB 所支持的锁的类型,这一章我们将详细探讨表锁和行锁。
锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算机资源(如 CPU、RAM、I/O 等)的争用外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。
从对数据操作的类型来分,锁可以分为读锁(共享锁)和写锁(排它锁);从对数据操作的粒度来分,锁可以分为表锁和行锁。
特点:偏向 MyISAM 存储引擎,开销小,加锁快,无死锁,锁定粒度大,发生锁冲突的概率最高,并发度低。
我们创建如下所示的表:
增加表锁命令的基本语法如下:
lock table <表名1> <read 或 write>, <表名2> <read 或 write>;
查看表是否被加锁:
show open tables;
如果 In_use 显示不为0,则表示表被加锁。
释放表锁命令:
unlock tables;
不需要加表名。
在 mylock 表上加读锁,并假设当前会话命名为 A,在 A 会话中查询 mylock 中的数据:
lock table mylock read;
select * from mylock;
结果如下:
数据查询正常,没有任何问题。
再开一个会话,命名为 B,查询 mylock 中的数据:
数据查询依旧正常,没有任何问题。
在 A 会话中进行更新操作:
update mylock set name='a1' where id=1;
提示 mylock 表被加锁,不能进行更新操作。原因是 mylock 正被读锁锁住,未解锁不能进行更新操作。
在 B 会话中读其他表:
select * from tb_emp;
A 会话 mylock 表的读锁,并不影响 B 会话对 mylock 表的读操作和其他表的读写操作。
在 A 会话中读其他表:
select * from tb_emp;
由于 A 会话对 mylock 表加了读锁,在未解锁前,不能操作其他表。
在 B 会话中修改 mylock 表中的内容:
update mylock set name='a1' where id=1;
出现了阻塞情况。原因是由于 A 会话对 mylock 表加锁,在锁未释放时,其他会话是不能对 mylock 表进行更新操作的。
在 A 会话中对 mylock 表进行解锁操作,注意观察 B 会话中的变化:
unlock tables;
在 A 会话中对 mylock 表解锁后,B 会话更新操作成功,可看到 B 会话中的更新操作等待了4分钟。
在 A 会话中对 mylock 表加写锁:
lock table mylock write;
show OPEN TABLES where In_use >0;
在 A 会话中对 mylock 表进行读写操作:
update mylock set name='a1' where id=1;
select * from mylock;
由于是 A 会话对 mylock 表加的写锁,所以读写操作都执行正常。
在 A 会话中对其他表进行操作:
select * from tb_emp;
insert into tb_emp (name , age, gender, email) values ( 'lock ',1, 'male', 'lock@qq.com');
在 A 会话中对其他表进行读写操作都失败,因为 A 会话中 mylock 表的写锁并未被释放。
在 B 会话中对 mylock 表进行读操作:
select * from mylock;
由于 mylock 表已经加写锁,而写锁为排它锁,因此在 B 会话中对 mylock 表进行读操作阻塞。由于 B 会话中对 mylock 的读操作都阻塞,所以其他操作也是阻塞的。
在 B 会话中对其他表进行读写操作:
select * from tb_emp;
insert into tb_emp (name , age, gender, email) values ( 'lock ',1, 'male', 'lock@qq.com');
A 会话 mylock 表的写锁,并不影响 B 会话对其他表的读写操作。
使用如下命令查看是否有表被锁定:
show open tables where In_use>0;
使用如下命令分析表锁:
show status like 'table%';
主要注意两个变量的值:
注意数据库引擎为 MyISAM。
简而言之,读锁会阻塞写,但是不会阻塞读,而写锁会把读和写都阻塞。此外,MyISAM 的读写锁调度是写优先,因此 MyISAM 不适合做以写为主的表的引擎,因为写锁后,其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成长时间阻塞。
行锁偏向 InnoDB 存储引擎,开销大,加锁慢,会出现死锁,锁定粒度小,发生锁冲突的概率低,但并发度高。
create index idx_lock_a on test_innodb_lock(a);
create index idx_lock_b on test_innodb_lock(b);
打开 A、B 两个会话,并在 A 会话中关闭数据库的自动提交:
set autocommit=0;
在 A 会话中做更新操作:
update test_innodb_lock set b='a1' where a=1;
select * from test_innodb_lock;
从查询结果可知,在 A 会话中更新数据成功。
在 B 会话中做查询操作:
select * from test_innodb_lock;
B 会话中并没有读取到 A 会话中更新后的值。(读己知所写:自己更改的数据自己知道,但是如果未提交,其他人是不知道的。)
在 A 会话中执行 commit 命令,然后在 B 会话中再次查询:
在 A 会话中做更新操作,然后在 B 会话中也做更新操作:
-- A 会话
update test_innodb_lock set b='a2' where a=1;
-- B 会话
update test_innodb_lock set b='a3' where a=1;
可以看出此时 B 会话发生了阻塞。
在 A 会话中 commit 操作,可看到B会话中发生了更新操作:
因为我们操作的是同一行数据,而由于 InnoDB 为行锁,在 A 会话未提交时,B 会话只有阻塞等待。如果操作不同行,则不会出现阻塞情况。
update test_innodb_lock set b='1000' where a=1;
update test_innodb_lock set b='2000' where a=2;
update test_innodb_lock set b='3000' where a=3;
update test_innodb_lock set b='4000' where a=4;
update test_innodb_lock set b='5000' where a=5;
-- A 会话
update test_innodb_lock set a=110 where b=1000;
-- B 会话
update test_innodb_lock set b='5001' where a=5;
间隙锁的定义:
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据记录的索引项加锁,对于键值在条件范围内但不存在的记录,叫作“间隙(GAP)”。 InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。
间隙锁的危害:
因为在 Query 执行过程中通过范围查找的话,会锁定整个范围内的所有索引键值,即使这个索引不存在。间隙锁有一个比较致命的弱点,就是当锁定一个范围键值后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定值范围内的任何数据。在某些场景下这个可能会对性能造成很大的危害。
间隙锁演示
我们需要对 test_innodb_lock 中的数据进行修改,修改后的数据如下:
在 A 会话中执行如下语句:
update test_innodb_lock set b='test' where a>1 and a<6;
在 B 会话中执行如下语句:
insert into test_innodb_lock values (2,'2000');
只有在 A 会话 commit 后,B 会话才能进行插入操作:
我们通常使用 for update
来锁定某一行。
在 A 会话中执行如下语句:
select * from test_innodb_lock where a=7 for update;
此时就锁定了 a=7 这一行。
在B会话中对该行进行更新操作:
update test_innodb_lock set b='xxx' where a=7;
只有在 A 会话中进行了 commit 后,B 会话的更新操作才能执行:
使用如下命令:
sql 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 | 系统启动后到现在总共等待锁的次数 |
这个五个状态量中,比较重要的是:Innodb_row_lock_time、Innodb_row_lock_time_avg 和 Innodb_row_lock_waits。尤其是等待次数很高,而且每次等待时长不小时,就需要分析优化了。可以看出,Innodb_row_lock_waits * Innodb_row_lock_time_avg = Innodb_row_lock_time 。
还有一种锁叫页锁,它的开销和加锁时间介于表锁和行锁之间,会出现死锁,锁定粒度介于表锁和行锁之间,并发度一般(了解即可)。
本章主要讲解 MySQL 主从复制的操作步骤。主机使用 Windows 环境,从机使用 Linux 环境。注意,MySQL 的版本最好一致。
slave(从机)会从 master(主机)读取 binlog 来进行数据同步。主要有以下三个步骤:
MySQL 的复制是异步且串行化的。
主从复制的规则如下:
在主从复制过程中,最大的问题就是延时。
要求:
MySQL 版本最好一致且后台以服务运行。并且保证主机与从机互相 ping 通。主从配置都在 [mysqld] 结点下,都是小写。
主机修改 my.ini 配置文件
log-bin = "E:\devSoft\mysql-5.7.22-winx64\data\mysql-bin"
。配置该项后,重新启动 mysql 服务,可看到如下内容:启用错误日志(可选):
log_error = "E:\devSoft\mysql-5.7.22-winx64\data\log\errorlog\log_error.log"
根目录、数据目录(可选):
# mysql安装根目录
basedir = "E:\devSoft\mysql-5.7.22-winx64"
# mysql数据文件所在位置
datadir = "E:\devSoft\mysql-5.7.22-winx64\data"
临时目录(可选):
tmpdir = "E:\devSoft\mysql-5.7.22-winx64\"
read-only=0,表示主机读写都可以。
可以设置不需要复制的数据库(可选):
binlog-ignore-db = mysql
可以设置需要复制的数据库(可选):
binlog-do-db=databasename
从机修改 my.cnf 配置文件
主机与从机都关闭防火墙,其实也可配置 ip 规则,但关闭防火墙更快速。
在 Windows 主机上建立账户并授权给 slave
# 字符%表示任何客户端都可以连接
grant all privileges on *.* to slaveaccount(用户名)@"%(或者指定ip)" identified by '你想设置的密码' with grant option;
flush privileges;
GRANT REPLICATION SLAVE ON *.* TO 'slaveaccount(上面创建的用户名)'@'从机数据库ip' IDENTIFIED BY '你想设置的密码';
查询 master 的状态:
show master status;
File 和 Position 这两个字段非常重要,File 告诉从机需要从哪个文件进行复制,Position 告诉从机从文件的哪个位置开始复制,在从机上配置时需用到。执行完此操作后,尽量不要在操作主服务器 MySQL,防止主服务器状态变化( File 和 Position 状态变化)。
在 Linux 从机上配置需要的主机
CHANGE MASTER TO MASTER_HOST='主机IP',MASTER_USER='salveaccount',MASTER_PASSWORD='主机授权的密码',MASTER_LOG_FILE='File名字',MASTER_LOG_POS=Position数字;
启动从服务器复制功能
start slave;
启动复制功能后,需要查看主从复制配置是否成功:
注意,只有当 Slave_IO_Running 和 Slave_SQL_Running 都为 Yes 的时候,主从复制配置才成功。
进行测试
停止从服务复制功能:
stop slave;
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。