数据库①:基础、事务、锁

数据库相关文章:

  • 数据库①:基础、事务、锁:https://blog.csdn.net/hancoder/article/details/105773038
  • 数据库②:索引、调优、explain(尚硅谷笔记)https://blog.csdn.net/hancoder/article/details/105773095
  • 数据库③:存储结构、页、聚簇索引 https://blog.csdn.net/hancoder/article/details/105773153

文章目录

MySQL安装

DBMS分为两类:
– 基于共享文件系统的DBMS (Access )
– 基于客户机——服务器的DBMS(MySQL、 Oracle、 SqlServer)

图形化界面:SQLyog

MySQL服务的启动和停止

方式一:计算机——右击管理——服务
方式二:通过管理员身份运行,然后输入
net start 服务名(启动服务) //net start mysql
net stop 服务名(停止服务)

/* Windows服务 */
-- 启动MySQL
    net start mysql
-- 创建Windows服务
    sc create mysql binPath= mysqld_bin_path(注意:等号与值之间有空格)
/* 连接与断开服务器 */
mysql -h 地址 -P 端口 -u 用户名 -p 密码
SHOW PROCESSLIST -- 显示哪些线程正在运行
SHOW VARIABLES -- 显示系统变量信息

MySQL服务的登录和退出

登录:
方式一:通过mysql自带的客户端cmd
只限于root用户

方式二:通过windows自带的客户端cmd
登录:
mysql 【-h主机名 -P端口号 】-u用户名 -p密码
例:mysql -h localhost -P 3306 -u root -p123456(最后一个不能有空格)
简例:mysql -u root -p(默认连本机的3306)


进去后我们面对的是库(项目)>>表>>数据
链接成功后,mysql和cmd的关系:
客户端client   服务端server。好比QQ浏览器和163网站的服务器一样。mysql mysqld

退出:
`exit或ctrl+C`

查询版本:
方式一:登录到mysql服务端
select version();
方式二:没有登录到mysql服务端
mysql --version
或 mysql --V

第 1 章 MySQL 简介

1.什么是 Mysql

  • MySQL 是一个关系型数据库管理系统, 由瑞典 MySQL AB 公司开发, 目前属于 Oracle 公司。
  • Mysql 是开源的, 可以定制的, 采用了 GPL 协议, 你可以修改源码来开发自己的 Mysql 系统。
  • MySQL 使用标准的 SQL 数据语言形式。
  • Mysql 可以允许于多个系统上, 并且支持多种语言。 这些编程语言包括 C、 C++、 Python、 Java、 Perl、 PHP、Eiffel、 Ruby 和 Tcl 等。
  • MySQL 支持大型数据库, 支持 5000 万条记录的数据仓库, 32 位系统表文件最大可支持 4GB, 64 位系统支持最大的表文件为 8TB.

2.在 Linux 上安装mysql

2.1 准备工作

2.1.1 检查当前系统是否安装过 Mysql

(1) CentOS6 环境下

命令: rpm -qa|grep mysql
默认 Linux 在安装的时候, 自带了 mysql 相关的组件。
先卸载系统自带的 mysql, 执行卸载命令 rpm -e --nodeps mysql-libs

(1) CentOS7 环境下
命令: rpm -qa|grep mariadb

默认 Linux(CentOS7) 在安装的时候, 自带了 mariadb(mysql 完全开源版本)相关的组件。
先卸载系统自带的 mariadb, 执行卸载命令 rpm -e --nodeps mariadb-libs

2.1.2 检查/tmp 文件夹权限

查看/tmp 文件夹权限:

赋予其最大权限

chmod -R 777 /tmp

2.2 Mysql 的安装

安装的版本是 mysql 5.5, 官网下载地址: http://dev.mysql.com/downloads/mysql/

①将 rpm 安装包拷贝到 opt 目录下
②在安装目录下执行 rpm 安装

rpm -ivh MySQL-client-5.5.54-1.linux2.6.x86_64.rpm
rpm -ivh MySQL-server-5.5.54-1.linux2.6.x86_64.rpm

装完成后, 出现如下警告, 需要为软件设置 root 用户的密码。

③查看是否安装成功: mysqladmin --version
或者也可以通过 rpm 命令来查看:

④设置用户和密码: mysqladmin –u root password ‘xxxxxx’

如果提示“GPG keys…”安装失败,解决方案:rpm -ivh rpm软件名 --force --nodoeps

2.3 Mysql 服务

2.3.1 Mysql 服务的启动和停止

查看状态: service mysql status
启动服务: service mysql start  
停止服务: service mysql stop
重启服务: service mysql restart

启动之后, 查看进程

ps -ef|grep mysql

2.3.2 Mysql 的安装位置

参数 路径 解释 备注
–datadir /var/lib/mysql/ mysql 数据库文件的存放路径
–basedir /usr/bin 相关命令目录 mysqladmin mysqldump 等命令
–plugin-dir /usr/lib64/mysql/plugin mysql 插件存放路径
–log-error /var/lib/mysql/jack.atguigu.err mysql 错误日志路径
–pid-file /var/lib/mysql/jack.atguigu.pid 进程 pid 文件
–socket /var/lib/mysql/mysql.sock 本地连接时用的 unix 套接字文件
/usr/share/mysql 配置文件目录 mysql 脚本及配置文件
/etc/init.d/mysql 服务启停相关脚本

2.3.3 Mysql 服务的自启动

Mysql 服务是开机自动启动的!

在计算机reboot后 登陆MySQL :  mysql
可能会报错:   "/var/lib/mysql/mysql.sock不存在"  
-- 原因:是Mysql服务没有启动
1.每次使用前 手动启动服务   /etc/init.d/mysql start
2.开机自启   chkconfig mysql on     ,  chkconfig mysql off    
检查开机是否自动启动: ntsysv	
  使用空格取消选中, 然后按 TAB 确定!

2.3.4 Mysql 的重复启动问题

此时查看, 多了很多进程:

尝试去登录或者操作: 报错!
查看服务状态:
解决: 杀死所有和 mysql 进程相关的操作, 然后重启服务!
注意是 mysqld, d 代表 demon, 守护进程。
然后再重启:

2.4 修改字符集

2.4.1 常用命令

SQL 语句 描述 备注
show databases 列出所有数据库
create database 库名 创建一个数据库
create database 库名 character set utf8 创建数据库, 顺便执行字符集为 utf-8
show create database 库名 查看数据库的字符集
show variables like ‘%char%’ 查询所有跟字符集相关的信息
set [字符集属性]=utf8 设置相应的属性为 utf8 只是临时修改, 当前有效。 服务重启后, 失效。
alter database 库名 character set ‘utf8’ 修改数据库的字符集
alter table 表 名 convert to character set ‘utf8’ 修改表的字符集

实验 SQL:

CREATE database mydb;
CREATE table mytable(id int,name varchar(30));
insert into mytable(id,name) values (1,‘jack’);
insert into mytable(id,name) values (2,‘张三’)

2.4.2 如果在建库建表的时候, 没有明确指定字符集, 则采用默认的字符集 其中是不包含中文字符的。 查看默 认的编码字符集:

show variables like '%char%'

2.4.3 永久修改

(1) 修改配置文件
在/usr/share/mysql/ 中找到 my.cnf 的配置文件, 拷贝其中的 my-huge.cnf 到 /etc/ 并命名为 my.cnf 。 添加以下内容后再重启服务。
注意: 必须将文件拷贝到指定路径, 且名称为 my.cnf

[client]
default-character-set=utf8
[mysqld]
character_set_server=utf8
character_set_client=utf8
collation-server=utf8_general_ci
[mysql]
default-character-set=utf8

再次查看

注意: 已经创建的数据库的设定不会发生变化, 参数修改只对新建的数据库有效!
(2) 修改已创建库、 表字符集
修改数据库的字符集
mysql> alter database mydb character set ‘utf8’;
修改数据表的字符集
mysql> alter table mytbl convert to character set ‘utf8’;
(3) 修改已经乱码数据
无论是修改 mysql 配置文件或是修改库、 表字符集, 都无法改变已经变成乱码的数据。
只能删除数据重新插入或更新数据才可以完全解决

2.5 设置大小写不敏感

①查看大小写是否敏感: show variables like ‘%lower_case_table_names%’
windows 系统默认大小写不敏感, 但是 linux 系统是大小写敏感的

②设置大小写不敏感: 在 my.cnf 这个配置文件 [mysqld] 中加入 lower_case_table_names = 1 , 然后重启服务器

属性设置 描述
0 大小写敏感
1 大小写不敏感。 创建的表, 数据库都是以小写形式存放在磁盘上, 对于 sql 语句都是转换为 小写对表和 DB 进行查找
2 创建的表和 DB 依据语句上格式存放, 凡是查找都是转换为小写进行

注意: 如果要设置属性为大小写不敏感, 要在重启数据库实例之前就需要将原来的数据库和表转换为小写, 否则将
找不到数据库名。 在进行数据库参数设置之前, 需要掌握这个参数带来的影响, 切不可盲目设置。

2.6 sql_mode

sql_mode 定义了对 Mysql 中 sql 语句语法的校验规则!
sql_mode 是个很容易被忽视的变量, 默认值是空值, 在这种设置下是可以允许一些非法操作的, 比如允许一些
非法数据的插入。 在生产环境必须将这个值设置为严格模式, 所以开发、 测试环境的数据库也必须要设置, 这样在
开发测试阶段就可以发现问题。
2.6.1 sql_mode 常用的值

ONLY_FULL_GROUP_BY 对于 GROUP BY 聚合操作, 如果在 SELECT 中的列, 没有在 GROUP BY 中出现, 那么这个 SQL 是不合法的, 因为列不在 GROUP BY 从句中
NO_AUTO_VALUE_ON_ZERO 该值影响自增长列的插入。 默认设置下, 插入 0 或 NULL 代表生成下一个自增 长值。 如果用户 希望插入的值为 0, 而该列又是自增长的, 那么这个选项就有 用了
STRICT_TRANS_TABLES 在该模式下, 如果一个值不能插入到一个事务表中, 则中断当前的操作, 对非 事务表不做限制
NO_ZERO_IN_DATE 在严格模式下, 不允许日期和月份为零
NO_ZERO_DATE 设置该值, mysql 数据库不允许插入零日期, 插入零日期会抛出错误而不是警告
ERROR_FOR_DIVISION_BY_ZERO 在 INSERT 或 UPDATE 过程中, 如果数据被零除, 则产生错误而非警告。 如 果 未给出该模式, 那么数据被零除时 MySQL 返回 NULL
NO_AUTO_CREATE_USER 禁止 GRANT 创建密码为空的用户
NO_ENGINE_SUBSTITUTION 如果需要的存储引擎被禁用或未编译, 那么抛出错误。 不设置此值时, 用默认 的存储引擎替代, 并抛出一个异常
PIPES_AS_CONCAT 将"||"视为字符串的连接操作符而非或运算符, 这和 Oracle 数据库是一样的, 也和字符串的拼接函数 Concat 相类似
ANSI_QUOTES 启用 ANSI_QUOTES 后, 不能用双引号来引用字符串, 因为它被解释为识别符

2.6.2 查看和修改
①查看当前的 sql_mode: select @@sql_mode;
②sql_mode 的影响案例: group by 查询语法错误!

CREATE TABLE mytbl2 (id INT,NAME VARCHAR(200),age INT,dept INT);
INSERT INTO mytbl2 VALUES(1,'zhang3',33,101);
INSERT INTO mytbl2 VALUES(2,'li4',34,101);
INSERT INTO mytbl2 VALUES(3,'wang5',34,102);
INSERT INTO mytbl2 VALUES(4,'zhao6',34,102);
INSERT INTO mytbl2 VALUES(5,'tian7',36,102);

查询每个 dept 中年龄最大的人:SELECT NAME,dept,MAX(age) FROM mytbl2 GROUP BY dept;

正 确 写 法 : SELECT id,name,ab.dept,ab.maxage FROM mytbl2 m INNER JOIN(SELECT dept,MAX(age)maxage FROM mytbl2
GROUP BY dept)ab ON ab.dept=m.dept AND m.age=ab.maxage;

③临时修改 sql_mode: set @@sql_mode=’’;
④永久修改, 需要在配置文件 my.cnf 中修改:
[mysqld] 下添加 sql_mode=’’ 然后重启 mysql 即可

MySQL的语法规范

1.不区分大小写,但建议关键字大写,表名、列名小写
2.每条命令最好用分号结尾(或\g结尾)
3.每条命令根据需要,可以进行缩进 或换行。各子句一般分行写
4.注释
	单行注释:#注释文字
	单行注释:-- 注释文字(要有空格)
	多行注释:/* 注释文字  */

SQL的语言分类

DQL(Data Query Language):数据查询语言
	select 
DML(Data Manipulate Language):数据操作语言
	insert 、update、delete
DDL(Data Define Languge):数据定义语言
	create、drop、alter
TCL(Transaction Control Language):事务控制语言
	commit、rollback

DML语言

DML(Data Manipulate Language):数据操作语言
insert 、update、delete

一千行命令:https://shockerli.net/post/1000-line-mysql-note/

tee D:\HHH.SQL

insert into customer(id,name,age) values('110','TOM',20);  #注意:插入 varchar 或 date 型的数据要用 单引号 引起来 

update customer
set age=22,birthday='1999-01-02' 
where id='110'

delete from customer
where id='110';

select * from table1 where or/ and />/ between and /age in(10,15,16) /name like '%om%' /salary is null / is not null
    CREATE TABLE stu(
        id INT PRIMARY KEY AUTO_INCREMENT comment '这是注释',
        sname VARCHAR(10) NOT NULL DEFAULT '',
        gender CHAR(1) NOT NULL DEFAULT '',
        company VARCHAR(20) NOT NULL DEFAULT '',
        salary DECIMAL(6.2) NOT NULL DEFAULT 8000.00,
        fanbu SMALLINT NOT NULL DEFAULT 0
    )ENGINE MYISAM CHARSET utf8;
    desc stu;

行插入insert

insert into stu表名
(字段名,...)
values
(,..),
(,..);#插入多行

方式2:
insert into 表名 set 字段=值,字段=值,...;

特点:

1、字段类型和值类型一致或兼容,而且一一对应
2、可以为空的字段,可以不用插入值,或用null填充。可以通过以下两种方式插入null值
①字段和值都省略
②字段写上,值使用null
3、不可以为空的字段,必须插入值
4、字段个数和值的个数必须一致
5、字段可以省略,但默认所有字段,并且顺序和表中的存储顺序一致

行修改update

修改单表语法:

update stu表名 
set 
gender='女',fanbu=200
【where 条件】//不加where全改

修改多表语法:

update 表1 别名1,表2 别名2
set 
字段=新值,字段=新值
where 
连接条件 and 筛选条件

update 表1 别名 
left|right|inner join 表2 别名 
on 连接条件  
set 字段=值,字段=值 
【where 筛选条件】;

行删除delete

删除就是删除一行,不存在删除某几列

删除表用drop

方式1:delete语句

# 单表的删除: ★
delete from 表名 
【where 筛选条件】

#多表的删除:
delete 别名1,别名2
from 表1 别名1,表2 别名2
where 连接条件
and 筛选条件;

delete 别名1,别名2 from 表1 别名 
inner|left|right join 表2 别名 
on 连接条件
 【where 筛选条件】

方式2:truncate语句 // 使成平面

truncate table 表名stu
where

两种方式的区别【面试题】

# 1.truncate不能加where筛选条件,而delete可以加where条件
#2.truncate的效率高一丢丢

#3.truncate 删除带自增长的列的表后,如果再插入数据,数据从【1】开始
#delete 删除带自增长列的表后,如果再插入数据,数据从上一次的【断点】处开始

#4.truncate删除不能回滚,delete删除可以回滚

#5.truncate没有返回值,delete可以返回受影响的行数

DDL语句

DDL(Data Define Languge):数据定义语言
create、drop、alter

库修改

#查看当前所有的数据库
show databases [ LIKE 'PATTERN'];
#不能动的库:information_schema、mysql、performance_schema
SELECT DATABASE(); # 查看当前数据库

# 查看所有存储引擎
show engines; #  MySQL 当前默认的存储引擎是InnoDB,并且在5.7版本所有的存储引擎中只有 InnoDB 是事务性存储引擎,也就是说只有 InnoDB 支持事务。

# 查看默认的存储引擎。
show variables like '%storage_engine%';

# 查看表的存储引擎
show table status like "table_name" ;

# 2.打开指定的库
use 库名

# 3.查看当前库的所有表
show tables;

# 查看建表语句
SHOW CREATE DATABASE 数据库名

# 4.查看其它库的所有表
show tables from 库名;

# 删除表
drop table 表名

# 6.查看表结构
desc 表名;

-- 显示当前时间、用户名、数据库版本
    SELECT now(), user(), version();
-- 创建库
    CREATE DATABASE[ IF NOT EXISTS] 数据库名 数据库选项
    数据库选项:
        CHARACTER SET charset_name
        COLLATE collation_name
-- 修改库的选项信息
    ALTER DATABASE 库名 选项信息
-- 删除库
    DROP DATABASE[ IF EXISTS] 数据库名
        同时删除该数据库相关的目录及其目录内容


一、创建库
create database [if not exists] 库名 character set 字符集名utf8;
如 create database 数据库名 default character set utf8;
二、删除库
drop database 库名
alter database 库名 character set 字符集名; 
alter table table1 convert to character set 字符集名; 
# 改名也可以在windows中直接改,改签需要net stop 名字 

查看MySQL提供的所有存储引擎

表修改

1.创建表

CREATE TABLE IF NOT EXISTS stuinfo(
	stuId INT,
	stuName VARCHAR(20),
	gender CHAR,
	birthday DATETIME
);

【主键约束primary key】
-- ,多个主键为可以最后再写primary key(id,name),联合主键只要有一个不同就ok,完全一样才报错。unique也同理
【自增约束auto_increment】
能够唯一确定他一张表中的一条记录,也就是我们通过给某个字段添加约束,就可以使得该字段不重复且不为空。因为能确定,反之每张表也应有主键,所以主键不能为空
后续补充可用 alter table 表名 add primary(id)

外键,唯一,非空,默认约束
Alter table 表明 drop primary key删除主键
【唯一约束unique】

DESC studentinfo;


ENGINE=InnoDB DEFAULT CHARSET=utf8


-- 创建表
    CREATE [TEMPORARY] TABLE[ IF NOT EXISTS] [库名.]表名 ( 表的结构定义 )[ 表选项]
        -- 每个字段必须有数据类型
        -- 最后一个字段后不能有逗号
        -- TEMPORARY 临时表,会话结束时表自动消失
      -- 对于字段的定义:
         字段名 数据类型 [NOT NULL | NULL] [DEFAULT default_value] [AUTO_INCREMENT] [UNIQUE [KEY] | [PRIMARY] KEY] [COMMENT 'string']
-- 表选项
    -- 字符集
        CHARSET = charset_name
        如果表没有设定,则使用数据库字符集
    -- 存储引擎
        ENGINE = engine_name
        表在管理数据时采用的不同的数据结构,结构不同会导致处理方式、提供的特性操作等不同
        常见的引擎:InnoDB MyISAM Memory/Heap BDB Merge Example CSV MaxDB Archive
        不同的引擎在保存表的结构和数据时采用不同的方式
        MyISAM表文件含义:.frm表定义,.MYD表数据,.MYI表索引
        InnoDB表文件含义:.frm表定义,表空间数据和日志文件
        SHOW ENGINES -- 显示存储引擎的状态信息
        SHOW ENGINE 引擎名 {LOGS|STATUS} -- 显示存储引擎的日志或状态信息
    -- 自增起始数
    	AUTO_INCREMENT = 行数
    -- 数据文件目录
        DATA DIRECTORY = '目录'
    -- 索引文件目录
        INDEX DIRECTORY = '目录'
    -- 表注释
        COMMENT = 'string'
    -- 分区选项
        PARTITION BY ... (详细见手册)

约束可以建表时候添加,可以使用alter …add或drop…,可以使用alter… modify。3种方式都可以。
外键约束。父表子表。主表副表。create的时候name int,foreign key (Name) references class(id)
班级主 学生

建表规范:

/* 建表规范 */ ------------------
    -- Normal Format, NF
        - 每个表保存一个实体信息
        - 每个具有一个ID字段作为主键
        - ID主键 + 原子表
    -- 1NF, 第一范式
        字段不能再分,就满足第一范式。
    -- 2NF, 第二范式
        满足第一范式的前提下,不能出现部分依赖。
        消除复合主键就可以避免部分依赖。增加单列关键字。
    -- 3NF, 第三范式
        满足第二范式的前提下,不能出现传递依赖。
        某个字段依赖于主键,而有其他字段依赖于该字段。这就是传递依赖。
        将一个实体信息的数据放在一个表内实现。

2.修改表 alter

ALTER TABLE 表名 
ADD| MODIFY |DROP| CHANGE 
增列|改列类型 |删列 | 改列名
COLUMN 
字段名 【字段类型】
【first|after 字段名】;;
-- 修改表
    -- 修改表本身的选项
        ALTER TABLE 表名 表的选项
        eg: ALTER TABLE 表名 ENGINE=MYISAM;
    -- 对表进行重命名
        RENAME TABLE 原表名 TO 新表名
        RENAME TABLE 原表名 TO 库名.表名 (可将表移动到另一个数据库)
        -- RENAME可以交换两个表名
    -- 修改表的字段机构(13.1.2. ALTER TABLE语法)
        ALTER TABLE 表名 操作名
        -- 操作名
            ADD[ COLUMN] 字段定义       -- 增加字段
                AFTER 字段名          -- 表示增加在该字段名后面
                FIRST               -- 表示增加在第一个
            ADD PRIMARY KEY(字段名)   -- 创建主键
            ADD UNIQUE [索引名] (字段名)-- 创建唯一索引
            ADD INDEX [索引名] (字段名) -- 创建普通索引
            DROP[ COLUMN] 字段名      -- 删除字段
            MODIFY[ COLUMN] 字段名 字段属性     -- 支持对字段属性进行修改,不能修改字段名(所有原有属性也需写上)
            CHANGE[ COLUMN] 原字段名 新字段名 字段属性      -- 支持对字段名修改
            DROP PRIMARY KEY    -- 删除主键(删除主键前需删除其AUTO_INCREMENT属性)
            DROP INDEX 索引名 -- 删除索引
            DROP FOREIGN KEY 外键    -- 删除外键
  • ①修改字段名
ALTER TABLE studentinfo 
CHANGE  COLUMN 
sex 
gender CHAR;
  • ②修改表名
ALTER TABLE stuinfo 
RENAME [TO]  
studentinfo;#已废弃
  • ③修改字段类型 和列级约束
ALTER TABLE studentinfo 
MODIFY COLUMN 
birthday DATE ;
  • ④添加字段
ALTER TABLE studentinfo 
ADD COLUMN 
email VARCHAR(20) 
first;

/* 数据操作 */ ------------------
-- 增
    INSERT [INTO] 表名 [(字段列表)] VALUES (值列表)[, (值列表), ...]
        -- 如果要插入的值列表包含所有字段并且顺序一致,则可以省略字段列表。
        -- 可同时插入多条数据记录!
        REPLACE 与 INSERT 完全一样,可互换。
    INSERT [INTO] 表名 SET 字段名=值[, 字段名=值, ...]
-- 查
    SELECT 字段列表 FROM 表名[ 其他子句]
        -- 可来自多个表的多个字段
        -- 其他子句可以不使用
        -- 字段列表可以用*代替,表示所有字段
-- 删
    DELETE FROM 表名[ 删除条件子句]
        没有条件子句,则会删除全部
-- 改
    UPDATE 表名 SET 字段名=新值[, 字段名=新值] [更新条件]
  • ⑤删除字段
ALTER TABLE studentinfo 
DROP COLUMN email;

3.删除表

DROP TABLE [IF EXISTS] studentinfo;

4.表的复制

可以跨库,加点即可

# 仅仅复制表的结构
CREATE TABLE copy1
LIKE stu;

#复制表的结构+数据
CREATE TABLE copy2
SELECT * FROM stu;

#只复制部分数据
CREATE TABLE copy2
SELECT id,sname
FROM stu
where nation='中国';

#只复制某些字段
CREATE TABLE copy3;
SELECT id,sname
From Stu
where 0;
-- 查看所有表
    SHOW TABLES[ LIKE 'pattern']
    SHOW TABLES FROM  库名
-- 查看表结构
    SHOW CREATE TABLE 表名 (信息更详细)
    DESC 表名 / DESCRIBE 表名 / EXPLAIN 表名 / SHOW COLUMNS FROM 表名 [LIKE 'PATTERN']
    SHOW TABLE STATUS [FROM db_name] [LIKE 'pattern']
-- 删除表
    DROP TABLE[ IF EXISTS] 表名 ...
-- 清空表数据
    TRUNCATE [TABLE] 表名
-- 复制表结构
    CREATE TABLE 表名 LIKE 要复制的表名
-- 复制表结构和数据
    CREATE TABLE 表名 [AS] SELECT * FROM 要复制的表名
-- 检查表是否有错误
    CHECK TABLE tbl_name [, tbl_name] ... [option] ...
-- 优化表
    OPTIMIZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ...
-- 修复表
    REPAIR [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ... [QUICK] [EXTENDED] [USE_FRM]
-- 分析表
    ANALYZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ...

约束:

  • NOT NULL:非空,用于保证该字段的值不能为空。
    • insert into tab values (null, ‘val’); – 此时表示将第一个字段的值设为null, 取决于该字段是否允许为null
  • DEFAULT:默认值,用于保证该字段有默认值
    • 创建:create table tab ( add_time timestamp default current_timestamp );-- 表示将当前时间的时间戳设为默认值。current_date, current_time
    • insert into tab values (default, ‘val’); – 此时表示强制使用默认值。
  • PRIMARY KEY:主键,用于保证该字段的值具有【唯一性】,一个表侄女有一个主键
    • 声明方式:1 跟在声明的列后 2 在列的最后create table tab ( id int, stu varchar(10), primary key (id));
    • 主键字段非空
    • 可以由多个字段组成create table tab ( id int, stu varchar(10), age int, primary key (stu, age));
  • UNIQUE:唯一,用于保证该字段的值具有唯一性,可以为空(对比主键)
  • AUTO_INCREMENT: 自增约束。自动增长必须为索引(主键或unique)
    • 默认为1开始自动增长。可以通过表属性 auto_increment = x进行设置,或 alter table tbl auto_increment = x;
  • CHECK:检查约束【mysql中不支持】
  • FOREIGN KEY:外键,用于限制两个表的关系,用于保证该字段的值必须来自于主表的关联列的值
    • 在从表添加外键约束,用于【引用主表】中某列的值
    • 比如学生表的专业编号,员工表的部门编号,员工表的工种编号
  • COMMENT:注释。create table tab ( id int comment ‘注释内容’ );
7. FOREIGN KEY 外键约束
    用于限制主表与从表数据完整性。
    alter table t1 add constraint `t1_t2_fk` foreign key (t1_id) references t2(id);
        -- 将表t1的t1_id外键关联到表t2的id字段。
        -- 每个外键都有一个名字,可以通过 constraint 指定
    存在外键的表,称之为从表(子表),外键指向的表,称之为主表(父表)。
    作用:保持数据一致性,完整性,主要目的是控制存储在外键表(从表)中的数据。
    MySQL中,可以对InnoDB引擎使用外键约束:
    语法:
    foreign key (外键字段) references 主表名 (关联字段) [主表记录删除时的动作] [主表记录更新时的动作]
    此时需要检测一个从表的外键需要约束为主表的已存在的值。外键在没有关联的情况下,可以设置为null.前提是该外键列,没有not null。
    可以不指定主表记录更改或更新时的动作,那么此时主表的操作被拒绝。
    如果指定了 on update 或 on delete:在删除或更新时,有如下几个操作可以选择:
    1. cascade,级联操作。主表数据被更新(主键值更新),从表也被更新(外键值更新)。主表记录被删除,从表相关记录也被删除。
    2. set null,设置为null。主表数据被更新(主键值更新),从表的外键被设置为null。主表记录被删除,从表相关记录外键被设置成null。但注意,要求该外键列,没有not null属性约束。
    3. restrict,拒绝父表删除和更新。
    注意,外键只被InnoDB存储引擎所支持。其他引擎是不支持的。
# 外键例子---列级约束例子------
CREATE TABLE stuiInfo(
	id INT PRIMARY KEY,#主键
	stuName VARCHAR(20) NOT NULL UNIQUE,#非空
	gender CHAR(1) CHECK(gender='男' OR gender ='女'),#检查
	seat INT UNIQUE,#唯一
	age INT DEFAULT  18,#默认约束
	majorId INT REFERENCES major(id)#外键-!!!
);

CREATE TABLE major(
	id INT PRIMARY KEY,
	majorName VARCHAR(20)
);

#查看stuinfo中的所有索引,包括主键、外键、唯一
SHOW INDEX FROM stuinfo;
  • 列级约束:六大约束语法上都支持,但外键约束没有效果(外键约束写了也白写)
  • 表级约束:除了非空、默认,其他的都支持
列级约束的例子即上个程序

表级约束语法:在各个字段的最下面
 【constraint 约束名】 约束类型(字段名)
 
# 表级约束例子--------
DROP TABLE IF EXISTS stuinfo;
CREATE TABLE stuinfo(
	id INT,
	stuname VARCHAR(20),
	gender CHAR(1),
	seat INT,
	age INT,
	majorid INT,
	
	CONSTRAINT pk PRIMARY KEY(id),#主键
	CONSTRAINT uq UNIQUE(seat),#唯一键
	CONSTRAINT ck CHECK(gender ='男' OR gender  = '女'),#检查
	CONSTRAINT fk_stuinfo_major FOREIGN KEY(majorid) REFERENCES major(id)#外键
);

SHOW INDEX FROM stuinfo;


#通用的写法:★------------------

CREATE TABLE IF NOT EXISTS stuinfo(
	id INT PRIMARY KEY,
	stuname VARCHAR(20),
	sex CHAR(1),
	age INT DEFAULT 18,
	seat INT UNIQUE,
	majorid INT,
	CONSTRAINT fk_stuinfo_major FOREIGN KEY(majorid) REFERENCES major(id)
);

主键和唯一的大对比:

保证唯一性 是否允许为空 一个表中可以有多少个 是否允许组合
主键 × 至多有1个 √,但不推荐
唯一 可以有多个 √,但不推荐

外键的特点:

  1. 要求在【从表】设置外键关系
  2. 从表的外键列的类型和主表的关联列的类型要求一致或兼容,名称无要求
  3. 【主表的关联列必须是一个key】(一般是主键或唯一)
  4. 插入数据时,先插入主表,再插入从表
    1. 删除数据时,先删除从表,再删除主表

上面的例子是创建表时添加约束,下面还有修改表时添加约束。

添加约束

/*
1、添加列级约束
alter table 表名 modify column 字段名 字段类型 新约束;

2、添加表级约束
alter table 表名 add 【constraint 约束名】 约束类型(字段名) 【外键的引用】;
*/
DROP TABLE IF EXISTS stuinfo;
CREATE TABLE stuinfo(
	id INT,
	stuname VARCHAR(20),
	gender CHAR(1),
	seat INT,
	age INT,
	majorid INT
)
DESC stuinfo;
#1.添加非空约束
ALTER TABLE stuinfo MODIFY COLUMN stuname VARCHAR(20)  NOT NULL;
#2.添加默认约束
ALTER TABLE stuinfo MODIFY COLUMN age INT DEFAULT 18;
#3.添加主键
#①列级约束
ALTER TABLE stuinfo MODIFY COLUMN id INT PRIMARY KEY;
#②表级约束
ALTER TABLE stuinfo ADD PRIMARY KEY(id);

#4.添加唯一
#①列级约束
ALTER TABLE stuinfo MODIFY COLUMN seat INT UNIQUE;
#②表级约束
ALTER TABLE stuinfo ADD UNIQUE(seat);

#5.添加外键
ALTER TABLE stuinfo ADD CONSTRAINT fk_stuinfo_major FOREIGN KEY(majorid) REFERENCES major(id); 

删除约束

1.删除非空约束

ALTER TABLE stuinfo MODIFY COLUMN stuname VARCHAR(20) NULL;

2.删除默认约束

ALTER TABLE stuinfo MODIFY COLUMN age INT ;

3.删除主键

ALTER TABLE stuinfo DROP PRIMARY KEY;

4.删除唯一

ALTER TABLE stuinfo DROP INDEX seat;

5.删除外键

ALTER TABLE stuinfo DROP FOREIGN KEY fk_stuinfo_major;

SHOW INDEX FROM stuinfo;

标识列:

又称自增长列

含义:可以不用手动地插入值,系统提供默认的序列值。

AUTO_INCREMENT

SET AUTO_INCREMENT=2; # 重新设置步长。
需要更改起始值的时候,手动添加一个值即可。
其余时候填入null即可
# 修改表时设置标识列
alter table stu modify column id int primary key auto_increment;
# 修改表时删除标识列
alter table stu modify column id int primary;

问:标识列必须跟主键搭配吗?

答:不一定,但要求是一个key。

问:一个表中可以有多少标识列。

答:一个表中只能有一个标识列。

问:标识列的类型?

答:必须是数值型

数据类型

整型:
默认有符号
TINYINT       8  bit
SMALLINT	  16 -32768 ~ 32767
MEDIUMINT 	  24 -8388608 ~ 8388607
INT 		  32
BIGINT		  64
int(M)    M表示总位数
存储范围总-2^(N-1)到2^(N-1)-1

1表示bool值真,0表示bool值假。MySQL没有布尔类型,通过整型0和1表示。
常用tinyint(1)表示布尔型。
-----------------
符号(默认有符号)
INT UNSIGNED
-----
INT(7) zerofill //会默认添加 unsigned 不够长度用0填充
例:int(5)   插入一个数'123',补填后

可以为整数类型指定宽度,
例如  INT(11)
对大多数应用这是没有意义的:它不会限制值的合法范围,只是规定了Mysql的一些交互工具(例如mysql命令行客户端)用来显示【字符的个数】。对于存储和计算来说,INT(1) 和 INT(20)是相同的。
实数:

分为浮点数和定点数

也可以使用DECIMAL存储比BIGINT还大的整数。

1.浮点数类型:

float(M,D)   32位
double(M,D)  64位
- M 精度,代表总位数(不包括小数点和正负号),也表示显示宽度(所有显示符号均包括)。
- D 标度,代表小数位

浮点型既支持符号位 unsigned 属性,也支持显示宽度 zerofill 属性。
支持科学计数法表示。
不同于整型,前后均会补填0.
可以省略M D,会根据插入数据的精度来决定精度

浮点数多大空间呢?
如果M≤24,占4个字节,否则占8字节。

2.定点数:

定点数是把整数部分和小数部分分开存储的。比float精确

定点数类型:

DEC(M,D) 
DECIMAL(M,D)  -- M也表示总位数,D表示小数位数。
decimal -- 可变长度


保存一个精确的数值,不会发生数据的改变,不同于浮点数的四舍五入。
将浮点数转换为字符串来保存,每9位数字保存为4个字节。

M默认为10 M默认为0

同样存储FLOAT(8.2)和DECIMAL(8.2) ,准确率可能是不一样的。

位类型:Bit(M)

字符串类型:
较短的文本:
VARCHAR 可变长 字符串 
CHAR 定长 字符串  
varchar(M)  M表示能存储的最大长度,此长度是字符数,非字节数。不同的编码,所占用的空间不同。不可以省略M

较长的文本:
text
blob

binary
varbinany

字符串编码问题:

char,最多255个字符,与编码无关。
varchar,最多65535字符,与编码有关。

    一条有效记录最大不能超过65535个字节。
        utf8 最大为21844个字符,gbk 最大为32766个字符,latin1 最大为65532个字符
    varchar 是变长的,需要利用存储空间保存 varchar 的长度,如果数据小于255个字节,则采用一个字节来保存长度,反之需要两个字节来保存。
    varchar 的最大有效长度由最大行大小和使用的字符集确定。
    最大有效长度是65532字节,因为在varchar存字符串时,第一个字节是空的,不存在任何数据,然后还需两个字节来存放字符串的长度,所以有效长度是65535-1-2=65532字节。
    例:若一个表定义为 CREATE TABLE tb(c1 int, c2 char(30), c3 varchar(N)) charset=utf8; 问N的最大值是多少? 答:(65535-1-2-4-30*3)/3
  • ASCII码中,一个英文字母(不分大小写)占一个字节的空间,一个中文汉字占两个字节的空间。一个二进制数字序列,在计算机中作为一个数字单元,一般为8位二进制数,换算为十进制。最小值0,最大值255。
  • UTF-8编码中,一个英文字符等于一个字节,一个中文(含繁体)等于三个字节。
  • Unicode编码中,一个英文等于两个字节,一个中文(含繁体)等于两个字节。

varchar(M)

存储可变长 字符串,比定长类型更节省空间。因为仅使用必要的空间。VARCHAR使用1或2个额外字节记录字符串的长度(列前?)。如过最大长度小于255,那么就置使用1字节,否则使用2字节。但是在UPDATE时可能使行变得比原来更长,这就导致需要做额外的工作。(MYISAM会将行拆封不同的片段存储,InnoDB则需要分裂页来使行可以放进页内。)

M可取 65535

M限制的是字符,不是字节。比如char(2) charset utf8,能存2个utf8字符,比如“中国”,而占2×3个字节

  • CHAR

当存储CHAR值时,MYSQL会删除所有的末尾空格。

不够M字符时,内部用空格补齐,取出时再把右侧空格删掉。char值会根据需要采用空格进行填充以方便比较。

空格填充是(右侧空格)

M可取 255

默认M=1

适合存储很短的字符串,如密码的MD5值

与CHAR和VARCHAR类似的类型还有BINARY和VARBINARY。填充使用的是\0,而不是空格

  • TEXT

文本类型,可以存比较大的文本段,搜索速度稍慢。

声明text列时,不必给默认值。

因此,如果不是特别大的内容,建议用char varchar来代替

TINYTEXT SMALLTEXT TEXT MEDIUMTEXT LONGTEXT
对应的二进制类型是
TINYBLOB SMALLBLOB BLOB MEDIUMBLOB LONGBLOB
  • BLOB

二进制类型,用来存储图像,音频等二进制信息。

意义:2进制,0-255都可能出现。

blob在于防止因为字符集的问题,导致信息丢失。

比如,一张图片中有0xFF字节,这个在ascii字符集认为非法,在入库的时候,被过滤了

-- b. blob, text ----------
    blob 二进制字符串(字节字符串)
        tinyblob, blob, mediumblob, longblob
    text 非二进制字符串(字符字符串)
        tinytext, text, mediumtext, longtext
    text 在定义时,不需要定义长度,也不会计算总长度。
    text 类型在定义时,不可给default值

binary

-- c. binary, varbinary ----------
    类似于char和varchar,用于保存二进制字符串,也就是保存字节字符串而非字符字符串。
    char, varchar, text 对应 binary, varbinary, blob.

ENUM

Mysql会根据列表值的数量压缩到一个或两个字节中。在内部会将每个值在列表中的位置保存为整数,并且在表的.frm文件中保存“数字-字符串"映射关系的查找表,

enum(val1, val2, val3...)
    在已知的值中进行单选。最大数量为65535.
    枚举值在保存时,以2个字节的整型(smallint)保存。每个枚举值,按保存的位置顺序,从1开始逐一递增。
    表现为字符串类型,存储却是整型。
    NULL值的索引是NULL。
    空字符串错误值的索引值是0。
Set类型

Set类型一次可以选取多个成员,而Enum只能选1个。

INsert INTO stu
ZIMU
VALUEs
('A','C')

set(val1, val2, val3...)
    create table tab ( gender set('男', '女', '无') );
    insert into tab values ('男, 女');
    最多可以有64个不同的成员。以bigint存储,共8个字节。采取位运算的形式。
    当创建表时,SET成员值的尾部空格将自动被删除。
日期和时间

必须用单引号引起来

DATE	   3B
TIME	   3B
DATETIME   8B
TIMESTAMP  4B (常用)保存自1970-1-1以来的秒数,只能保存到2038年。
NOW()

3. 日期时间类型
    一般用整型保存时间戳,因为PHP可以很方便的将时间戳进行格式化。
    datetime    8字节    日期及时间     1000-01-01 00:00:00 到 9999-12-31 23:59:59
    date        3字节    日期         1000-01-01 到 9999-12-31
    timestamp   4字节    时间戳        19700101000000 到 2038-01-19 03:14:07
    time        3字节    时间         -838:59:59 到 838:59:59
    year        1字节    年份         1901 - 2155
datetime    YYYY-MM-DD hh:mm:ss
timestamp   YY-MM-DD hh:mm:ss
            YYYYMMDDhhmmss
            YYMMDDhhmmss
            YYYYMMDDhhmmss
            YYMMDDhhmmss
date        YYYY-MM-DD
            YY-MM-DD
            YYYYMMDD
            YYMMDD
            YYYYMMDD
            YYMMDD
time        hh:mm:ss
            hhmmss
            hhmmss
year        YYYY
            YY
            YYYY
            YY

TIMESTAMP和实际时区有关,更能反映实际的日期,而datetime则只能反映出插入时的当地时区,timestamp的属性受mysql版本和sqlmode的影响很大。

BIT

最大存储64个位,如BIT(2)表示存储2个位。InnoDB为每个BIT列使用一个足够存储的最小整数类型来存放,所以不能节省存储空间。

mysql把BIT当做字符串类型,而不是数字类型。当检索BIT(1)的值时,结果是一个包含二进制0或1的字符串,而不是ASCII码的0或1。然而,在数字上下文的场景中检索时,结果将是位字符串转换成的数字。如果需要和另外的值进行比较,一定要记得这一点。比如存储一个值b’00111001’(二进制等于57)到BIT(8)的列并且检索他,得到的内容是字符码为57的字符串。也就说,得到ASCII码为57的’9‘,但在数字上下文场景中,得到的是数字57

create table test(a bit(8));
insert into test values(b'00111001');
select a,a+0 from test;
a  a+0
9   57

DQL语言

查哪些列 查哪张表 查哪些行

DQL(Data Query Language):数据查询语言
select

顺序问题

写的顺序:

SELECT [ALL|DISTINCT] 要查询的字段|表达式select_expr|常量值|函数 
FROM ->
WHERE -> 
GROUP BY [合计函数] -> 
HAVING -> 
ORDER BY -> 
LIMIT
顺序的记法:LIMIT最后,HAVING在两个by中间

FROM

用于标识查询来源。
    -- 可以为表起别名。使用as关键字。
        SELECT * FROM tb1 AS tt, tb2 AS bb;
    -- from子句后,可以同时出现多个表。
        -- 多个表会横向叠加到一起,而数据会形成一个笛卡尔积。
        SELECT * FROM tb1, tb2;
    -- 向优化符提示如何选择索引
        USE INDEX、IGNORE INDEX、FORCE INDEX
        SELECT * FROM table1 USE INDEX (key1,key2) WHERE key1=1 AND key2=2 AND key3=3;
        SELECT * FROM table1 IGNORE INDEX (key3) WHERE key1=1 AND key2=2 AND key3=3;
c. WHERE 子句
    -- 从from获得的数据源中进行筛选。
    -- 整型1表示真,0表示假。
    -- 表达式由运算符和运算数组成。
        -- 运算数:变量(字段)、值、函数返回值
        -- 运算符:
            =, <=>, <>, !=, <=, <, >=, >, !, &&, ||,
            in (not) null, (not) like, (not) in, (not) between and, is (not), and, or, not, xor
            is/is not 加上ture/false/unknown,检验某个值的真假
            <=>与<>功能相同,<=>可用于null比较
            
d. GROUP BY 子句, 分组子句
    GROUP BY 字段/别名 [排序方式]
    分组后会进行排序。升序:ASC,降序:DESC
    
    以下[合计函数]需配合 GROUP BY 使用:
    count 返回不同的非NULL值数目  count(*)、count(字段)
    sum 求和
    max 求最大值
    min 求最小值
    avg 求平均值
    group_concat 返回带有来自一个组的连接的非NULL值的字符串结果。组内字符串连接。
    
e. HAVING 子句,条件子句
    与 where 功能、用法相同,执行时机不同。
    where 在开始时执行检测数据,对原数据进行过滤。
    having 对筛选出的结果再次进行过滤。
    having 字段必须是查询出来的,where 字段必须是数据表存在的。
    where 不可以使用字段的别名,having 可以。因为执行WHERE代码时,可能尚未确定列值。
    where 不可以使用合计函数。一般需用合计函数才会用 having
    SQL标准要求HAVING必须引用GROUP BY子句中的列或用于合计函数中的列。
    
f. ORDER BY 子句,排序子句
    order by 排序字段/别名 排序方式 [,排序字段/别名 排序方式]...
    升序:ASC,降序:DESC
    支持多个字段的排序。
    
g. LIMIT 子句,限制结果数量子句
    仅对处理好的结果进行数量限制。将处理好的结果的看作是一个集合,按照记录出现的顺序,索引从0开始。
    limit 起始位置, 获取条数
    省略第一个参数,表示从索引0开始。limit 获取条数
    
h. DISTINCT, ALL 选项
    distinct 去除重复记录
    默认为 all, 全部记录

select

①通过select查询完的结果 ,是一个虚拟的表格,不是真实存在
② 要查询的东西 可以是常量值、可以是表达式、可以是字段、可以是函数

数值型和日期型的常量值必须用单引号引起来,数值型不需要

-- 可以用 * 表示所有字段。
        select * from tb;
-- 可以使用表达式(计算公式、函数调用、字段也是个表达式)
        select stu, 29+25, now() from tb;
-- 可以为每个列使用别名。适用于简化列标识,避免多个列标识符重复。
        - 使用 as 关键字,也可省略 as.
        select stu+10 as add10 from tb;
#一、字符函数

#1.l lenth 获取参数值的字节个数
SELECT LENGTH('john');
SELECT LENGTH('张三丰hahaha');

SHOW VARIABLES LIKE '%char%'

#2.concat 拼接字符串
SELECT CONCAT(last_name,'_',first_name) 姓名 FROM employees;

#3.upper、lower
SELECT UPPER('john');
SELECT LOWER('joHn');
#示例:将姓变大写,名变小写,然后拼接
SELECT CONCAT(UPPER(last_name),LOWER(first_name))  姓名 FROM employees;

#4.substr、substring 注意:索引从1开始
#截取从指定索引处后面所有字符
SELECT SUBSTR('李莫愁爱上了陆展元',7)  out_put;

#截取从指定索引处指定字符长度的字符
SELECT SUBSTR('李莫愁爱上了陆展元',1,3) out_put;

#案例:姓名中首字符大写,其他字符小写然后用_拼接,显示出来
SELECT CONCAT(UPPER(SUBSTR(last_name,1,1)),'_',LOWER(SUBSTR(last_name,2)))  out_put
FROM employees;

#5.instr 返回子串第一次出现的索引,如果找不到返回0
SELECT INSTR('杨不殷六侠悔爱上了殷六侠','殷八䩠 'AS out_put;

#6.trim去前后空格
SELECT LENGTH(TRIM('    张翠山    ')) AS out_put;
SELECT TRIM('aa' FROM 'aaaa张aaa翠山aaaaa')  AS out_put;

#7.lpad 用指定的字符实现左填充指定长度
SELECT LPAD('殷素素',2,'*') AS out_put;

#8.rpad 用指定的字符实现右填充指定长度
SELECT RPAD('殷素素',12,'ab') AS out_put;


#9.replace 替换
SELECT REPLACE('周芷若周芷若周芷若周芷若张无忌爱上了周芷若','周芷若','赵敏') AS out_put;
             
#--------------------------------------------------------------
#二、数学函数

#round 四舍五入
SELECT ROUND(-1.55);
SELECT ROUND(1.567,2);


#ceil 向上取整,返回>=该参数的最小整数
SELECT CEIL(-1.02);

#floor 向下取整,返回<=该参数的最大整数
SELECT FLOOR(-9.99);

#truncate 截断
SELECT TRUNCATE(1.69999,1);

#mod取余
/*
mod(a,b) :  a-a/b*b

mod(-10,-3):-10- (-10)/(-3)*(-3)=-1
*/
SELECT MOD(10,-3);
SELECT 10%3;


#三、日期函数

#now 返回当前系统旟+时间
SELECT NOW();

#curdate 返回当前系统日期,不包含时间
SELECT CURDATE();

#curtime 返回当前时间,不包含日期
SELECT CURTIME();


#可以获取指定的部分,年、月、日、小时、分钟、秒
SELECT YEAR(NOW()) 年;
SELECT YEAR('1998-1-1') 年;

SELECT  YEAR(hiredate) 年 FROM employees;

SELECT MONTH(NOW()) 月;
SELECT MONTHNAME(NOW()) 月;

#str_to_date 将字符通过指定的格式转换成日期,中间的符号要一致
SELECT STR_TO_DATE('1998-3-2','%Y-%c-%d') AS out_put;

#查询入职日期为1992--4-3的员工信息
SELECT * FROM employees WHERE hiredate = '1992-4-3';
或
SELECT * FROM employees WHERE hiredate = STR_TO_DATE('4-3 1992','%c-%d %Y');
%Y四位的年份 %y二位的年份 %m二位的月份 %c月份 %d二位的日 %H二十四制小时 %h十二制小时 %i二位制分钟 %s二位制秒

#date_format 将日期转换成字符
SELECT DATE_FORMAT(NOW(),'%Y年%m月%d日') AS out_put;

#查询有奖金的员工名和入职日期(xx月/xx日 xx年)
SELECT last_name,DATE_FORMAT(hiredate,'%m月/%d日 %Y年') 入职日期
FROM employees
WHERE commission_pct IS NOT NULL;


#四、其他函敍
SELECT VERSION();
SELECT DATABASE();
SELECT USER();
一、单行函数
1、字符函数
	length长度,中文是3B
	concat()拼接
	substr截取子串(字符串,开始位置从1开始,长度中文也是1个)
	instr 返回子串第一次出现的索引,如果找不到返回0
	upper转换成大写
	lower转换成小写
	trim去前后指定的空格和字符(字符串,去除内容' ')
	ltrim去左边空格
	rtrim去右边空格
	replace替换(字符串,被替换内容,用什么替换)
	lpad左填充(字符串,填充后的长度,填空内容'')
	rpad右填充
	instr返回子串第一次出现的索引
	length 获取字节个数
	
CONCAT('Hello', 'World') HelloWorld
SUBSTR('HelloWorld',1,5) Hello
LENGTH('HelloWorld') 10
INSTR('HelloWorld', 'W') 6
LPAD(salary,10,'*')  *****24000
RPAD(salary, 10, '*')  24000*****
TRIM('H' FROM 'HelloWorld')  elloWorld
REPLACE('abcd','b','m')  amcd


2、数学函数
	round 四舍五入(数,小数点后保留位数=0)
	rand 随机数
	floor向下取整(小于等于)
	ceil向上取整(大于等于)
	mod取余MOD(10.3)
	truncate截断
3、日期函数
	now()当前系统日期+时间
	curdate()当前系统日期
	curtime()当前系统时间
	str_to_date(,) 将字符转换成日期
	date_format(,)将日期转换成字符

where

where分类:
一、条件表达式
	示例:salary>10000
	条件运算符:
	> < >= <= = != <>不等于,注意等于是=

二、逻辑表达式
示例:salary>10000 && salary<20000
逻辑运算符:
	and(&&):两个条件如果同时成立,结果为true,否则为false
	or(||):两个条件只要有一个成立,结果为true,否则为false
	not(!):如果条件成立,则not后为false,否则为true
# SELECT * FROM employees WHERE name LIKE '%a%' OR salary>100;

三、模糊查询
like
示例:last_name like 'a%'
# SELECT * FROM employees WHERE last_name LIKE '%a%';%代表通配符
通配符:
%任意多个字符
_任一个字符,可以用转义符

between and
SELECT * FROM `employees` WHERE `employee_id` BETWEEN 100小值 AND 120大值;不可以换顺序

in//NOT IN
SELECT * FROM `employees` WHERE job_id IN ['AD_VP','AD_PRES']不可以写通配符,可以写()


SELECT `last_name`,`commission_pct` FROM employees WHERE `commission_pct` IS NOT NULL;等号不能用于判断null值
SELECT `last_name`,`commission_pct` FROM employees WHERE `commission_pct` <=> 120;安全等于。即可以判断NULL,又可以判断数值

group

分组函数作用于一组数据,并每组数据返回一个值。

select 分组函数
from 表
where 条件
group by 
order by 排序的字段|表达式|函数|别名 【asc|desc】

分组函数:
• AVG()
• COUNT(expr) -- 计数,返回expr不为空的记录总数。,适用于任意数据类型。// select count(*) from customer where id=110;
• MAX()
• MIN()
• SUM()

# 在SELECT 列表中所有未包含在组函数中的列都应该包含在 GROUP BY 子句中。
SELECT ID,AVG(salary)
FROM employees
GROUP BY ID;
#包含在 GROUP BY 子句中的列不必包含在SELECT 列表中
SELECT AVG(salary)
FROM employees
GROUP BY ID;

• 不能在 WHERE 子句中使用组函数。
• 可以在 HAVING 子句中使用组函数。

分组前筛选: 原始表 group by的前面 where
分组后筛选: 分组后的结果集 group by的后面 having

	sum 求和
	max 最大值(可以算字母顺序,日期顺序)
	min 最小值
	avg 平均值
	count 计数//非空的个数

	特点:
	1、以上五个分组函数都忽略null值,avg也只计算非null,除了count(*)
	2、sum和avg一般用于处理数值型
		max、min、count可以处理任何数据类型
    3、都可以搭配distinct使用,用于统计去重后的结果
	4、count的参数可以支持:
		字段、*、常量值,一般放1

	   建议使用 count(*)统计行数,括号内任意数字或任意常量也可以统计行数
	   
select avg(score) from stu where class='1';
select avg(score) from stu group by class;

#1.查询各job_id的员工工资的最大值,最小值,平均值,总和,并按job_id升序
SELECT MAX(salary),MIN(salary),AVG(salary),SUM(salary),job_id
FROM employees
GROUP BY job_id
ORDER BY job_id;


#2.查询员工最高工资和最低工资的差距(DIFFERENCE)
SELECT MAX(salary)-MIN(salary) DIFFRENCE
FROM employees;
#3.查询各个管理者手下员工的最低工资,其中最低工资不能低于6000,没有管理者的员工不计算在内
SELECT MIN(salary),manager_id
FROM employees
WHERE manager_id IS NOT NULL
GROUP BY manager_id
HAVING MIN(salary)>=6000;

#4.查询所有部门的编号,员工数量和工资平均值,并按平均工资降序
SELECT department_id,COUNT(*),AVG(salary) a
FROM employees
GROUP BY department_id
ORDER BY a DESC;
#5.选择具有各个job_id的员工人数
SELECT COUNT(*) 个数,job_id
FROM employees
GROUP BY job_id;

having

使用 HAVING 过滤分组前提:

  1. 行已经被分组。
  2. 使用了组函数。
  3. 满足HAVING 子句中条件的分组将被显示。

不可以使用having的情况:筛选条件的列没有出现在查询select查询字段中

having的原理是先select 然后从select出来的进行筛选。而where是先筛选在select。

SELECT region, SUM(population), SUM(area)
FROM bbc
GROUP BY region
HAVING SUM(area)>1000 -- 在这里,我们不能用where来筛选超过1000的地区,因为表中不存在这样一条记录。

select sum(score)
from student  
where sex='man'
group by name
having sum(score)>210

having和where的区别

  • having与 where 功能、用法相同,执行时机不同。
  • where 在开始时执行检测数据,对原数据进行过滤。
  • having 对筛选出的结果再次进行过滤。
  • having 字段必须是查询出来的,where 字段必须是数据表存在的。
  • where 不可以使用字段的别名,having 可以。因为执行WHERE代码时,可能尚未确定列值。
  • where 不可以使用聚合函数。一般需用聚合函数才会用 having,where执行顺序大于聚合函数
  • where 子句的作用是在对查询结果进行分组前,将不符合where条件的行去掉,即在分组之前过滤数据,条件中不能包含聚组函数,使用where条件显示特定的行。
  • SQL标准要求HAVING必须引用GROUP BY子句中的列或用于合计函数中的列
  • 聚合函数是比较where、having 的关键。若须引入聚合函数来对group by 结果进行过滤 则只能用having
  • having 子句的作用是筛选满足条件的组,即在分组之后过滤数据,条件中经常包含聚组函数,使用having 条件显示特定的组,也可以使用多个分组标准进行分组。

执行顺序:

where >>> 聚合函数(sum,min,max,avg,count) >>> having

order

select 要查询的东西
from 表
where 条件

order by 排序的字段|表达式|函数|别名 【asc|desc】
/*
- ORDER BY 子句在SELECT语句的【结尾】。 

- 【asc|desc】为升降序
- 多个顺序时,写多个即可,逗号分隔
*/

执行顺序:from 、join、 on、 where 、
、group by(开始使用select中的别名,后面的语句中都可以使用)
、avg,sum… having、 select 、distinct 、order by、limit

limit

-- 分页查询

select 字段|表达式,...
from 表
【where 条件】
【group by 分组字段】
【having 条件】
【order by 排序的字段】
limit 【起始的条目索引,最大条目数】;-- 0开始  -- 第二个参数为-1代表打印后面所有
1.起始条目索引从0开始

2.limit子句放在查询语句的最后

3.公式:select * from  表 limit (page-1)*sizePerPage,sizePerPage
假如:
每页显示条目数sizePerPage
要显示的页数 page

- select sname from score where degree=(select max(degree) from score);
这句的启发是:括号里的select的结果是一个值,这样degree=值就是一个where语句,缺点是有可能返回多个等大最大值。即先找到最大值再用where
- 可以用limit实现查取某列第一大的值
select name from stu order by degree limit 0,1; // 从0开始一个值

if case when

if 处理双分支
case语句 处理多分支
	when情况1  then处理等值判断
    when情况2  then处理条件判断
    ELSE
    END
#五、流程控制函数
#1.if函数: if else 的效果IF(表达式,真的话,假的话)

SELECT IF(10<5,'大','小');
SELECT last_name,commission_pct,IF(commission_pct IS NULL,'没奖金,呵呵','有奖金,嘻嘻') 备注
FROM employees;


#2.case函数的使用一: switch case 的效果
/*
java中
switch(变量或表达式){
	case 常量1:语句1;break;
	...
	default:语句n;break;
}
mysql中:
case 要判断的字段或表达式
when 常量1 then 要显示的值1或语句1;
when 常量2 then 要显示的值2或语句2;
...
else 要显示的值n或语句n;
end
*/

/*案例:查询员工的工资,要求
部门号=30,显示的工资为1.1倍
部门号=40,显示的工资为1.2倍
部门号=50,显示的工资为1.3倍
其他部门,显示的工资为原工资
*/
SELECT salary 原始工资,department_id,
CASE department_id
WHEN 30 THEN salary*1.1
WHEN 40 THEN salary*1.2
WHEN 50 THEN salary*1.3
ELSE salary 
END AS 新工资
FROM employees;

#3.case 函数的使用二:类似于 多重if
/*
mysql中:
case[case后没有语句直接when]
when 条件1 then 要显示的值1或语句1
when 条件2 then 要显示的值2或语句2
。。。
else 要显示的值n或语句n
end
*/

#案例:查询员工的工资的情况
如果工资>20000,显示A级别
如果工资>15000,显示B级别
如果工资>10000,显示C级别
否则,显示D级别


SELECT salary,
CASE 
WHEN salary>20000 THEN 'A'
WHEN salary>15000 THEN 'B'
WHEN salary>10000 THEN 'C'
ELSE 'D'
END AS 工资级别
FROM employees;

join

等值连接、非等值连接 (内连接)
外连接
交叉连接

语法:

select 字段,...
from 表1
【inner|left outer|right outer|cross】join 表2 on  连接条件
【inner|left outer|right outer|cross】join 表3 on  连接条件
【where 筛选条件】
【group by 分组字段】
【having 分组后的筛选条件】
【order by 排序的字段或表达式】

/* 连接查询(join) */ ------------------
    将多个表的字段进行连接,可以指定连接条件。
-- 内连接(inner join)
    - 默认就是内连接,可省略inner。
    - 只有数据存在时才能发送连接。即连接结果不能出现空行。
    on 表示连接条件。其条件表达式与where类似。也可以省略条件(表示条件永远为真)
    也可用where表示连接条件。
    还有 using, 但需字段名相同。 using(字段名)
    -- 交叉连接 cross join
        即,没有条件的内连接。
        select * from tb1 cross join tb2;
-- 外连接(outer join)
    - 如果数据不存在,也会出现在连接结果中。
    -- 左外连接 left join
        如果数据不存在,左表记录会出现,而右表为null填充
    -- 右外连接 right join
        如果数据不存在,右表记录会出现,而左表为null填充
-- 自然连接(natural join)
    自动判断连接条件完成连接。
    相当于省略了using,会自动查找相同字段名。
    natural join
    natural left join
    natural right join
select info.id, info.name, info.stu_num, extra_info.hobby, extra_info.sex from info, extra_info where info.stu_num = extra_info.stu_id;

在使用left join时,on和where条件的区别如下:

1、on条件是在生成临时表时使用的条件,它不管on中的条件是否为真,都会返回左边表中的记录。

2、where条件是在临时表生成好后,再对临时表进行过滤的条件。这时已经没有left join的含义(必须返回左边表的记录)了,条件不为真的就全部过滤掉

先进行on过滤,后进行where过滤

使用left join时on后面的条件只对右表有效

假设有两张表:

表1:tab2

id size
1 10
2 20
3 30

表2:tab2

size name
10 AAA
20 BBB
20 CCC

两条SQL:

1、select * form tab1 left join tab2 on (tab1.size = tab2.size) where tab2.name=’AAA’
2、select * form tab1 left join tab2 on (tab1.size = tab2.size and tab2.name=’AAA’)

1、第一条SQL的过程:select * form tab1 left join tab2 on (tab1.size = tab2.size) where tab2.name=’AAA’

1.1、中间表
on条件: tab1.size = tab2.size

ab1.id tab1.size tab2.size tab2.name
1 10 10 AAA
2 20 20 BBB
2 20 20 CCC
3 30 (null) (null)

1.2、再对中间表过滤
where 条件:tab2.name=’AAA’

tab1.id tab1.size tab2.size tab2.name
1 10 10 AAA

2、第二条SQL的过程:select * form tab1 left join tab2 on (tab1.size = tab2.size and tab2.name=’AAA’)

tab1.id tab1.size tab2.size tab2.name
1 10 10 AAA
2 20 (null) (null)
3 30 (null) (null)

其实以上结果的关键原因就是left join,right join,full join的特殊性,不管on上的条件是否为真都会返回left或right表中的记录,full则具有left和right的特性的并集。 而inner jion没这个特殊性,则条件放在on中和where中,返回的结果集是相同的。

子查询

/* 子查询 */ ------------------
    - 子查询需用括号包裹。
-- from型
    from后要求是一个表,必须给子查询结果取个别名。
    - 简化每个查询内的条件。
    - from型需将结果生成一个临时表格,可用以原表的锁定的释放。
    - 子查询返回一个表,表型子查询。
    select * from (select * from tb where id>0) as subfrom where id>1;
-- where型
    - 子查询返回一个值,标量子查询。
    - 不需要给子查询取别名。
    - where子查询内的表,不能直接用以更新。
    select * from tb where money = (select max(money) from tb);
    -- 列子查询
        如果子查询结果返回的是一列。
        使用 in 或 not in 完成查询
        exists 和 not exists 条件
            如果子查询返回数据,则返回1或0。常用于判断条件。
            select column1 from t1 where exists (select * from t2);
    -- 行子查询
        查询条件是一个行。
        select * from t1 where (id, gender) in (select id, gender from t2);
        行构造符:(col1, col2, ...) 或 ROW(col1, col2, ...)
        行构造符通常用于与对能返回两个或两个以上列的子查询进行比较。
    -- 特殊运算符
    != all()    相当于 not in
    = some()    相当于 in。any 是 some 的别名
    != some()   不等同于 not in,不等于其中某一个。
    all, some 可以配合其他运算符一起使用。

子查询:内层被嵌套的select

  • 子查询都放在小括号内
  • 子查询可以放在from后面、select后面、where后面、having后面,但一般放在条件的右侧
  • 查询结果:
    • 结果集只有一行:一般搭配单行操作符使用:> < = <> >= <=
    • 结果集有多行:一般搭配多行操作符使用:any、all、in、not in

主查询:外层的select

union

1、多条查询语句的查询的列数必须是一致的
2、多条查询语句的查询的列的类型几乎相同
3、union代表去重,union all代表不去重

select 字段|常量|表达式|函数 【from 表】 【where 条件】 union 【all】

select 字段|常量|表达式|函数 【from 表】 【where 条件】 union 【all】
select 字段|常量|表达式|函数 【from 表】 【where 条件】 union  【all】
.....
select 字段|常量|表达式|函数 【from 表】 【where 条件】


/* UNION */ ------------------
    将多个select查询的结果组合成一个结果集合。
    SELECT ... UNION [ALL|DISTINCT] SELECT ...
    默认 DISTINCT 方式,即所有返回的行都是唯一的
    建议,对每个SELECT查询加上小括号包裹。
    ORDER BY 排序时,需加上 LIMIT 进行结合。
    需要各select查询的字段数量一样。
    每个select查询的字段列表(数量、类型)应一致,因为结果中的字段名以第一条select语句为准。

TRUNCATE

/* TRUNCATE */ ------------------
TRUNCATE [TABLE] tbl_name
清空数据
删除重建表
区别:
1,truncate 是删除表再创建,delete 是逐条删除
2,truncate 重置auto_increment的值。而delete不会
3,truncate 不知道删除了几条,而delete知道。
4,当被用于带分区的表时,truncate 会保留分区

MyISAM和InnoDB区别

MyISAM是MySQL的默认数据库引擎(5.5版之前)。虽然性能极佳,而且提供了大量的特性,包括全文索引、压缩、空间函数等,但MyISAM不支持事务和行级锁,而且最大的缺陷就是崩溃后无法安全恢复。不过,5.5版本之后,MySQL引入了InnoDB(事务性数据库引擎),MySQL 5.5版本后默认的存储引擎为InnoDB。

大多数时候我们使用的都是 InnoDB 存储引擎,但是在某些情况下使用 MyISAM 也是合适的比如读密集的情况下。(如果你不介意 MyISAM 崩溃恢复问题的话)。

两者的对比:

  1. 是否支持行级锁 : MyISAM 只有表级锁,而InnoDB 支持行级锁和表级锁,默认为行级锁。
  2. 是否支持事务和崩溃后的安全恢复: MyISAM 强调的是性能,每次查询具有原子性,其执行速度比InnoDB类型更快,但是不提供事务支持。但是InnoDB 提供事务支持事务,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。
  3. 是否支持外键: MyISAM不支持,而InnoDB支持。
  4. 是否支持MVCC :仅 InnoDB 支持。应对高并发事务, MVCC比单纯的加锁更高效;MVCC只在 READ COMMITTEDREPEATABLE READ 两个隔离级别下工作;MVCC可以使用 乐观(optimistic)锁 和 悲观(pessimistic)锁来实现;各数据库中MVCC实现并不统一。推荐阅读:MySQL-InnoDB-MVCC多版本并发控制

《MySQL高性能》:不要轻易相信“MyISAM比InnoDB快”之类的经验之谈,这个结论往往不是绝对的。在很多我们已知场景中,InnoDB的速度都可以让MyISAM望尘莫及,尤其是用到了聚簇索引,或者需要访问的数据都可以放入内存的应用。

一般情况下我们选择 InnoDB 都是没有问题的,但是某些情况下你并不在乎可扩展能力和并发能力,也不需要事务支持,也不在乎崩溃后的安全恢复问题的话,选择MyISAM也是一个不错的选择。但是一般情况下,我们都是需要考虑到这些问题的。

事务

Transaction Control Language 事务控制语言

事务是指逻辑上的一组操作,组成这组操作的各个单元,要不全成功要不全失败。

  • 支持连续SQL的集体成功或集体撤销。
  • 事务是数据库在数据完整性方面的一个功能。
  • 需要利用 InnoDB 或 BDB 存储引擎,对自动提交的特性支持完成。
  • InnoDB被称为事务安全型引擎。

1 ACID

  • 原子性Atomicity:要么都执行,要么都回滚。(事务不可分割)
  • 一致性Consistency:保证数据的状态操作前和操作后保持一致。数据库总是从一个一致性的状态转换到另一个一致性的状态。事务前后数据的完整性必须保持一致。
    • 事务前后数据的完整性必须保持一致。
    • 在整个事务过程中,操作是连续的
  • 隔离性Isolation:多个事务同时操作相同数据库的同一个数据时,一个事务的执行不受另外一个事务的干扰。“通常来说”(有隔离级别的区别),一个事务所做的修改在最终提交之前,对其他事务是不可见的。
  • 持久性Durability:一个事务一旦提交,则数据将持久化到本地,除非其他事务对其进行修改

事务的分类:

  • 隐式事务,没有明显的开启和结束事务的标志:比如insert、update、delete语句本身就是一个事务
  • 显式事务,具有明显的开启和结束事务的标志。开启事务、编写事务逻辑、提交/回滚事务

2 事务命令

-- 开启事务有下面3种方式:
- START TRANSACTION; -- 临时一次事务
- begin -- 临时一次事务
- set autocommit=0 -- 永久变为事务
开启事务后,所有被执行的SQL语句均被认作当前事务内的SQL语句。

set autocommit=0;
start transaction;
...
commit;-- 事务提交
rollback; -- 事务回滚


savepoint  断点 # 可以回滚 # 在事务的过程中使用
commit to 断点
rollback to 断点

-- 事务的原理
    利用InnoDB的自动提交(autocommit)特性完成。
    普通的MySQL执行语句后,当前的数据提交操作均可被其他客户端可见。
    而事务是暂时关闭“自动提交”机制,需要commit提交持久化数据操作。
-- 注意
    1. 数据定义语言(DDL)语句不能被回滚,比如创建或取消数据库的语句,和创建、取消或更改表或存储的子程序的语句。
    2. 事务不能被嵌套
-- 保存点
    SAVEPOINT 保存点名称 -- 设置一个事务保存点
    ROLLBACK TO SAVEPOINT 保存点名称 -- 回滚到保存点
    RELEASE SAVEPOINT 保存点名称 -- 删除保存点
-- InnoDB自动提交特性设置
    SET autocommit = 0|1;   0表示关闭自动提交,1表示开启自动提交。
    - 如果关闭了,那普通操作的结果对其他客户端也不可见,需要commit提交后才能持久化数据操作。
    - 也可以关闭自动提交来开启事务。但与START TRANSACTION不同的是,
        SET autocommit是永久改变服务器的设置,直到下次再次修改该设置。(针对当前连接)
        而START TRANSACTION记录开启前的状态,而一旦事务提交或回滚后就需要再次开启事务。(针对当前事务)

3 事务隔离级别:

  • READ UNCOMMITTED:读未提交:这个事务修改了数据,即使没有提交,其他事务也可以读到修改后的值,这也被称为脏读。这个级别会导师制很多问题,从性能上来说也没有比其他级别好太多,所以除非真的必要,实际中很少使用
    • 脏读:T1读取到了T2未提交的数据。【更新】
  • READ COMMITTED:读已提交。可以避免脏读。这个级别也叫做不可重复度,因为两次执行同样的查询,可能会得到不同的结果。
    • 不可重复读:T1,T2读取了同一字段,T2改后,T1再读发现不一样了。脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了其他事务已提交的数据。需要注意的是在某些情况下不可重复读并不是问题。
  • REPEATABLE READ==(Mysql默认)==:可重复读:可以避免脏读、不可重复读。但是有时可能出现幻读数据。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。Mysql默认使用该隔离级别。这可以通过“共享读锁”和“排他写锁”实现,即事物需要对某些数据进行修改必须对这些数据加 X 锁,读数据时需要加上 S 锁,当数据读取完成并不立刻释放 S 锁,而是等到事物结束后再释放。
    • 幻读phantom read:T1打开事务后select只发现3行,然后T1暂停,T2 insert后提交,T1再update table1 set '111’发现更新了4行。
    • 幻读:即例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读可能发生在update,delete操作中,而幻读发生在insert操作中。
    • 另外一种幻读情景:例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读可能发生在update,delete操作中,而幻读发生在insert操作中。
    • 如何防止幻读:去看MVCC和间隙锁吧
  • SERIALIZABLE可以避免脏读、不可重复读和幻读

锁的类型

  • 行锁只是一个比较泛的概念,在innodb存储引擎中,行锁的实现主要包含三种算法:
    1. Record-Key Lock:单个数据行的锁,锁住单条记录;
    2. Gap Lock:间隙锁,锁住一个范围,但是不包含数据行本身;
    3. Next-Key Lock:Record-Key Lock + Gap Lock,锁住数据行本身和一个范围的数据行。
  • 所以innodb的行锁不是简单的锁住某一个数据行这个单条记录,而是根据更新条件,如WHERE中可能包含 > 等范围条件,和事务隔离级别来确定是锁住单条还是多条数据行。

表9-1事务隔离级别

隔离级别 脏读 不可重复度 幻读 第一类丢失更新 第二类丢失更新
READ UNCOMMITED 允许 允许 允许 不允许 允许
READ COMMITTED 不允许 允许 允许 不允许 允许
REPEATABLE READ 不允许 不允许 允许 不允许 不允许
SERIALIZABLE 不允许 不允许 不允许 不允许 不允许
隔离级别 脏读 不可重复读 幻读
READ-UNCOMMITTED
READ-COMMITTED ×
REPEATABLE-READ × ×
SERIALIZABLE × × ×

设置隔离级别:

# 设置隔离级别
set session|global  transaction isolation level 隔离级别名;
# 查看隔离级别
select @@tx_isolation;

4 Mysql中的事务

自动提交

mysql默认采用自动提交auto commit的模式,即一条语句输入完后立即执行,不等后一句输入。

show variables lick 'autocommit'; 可以查询自动提交状态

关闭自动提交
set autocommit=1
修改该值对非事务型的表,如MyISAM或者内存表,不会有任何影响。因为他们没有COMMIT和ROLLBACK的概念,也可以说相当于一只处于autocommit启用的模式。
强制自动提交

还有一些命令,也会强制执行COMMIT提交当前的活动事务。如数据定义语言DDL中,如果是导致大量数据改变的操作,如ALTER TABLE、LOCK TABLE,会自动提交之前的语句

隐式和显式锁定

InnoDB采用的是两阶段锁定协议。在事务执行过程中,随时都可以执行锁定,锁只有在执行COMMIT或者ROLLBACK的时候才会释放,并且所有的锁是在同一时刻释放。前面描述的锁定都是隐式锁定,InnoDB会根据隔离级别在需要的时候自动加锁。

另外InnoDB也支持通过特定的语句进行显示锁定,这些语句不属于SQL规范

select ...LOCK IN SHARE MODE;

select ...FOR UPDATE;

MVCC

参考:(推荐)https://blog.csdn.net/SnailMann/article/details/94724197

https://juejin.im/post/6844903799534911496

https://segmentfault.com/a/1190000012650596

多版本并发控制(Multi-Version Concurrency Control, MVCC)是MySQL中基于乐观锁理论实现隔离级别的方式,用于实现读已提交和可重复读取隔离级别的实现。可以认为MVCC是行级锁的一个变种。

MySQL就利用了MVCC来判断在一个事务中,哪个数据可以被读出来,哪个数据不能被读出来。实现对数据库的并发访问。

多版本控制: 指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,与Postgres在数据行上实现多版本不同,InnoDB是在undo log中实现的,通过undo log可以找回数据的历史版本。找回的数据历史版本可以提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。在InnoDB内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性。

快照读和当前读

提醒:阅读MVCC和快照读的时候要注意区分是在上面隔离级别下

MySQL InnoDB下的当前读快照读?

说白了MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现

出现了上面的情况我们需要知道为什么会出现这种情况。在查阅了一些资料后发现在RR级别中,通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,不是数据库最新的数据

  • 这种读取历史数据的方式,我们叫它快照读 (snapshot read),
  • 读取数据库最新版本数据的方式,叫当前读 (current read)
快照读select

当执行select操作(不加锁时)是innodb默认会执行快照读,会记录下这次select后的结果,之后select 的时候就会返回这次快照的数据,即使其他事务提交了不会影响当前select的数据,这就实现了可重复读了。

不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

快照的生成是在第一次执行select的时候,也就是说假设当开启了事务A,然后没有执行任何操作,这时候事务B insert了一条数据然后commit,这时候A执行 select,那么返回的数据中就会有B添加的那条数据。之后无论再有其他事务commit都没有关系,因为快照已经生成了,后面的select都是根据快照来的。

事务1 事务2 说明
begin; begin;
insert into test values(null,‘D’)
commit;
select * from test; - 事务1能查看插入的D,此时生成快照
commit;

如何让select读的时候别的事务无法更改这些行:

select的当前读需要手动地加锁:(不加锁就是快照读)

select * from table where ? lock in share mode;
select * from table where ? for update;
# 其实上面这些操作依据是当前读了
当前读

对于会对数据修改的操作(update、insert、delete)都是采用当前读的模式。在执行这几个操作时会读取最新的记录,即使是别的事务提交的数据也可以查询到。假设要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。也正是因为这样所以才导致上面(可重复读级别下幻读)我们测试的那种情况。

  • 在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,幻读只有发生过当前读才会出现
  • 幻读专指新插入的行,读到原本存在行的更新结果不算。因为当前读的作用就是能读到所有已经提交记录的最新值

如下操作是当前读:

  • select lock in share mode(共享锁),
  • select for update; 排他锁
  • update, insert ,delete(排他锁)

为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录(要获取写锁),会对读取的记录进行加锁。如果别的事务已经修改了但没提交,当前事务就会卡住阻塞等别的事务commit

在数据库的增、删、改、查中,只有增、删、改才会加上排它锁,而只是查询并不会加锁,只能通过在select语句后显式加lock in share mode或者for update来加共享锁或者排它锁

假设要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。

事务 1 事务 2 说明
begin begin
- select * from test 1还没开启事务
- insert into test(name) values(“E”) 2生成写锁
select * from test - 1没卡住,快照读不卡
delete from test where name=‘E’; - 1卡住了,当前读是冲突的
阻塞放行 commit 1放行,2提交后2的写锁就取消了,1就拿到了写锁
commit

利用MVCC解读幻读

https://blog.csdn.net/weixin_33755554/article/details/93881494

前提:InnoDB引擎,可重复读隔离级别下,使用过当前读时。

  • 在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,幻读只在当前读下才会出现
  • 幻读专指新插入的行,读到原本存在行的更新结果不算。因为当前读的作用就是能读到所有一级提交记录的最新值。
事务 1 事务 2 说明
begin begin 假设现有数据一行A
select * from test
- insert into test(name) values(“B”)
- commit 2提交后1还是只有1条
update test set name=“C” 此时1再查询就已经2条C了
commit 保存了C的记录
如果把上个commit换成rollback 查询结果还是B

如下,我们永远锁的是当前d=5的数据,而无法锁新插入d=5的数据,所以再次当前读 读数据 的时候会发现跟原来读的不一样

img

幻读的影响:

  • 会造成一个事务中先产生的锁,无法锁住后加入的满足条件的行
  • 产生数据一致性问题,在一个事务中,先对符合条件的目标行做变更,而在事务提交前有新的符合目标条件的行加入,这样通过binlog恢复的数据是会将所有符合条件的目标行都进行变更的。

如何解决幻读:

  • 将两行记录间的空隙加上锁,组合新记录的插入;这个锁称为间隙锁
  • 间隙锁和间隙锁之间没有冲突。跟间隙锁存在冲突关系的是"往这个间隙锁中插入一个记录"这个操作
原值
id  name
1   A
-----------
理论值
id  name
1   C
2   B
-----------
实际值
id  name
1   C
2   C
-----------
本来我们希望得到的结果只是第一条数据的改为C,但是结果却是两条数据都被修改C了。
这种结果告诉我们其实在MySQL可重复读的隔离级别中并不是完全解决了幻读的问题,而是解决了读数据情况下的幻读问题。而对于修改的操作依旧存在幻读问题,就是说MVCC对于幻读的解决时不彻底的。

怎么用READVIEW的理论解释:

先介绍两个概念:

  • 系统版本号:一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
  • 事务版本号:事务开始时的系统版本号。

MVCC的实现原理

MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突MVCC的实现原理主要是依赖记录中的 3个隐式字段undo日志Read View 来实现的。所以我们先来看看这个三个point的概念

快照读就是MySQL为我们实现MVCC理想模型的其中一个具体非阻塞读功能。而相对而言,当前读就是悲观锁的具体功能实现

①行格式简介

主要说行格式里的隐式字段

每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段。(在InnoDB引擎表中,它的聚簇索引记录中。)

  • DB_TRX_ID
    6byte,最近修改(修改/插入)事务ID:记录创建这条记录/或最后一次修改该记录的事务ID
  • DB_ROLL_PTR
    7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)。roll_pointer。每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。这个roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为它没有老版本)
  • DB_ROW_ID
    6byte,隐含的自增ID(隐藏主键),如果数据表没有主键和非空唯一主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引。
    • 如果没有定义主键,InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引,InnoDB会隐式地定义一个主键来作为聚簇索引。InnoDB只聚集在同一个页面中的记录。包含相邻键值的页面可能会相距甚远。
  • 实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了

在这里插入图片描述

id name 创建时间(事务ID) 删除时间(事务ID)
1 yang 1 undefined
2 long 1 undefined
3 fei 1 undefined

如上图,DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键,DB_TRX_ID是当前操作该记录的事务ID,而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本

比如我们原有数据

id name transaction_ID roll_pointer
1 A 5 上一个版本的地址

比如执行了一条更新操作

# 事务id=6
update test set name='B' where id=1;
id name transaction_id roll_pointer
1 B 6 指向本条记录的旧版本id=5
1 A 5

具体的执行过程

begin->用排他锁锁定该行->记录redo log->记录undo log->修改当前行的值,写事务编号,回滚指针指向undo log中的修改前的行

上述过程确切地说是描述了UPDATE的事务过程,其实undo log分insert和update undo log,因为insert时,原始的数据并不存在,所以回滚时把insert undo log丢弃即可,而update undo log则必须遵守上述过程

②undo日志

undo log主要分为两种:

  • insert undo log
    代表事务在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
  • update undo log
    事务在进行updatedelete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除

purge

  • 从前面的分析可以看出,为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除。
  • 为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。

对MVCC有帮助的实质是update undo logundo log实际上就是存在rollback segment中旧记录链,它的执行流程如下:

一、 比如一个有个事务插入person表插入了一条新记录,记录如下,name为Jerry, age为24岁,隐式主键是1,事务ID回滚指针,我们假设为NULL

img

二、 现在来了一个事务1对该记录的name做出了修改,改为Tom

  • 事务1修改该行(记录)数据时,数据库会先对该行加排他锁(自动加,事务提交后时释放)
  • 然后把该行数据拷贝到undo log中,作为旧记录,既在undo log中有当前行的拷贝副本(修改前的)。(unlog是每行有一个log还是全部行共享一个log无所谓,反正有指针串联)
  • 拷贝完毕后,修改该行name为Tom(数据库中的),并且修改隐藏字段的事务ID为当前事务1的ID, 我们默认从1开始,之后递增,回滚指针指向拷贝到undo log的副本记录,既表示我的上一个版本就是它
  • 事务提交后,释放锁

img

三、 又来了个事务2修改person表的同一个记录,将age修改为30岁

  • 事务2修改该行数据时,数据库也先为该行加锁
  • 然后把该行数据拷贝到undo log中,作为旧记录,发现该行记录已经有undo log了,那么最新的旧数据作为链表的表头(头插?),插在该行记录的undo log最前面
  • 修改该行age为30岁,并且修改隐藏字段的事务ID为当前事务2的ID, 那就是2,回滚指针指向刚刚拷贝到undo log的副本记录
  • 事务提交,释放锁

img

从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录(当然就像之前说的该undo log的节点可能是会purge线程清除掉,向图中的第一条insert undo log,其实在事务提交之后可能就被删除丢失了,不过这里为了演示,所以还放在这里

③ReadView

已提交读和可重复读的区别就在于它们生成ReadView的策略不同。

ReadView:ReadView中主要就是有个列表来存储我们系统中当前活跃着的读写事务,也就是begin了还未提交的事务

什么是Read View?

什么是Read View,说白了Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)

所以我们知道 Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。

Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(即当前行事务ID)取出来,与系统当前其他活跃事务的ID去对比(由Read View维护),如果DB_TRX_ID跟Read View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出Undo Log中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID, 那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本

通过这个列表来判断记录的某个版本是否对当前事务可见,“我到底可以读取这个数据的哪个版本”。

那么这个判断条件是什么呢?
在这里插入图片描述
我们这里盗窃@呵呵一笑百媚生一张源码图,如上,它是一段MySQL判断可见性的一段源码,即changes_visible方法(不完全哈,但能看出大致逻辑),该方法展示了我们拿DB_TRX_ID去跟Read View某些属性进行怎么样的比较

ReadView包含四个比较重要的内容:

  • m_ids:生成ReadView时,系统中活跃的事务{id}集合。trx_list(名字我随便取的)
  • up_limit_id:上面{id}中的最小值。
  • low_limit_id:生成ReadView时,系统应该分配给下一个事务的id。也就是目前已出现过的事务ID的最大值+1
  • creator_trx_id:成该ReadView的事务id。

img

使用方法:

  • 比较当前行的事务idDB_TRX_ID和READVIEW的内容
  • 首先比较当前行事务号DB_TRX_ID < up_limit_idid==m_createot_trx_id(满足则看得见), 如果小于,则当前事务能看到DB_TRX_ID 所在的记录,如果大于等于进入下一个判断。(该行没有在事务中)
  • 接下来判断 DB_TRX_ID >= low_limit_id(满足也看不见) , 如果大于等于则代表DB_TRX_ID 所在的记录在Read View生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断
  • 判断DB_TRX_ID 是否在活跃事务之中,trx_list.contains(DB_TRX_ID),如果在,则代表我Read View生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是看不见的;如果不在,则说明,你这个事务在Read View生成之前就已经Commit了,你修改的结果,我当前事务是能看见的
在这里插入图片描述
  • 如果被访问的版本的trx_id和ReadView中的creator_trx_id相同,就意味着当前版本就是由你“造成”的,可以读出来。
  • 如果被访问的版本的trx_id小于ReadView中的low_limit_id,表示生成该版本的事务在创建ReadView的时候,已经提交了,所以该版本可以读出来。(在ReadView里的检查过了不符合条件才读不在ReadView里的)
  • 如果被访问版本的trx_id大于或等于ReadView中的up_limit_id值,说明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被读出来。(等于读不出来是因为最大值不是活跃的事务,而是不存在的下个版本)
  • 如果生成被访问版本的trx_id在low_limit_id和up_limit_id之间,那就需要判断下trx_id在不在m_ids中:如果在,说明创建ReadView的时候,生成该版本的事务还是活跃的(没有被提交),该版本不可以被读出来;如果不在,说明创建ReadView的时候,生成该版本的事务已经被提交了,该版本可以被读出来。

如果某个数据的最新版本不可以被读出来,就顺着roll_pointer找到该数据的上一个版本,继续做如上的判断,以此类推,如果第一个版本也不可见的话,代表该数据对当前事务完全不可见,查询结果就不包含这条记录了。

两种隔离级别下的ReadView
①REPEATABLE READ可重复读 隔离级别

关键:只有首次读取数据会创建ReadView

原始版本号0,原始id=1的name为小明

事务1:事务版本号 #1,要修改id=1的name为小明1

事务2:事务版本号#2,要查询id=1的name

事务1 事务2 说明
begin; begin;
update test set name=‘AA’ where id=1 - 1先改,2再查
select * from test where id=1;

此时事务2生成的READVIEW-2如下:

活跃事务号 row-id name 回滚指针roll_ponit
#1 1 小明1 指向0,该行不在READVIEW中
事务前版本#0 1 小明 本行不在READVIEW中

ReadView-2的其他信息:

  • m_ids是{1},。生成ReadView时,系统中活跃的事务id集合。
  • low_limit_id是1, 。生成ReadView时,系统中活跃的最小事务id,也就是 m_ids中的最小值。
  • up_limit_id是3,。生成ReadView时,系统应该分配给下一个事务的id。
  • creator_trx_id是2。生成该ReadView的事务id。

那么A事务执行的select语句会读到什么数据呢?

  1. 判断最新的数据版本,name是“小明1”,对应的trx_id是#1,trx_id在m_ids里面,说明当前事务是活跃事务,这个数据版本是由还没有提交的事务创建的,所以这个版本不可见。
  2. 顺着roll_ponit找到这个数据的上一个版本,name是“小明”,对应的trx_id是0,而ReadView中的low_limit_id是1,trx_id<low_limit_id,代表当前数据版本是由已经提交的事务创建的,该版本可见。

所以读到的数据的name是“小明”。

接着往下执行上面的事务

事务1 事务2 说明
begin; begin;
update test set name=‘AA’ where id=1 - 1先改,2再查
select * from test where id=1;
commit;
select * from test where id=1; 还是查到小明

随后,事务#1提交了事务,由于REPEATABLE READ是首次读取数据才会创建ReadView,所以事务#2再次执行select语句,不会再创建ReadView,用的还是上一次的ReadView,所以判断流程和上面也是一样的,所以读到的name还是“小明”。

  • 其他知识点:开启事务后,查询id=1后(算是事务开始了),其他会话改了id=2,然后事务1再查id=2,事务1查不到其他会话更改commit后的结果,说明事务1的READVIEW是所有行的
②READ COMMITTED隔离级别

这个隔离级别:每次select读取数据会重新创建ReadView

和上面一样,id=1的对应的原始name为小明,事务1要改为小明1,事务2要查

假设,现在系统只有一个活跃的事务#1,事务id是1,事务中修改了数据,但是还没有提交,形成的版本链是这样的:

事务1 事务2 说明
begin; begin;
update test set name=‘AA’ where id=1 - 1先改,2再查
select * from test where id=1; 查到的是小明
commit

现在事务2启动,并且执行了select语句,此时会创建出一个ReadView-2,

  • m_ids是{1}
  • up_limit_id是1,
  • low_limit_id是3,
  • creator_trx_id是2。

那么A事务执行的select语句会读到什么数据呢?

  1. 判断最新的数据版本,name是“梦境地底王”,对应的trx_id是100,trx_id在m_ids里面,说明当前事务是活跃事务,这个数据版本是由还没有提交的事务创建的,所以这个版本不可见。
  2. 顺着roll_pointer找到这个数据的上一个版本,name是“地底王”,对应的trx_id是99,而ReadView中的low_limit_id是100,trx_id<low_limit_id,代表当前数据版本是由已经提交的事务创建的,该版本可见。

所以读到的数据的name是“地底王”。

事务1提交后事务2再次查

事务1 事务2 说明
begin; begin;
update test set name=‘AA’ where id=1 - 1先改,2再查
select * from test where id=1; 查到小明
commit;
select * from test where id=1; 查到小明1

因为在"读已提交"隔离级别下,每次查会重新创建READVIEW,所以新的参数为:

  • m_ids是{空},
  • up_limit_id是null,
  • low_limit_id是3,
  • creator_trx_id是2。

查的内容不在m_ids里面,说明这个数据版本是由已经提交的事务创建的,该版本可见。所以查到小明1

我的思考:READVIEW其实就是隐藏列,但隐藏列是本来就有的,那岂不是READVIEW也本来就有?(看不懂这句话的先看看行格式吧,他主要包括记录头+隐藏列+数据列三大部分)

我的思考结果是:确实READVIEW是本来就有的,我们每次select新产生的只是m_ids、low_limit_id、up_limit_id、creator_trx_id这几个值。可能不对,后续再看吧

③④其他隔离级别
  • 对于READ UNCOMMITTED来说,可以读取到其他事务还没有提交的数据,所以直接把这个数据的最新版本读出来就可以了。读未提交是没有加任何锁的,所以对于它来说也就是没有隔离的效果,所以它的性能也是最好的。
  • 对于SERIALIZABLE来说,是用加锁的方式来访问记录。 对于串行化加的是一把大锁,读的时候加共享锁,不能写,写的时候,加的是排它锁,阻塞其它事务的写入和读取,若是其它的事务长时间不能写入就会直接报超时,所以它的性能也是最差的,对于它来就没有什么并发性可言。

④整体流程

我们在了解了隐式字段undo log, 以及Read View的概念之后,就可以来看看MVCC实现的整体流程是怎么样了

整体的流程是怎么样的呢?我们可以模拟一下

  • 事务2对某行数据执行了快照读,数据库为该行数据生成一个Read View读视图,假设当前事务ID为2,此时还有事务1事务3在活跃中,事务4事务2快照读前一刻提交更新了,所以Read View记录了系统当前活跃事务1,3的ID,维护在一个列表上,假设我们称为trx_list
事务1 事务2 事务3 事务4
事务开始 事务开始 事务开始 事务开始
修改且已提交
进行中 快照读 进行中
  • Read View不仅仅会通过一个列表trx_list来维护事务2执行快照读那刻系统正活跃的事务ID,还会有两个属性up_limit_id记录trx_list列表中事务ID最小的ID),low_limit_id(记录trx_list列表中事务ID最大的ID,也有人说快照读那刻系统尚未分配的下一个事务ID也就是目前已出现过的事务ID的最大值+1,我更倾向于后者 >>>资料传送门 | 呵呵一笑百媚生的回答) ;所以在这里例子中up_limit_id就是1,low_limit_id就是4 + 1 = 5(这两个命名是反的,真是个坑),trx_list集合的值是1,3,Read View如下图

img

  • 我们的例子中,只有事务4修改过该行记录,并在事务2执行快照读前,就提交了事务,所以当前该行当前数据的undo log如下图所示;我们的事务2在快照读该行记录的时候,就会拿该行记录的DB_TRX_ID去跟up_limit_id,low_limit_id活跃事务ID列表(trx_list)进行比较,判断当前事务2能看到该记录的版本是哪个。

img

  • 所以先拿该记录DB_TRX_ID字段记录的事务ID 4去跟Read View的的up_limit_id比较,看4是否小于up_limit_id(1),所以不符合条件,继续判断 4 是否大于等于 low_limit_id(5),也不符合条件,最后判断4是否处于trx_list中的活跃事务, 最后发现事务ID为4的事务不在当前活跃事务列表中, 符合可见性条件,所以事务4修改后提交的最新结果对事务2快照读时是可见的,所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本

在这里插入图片描述

  • 也正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同

MVCC的术语总结

<高性能MySQL>中对MVCC的部分介绍

  • MySQL的大多数事务型存储引擎实现的其实都不是简单的行级锁。基于提升并发性能的考虑, 它们一般都同时实现了多版本并发控制(MVCC)。不仅是MySQL, 包括Oracle,PostgreSQL等其他数据库系统也都实现了MVCC, 但各自的实现机制不尽相同, 因为MVCC没有一个统一的实现标准。
  • 可以认为MVCC是行级锁的一个变种, 但是它在很多情况下避免了加锁操作, 因此开销更低。虽然实现机制有所不同, 但大都实现了非阻塞的读操作,写操作也只锁定必要的行。
  • MVCC的实现方式有多种, 典型的有乐观(optimistic)并发控制 和 悲观(pessimistic)并发控制。
  • MVCC只在 READ COMMITTEDREPEATABLE READ 两个隔离级别下工作。其他两个隔离级别够和MVCC不兼容, 因为 READ UNCOMMITTED 总是读取最新的数据行, 而不是符合当前事务版本的数据行。而 SERIALIZABLE 则会对所有读取的行都加锁。

从书中可以了解到:

  • MVCC是被Mysql中 事务型存储引擎InnoDB 所支持的;
  • 应对高并发事务, MVCC比单纯的加锁更高效;
  • MVCC只在 READ COMMITTEDREPEATABLE READ 两个隔离级别下工作;
  • MVCC可以使用 乐观(optimistic)锁悲观(pessimistic)锁来实现;
  • 各数据库中MVCC实现并不统一

下面的MVCC内容是高性能Mysql中的内容,但我觉得有很多地方跟上面对不上号,觉得他说的是不开启事务的情况的理论,我觉得可以不看。

在MySQL中,会在表中每一条数据后面添加两个字段:

  • 创建版本号:创建一行数据时,将当前系统版本号作为创建版本号赋值
  • 删除版本号:删除一行数据时,将当前系统版本号作为删除版本号赋值

前提:可重复读隔离级别下,MVCC的具体:

SELECT

select时读取数据的规则为:创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号

  • ①创建版本号<=当前事务版本号:保证取出的数据不会有后启动的事务中创建的数据。这也是为什么在开始的示例中我们不会查出后来添加的数据的原因。

  • ②删除版本号为空或>当前事务版本号:保证了在该事物开启之前该数据没有被删除,是应该被查出来的数据。

  • InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是事务开始前已经存在,要么是事务自身插入或修改过的。

  • 行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。

  • 符合上述两个条件的记录,才能返回作为查询结果。

注意:如果只是执行begin语句实际上并没有开启一个事务。对数据进行了增删改查等操作后才开启了一个事务。

事务 1 事务 2 说明
begin begin
- select * from test
- insert into test(name) values(“E”) 插入表示的是系统版本号
- commit
select * from test 能查看E了。执行增删改查后事务才真正开始,而不是begin时就开始。所以能查到
commit
事务 1 事务 2 说明
begin begin
- select * from test
- insert into test(name) values(“E”) 插入标识的是系统版本号
select * from test 查不到新数据,事务2在事务1查询前还没提交
commit
commit

幻读:

事务 1 事务 2 说明
begin begin 假设现有数据一行A
select * from test
- insert into test(name) values(“B”)
- commit 2提交后1还是只有1条
update test set name=“C” 此时1再查询就已经2条C了
commit

如何解决幻读:

很明显可重复读的隔离级别没有办法彻底的解决幻读的问题,如果我们的项目中需要解决幻读的话也有两个办法:

  • 使用串行化读的隔离级别
  • MVCC+next-key locks:next-key locks由record locks(索引加锁) 和 gap locks(间隙锁,每次锁住的不光是需要使用的数据,还会锁住这些数据附近的数据)

实际上很多的项目中是不会使用到上面的两种方法的,串行化读的性能太差,而且其实幻读很多时候是我们完全可以接受的。

INSERT

insert时将当前的事务版本号赋值给创建版本号字段。

insert into testmvcc values(1,“test”);

Mysql中MVCC的使用及原理详解

我觉得是隐藏列里的事务版本号变化,隐藏列里并没有create version和delete这一说,所以我觉得网上很多说法并不正确,即上图也不正确,但好理解,实际上事务id只是表现为创建版本号和删除版本号,本质都是只有一个事务id,细节一会挨个讲吧

UPDATE

在更新操作的时候,采用的是先标记旧的那行记录为已删除,并且删除版本号是事务版本号,然后插入一行新的记录的方式。

比如,针对上面那行记录,事务Id为2 要把name字段更新

update table set name= ‘new_value’ where id=1;

Mysql中MVCC的使用及原理详解

修改的时候会先delete后insert。

delete后把行记录里头信息的删除位置为1,然后把版本号写入事务id位置(我觉得细节这么说也不完全正确,下节再说)。

然后再创建一个行,事务id为当前事务id,删除位为空,回滚指针指向刚才删除的那行

DELETE

删除操作的时候,就把删除版本号位填入当前事务版本号。比如

delete from table where id=1;

前面说了delete会把行记录里头信息的删除位置为1,但我又在想,如果删除后回滚呢?怎么还原到原来的事务号?所以我觉得delete操作也会新建一行然后把原来的行标记一下。

MVCC总结

  1. 一般我们认为MVCC有下面几个特点:
    • 每行数据都存在一个版本,每次数据更新时都更新该版本
    • 修改时Copy出当前版本, 然后随意修改,各个事务之间无干扰
    • 保存时比较版本号,如果成功(commit),则覆盖原记录, 失败则放弃copy(rollback)
    • 就是每行都有版本号,保存时根据版本号决定是否成功,听起来含有乐观锁的味道, 因为这看起来正是,在提交的时候才能知道到底能否提交成功
  2. 而InnoDB实现MVCC的方式是:
    • 事务以排他锁的形式修改原始数据
    • 把修改前的数据存放于undo log,通过回滚指针与主数据关联
    • 修改成功(commit)啥都不做,失败则恢复undo log中的数据(rollback)
  3. 二者最本质的区别是: 当修改数据时是否要排他锁定,如果锁定了还算不算是MVCC?
  • Innodb的实现真算不上MVCC, 因为并没有实现核心的多版本共存, undo log 中的内容只是串行化的结果, 记录了多个事务的过程, 不属于多版本共存。但理想的MVCC是难以实现的, 当事务仅修改一行记录使用理想的MVCC模式是没有问题的, 可以通过比较版本号进行回滚, 但当事务影响到多行数据时, 理想的MVCC就无能为力了。
  • 比如, 如果事务A执行理想的MVCC, 修改Row1成功, 而修改Row2失败, 此时需要回滚Row1, 但因为Row1没有被锁定, 其数据可能又被事务B所修改, 如果此时回滚Row1的内容,则会破坏事务B的修改结果,导致事务B违反ACID。 这也正是所谓的 第一类更新丢失 的情况。
  • 也正是因为InnoDB使用的MVCC中结合了排他锁, 不是纯的MVCC, 所以第一类更新丢失是不会出现了, 一般说更新丢失都是指第二类丢失更新。

锁机制:

解决因资源共享 而造成的并发问题。
示例:买最后一件衣服X
A: X 买 : X加锁 ->试衣服…下单…付款…打包 ->X解锁
B: X 买:发现X已被加锁,等待X解锁, X已售空

分类:
根据操作类型分类:
	a.读锁(共享锁): 对同一个数据(衣服),多个读操作可以同时进行,互不干扰。
	b.写锁(互斥锁): 如果当前写操作没有完毕(买衣服的一系列操作),则无法进行其他的读操作、写操作

操作范围:
	a.表锁 :一次性对一张表整体加锁。如MyISAM存储引擎使用表锁,开销小、加锁快;无死锁;但锁的范围大,容易发生锁冲突、并发度低。
	b.行锁 :一次性对一条数据加锁。如InnoDB存储引擎使用行锁,开销大,加锁慢;容易出现死锁;锁的范围较小,不易发生锁冲突,并发度高(很小概率 发生高并发问题:脏读、幻读、不可重复度、丢失更新等问题)。
	c.页锁	
    
    MyISAM不支持事务,InnoDB支持事务

加锁解锁语法:

-- 增加锁:
lock table 表1  read/ write  ,表2  read/ write   ,...
unlock tables;

-- 查看加锁的表:
show open tables ; #InUse=0代表没有锁

按锁定的对象的不同,一般可以分为表锁定和行锁定,前者对整个表进行锁定,而后者对表中特定行进行锁定。

从并发事务锁定的关系上看,可以分为共享锁定和独占锁定。共享锁定会防止独占锁定,但允许其他的共享锁定。而独占锁定既防止其他的独占锁定也防止其他的共享锁定。

为了更改数据,数据库必须在进行更改的行上施加行独占锁定,INSERT、 UPDATE、 DELETE和 SELECT FOR UPDATE语句都会隐式采用必要的行锁定。下面我们介绍一下 Oracle数据库常用的5种锁定。

  • 行共享锁定:一般通过SELECT FOR UPDATE语句隐式获得行共享锁定,在 Oracle中用户也可以通过 LOCK TABLE IN ROW SHARE MODE语句显式获得行共享锁定。行共享锁定并不防止对数据行进行更改的操作,但是可以防止其他会话获取独占性数据表锁定。允许进行多个并发的行共享和行独占性锁定,还允许进行数据表的共享或者采用共享行独占锁定
  • 行独占锁定:通过一条 INSERT、 UPDATE或 DELETE语句隐式获取,或者通过条LOCK TABLE IN ROW EXCLUSMVE MODE语句显式获取。这个锁定可以防止其他会话获取一个共享锁定、共享行独占锁定或独占锁定。
  • 表共享锁定:通过LOCK TABLE IN SHARE MODE语句显式获得。这种锁定可以防止其他会话获取行独占锁定( INSERT、 UPDATE或 DELETB),或者防止其他表共享行独占锁定或表独占锁定,它允许在表中拥有多个行共享和表共享锁定。该锁定可以让会话具有对表事务级一致性访问,因为其他会话在用户提交或者回滚该事务并释放对该表的锁定之前不能更改这个被锁定的表。
  • 表共享行独占:通过 LOCK TABLE IN SHARE ROW EXCLUSTVE MODE语句显式获得。这种锁定可以防止其他会话获取一个表共享、行独占或者表独占锁定,它允许其他行共享锁定。这种锁定类似于表共享锁定,只是一次只能对一个表放置一个表共享行独占锁定。如果A会话拥有该锁定,则B会话可以执行 SELECT FOR UPDATE操作,但如果B会话试图更新选择的行,则需要等待
  • 表独占:通过LOCK TABLE IN EXCLUSIVE MODE显式获得。这个锁定防止其他会话对该表的任何其他锁定。

(1)表锁(MyISAM)

MySQL的默认锁:

MyISAM在执行【查询】语句(SELECT)前,会自动给涉及的所有表加读锁
在执行【增删改】操作(DML)前,会自动给涉及的表加写锁
所以对MyISAM表进行操作,会有以下情况:

  • a、对MyISAM表的读操作(加读锁),不会阻塞其他进程(会话)对同一表的读请求,
    但会阻塞对同一表的写请求。只有当读锁释放后,才会执行其它进程的写操作。
  • b、对MyISAM表的写操作(加写锁),会阻塞其他进程(会话)对同一表的读和写操作,
    只有当写锁释放后,才会执行其它进程的读写操作。
create table tablelock(
	id int primary key auto_increment , 
	name varchar(20)
)engine myisam;#默认InnoDB

表锁定只用于防止其它客户端进行不正当地读取和写入
MyISAM 支持表锁,InnoDB 支持行锁
-- 锁定
    LOCK TABLES tbl_name [AS alias]
-- 解锁
    UNLOCK TABLES

会话:session :每一个访问数据的dos命令行、数据库客户端工具  都是一个会话

myISAM读取时会对需要读到的所有表加共享锁,写入时对表加排它锁。

1.1 表锁读锁(共享锁)

读锁不限制任何对话读,但限制任何对话写

读锁,锁表的当前会话只能读该表,不能写该表,而且不能读/写其他表。其他会话可以读写任何表,但是写该表的时候会等价锁表的会话解锁后才继续执行。

  • – 如果某一个会话 对A表加了read锁,则 该会话 可以对A表进行读操作、不能进行写操作; 且 该会话不能对其他表进行读、写操作。
    – 即如果给A表加了读锁,则当前会话只能对A表进行读操作。

  • 其他会话:a.可以对其他表(A表以外的表)进行读、写操作
    b.对A表:读-可以; 写-需要等待释放锁。

lock table tablelock read

会话1 会话2 会话3 说明
lock table tablelock read;
select * from tablelock; – 读(查),可以
delete from tablelock where id =1 ; – 写(增删改),不可以
select * from emp ; – 不可以读其他表 # 意思是先把读锁去掉再干别的事
delete from emp where eid = 1; – 不可以写其他表
select * from tablelock; – 读(查),可以 select * from emp ; – 读(查),可以
delete from tablelock where id =1 ; – 写,会“等待”会话0将锁释放再执行 delete from emp where eno = 1; – 写,可以

1.2 表锁写锁(排他锁)

lock table table1 write

锁写锁的会话只能写该表,不能增删改查其他表。

其他会话可以任意增删改查,但涉及该表的命令会等待原来会话释放。

分析表锁定:

查看哪些表加了锁:   show open tables ;  In_use=1代表被加了锁

可以通过检查table_locks_waited和table_locks_immediate状态变量来分析系统上的表锁定;
show status like 'table%';#分析表锁定的严重程度

	Table_locks_immediate :产生表级锁定的次数,立刻能获取到的锁数,每立即锁值加1
	Table_locks_waited:需要等待的表锁数(如果该值越大,说明存在越大的锁竞争),每等待一次锁值加1
	一般建议:
		Table_locks_immediate/Table_locks_waited > 5000, 建议采用InnoDB引擎,否则MyISAM引擎

区别是一个是锁定的次数,一个是等待的次数

(2)行锁(InnoDB)

create table test(
    id int(5) primary key auto_increment,#主键,有索引
    name varchar(20)
)engine=innodb ;


-- mysql默认自动commit;	oracle默认不会自动commit ;

为了研究行锁,暂时将自动commit关闭:  
set autocommit =0 ; -- 以后记得恢复,不然永久关闭自动提交了
以后需要通过commit

InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为表锁

一写一读、相同行:不阻塞,拿到旧数据

事务1 事务2 说明
BEGIN; BEGIN;
update test set name=‘A’ where id=1;
select * from test where id=1;– 2查到旧数据,且没有被1阻塞
select * from test where id=1; – name=A
commit
commit;

两写、相同行:后写的阻塞

事务1 事务2 说明
BEGIN;
update test set name=‘C’ where id=1; id=1被事务1加写锁了
select * from test where id=1;-- name=2
update linelock set name=‘D’ where id=1;-- 阻塞到会话0 commit
commit; 放行

两写:相同行,也阻塞,即使是新插入数据

事务1 事务2 说明
BEGIN; BEGIN
insert into test values(null,‘E’); 此时自增吓一跳是3
update test set name=‘f’ where id = 3; 阻塞
commit; 放行
rollback;

两写、不同行:不会阻塞

事务1 事务2 说明
BEGIN;
update linelock set name=‘2’ where id=1;
select * from linelock where id=1;-- name=2 update linelock set name=‘4’ where id=2;-- 不阻塞不报错
对行锁情况:
	1.如果会话x对某条数据a进行 DML操作(研究时:关闭了自动commit的情况下),则其他会话必须等待会话x结束事务(commit/rollback)后  才能对数据a进行操作。
	2.表锁 是通过unlock tables,也可以通过事务解锁 ; 而行锁 是通过事务解锁。
	
	
行锁的注意事项:
a.如果没有索引,则行锁会转为表锁
show index from linelock ;
alter table linelock add index idx_linelock_name(name);

无索引行锁升级为表锁,类型转换会使索引失效,从而转成行锁

varchar必须加单引号,否则是重罪。不加引号会引起类型转换,造成索引失效,转为表锁

两个会话,id和name都有索引,那么操作不同行,一个where id=一个where name='',不会相互阻塞

两个会话,id和name都有索引,那么操作不同行,一个where id=一个where name=,其中name不加引号
name那个因为类型转换所以可以执行成功,但是因为做了类型转换,那一列索引失效。
此时会话0还没提交,拿id查的那个会话就阻塞了,因为索引失效就转化为表锁了

类型转换造成失效例子:

操作不是索引的列,或索引类 发生了类型转换,则索引失效。 因此 此次操作,会从行锁 转为表锁。

事务1 事务2 说明
begin; begin;
select * from test ; select * from test ;
update test set name = ‘AA’ where name = ‘A’ ;
update test set name = ‘BB’ where name = ‘B’ ; 阻塞
commit 放行
b.行锁的一种特殊情况:间隙锁:值在范围内,但却不存在,也会同样加锁,其他会话插入不了id=7
 -- 此时linelock表中 没有id=7的数据
 update linelock set name ='x' where id >1 and id<9 ;   -- 即在此where范围中,没有id=7的数据,则id=7的数据成为间隙。
间隙:Mysql会自动给间隙加锁 ->加的锁称为间隙锁。即 本题 会自动给id=7的数据加 间隙锁(行锁)。
行锁:如果有where,则实际加索的范围 就是where后面的范围(不是实际的值)

此时另外一个会话要在id=7插入数据,不能插入,需要先在原来update的会话中commit,然后第二个会话就能插入成功了。

有了上面的模拟操作,结果和理论又惊奇的一致,似乎可以放心大胆的实战。。。。。。但现实真的很残酷。

现实:当执行批量修改数据脚本的时候,行锁升级为表锁。其他对订单的操作都处于等待中,,,

原因:==InnoDB只有在通过索引条件检索数据时使用行级锁,否则使用表锁!==而模拟操作正是通过id去作为检索条件,而id又是MySQL自动创建的唯一索引,所以才忽略了行锁变表锁的情况。

所以:给需要作为查询条件的字段添加索引。用完后可以删掉。

总结:InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为表锁

从上面的案例看出,行锁变表锁似乎是一个坑,可MySQL没有这么无聊给你挖坑。这是因为MySQL有自己的执行计划。

当你需要更新一张较大表的大部分甚至全表的数据时。而你又傻乎乎地用索引作为检索条件。一不小心开启了行锁(没毛病啊!保证数据的一致性!)。可MySQL却认为大量对一张表使用行锁,会导致事务执行效率低,从而可能造成其他事务长时间锁等待和更多的锁冲突问题,性能严重下降。所以MySQL会将行锁升级为表锁,即实际上并没有使用索引。

我们仔细想想也能理解,既然整张表的大部分数据都要更新数据,在一行一行地加锁效率则更低。其实我们可以通过explain命令查看MySQL的执行计划,你会发现key为null。表明MySQL实际上并没有使用索引,行锁升级为表锁也和上面的结论一致。

行锁分析:

行锁分析:
  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 :	等待次数。从系统启到现在一共等待的次数

加锁的方式:自动加锁。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁;对于普通SELECT语句,InnoDB不会加任何锁;当然我们也可以显示的加锁:

共享锁:select * from tableName where … + lock in share more

排他锁:select * from tableName where … + for update

间隙锁

https://blog.csdn.net/andyxm/article/details/44810417

当id值为1,3,4,5,6…

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

# 此时没有id=2哪一行,
    # 会话0进行
    update linelock set name='3' where id>1 and id<6;
    或
    select ... for update;# 即使没有id=2,也会锁住键值在条件范围内的

    # 会话1:
    insert into linelock values(2,'22');-- 阻塞了,等到会话0 commit才执行

    宁可错杀,不可放过,这个范围的行都锁了,即使没有这个行

隙锁的主要作用是为了防止出现幻读,但是它会把锁定范围扩大,有时候也会给我们带来麻烦,我们就遇到了。 在数据库参数中, 控制间隙锁的参数是:innodb_locks_unsafe_for_binlog, 这个参数默认值是OFF, 也就是启用间隙锁, 他是一个bool值, 当值为true时表示disable间隙锁。那为了防止间隙锁是不是直接将innodb_locaks_unsafe_for_binlog设置为true就可以了呢? 不一定!而且这个参数会影响到主从复制及灾难恢复, 这个方法还尚待商量。

间隙锁的出现主要集中在同一个事务中先delete 后 insert的情况下, 当我们通过一个参数去删除一条记录的时候,

  • 如果参数在数据库中存在, 那么这个时候产生的是普通行锁, 锁住这个记录, 然后删除, 然后释放锁。
  • 如果这条记录不存在,问题就来了, 数据库会扫描索引,发现这个记录不存在, 这个时候的delete语句获取到的就是一个间隙锁,然后数据库会向左扫描扫到第一个比给定参数小的值, 向右扫描扫描到第一个比给定参数大的值, 然后以此为界,构建一个区间, 锁住整个区间内的数据, 一个特别容易出现死锁的间隙锁诞生了。
举个例子:
表task_queue
Id           taskId
1              2
3              9
10            20
40            41

开启一个会话: session 1
sql> set autocommit=0;##取消自动提交
sql> delete from task_queue where taskId = 20;
sql> insert into task_queue values(20, 20);

在开启一个会话: session 2
sql> set autocommit=0; ##取消自动提交
sql> delete from task_queue where taskId = 25;
sql> insert into task_queue values(30, 25);

在没有并发,或是极少并发的情况下, 这样会可能会正常执行,在Mysql中, 事务最终都是穿行执行, 但是在高并发的情况下, 执行的顺序就极有可能发生改变, 变成下面这个样子:
sql> delete from task_queue where taskId = 20;
sql> delete from task_queue where taskId = 25;
sql> insert into task_queue values(20, 20);
sql> insert into task_queue values(30, 25)

这个时候最后一条语句:insert into task_queue values(30, 25); 执行时就会爆出死锁错误。因为删除taskId = 20这条记录的时候,20 --  41 都被锁住了, 他们都取得了这一个数据段的共享锁, 所以在获取这个数据段的排它锁时出现死锁。

这种问题的解决办法:前面说了, 通过修改innodb_locaks_unsafe_for_binlog参数来取消间隙锁从而达到避免这种情况的死锁的方式尚待商量, 那就只有修改代码逻辑, 存在才删除,尽量不去删除不存在的记录。
  • 可重复读 隔离级别:存在间隙锁,可以锁住(2,5)这个间隙,防止其他事务插入数据!
  • 读已提交 隔离级别:不存在间隙锁,其他事务是可以插入数据

比如我们执行下面的语句

update test set color = 'blue' where color = 'red'; 

体现到聚簇索引上为:

读已提交时,会先走聚簇索引,进行全部扫描。加锁如下:

img

但在实际中,MySQL做了优化,在MySQL Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录放锁。

读已提交下,实际加锁如下
img

然而,在可重复读隔离级别下,走聚簇索引,进行全部扫描,最后会将整个表锁上,如下所示
img

如何具体锁定某一行:

select for update;
一读一写,写阻塞

乐观锁

乐观锁不是数据库自带的,需要我们自己去实现。乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在进行更新后,再去判断是否有冲突了

通常实现是这样的:在表中的数据进行操作时(更新),先给数据表加一个版本(version)字段,每操作一次,将那条记录的版本号加1。也就是先查询出那条记录,获取出version字段,如果要对那条记录进行操作(更新),则先判断此刻version的值是否与刚刚查询出来时的version的值相等,如果相等,则说明这段期间,没有其他程序对其进行操作,则可以执行更新,将version字段的值加1;如果更新时发现此刻的version值与刚刚获取出来的version的值不相等,则说明这段期间已经有其他程序对其进行操作了,则不进行更新操作。
乐观锁的概念中其实已经阐述了它的具体实现细节。主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是CAS(Compare and Swap)。

CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

// 查询出商品库存信息
select number from items where id=1; # number=3
#修改库存为2
update items set number=2 where id=1 and number =3;

以上,我们在更新之前,先查询一下库存表中当前库存数(quantity),然后在做update的时候,以库存数作为一个修改条件。当我们提交更新的时候,判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对,如果数据库表当前库存数与第一次取出来的库存数相等,则予以更新,否则认为是过期数据。

以上更新语句存在一个比较重要的问题,即传说中的ABA问题

比如说一个线程one从数据库中取出库存数3,这时候另一个线程two也从数据库中取出库存数3,并且two进行了一些操作变成了2,然后two又将库存数变成3,这时候线程one进行CAS操作发现数据库中仍然是3,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

有一个比较好的办法可以解决ABA问题,那就是通过一个单独的可以顺序递增的version字段。改为以下方式即可:

下单操作包括3步骤:
1.查询出商品信息

select (status,status,version) from t_goods where id=#{id}

2.根据商品信息生成订单
3.修改商品status为2

update t_goods 
set status=2,version=version+1
where id=#{id} and version=#{version};

乐观锁每次在执行数据的修改操作时,都会带上一个版本号(或时间戳),一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。

以上SQL其实还是有一定的问题的,就是一旦遇上高并发的时候,就只有一个线程可以修改成功,那么就会存在大量的失败。

有一条比较好的建议,可以减小乐观锁力度,最大程度的提升吞吐率,提高并发能力!如下:

# 超卖
update item set number=number-1
where id=1 and number-1 > 0  # 自减之前要确保还有库存  # 因为对该行记录加了些锁,所以相当于是线程同步的

以上SQL语句中,如果用户下单数为1,则通过quantity - 1 > 0的方式进行乐观锁控制。

以上update语句,在执行过程中,会在一次原子操作中自己查询一遍quantity的值,并将其扣减掉1。

悲观锁

与乐观锁相对应的就是悲观锁了。悲观锁就是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作,这点跟java中的synchronized很相似,所以悲观锁需要耗费较多的时间。另外与乐观锁相对应的,悲观锁是由数据库自己实现了的,要用的时候,我们直接调用数据库的相关语句就可以了。

说到这里,由悲观锁涉及到的另外两个锁概念就出来了,它们就是共享锁与排它锁。共享锁和排它锁是悲观锁的不同的实现,它俩都属于悲观锁的范畴

悲观锁的实现,往往依靠数据库提供的锁机制。在数据库中,悲观锁的流程如下:

  1. 在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。
  2. 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。
  3. 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
  4. 期间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。

在乐观锁与悲观锁的选择上面,主要看下两者的区别以及适用场景就可以了。

  1. 乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
  2. 悲观锁依赖数据库锁,效率低。更新失败的概率比较低。

排他锁-写锁X

排它锁(Exclusive),又称为X 锁,写锁。

查询该表会锁住该表,禁止其他事务读写

排他锁S锁,也称写锁,独占锁,当前写操作没有完成前,它会阻断其他写锁和读锁

A先查了指定表,B再查该表就阻塞,直到A提交后才执行。

如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。

在查询语句后面增加...FOR UPDATE;,Mysql会对查询结果中的每行都加排他锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请排他锁,否则会被阻塞。

事务 1 事务 2 说明
begin -
select * from test where id=1 for update -
- select * from test where id=1 没问题
- select * from test where id=1 for update 2阻塞
update test set name=‘F’ where id=1; -
commit; -
- 放行出来结果

排他锁

共享锁-读锁S

共享锁(Shared),又称为S 锁,读锁。

查询该表不会造成锁表

读锁多用于判断数据是否存在,多个读操作可以同时进行而不会互相影响。当如果事务对读锁进行修改操作,很可能会造成死锁

读锁只有一种使用方式:SELECT ... LOCK IN SHARE MODE;

在事务当中,读操作SELECT不需要加锁,而是读取undo日志中的最新快照。其中undo日志为用来实现事务回滚,本身没有带来额外的开销

如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。

在查询语句后面增加,Mysql会对查询结果中的每行都加共享锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请共享锁,否则会被阻塞。其他线程也可以读取使用了共享锁的表,而且这些线程读取的是同一个版本的数据。

共享锁

共享锁和排它锁是悲观锁的不同的实现,它俩都属于悲观锁的范畴

比如,我这里通过mysql打开两个查询编辑器,在其中开启一个事务,并不执行commit语句
city表DDL如下:

CREATE TABLE `city` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`state` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8;

 

begin;
SELECT * from city where id = "1"    lock in share mode;

然后在另一个查询窗口中,对id为1的数据进行更新

update  city set name="666" where id ="1";

此时,操作界面进入了卡顿状态,过几秒后,也提示错误信息

[SQL]update  city set name="666" where id ="1";
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction

那么证明,对于id=1的记录加锁成功了,在上一条记录还没有commit之前,这条id=1的记录被锁住了,只有在上一个事务释放掉锁后才能进行操作,或用共享锁才能对此数据进行操作。
再实验一下:

update city set name="666" where id ="1" lock in share mode;
[Err] 1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'lock in share mode' at line 1

加上共享锁后,也提示错误信息了,通过查询资料才知道,对于update,insert,delete语句会自动加排它锁的原因

于是,我又试了试SELECT * from city where id = "1" lock in share mode;

字符集

一 字符集

https://www.cnblogs.com/geaozhang/p/6724393.html#MySQLyuzifuji

是多个字符(英文字符,汉字字符,或者其他国家语言字符)的集合,字符集种类较多,每个字符集包含的字符个数不同。

特点:

①字符编码方式是用一个或多个字节表示字符集中的一个字符

②每种字符集都有自己特有的编码方式,因此同一个字符,在不同字符集的编码方式下,会产生不同的二进制

常见字符集:

  • ASCII字符集:基于罗马字母表的一套字符集,它采用1个字节的低7位表示字符,高位始终为0。
  • LATIN1字符集:相对于ASCII字符集做了扩展,仍然使用一个字节表示字符,但启用了高位,扩展了字符集的表示范围。
  • GBK字符集:支持中文,字符有一字节编码和两字节编码方式。
  • UTF8字符集:Unicode字符集的一种,是计算机科学领域里的一项业界标准,支持了所有国家的文字字符,utf8采用1-4个字节表示字符。

1、MySQL与字符集

只要涉及到文字的地方,就会存在字符集和编码方式。

mysql> show variables like '%character%';
+--------------------------+----------------------------+
| Variable_name            | Value                      |
+--------------------------+----------------------------+
| character_set_client     | utf8                       |客户端来源数据使用的字符集
| character_set_connection | utf8                       |连接层字符集
| character_set_database   | latin1                     |当前选择数据库的默认字符集
| character_set_filesystem | binary                     |系统元数据(字段名等)字符集
| character_set_results    | utf8                       |查询结果字符集
| character_set_server     | latin1                     |默认的内部操作字符集
| character_set_system     | utf8                       |
| character_sets_dir       | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
8 rows in set (0.01 sec)

以上这些参数如何起作用:

1.库、表、列字符集的由来

①建库时,若未明确指定字符集,则采用character_set_server指定的字符集。

②建表时,若未明确指定字符集,则采用当前库所采用的字符集。

③新增时,修改表字段时,若未明确指定字符集,则采用当前表所采用的字符集。

2.更新、查询涉及到得字符集变量

更新流程字符集转换过程:character_set_client–>character_set_connection–>表字符集。

查询流程字符集转换过程:表字符集–>character_set_result

3.character_set_database

当前默认数据库的字符集,比如执行use xxx后,当前数据库变为xxx,若xxx的字符集为utf8,那么此变量值就变为utf8(供系统设置,无需人工设置)。

3、MySQL客户端与字符集

1.对于输入来说:

客户端使用的字符集必须通过character_set_client、character_set_connection体现出来:

①在客户端对数据进行编码(Linux:utf8、windows:gbk)

②MySQL接到SQL语句后(比如insert),发现有字符,询问客户端通过什么方式对字符编码:客户端通过character_set_client参数告知MySQL客户端的编码方式(所以此参数需要正确反映客户端对应的编码)

③当MySQL发现客户端的client所传输的字符集与自己的connection不一样时,会将client的字符集转换为connection的字符集

④MySQL将转换后的编码存储到MySQL表的列上,在存储的时候再判断编码是否与内部存储字符集(按照优先级判断字符集类型)上的编码一致,如果不一致需要再次转换

2.对于查询来说:

客户端使用的字符集必须通过character_set_results来体现,服务器询问客户端字符集,通过character_set_results将结果转换为与客户端相同的字符集传递给客户端。(character_set_results默认等于character_set_client)

4、MySQL字符编码转换原理:

问:若character_set_client为UTF8,而character_set_database为GBK,则会出现需要进行编码转换的情况,字符集转换的原理是什么?

答:假设gbk字符集的字符串“你好”,需要转为utf8字符集存储,实际就是对于“你好”字符串中的每个汉字去utf8编码表里面查询对应的二进制,然后存储。

img

图解字符集转换过程:

  • ①MySQL Server收到请求时将请求数据从character_set_client转换为character_set_connection;
  • ②进行内部操作前将请求数据从character_set_connection转换为内部操作字符集
    • 确定步骤:
    • –使用每个数据字段的CHARACTER SET设定值;
    • –若上述值不存在,则使用对应数据表的DEFAULT CHARACTER SET设定值;
    • –若上述值不存在,则使用对应数据库的DEFAULT CHARACTER SET设定值;
    • –若上述值不存在,则使用character_set_server设定值;
  • ③将操作结果从内部操作字符集转换为character_set_results。

5、字符集常见处理操作

1.查看字符集编码设置

  mysql> show variables like '%character%';
  
  # 查看指定表的字符集
  show create table 表名;
  
  # 查看数据库支持的所有字符集
  show character set;
  
/* 字符集编码 */ 
-- MySQL、数据库、表、字段均可设置编码
-- 数据编码与客户端编码不需一致

2.设置字符集编码

  mysql> set names 'utf8';

相当于同时:
  set character_set_client = utf8;
  set character_set_results = utf8;
  set character_set_connection = utf8;

3.修改数据库字符集

mysql> alter database database_name character set xxx;

只修改库的字符集,影响后续创建的表的默认定义;对于已创建的表的字符集不受影响。(一般在数据库实现字符集即可,表和列都默认采用数据库的字符集)

4.修改表的字符集

mysql> alter table table_name character set xxx;
只修改表的字符集,影响后续该表新增列的默认定义,已有列的字符集不受影响。


mysql> alter table table_name convert to character set xxx;
同时修改表字符集和已有列字符集,并将已有数据进行字符集编码转换。

5.修改列字符集

格式:

ALTER TABLE table_name MODIFY

column_name {CHAR | VARCHAR | TEXT} (column_length)

  [CHARACTER SET charset_name]

  [COLLATE collation_name]

mysql> alter table table_name modify col_name varchar(col_length) character set xxx;

6、字符集的正确实践:

MySQL软件工具本身是没有字符集的,主要是因为工具所在的OS的字符集(Windows:gbk、Linux:utf8),所以字符集的正确实践非常重要:

1.对于insert来说,character_set_client、character_set_connection相同,而且正确反映客户端使用的字符集

2.对于select来说,character_set_results正确反映客户端字符集

3.数据库字符集取决于我们要存储的字符类型

4.字符集转换最多发生一次,这就要求character_set_client、character_set_connection相同

5.所有的字符集转换都发生在数据库端

综述:

1、建立数据库的时候注意字符集(gbk、utf8);

2、连接数据库以后,无论是执行delete modify insert还是select,只要涉及到varchar、char列,就需要设置正确的字符集参数。

二、校对规则collation校对

字符集是一套符号和对应的编号

查看数据库支持的所有字符集(charset):
mysql> show character set;

校对规则(collation):是在字符集内用于字符比较和排序的一套规则,比如有的规则区分大小写,有的则无视。

# 查看所有字符集
SHOW CHARACTER SET [LIKE 'pattern']/SHOW CHARSET [LIKE 'pattern']   
# 查看所有校对集
    SHOW COLLATION [LIKE 'pattern']     
    CHARSET 字符集编码     设置字符集编码
    COLLATE 校对集编码     设置校对集编码


mysql> create table t1(id int,name varchar(20));  #t1建表没有指定校对规则
img
mysql> show collation; #查看数据库支持的所有校对规则

mysql> show variables like 'collation_%'; #查看当前字符集和校对规则设置
img

校对规则特征:

  • ①两个不同的字符集不能有相同的校对规则;
  • ②每个字符集有一个默认校对规则;
  • ③存在校对规则命名约定:以其相关的字符集名开始,中间包括一个语言名,并且以_ci(大小写不敏感)、_cs(大小写敏感)或_bin(二元)结束。

注意:

系统使用utf8字符集,若使用utf8_bin校对规则执行SQL查询时区分大小写,使用utf8_general_ci不区分大小写(默认的utf8字符集对应的校对规则是utf8_general_ci)。

示例:

mysql> create table t2(id int,name varchar(20)) character set=gbk collate=gbk_bin; 
#t2建表指定校对规则(区分大小写)
img

视图

含义:理解成一张虚拟的表

什么是视图:
    视图是一个虚拟表,其内容由查询定义。同真实的表一样,视图包含一系列带有名称的列和行数据。但是,视图并不在数据库中以存储的数据值集形式存在。行和列数据来自由定义视图的查询所引用的表,并且在引用视图时动态生成。
    视图具有表结构文件,但不存在数据文件。
    对其中所引用的基础表来说,视图的作用类似于筛选。定义视图的筛选可以来自当前或其它数据库的一个或多个表,或者其它视图。通过视图进行查询没有任何限制,通过它们进行数据修改时的限制也很少。
    视图是存储在数据库中的查询的sql语句,它主要出于两种原因:安全原因,视图可以隐藏一些数据,如:社会保险基金表,可以用视图只显示姓名,地址,而不显示社会保险号和工资数等,另一原因是可使复杂的查询易于理解和使用。
-- 创建视图
CREATE [OR REPLACE] [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}] VIEW view_name [(column_list)] AS select_statement
    - 视图名必须唯一,同时不能与表重名。
    - 视图可以使用select语句查询到的列名,也可以自己指定相应的列名。
    - 可以指定视图执行的算法,通过ALGORITHM指定。
    - column_list如果存在,则数目必须等于SELECT语句检索的列数
-- 查看结构
    SHOW CREATE VIEW view_name
-- 删除视图
    - 删除视图后,数据依然存在。
    - 可同时删除多个视图。
    DROP VIEW [IF EXISTS] view_name ...
-- 修改视图结构
    - 一般不修改视图,因为不是所有的更新视图都会映射到表上。
    ALTER VIEW view_name [(column_list)] AS select_statement
-- 视图作用
    1. 简化业务逻辑
    2. 对客户端隐藏真实的表结构
-- 视图算法(ALGORITHM)
    MERGE       合并
        将视图的查询语句,与外部查询需要先合并再执行!
    TEMPTABLE   临时表
        将视图执行完毕后,形成临时表,再做外层查询!
    UNDEFINED   未定义(默认),指的是MySQL自主去选择相应的算法。

视图和表的区别:

	使用方式	占用物理空间

视图	完全相同	不占用,仅仅保存的是sql逻辑

表	完全相同	占用

视图的好处:

1、sql语句提高重用性,效率高
2、和表实现了分离,提高了安全性

视图的创建

​ 语法:

# 视图创建
CREATE VIEW  视图名
AS
select ...

#视图使用
SELECT * FROM my_v4; # insert ... Update...DELETE...

#视图更新
##方式一:
CREATE OR REPLACE VIEW test_v7
AS
SELECT ...
##方式二:
ALTER VIEW test_v7
AS
SELECT  ...
###某些视图不能更新
	包含以下关键字的sql语句:分组函数、distinct、group  by、having、union或者union all
	常量视图
	Select中包含子查询
	join
	from一个不能更新的视图
	where子句的子查询引用了from子句中的表

# 视图删除
DROP VIEW test_v1,test_v2,test_v3;

# 视图结构的查看	
DESC test_v7;
SHOW CREATE VIEW test_v7;

存储过程

含义:一组经过预先编译的sql语句的集合

分类:

1、无返回无参
2、仅仅带in类型,无返回有参
3、仅仅带out类型,有返回无参
4、既带in又带out,有返回有参
5、带inout,有返回有参
注意:in、out、inout都可以在一个存储过程中带多个
# 创建存储过程
create procedure 存储过程名(in|out|inout 参数名  参数类型,...)
begin
	存储过程体

end

#调用存储过程
	call 存储过程名(实参列表)

注意

1、需要设置新的结束标记
delimiter 新的结束标记
示例:
delimiter $

CREATE PROCEDURE 存储过程名(IN|OUT|INOUT 参数名  参数类型,...)
BEGIN
	sql语句1;
	sql语句2;

END $

2、存储过程体中可以有多条sql语句,如果仅仅一条sql语句,则可以省略begin end

3、参数前面的符号的意思
in:该参数只能作为输入 (该参数不能做返回值)
out:该参数只能作为输出(该参数只能做返回值)
inout:既能做输入又能做输出

##函数

###创建函数

学过的函数:LENGTH、SUBSTR、CONCAT等
语法:

CREATE FUNCTION 函数名(参数名 参数类型,...) RETURNS 返回类型
BEGIN
	函数体

END

###函数和存储过程的区别

		关键字		调用语法	返回值			应用场景
函数		FUNCTION	SELECT 函数()	只能是一个		一般用于查询结果为一个值并返回时,当有返回值而且仅仅一个
存储过程	PROCEDURE	CALL 存储过程()	可以有0个或多个		一般用于更新

##流程控制结构

###系统变量
一、全局变量

作用域:针对于所有会话(连接)有效,但不能跨重启

查看所有全局变量
SHOW GLOBAL VARIABLES;
查看满足条件的部分系统变量
SHOW GLOBAL VARIABLES LIKE '%char%';
查看指定的系统变量的值
SELECT @@global.autocommit;
为某个系统变量赋值
SET @@global.autocommit=0;
SET GLOBAL autocommit=0;

二、会话变量

作用域:针对于当前会话(连接)有效

查看所有会话变量
SHOW SESSION VARIABLES;
查看满足条件的部分会话变量
SHOW SESSION VARIABLES LIKE '%char%';
查看指定的会话变量的值
SELECT @@autocommit;
SELECT @@session.tx_isolation;
为某个会话变量赋值
SET @@session.tx_isolation='read-uncommitted';
SET SESSION tx_isolation='read-committed';

###自定义变量
一、用户变量

声明并初始化:

SET @变量名=值;
SET @变量名:=值;
SELECT @变量名:=值;

赋值:

方式一:一般用于赋简单的值
SET 变量名=值;
SET 变量名:=值;
SELECT 变量名:=值;


方式二:一般用于赋表 中的字段值
SELECT 字段名或表达式 INTO 变量
FROM 表;

使用:

select @变量名;

二、局部变量

声明:

declare 变量名 类型 【default 值】;

赋值:

方式一:一般用于赋简单的值
SET 变量名=值;
SET 变量名:=值;
SELECT 变量名:=值;


方式二:一般用于赋表 中的字段值
SELECT 字段名或表达式 INTO 变量
FROM 表;

使用:

select 变量名

二者的区别:

		作用域			定义位置		语法

用户变量 当前会话 会话的任何地方 加@符号,不用指定类型
局部变量 定义它的BEGIN END中 BEGIN END的第一句话 一般不用加@,需要指定类型

###分支

if(条件,值1,值2) #可以用在任何位置



# 只能用在begin end中!
if 情况1 then 语句1;
elseif 情况2 then 语句2;
...
else 语句n;
end if;

二、case语句

情况一:类似于switch
case 表达式
when 值1 then 结果1或语句1(如果是语句,需要加分号) 
when 值2 then 结果2或语句2(如果是语句,需要加分号)
...
else 结果n或语句n(如果是语句,需要加分号)
end 【case】(如果是放在begin end中需要加上case,如果放在select后面不需要)

情况二:类似于多重if
case 
when 条件1 then 结果1或语句1(如果是语句,需要加分号) 
when 条件2 then 结果2或语句2(如果是语句,需要加分号)
...
else 结果n或语句n(如果是语句,需要加分号)
end 【case】(如果是放在begin end中需要加上case,如果放在select后面不需要)

特点:
可以用在任何位置

三者比较:
应用场合
if函数 简单双分支
case结构 等值判断 的多分支
if结构 区间判断 的多分支

###循环

【标签:】WHILE 循环条件  DO
	循环体
END WHILE 【标签】;
只能放在BEGIN END里面

如果要搭配leave跳转语句,需要使用标签,否则可以不用标签

leave类似于java中的break语句,跳出所在循环!!!

备份与还原

/* 备份与还原 */ ------------------
备份,将数据的结构与表内数据保存起来。
利用 mysqldump 指令完成。
-- 导出
mysqldump [options] db_name [tables]
mysqldump [options] ---database DB1 [DB2 DB3...]
mysqldump [options] --all--database
1. 导出一张表
  mysqldump -u用户名 -p密码 库名 表名 > 文件名(D:/a.sql)
2. 导出多张表
  mysqldump -u用户名 -p密码 库名 表1 表2 表3 > 文件名(D:/a.sql)
3. 导出所有表
  mysqldump -u用户名 -p密码 库名 > 文件名(D:/a.sql)
4. 导出一个库
  mysqldump -u用户名 -p密码 --lock-all-tables --database 库名 > 文件名(D:/a.sql)
可以-w携带WHERE条件
-- 导入
1. 在登录mysql的情况下:
  source  备份文件
2. 在不登录的情况下
  mysql -u用户名 -p密码 库名 < 备份文件

触发器

/* 触发器 */ ------------------
    触发程序是与表有关的命名数据库对象,当该表出现特定事件时,将激活该对象
    监听:记录的增加、修改、删除。
-- 创建触发器
CREATE TRIGGER trigger_name trigger_time trigger_event ON tbl_name FOR EACH ROW trigger_stmt
    参数:
    trigger_time是触发程序的动作时间。它可以是 before 或 after,以指明触发程序是在激活它的语句之前或之后触发。
    trigger_event指明了激活触发程序的语句的类型
        INSERT:将新行插入表时激活触发程序
        UPDATE:更改某一行时激活触发程序
        DELETE:从表中删除某一行时激活触发程序
    tbl_name:监听的表,必须是永久性的表,不能将触发程序与TEMPORARY表或视图关联起来。
    trigger_stmt:当触发程序激活时执行的语句。执行多个语句,可使用BEGIN...END复合语句结构
-- 删除
DROP TRIGGER [schema_name.]trigger_name
可以使用old和new代替旧的和新的数据
    更新操作,更新前是old,更新后是new.
    删除操作,只有old.
    增加操作,只有new.
-- 注意
    1. 对于具有相同触发程序动作时间和事件的给定表,不能有两个触发程序。
-- 字符连接函数
concat(str1,str2,...])
concat_ws(separator,str1,str2,...)
-- 分支语句
if 条件 then
    执行语句
elseif 条件 then
    执行语句
else
    执行语句
end if;
-- 修改最外层语句结束符
delimiter 自定义结束符号
    SQL语句
自定义结束符号
delimiter ;     -- 修改回原来的分号
-- 语句块包裹
begin
    语句块
end
-- 特殊的执行
1. 只要添加记录,就会触发程序。
2. Insert into on duplicate key update 语法会触发:
    如果没有重复记录,会触发 before insert, after insert;
    如果有重复记录并更新,会触发 before insert, before update, after update;
    如果有重复记录但是没有发生更新,则触发 before insert, before update
3. Replace 语法 如果有记录,则执行 before insert, before delete, after delete, after insert

SQL编程

/* SQL编程 */ ------------------
--// 局部变量 ----------
-- 变量声明
    declare var_name[,...] type [default value]
    这个语句被用来声明局部变量。要给变量提供一个默认值,请包含一个default子句。值可以被指定为一个表达式,不需要为一个常数。如果没有default子句,初始值为null。
-- 赋值
    使用 set 和 select into 语句为变量赋值。
    - 注意:在函数内是可以使用全局变量(用户自定义的变量)
--// 全局变量 ----------
-- 定义、赋值
set 语句可以定义并为变量赋值。
set @var = value;
也可以使用select into语句为变量初始化并赋值。这样要求select语句只能返回一行,但是可以是多个字段,就意味着同时为多个变量进行赋值,变量的数量需要与查询的列数一致。
还可以把赋值语句看作一个表达式,通过select执行完成。此时为了避免=被当作关系运算符看待,使用:=代替。(set语句可以使用= 和 :=)。
select @var:=20;
select @v1:=id, @v2=name from t1 limit 1;
select * from tbl_name where @var:=30;
select into 可以将表中查询获得的数据赋给变量。
    -| select max(height) into @max_height from tb;
-- 自定义变量名
为了避免select语句中,用户自定义的变量与系统标识符(通常是字段名)冲突,用户自定义变量在变量名前使用@作为开始符号。
@var=10;
    - 变量被定义后,在整个会话周期都有效(登录到退出)
--// 控制结构 ----------
-- if语句
if search_condition then
    statement_list   
[elseif search_condition then
    statement_list]
...
[else
    statement_list]
end if;
-- case语句
CASE value WHEN [compare-value] THEN result
[WHEN [compare-value] THEN result ...]
[ELSE result]
END
-- while循环
[begin_label:] while search_condition do
    statement_list
end while [end_label];
- 如果需要在循环内提前终止 while循环,则需要使用标签;标签需要成对出现。
    -- 退出循环
        退出整个循环 leave
        退出当前循环 iterate
        通过退出的标签决定退出哪个循环
--// 内置函数 ----------
-- 数值函数
abs(x)          -- 绝对值 abs(-10.9) = 10
format(x, d)    -- 格式化千分位数值 format(1234567.456, 2) = 1,234,567.46
ceil(x)         -- 向上取整 ceil(10.1) = 11
floor(x)        -- 向下取整 floor (10.1) = 10
round(x)        -- 四舍五入去整
mod(m, n)       -- m%n m mod n 求余 10%3=1
pi()            -- 获得圆周率
pow(m, n)       -- m^n
sqrt(x)         -- 算术平方根
rand()          -- 随机数
truncate(x, d)  -- 截取d位小数
-- 时间日期函数
now(), current_timestamp();     -- 当前日期时间
current_date();                 -- 当前日期
current_time();                 -- 当前时间
date('yyyy-mm-dd hh:ii:ss');    -- 获取日期部分
time('yyyy-mm-dd hh:ii:ss');    -- 获取时间部分
date_format('yyyy-mm-dd hh:ii:ss', '%d %y %a %d %m %b %j'); -- 格式化时间
unix_timestamp();               -- 获得unix时间戳
from_unixtime();                -- 从时间戳获得时间
-- 字符串函数
length(string)          -- string长度,字节
char_length(string)     -- string的字符个数
substring(str, position [,length])      -- 从str的position开始,取length个字符
replace(str ,search_str ,replace_str)   -- 在str中用replace_str替换search_str
instr(string ,substring)    -- 返回substring首次在string中出现的位置
concat(string [,...])   -- 连接字串
charset(str)            -- 返回字串字符集
lcase(string)           -- 转换成小写
left(string, length)    -- 从string2中的左边起取length个字符
load_file(file_name)    -- 从文件读取内容
locate(substring, string [,start_position]) -- 同instr,但可指定开始位置
lpad(string, length, pad)   -- 重复用pad加在string开头,直到字串长度为length
ltrim(string)           -- 去除前端空格
repeat(string, count)   -- 重复count次
rpad(string, length, pad)   --在str后用pad补充,直到长度为length
rtrim(string)           -- 去除后端空格
strcmp(string1 ,string2)    -- 逐字符比较两字串大小
-- 流程函数
case when [condition] then result [when [condition] then result ...] [else result] end   多分支
if(expr1,expr2,expr3)  双分支。
-- 聚合函数
count()
sum();
max();
min();
avg();
group_concat()
-- 其他常用函数
md5();
default();
--// 存储函数,自定义函数 ----------
-- 新建
    CREATE FUNCTION function_name (参数列表) RETURNS 返回值类型
        函数体
    - 函数名,应该合法的标识符,并且不应该与已有的关键字冲突。
    - 一个函数应该属于某个数据库,可以使用db_name.funciton_name的形式执行当前函数所属数据库,否则为当前数据库。
    - 参数部分,由"参数名"和"参数类型"组成。多个参数用逗号隔开。
    - 函数体由多条可用的mysql语句,流程控制,变量声明等语句构成。
    - 多条语句应该使用 begin...end 语句块包含。
    - 一定要有 return 返回值语句。
-- 删除
    DROP FUNCTION [IF EXISTS] function_name;
-- 查看
    SHOW FUNCTION STATUS LIKE 'partten'
    SHOW CREATE FUNCTION function_name;
-- 修改
    ALTER FUNCTION function_name 函数选项
--// 存储过程,自定义功能 ----------
-- 定义
存储存储过程 是一段代码(过程),存储在数据库中的sql组成。
一个存储过程通常用于完成一段业务逻辑,例如报名,交班费,订单入库等。
而一个函数通常专注与某个功能,视为其他程序服务的,需要在其他语句中调用函数才可以,而存储过程不能被其他调用,是自己执行 通过call执行。
-- 创建
CREATE PROCEDURE sp_name (参数列表)
    过程体
参数列表:不同于函数的参数列表,需要指明参数类型
IN,表示输入型
OUT,表示输出型
INOUT,表示混合型
注意,没有返回值。

存储过程

/* 存储过程 */ ------------------
存储过程是一段可执行性代码的集合。相比函数,更偏向于业务逻辑。
调用:CALL 过程名
-- 注意
- 没有返回值。
- 只能单独调用,不可夹杂在其他语句中
-- 参数
IN|OUT|INOUT 参数名 数据类型
IN      输入:在调用过程中,将数据输入到过程体内部的参数
OUT     输出:在调用过程中,将过程体处理完的结果返回到客户端
INOUT   输入输出:既可输入,也可输出
-- 语法
CREATE PROCEDURE 过程名 (参数列表)
BEGIN
    过程体
END

用户和权限管理

/* 用户和权限管理 */ ------------------
-- root密码重置
1. 停止MySQL服务
2.  [Linux] /usr/local/mysql/bin/safe_mysqld --skip-grant-tables &
    [Windows] mysqld --skip-grant-tables
3. use mysql;
4. UPDATE `user` SET PASSWORD=PASSWORD("密码") WHERE `user` = "root";
5. FLUSH PRIVILEGES;
用户信息表:mysql.user
-- 刷新权限
FLUSH PRIVILEGES;
-- 增加用户
CREATE USER 用户名 IDENTIFIED BY [PASSWORD] 密码(字符串)
    - 必须拥有mysql数据库的全局CREATE USER权限,或拥有INSERT权限。
    - 只能创建用户,不能赋予权限。
    - 用户名,注意引号:如 'user_name'@'192.168.1.1'
    - 密码也需引号,纯数字密码也要加引号
    - 要在纯文本中指定密码,需忽略PASSWORD关键词。要把密码指定为由PASSWORD()函数返回的混编值,需包含关键字PASSWORD
-- 重命名用户
RENAME USER old_user TO new_user
-- 设置密码
SET PASSWORD = PASSWORD('密码')  -- 为当前用户设置密码
SET PASSWORD FOR 用户名 = PASSWORD('密码') -- 为指定用户设置密码
-- 删除用户
DROP USER 用户名
-- 分配权限/添加用户
GRANT 权限列表 ON 表名 TO 用户名 [IDENTIFIED BY [PASSWORD] 'password']
    - all privileges 表示所有权限
    - *.* 表示所有库的所有表
    - 库名.表名 表示某库下面的某表
    GRANT ALL PRIVILEGES ON `pms`.* TO 'pms'@'%' IDENTIFIED BY 'pms0817';
-- 查看权限
SHOW GRANTS FOR 用户名
    -- 查看当前用户权限
    SHOW GRANTS; 或 SHOW GRANTS FOR CURRENT_USER; 或 SHOW GRANTS FOR CURRENT_USER();
-- 撤消权限
REVOKE 权限列表 ON 表名 FROM 用户名
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 用户名   -- 撤销所有权限
-- 权限层级
-- 要使用GRANT或REVOKE,您必须拥有GRANT OPTION权限,并且您必须用于您正在授予或撤销的权限。
全局层级:全局权限适用于一个给定服务器中的所有数据库,mysql.user
    GRANT ALL ON *.*和 REVOKE ALL ON *.*只授予和撤销全局权限。
数据库层级:数据库权限适用于一个给定数据库中的所有目标,mysql.db, mysql.host
    GRANT ALL ON db_name.*和REVOKE ALL ON db_name.*只授予和撤销数据库权限。
表层级:表权限适用于一个给定表中的所有列,mysql.talbes_priv
    GRANT ALL ON db_name.tbl_name和REVOKE ALL ON db_name.tbl_name只授予和撤销表权限。
列层级:列权限适用于一个给定表中的单一列,mysql.columns_priv
    当使用REVOKE时,您必须指定与被授权列相同的列。
-- 权限列表
ALL [PRIVILEGES]    -- 设置除GRANT OPTION之外的所有简单权限
ALTER   -- 允许使用ALTER TABLE
ALTER ROUTINE   -- 更改或取消已存储的子程序
CREATE  -- 允许使用CREATE TABLE
CREATE ROUTINE  -- 创建已存储的子程序
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     -- 允许用户运行已存储的子程序
FILE    -- 允许使用SELECT...INTO OUTFILE和LOAD DATA INFILE
INDEX   -- 允许使用CREATE INDEX和DROP INDEX
INSERT  -- 允许使用INSERT
LOCK TABLES     -- 允许对您拥有SELECT权限的表使用LOCK TABLES
PROCESS     -- 允许使用SHOW FULL PROCESSLIST
REFERENCES  -- 未被实施
RELOAD  -- 允许使用FLUSH
REPLICATION CLIENT  -- 允许用户询问从属服务器或主服务器的地址
REPLICATION SLAVE   -- 用于复制型从属服务器(从主服务器中读取二进制日志事件)
SELECT  -- 允许使用SELECT
SHOW DATABASES  -- 显示所有数据库
SHOW VIEW   -- 允许使用SHOW CREATE VIEW
SHUTDOWN    -- 允许使用mysqladmin shutdown
SUPER   -- 允许使用CHANGE MASTER, KILL, PURGE MASTER LOGS和SET GLOBAL语句,mysqladmin debug命令;允许您连接(一次),即使已达到max_connections。
UPDATE  -- 允许使用UPDATE
USAGE   -- “无权限”的同义词
GRANT OPTION    -- 允许授予权限

表维护

/* 表维护 */
-- 分析和存储表的关键字分布
ANALYZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE 表名 ...
-- 检查一个或多个表是否有错误
CHECK TABLE tbl_name [, tbl_name] ... [option] ...
option = {QUICK | FAST | MEDIUM | EXTENDED | CHANGED}
-- 整理数据文件的碎片
OPTIMIZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ...

查询缓存

查询缓存的使用

执行查询语句的时候,会先查询缓存。不过,MySQL 8.0 版本后移除,因为这个功能不太实用

my.cnf加入以下配置,重启MySQL开启查询缓存

query_cache_type=1
query_cache_size=600000

MySQL执行以下命令也可以开启查询缓存

set global  query_cache_type=1;
set global  query_cache_size=600000;

如上,开启查询缓存后在同样的查询条件以及数据情况下,会直接在缓存中返回结果。这里的查询条件包括查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息。因此任何两个查询在任何字符上的不同都会导致缓存不命中。此外,如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL库中的系统表,其查询结果也不会被缓存。

缓存建立之后,MySQL的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。

缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。 因此,开启缓存查询要谨慎,尤其对于写密集的应用来说更是如此。如果开启,要注意合理控制缓存空间大小,一般来说其大小设置为几十MB比较合适。此外,还可以通过sql_cache和sql_no_cache来控制某个查询语句是否需要缓存:

select sql_no_cache count(*) from usr;

索引

MySQL索引使用的数据结构主要有BTree索引哈希索引 。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。

MySQL的BTree索引使用的是B树中的B+Tree,但对于主要的两种存储引擎的实现方式是不同的。

  • MyISAM: B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。
  • InnoDB: 其数据文件本身就是索引文件。相比MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按B+Tree组织的一个索引结构,树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。这被称为“聚簇索引(或聚集索引)”。而其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值而不是地址,这也是和MyISAM不同的地方。在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。 PS:整理自《Java工程师修炼之道》

数据库连接池

https://juejin.im/post/6844903661605240845

杂项

/* 杂项 */ ------------------
1. 可用反引号(`)为标识符(库名、表名、字段名、索引、别名)包裹,以避免与关键字重名!中文也可以作为标识符!
2. 每个库目录存在一个保存当前数据库的选项文件db.opt。
3. 注释:
    单行注释 # 注释内容
    多行注释 /* 注释内容 */
    单行注释 -- 注释内容     (标准SQL注释风格,要求双破折号后加一空格符(空格、TAB、换行等))
4. 模式通配符:
    _   任意单个字符
    %   任意多个字符,甚至包括零字符
    单引号需要进行转义 \'
5. CMD命令行内的语句结束符可以为 ";", "\G", "\g",仅影响显示结果。其他地方还是用分号结束。delimiter 可修改当前对话的语句结束符。
6. SQL对大小写不敏感
7. 清除已有语句:\c

推荐参考

  • 可重复读和读已提交 原理上的区别:https://www.cnblogs.com/shoshana-kong/p/10516404.html

猜你喜欢

转载自blog.csdn.net/hancoder/article/details/105773038