一、MySQL的一些基础知识

背景介绍

我们每天都在访问各种网站、APP,如微信、QQ、抖音、今日头条、腾讯新闻等,这些东西上面都存在大量的信息,这些信息都需要有地方存储,存储在哪呢?数据库。
所以如果我们需要开发一个网站、app,数据库我们必须掌握的技术,常用的数据库有 mysql、oracle、sqlserver、db2 等。
上面介绍的几个数据库,oracle 性能排名第一,服务也是相当到位的,但是收费也是非常高的,金融公司对数据库稳定性要求比较高,一般会选择 oracle。
mysql 是免费的,其他几个目前暂时收费的,mysql 在互联网公司使用率也是排名第一,资料也非常完善,社区也非常活跃,所以我们主要学习 mysql。

数据库常见的概念

DB:数据库,存储数据的容器。
DBMS:数据库管理系统,又称为数据库软件或数据库产品,用于创建或管理 DB。
SQL:结构化查询语言,用于和数据库通信的语言,不是某个数据库软件持有的,而是几乎所有的主流数据库软件通用的语言。中国人之间交流需要说汉语,和美国人之间交流需要说英语,和数据库沟通需要说 SQL 语言。

数据库存储数据的一些特点

  • 数据存放在表中,然后表存放在数据库中
  • 一个库中可以有多张表,每张表具有唯一的名称(表名)来标识自己
  • 表中有一个或多个列,列又称为“字段”,相当于 java 中的“属性”
  • 表中每一行数据,相当于 java 中的“对象”

    window 中安装 mysql

    官网下载 mysql5.7.25:https://dev.mysql.com/downloads/mysql/5.7.html#downloads
    win10 安装 mysql5.7 详细步骤可以看:http://www.itsoku.com/article/192

    mysql 常用的一些命令

    mysql 启动 2 种方式
    方式 1:
    cmd 中运行services.msc
    基础知识 - 图1
    会打开服务窗口,在服务窗口中找到 mysql 服务,点击右键可以启动或者停止

基础知识 - 图2
基础知识 - 图3
方式 2
以管理员身份运行 cmd 命令
基础知识 - 图4
停止命令:net stop mysql
启动命令:net start mysql

  1. C:\Windows\system32>net stop mysql
  2. mysql 服务正在停止.
  3. mysql 服务已成功停止。
  4. C:\Windows\system32>net start mysql
  5. mysql 服务正在启动 .
  6. mysql 服务已经启动成功。

注意:命令后面没有结束符号

mysql 登录命令
mysql -h ip -P 端口 -u 用户名 -p

  1. C:\Windows\system32>mysql -h localhost -P 3306 -u root -p
  2. Enter password: *******
  3. Welcome to the MySQL monitor. Commands end with ; or \g.
  4. Your MySQL connection id is 10
  5. Server version: 5.7.25-log MySQL Community Server (GPL)
  6. Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.
  7. Oracle is a registered trademark of Oracle Corporation and/or its
  8. affiliates. Other names may be trademarks of their respective
  9. owners.
  10. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

说明:

  • -P 大写的 P 后面跟上端口
  • 如果是登录本金 ip 和端口可以省略,如:
    • mysql -u 用户名 -p
  • 可以通过上面的命令连接原创机器的 mysql

查看数据库版本
mysql —version 或者mysql -V用于在未登录情况下,查看本机 mysql 版本:

  1. C:\Windows\system32>mysql -V
  2. mysql Ver 14.14 Distrib 5.7.25, for Win64 (x86_64)
  3. C:\Windows\system32>mysql --version
  4. mysql Ver 14.14 Distrib 5.7.25, for Win64 (x86_64)

select version();:登录情况下,查看链接的库版本:

  1. mysql> select version();
  2. +------------+
  3. | version() |
  4. +------------+
  5. | 5.7.25-log |
  6. +------------+
  7. 1 row in set (0.00 sec)

显示所有数据库:show databases;

  1. mysql> show databases;
  2. +--------------------+
  3. | Database |
  4. +--------------------+
  5. | information_schema |
  6. | apolloconfigdb |
  7. | apolloportaldb |
  8. | config-server |
  9. | dblog |
  10. | diamond_devtest |
  11. | mysql |
  12. | nacos_config |
  13. | performance_schema |
  14. | rs_elastic_job |
  15. | rs_master |
  16. | seata |
  17. | sys |
  18. +--------------------+
  19. 13 rows in set (0.00 sec)

进入指定的库:use 库名;

  1. mysql> use seata;
  2. Database changed

显示当前库中所有的表:show tables;

  1. mysql> show tables;
  2. +--------------------+
  3. | Tables_in_dblog |
  4. +--------------------+
  5. | biz_article |
  6. | biz_article_look |
  7. | biz_article_love |
  8. | biz_article_tags |
  9. | biz_comment |
  10. | biz_file |
  11. | biz_tags |
  12. | biz_type |
  13. | sys_config |
  14. | sys_link |
  15. | sys_log |
  16. | sys_notice |
  17. | sys_resources |
  18. | sys_role |
  19. | sys_role_resources |
  20. | sys_template |
  21. | sys_update_recorde |
  22. | sys_user |
  23. | sys_user_role |
  24. +--------------------+
  25. 19 rows in set (0.00 sec)

查看其他库中所有的表:show tables from 库名;

  1. mysql> show tables from seata;
  2. +-----------------+
  3. | Tables_in_seata |
  4. +-----------------+
  5. | branch_table |
  6. | global_table |
  7. | lock_table |
  8. | t_account |
  9. | t_order |
  10. | t_storage |
  11. | undo_log |
  12. +-----------------+
  13. 7 rows in set (0.00 sec)

查看表的创建语句:show create table 表名;

  1. mysql> show create table biz_tags;
  2. +----------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
  3. | Table | Create Table |
  4. +----------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
  5. | biz_tags | CREATE TABLE `biz_tags` (
  6. `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  7. `name` varchar(50) NOT NULL COMMENT '书签名',
  8. `description` varchar(100) DEFAULT NULL COMMENT '描述',
  9. `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
  10. `update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  11. PRIMARY KEY (`id`) USING BTREE
  12. ) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT |
  13. +----------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
  14. 1 row in set (0.00 sec)

查看表结构:desc 表名;

  1. mysql> desc biz_tags;
  2. +-------------+---------------------+------+-----+-------------------+----------------+
  3. | Field | Type | Null | Key | Default | Extra |
  4. +-------------+---------------------+------+-----+-------------------+----------------+
  5. | id | bigint(20) unsigned | NO | PRI | NULL | auto_increment |
  6. | name | varchar(50) | NO | | NULL | |
  7. | description | varchar(100) | YES | | NULL | |
  8. | create_time | datetime | YES | | CURRENT_TIMESTAMP | |
  9. | update_time | datetime | YES | | CURRENT_TIMESTAMP | |
  10. +-------------+---------------------+------+-----+-------------------+----------------+
  11. 5 rows in set (0.00 sec)

查看当前所在库:select database();

  1. C:\Windows\system32>mysql -h localhost -P 3306 -u root -p
  2. Enter password: *******
  3. Welcome to the MySQL monitor. Commands end with ; or \g.
  4. Your MySQL connection id is 7
  5. Server version: 5.7.25-log MySQL Community Server (GPL)
  6. Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.
  7. Oracle is a registered trademark of Oracle Corporation and/or its
  8. affiliates. Other names may be trademarks of their respective
  9. owners.
  10. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
  11. mysql> select database();
  12. +------------+
  13. | database() |
  14. +------------+
  15. | NULL |
  16. +------------+
  17. 1 row in set (0.00 sec)
  18. mysql> use dblog;
  19. Database changed
  20. mysql> select database();
  21. +------------+
  22. | database() |
  23. +------------+
  24. | dblog |
  25. +------------+
  26. 1 row in set (0.00 sec)

查看当前 mysql 支持的存储引擎:SHOW ENGINES;

  1. mysql> SHOW ENGINES;
  2. +--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
  3. | Engine | Support | Comment | Transactions | XA | Savepoints |
  4. +--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
  5. | InnoDB | DEFAULT | Supports transactions, row-level locking, and foreign keys | YES | YES | YES |
  6. | MRG_MYISAM | YES | Collection of identical MyISAM tables | NO | NO | NO |
  7. | MEMORY | YES | Hash based, stored in memory, useful for temporary tables | NO | NO | NO |
  8. | BLACKHOLE | YES | /dev/null storage engine (anything you write to it disappears) | NO | NO | NO |
  9. | MyISAM | YES | MyISAM storage engine | NO | NO | NO |
  10. | CSV | YES | CSV storage engine | NO | NO | NO |
  11. | ARCHIVE | YES | Archive storage engine | NO | NO | NO |
  12. | PERFORMANCE_SCHEMA | YES | Performance Schema | NO | NO | NO |
  13. | FEDERATED | NO | Federated MySQL storage engine | NULL | NULL | NULL |
  14. +--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
  15. 9 rows in set (0.00 sec)

查看系统变量及其值:SHOW VARIABLES;

  1. mysql> SHOW VARIABLES;
  2. +---------------------------------+--------------------------------------------------+
  3. | Variable_name | Value |
  4. +---------------------------------+--------------------------------------------------+
  5. | auto_increment_increment | 1 |
  6. | auto_increment_offset | 1 |
  7. | autocommit | ON |
  8. | automatic_sp_privileges | ON |
  9. | avoid_temporal_upgrade | OFF |
  10. | back_log | 90 |
  11. | basedir | D:\installsoft\MySQL\mysql-5.7.25-winx64\ |
  12. | big_tables | OFF |
  13. | bind_address | * |
  14. | binlog_cache_size | 32768 |
  15. | binlog_checksum | CRC32
  16. 513 rows in set, 1 warning (0.00 sec)

查看某个系统变量:SHOW VARIABLES like ‘变量名’;

  1. mysql> SHOW VARIABLES like 'wait_timeout';
  2. +---------------+-------+
  3. | Variable_name | Value |
  4. +---------------+-------+
  5. | wait_timeout | 28800 |
  6. +---------------+-------+
  7. 1 row in set, 1 warning (0.00 sec)
  8. mysql> SHOW VARIABLES like '%wait_timeou%t';
  9. +--------------------------+----------+
  10. | Variable_name | Value |
  11. +--------------------------+----------+
  12. | innodb_lock_wait_timeout | 50 |
  13. | lock_wait_timeout | 31536000 |
  14. | wait_timeout | 28800 |
  15. +--------------------------+----------+
  16. 3 rows in set, 1 warning (0.00 sec)

mysql 语法规范

  1. 不区分大小写,但建议关键字大写,表名、列名小写
  2. 每条命令最好用英文分号结尾
  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):事务控制语言 set autocommit=0、start transaction、savepoint、commit、rollback

    二、MySQL数据类型详解

    MySQL 的数据类型

    主要包括以下五大类

  • 整数类型:bit、bool、tinyint、smallint、mediumint、int、bigint

  • 浮点数类型:float、double、decimal
  • 字符串类型:char、varchar、tinyblob、blob、mediumblob、longblob、tinytext、text、mediumtext、longtext
  • 日期类型:Date、DateTime、TimeStamp、Time、Year
  • 其他数据类型:暂不介绍,用的比较少。

    整数类型

    | 类型 | 字节数 | 有符号值范围 | 无符号值范围 | | —- | —- | —- | —- | | tinyint[(n)] [unsigned] | 1 | [-,-1] | [0,-1] | | smallint[(n)] [unsigned] | 2 | [-,-1] | [0,-1] | | mediumint[(n)] [unsigned] | 3 | [-,-1] | [0,-1] | | int[(n)] [unsigned] | 4 | [-,-1] | [0,-1] | | bigint[(n)] [unsigned] | 8 | [-,-1] | [0,-1] |

上面[]包含的内容是可选的,默认是有符号类型的,无符号的需要在类型后面跟上unsigned
示例 1:有符号类型

  1. mysql> create table demo1(
  2. c1 tinyint
  3. );
  4. Query OK, 0 rows affected (0.01 sec)
  5. mysql> insert into demo1 values(-pow(2,7)),(pow(2,7)-1);
  6. Query OK, 2 rows affected (0.00 sec)
  7. Records: 2 Duplicates: 0 Warnings: 0
  8. mysql> select * from demo1;
  9. +------+
  10. | c1 |
  11. +------+
  12. | -128 |
  13. | 127 |
  14. +------+
  15. 2 rows in set (0.00 sec)
  16. mysql> insert into demo1 values(pow(2,7));
  17. ERROR 1264 (22003): Out of range value for column 'c1' at row 1

demo1 表中c1字段为 tinyint 有符号类型的,可以看一下上面的演示,有超出范围报错的。
关于数值对应的范围计算方式属于计算机基础的一些知识,可以去看一下计算机的二进制表示相关的文章。
示例 2:无符号类型

  1. mysql> create table demo2(
  2. c1 tinyint unsigned
  3. );
  4. Query OK, 0 rows affected (0.01 sec)
  5. mysql> insert into demo2 values (-1);
  6. ERROR 1264 (22003): Out of range value for column 'c1' at row 1
  7. mysql> insert into demo2 values (pow(2,8)+1);
  8. ERROR 1264 (22003): Out of range value for column 'c1' at row 1
  9. mysql> insert into demo2 values (0),(pow(2,8));
  10. mysql> insert into demo2 values (0),(pow(2,8)-1);
  11. Query OK, 2 rows affected (0.00 sec)
  12. Records: 2 Duplicates: 0 Warnings: 0
  13. mysql> select * from demo2;
  14. +------+
  15. | c1 |
  16. +------+
  17. | 0 |
  18. | 255 |
  19. +------+
  20. 2 rows in set (0.00 sec)

c1 是无符号的 tinyint 类型的,插入了负数会报错。
类型(n)说明
在开发中,我们会碰到有些定义整型的写法是 int(11),这种写法个人感觉在开发过程中没有什么用途,不过还是来说一下,int(N)我们只需要记住两点:

  • 无论 N 等于多少,int 永远占 4 个字节
  • N 表示的是显示宽度,不足的用 0 补足,超过的无视长度而直接显示整个数字,但这要整型设置了 unsigned zerofill 才有效

看一下示例,理解更方便:

  1. mysql> CREATE TABLE test3 (
  2. `a` int,
  3. `b` int(5),
  4. `c` int(5) unsigned,
  5. `d` int(5) zerofill,
  6. `e` int(5) unsigned zerofill,
  7. `f` int zerofill,
  8. `g` int unsigned zerofill
  9. );
  10. Query OK, 0 rows affected (0.01 sec)
  11. mysql> insert into test3 values (1,1,1,1,1,1,1),(11,11,11,11,11,11,11),(12345,12345,12345,12345,12345,12345,12345);
  12. Query OK, 3 rows affected (0.00 sec)
  13. Records: 3 Duplicates: 0 Warnings: 0
  14. mysql> select * from test3;
  15. +-------+-------+-------+-------+-------+------------+------------+
  16. | a | b | c | d | e | f | g |
  17. +-------+-------+-------+-------+-------+------------+------------+
  18. | 1 | 1 | 1 | 00001 | 00001 | 0000000001 | 0000000001 |
  19. | 11 | 11 | 11 | 00011 | 00011 | 0000000011 | 0000000011 |
  20. | 12345 | 12345 | 12345 | 12345 | 12345 | 0000012345 | 0000012345 |
  21. +-------+-------+-------+-------+-------+------------+------------+
  22. 3 rows in set (0.00 sec)
  23. mysql> show create table test3;
  24. | Table | Create Table
  25. | test3 | CREATE TABLE `test3` (
  26. `a` int(11) DEFAULT NULL,
  27. `b` int(5) DEFAULT NULL,
  28. `c` int(5) unsigned DEFAULT NULL,
  29. `d` int(5) unsigned zerofill DEFAULT NULL,
  30. `e` int(5) unsigned zerofill DEFAULT NULL,
  31. `f` int(10) unsigned zerofill DEFAULT NULL,
  32. `g` int(10) unsigned zerofill DEFAULT NULL
  33. ) ENGINE=InnoDB DEFAULT CHARSET=utf8
  34. 1 row in set (0.00 sec)

show create table test3;输出了表test3的创建语句,和我们原始的创建语句不一致了,原始的d字段用的是无符号的,可以看出当使用了zerofill自动会将无符号提升为有符号。
说明:
int(5)输出宽度不满 5 时,前面用 0 来进行填充
int(n)中的 n 省略的时候,宽度为对应类型无符号最大值的十进制的长度,如 bigint 无符号最大值为 -1 = 18,446,744,073,709,551,615‬;长度是 20 位,来个 bigint 左边 0 填充的示例看一下

  1. mysql> CREATE TABLE test4 (
  2. `a` bigint zerofill
  3. );
  4. Query OK, 0 rows affected (0.01 sec)
  5. mysql> insert into test4 values(1);
  6. Query OK, 1 row affected (0.00 sec)
  7. mysql> select *from test4;
  8. +----------------------+
  9. | a |
  10. +----------------------+
  11. | 00000000000000000001 |
  12. +----------------------+
  13. 1 row in set (0.00 sec)

上面的结果中 1 前面补了 19 个 0,和期望的结果一致。

浮点类型(容易懵,注意看)

类型 字节大小 范围(有符号) 范围(无符号) 用途
float[(m,d)] 4 (-3.402823466E+38,3.402823466351E+38) [0,3.402823466E+38) 单精度
浮点数值
double[(m,d)] 8 (-1.7976931348623157E+308,1.797693134 8623157E+308) [0,1.797693134862315 7E+308) 双精度
浮点数值
decimal[(m,d)] 对 DECIMAL(M,D) ,如果 M>D,为 M+2 否则为 D+2 依赖于 M 和 D 的值 依赖于 M 和 D 的值 小数值

float 数值类型用于表示单精度浮点数值,而 double 数值类型用于表示双精度浮点数值,float 和 double 都是浮点型,而 decimal 是定点型。
浮点型和定点型可以用类型名称后加(M,D)来表示,M 表示该值的总共长度,D 表示小数点后面的长度,M 和 D 又称为精度和标度。
float 和 double 在不指定精度时,默认会按照实际的精度来显示,而 DECIMAL 在不指定精度时,默认整数为 10,小数为 0。
示例 1(重点)

  1. mysql> create table test5(a float(5,2),b double(5,2),c decimal(5,2));
  2. Query OK, 0 rows affected (0.01 sec)
  3. mysql> insert into test5 values (1,1,1),(2.1,2.1,2.1),(3.123,3.123,3.123),(4.125,4.125,4.125),(5.115,5.115,5.115),(6.126,6.126,6.126),(7.116,7.116,7.116),(8.1151,8.1151,8.1151),(9.1251,9.1251,9.1251),(10.11501,10.11501,10.11501),(11.12501,11.12501,11.12501);
  4. Query OK, 7 rows affected, 5 warnings (0.01 sec)
  5. Records: 7 Duplicates: 0 Warnings: 5
  6. mysql> select * from test5;
  7. +-------+-------+-------+
  8. | a | b | c |
  9. +-------+-------+-------+
  10. | 1.00 | 1.00 | 1.00 |
  11. | 2.10 | 2.10 | 2.10 |
  12. | 3.12 | 3.12 | 3.12 |
  13. | 4.12 | 4.12 | 4.13 |
  14. | 5.12 | 5.12 | 5.12 |
  15. | 6.13 | 6.13 | 6.13 |
  16. | 7.12 | 7.12 | 7.12 |
  17. | 8.12 | 8.12 | 8.12 |
  18. | 9.13 | 9.13 | 9.13 |
  19. | 10.12 | 10.12 | 10.12 |
  20. | 11.13 | 11.13 | 11.13 |
  21. +-------+-------+-------+
  22. 11 rows in set (0.00 sec)

结果说明(注意看):
c 是 decimal 类型,认真看一下输入和输出,发现decimal 采用的是四舍五入
认真看一下a和b的输入和输出,尽然不是四舍五入,一脸闷逼,float 和 double 采用的是四舍六入五成双
decimal 插入的数据超过精度之后会触发警告。
什么是四舍六入五成双?
就是 5 以下舍弃 5 以上进位,如果需要处理数字为 5 的时候,需要看 5 后面是否还有不为 0 的任何数字,如果有,则直接进位,如果没有,需要看 5 前面的数字,若是奇数则进位,若是偶数则将 5 舍掉

示例 2
我们将浮点类型的(M,D)精度和标度都去掉,看看效果:

  1. mysql> create table test6(a float,b double,c decimal);
  2. Query OK, 0 rows affected (0.02 sec)
  3. mysql> insert into test6 values (1,1,1),(1.234,1.234,1.4),(1.234,0.01,1.5);
  4. Query OK, 3 rows affected, 2 warnings (0.00 sec)
  5. Records: 3 Duplicates: 0 Warnings: 2
  6. mysql> select * from test6;
  7. +-------+-------+------+
  8. | a | b | c |
  9. +-------+-------+------+
  10. | 1 | 1 | 1 |
  11. | 1.234 | 1.234 | 1 |
  12. | 1.234 | 0.01 | 2 |
  13. +-------+-------+------+
  14. 3 rows in set (0.00 sec)

说明:
a 和 b 的数据正确插入,而 c 被截断了
浮点数 float、double 如果不写精度和标度,则会按照实际显示
decimal 不写精度和标度,小数点后面的会进行四舍五入,并且插入时会有警告!
再看一下下面代码:

  1. mysql> select sum(a),sum(b),sum(c) from test5;
  2. +--------+--------+--------+
  3. | sum(a) | sum(b) | sum(c) |
  4. +--------+--------+--------+
  5. | 67.21 | 67.21 | 67.22 |
  6. +--------+--------+--------+
  7. 1 row in set (0.00 sec)
  8. mysql> select sum(a),sum(b),sum(c) from test6;
  9. +--------------------+--------------------+--------+
  10. | sum(a) | sum(b) | sum(c) |
  11. +--------------------+--------------------+--------+
  12. | 3.4679999351501465 | 2.2439999999999998 | 4 |
  13. +--------------------+--------------------+--------+
  14. 1 row in set (0.00 sec)

从上面 sum 的结果可以看出float、double会存在精度问题,decimal精度正常的,比如银行对统计结果要求比较精准的建议使用decimal。

日期类型

类型 字节大小 范围 格式 用途
DATE 3 1000-01-01/9999-12-31 YYYY-MM-DD 日期值
TIME 3 ‘-838:59:59’/‘838:59:59’ HH:MM:SS 时间值或持续时间
YEAR 1 1901/2155 YYYY 年份值
DATETIME 8 1000-01-01 00:00:00/9999-12-31 23:59:59 YYYY-MM-DD HH:MM:SS 混合日期和时间值
TIMESTAMP 4 1970-01-01 00:00:00/2038 结束时间是第 2147483647 秒,北京时间 2038-1-19 11:14:07,格林尼治时间 2038 年 1 月 19 日 凌晨 03:14:07 YYYYMMDD HHMMSS 混合日期和时间值,时间戳

字符串类型

类型 范围 存储所需字节 说明
char(M) [0,m],m 的范围[0,-1] m 定产字符串
varchar(M) [0,m],m 的范围[0,-1] m 0-65535 字节
tinyblob 0-255(-1)字节 L+1 不超过 255 个字符的二进制字符串
blob 0-65535(-1)字节 L+2 二进制形式的长文本数据
mediumblob 0-16777215(-1)字节 L+3 二进制形式的中等长度文本数据
longblob 0-4294967295(-1)字节 L+4 二进制形式的极大文本数据
tinytext 0-255(-1)字节 L+1 短文本字符串
text 0-65535(-1)字节 L+2 长文本数据
mediumtext 0-16777215(-1)字节 L+3 中等长度文本数据
longtext 0-4294967295(-1)字节 L+4 极大文本数据

char 类型占用固定长度,如果存放的数据为固定长度的建议使用 char 类型,如:手机号码、身份证等固定长度的信息。
表格中的 L 表示存储的数据本身占用的字节,L 以外所需的额外字节为存放该值的长度所需的字节数。
MySQL 通过存储值的内容及其长度来处理可变长度的值,这些额外的字节是无符号整数。
请注意,可变长类型的最大长度、此类型所需的额外字节数以及占用相同字节数的无符号整数之间的对应关系:例如,MEDIUMBLOB 值可能最多 - 1 字节长并需要 3 个字节记录其长度,3 个字节的整数类型MEDIUMINT 的最大无符号值为 - 1。

mysql 类型和 java 类型对应关系

MySQL Type Name Return value ofGetColumnClassName Returned as Java Class
BIT(1) (new in MySQL-5.0) BIT java.lang.Boolean
BIT( > 1) (new in MySQL-5.0) BIT byte[]
TINYINT TINYINT java.lang.Boolean if the configuration property tinyInt1isBit is set to true(the default) and the storage size is 1, orjava.lang.Integer if not.
BOOL, BOOLEAN TINYINT See TINYINT, above as these are aliases forTINYINT(1), currently.
SMALLINT[(M)][unsigned] SMALLINT [UNSIGNED] java.lang.Integer (regardless if UNSIGNED or not)
MEDIUMINT[(M)][unsigned] MEDIUMINT [UNSIGNED] java.lang.Integer, if UNSIGNEDjava.lang.Long
INT,INTEGER[(M)][unsigned] INTEGER [UNSIGNED] java.lang.Integer, if UNSIGNEDjava.lang.Long
BIGINT[(M)][unsigned] BIGINT [UNSIGNED] java.lang.Long, if UNSIGNEDjava.math.BigInteger
FLOAT[(M,D)] FLOAT java.lang.Float
DOUBLE[(M,B)] DOUBLE java.lang.Double
DECIMAL[(M[,D])] DECIMAL java.math.BigDecimal
DATE DATE java.sql.Date
DATETIME DATETIME java.sql.Timestamp
TIMESTAMP[(M)] TIMESTAMP java.sql.Timestamp
TIME TIME java.sql.Time
YEAR[(2|4)] YEAR If yearIsDateType configuration property is set to false, then the returned object type isjava.sql.Short. If set to true (the default) then an object of type java.sql.Date (with the date set to January 1st, at midnight).
CHAR(M) CHAR java.lang.String (unless the character set for the column is BINARY, then byte[]is returned.
VARCHAR(M) [BINARY] VARCHAR java.lang.String (unless the character set for the column is BINARY, then byte[]is returned.
BINARY(M) BINARY byte[]
VARBINARY(M) VARBINARY byte[]
TINYBLOB TINYBLOB byte[]
TINYTEXT VARCHAR java.lang.String
BLOB BLOB byte[]
TEXT VARCHAR java.lang.String
MEDIUMBLOB MEDIUMBLOB byte[]
MEDIUMTEXT VARCHAR java.lang.String
LONGBLOB LONGBLOB byte[]
LONGTEXT VARCHAR java.lang.String
ENUM(‘value1’,’value2’,…) CHAR java.lang.String
SET(‘value1’,’value2’,…) CHAR java.lang.String

数据类型选择的一些建议

  • 选小不选大:一般情况下选择可以正确存储数据的最小数据类型,越小的数据类型通常更快,占用磁盘,内存和 CPU 缓存更小。
  • 简单就好:简单的数据类型的操作通常需要更少的 CPU 周期,例如:整型比字符操作代价要小得多,因为字符集和校对规则(排序规则)使字符比整型比较更加复杂。
  • 尽量避免 NULL:尽量制定列为 NOT NULL,除非真的需要 NULL 类型的值,有 NULL 的列值会使得索引、索引统计和值比较更加复杂。
  • 浮点类型的建议统一选择 decimal
  • 记录时间的建议使用 int 或者 bigint 类型,将时间转换为时间戳格式,如将时间转换为秒、毫秒,进行存储,方便走索引

    三、MySQL管理员常用命令总结

    Mysql 权限工作原理

    mysql 是如何来识别一个用户的呢?
    mysql 为了安全性考虑,采用主机名+用户名来判断一个用户的身份,因为在互联网中很难通过用户名来判断一个用户的身份,但是我们可以通过 ip 或者主机名判断一台机器,某个用户通过这个机器过来的,我们可以识别为一个用户,所以mysql 中采用用户名+主机名来识别用户的身份。当一个用户对 mysql 发送指令的时候,mysql 就是通过用户名和来源(主机)来断定用户的权限。
    Mysql 权限验证分为 2 个阶段:
  1. 阶段 1:连接数据库,此时 mysql 会根据你的用户名及你的来源(ip 或者主机名称)判断是否有权限连接
  2. 阶段 2:对 mysql 服务器发起请求操作,如 create table、select、delete、update、create index 等操作,此时 mysql 会判断你是否有权限操作这些指令

    权限生效时间

    用户及权限信息放在库名为 mysql 的库中,mysql 启动时,这些内容被读进内存并且从此时生效,所以如果通过直接操作这些表来修改用户及权限信息的,需要重启mysql或者执行flush privileges;才可以生效。
    用户登录之后,mysql 会和当前用户之间创建一个连接,此时用户相关的权限信息都保存在这个连接中,存放在内存中,此时如果有其他地方修改了当前用户的权限,这些变更的权限会在下一次登录时才会生效。

    查看 mysql 中所有用户

    用户信息在mysql.user表中,如下:
    mysql> use mysql;
    Database changed
    mysql> select user,host from user;
    +———————-+———————+
    | user | host |
    +———————-+———————+
    | test4 | 127.0.0.% |
    | test4 | 192.168.11.% |
    | mysql.session | localhost |
    | mysql.sys | localhost |
    | root | localhost |
    | test2 | localhost |
    +———————-+———————+
    6 rows in set (0.00 sec)

创建用户

语法:
create user 用户名[@主机名] [identified by ‘密码’];

说明:

  1. 主机名默认值为%,表示这个用户可以从任何主机连接 mysql 服务器
  2. 密码可以省略,表示无密码登录

示例 1:不指定主机名
不指定主机名时,表示这个用户可以从任何主机连接 mysql 服务器
mysql> use mysql;
Database changed

mysql> select user,host from user;
+———————-+—————-+
| user | host |
+———————-+—————-+
| mysql.session | localhost |
| mysql.sys | localhost |
| root | localhost |
+———————-+—————-+
3 rows in set (0.00 sec)

mysql> create user test1;
Query OK, 0 rows affected (0.00 sec)

mysql> select user,host from user;
+———————-+—————-+
| user | host |
+———————-+—————-+
| test1 | % |
| mysql.session | localhost |
| mysql.sys | localhost |
| root | localhost |
+———————-+—————-+
4 rows in set (0.00 sec)

mysql> exit
Bye

C:\Users\Think>mysql -utest1
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 49
Server version: 5.7.25-log MySQL Community Server (GPL)

Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type ‘help;’ or ‘\h’ for help. Type ‘\c’ to clear the current input statement.

上面创建了用户名为test1无密码的用户,没有指定主机,可以看出 host 的默认值为%,表示test1可以从任何机器登录到 mysql 中。
用户创建之后可以在mysql库中通过 select user,host from user;查看到。
其他示例
create user ‘test2’@’localhost’ identified by ‘123’;

说明:test2 的主机为 localhost 表示本机,此用户只能登陆本机的 mysql
create user ‘test3’@% identified by ‘123’;

说明:test3 可以从任何机器连接到 mysql 服务器
create user ‘test4’@’192.168.11.%’ identified by ‘123’;

说明:test4 可以从 192.168.11 段的机器连接 mysql

修改密码【3 种方式】

方式 1:通过管理员修改密码
SET PASSWORD FOR ‘用户名’@’主机’ = PASSWORD(‘密码’);

方式 2:create user 用户名[@主机名][identified by ‘密码’];
set password = password(‘密码’);

方式 3:通过修改 mysql.user 表修改密码
use mysql;
update user set authentication_string = password(‘321’) where user = ‘test1’ and host = ‘%’;
flush privileges;

注意:
通过表的方式修改之后,需要执行flush privileges;才能对用户生效。
5.7 中 user 表中的 authentication_string 字段表示密码,老的一些版本中密码字段是 password。

给用户授权

创建用户之后,需要给用户授权,才有意义。
语法:
grant privileges ON database.table TO ‘username’[@’host’] [with grant option]

grant 命令说明:

  • priveleges (权限列表),可以是all,表示所有权限,也可以是select、update等权限,多个权限之间用逗号分开。
  • ON 用来指定权限针对哪些库和表,格式为数据库.表名 ,点号前面用来指定数据库名,点号后面用来指定表名,. 表示所有数据库所有表。
  • TO 表示将权限赋予某个用户, 格式为username@host,@前面为用户名,@后面接限制的主机,可以是 IP、IP 段、域名以及%,%表示任何地方。
  • WITH GRANT OPTION 这个选项表示该用户可以将自己拥有的权限授权给别人。注意:经常有人在创建操作用户的时候不指定 WITH GRANT OPTION 选项导致后来该用户不能使用 GRANT 命令创建用户或者给其它用户授权。备注:可以使用 GRANT 重复给用户添加权限,权限叠加,比如你先给用户添加一个 select 权限,然后又给用户添加一个 insert 权限,那么该用户就同时拥有了 select 和 insert 权限。

示例:
grant all on . to ‘test1’@‘%’;

说明:给 test1 授权可以操作所有库所有权限,相当于 dba
grant select on seata.* to ‘test1’@’%’;

说明:test1 可以对 seata 库中所有的表执行 select
grant select,update on seata.* to ‘test1’@’%’;

说明:test1 可以对 seata 库中所有的表执行 select、update
grant select(user,host) on mysql.user to ‘test1’@’localhost’;

说明:test1 用户只能查询 mysql.user 表的 user,host 字段

查看用户有哪些权限

show grants for ‘用户名’[@’主机’]
主机可以省略,默认值为%,示例:
mysql> show grants for ‘test1’@’localhost’;
+——————————————————————————————————+
| Grants for test1@localhost |
+——————————————————————————————————+
| GRANT USAGE ON . TO ‘test1’@’localhost’ |
| GRANT SELECT (host, user) ON mysql.user TO ‘test1’@’localhost’ |
+——————————————————————————————————+
2 rows in set (0.00 sec)

show grants;
查看当前用户的权限,如:
mysql> show grants;
+——————————————————————————————————-+
| Grants for root@localhost |
+——————————————————————————————————-+
| GRANT ALL PRIVILEGES ON . TO ‘root’@’localhost’ WITH GRANT OPTION |
| GRANT ALL PRIVILEGES ON test. TO ‘root’@’localhost’ |
| GRANT DELETE ON seata.
TO ‘root’@’localhost’ |
| GRANT PROXY ON ‘’@’’ TO ‘root’@’localhost’ WITH GRANT OPTION |
+——————————————————————————————————-+
4 rows in set (0.00 sec)

撤销用户的权限

语法
revoke privileges ON database.table FROM ‘用户名’[@’主机’];

可以先通过show grants命令查询一下用户对于的权限,然后使用revoke命令撤销用户对应的权限,示例:
mysql> show grants for ‘test1’@’localhost’;
+——————————————————————————————————+
| Grants for test1@localhost |
+——————————————————————————————————+
| GRANT USAGE ON . TO ‘test1’@’localhost’ |
| GRANT SELECT (host, user) ON mysql.user TO ‘test1’@’localhost’ |
+——————————————————————————————————+
2 rows in set (0.00 sec)

mysql> revoke select(host) on mysql.user from test1@localhost;
Query OK, 0 rows affected (0.00 sec)

mysql> show grants for ‘test1’@’localhost’;
+———————————————————————————————+
| Grants for test1@localhost |
+———————————————————————————————+
| GRANT USAGE ON . TO ‘test1’@’localhost’ |
| GRANT SELECT (user) ON mysql.user TO ‘test1’@’localhost’ |
+———————————————————————————————+
2 rows in set (0.00 sec)

上面我们先通过grants命令查看 test1 的权限,然后调用 revoke 命令撤销对mysql.user表host字段的查询权限,最后又通过 grants 命令查看了 test1 的权限,和预期结果一致。

删除用户【2 种方式】

方式 1:
drop user ‘用户名’[@‘主机’],示例:
mysql> drop user test1@localhost;
Query OK, 0 rows affected (0.00 sec)

drop 的方式删除用户之后,用户下次登录就会起效。
方式 2:
通过删除 mysql.user 表数据的方式删除,如下:
delete from user where user=’用户名’ and host=’主机’;
flush privileges;

注意通过表的方式删除的,需要调用flush privileges;刷新权限信息(权限启动的时候在内存中保存着,通过表的方式修改之后需要刷新一下)。

授权原则说明

  • 只授予能满足需要的最小权限,防止用户干坏事,比如用户只是需要查询,那就只给 select 权限就可以了,不要给用户赋予 update、insert 或者 delete 权限
  • 创建用户的时候限制用户的登录主机,一般是限制成指定 IP 或者内网 IP 段
  • 初始化数据库的时候删除没有密码的用户,安装完数据库的时候会自动创建一些用户,这些用户默认没有密码
  • 为每个用户设置满足密码复杂度的密码
  • 定期清理不需要的用户,回收权限或者删除用户

    总结

  1. 通过命令的方式操作用户和权限不需要刷新,下次登录自动生效
  2. 通过操作 mysql 库中表的方式修改、用户信息,需要调用flush privileges;刷新一下,下次登录自动生效
  3. mysql 识别用户身份的方式是:用户名+主机
  4. 本文中讲到的一些指令中带主机的,主机都可以省略,默认值为%,表示所有机器
  5. mysql 中用户和权限的信息在库名为 mysql 的库中

    四、MySQL中 DDL 常见操作命令汇总

    image.png

    DDL 介绍

    DDL:Data Define Language 数据定义语言,主要用来对数据库、表进行一些管理操作。
    如:建库、删库、建表、修改表、删除表、对列的增删改等等。
    文中涉及到的语法用[]包含的内容属于可选项,下面做详细说明。

    库的管理

    创建库

    create database [if not exists] 库名;

删除库

drop databases [if exists] 库名;

建库通用的写法

drop database if exists 旧库名;
create database 新库名;

示例

mysql> show databases like ‘javacode2018’;
+————————————-+
| Database (javacode2018) |
+————————————-+
| javacode2018 |
+————————————-+
1 row in set (0.00 sec)

mysql> drop database if exists javacode2018;
Query OK, 0 rows affected (0.00 sec)

mysql> show databases like ‘javacode2018’;
Empty set (0.00 sec)

mysql> create database javacode2018;
Query OK, 1 row affected (0.00 sec)

show databases like ‘javacode2018’;列出javacode2018库信息。

表管理

创建表

create table 表名(
字段名1 类型[(宽度)] [约束条件] [comment ‘字段说明’],
字段名2 类型[(宽度)] [约束条件] [comment ‘字段说明’],
字段名3 类型[(宽度)] [约束条件] [comment ‘字段说明’]
)[表的一些设置];

注意:

  1. 在同一张表中,字段名不能相同
  2. 宽度和约束条件为可选参数,字段名和类型是必须的
  3. 最后一个字段后不能加逗号
  4. 类型是用来限制 字段 必须以何种数据类型来存储记录
  5. 类型其实也是对字段的约束(约束字段下的记录必须为 XX 类型)
  6. 类型后写的 约束条件 是在类型之外的 额外添加的约束

约束说明
not null:标识该字段不能为空
mysql> create table test1(a int not null comment ‘字段a’);
Query OK, 0 rows affected (0.01 sec)

mysql> insert into test1 values (null);
ERROR 1048 (23000): Column ‘a’ cannot be null
mysql> insert into test1 values (1);
Query OK, 1 row affected (0.00 sec)

mysql> select from test1;
+—-+
| a |
+—-+
| 1 |
+—-+
1 row in *set
(0.00 sec)

default value:为该字段设置默认值,默认值为 value
mysql> drop table IF EXISTS test2;
Query OK, 0 rows affected (0.01 sec)

mysql> create table test2(
-> a int not null comment ‘字段a’,
-> b int not null default 0 comment ‘字段b’
-> );
Query OK, 0 rows affected (0.02 sec)

mysql> insert into test2(a) values (1);
Query OK, 1 row affected (0.00 sec)

mysql> select from test2;
+—-+—-+
| a | b |
+—-+—-+
| 1 | 0 |
+—-+—-+
1 row in *set
(0.00 sec)

上面插入时未设置 b 的值,自动取默认值 0
primary key:标识该字段为该表的主键,可以唯一的标识记录,插入重复的会报错
两种写法,如下:
方式 1:跟在列后,如下:
mysql> drop table IF EXISTS test3;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> create table test3(
-> a int not null comment ‘字段a’ primary key
-> );
Query OK, 0 rows affected (0.01 sec)

mysql> insert into test3 (a) values (1);
Query OK, 1 row affected (0.01 sec)

mysql> insert into test3 (a) values (1);
ERROR 1062 (23000): Duplicate entry ‘1’ for key ‘PRIMARY’

方式 2:在所有列定义之后定义,如下:
mysql> drop table IF EXISTS test4;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> create table test4(
-> a int not null comment ‘字段a’,
-> b int not null default 0 comment ‘字段b’,
-> primary key(a)
-> );
Query OK, 0 rows affected (0.02 sec)

mysql> insert into test4(a,b) values (1,1);
Query OK, 1 row affected (0.00 sec)

mysql> insert into test4(a,b) values (1,2);
ERROR 1062 (23000): Duplicate entry ‘1’ for key ‘PRIMARY’

插入重复的值,会报违法主键约束
方式 2 支持多字段作为主键,多个之间用逗号隔开,语法:primary key(字段 1,字段 2,字段 n),示例:
mysql> drop table IF EXISTS test7;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql>
mysql> create table test7(
-> a int not null comment ‘字段a’,
-> b int not null comment ‘字段b’,
-> PRIMARY KEY (a,b)
-> );
Query OK, 0 rows affected (0.02 sec)

mysql>
mysql> insert into test7(a,b) VALUES (1,1);
Query OK, 1 row affected (0.00 sec)

mysql> insert into test7(a,b) VALUES (1,1);
ERROR 1062 (23000): Duplicate entry ‘1-1’ for key ‘PRIMARY’

foreign key:为表中的字段设置外键
语法:foreign key(当前表的列名) references 引用的外键表(外键表中字段名称)
mysql> drop table IF EXISTS test6;
Query OK, 0 rows affected (0.01 sec)

mysql> drop table IF EXISTS test5;
Query OK, 0 rows affected (0.01 sec)

mysql>
mysql> create table test5(
-> a int not null comment ‘字段a’ primary key
-> );
Query OK, 0 rows affected (0.02 sec)

mysql>
mysql> create table test6(
-> b int not null comment ‘字段b’,
-> ts5_a int not null,
-> foreign key(ts5_a) references test5(a)
-> );
Query OK, 0 rows affected (0.01 sec)

mysql> insert into test5 (a) values (1);
Query OK, 1 row affected (0.00 sec)

mysql> insert into test6 (b,test6.ts5_a) values (1,1);
Query OK, 1 row affected (0.00 sec)

mysql> insert into test6 (b,test6.ts5_a) values (2,2);
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (javacode2018.test6, CONSTRAINT test6_ibfk_1 FOREIGN KEY (ts5_a) REFERENCES test5 (a))

说明:表示 test6 中 ts5_a 字段的值来源于表 test5 中的字段 a。
注意几点:

  • 两张表中需要建立外键关系的字段类型需要一致
  • 要设置外键的字段不能为主键
  • 被引用的字段需要为主键
  • 被插入的值在外键表必须存在,如上面向 test6 中插入 ts5_a 为 2 的时候报错了,原因:2 的值在 test5 表中不存在

unique key(uq):标识该字段的值是唯一的
支持一个到多个字段,插入重复的值会报违反唯一约束,会插入失败。
定义有 2 种方式。
方式 1:跟在字段后,如下:
mysql> drop table IF EXISTS test8;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql>
mysql> create table test8(
-> a int not null comment ‘字段a’ unique key
-> );
Query OK, 0 rows affected (0.01 sec)

mysql>
mysql> insert into test8(a) VALUES (1);
Query OK, 1 row affected (0.00 sec)

mysql> insert into test8(a) VALUES (1);
ERROR 1062 (23000): Duplicate entry ‘1’ for key ‘a’

方式 2:所有列定义之后定义,如下:
mysql> drop table IF EXISTS test9;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql>
mysql> create table test9(
-> a int not null comment ‘字段a’,
-> unique key(a)
-> );
Query OK, 0 rows affected (0.01 sec)

mysql>
mysql> insert into test9(a) VALUES (1);
Query OK, 1 row affected (0.00 sec)

mysql> insert into test9(a) VALUES (1);
ERROR 1062 (23000): Duplicate entry ‘1’ for key ‘a’

方式 2 支持多字段,多个之间用逗号隔开,语法:primary key(字段 1,字段 2,字段 n),示例:
mysql> drop table IF EXISTS test10;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql>
mysql> create table test10(
-> a int not null comment ‘字段a’,
-> b int not null comment ‘字段b’,
-> unique key(a,b)
-> );
Query OK, 0 rows affected (0.01 sec)

mysql>
mysql> insert into test10(a,b) VALUES (1,1);
Query OK, 1 row affected (0.00 sec)

mysql> insert into test10(a,b) VALUES (1,1);
ERROR 1062 (23000): Duplicate entry ‘1-1’ for key ‘a’

auto_increment:标识该字段的值自动增长(整数类型,而且为主键)
mysql> drop table IF EXISTS test11;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql>
mysql> create table test11(
-> a int not null AUTO_INCREMENT PRIMARY KEY comment ‘字段a’,
-> b int not null comment ‘字段b’
-> );
Query OK, 0 rows affected (0.01 sec)

mysql>
mysql> insert into test11(b) VALUES (10);
Query OK, 1 row affected (0.00 sec)

mysql> insert into test11(b) VALUES (20);
Query OK, 1 row affected (0.00 sec)

mysql> select from test11;
+—-+——+
| a | b |
+—-+——+
| 1 | 10 |
| 2 | 20 |
+—-+——+
2 rows in *set
(0.00 sec)

字段 a 为自动增长,默认值从 1 开始,每次+1
关于自动增长字段的初始值、步长可以在 mysql 中进行设置,比如设置初始值为 1 万,每次增长 10
注意:
自增长列当前值存储在内存中,数据库每次重启之后,会查询当前表中自增列的最大值作为当前值,如果表数据被清空之后,数据库重启了,自增列的值将从初始值开始
我们来演示一下:
mysql> delete from test11;
Query OK, 2 rows affected (0.00 sec)

mysql> insert into test11(b) VALUES (10);
Query OK, 1 row affected (0.00 sec)

mysql> select from test11;
+—-+——+
| a | b |
+—-+——+
| 3 | 10 |
+—-+——+
1 row in *set
(0.00 sec)

上面删除了 test11 数据,然后插入了一条,a 的值为 3,执行下面操作:
删除 test11 数据,重启 mysql,插入数据,然后看 a 的值是不是被初始化了?如下:
mysql> delete from test11;
Query OK, 1 row affected (0.00 sec)

mysql> select from test11;
Empty *set
(0.00 sec)

mysql> exit
Bye

C:\Windows\system32>net stop mysql
mysql 服务正在停止..
mysql 服务已成功停止。

C:\Windows\system32>net start mysql
mysql 服务正在启动 .
mysql 服务已经启动成功。

C:\Windows\system32>mysql -uroot -p
Enter password: *
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.25-log MySQL Community Server (GPL)

Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type ‘help;’ or ‘\h’ for help. Type ‘\c’ to clear the current input statement.

mysql> use javacode2018;
Database changed
mysql> select * from test11;
Empty set (0.01 sec)

mysql> insert into test11 (b) value (100);
Query OK, 1 row affected (0.00 sec)

mysql> select * from test11;
+—-+——-+
| a | b |
+—-+——-+
| 1 | 100 |
+—-+——-+
1 row in set (0.00 sec)

删除表

drop table [if exists] 表名;

修改表名

alter table 表名 rename [to] 新表名;

表设置备注

alter table 表名 comment ‘备注信息’;

复制表

只复制表结构

create table 表名 like 被复制的表名;

如:
mysql> create table test12 like test11;
Query OK, 0 rows affected (0.01 sec)

mysql> select from test12;
Empty *set
(0.00 sec)

mysql> show create table test12;
+————+———-+
| Table | Create Table
+————+———-+
| test12 | CREATE TABLE test12 (
a int(11) NOT NULL AUTO_INCREMENT COMMENT ‘字段a’,
b int(11) NOT NULL COMMENT ‘字段b’,
PRIMARY KEY (a)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
+————+———-+
1 row in set (0.00 sec)

复制表结构+数据

create table 表名 [as] select 字段,… from 被复制的表 [where 条件];

如:
mysql> create table test13 as select from test11;
Query OK, 1 row *affected
(0.02 sec)
Records: 1 Duplicates: 0 Warnings: 0

mysql> select from test13;
+—-+——-+
| a | b |
+—-+——-+
| 1 | 100 |
+—-+——-+
1 row in *set
(0.00 sec)

表结构和数据都过来了。

表中列的管理

添加列

alter table 表名 add column 列名 类型 [列约束];

示例:
mysql> drop table IF EXISTS test14;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql>
mysql> create table test14(
-> a int not null AUTO_INCREMENT PRIMARY KEY comment ‘字段a’
-> );
Query OK, 0 rows affected (0.02 sec)

mysql> alter table test14 add column b int not null default 0 comment ‘字段b’;
Query OK, 0 rows affected (0.03 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> alter table test14 add column c int not null default 0 comment ‘字段c’;
Query OK, 0 rows affected (0.05 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> insert into test14(b) values (10);
Query OK, 1 row affected (0.00 sec)

mysql> select from test14; c
+—-+——+—-+
| a | b | c |
+—-+——+—-+
| 1 | 10 | 0 |
+—-+——+—-+
1 row in *set
(0.00 sec)

修改列

alter table 表名 modify column 列名 新类型 [约束];
或者
alter table 表名 change column 列名 新列名 新类型 [约束];

2 种方式区别:modify 不能修改列名,change 可以修改列名
我们看一下 test14 的表结构:
mysql> show create table test14;
+————+————+
| Table | Create Table |
+————+————+
| test14 | CREATE TABLE test14 (
a int(11) NOT NULL AUTO_INCREMENT COMMENT ‘字段a’,
b int(11) NOT NULL DEFAULT ‘0’ COMMENT ‘字段b’,
c int(11) NOT NULL DEFAULT ‘0’ COMMENT ‘字段c’,
PRIMARY KEY (a)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 |
+————+————+
1 row in set (0.00 sec)

我们将字段 c 名字及类型修改一下,如下:
mysql> alter table test14 change column c d varchar(10) not null default ‘’ comment ‘字段d’;
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> show create table test14; ;;
+————+————+
| Table | Create Table |
+————+————+
| test14 | CREATE TABLE test14 (
a int(11) NOT NULL AUTO_INCREMENT COMMENT ‘字段a’,
b int(11) NOT NULL DEFAULT ‘0’ COMMENT ‘字段b’,
d varchar(10) NOT NULL DEFAULT ‘’ COMMENT ‘字段d’,
PRIMARY KEY (a)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 |
+————+————+
1 row in set (0.00 sec)

删除列

alter table 表名 drop column 列名;

示例:
mysql> alter table test14 drop column d;
Query OK, 0 rows affected (0.05 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> show create table test14;
+————+————+
| Table | Create Table |
+————+————+
| test14 | CREATE TABLE test14 (
a int(11) NOT NULL AUTO_INCREMENT COMMENT ‘字段a’,
b int(11) NOT NULL DEFAULT ‘0’ COMMENT ‘字段b’,
PRIMARY KEY (a)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 |
+————+————+
1 row in set (0.00 sec)

五、MySQL中(insert/update/delete)到底有多少种写法?

DML

DML(Data Manipulation Language)数据操作语言,以 INSERT、UPDATE、DELETE 三种指令为核心,分别代表插入、更新与删除,是必须要掌握的指令,DML 和 SQL 中的 select 熟称 CRUD(增删改查)。
文中涉及到的语法用[]包含的内容属于可选项,下面做详细说明。

插入操作

插入单行 2 种方式

方式 1

insert into 表名[(字段,字段)] values (值,值);

说明:
值和字段需要一一对应
如果是字符型或日期类型,值需要用单引号引起来;如果是数值类型,不需要用单引号
字段和值的个数必须一致,位置对应
字段如果不能为空,则必须插入值
可以为空的字段可以不用插入值,但需要注意:字段和值都不写;或字段写上,值用 null 代替
表名后面的字段可以省略不写,此时表示所有字段,顺序和表中字段顺序一致。

方式 2

insert into 表名 set 字段 = 值,字段 = 值;

方式 2 不常见,建议使用方式 1

批量插入 2 种方式

方式 1

insert into 表名 [(字段,字段)] values (值,值),(值,值),(值,值);

方式 2

insert into 表 [(字段,字段)]
数据来源select语句;

说明:
数据来源 select 语句可以有很多种写法,需要注意:select 返回的结果和插入数据的字段数量、顺序、类型需要一致。
关于 select 的写法后面文章会详细介绍。
如:
— 删除test1
drop table if exists test1;
— 创建test1
create table test1(a int,b int);
— 删除test2
drop table if exists test2;
— 创建test2
create table test2(c1 int,c2 int,c3 int);
— 向test2中插入数据
insert into test2 values (100,101,102),(200,201,202),(300,301,302),(400,401,402);
— 向test1中插入数据
insert into test1 (a,b) select 1,1 union all select 2,2 union all select 2,2;
— 向test1插入数据,数据来源于test2表
insert into test1 (a,b) select c2,c3 from test2 where c1>=200;

select *from test1;

mysql> select *from test1;
+———+———+
| a | b |
+———+———+
| 1 | 1 |
| 2 | 2 |
| 2 | 2 |
| 201 | 202 |
| 301 | 302 |
| 401 | 402 |

mysql> select from test2;
+———+———+———+
| c1 | c2 | c3 |
+———+———+———+
| 100 | 101 | 102 |
| 200 | 201 | 202 |
| 300 | 301 | 302 |
| 400 | 401 | 402 |
+———+———+———+
4 rows in *set
(0.00 sec)

数据更新

单表更新

语法:

update 表名 [[as] 别名] set [别名.]字段 = 值,[别名.]字段 = 值 [where条件];

有些表名可能名称比较长,为了方便操作,可以给这个表名起个简单的别名,更方便操作一些。
如果无别名的时候,表名就是别名。

示例:

mysql> update test1 t set t.a = 2;
Query OK, 4 rows affected (0.00 sec)
Rows matched: 6 Changed: 4 Warnings: 0

mysql> update test1 as t set t.a = 3;
Query OK, 6 rows affected (0.00 sec)
Rows matched: 6 Changed: 6 Warnings: 0

mysql> update test1 set a = 1,b=2;
Query OK, 6 rows affected (0.00 sec)
Rows matched: 6 Changed: 6 Warnings: 0

多表更新

可以同时更新多个表中的数据

语法:

update 表1 [[as] 别名1],表名2 [[as] 别名2]
set [别名.]字段 = 值,[别名.]字段 = 值
[where条件]

示例:

— 无别名方式
update test1,test2 set test1.a = 2 ,test1.b = 2, test2.c1 = 10;
— 无别名方式
update test1,test2 set test1.a = 2 ,test1.b = 2, test2.c1 = 10 where test1.a = test2.c1;
— 别名方式更新
update test1 t1,test2 t2 set t1.a = 2 ,t1.b = 2, t2.c1 = 10 where t1.a = t2.c1;
— 别名的方式更新多个表的多个字段
update test1 as t1,test2 t2 set t1.a = 2 ,t1.b = 2, t2.c1 = 10 where t1.a = t2.c1;

使用建议

建议采用单表方式更新,方便维护。

删除数据操作

使用 delete 删除

delete 单表删除

delete [别名] from 表名 [[as] 别名] [where条件];

注意:
如果无别名的时候,表名就是别名
如果有别名,delete 后面必须写别名
如果没有别名,delete 后面的别名可以省略不写。

示例

— 删除test1表所有记录
delete from test1;
— 删除test1表所有记录
delete test1 from test1;
— 有别名的方式,删除test1表所有记录
delete t1 from test1 t1;
— 有别名的方式删除满足条件的记录
delete t1 from test1 t1 where t1.a>100;

上面的 4 种写法,大家可以认真看一下。

多表删除

可以同时删除多个表中的记录,语法如下:
delete [别名1,别名2] from 表1 [[as] 别名1],表2 [[as] 别名2] [where条件];

说明:
别名可以省略不写,但是需要在 delete 后面跟上表名,多个表名之间用逗号隔开。

示例 1

delete t1 from test1 t1,test2 t2 where t1.a=t2.c2;

删除 test1 表中的记录,条件是这些记录的字段 a 在 test.c2 中存在的记录
看一下运行效果:
— 删除test1
drop table if exists test1;
— 创建test1
create table test1(a int,b int);
— 删除test2
drop table if exists test2;
— 创建test2
create table test2(c1 int,c2 int,c3 int);
— 向test2中插入数据
insert into test2 values (100,101,102),(200,201,202),(300,301,302),(400,401,402);
— 向test1中插入数据
insert into test1 (a,b) select 1,1 union all select 2,2 union all select 2,2;
— 向test1插入数据,数据来源于test2表
insert into test1 (a,b) select c2,c3 from test2 where c1>=200;

mysql> select * from test1;
+———+———+
| a | b |
+———+———+
| 1 | 1 |
| 2 | 2 |
| 2 | 2 |
| 201 | 202 |
| 301 | 302 |
| 401 | 402 |

mysql> select from test2;
+———+———+———+
| c1 | c2 | c3 |
+———+———+———+
| 100 | 101 | 102 |
| 200 | 201 | 202 |
| 300 | 301 | 302 |
| 400 | 401 | 402 |
+———+———+———+
4 rows in *set
(0.00 sec)

mysql> delete t1 from test1 t1,test2 t2 where t1.a=t2.c2;
Query OK, 3 rows affected (0.00 sec)

mysql> select from test1;
+———+———+
| a | b |
+———+———+
| 1 | 1 |
| 2 | 2 |
| 2 | 2 |
+———+———+
3 rows in *set
(0.00 sec)

从上面的输出中可以看到 test1 表中 3 条记录被删除了。

示例 2

delete t2,t1 from test1 t1,test2 t2 where t1.a=t2.c2;

同时对 2 个表进行删除,条件是 test.a=test.c2 的记录
看一下运行效果:
— 删除test1
drop table if exists test1;
— 创建test1
create table test1(a int,b int);
— 删除test2
drop table if exists test2;
— 创建test2
create table test2(c1 int,c2 int,c3 int);
— 向test2中插入数据
insert into test2 values (100,101,102),(200,201,202),(300,301,302),(400,401,402);
— 向test1中插入数据
insert into test1 (a,b) select 1,1 union all select 2,2 union all select 2,2;
— 向test1插入数据,数据来源于test2表
insert into test1 (a,b) select c2,c3 from test2 where c1>=200;

mysql> select from test1;
+———+———+
| a | b |
+———+———+
| 1 | 1 |
| 2 | 2 |
| 2 | 2 |
| 201 | 202 |
| 301 | 302 |
| 401 | 402 |
+———+———+
6 rows in *set
(0.00 sec)

mysql> select from test2;
+———+———+———+
| c1 | c2 | c3 |
+———+———+———+
| 100 | 101 | 102 |
| 200 | 201 | 202 |
| 300 | 301 | 302 |
| 400 | 401 | 402 |
+———+———+———+
4 rows in *set
(0.00 sec)

mysql> delete t2,t1 from test1 t1,test2 t2 where t1.a=t2.c2;
Query OK, 6 rows affected (0.00 sec)

mysql> select from test1;
+———+———+
| a | b |
+———+———+
| 1 | 1 |
| 2 | 2 |
| 2 | 2 |
+———+———+
3 rows in *set
(0.00 sec)

mysql> select from test2;
+———+———+———+
| c1 | c2 | c3 |
+———+———+———+
| 100 | 101 | 102 |
+———+———+———+
1 row in *set
(0.00 sec)

从输出中可以看出 test1 和 test2 总计 6 条记录被删除了。
平时我们用的比较多的方式是delete from 表名这种语法,上面我们介绍了再 delete 后面跟上表名的用法,大家可以在回顾一下,加深记忆。

使用 truncate 删除

语法

truncate 表名;

drop,truncate,delete 区别

  • drop (删除表):删除内容和定义,释放空间,简单来说就是把整个表去掉,以后要新增数据是不可能的,除非新增一个表。drop 语句将删除表的结构被依赖的约束(constrain),触发器(trigger)索引(index),依赖于该表的存储过程/函数将被保留,但其状态会变为:invalid。如果要删除表定义及其数据,请使用 drop table 语句。
  • truncate (清空表中的数据):删除内容、释放空间但不删除定义(保留表的数据结构),与 drop 不同的是,只是清空表数据而已。注意:truncate 不能删除具体行数据,要删就要把整个表清空了。
  • delete (删除表中的数据):delete 语句用于删除表中的行。delete 语句执行删除的过程是每次从表中删除一行,并且同时将该行的删除操作作为事务记录在日志中保存,以便进行进行回滚操作。truncate 与不带 where 的 delete :只删除数据,而不删除表的结构(定义)truncate table 删除表中的所有行,但表结构及其列、约束、索引等保持不变。对于由 foreign key 约束引用的表,不能使用 truncate table ,而应使用不带 where 子句的 delete 语句。由于 truncate table 记录在日志中,所以它不能激活触发器。delete 语句是数据库操作语言(dml),这个操作会放到 rollback segement 中,事务提交之后才生效;如果有相应的 trigger,执行的时候将被触发。truncate、drop 是数据库定义语言(ddl),操作立即生效,原数据不放到 rollback segment 中,不能回滚,操作不触发 trigger。如果有自增列,truncate 方式删除之后,自增列的值会被初始化,delete 方式要分情况(如果数据库被重启了,自增列值也会被初始化,数据库未被重启,则不变)
  • 如果要删除表定义及其数据,请使用 drop table 语句
  • 安全性:小心使用 drop 和 truncate,尤其没有备份的时候,否则哭都来不及
  • 删除速度,一般来说: drop> truncate > delete |

    | drop | truncate | delete | | —- | —- | —- | —- | | 条件删除 | 不支持 | 不支持 | 支持 | | 删除表结构 | 支持 | 不支持 | 不支持 | | 事务的方式删除 | 不支持 | 不支持 | 支持 | | 触发触发器 | 否 | 否 | 是 |

六、MySQL select基础篇,查询到底有几种写法?

DQL

DQL(Data QueryLanguage):数据查询语言,通俗点讲就是从数据库获取数据的,按照 DQL 的语法给数据库发送一条指令,数据库将按需求返回数据。
DQL 分多篇来说,本文属于第 1 篇。

基本语法

select 查询的列 from 表名;

注意:
select 语句中不区分大小写,SELECT 和 select、FROM 和 from 效果一样。
查询的结果放在一个表格中,表格的第 1 行称为列头,第 2 行开始是数据,类属于一个二维数组。

查询常量

select 常量值1,常量值2,常量值3;

如:
mysql> select 1,’b’;
+—-+—-+
| 1 | b |
+—-+—-+
| 1 | b |
+—-+—-+
1 row in set (0.00 sec)

查询表达式

select 表达式;

如:
mysql> select 1+2,310,10/3;
+——-+———+————+
| 1+2 | 3
10 | 10/3 |
+——-+———+————+
| 3 | 30 | 3.3333 |
+——-+———+————+
1 row in set (0.00 sec)

查询函数

select 函数;

如:
mysql> select mod(10,4),isnull(null),ifnull(null,’第一个参数为空返回这个值’),ifnull(1,’第一个参数为空返回这个值,否知返回第一个参数’);
+—————-+———————+——————————————————————————-+————————————————————————————————————————+
| mod(10,4) | isnull(null) | ifnull(null,’第一个参数为空返回这个值’) | ifnull(1,’第一个参数为空返回这个值,否知返回第一个参数’) |
+—————-+———————+——————————————————————————-+————————————————————————————————————————+
| 2 | 1 | 第一个参数为空返回这个值 | 1 |
+—————-+———————+——————————————————————————-+————————————————————————————————————————+
1 row in set (0.00 sec)

说明一下:
mod 函数,对两个参数取模运算。
isnull 函数,判断参数是否为空,若为空返回 1,否则返回 0。
ifnull 函数,2 个参数,判断第一个参数是否为空,如果为空返回第 2 个参数的值,否则返回第 1 个参数的值。

查询指定的字段

select 字段1,字段2,字段3 from 表名;

如:
mysql> drop table if exists test1;
Query OK, 0 rows affected (0.01 sec)

mysql> create table test1(a int not null comment ‘字段a’,b varchar(10) not null default ‘’ comment ‘字段b’);
Query OK, 0 rows affected (0.01 sec)

mysql> insert into test1 values(1,’a’),(2,’b’),(3,’c’);
Query OK, 3 rows affected (0.01 sec)
Records: 3 Duplicates: 0 Warnings: 0

mysql> select a,b from test1;
+—-+—-+
| a | b |
+—-+—-+
| 1 | a |
| 2 | b |
| 3 | c |
+—-+—-+
3 rows in set (0.00 sec)

说明:
test1 表有两个字段 a、b,select a,b from test1;用于查询test1中两个字段的数据。

查询所有列

select * from 表名

说明:
表示返回表中所有字段。
如:
mysql> select
from test1;
+—-+—-+
| a | b |
+—-+—-+
| 1 | a |
| 2 | b |
| 3 | c |
+—-+—-+
3 rows in set (0.00 sec)

列别名

在创建数据表时,一般都会使用英文单词或英文单词缩写来设置字段名,在查询时列名都会以英文的形式显示,这样会给用户查看数据带来不便,这种情况可以使用别名来代替英文列名,增强阅读性。
语法:
select 列 [as] 别名 from 表;

使用双引号创建别名:
mysql> select a “列1”,b “列2” from test1;
+———+———+
| 列1 | 列2 |
+———+———+
| 1 | a |
| 2 | b |
| 3 | c |
+———+———+
3 rows in set (0.00 sec)

使用单引号创建别名:
mysql> select a ‘列1’,b ‘列2’ from test1;;
+———+———+
| 列1 | 列2 |
+———+———+
| 1 | a |
| 2 | b |
| 3 | c |
+———+———+
3 rows in set (0.00 sec)

不用引号创建别名:
mysql> select a 列1,b 列2 from test1;
+———+———+
| 列1 | 列2 |
+———+———+
| 1 | a |
| 2 | b |
| 3 | c |
+———+———+
3 rows in set (0.00 sec)

使用 as 创建别名:
mysql> select a as 列1,b as 列 2 from test1;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ‘2 from test1’ at line 1
mysql> select a as 列1,b as ‘列 2’ from test1;
+———+———-+
| 列1 | 列 2 |
+———+———-+
| 1 | a |
| 2 | b |
| 3 | c |
+———+———-+
3 rows in set (0.00 sec)

别名中有特殊符号的,比如空格,此时别名必须用引号引起来。
懵逼示例,看效果:
mysql> select ‘a’ ‘b’;
+——+
| a |
+——+
| ab |
+——+
1 row in set (0.00 sec)

mysql> select ‘a’ b;
+—-+
| b |
+—-+
| a |
+—-+
1 row in set (0.00 sec)

mysql> select ‘a’ “b”;
+——+
| a |
+——+
| ab |
+——+
1 row in set (0.00 sec)

mysql> select ‘a’ as “b”;
+—-+
| b |
+—-+
| a |
+—-+
1 row in set (0.00 sec)

认真看一下第 1 个和第 3 个返回的结果(列头和数据),是不是懵逼状态,建议这种的最好使用as,as 后面跟上别名。

表别名

select 别名.字段,别名.* from 表名 [as] 别名;

如:
mysql> select t.a,t.b from test1 as t;
+—-+—-+
| a | b |
+—-+—-+
| 1 | a |
| 2 | b |
| 3 | c |
+—-+—-+
3 rows in set (0.00 sec)

mysql> select t.a as ‘列 1’,t.b as 列2 from test1 as t;
+———-+———+
| 列 1 | 列2 |
+———-+———+
| 1 | a |
| 2 | b |
| 3 | c |
+———-+———+
3 rows in set (0.00 sec)

mysql> select t. from test1 as t; ;;
+—-+—-+
| a | b |
+—-+—-+
| 1 | a |
| 2 | b |
| 3 | c |
+—-+—-+
3 rows in *set
(0.00 sec)

mysql> select from test1 as t;
+—-+—-+
| a | b |
+—-+—-+
| 1 | a |
| 2 | b |
| 3 | c |
+—-+—-+
3 rows in *set
(0.00 sec)

总结

  • 建议别名前面跟上 as 关键字
  • 查询数据的时候,避免使用 select *,建议需要什么字段写什么字段

    七、MySQL条件查询,这些写法你都会?

    条件查询

    语法:
    select 列名 from 表名 where 列 运算符 值

说明:
注意关键字 where,where 后面跟上一个或者多个条件,条件是对前面数据的过滤,只有满足 where 后面条件的数据才会被返回。
下面介绍常见的查询运算符。

条件查询运算符

操作符 描述
= 等于
<> 或者 != 不等于
> 大于
< 小于
>= 大于等于
<= 小于等于

等于(=)

select 列名 from 表名 where 列 = 值;

说明:
查询出指定的列和对应的值相等的记录。
值如果是字符串类型,需要用单引号或者双引号引起来。
示例:
mysql> create table test1 (a int,b varchar(10));
Query OK, 0 rows affected (0.01 sec)

mysql> insert into test1 values (1,’abc’),(2,’bbb’);
Query OK, 2 rows affected (0.01 sec)
Records: 2 Duplicates: 0 Warnings: 0

mysql> select from test1;
+———+———+
| a | b |
+———+———+
| 1 | abc |
| 2 | bbb |
+———+———+
2 rows in *set
(0.00 sec)

mysql> select from test1 where a=2;
+———+———+
| a | b |
+———+———+
| 2 | bbb |
+———+———+
1 row in *set
(0.00 sec)

mysql> select from test1 where b = ‘abc’;
+———+———+
| a | b |
+———+———+
| 1 | abc |
+———+———+
1 row in *set
(0.00 sec)

mysql> select from test1 where b = “abc”;
+———+———+
| a | b |
+———+———+
| 1 | abc |
+———+———+
1 row in *set
(0.00 sec)

不等于(<>、!=)

不等于有两种写法:<>或者!=
select 列名 from 表名 where 列 <> 值;
或者
select 列名 from 表名 where 列 != 值;

示例:
mysql> select from test1 where a<>1;
+———+———+
| a | b |
+———+———+
| 2 | bbb |
+———+———+
1 row in *set
(0.00 sec)

mysql> select from test1 where a!=1;
+———+———+
| a | b |
+———+———+
| 2 | bbb |
+———+———+
1 row in *set
(0.00 sec)

注意:
<> 这个是最早的用法。
!=是后来才加上的。
两者意义相同,在可移植性上前者优于后者
故而 sql 语句中尽量使用<>来做不等判断

大于(>)

select 列名 from 表名 where 列 > 值;

示例:
mysql> select from test1 where a>1;
+———+———+
| a | b |
+———+———+
| 2 | bbb |
+———+———+
1 row in *set
(0.00 sec)

mysql> select from test1 where b>’a’;
+———+———+
| a | b |
+———+———+
| 1 | abc |
| 2 | bbb |
+———+———+
2 rows in *set
(0.00 sec)

mysql> select from test1 where b>’ac’;
+———+———+
| a | b |
+———+———+
| 2 | bbb |
+———+———+
1 row in *set
(0.00 sec)

说明:
数值按照大小比较。
字符按照_ASC_II 码对应的值进行比较,比较时按照字符对应的位置一个字符一个字符的比较。
其他几个运算符(<、<=、>=)在此就不介绍了,用法和上面类似,大家可以自己练习一下。

逻辑查询运算符

当我们需要使用多个条件进行查询的时候,需要使用逻辑查询运算符。

逻辑运算符 描述
AND 多个条件都成立
OR 多个条件中满足一个

AND(并且)

select 列名 from 表名 where 条件1 and 条件2;

表示返回满足条件 1 和条件 2 的记录。
示例:
mysql> create table test3(a int not null,b varchar(10) not null);
Query OK, 0 rows affected (0.01 sec)

mysql> insert into test3 (a,b) values (1,’a’),(2,’b’),(2,’c’),(3,’c’);
Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0

mysql> select from test3;
+—-+—-+
| a | b |
+—-+—-+
| 1 | a |
| 2 | b |
| 2 | c |
| 3 | c |
+—-+—-+
4 rows in *set
(0.00 sec)

mysql> select from test3 t where t.a=2 and t.b=’c’;
+—-+—-+
| a | b |
+—-+—-+
| 2 | c |
+—-+—-+
1 row in *set
(0.00 sec)

查询出了 a=2 并且 b=’c’的记录,返回了一条结果。

OR(或者)

select 列名 from 表名 where 条件1 or 条件2;

满足条件 1 或者满足条件 2 的记录都会被返回。
示例:
mysql> select from test3;
+—-+—-+
| a | b |
+—-+—-+
| 1 | a |
| 2 | b |
| 2 | c |
| 3 | c |
+—-+—-+
4 rows in *set
(0.00 sec)

mysql> select from test3 t where t.a=1 or t.b=’c’;
+—-+—-+
| a | b |
+—-+—-+
| 1 | a |
| 2 | c |
| 3 | c |
+—-+—-+
3 rows in *set
(0.00 sec)

查询出了 a=1 或者 b=’c’的记录,返回了 3 条记录。

like(模糊查询)

有个学生表,包含(学生 id,年龄,姓名),当我们需要查询姓“张”的学生的时候,如何查询呢?
此时我们可以使用 sql 中的 like 关键字。语法:
select 列名 from 表名 where 列 like pattern;

pattern 中可以包含通配符,有以下通配符:
%:表示匹配任意一个或多个字符
_:表示匹配任意一个字符。
学生表,查询名字姓“张”的学生,如下:
mysql> create table stu (id int not null comment ‘编号’,age smallint not null comment ‘年龄’,name varchar(10) not null comment ‘姓名’);
Query OK, 0 rows affected (0.01 sec)

mysql> insert into stu values (1,22,’张三’),(2,25,’李四’),(3,26,’张学友’),(4,32,’刘德华’),(5,55,’张学良’);
Query OK, 5 rows affected (0.00 sec)
Records: 5 Duplicates: 0 Warnings: 0

mysql> select from stu;
+——+——-+—————-+
| id | age | name |
+——+——-+—————-+
| 1 | 22 | 张三 |
| 2 | 25 | 李四 |
| 3 | 26 | 张学友 |
| 4 | 32 | 刘德华 |
| 5 | 55 | 张学良 |
+——+——-+—————-+
5 rows in *set
(0.00 sec)

mysql> select from stu a where a.name like ‘张%’;
+——+——-+—————-+
| id | age | name |
+——+——-+—————-+
| 1 | 22 | 张三 |
| 3 | 26 | 张学友 |
| 5 | 55 | 张学良 |
+——+——-+—————-+
3 rows in *set
(0.00 sec)

查询名字中带有’学’的学生,’学’的位置不固定,可以这么查询,如下:
mysql> select from stu a where a.name like ‘%学%’; ;
+——+——-+—————-+
| id | age | name |
+——+——-+—————-+
| 3 | 26 | 张学友 |
| 5 | 55 | 张学良 |
+——+——-+—————-+
2 rows in *set
(0.00 sec)

查询姓’张’,名字 2 个字的学生:
mysql> select from stu a where a.name like ‘张_’;
+——+——-+————+
| id | age | name |
+——+——-+————+
| 1 | 22 | 张三 |
+——+——-+————+
1 row in *set
(0.00 sec)

上面的代表任意一个字符,如果要查询姓’张’的 3 个字的学生,条件变为了’张_‘,2 个下划线符号。

BETWEEN AND(区间查询)

操作符 BETWEEN … AND 会选取介于两个值之间的数据范围,这些值可以是数值、文本或者日期,属于一个闭区间查询。
selec 列名 from 表名 where 列名 between 值1 and 值2;

返回对应的列的值在[值 1,值 2]区间中的记录
使用 between and 可以提高语句的简洁度
两个临界值不要调换位置,只能是大于等于左边的值,并且小于等于右边的值。
示例:
查询年龄在[25,32]的,如下:
mysql> select from stu;
+——+——-+—————-+
| id | age | name |
+——+——-+—————-+
| 1 | 22 | 张三 |
| 2 | 25 | 李四 |
| 3 | 26 | 张学友 |
| 4 | 32 | 刘德华 |
| 5 | 55 | 张学良 |
+——+——-+—————-+
5 rows in *set
(0.00 sec)

mysql> select from stu t where t.age between 25 and 32;
+——+——-+—————-+
| id | age | name |
+——+——-+—————-+
| 2 | 25 | 李四 |
| 3 | 26 | 张学友 |
| 4 | 32 | 刘德华 |
+——+——-+—————-+
3 rows in *set
(0.00 sec)

下面两条 sql 效果一样
select from stu t where t.age between 25 and 32;
select
from stu t where t.age >= 25 and t.age <= 32;

IN 查询

我们需要查询年龄为 10 岁、15 岁、20 岁、30 岁的人,怎么查询呢?可以用 or 查询,如下:
mysql> create table test6(id int,age smallint);
Query OK, 0 rows affected (0.01 sec)

mysql> insert into test6 values(1,14),(2,15),(3,18),(4,20),(5,28),(6,10),(7,10),(8,30);
Query OK, 8 rows affected (0.00 sec)
Records: 8 Duplicates: 0 Warnings: 0

mysql> select from test6;
+———+———+
| id | age |
+———+———+
| 1 | 14 |
| 2 | 15 |
| 3 | 18 |
| 4 | 20 |
| 5 | 28 |
| 6 | 10 |
| 7 | 10 |
| 8 | 30 |
+———+———+
8 rows in *set
(0.00 sec)

mysql> select from test6 t where t.age=10 or t.age=15 or t.age=20 or t.age = 30;
+———+———+
| id | age |
+———+———+
| 2 | 15 |
| 4 | 20 |
| 6 | 10 |
| 7 | 10 |
| 8 | 30 |
+———+———+
5 rows in *set
(0.00 sec)

用了这么多 or,有没有更简单的写法?有,用 IN 查询
IN 操作符允许我们在 WHERE 子句中规定多个值。
select 列名 from 表名 where 字段 in (值1,值2,值3,值4);

in 后面括号中可以包含多个值,对应记录的字段满足 in 中任意一个都会被返回
in 列表的值类型必须一致或兼容
in 列表中不支持通配符。
上面的示例用 IN 实现如下:
mysql> select from test6 t where t.age in (10,15,20,30);
+———+———+
| id | age |
+———+———+
| 2 | 15 |
| 4 | 20 |
| 6 | 10 |
| 7 | 10 |
| 8 | 30 |
+———+———+
5 rows in *set
(0.00 sec)

相对于 or 简洁了很多。

NOT IN 查询

not in 和 in 刚好相反,in 是列表中被匹配的都会被返回,NOT IN 是和列表中都不匹配的会被返回。
select 列名 from 表名 where 字段 not in (值1,值2,值3,值4);

如查询年龄不在 10、15、20、30 之内的,如下:
mysql> select from test6 t where t.age not in (10,15,20,30);
+———+———+
| id | age |
+———+———+
| 1 | 14 |
| 3 | 18 |
| 5 | 28 |
+———+———+
3 rows in *set
(0.00 sec)

NULL 存在的坑

我们先看一下效果,然后在解释,示例如下:
mysql> create table test5 (a int not null,b int,c varchar(10));
Query OK, 0 rows affected (0.01 sec)

mysql> insert into test5 values (1,2,’a’),(3,null,’b’),(4,5,null);
Query OK, 3 rows affected (0.01 sec)
Records: 3 Duplicates: 0 Warnings: 0

mysql> select from test5;
+—-+———+———+
| a | b | c |
+—-+———+———+
| 1 | 2 | a |
| 3 | NULL | b |
| 4 | 5 | NULL |
+—-+———+———+
3 rows *in
set (0.00 sec)

上面我们创建了一个表 test5,3 个字段,a 不能为空,b、c 可以为空,插入了 3 条数据,睁大眼睛看效果了:
mysql> select from test5 where b>0;
+—-+———+———+
| a | b | c |
+—-+———+———+
| 1 | 2 | a |
| 4 | 5 | NULL |
+—-+———+———+
2 rows in *set
(0.00 sec)

mysql> select from test5 where b<=0;
Empty *set
(0.00 sec)

mysql> select from test5 where b=NULL;
Empty *set
(0.00 sec)

mysql> select from test5 t where t.b between 0 and 100;
+—-+———+———+
| a | b | c |
+—-+———+———+
| 1 | 2 | a |
| 4 | 5 | NULL |
+—-+———+———+
2 rows in *set
(0.00 sec)

mysql> select from test5 where c like ‘%’;
+—-+———+———+
| a | b | c |
+—-+———+———+
| 1 | 2 | a |
| 3 | NULL | b |
+—-+———+———+
2 rows in *set
(0.00 sec)

mysql> select from test5 where c in (‘a’,’b’,NULL);
+—-+———+———+
| a | b | c |
+—-+———+———+
| 1 | 2 | a |
| 3 | NULL | b |
+—-+———+———+
2 rows in *set
(0.00 sec)

mysql> select from test5 where c not in (‘a’,’b’,NULL);
Empty *set
(0.00 sec)

认真看一下上面的查询:
上面带有条件的查询,对字段 b 进行条件查询的,b 的值为 NULL 的都没有出现。
对 c 字段进行 like ‘%’查询、in、not 查询,c 中为 NULL 的记录始终没有查询出来。
between and 查询,为空的记录也没有查询出来。
结论:查询运算符、like、between and、in、not in 对 NULL 值查询不起效。
那 NULL 如何查询呢?继续向下看

IS NULL/IS NOT NULL(NULL 值专用查询)

上面介绍的各种运算符对 NULL 值均不起效,mysql 为我们提供了查询空值的语法:IS NULL、IS NOT NULL。

IS NULL(返回值为空的记录)

select 列名 from 表名 where 列 is null;

查询指定的列的值为 NULL 的记录。
如:
mysql> create table test7 (a int,b varchar(10));
Query OK, 0 rows affected (0.01 sec)

mysql> insert into test7 (a,b) values (1,’a’),(null,’b’),(3,null),(null,null),(4,’c’);
Query OK, 5 rows affected (0.00 sec)
Records: 5 Duplicates: 0 Warnings: 0

mysql> select from test7;
+———+———+
| a | b |
+———+———+
| 1 | a |
| NULL | b |
| 3 | NULL |
| NULL | NULL |
| 4 | c |
+———+———+
5 rows in *set
(0.00 sec)

mysql> select from test7 t where t.a is null;
+———+———+
| a | b |
+———+———+
| NULL | b |
| NULL | NULL |
+———+———+
2 rows in *set
(0.00 sec)

mysql> select from test7 t where t.a is null or t.b is null;
+———+———+
| a | b |
+———+———+
| NULL | b |
| 3 | NULL |
| NULL | NULL |
+———+———+
3 rows in *set
(0.00 sec)

IS NULL(返回值不为空的记录)

select 列名 from 表名 where 列 is not null;

查询指定的列的值不为 NULL 的记录。
如:
mysql> select from test7 t where t.a is not null;
+———+———+
| a | b |
+———+———+
| 1 | a |
| 3 | NULL |
| 4 | c |
+———+———+
3 rows in *set
(0.00 sec)

mysql> select from test7 t where t.a is not null and t.b is not null;
+———+———+
| a | b |
+———+———+
| 1 | a |
| 4 | c |
+———+———+
2 rows in *set
(0.00 sec)

<=>(安全等于)

<=>:既可以判断 NULL 值,又可以判断普通的数值,可读性较低,用得较少
示例:
mysql> create table test8 (a int,b varchar(10));
Query OK, 0 rows affected (0.01 sec)

mysql> insert into test8 (a,b) values (1,’a’),(null,’b’),(3,null),(null,null),(4,’c’);
Query OK, 5 rows affected (0.01 sec)
Records: 5 Duplicates: 0 Warnings: 0

mysql> select from test8;
+———+———+
| a | b |
+———+———+
| 1 | a |
| NULL | b |
| 3 | NULL |
| NULL | NULL |
| 4 | c |
+———+———+
5 rows in *set
(0.00 sec)

mysql> select from test8 t where t.a<=>null;
+———+———+
| a | b |
+———+———+
| NULL | b |
| NULL | NULL |
+———+———+
2 rows in *set
(0.00 sec)

mysql> select from test8 t where t.a<=>1;
+———+———+
| a | b |
+———+———+
| 1 | a |
+———+———+
1 row in *set
(0.00 sec)

可以看到<=>可以将 NULL 查询出来。

经典面试题

下面的 2 个 sql 查询结果一样么?
select from students;
select
from students where name like ‘%’;

结果分 2 种情况:
当 name 没有 NULL 值时,返回的结果一样。
当 name 有 NULL 值时,第 2 个 sql 查询不出 name 为 NULL 的记录。

总结

  • like 中的%可以匹配一个到多个任意的字符,_可以匹配任意一个字符
  • 空值查询需要使用 IS NULL 或者 IS NOT NULL,其他查询运算符对 NULL 值无效
  • 建议创建表的时候,尽量设置表的字段不能为空,给字段设置一个默认值
  • <=>(安全等于)玩玩可以,建议少使用
  • sql 方面有问题的欢迎留言?或者加我微信itsoku交流。

    八、详解MySQL排序和分页(order by & limit),及存在的坑

    排序查询(order by)

    电商中:我们想查看今天所有成交的订单,按照交易额从高到低排序,此时我们可以使用数据库中的排序功能来完成。
    排序语法:
    select 字段名 from 表名 order by 字段1 [asc|desc],字段2 [asc|desc];

需要排序的字段跟在order by之后;
asc|desc 表示排序的规则,asc:升序,desc:降序,默认为 asc;
支持多个字段进行排序,多字段排序之间用逗号隔开。

单字段排序

mysql> create table test2(a int,b varchar(10));
Query OK, 0 rows affected (0.01 sec)

mysql> insert into test2 values (10,’jack’),(8,’tom’),(5,’ready’),(100,’javacode’);
Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0

mysql> select from test2;
+———+—————+
| a | b |
+———+—————+
| 10 | jack |
| 8 | tom |
| 5 | ready |
| 100 | javacode |
+———+—————+
4 rows in *set
(0.00 sec)

mysql> select from test2 order by a asc;
+———+—————+
| a | b |
+———+—————+
| 5 | ready |
| 8 | tom |
| 10 | jack |
| 100 | javacode |
+———+—————+
4 rows in *set
(0.00 sec)

mysql> select from test2 order by a desc;
+———+—————+
| a | b |
+———+—————+
| 100 | javacode |
| 10 | jack |
| 8 | tom |
| 5 | ready |
+———+—————+
4 rows in *set
(0.00 sec)

mysql> select from test2 order by a;
+———+—————+
| a | b |
+———+—————+
| 5 | ready |
| 8 | tom |
| 10 | jack |
| 100 | javacode |
+———+—————+
4 rows in *set
(0.00 sec)

多字段排序

比如学生表,先按学生年龄降序,年龄相同时,再按学号升序,如下:
mysql> create table stu(id int not null comment ‘学号’ primary key,age tinyint not null comment ‘年龄’,name varchar(16) comment ‘姓名’);
Query OK, 0 rows affected (0.01 sec)

mysql> insert into stu (id,age,name) values (1001,18,’路人甲Java’),(1005,20,’刘德华’),(1003,18,’张学友’),(1004,20,’张国荣’),(1010,19,’梁朝伟’);
Query OK, 5 rows affected (0.00 sec)
Records: 5 Duplicates: 0 Warnings: 0

mysql> select from stu;
+———+——-+———————-+
| id | age | name |
+———+——-+———————-+
| 1001 | 18 | 路人甲Java |
| 1003 | 18 | 张学友 |
| 1004 | 20 | 张国荣 |
| 1005 | 20 | 刘德华 |
| 1010 | 19 | 梁朝伟 |
+———+——-+———————-+
5 rows in *set
(0.00 sec)

mysql> select from stu order by age desc,id asc;
+———+——-+———————-+
| id | age | name |
+———+——-+———————-+
| 1004 | 20 | 张国荣 |
| 1005 | 20 | 刘德华 |
| 1010 | 19 | 梁朝伟 |
| 1001 | 18 | 路人甲Java |
| 1003 | 18 | 张学友 |
+———+——-+———————-+
5 rows in *set
(0.00 sec)

按别名排序

mysql> select from stu;
+———+——-+———————-+
| id | age | name |
+———+——-+———————-+
| 1001 | 18 | 路人甲Java |
| 1003 | 18 | 张学友 |
| 1004 | 20 | 张国荣 |
| 1005 | 20 | 刘德华 |
| 1010 | 19 | 梁朝伟 |
+———+——-+———————-+
5 rows in *set
(0.00 sec)

mysql> select age ‘年龄’,id as ‘学号’ from stu order by 年龄 asc,学号 desc;
+————+————+
| 年龄 | 学号 |
+————+————+
| 18 | 1003 |
| 18 | 1001 |
| 19 | 1010 |
| 20 | 1005 |
| 20 | 1004 |
+————+————+

按函数排序

有学生表(id:编号,birth:出生日期,name:姓名),如下:
mysql> drop table if exists student;
Query OK, 0 rows affected (0.01 sec)

mysql> CREATE TABLE student (
-> id int(11) NOT NULL COMMENT ‘学号’,
-> birth date NOT NULL COMMENT ‘出生日期’,
-> name varchar(16) DEFAULT NULL COMMENT ‘姓名’,
-> PRIMARY KEY (id)
-> );
Query OK, 0 rows affected (0.01 sec)

mysql> insert into student (id,birth,name) values (1001,’1990-10-10’,’路人甲Java’),(1005,’1960-03-01’,’刘德华’),(1003,’1960-08-16’,’张学友’),(1004,’1968-07-01’,’张国荣’),(1010,’1962-05-16’,’梁朝伟’);
Query OK, 5 rows affected (0.00 sec)
Records: 5 Duplicates: 0 Warnings: 0

mysql>
mysql> SELECT FROM student;
+———+——————+———————-+
| id | birth | name |
+———+——————+———————-+
| 1001 | 1990-10-10 | 路人甲Java |
| 1003 | 1960-08-16 | 张学友 |
| 1004 | 1968-07-01 | 张国荣 |
| 1005 | 1960-03-01 | 刘德华 |
| 1010 | 1962-05-16 | 梁朝伟 |
+———+——————+———————-+
5 rows in *set
(0.00 sec)

需求:按照出生年份升序、编号升序,查询出编号、出生日期、出生年份、姓名,2 种写法如下:
mysql> SELECT id 编号,birth 出生日期,year(birth) 出生年份,name 姓名 from student ORDER BY year(birth) asc,id asc;
+————+———————+———————+———————-+
| 编号 | 出生日期 | 出生年份 | 姓名 |
+————+———————+———————+———————-+
| 1003 | 1960-08-16 | 1960 | 张学友 |
| 1005 | 1960-03-01 | 1960 | 刘德华 |
| 1010 | 1962-05-16 | 1962 | 梁朝伟 |
| 1004 | 1968-07-01 | 1968 | 张国荣 |
| 1001 | 1990-10-10 | 1990 | 路人甲Java |
+————+———————+———————+———————-+
5 rows in set (0.00 sec)

mysql> SELECT id 编号,birth 出生日期,year(birth) 出生年份,name 姓名 from student ORDER BY 出生年份 asc,id asc;
+————+———————+———————+———————-+
| 编号 | 出生日期 | 出生年份 | 姓名 |
+————+———————+———————+———————-+
| 1003 | 1960-08-16 | 1960 | 张学友 |
| 1005 | 1960-03-01 | 1960 | 刘德华 |
| 1010 | 1962-05-16 | 1962 | 梁朝伟 |
| 1004 | 1968-07-01 | 1968 | 张国荣 |
| 1001 | 1990-10-10 | 1990 | 路人甲Java |
+————+———————+———————+———————-+
5 rows in set (0.00 sec)

说明:
year 函数:属于日期函数,可以获取对应日期中的年份。
上面使用了 2 种方式排序,第一种是在 order by 中使用了函数,第二种是使用了别名排序。

where 之后进行排序

有订单数据如下:
mysql> drop table if exists t_order;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> create table t_order(
-> id int not null auto_increment comment ‘订单编号’,
-> price decimal(10,2) not null default 0 comment ‘订单金额’,
-> primary key(id)
-> )comment ‘订单表’;
Query OK, 0 rows affected (0.01 sec)

mysql> insert into t_order (price) values (88.95),(100.68),(500),(300),(20.88),(200.5);
Query OK, 6 rows affected (0.00 sec)
Records: 6 Duplicates: 0 Warnings: 0

mysql> select from t_order;
+——+————+
| id | price |
+——+————+
| 1 | 88.95 |
| 2 | 100.68 |
| 3 | 500.00 |
| 4 | 300.00 |
| 5 | 20.88 |
| 6 | 200.50 |
+——+————+
6 rows in *set
(0.00 sec)

需求:查询订单金额>=100 的,按照订单金额降序排序,显示 2 列数据,列头:订单编号、订单金额,如下:
mysql> select a.id 订单编号,a.price 订单金额 from t_order a where a.price>=100 order by a.price desc;
+———————+———————+
| 订单编号 | 订单金额 |
+———————+———————+
| 3 | 500.00 |
| 4 | 300.00 |
| 6 | 200.50 |
| 2 | 100.68 |
+———————+———————+
4 rows in set (0.00 sec)

limit 介绍

limit 用来限制 select 查询返回的行数,常用于分页等操作。
语法:
select 列 from 表 limit [offset,] count;

说明:
offset:表示偏移量,通俗点讲就是跳过多少行,offset 可以省略,默认为 0,表示跳过 0 行;范围:[0,+∞)。
count:跳过 offset 行之后开始取数据,取 count 行记录;范围:[0,+∞)。
limit 中 offset 和 count 的值不能用表达式。
下面我们列一些常用的示例来加深理解。

获取前 n 行记录

select 列 from 表 limit 0,n;
或者
select 列 from 表 limit n;

示例,获取订单的前 2 条记录,如下:
mysql> create table t_order(
-> id int not null auto_increment comment ‘订单编号’,
-> price decimal(10,2) not null default 0 comment ‘订单金额’,
-> primary key(id)
-> )comment ‘订单表’;
Query OK, 0 rows affected (0.01 sec)

mysql> insert into t_order (price) values (88.95),(100.68),(500),(300),(20.88),(200.5);
Query OK, 6 rows affected (0.01 sec)
Records: 6 Duplicates: 0 Warnings: 0

mysql> select from t_order;
+——+————+
| id | price |
+——+————+
| 1 | 88.95 |
| 2 | 100.68 |
| 3 | 500.00 |
| 4 | 300.00 |
| 5 | 20.88 |
| 6 | 200.50 |
+——+————+
6 rows in *set
(0.00 sec)

mysql> select a.id 订单编号,a.price 订单金额 from t_order a limit 2;
+———————+———————+
| 订单编号 | 订单金额 |
+———————+———————+
| 1 | 88.95 |
| 2 | 100.68 |
+———————+———————+
2 rows in set (0.00 sec)

mysql> select a.id 订单编号,a.price 订单金额 from t_order a limit 0,2;
+———————+———————+
| 订单编号 | 订单金额 |
+———————+———————+
| 1 | 88.95 |
| 2 | 100.68 |
+———————+———————+
2 rows in set (0.00 sec)

获取最大的一条记录

我们需要获取订单金额最大的一条记录,可以这么做:先按照金额降序,然后取第一条记录,如下:
mysql> select a.id 订单编号,a.price 订单金额 from t_order a order by a.price desc;
+———————+———————+
| 订单编号 | 订单金额 |
+———————+———————+
| 3 | 500.00 |
| 4 | 300.00 |
| 6 | 200.50 |
| 2 | 100.68 |
| 1 | 88.95 |
| 5 | 20.88 |
+———————+———————+
6 rows in set (0.00 sec)

mysql> select a.id 订单编号,a.price 订单金额 from t_order a order by a.price desc limit 1;
+———————+———————+
| 订单编号 | 订单金额 |
+———————+———————+
| 3 | 500.00 |
+———————+———————+
1 row in set (0.00 sec)

mysql> select a.id 订单编号,a.price 订单金额 from t_order a order by a.price desc limit 0,1;
+———————+———————+
| 订单编号 | 订单金额 |
+———————+———————+
| 3 | 500.00 |
+———————+———————+
1 row in set (0.00 sec)

获取排名第 n 到 m 的记录

我们需要先跳过 n-1 条记录,然后取 m-n+1 条记录,如下:
select 列 from 表 limit n-1,m-n+1;

如:我们想获取订单金额最高的 3 到 5 名的记录,我们需要跳过 2 条,然后获取 3 条记录,如下:
mysql> select a.id 订单编号,a.price 订单金额 from t_order a order by a.price desc;
+———————+———————+
| 订单编号 | 订单金额 |
+———————+———————+
| 3 | 500.00 |
| 4 | 300.00 |
| 6 | 200.50 |
| 2 | 100.68 |
| 1 | 88.95 |
| 5 | 20.88 |
+———————+———————+
6 rows in set (0.00 sec)

mysql> select a.id 订单编号,a.price 订单金额 from t_order a order by a.price desc limit 2,3;
+———————+———————+
| 订单编号 | 订单金额 |
+———————+———————+
| 6 | 200.50 |
| 2 | 100.68 |
| 1 | 88.95 |
+———————+———————+
3 rows in set (0.00 sec)

分页查询

开发过程中,分页我们经常使用,分页一般有 2 个参数:
page:表示第几页,从 1 开始,范围[1,+∞)
pageSize:每页显示多少条记录,范围[1,+∞)
如:page = 2,pageSize = 10,表示获取第 2 页 10 条数据。
我们使用 limit 实现分页,语法如下:
select 列 from 表名 limit (page - 1) * pageSize,pageSize;

需求:我们按照订单金额降序,每页显示 2 条,依次获取所有订单数据、第 1 页、第 2 页、第 3 页数据,如下:
mysql> select a.id 订单编号,a.price 订单金额 from t_order a order by a.price desc;
+———————+———————+
| 订单编号 | 订单金额 |
+———————+———————+
| 3 | 500.00 |
| 4 | 300.00 |
| 6 | 200.50 |
| 2 | 100.68 |
| 1 | 88.95 |
| 5 | 20.88 |
+———————+———————+
6 rows in set (0.00 sec)

mysql> select a.id 订单编号,a.price 订单金额 from t_order a order by a.price desc limit 0,2;
+———————+———————+
| 订单编号 | 订单金额 |
+———————+———————+
| 3 | 500.00 |
| 4 | 300.00 |
+———————+———————+
2 rows in set (0.00 sec)

mysql> select a.id 订单编号,a.price 订单金额 from t_order a order by a.price desc limit 2,2;
+———————+———————+
| 订单编号 | 订单金额 |
+———————+———————+
| 6 | 200.50 |
| 2 | 100.68 |
+———————+———————+
2 rows in set (0.00 sec)

mysql> select a.id 订单编号,a.price 订单金额 from t_order a order by a.price desc limit 4,2;
+———————+———————+
| 订单编号 | 订单金额 |
+———————+———————+
| 1 | 88.95 |
| 5 | 20.88 |
+———————+———————+
2 rows in set (0.00 sec)

避免踩坑

limit 中不能使用表达式

mysql> select from t_order where limit 1,4+1;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ‘limit 1,4+1’ at line 1
mysql> select
from t_order where limit 1+0;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ‘limit 1+0’ at line 1
mysql>

结论:limit 后面只能够跟明确的数字。

limit 后面的 2 个数字不能为负数

mysql> select from t_order where limit -1;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ‘limit -1’ at line 1
mysql> select
from t_order where limit 0,-1;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ‘limit 0,-1’ at line 1
mysql> select from t_order where limit -1,-1;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version *for
the right syntax to use near ‘limit -1,-1’ at line 1

排序分页存在的坑

准备数据:
mysql> insert into test1 (b) values (1),(2),(3),(4),(2),(2),(2),(2);
Query OK, 8 rows affected (0.01 sec)
Records: 8 Duplicates: 0 Warnings: 0

mysql> select from test1;
+—-+—-+
| a | b |
+—-+—-+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 4 |
| 5 | 2 |
| 6 | 2 |
| 7 | 2 |
| 8 | 2 |
+—-+—-+
8 rows in *set
(0.00 sec)

mysql> select from test1 order by b asc;
+—-+—-+
| a | b |
+—-+—-+
| 1 | 1 |
| 2 | 2 |
| 5 | 2 |
| 6 | 2 |
| 7 | 2 |
| 8 | 2 |
| 3 | 3 |
| 4 | 4 |
+—-+—-+
8 rows in *set
(0.00 sec)

下面我们按照 b 升序,每页 2 条数据,来获取数据。
下面的 sql 依次为第 1 页、第 2 页、第 3 页、第 4 页、第 5 页的数据,如下:
mysql> select from test1 order by b asc limit 0,2;
+—-+—-+
| a | b |
+—-+—-+
| 1 | 1 |
| 2 | 2 |
+—-+—-+
2 rows in *set
(0.00 sec)

mysql> select from test1 order by b asc limit 2,2;
+—-+—-+
| a | b |
+—-+—-+
| 8 | 2 |
| 6 | 2 |
+—-+—-+
2 rows in *set
(0.00 sec)

mysql> select from test1 order by b asc limit 4,2;
+—-+—-+
| a | b |
+—-+—-+
| 6 | 2 |
| 7 | 2 |
+—-+—-+
2 rows in *set
(0.00 sec)

mysql> select from test1 order by b asc limit 6,2;
+—-+—-+
| a | b |
+—-+—-+
| 3 | 3 |
| 4 | 4 |
+—-+—-+
2 rows in *set
(0.00 sec)

mysql> select from test1 order by b asc limit 7,2;
+—-+—-+
| a | b |
+—-+—-+
| 4 | 4 |
+—-+—-+
1 row in *set
(0.00 sec)

上面有 2 个问题:
问题 1:看一下第 2 个 sql 和第 3 个 sql,分别是第 2 页和第 3 页的数据,结果出现了相同的数据,是不是懵逼了。
问题 2:整个表只有 8 条记录,怎么会出现第 5 页的数据呢,又懵逼了。
我们来分析一下上面的原因:主要是 b 字段存在相同的值,当排序过程中存在相同的值时,没有其他排序规则时,mysql 懵逼了,不知道怎么排序了。
就像我们上学站队一样,按照身高排序,那身高一样的时候如何排序呢?身高一样的就乱排了。
建议:排序中存在相同的值时,需要再指定一个排序规则,通过这种排序规则不存在二义性,比如上面可以再加上 a 降序,如下:
mysql> select from test1 order by b asc,a desc;
+—-+—-+
| a | b |
+—-+—-+
| 1 | 1 |
| 8 | 2 |
| 7 | 2 |
| 6 | 2 |
| 5 | 2 |
| 2 | 2 |
| 3 | 3 |
| 4 | 4 |
+—-+—-+
8 rows in *set
(0.00 sec)

mysql> select from test1 order by b asc,a desc limit 0,2;
+—-+—-+
| a | b |
+—-+—-+
| 1 | 1 |
| 8 | 2 |
+—-+—-+
2 rows in *set
(0.00 sec)

mysql> select from test1 order by b asc,a desc limit 2,2;
+—-+—-+
| a | b |
+—-+—-+
| 7 | 2 |
| 6 | 2 |
+—-+—-+
2 rows in *set
(0.00 sec)

mysql> select from test1 order by b asc,a desc limit 4,2;
+—-+—-+
| a | b |
+—-+—-+
| 5 | 2 |
| 2 | 2 |
+—-+—-+
2 rows in *set
(0.00 sec)

mysql> select from test1 order by b asc,a desc limit 6,2;
+—-+—-+
| a | b |
+—-+—-+
| 3 | 3 |
| 4 | 4 |
+—-+—-+
2 rows in *set
(0.00 sec)

mysql> select from test1 order by b asc,a desc limit 8,2;
Empty *set
(0.00 sec)

看上面的结果,分页数据都正常了,第 5 页也没有数据了。

总结

  • order by … [asc|desc]用于对查询结果排序,asc:升序,desc:降序,asc|desc 可以省略,默认为 asc
  • limit 用来限制查询结果返回的行数,有 2 个参数(offset,count),offset:表示跳过多少行,count:表示跳过 offset 行之后取 count 行
  • limit 中 offset 可以省略,默认值为 0
  • limit 中 offset 和 count 都必须大于等于 0
  • limit 中 offset 和 count 的值不能用表达式
  • 分页排序时,排序不要有二义性,二义性情况下可能会导致分页结果乱序,可以在后面追加一个主键排序

    九、MySQL分组查询(group by、having)详解

    分组查询

    语法:
    SELECT column, group_function,… FROM table
    [WHERE condition]
    GROUP BY group_by_expression
    [HAVING group_condition];

说明:
group_function:聚合函数。
group_by_expression:分组表达式,多个之间用逗号隔开。
group_condition:分组之后对数据进行过滤。
分组中,select 后面只能有两种类型的列:

  1. 出现在 group by 后的列
  2. 或者使用聚合函数的列

    聚合函数

    | 函数名称 | 作用 | | —- | —- | | max | 查询指定列的最大值 | | min | 查询指定列的最小值 | | count | 统计查询结果的行数 | | sum | 求和,返回指定列的总和 | | avg | 求平均值,返回指定列数据的平均值 |

分组时,可以使用使用上面的聚合函数。

准备数据

drop table if exists t_order;

— 创建订单表
create table t_order(
id int not null AUTO_INCREMENT COMMENT ‘订单id’,
user_id bigint not null comment ‘下单人id’,
user_name varchar(16) not null default ‘’ comment ‘用户名’,
price decimal(10,2) not null default 0 comment ‘订单金额’,
the_year SMALLINT not null comment ‘订单创建年份’,
PRIMARY KEY (id)
) comment ‘订单表’;

— 插入数据
insert into t_order(user_id,user_name,price,the_year) values
(1001,’路人甲Java’,11.11,’2017’),
(1001,’路人甲Java’,22.22,’2018’),
(1001,’路人甲Java’,88.88,’2018’),
(1002,’刘德华’,33.33,’2018’),
(1002,’刘德华’,12.22,’2018’),
(1002,’刘德华’,16.66,’2018’),
(1002,’刘德华’,44.44,’2019’),
(1003,’张学友’,55.55,’2018’),
(1003,’张学友’,66.66,’2019’);

mysql> select from t_order;
+——+————-+———————-+———-+—————+
| id | user_id | user_name | price | the_year |
+——+————-+———————-+———-+—————+
| 1 | 1001 | 路人甲Java | 11.11 | 2017 |
| 2 | 1001 | 路人甲Java | 22.22 | 2018 |
| 3 | 1001 | 路人甲Java | 88.88 | 2018 |
| 4 | 1002 | 刘德华 | 33.33 | 2018 |
| 5 | 1002 | 刘德华 | 12.22 | 2018 |
| 6 | 1002 | 刘德华 | 16.66 | 2018 |
| 7 | 1002 | 刘德华 | 44.44 | 2019 |
| 8 | 1003 | 张学友 | 55.55 | 2018 |
| 9 | 1003 | 张学友 | 66.66 | 2019 |
+——+————-+———————-+———-+—————+
9 rows in *set
(0.00 sec)

单字段分组

需求:查询每个用户下单数量,输出:用户 id、下单数量,如下:
mysql> SELECT
user_id 用户id, COUNT(id) 下单数量
FROM
t_order
GROUP BY user_id;
+—————+———————+
| 用户id | 下单数量 |
+—————+———————+
| 1001 | 3 |
| 1002 | 4 |
| 1003 | 2 |
+—————+———————+
3 rows in set (0.00 sec)

多字段分组

需求:查询每个用户每年下单数量,输出字段:用户 id、年份、下单数量,如下:
mysql> SELECT
user_id 用户id, the_year 年份, COUNT(id) 下单数量
FROM
t_order
GROUP BY user_id , the_year;
+—————+————+———————+
| 用户id | 年份 | 下单数量 |
+—————+————+———————+
| 1001 | 2017 | 1 |
| 1001 | 2018 | 2 |
| 1002 | 2018 | 3 |
| 1002 | 2019 | 1 |
| 1003 | 2018 | 1 |
| 1003 | 2019 | 1 |
+—————+————+———————+
6 rows in set (0.00 sec)

分组前筛选数据

分组前对数据进行筛选,使用 where 关键字
需求:需要查询 2018 年每个用户下单数量,输出:用户 id、下单数量,如下:
mysql> SELECT
user_id 用户id, COUNT(id) 下单数量
FROM
t_order t
WHERE
t.the_year = 2018
GROUP BY user_id;
+—————+———————+
| 用户id | 下单数量 |
+—————+———————+
| 1001 | 2 |
| 1002 | 3 |
| 1003 | 1 |
+—————+———————+
3 rows in set (0.00 sec)

分组后筛选数据

分组后对数据筛选,使用 having 关键字
需求:查询 2018 年订单数量大于 1 的用户,输出:用户 id,下单数量,如下:
方式 1:
mysql> SELECT
user_id 用户id, COUNT(id) 下单数量
FROM
t_order t
WHERE
t.the_year = 2018
GROUP BY user_id
HAVING count(id)>=2;
+—————+———————+
| 用户id | 下单数量 |
+—————+———————+
| 1001 | 2 |
| 1002 | 3 |
+—————+———————+
2 rows in set (0.00 sec)

方式 2:
mysql> SELECT
user_id 用户id, count(id) 下单数量
FROM
t_order t
WHERE
t.the_year = 2018
GROUP BY user_id
HAVING 下单数量>=2;
+—————+———————+
| 用户id | 下单数量 |
+—————+———————+
| 1001 | 2 |
| 1002 | 3 |
+—————+———————+
2 rows in set (0.00 sec)

where 和 having 的区别

where 是在分组(聚合)前对记录进行筛选,而 having 是在分组结束后的结果里筛选,最后返回整个 sql 的查询结果。
可以把 having 理解为两级查询,即含 having 的查询操作先获得不含 having 子句时的 sql 查询结果表,然后在这个结果表上使用 having 条件筛选出符合的记录,最后返回这些记录,因此,having 后是可以跟聚合函数的,并且这个聚集函数不必与 select 后面的聚集函数相同。

分组后排序

需求:获取每个用户最大金额,然后按照最大金额倒序,输出:用户 id,最大金额,如下:
mysql> SELECT
user_id 用户id, max(price) 最大金额
FROM
t_order t
GROUP BY user_id
ORDER BY 最大金额 desc;
+—————+———————+
| 用户id | 最大金额 |
+—————+———————+
| 1001 | 88.88 |
| 1003 | 66.66 |
| 1002 | 44.44 |
+—————+———————+
3 rows in set (0.00 sec)

where & group by & having & order by & limit 一起协作

where、group by、having、order by、limit 这些关键字一起使用时,先后顺序有明确的限制,语法如下:
select 列 from
表名
where [查询条件]
group by [分组表达式]
having [分组过滤条件]
order by [排序条件]
limit [offset,] count;

注意:
写法上面必须按照上面的顺序来写。
示例:
需求:查询出 2018 年,下单数量大于等于 2 的,按照下单数量降序排序,最后只输出第 1 条记录,显示:用户 id,下单数量,如下:
mysql> SELECT
user_id 用户id, COUNT(id) 下单数量
FROM
t_order t
WHERE
t.the_year = 2018
GROUP BY user_id
HAVING count(id)>=2
ORDER BY 下单数量 DESC
LIMIT 1;
+—————+———————+
| 用户id | 下单数量 |
+—————+———————+
| 1002 | 3 |
+—————+———————+
1 row in set (0.00 sec)

mysql 分组中的坑

本文开头有介绍,分组中 select 后面的列只能有 2 种:

  1. 出现在 group by 后面的列
  2. 使用聚合函数的列

oracle、sqlserver、db2 中也是按照这种规范来的。
文中使用的是 5.7 版本,默认是按照这种规范来的。
mysql 早期的一些版本,没有上面这些要求,select 后面可以跟任何合法的列。

示例

需求:获取每个用户下单的最大金额及下单的年份,输出:用户 id,最大金额,年份,写法如下:
mysql> select
user_id 用户id, max(price) 最大金额, the_year 年份
FROM t_order t
GROUP BY t.user_id;
ERROR 1055 (42000): Expression #3 of SELECT list is not in GROUP BY clause and contains nonaggregated column ‘javacode2018.t.the_year’ which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

上面的 sql 报错了,原因因为the_year不符合上面说的 2 条规则(select 后面的列必须出现在 group by 中或者使用聚合函数),而sql_mode限制了这种规则,我们看一下sql_mode的配置:
mysql> select @@sql_mode;
+—————————————————————————————————————————————————————————————————————-+
| @@sql_mode |
+—————————————————————————————————————————————————————————————————————-+
| ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION |
+—————————————————————————————————————————————————————————————————————-+
1 row in set (0.00 sec)

sql_mode 中包含了ONLY_FULL_GROUP_BY,这个表示 select 后面的列必须符合上面的说的 2 点规范。
可以将ONLY_FULL_GROUP_BY去掉,select 后面就可以加任意列了,我们来看一下效果。
修改 mysql 中的my.ini文件:
sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION

重启 mysql,再次运行,效果如下:
mysql> select
user_id 用户id, max(price) 最大金额, the_year 年份
FROM t_order t
GROUP BY t.user_id;
+—————+———————+————+
| 用户id | 最大金额 | 年份 |
+—————+———————+————+
| 1001 | 88.88 | 2017 |
| 1002 | 44.44 | 2018 |
| 1003 | 66.66 | 2018 |
+—————+———————+————+
3 rows in set (0.03 sec)

看一下上面的数据,第一条88.88的年份是2017年,我们再来看一下原始数据:
mysql> select from t_order;
+——+————-+———————-+———-+—————+
| id | user_id | user_name | price | the_year |
+——+————-+———————-+———-+—————+
| 1 | 1001 | 路人甲Java | 11.11 | 2017 |
| 2 | 1001 | 路人甲Java | 22.22 | 2018 |
| 3 | 1001 | 路人甲Java | 88.88 | 2018 |
| 4 | 1002 | 刘德华 | 33.33 | 2018 |
| 5 | 1002 | 刘德华 | 12.22 | 2018 |
| 6 | 1002 | 刘德华 | 16.66 | 2018 |
| 7 | 1002 | 刘德华 | 44.44 | 2019 |
| 8 | 1003 | 张学友 | 55.55 | 2018 |
| 9 | 1003 | 张学友 | 66.66 | 2019 |
+——+————-+———————-+———-+—————+
9 rows in *set
(0.00 sec)

对比一下,user_id=1001、price=88.88 是第 3 条数据,即 the_year 是 2018 年,但是上面的分组结果是 2017 年,结果和我们预期的不一致,此时 mysql 对这种未按照规范来的列,乱序了,mysql 取的是第一条。
正确的写法,提供两种,如下:
mysql> SELECT
user_id 用户id,
price 最大金额,
the_year 年份
FROM
t_order t1
WHERE
(t1.user_id , t1.price)
IN
(SELECT
t.user_id, MAX(t.price)
FROM
t_order t
GROUP BY t.user_id);
+—————+———————+————+
| 用户id | 最大金额 | 年份 |
+—————+———————+————+
| 1001 | 88.88 | 2018 |
| 1002 | 44.44 | 2019 |
| 1003 | 66.66 | 2019 |
+—————+———————+————+
3 rows in set (0.00 sec)

mysql> SELECT
user_id 用户id,
price 最大金额,
the_year 年份
FROM
t_order t1,(SELECT
t.user_id uid, MAX(t.price) pc
FROM
t_order t
GROUP BY t.user_id) t2
WHERE
t1.user_id = t2.uid
AND t1.price = t2.pc;
+—————+———————+————+
| 用户id | 最大金额 | 年份 |
+—————+———————+————+
| 1001 | 88.88 | 2018 |
| 1002 | 44.44 | 2019 |
| 1003 | 66.66 | 2019 |
+—————+———————+————+
3 rows in set (0.00 sec)

上面第 1 种写法,比较少见,in中使用了多字段查询。
建议:在写分组查询的时候,最好按照标准的规范来写,select 后面出现的列必须在 group by 中或者必须使用聚合函数。

总结

  1. 在写分组查询的时候,最好按照标准的规范来写,select 后面出现的列必须在 group by 中或者必须使用聚合函数
  2. select 语法顺序:select、from、where、group by、having、order by、limit,顺序不能搞错了,否则报错。
  3. in 多列查询的使用,下去可以试试

    十、MySQL常用函数汇总

    MySQL 数值型函数

    | 函数名称 | 作 用 | | —- | —- | | abs | 求绝对值 | | sqrt | 求二次方根 | | mod | 求余数 | | ceil 和 ceiling | 两个函数功能相同,都是返回不小于参数的最小整数,即向上取整 | | floor | 向下取整,返回值转化为一个 BIGINT | | rand | 生成一个 0~1 之间的随机数,传入整数参数是,用来产生重复序列 | | round | 对所传参数进行四舍五入 | | sign | 返回参数的符号 | | pow 和 power | 两个函数的功能相同,都是所传参数的次方的结果值 | | sin | 求正弦值 | | asin | 求反正弦值,与函数 SIN 互为反函数 | | cos | 求余弦值 | | acos | 求反余弦值,与函数 COS 互为反函数 | | tan | 求正切值 | | atan | 求反正切值,与函数 TAN 互为反函数 | | cot | 求余切值 |

abs:求绝对值

函数 ABS(x) 返回 x 的绝对值。正数的绝对值是其本身,负数的绝对值为其相反数,0 的绝对值是 0。
mysql> select abs(5),abs(-2.4),abs(-24),abs(0);
+————+—————-+—————+————+
| abs(5) | abs(-2.4) | abs(-24) | abs(0) |
+————+—————-+—————+————+
| 5 | 2.4 | 24 | 0 |
+————+—————-+—————+————+
1 row in set (0.00 sec)

sqrt:求二次方跟(开方)

函数 SQRT(x) 返回非负数 x 的二次方根。负数没有平方根,返回结果为 NULL。
mysql> select sqrt(25),sqrt(120),sqrt(-9);
+—————+——————————+—————+
| sqrt(25) | sqrt(120) | sqrt(-9) |
+—————+——————————+—————+
| 5 | 10.954451150103322 | NULL |
+—————+——————————+—————+
1 row in set (0.00 sec)

mod:求余数

函数 MOD(x,y) 返回 x 被 y 除后的余数,MOD() 对于带有小数部分的数值也起作用,它返回除法运算后的余数。
mysql> select mod(63,8),mod(120,10),mod(15.5,3);
+—————-+——————-+——————-+
| mod(63,8) | mod(120,10) | mod(15.5,3) |
+—————-+——————-+——————-+
| 7 | 0 | 0.5 |
+—————-+——————-+——————-+
1 row in set (0.00 sec)

ceil 和 ceiling:向上取整

函数 CEIL(x) 和 CEILING(x) 的意义相同,返回不小于 x 的最小整数值,返回值转化为一个 BIGINT。
mysql> select ceil(-2.5),ceiling(2.5);
+——————+———————+
| ceil(-2.5) | ceiling(2.5) |
+——————+———————+
| -2 | 3 |
+——————+———————+
1 row in set (0.00 sec)

floor:向下取整

floor(x) 函数返回小于 x 的最大整数值。
mysql> select floor(5),floor(5.66),floor(-4),floor(-4.66);
+—————+——————-+—————-+———————+
| floor(5) | floor(5.66) | floor(-4) | floor(-4.66) |
+—————+——————-+—————-+———————+
| 5 | 5 | -4 | -5 |
+—————+——————-+—————-+———————+
1 row in set (0.00 sec)

rand:生成一个随机数

生成一个 0~1 之间的随机数,传入整数参数是,用来产生重复序列
mysql> select rand(), rand(), rand();
+——————————+——————————+——————————+
| rand() | rand() | rand() |
+——————————+——————————+——————————+
| 0.5224735778965741 | 0.3678060549942833 | 0.2716095720153391 |
+——————————+——————————+——————————+
1 row in set (0.00 sec)

mysql> select rand(1),rand(2),rand(1);
+——————————-+——————————+——————————-+
| rand(1) | rand(2) | rand(1) |
+——————————-+——————————+——————————-+
| 0.40540353712197724 | 0.6555866465490187 | 0.40540353712197724 |
+——————————-+——————————+——————————-+
1 row in set (0.00 sec)

mysql> select rand(1),rand(2),rand(1);
+——————————-+——————————+——————————-+
| rand(1) | rand(2) | rand(1) |
+——————————-+——————————+——————————-+
| 0.40540353712197724 | 0.6555866465490187 | 0.40540353712197724 |
+——————————-+——————————+——————————-+
1 row in set (0.00 sec)

round:四舍五入函数

返回最接近于参数 x 的整数;ROUND(x,y) 函数对参数 x 进行四舍五入的操作,返回值保留小数点后面指定的 y 位。
mysql> select round(-6.6),round(-8.44),round(3.44);
+——————-+———————+——————-+
| round(-6.6) | round(-8.44) | round(3.44) |
+——————-+———————+——————-+
| -7 | -8 | 3 |
+——————-+———————+——————-+
1 row in set (0.00 sec)

mysql> select round(-6.66,1),round(3.33,3),round(88.66,-1),round(88.46,-2);
+————————+———————-+————————-+————————-+
| round(-6.66,1) | round(3.33,3) | round(88.66,-1) | round(88.46,-2) |
+————————+———————-+————————-+————————-+
| -6.7 | 3.330 | 90 | 100 |
+————————+———————-+————————-+————————-+
1 row in set (0.00 sec)

sign:返回参数的符号

返回参数的符号,x 的值为负、零和正时返回结果依次为 -1、0 和 1。
mysql> select sign(-6),sign(0),sign(34);
+—————+————-+—————+
| sign(-6) | sign(0) | sign(34) |
+—————+————-+—————+
| -1 | 0 | 1 |
+—————+————-+—————+
1 row in set (0.00 sec)

pow 和 power:次方函数

POW(x,y) 函数和 POWER(x,y) 函数用于计算 x 的 y 次方。
mysql> select pow(5,-2),pow(10,3),pow(100,0),power(4,3),power(6,-3);
+—————-+—————-+——————+——————+———————————+
| pow(5,-2) | pow(10,3) | pow(100,0) | power(4,3) | power(6,-3) |
+—————-+—————-+——————+——————+———————————+
| 0.04 | 1000 | 1 | 64 | 0.004629629629629629 |
+—————-+—————-+——————+——————+———————————+
1 row in set (0.00 sec)

sin:正弦函数

SIN(x) 返回 x 的正弦值,其中 x 为弧度值。
mysql> select sin(1),sin(0.5pi()),pi();
+——————————+———————-+—————+
| sin(1) | sin(0.5
pi()) | pi() |
+——————————+———————-+—————+
| 0.8414709848078965 | 1 | 3.141593 |
+——————————+———————-+—————+
1 row in set (0.00 sec)

注:PI() 函数返回圆周率(3.141593)
其他几个三角函数在此就不说了,有兴趣的可以自己去练习一下。

MySQL 字符串函数

函数名称 作 用
length 计算字符串长度函数,返回字符串的字节长度
concat 合并字符串函数,返回结果为连接参数产生的字符串,参数可以使一个或多个
insert 替换字符串函数
lower 将字符串中的字母转换为小写
upper 将字符串中的字母转换为大写
left 从左侧字截取符串,返回字符串左边的若干个字符
right 从右侧字截取符串,返回字符串右边的若干个字符
trim 删除字符串左右两侧的空格
replace 字符串替换函数,返回替换后的新字符串
substr 和 substring 截取字符串,返回从指定位置开始的指定长度的字符换
reverse 字符串反转(逆序)函数,返回与原始字符串顺序相反的字符串

length:返回字符串直接长度

返回值为字符串的字节长度,使用 uft8(UNICODE 的一种变长字符编码,又称万国码)编码字符集时,一个汉字是 3 个字节,一个数字或字母是一个字节。
mysql> select length(‘javacode2018’),length(‘路人甲Java’),length(‘路人’);
+————————————+————————————-+—————————+
| length(‘javacode2018’) | length(‘路人甲Java’) | length(‘路人’) |
+————————————+————————————-+—————————+
| 12 | 13 | 6 |
+————————————+————————————-+—————————+
1 row in set (0.00 sec)

concat:合并字符串

CONCAT(sl,s2,…) 函数返回结果为连接参数产生的字符串,或许有一个或多个参数。
若有任何一个参数为 NULL,则返回值为 NULL。若所有参数均为非二进制字符串,则结果为非二进制字符串。若自变量中含有任一二进制字符串,则结果为一个二进制字符串。
mysql> select concat(‘路人甲’,’java’),concat(‘路人甲’,null,’java’);
+——————————————+————————————————-+
| concat(‘路人甲’,’java’) | concat(‘路人甲’,null,’java’) |
+——————————————+————————————————-+
| 路人甲java | NULL |
+——————————————+————————————————-+
1 row in set (0.00 sec)

insert:替换字符串

INSERT(s1,x,len,s2) 返回字符串 s1,子字符串起始于 x 位置,并且用 len 个字符长的字符串代替 s2。
x 的值从 1 开始,第一个字符的 x=1,若 x 超过字符串长度,则返回值为原始字符串。
假如 len 的长度大于其他字符串的长度,则从位置 x 开始替换。
若任何一个参数为 NULL,则返回值为 NULL。
mysql> select
-> insert(‘路人甲Java’, 2, 4, ‘‘) AS col1,
-> insert(‘路人甲Java’, -1, 4,’
‘) AS col2,
-> insert(‘路人甲Java’, 3, 20,’‘) AS col3;
+————-+———————-+—————+
| col1 | col2 | col3 |
+————-+———————-+—————+
| 路
va | 路人甲Java | 路人 |
+————-+———————-+—————+
1 row in
set** (0.00 sec)

lower:将字母转换成小写

LOWER(str) 可以将字符串 str 中的字母字符全部转换成小写。
mysql> select lower(‘路人甲JAVA’);
+————————————+
| lower(‘路人甲JAVA’) |
+————————————+
| 路人甲java |
+————————————+
1 row in set (0.00 sec)

upper:将字母转换成大写

UPPER(str) 可以将字符串 str 中的字母字符全部转换成大写。
mysql> select upper(‘路人甲java’);
+————————————+
| upper(‘路人甲java’) |
+————————————+
| 路人甲JAVA |
+————————————+
1 row in set (0.00 sec)

left:从左侧截取字符串

LEFT(s,n) 函数返回字符串 s 最左边的 n 个字符,s=1 表示第一个字符。
mysql> select left(‘路人甲JAVA’,2),left(‘路人甲JAVA’,10),left(‘路人甲JAVA’,-1);
+————————————-+—————————————+—————————————+
| left(‘路人甲JAVA’,2) | left(‘路人甲JAVA’,10) | left(‘路人甲JAVA’,-1) |
+————————————-+—————————————+—————————————+
| 路人 | 路人甲JAVA | |
+————————————-+—————————————+—————————————+
1 row in set (0.00 sec)

right:从右侧截取字符串

RIGHT(s,n) 函数返回字符串 s 最右边的 n 个字符。
mysql> select right(‘路人甲JAVA’,1),right(‘路人甲JAVA’,10),right(‘路人甲JAVA’,-1);
+—————————————+—————————————-+—————————————-+
| right(‘路人甲JAVA’,1) | right(‘路人甲JAVA’,10) | right(‘路人甲JAVA’,-1) |
+—————————————+—————————————-+—————————————-+
| A | 路人甲JAVA | |
+—————————————+—————————————-+—————————————-+
1 row in set (0.00 sec)

trim:删除字符串两侧空格

TRIM(s) 删除字符串 s 两侧的空格。
mysql> select ‘[ 路人甲Java ]’,concat(‘[‘,trim(‘ 路人甲Java ‘),’]’);
+———————————-+——————————————————————-+
| [ 路人甲Java ] | concat(‘[‘,trim(‘ 路人甲Java ‘),’]’) |
+———————————-+——————————————————————-+
| [ 路人甲Java ] | [路人甲Java] |
+———————————-+——————————————————————-+
1 row in set (0.00 sec)

replace:字符串替换

REPLACE(s,s1,s2) 使用字符串 s2 替换字符串 s 中所有的字符串 s1。

substr 和 substring:截取字符串

substr(str,pos)
substr(str from pos)
substr(str,pos,len)
substr(str from pos for len)
substr()是 substring()的同义词。
没有 len 参数的形式是字符串 str 从位置 pos 开始返回一个子字符串。
带有 len 参数的形式是字符串 str 从位置 pos 开始返回长度为 len 的子字符串。
使用 FROM 的形式是标准的 SQL 语法。
也可以对 pos 使用负值,在这种情况下,子字符串的开头是字符串末尾的 pos 字符,而不是开头。在这个函数的任何形式中 pos 可以使用负值。
对于所有形式的 substring(),从中提取子串的字符串中第一个字符的位置被认为是 1。
/ 第三个字符之后的子字符串:inese /
SELECT substring(‘chinese’, 3);
/ 倒数第三个字符之后的子字符串:ese /
SELECT substring(‘chinese’, -3);
/ 第三个字符之后的两个字符:in /
SELECT substring(‘chinese’, 3, 2);
/ 倒数第三个字符之后的两个字符:es /
SELECT substring(‘chinese’, -3, 2);
/ 第三个字符之后的子字符串:inese /
SELECT substring(‘chinese’ FROM 3);
/ 倒数第三个字符之后的子字符串:ese /
SELECT substring(‘chinese’ FROM -3);
/ 第三个字符之后的两个字符:in /
SELECT substring(‘chinese’ FROM 3 FOR 2);
/ 倒数第三个字符之后的两个字符:es /
SELECT substring(‘chinese’ FROM -3 FOR 2);

reverse:反转字符串

REVERSE(s) 可以将字符串 s 反转,返回的字符串的顺序和 s 字符串的顺序相反。
mysql> select reverse(‘路人甲Java’);
+—————————————+
| reverse(‘路人甲Java’) |
+—————————————+
| avaJ甲人路 |
+—————————————+
1 row in set (0.00 sec)

MySQL 日期和时间函数

函数名称 作 用
curdate 和 current_date 两个函数作用相同,返回当前系统的日期值
curtime 和 current_time 两个函数作用相同,返回当前系统的时间值
now 和 sysdate 两个函数作用相同,返回当前系统的日期和时间值
unix_timestamp 获取 UNIX 时间戳函数,返回一个以 UNIX 时间戳为基础的无符号整数
from_unixtime 将 UNIX 时间戳转换为时间格式,与 UNIX_TIMESTAMP 互为反函数
month 获取指定日期中的月份
monthname 获取指定日期中的月份英文名称
dayname 获取指定曰期对应的星期几的英文名称
dayofweek 获取指定日期是一周中是第几天,返回值范围是 1~7,1=周日
week 获取指定日期是一年中的第几周,返回值的范围是否为 0〜52 或 1〜53
dayofyear 获取指定曰期是一年中的第几天,返回值范围是 1~366
dayofmonth 获取指定日期是一个月中是第几天,返回值范围是 1~31
year 获取年份,返回值范围是 1970〜2069
time_to_sec 将时间参数转换为秒数
sec_to_time 将秒数转换为时间,与 TIME_TO_SEC 互为反函数
date_add 和 adddate 两个函数功能相同,都是向日期添加指定的时间间隔
date_sub 和 subdate 两个函数功能相同,都是向日期减去指定的时间间隔
addtime 时间加法运算,在原始时间上添加指定的时间
subtime 时间减法运算,在原始时间上减去指定的时间
datediff 获取两个日期之间间隔,返回参数 1 减去参数 2 的值
date_format 格式化指定的日期,根据参数返回指定格式的值
weekday 获取指定日期在一周内的对应的工作日索引

curdate 和 current_date:两个函数作用相同,返回当前系统的日期值

CURDATE() 和 CURRENT_DATE() 函数的作用相同,将当前日期按照“YYYY-MM-DD”或“YYYYMMDD”格式的值返回,具体格式根据函数用在字符串或数字语境中而定,返回的date类型。
mysql> select curdate(),current_date(),current_date()+1;
+——————+————————+—————————+
| curdate() | current_date() | current_date()+1 |
+——————+————————+—————————+
| 2019-09-17 | 2019-09-17 | 20190918 |
+——————+————————+—————————+
1 row in set (0.00 sec)

curtime 和 current_time:获取系统当前时间

CURTIME() 和 CURRENT_TIME() 函数的作用相同,将当前时间以“HH:MM:SS”或“HHMMSS”格式返回,具体格式根据函数用在字符串或数字语境中而定,返回time类型。
mysql> select curtime(),current_time(),current_time()+1;
+—————-+————————+—————————+
| curtime() | current_time() | current_time()+1 |
+—————-+————————+—————————+
| 16:11:25 | 16:11:25 | 161126 |
+—————-+————————+—————————+
1 row in set (0.00 sec)

now 和 sysdate:获取当前时间日期

NOW() 和 SYSDATE() 函数的作用相同,都是返回当前日期和时间值,格式为“YYYY-MM-DD HH:MM:SS”或“YYYYMMDDHHMMSS”,具体格式根据函数用在字符串或数字语境中而定,返回datetime类型。
mysql> select now(),sysdate();
+——————————-+——————————-+
| now() | sysdate() |
+——————————-+——————————-+
| 2019-09-17 16:13:28 | 2019-09-17 16:13:28 |
+——————————-+——————————-+
1 row in set (0.00 sec)

unix_timestamp:获取 UNIX 时间戳

UNIX_TIMESTAMP(date) 若无参数调用,返回一个无符号整数类型的 UNIX 时间戳(’1970-01-01 00:00:00’GMT 之后的秒数)。
mysql> select unix_timestamp(),unix_timestamp(now()),now(),unix_timestamp(‘2019-09-17 12:00:00’);
+—————————+———————————-+——————————-+———————————————————-+
| unix_timestamp() | unix_timestamp(now()) | now() | unix_timestamp(‘2019-09-17 12:00:00’) |
+—————————+———————————-+——————————-+———————————————————-+
| 1568710893 | 1568710893 | 2019-09-17 17:01:33 | 1568692800 |
+—————————+———————————-+——————————-+———————————————————-+
1 row in set (0.00 sec)

from_unixtime:时间戳转日期

FROM_UNIXTIME(unix_timestamp[,format]) 函数把 UNIX 时间戳转换为普通格式的日期时间值,与 UNIX_TIMESTAMP () 函数互为反函数。
有 2 个参数:
unix_timestamp:时间戳(秒)
format:要转化的格式 比如“”%Y-%m-%d“” 这样格式化之后的时间就是 2017-11-30
可以有的形式:

格式 说明
%M 月名字(January ~ December)
%W 星期名字(Sunday ~ Saturday)
%D 有英语前缀的月份的日期(1st, 2nd, 3rd, 等等)
%Y 年, 数字, 4 位
%y 年, 数字, 2 位
%a 缩写的星期名字(Sun ~ Sat)
%d 月份中的天数, 数字(00 ~ 31)
%e 月份中的天数, 数字(0 ~ 31)
%m 月, 数字(01 ~ 12)
%c 月, 数字(1 ~ 12)
%b 缩写的月份名字(Jan ~ Dec)
%j 一年中的天数(001 ~ 366)
%H 小时(00 ~ 23)
%k 小时(0 ~ 23)
%h 小时(01 ~ 12)
%I(i 的大写) 小时(01 ~ 12)
%l(L 的小写) 小时(1 ~ 12)
%i 分钟, 数字(00 ~ 59)
%r 时间,12 小时(hh:mm:ss [AP]M)
%T 时间,24 小时(hh:mm:ss)
%S 秒(00 ~ 59)
%s 秒(00 ~ 59)
%p AM 或 PM
%W 一个星期中的天数英文名称(Sunday~Saturday)
%w 一个星期中的天数(0=Sunday ~ 6=Saturday)
%U 星期(0 ~ 52), 这里星期天是星期的第一天
%u 星期(0 ~ 52), 这里星期一是星期的第一天
%% 输出%

mysql> select from_unixtime(1568710866),from_unixtime(1568710866,’%Y-%m-%d %H:%h:%s’);
+—————————————-+———————————————————————-+
| from_unixtime(1568710866) | from_unixtime(1568710866,’%Y-%m-%d %H:%h:%s’) |
+—————————————-+———————————————————————-+
| 2019-09-17 17:01:06 | 2019-09-17 17:05:06 |
+—————————————-+———————————————————————-+
1 row in set (0.00 sec)

month:获取指定日期的月份

MONTH(date) 函数返回指定 date 对应的月份,范围为 1 ~ 12。
mysql> select month(‘2017-12-15’),month(now());
+——————————-+———————+
| month(‘2017-12-15’) | month(now()) |
+——————————-+———————+
| 12 | 9 |
+——————————-+———————+
1 row in set (0.00 sec)

monthname:获取指定日期月份的英文名称

MONTHNAME(date) 函数返回日期 date 对应月份的英文全名。
mysql> select monthname(‘2017-12-15’),monthname(now());
+————————————-+—————————+
| monthname(‘2017-12-15’) | monthname(now()) |
+————————————-+—————————+
| December | September |
+————————————-+—————————+
1 row in set (0.00 sec)

dayname:获取指定日期的星期名称

DAYNAME(date) 函数返回 date 对应的工作日英文名称,例如 Sunday、Monday 等。
mysql> select now(),dayname(now());
+——————————-+————————+
| now() | dayname(now()) |
+——————————-+————————+
| 2019-09-17 17:13:08 | Tuesday |
+——————————-+————————+
1 row in set (0.00 sec)

dayofweek:获取日期对应的周索引

DAYOFWEEK(d) 函数返回 d 对应的一周中的索引(位置)。1 表示周日,2 表示周一,……,7 表示周六。这些索引值对应于 ODBC 标准。
mysql> select now(),dayofweek(now());
+——————————-+—————————+
| now() | dayofweek(now()) |
+——————————-+—————————+
| 2019-09-17 17:14:21 | 3 |
+——————————-+—————————+
1 row in set (0.00 sec)

week:获取指定日期是一年中的第几周

WEEK(date[,mode]) 函数计算日期 date 是一年中的第几周。WEEK(date,mode) 函数允许指定星期是否起始于周日或周一,以及返回值的范围是否为 0 ~ 52 或 1 ~ 53。
WEEK 函数接受两个参数:

  • date是要获取周数的日期。
  • mode是一个可选参数,用于确定周数计算的逻辑。它允许您指定本周是从星期一还是星期日开始,返回的周数应在0到52之间或0到53之间。

如果忽略mode参数,默认情况下WEEK函数将使用default_week_format系统变量的值。
要获取default_week_format变量的当前值,请使用SHOW VARIABLES语句如下:
mysql> SHOW VARIABLES LIKE ‘default_week_format’;
+——————————-+———-+
| Variable_name | Value |
+——————————-+———-+
| default_week_format | 0 |
+——————————-+———-+
1 row in set, 1 warning (0.00 sec)

在我们的服务器中,default_week_format的默认值为0,下表格说明了mode参数如何影响WEEK函数:

模式 一周的第一天 范围
0 星期日 0-53
1 星期一 0-53
2 星期日 1-53
3 星期一 1-53
4 星期日 0-53
5 星期一 0-53
6 星期日 1-53
7 星期一 1-53

上表中“今年有 4 天以上”表示:

  • 如果星期包含 1 月 1 日,并且在新的一年中有4天或更多天,那么这周是第1周。
  • 否则,这一周的数字是前一年的最后一周,下周是第 1 周。

mysql> select now(),week(now());
+——————————-+——————-+
| now() | week(now()) |
+——————————-+——————-+
| 2019-09-17 17:20:28 | 37 |
+——————————-+——————-+
1 row in set (0.00 sec)

dayofyear:获取指定日期在一年中的位置

DAYOFYEAR(d) 函数返回 d 是一年中的第几天,范围为 1 ~ 366。
mysql> select now(),dayofyear(now()),dayofyear(‘2019-01-01’);
+——————————-+—————————+————————————-+
| now() | dayofyear(now()) | dayofyear(‘2019-01-01’) |
+——————————-+—————————+————————————-+
| 2019-09-17 17:22:00 | 260 | 1 |
+——————————-+—————————+————————————-+
1 row in set (0.00 sec)

dayofmonth:获取指定日期在一个月的位置

DAYOFMONTH(d) 函数返回 d 是一个月中的第几天,范围为 1 ~ 31。
mysql> select now(),dayofmonth(now()),dayofmonth(‘2019-01-01’);
+——————————-+—————————-+—————————————+
| now() | dayofmonth(now()) | dayofmonth(‘2019-01-01’) |
+——————————-+—————————-+—————————————+
| 2019-09-17 17:23:09 | 17 | 1 |
+——————————-+—————————-+—————————————+
1 row in set (0.00 sec)

year:获取年份

YEAR() 函数可以从指定日期值中来获取年份值。
mysql> select now(),year(now()),year(‘2019-01-02’);
+——————————-+——————-+——————————+
| now() | year(now()) | year(‘2019-01-02’) |
+——————————-+——————-+——————————+
| 2019-09-17 17:28:10 | 2019 | 2019 |
+——————————-+——————-+——————————+
1 row in set (0.00 sec)

time_to_sec:将时间转换为秒值

TIME_TO_SEC(time) 函数返回将参数 time 转换为秒数的时间值,转换公式为“小时 ×3600+ 分钟 ×60+ 秒”。
mysql> select time_to_sec(‘15:15:15’),now(),time_to_sec(now());
+————————————-+——————————-+——————————+
| time_to_sec(‘15:15:15’) | now() | time_to_sec(now()) |
+————————————-+——————————-+——————————+
| 54915 | 2019-09-17 17:30:44 | 63044 |
+————————————-+——————————-+——————————+
1 row in set (0.00 sec)

sec_to_time:将秒值转换为时间格式

SEC_TO_TIME(seconds) 函数返回将参数 seconds 转换为小时、分钟和秒数的时间值。
mysql> select sec_to_time(100),sec_to_time(10000);
+—————————+——————————+
| sec_to_time(100) | sec_to_time(10000) |
+—————————+——————————+
| 00:01:40 | 02:46:40 |
+—————————+——————————+
1 row in set (0.00 sec)

date_add 和 adddate:向日期添加指定时间间隔

DATEADD(date,INTERVAL expr type)
date:参数是合法的日期表达式。_expr
参数是您希望添加的时间间隔。
type:参数可以是下列值

Type 值
MICROSECOND
SECOND
MINUTE
HOUR
DAY
WEEK
MONTH
QUARTER
YEAR
SECOND_MICROSECOND
MINUTE_MICROSECOND
MINUTE_SECOND
HOUR_MICROSECOND
HOUR_SECOND
HOUR_MINUTE
DAY_MICROSECOND
DAY_SECOND
DAY_MINUTE
DAY_HOUR
YEAR_MONTH

mysql> select date_add(‘2019-01-01’,INTERVAL 10 day),adddate(‘2019-01-01 16:00:00’,interval 100 SECOND);
+————————————————————+——————————————————————————+
| date_add(‘2019-01-01’,INTERVAL 10 day) | adddate(‘2019-01-01 16:00:00’,interval 100 SECOND) |
+————————————————————+——————————————————————————+
| 2019-01-11 | 2019-01-01 16:01:40 |
+————————————————————+——————————————————————————+
1 row in set (0.00 sec)

mysql> select date_add(‘2019-01-01’,INTERVAL -10 day),adddate(‘2019-01-01 16:00:00’,interval -100 SECOND);
+————————————————————-+——————————————————————————-+
| date_add(‘2019-01-01’,INTERVAL -10 day) | adddate(‘2019-01-01 16:00:00’,interval -100 SECOND) |
+————————————————————-+——————————————————————————-+
| 2018-12-22 | 2019-01-01 15:58:20 |
+————————————————————-+——————————————————————————-+
1 row in set (0.00 sec)

date_sub 和 subdate:日期减法运算

DATESUB(date,INTERVAL expr type)
date:参数是合法的日期表达式。_expr
参数是您希望添加的时间间隔。
type 的类型和 date_add 中的 type 一样。
mysql> select date_sub(‘2019-01-01’,INTERVAL 10 day),subdate(‘2019-01-01 16:00:00’,interval 100 SECOND);
+————————————————————+——————————————————————————+
| date_sub(‘2019-01-01’,INTERVAL 10 day) | subdate(‘2019-01-01 16:00:00’,interval 100 SECOND) |
+————————————————————+——————————————————————————+
| 2018-12-22 | 2019-01-01 15:58:20 |
+————————————————————+——————————————————————————+
1 row in set (0.00 sec)

mysql> select date_sub(‘2019-01-01’,INTERVAL -10 day),subdate(‘2019-01-01 16:00:00’,interval -100 SECOND);
+————————————————————-+——————————————————————————-+
| date_sub(‘2019-01-01’,INTERVAL -10 day) | subdate(‘2019-01-01 16:00:00’,interval -100 SECOND) |
+————————————————————-+——————————————————————————-+
| 2019-01-11 | 2019-01-01 16:01:40 |
+————————————————————-+——————————————————————————-+
1 row in set (0.00 sec)

addtime:时间加法运算

ADDTIME(time,expr) 函数用于执行时间的加法运算。添加 expr 到 time 并返回结果。
其中:time 是一个时间或日期时间表达式,expr 是一个时间表达式。
mysql> select addtime(‘2019-09-18 23:59:59’,’0:1:1’), addtime(‘10:30:59’,’5:10:37’);
+————————————————————+———————————————-+
| addtime(‘2019-09-18 23:59:59’,’0:1:1’) | addtime(‘10:30:59’,’5:10:37’) |
+————————————————————+———————————————-+
| 2019-09-19 00:01:00 | 15:41:36 |
+————————————————————+———————————————-+
1 row in set (0.00 sec)

subtime:时间减法运算

SUBTIME(time,expr) 函数用于执行时间的减法运算。
函数返回 time。expr 表示的值和格式 time 相同。time 是一个时间或日期时间表达式, expr 是一个时间。
mysql> select subtime(‘2019-09-18 23:59:59’,’0:1:1’),subtime(‘10:30:59’,’5:12:37’);
+————————————————————+———————————————-+
| subtime(‘2019-09-18 23:59:59’,’0:1:1’) | subtime(‘10:30:59’,’5:12:37’) |
+————————————————————+———————————————-+
| 2019-09-18 23:58:58 | 05:18:22 |
+————————————————————+———————————————-+
1 row in set (0.00 sec)

datediff:获取两个日期的时间间隔

DATEDIFF(date1,date2) 返回起始时间 date1 和结束时间 date2 之间的天数。date1 和 date2 为日期或 date-and-time 表达式。计算时只用到这些值的日期部分。
mysql> select datediff(‘2017-11-30’,’2017-11-29’) as col1, datediff(‘2017-11-30’,’2017-12-15’) as col2;
+———+———+
| col1 | col2 |
+———+———+
| 1 | -15 |
+———+———+
1 row in set (0.00 sec)

date_format:格式化指定的日期

DATE_FORMAT(date,format) 函数是根据 format 指定的格式显示 date 值。
DATE_FORMAT() 函数接受两个参数:
date:是要格式化的有效日期值 format:是由预定义的说明符组成的格式字符串,每个说明符前面都有一个百分比字符(%)。
format:格式和上面的函数from_unixtime中的 format 一样,可以参考上面的。
mysql> select date_format(‘2017-11-30’,’%Y%m%d’) as col0,now() as col1, date_format(now(),’%Y%m%d%H%i%s’) as col2;
+—————+——————————-+————————+
| col0 | col1 | col2 |
+—————+——————————-+————————+
| 20171130 | 2019-09-17 17:56:12 | 20190917175612 |
+—————+——————————-+————————+
1 row in set (0.00 sec)

weekday:获取指定日期在一周内的索引位置

WEEKDAY(date) 返回 date 的星期索引(0=星期一,1=星期二, ……6= 星期天)。
mysql> select now(),weekday(now());
+——————————-+————————+
| now() | weekday(now()) |
+——————————-+————————+
| 2019-09-17 18:01:34 | 1 |
+——————————-+————————+
1 row in set (0.00 sec)

mysql> select now(),dayofweek(now());
+——————————-+—————————+
| now() | dayofweek(now()) |
+——————————-+—————————+
| 2019-09-17 18:01:34 | 3 |
+——————————-+—————————+
1 row in set (0.00 sec)

MySQL 聚合函数

函数名称 作用
max 查询指定列的最大值
min 查询指定列的最小值
count 统计查询结果的行数
sum 求和,返回指定列的总和
avg 求平均值,返回指定列数据的平均值

MySQL 流程控制函数

函数名称 作用
if 判断,流程控制
ifnull 判断是否为空
case 搜索语句

if:判断

IF(expr,v1,v2)
当 expr 为真是返回 v1 的值,否则返回 v2
mysql> select if(1<2,1,0) c1,**if**(1>5,’√’,’×’) c2,if(strcmp(‘abc’,’ab’),’yes’,’no’) c3;
+——+——+——-+
| c1 | c2 | c3 |
+——+——+——-+
| 1 | × | yes |
+——+——+——-+
1 row in set (0.00 sec)

ifnull:判断是否为空

IFNULL(v1,v2):v1 为空返回 v2,否则返回 v1。
mysql> select ifnull(null,’路人甲Java’),ifnull(‘非空’,’为空’);
+———————————————+—————————————-+
| ifnull(null,’路人甲Java’) | ifnull(‘非空’,’为空’) |
+———————————————+—————————————-+
| 路人甲Java | 非空 |
+———————————————+—————————————-+
1 row in set (0.00 sec)

case:搜索语句,类似于 java 中的 if..else if..else

类似于 java 中的 if..else if..else
有 2 种写法
方式 1:
CASE <表达式>
WHEN <值1> THEN <操作>
WHEN <值2> THEN <操作>

ELSE <操作>
END CASE;

方式 2:
CASE
WHEN <条件1> THEN <命令>
WHEN <条件2> THEN <命令>

ELSE commands
END CASE;

示例:
准备数据:
CREATE TABLE t_stu (
id INT AUTO_INCREMENT COMMENT ‘编号’,
name VARCHAR(10) COMMENT ‘姓名’,
sex TINYINT COMMENT ‘性别,0:未知,1:男,2:女’,
PRIMARY KEY (id)
) COMMENT ‘学生表’;

insert into t_stu (name,sex) VALUES
(‘张学友’,1),
(‘刘德华’,1),
(‘郭富城’,1),
(‘蔡依林’,2),
(‘xxx’,0);

mysql> select from t_stu;
+——+—————-+———+
| id | name | sex |
+——+—————-+———+
| 1 | 张学友 | 1 |
| 2 | 刘德华 | 1 |
| 3 | 郭富城 | 1 |
| 4 | 蔡依林 | 2 |
| 5 | xxx | 0 |
+——+—————-+———+
5 rows in *set
(0.00 sec)

需求:查询所有学生信息,输出:姓名,性别(男、女、未知),如下:
mysql> SELECT
t.name 姓名,
(CASE t.sex
WHEN 1
THEN ‘男’
WHEN 2
THEN ‘女’
ELSE ‘未知’ END) 性别
FROM t_stu t;
+—————-+————+
| 姓名 | 性别 |
+—————-+————+
| 张学友 | 男 |
| 刘德华 | 男 |
| 郭富城 | 男 |
| 蔡依林 | 女 |
| xxx | 未知 |
+—————-+————+
5 rows in set (0.00 sec)

mysql> SELECT
t.name 姓名,
(CASE
WHEN t.sex = 1
THEN ‘男’
WHEN t.sex = 2
THEN ‘女’
ELSE ‘未知’ END) 性别
FROM t_stu t;
+—————-+————+
| 姓名 | 性别 |
+—————-+————+
| 张学友 | 男 |
| 刘德华 | 男 |
| 郭富城 | 男 |
| 蔡依林 | 女 |
| xxx | 未知 |
+—————-+————+
5 rows in set (0.00 sec)

其他函数

函数名称 作用
version 数据库版本号
database 当前的数据库
user 当前连接用户
password 返回字符串密码形式
md5 返回字符串的 md5 数据

mysql> SELECT version();
+——————+
| version() |
+——————+
| 5.7.25-log |
+——————+
1 row in set (0.00 sec)

mysql> SELECT database();
+———————+
| database() |
+———————+
| javacode2018 |
+———————+
1 row in set (0.00 sec)

mysql> SELECT user();
+————————+
| user() |
+————————+
| root@localhost |
+————————+
1 row in set (0.00 sec)

mysql> SELECT password(‘123456’);
+—————————————————————-+
| password(‘123456’) |
+—————————————————————-+
| *6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9 |
+—————————————————————-+
1 row in set, 1 warning (0.00 sec)

mysql> SELECT md5(‘123456’);
+—————————————————+
| md5(‘123456’) |
+—————————————————+
| e10adc3949ba59abbe56e057f20f883e |
+—————————————————+
1 row in set (0.00 sec)

十一、MySQL,深入了解连接查询及原理

准备数据

2 张表:
t_team:组表。
t_employee:员工表,内部有个 team_id 引用组表的 id。
drop table if exists t_team;
create table t_team(
id int not null AUTO_INCREMENT PRIMARY KEY comment ‘组id’,
team_name varchar(32) not null default ‘’ comment ‘名称’
) comment ‘组表’;

drop table if exists t_employee;
create table t_employee(
id int not null AUTO_INCREMENT PRIMARY KEY comment ‘部门id’,
emp_name varchar(32) not null default ‘’ comment ‘员工名称’,
team_id int not null default 0 comment ‘员工所在组id’
) comment ‘员工表表’;

insert into t_team values (1,’架构组’),(2,’测试组’),(3,’java组’),(4,’前端组’);
insert into t_employee values (1,’路人甲Java’,1),(2,’张三’,2),(3,’李四’,3),(4,’王五’,0),(5,’赵六’,0);

t_team表 4 条记录,如下:
mysql> select from t_team;
+——+—————-+
| id | team_name |
+——+—————-+
| 1 | 架构组 |
| 2 | 测试组 |
| 3 | java组 |
| 4 | 前端组 |
+——+—————-+
4 rows in *set
(0.00 sec)

t_employee表 5 条记录,如下:
mysql> select from t_employee;
+——+———————-+————-+
| id | emp_name | team_id |
+——+———————-+————-+
| 1 | 路人甲Java | 1 |
| 2 | 张三 | 2 |
| 3 | 李四 | 3 |
| 4 | 王五 | 0 |
| 5 | 赵六 | 0 |
+——+———————-+————-+
5 rows in *set
(0.00 sec)

笛卡尔积

介绍连接查询之前,我们需要先了解一下笛卡尔积。
笛卡尔积简单点理解:有两个集合 A 和 B,笛卡尔积表示 A 集合中的元素和 B 集合中的元素任意相互关联产生的所有可能的结果。
假如 A 中有 m 个元素,B 中有 n 个元素,A、B 笛卡尔积产生的结果有 mn 个结果,相当于循环遍历两个集合中的元素,任意组合。
java 伪代码表示如下:
for(Object eleA : A){
*for
(Object eleB : B){
System.out.print(eleA+”,”+eleB);
}
}

过程:拿 A 集合中的第 1 行,去匹配集合 B 中所有的行,然后再拿集合 A 中的第 2 行,去匹配集合 B 中所有的行,最后结果数量为 m*n。

sql 中笛卡尔积语法

select 字段 from 表1,表2[,表N];
或者
select 字段 from 表1 join 表2 [join 表N];

示例:
mysql> select from t_team,t_employee;
+——+—————-+——+———————-+————-+
| id | team_name | id | emp_name | team_id |
+——+—————-+——+———————-+————-+
| 1 | 架构组 | 1 | 路人甲Java | 1 |
| 2 | 测试组 | 1 | 路人甲Java | 1 |
| 3 | java组 | 1 | 路人甲Java | 1 |
| 4 | 前端组 | 1 | 路人甲Java | 1 |
| 1 | 架构组 | 2 | 张三 | 2 |
| 2 | 测试组 | 2 | 张三 | 2 |
| 3 | java组 | 2 | 张三 | 2 |
| 4 | 前端组 | 2 | 张三 | 2 |
| 1 | 架构组 | 3 | 李四 | 3 |
| 2 | 测试组 | 3 | 李四 | 3 |
| 3 | java组 | 3 | 李四 | 3 |
| 4 | 前端组 | 3 | 李四 | 3 |
| 1 | 架构组 | 4 | 王五 | 0 |
| 2 | 测试组 | 4 | 王五 | 0 |
| 3 | java组 | 4 | 王五 | 0 |
| 4 | 前端组 | 4 | 王五 | 0 |
| 1 | 架构组 | 5 | 赵六 | 0 |
| 2 | 测试组 | 5 | 赵六 | 0 |
| 3 | java组 | 5 | 赵六 | 0 |
| 4 | 前端组 | 5 | 赵六 | 0 |
+——+—————-+——+———————-+————-+
20 rows *in
set (0.00 sec)

t_team 表 4 条记录,t_employee 表 5 条记录,笛卡尔积结果输出了 20 行记录。

内连接

语法:
select 字段 from 表1 inner join 表2 on 连接条件;

select 字段 from 表1 join 表2 on 连接条件;

select 字段 from 表1, 表2 [where 关联条件];

内连接相当于在笛卡尔积的基础上加上了连接的条件。
当没有连接条件的时候,内连接上升为笛卡尔积。
过程用 java 伪代码如下:
for(Object eleA : A){
for(Object eleB : B){
if(连接条件是否为true){
System.out.print(eleA+”,”+eleB);
}
}
}

示例 1:有连接条件

查询员工及所属部门
mysql> select t1.emp_name,t2.team_name from t_employee t1 inner join t_team t2 on t1.team_id = t2.id;
+———————-+—————-+
| emp_name | team_name |
+———————-+—————-+
| 路人甲Java | 架构组 |
| 张三 | 测试组 |
| 李四 | java组 |
+———————-+—————-+
3 rows in set (0.00 sec)

mysql> select t1.emp_name,t2.team_name from t_employee t1 join t_team t2 on t1.team_id = t2.id;
+———————-+—————-+
| emp_name | team_name |
+———————-+—————-+
| 路人甲Java | 架构组 |
| 张三 | 测试组 |
| 李四 | java组 |
+———————-+—————-+
3 rows in set (0.00 sec)

mysql> select t1.emp_name,t2.team_name from t_employee t1, t_team t2 where t1.team_id = t2.id;
+———————-+—————-+
| emp_name | team_name |
+———————-+—————-+
| 路人甲Java | 架构组 |
| 张三 | 测试组 |
| 李四 | java组 |
+———————-+—————-+
3 rows in set (0.00 sec)

上面相当于获取了 2 个表的交集,查询出了两个表都有的数据。

示例 2:无连接条件

无条件内连接,上升为笛卡尔积,如下:
mysql> select t1.emp_name,t2.team_name from t_employee t1 inner join t_team t2;
+———————-+—————-+
| emp_name | team_name |
+———————-+—————-+
| 路人甲Java | 架构组 |
| 路人甲Java | 测试组 |
| 路人甲Java | java组 |
| 路人甲Java | 前端组 |
| 张三 | 架构组 |
| 张三 | 测试组 |
| 张三 | java组 |
| 张三 | 前端组 |
| 李四 | 架构组 |
| 李四 | 测试组 |
| 李四 | java组 |
| 李四 | 前端组 |
| 王五 | 架构组 |
| 王五 | 测试组 |
| 王五 | java组 |
| 王五 | 前端组 |
| 赵六 | 架构组 |
| 赵六 | 测试组 |
| 赵六 | java组 |
| 赵六 | 前端组 |
+———————-+—————-+
20 rows in set (0.00 sec)

示例 3:组合条件进行查询

查询架构组的员工,3 种写法
mysql> select t1.emp_name,t2.team_name from t_employee t1 inner join t_team t2 on t1.team_id = t2.id and t2.team_name = ‘架构组’;
+———————-+—————-+
| emp_name | team_name |
+———————-+—————-+
| 路人甲Java | 架构组 |
+———————-+—————-+
1 row in set (0.00 sec)

mysql> select t1.emp_name,t2.team_name from t_employee t1 inner join t_team t2 on t1.team_id = t2.id where t2.team_name = ‘架构组’;
+———————-+—————-+
| emp_name | team_name |
+———————-+—————-+
| 路人甲Java | 架构组 |
+———————-+—————-+
1 row in set (0.00 sec)

mysql> select t1.emp_name,t2.team_name from t_employee t1, t_team t2 where t1.team_id = t2.id and t2.team_name = ‘架构组’;
+———————-+—————-+
| emp_name | team_name |
+———————-+—————-+
| 路人甲Java | 架构组 |
+———————-+—————-+
1 row in set (0.00 sec)

上面 3 中方式解说。
方式 1:on 中使用了组合条件。
方式 2:在连接的结果之后再进行过滤,相当于先获取连接的结果,然后使用 where 中的条件再对连接结果进行过滤。
方式 3:直接在 where 后面进行过滤。

总结

内连接建议使用第 3 种语法,简洁:
select 字段 from 表1, 表2 [where 关联条件];

外连接

外连接涉及到 2 个表,分为:主表和从表,要查询的信息主要来自于哪个表,谁就是主表。
外连接查询结果为主表中所有记录。如果从表中有和它匹配的,则显示匹配的值,这部分相当于内连接查询出来的结果;如果从表中没有和它匹配的,则显示 null。
最终:外连接查询结果 = 内连接的结果 + 主表中有的而内连接结果中没有的记录。
外连接分为 2 种:
左外链接:使用 left join 关键字,left join 左边的是主表。
右外连接:使用 right join 关键字,right join 右边的是主表。

左连接

语法

select 列 from 主表 left join 从表 on 连接条件;

示例 1:

查询所有员工信息,并显示员工所在组,如下:
mysql> SELECT
t1.emp_name,
t2.team_name
FROM
t_employee t1
LEFT JOIN
t_team t2
ON
t1.team_id = t2.id;
+———————-+—————-+
| emp_name | team_name |
+———————-+—————-+
| 路人甲Java | 架构组 |
| 张三 | 测试组 |
| 李四 | java组 |
| 王五 | NULL |
| 赵六 | NULL |
+———————-+—————-+
5 rows in set (0.00 sec)

上面查询出了所有员工,员工 team_id=0 的,team_name 为 NULL。

示例 2:

查询员工姓名、组名,返回组名不为空的记录,如下:
mysql> SELECT
t1.emp_name,
t2.team_name
FROM
t_employee t1
LEFT JOIN
t_team t2
ON
t1.team_id = t2.id
WHERE
t2.team_name IS NOT NULL;
+———————-+—————-+
| emp_name | team_name |
+———————-+—————-+
| 路人甲Java | 架构组 |
| 张三 | 测试组 |
| 李四 | java组 |
+———————-+—————-+
3 rows in set (0.00 sec)

上面先使用内连接获取连接结果,然后再使用 where 对连接结果进行过滤。

右连接

语法

select 列 from 从表 right join 主表 on 连接条件;

示例

我们使用右连接来实现上面左连接实现的功能,如下:
mysql> SELECT
t2.team_name,
t1.emp_name
FROM
t_team t2
RIGHT JOIN
t_employee t1
ON
t1.team_id = t2.id;
+—————-+———————-+
| team_name | emp_name |
+—————-+———————-+
| 架构组 | 路人甲Java |
| 测试组 | 张三 |
| java组 | 李四 |
| NULL | 王五 |
| NULL | 赵六 |
+—————-+———————-+
5 rows in set (0.00 sec)

mysql> SELECT
t2.team_name,
t1.emp_name
FROM
t_team t2
RIGHT JOIN
t_employee t1
ON
t1.team_id = t2.id
WHERE
t2.team_name IS NOT NULL;
+—————-+———————-+
| team_name | emp_name |
+—————-+———————-+
| 架构组 | 路人甲Java |
| 测试组 | 张三 |
| java组 | 李四 |
+—————-+———————-+
3 rows in set (0.00 sec)

理解表连接原理

准备数据
drop table if exists test1;
create table test1(
a int
);
drop table if exists test2;
create table test2(
b int
);
insert into test1 values (1),(2),(3);
insert into test2 values (3),(4),(5);

mysql> select from test1;
+———+
| a |
+———+
| 1 |
| 2 |
| 3 |
+———+
3 rows in *set
(0.00 sec)

mysql> select from test2;
+———+
| b |
+———+
| 3 |
| 4 |
| 5 |
+———+
3 rows in *set
(0.00 sec)

我们来写几个连接,看看效果。

示例 1:内连接

mysql> select from test1 t1,test2 t2;
+———+———+
| a | b |
+———+———+
| 1 | 3 |
| 2 | 3 |
| 3 | 3 |
| 1 | 4 |
| 2 | 4 |
| 3 | 4 |
| 1 | 5 |
| 2 | 5 |
| 3 | 5 |
+———+———+
9 rows in *set
(0.00 sec)

mysql> select from test1 t1,test2 t2 where t1.a = t2.b;
+———+———+
| a | b |
+———+———+
| 3 | 3 |
+———+———+
1 row in *set
(0.00 sec)

9 条数据正常。

示例 2:左连接

mysql> select from test1 t1 left join test2 t2 on t1.a = t2.b;
+———+———+
| a | b |
+———+———+
| 3 | 3 |
| 1 | NULL |
| 2 | NULL |
+———+———+
3 rows in *set
(0.00 sec)

mysql> select from test1 t1 left join test2 t2 on t1.a>10;
+———+———+
| a | b |
+———+———+
| 1 | NULL |
| 2 | NULL |
| 3 | NULL |
+———+———+
3 rows in *set
(0.00 sec)

mysql> select from test1 t1 left join test2 t2 on 1=1;
+———+———+
| a | b |
+———+———+
| 1 | 3 |
| 2 | 3 |
| 3 | 3 |
| 1 | 4 |
| 2 | 4 |
| 3 | 4 |
| 1 | 5 |
| 2 | 5 |
| 3 | 5 |
+———+———+
9 rows in *set
(0.00 sec)

上面的左连接第一个好理解。
第 2 个 sql 连接条件t1.a>10,这个条件只关联了 test1 表,再看看结果,是否可以理解?不理解的继续向下看,我们用 java 代码来实现连接查询。
第 3 个 sql 中的连接条件 1=1 值为 true,返回结果为笛卡尔积。

java 代码实现连接查询

下面是一个简略版的实现
package com.itsoku.sql;

import org.junit.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;

public class Test1 {
public static class Table1 {
int a;

  1. **public** **int** **getA**() {<br /> **return** a;<br /> }
  2. **public** **void** **setA**(**int** a) {<br /> **this**.a = a;<br /> }
  3. **public** **Table1**(**int** a) {<br /> **this**.a = a;<br /> }
  4. @Override<br /> **public** String **toString**() {<br /> **return** "Table1{" +<br /> "a=" + a +<br /> '}';<br /> }
  5. **public** **static** Table1 **build**(**int** a) {<br /> **return** **new** Table1(a);<br /> }<br /> }
  6. **public** **static** **class** **Table2** {<br /> **int** b;
  7. **public** **int** **getB**() {<br /> **return** b;<br /> }
  8. **public** **void** **setB**(**int** b) {<br /> **this**.b = b;<br /> }
  9. **public** **Table2**(**int** b) {<br /> **this**.b = b;<br /> }
  10. **public** **static** Table2 **build**(**int** b) {<br /> **return** **new** Table2(b);<br /> }
  11. @Override<br /> **public** String **toString**() {<br /> **return** "Table2{" +<br /> "b=" + b +<br /> '}';<br /> }<br /> }
  12. **public** **static** **class** **Record**<**R1**, **R2**> {<br /> R1 r1;<br /> R2 r2;
  13. **public** R1 **getR1**() {<br /> **return** r1;<br /> }
  14. **public** **void** **setR1**(R1 r1) {<br /> **this**.r1 = r1;<br /> }
  15. **public** R2 **getR2**() {<br /> **return** r2;<br /> }
  16. **public** **void** **setR2**(R2 r2) {<br /> **this**.r2 = r2;<br /> }
  17. **public** **Record**(R1 r1, R2 r2) {<br /> **this**.r1 = r1;<br /> **this**.r2 = r2;<br /> }
  18. @Override<br /> **public** String **toString**() {<br /> **return** "Record{" +<br /> "r1=" + r1 +<br /> ", r2=" + r2 +<br /> '}';<br /> }
  19. **public** **static** <R1, R2> Record<R1, R2> **build**(R1 r1, R2 r2) {<br /> **return** **new** Record(r1, r2);<br /> }<br /> }
  20. **public** **static** **enum** JoinType {<br /> innerJoin, leftJoin<br /> }
  21. **public** **static** **interface** **Filter**<**R1**, **R2**> {<br /> **boolean** **accept**(R1 r1, R2 r2);<br /> }
  22. **public** **static** <R1, R2> List<Record<R1, R2>> join(List<R1> table1, List<R2> table2, JoinType joinType, Filter<R1, R2> onFilter, Filter<R1, R2> whereFilter) {<br /> **if** (Objects.isNull(table1) || Objects.isNull(table2) || joinType == **null**) {<br /> **return** **new** ArrayList<>();<br /> }
  23. List<Record<R1, R2>> result = **new** CopyOnWriteArrayList<>();
  24. **for** (R1 r1 : table1) {<br /> List<Record<R1, R2>> onceJoinResult = joinOn(r1, table2, onFilter);<br /> result.addAll(onceJoinResult);<br /> }
  25. **if** (joinType == JoinType.leftJoin) {<br /> List<R1> r1Record = result.stream().map(Record::getR1).collect(Collectors.toList());<br /> List<Record<R1, R2>> leftAppendList = **new** ArrayList<>();<br /> **for** (R1 r1 : table1) {<br /> **if** (!r1Record.contains(r1)) {<br /> leftAppendList.add(Record.build(r1, **null**));<br /> }<br /> }<br /> result.addAll(leftAppendList);<br /> }<br /> **if** (Objects.nonNull(whereFilter)) {<br /> **for** (Record<R1, R2> record : result) {<br /> **if** (!whereFilter.accept(record.r1, record.r2)) {<br /> result.remove(record);<br /> }<br /> }<br /> }<br /> **return** result;<br /> }
  26. **public** **static** <R1, R2> List<Record<R1, R2>> joinOn(R1 r1, List<R2> table2, Filter<R1, R2> onFilter) {<br /> List<Record<R1, R2>> result = **new** ArrayList<>();<br /> **for** (R2 r2 : table2) {<br /> **if** (Objects.nonNull(onFilter) ? onFilter.accept(r1, r2) : **true**) {<br /> result.add(Record.build(r1, r2));<br /> }<br /> }<br /> **return** result;<br /> }
  27. @Test<br /> **public** **void** **innerJoin**() {<br /> List<Table1> table1 = Arrays.asList(Table1.build(1), Table1.build(2), Table1.build(3));<br /> List<Table2> table2 = Arrays.asList(Table2.build(3), Table2.build(4), Table2.build(5));
  28. join(table1, table2, JoinType.innerJoin, **null**, **null**).forEach(System.out::println);<br /> System.out.println("-----------------");<br /> join(table1, table2, JoinType.innerJoin, (r1, r2) -> r1.a == r2.b, **null**).forEach(System.out::println);<br /> }
  29. @Test<br /> **public** **void** **leftJoin**() {<br /> List<Table1> table1 = Arrays.asList(Table1.build(1), Table1.build(2), Table1.build(3));<br /> List<Table2> table2 = Arrays.asList(Table2.build(3), Table2.build(4), Table2.build(5));
  30. join(table1, table2, JoinType.leftJoin, (r1, r2) -> r1.a == r2.b, **null**).forEach(System.out::println);<br /> System.out.println("-----------------");<br /> join(table1, table2, JoinType.leftJoin, (r1, r2) -> r1.a > 10, **null**).forEach(System.out::println);<br /> }

}

代码中的innerJoin()方法模拟了下面的 sql:
mysql> select from test1 t1,test2 t2;
+———+———+
| a | b |
+———+———+
| 1 | 3 |
| 2 | 3 |
| 3 | 3 |
| 1 | 4 |
| 2 | 4 |
| 3 | 4 |
| 1 | 5 |
| 2 | 5 |
| 3 | 5 |
+———+———+
9 rows in *set
(0.00 sec)

mysql> select from test1 t1,test2 t2 where t1.a = t2.b;
+———+———+
| a | b |
+———+———+
| 3 | 3 |
+———+———+
1 row in *set
(0.00 sec)

运行一下innerJoin()输出如下:
Record{r1=Table1{a=1}, r2=Table2{b=3}}
Record{r1=Table1{a=1}, r2=Table2{b=4}}
Record{r1=Table1{a=1}, r2=Table2{b=5}}
Record{r1=Table1{a=2}, r2=Table2{b=3}}
Record{r1=Table1{a=2}, r2=Table2{b=4}}
Record{r1=Table1{a=2}, r2=Table2{b=5}}
Record{r1=Table1{a=3}, r2=Table2{b=3}}
Record{r1=Table1{a=3}, r2=Table2{b=4}}
Record{r1=Table1{a=3}, r2=Table2{b=5}}
————————-
Record{r1=Table1{a=3}, r2=Table2{b=3}}

对比一下 sql 和 java 的结果,输出的结果条数、数据基本上一致,唯一不同的是顺序上面不一样,顺序为何不一致,稍微介绍
代码中的leftJoin()方法模拟了下面的 sql:
mysql> select from test1 t1 left join test2 t2 on t1.a = t2.b;
+———+———+
| a | b |
+———+———+
| 3 | 3 |
| 1 | NULL |
| 2 | NULL |
+———+———+
3 rows in *set
(0.00 sec)

mysql> select from test1 t1 left join test2 t2 on t1.a>10;
+———+———+
| a | b |
+———+———+
| 1 | NULL |
| 2 | NULL |
| 3 | NULL |
+———+———+
3 rows in *set
(0.00 sec)

运行leftJoin(),结果如下:
Record{r1=Table1{a=3}, r2=Table2{b=3}}
Record{r1=Table1{a=1}, r2=null}
Record{r1=Table1{a=2}, r2=null}
————————-
Record{r1=Table1{a=1}, r2=null}
Record{r1=Table1{a=2}, r2=null}
Record{r1=Table1{a=3}, r2=null}

效果和 sql 的效果完全一致,可以对上。
现在我们来讨论 java 输出的顺序为何和 sql 不一致?
上面 java 代码中两个表的连接查询使用了嵌套循环,外循环每执行一次,内循环的表都会全部遍历一次,如果放到 mysql 中,就相当于内表(被驱动表)全部扫描了一次(一次全表 io 读取操作),主表(外循环)如果有 n 条数据,那么从表就需要全表扫描 n 次,表的数据是存储在磁盘中,每次全表扫描都需要做 io 操作,io 操作是最耗时间的,如果 mysql 按照上面的 java 方式实现,那效率肯定很低。
那 mysql 是如何优化的呢?
msql 内部使用了一个内存缓存空间,就叫他join_buffer吧,先把外循环的数据放到join_buffer中,然后对从表进行遍历,从表中取一条数据和join_buffer的数据进行比较,然后从表中再取第 2 条和join_buffer数据进行比较,直到从表遍历完成,使用这方方式来减少从表的 io 扫描次数,当join_buffer足够大的时候,大到可以存放主表所有数据,那么从表只需要全表扫描一次(即只需要一次全表 io 读取操作)。
mysql 中这种方式叫做Block Nested Loop。
java 代码改进一下,来实现 join_buffer 的过程。

java 代码改进版本

package com.itsoku.sql;

import org.junit.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;

import com.itsoku.sql.Test1.*;

public class Test2 {

  1. **public** **static** **int** joinBufferSize = 10000;<br /> **public** **static** List<?> joinBufferList = **new** ArrayList<>();
  2. **public** **static** <R1, R2> List<Record<R1, R2>> join(List<R1> table1, List<R2> table2, JoinType joinType, Filter<R1, R2> onFilter, Filter<R1, R2> whereFilter) {<br /> **if** (Objects.isNull(table1) || Objects.isNull(table2) || joinType == **null**) {<br /> **return** **new** ArrayList<>();<br /> }
  3. List<Test1.Record<R1, R2>> result = **new** CopyOnWriteArrayList<>();
  4. **int** table1Size = table1.size();<br /> **int** fromIndex = 0, toIndex = joinBufferSize;<br /> toIndex = Integer.min(table1Size, toIndex);<br /> **while** (fromIndex < table1Size && toIndex <= table1Size) {<br /> joinBufferList = table1.subList(fromIndex, toIndex);<br /> fromIndex = toIndex;<br /> toIndex += joinBufferSize;<br /> toIndex = Integer.min(table1Size, toIndex);
  5. List<Record<R1, R2>> blockNestedLoopResult = blockNestedLoop((List<R1>) joinBufferList, table2, onFilter);<br /> result.addAll(blockNestedLoopResult);<br /> }
  6. **if** (joinType == JoinType.leftJoin) {<br /> List<R1> r1Record = result.stream().map(Record::getR1).collect(Collectors.toList());<br /> List<Record<R1, R2>> leftAppendList = **new** ArrayList<>();<br /> **for** (R1 r1 : table1) {<br /> **if** (!r1Record.contains(r1)) {<br /> leftAppendList.add(Record.build(r1, **null**));<br /> }<br /> }<br /> result.addAll(leftAppendList);<br /> }<br /> **if** (Objects.nonNull(whereFilter)) {<br /> **for** (Record<R1, R2> record : result) {<br /> **if** (!whereFilter.accept(record.r1, record.r2)) {<br /> result.remove(record);<br /> }<br /> }<br /> }<br /> **return** result;<br /> }
  7. **public** **static** <R1, R2> List<Record<R1, R2>> blockNestedLoop(List<R1> joinBufferList, List<R2> table2, Filter<R1, R2> onFilter) {<br /> List<Record<R1, R2>> result = **new** ArrayList<>();<br /> **for** (R2 r2 : table2) {<br /> **for** (R1 r1 : joinBufferList) {<br /> **if** (Objects.nonNull(onFilter) ? onFilter.accept(r1, r2) : **true**) {<br /> result.add(Record.build(r1, r2));<br /> }<br /> }<br /> }<br /> **return** result;<br /> }
  8. @Test<br /> **public** **void** **innerJoin**() {<br /> List<Table1> table1 = Arrays.asList(Table1.build(1), Table1.build(2), Table1.build(3));<br /> List<Table2> table2 = Arrays.asList(Table2.build(3), Table2.build(4), Table2.build(5));
  9. join(table1, table2, JoinType.innerJoin, **null**, **null**).forEach(System.out::println);<br /> System.out.println("-----------------");<br /> join(table1, table2, JoinType.innerJoin, (r1, r2) -> r1.a == r2.b, **null**).forEach(System.out::println);<br /> }
  10. @Test<br /> **public** **void** **leftJoin**() {<br /> List<Table1> table1 = Arrays.asList(Table1.build(1), Table1.build(2), Table1.build(3));<br /> List<Table2> table2 = Arrays.asList(Table2.build(3), Table2.build(4), Table2.build(5));
  11. join(table1, table2, JoinType.leftJoin, (r1, r2) -> r1.a == r2.b, **null**).forEach(System.out::println);<br /> System.out.println("-----------------");<br /> join(table1, table2, JoinType.leftJoin, (r1, r2) -> r1.a > 10, **null**).forEach(System.out::println);<br /> }<br />}

执行innerJoin(),输出:
Record{r1=Table1{a=1}, r2=Table2{b=3}}
Record{r1=Table1{a=2}, r2=Table2{b=3}}
Record{r1=Table1{a=3}, r2=Table2{b=3}}
Record{r1=Table1{a=1}, r2=Table2{b=4}}
Record{r1=Table1{a=2}, r2=Table2{b=4}}
Record{r1=Table1{a=3}, r2=Table2{b=4}}
Record{r1=Table1{a=1}, r2=Table2{b=5}}
Record{r1=Table1{a=2}, r2=Table2{b=5}}
Record{r1=Table1{a=3}, r2=Table2{b=5}}
————————-
Record{r1=Table1{a=3}, r2=Table2{b=3}}

执行leftJoin(),输出:
Record{r1=Table1{a=3}, r2=Table2{b=3}}
Record{r1=Table1{a=1}, r2=null}
Record{r1=Table1{a=2}, r2=null}
————————-
Record{r1=Table1{a=1}, r2=null}
Record{r1=Table1{a=2}, r2=null}
Record{r1=Table1{a=3}, r2=null}

结果和 sql 的结果完全一致。

扩展

表连接中还可以使用前面学过的group by、having、order by、limit。
这些关键字相当于在表连接的结果上在进行操作,大家下去可以练习一下,加深理解。

十二、MySQL子查询(非常重要,高手必备)

子查询

出现在 select 语句中的 select 语句,称为子查询或内查询。
外部的 select 查询语句,称为主查询或外查询。

子查询分类

按结果集的行列数不同分为 4 种

  • 标量子查询(结果集只有一行一列)
  • 列子查询(结果集只有一列多行)
  • 行子查询(结果集有一行多列)
  • 表子查询(结果集一般为多行多列)

    按子查询出现在主查询中的不同位置分

  • select 后面:仅仅支持标量子查询。

  • from 后面:支持表子查询。
  • where 或 having 后面:支持标量子查询(单列单行)、列子查询(单列多行)、行子查询(多列多行)
  • exists 后面(即相关子查询):表子查询(多行、多列)

    准备测试数据

    测试数据比较多,放在我的个人博客上了。
    浏览器中打开链接:http://www.itsoku.com/article/196
    mysql 中执行里面的javacode2018_employees库部分的脚本。
    成功创建javacode2018_employees库及 5 张表,如下:
表名 描述
departments 部门表
employees 员工信息表
jobs 职位信息表
locations 位置表(部门表中会用到)
job_grades 薪资等级表

select 后面的子查询

子查询位于 select 后面的,仅仅支持标量子查询

示例 1

查询每个部门员工个数
SELECT
a.,
(SELECT count(
)
FROM employees b
WHERE b.department_id = a.department_id) AS 员工个数
FROM departments a;

示例 2

查询员工号=102 的部门名称
SELECT (SELECT a.department_name
FROM departments a, employees b
WHERE a.department_id = b.department_id
AND b.employee_id = 102) AS 部门名;

from 后面的子查询

将子查询的结果集充当一张表,要求必须起别名,否者这个表找不到。
然后将真实的表和子查询结果表进行连接查询。

示例 1

查询每个部门平均工资的工资等级
— 查询每个部门平均工资
SELECT
department_id,
avg(a.salary)
FROM employees a
GROUP BY a.department_id;

— 薪资等级表
SELECT *
FROM job_grades;

— 将上面2个结果连接查询,筛选条件:平均工资 between lowest_sal and highest_sal;
SELECT
t1.department_id,
sa AS ‘平均工资’,
t2.grade_level
FROM (SELECT
department_id,
avg(a.salary) sa
FROM employees a
GROUP BY a.department_id) t1, job_grades t2
WHERE
t1.sa BETWEEN t2.lowest_sal AND t2.highest_sal;

运行最后一条结果如下:
mysql> SELECT
t1.department_id,
sa AS ‘平均工资’,
t2.grade_level
FROM (SELECT
department_id,
avg(a.salary) sa
FROM employees a
GROUP BY a.department_id) t1, job_grades t2
WHERE
t1.sa BETWEEN t2.lowest_sal AND t2.highest_sal;
+———————-+———————+——————-+
| department_id | 平均工资 | grade_level |
+———————-+———————+——————-+
| NULL | 7000.000000 | C |
| 10 | 4400.000000 | B |
| 20 | 9500.000000 | C |
| 30 | 4150.000000 | B |
| 40 | 6500.000000 | C |
| 50 | 3475.555556 | B |
| 60 | 5760.000000 | B |
| 70 | 10000.000000 | D |
| 80 | 8955.882353 | C |
| 90 | 19333.333333 | E |
| 100 | 8600.000000 | C |
| 110 | 10150.000000 | D |
+———————-+———————+——————-+
12 rows in set (0.00 sec)

where 和 having 后面的子查询

where 或 having 后面,可以使用

  1. 标量子查询(单行单列行子查询)
  2. 列子查询(单列多行子查询)
  3. 行子查询(一行多列)

    特点

  4. 子查询放在小括号内。

  5. 子查询一般放在条件的右侧。
  6. 标量子查询,一般搭配着单行单列操作符使用 >、<、>=、<=、=、<>、!=
  7. 列子查询,一般搭配着多行操作符使用in(not in):列表中的“任意一个”any 或者 some:和子查询返回的“某一个值”比较,比如 a>some(10,20,30),a 大于子查询中任意一个即可,a 大于子查询中最小值即可,等同于 a>min(10,20,30)。all:和子查询返回的“所有值”比较,比如 a>all(10,20,30),a 大于子查询中所有值,换句话说,a 大于子查询中最大值即可满足查询条件,等同于 a>max(10,20,30);
  8. 子查询的执行优先于主查询执行,因为主查询的条件用到了子查询的结果。

    mysql 中的 in、any、some、all

    in,any,some,all 分别是子查询关键词之一。
    in:in 常用于 where 表达式中,其作用是查询某个范围内的数据
    any 和 some 一样:可以与=、>、>=、<、<=、<>结合起来使用,分别表示等于、大于、大于等于、小于、小于等于、不等于其中的任何一个数据。
    all:可以与=、>、>=、<、<=、<>结合是来使用,分别表示等于、大于、大于等于、小于、小于等于、不等于其中的其中的所有数据。
    下文中会经常用到这些关键字。

    标量子查询

    一般标量子查询,示例

    查询谁的工资比 Abel 的高?
    /①查询abel的工资【改查询是标量子查询】/
    SELECT salary
    FROM employees
    WHERE last_name = ‘Abel’;

/②查询员工信息,满足salary>①的结果/
SELECT *
FROM employees a
WHERE a.salary > (SELECT salary
FROM employees
WHERE last_name = ‘Abel’);

多个标量子查询,示例

返回 job_id 与 141 号员工相同,salary 比 143 号员工多的员工、姓名、job_id 和工资
/返回job_id与141号员工相同,salary比143号员工多的员工、姓名、job_id和工资/
/①查询141号员工的job_id/
SELECT job_id
FROM employees
WHERE employee_id = 141;
/②查询143好员工的salary/
SELECT salary
FROM employees
WHERE employee_id = 143;
/③查询员工的姓名、job_id、工资,要求job_id=① and salary>②/
SELECT
a.last_name 姓名,
a.job_id,
a.salary 工资
FROM employees a
WHERE a.job_id = (SELECT job_id
FROM employees
WHERE employee_id = 141)
AND
a.salary > (SELECT salary
FROM employees
WHERE employee_id = 143);

子查询+分组函数,示例

查询最低工资大于 50 号部门最低工资的部门 id 和其最低工资【having】
/查询最低工资大于50号部门最低工资的部门id和其最低工资【having】/
/①查询50号部门的最低工资/
SELECT min(salary)
FROM employees
WHERE department_id = 50;
/②查询每个部门的最低工资/
SELECT
min(salary),
department_id
FROM employees
GROUP BY department_id;
/③在②的基础上筛选,满足min(salary)>①/
SELECT
min(a.salary) minsalary,
department_id
FROM employees a
GROUP BY a.department_id
HAVING min(a.salary) > (SELECT min(salary)
FROM employees
WHERE department_id = 50);

错误的标量子查询,示例

将上面的示例 ③ 中子查询语句中的 min(salary)改为 salary,执行效果如下:
mysql> SELECT
min(a.salary) minsalary,
department_id
FROM employees a
GROUP BY a.department_id
HAVING min(a.salary) > (SELECT salary
FROM employees
WHERE department_id = 500000);
ERROR 1242 (21000): Subquery returns more than 1 row

错误提示:子查询返回的结果超过了 1 行记录。
说明:上面的子查询只支持最多一列一行记录

列子查询(子查询结果集一列多行)

列子查询需要搭配多行操作符使用:in(not in)、any/some、all。
为了提升效率,最好去重一下distinct关键字。

示例 1

返回 location_id 是 1400 或 1700 的部门中的所有员工姓名
/返回location_id是1400或1700的部门中的所有员工姓名/
/方式1/
/①查询location_id是1400或1700的部门编号/
SELECT DISTINCT department_id
FROM departments
WHERE location_id IN (1400, 1700);

/②查询员工姓名,要求部门是①列表中的某一个/
SELECT a.last_name
FROM employees a
WHERE a.department_id IN (SELECT DISTINCT department_id
FROM departments
WHERE location_id IN (1400, 1700));

/方式2:使用any实现/
SELECT a.last_name
FROM employees a
WHERE a.department_id = ANY (SELECT DISTINCT department_id
FROM departments
WHERE location_id IN (1400, 1700));

/拓展,下面与not in等价/
SELECT a.last_name
FROM employees a
WHERE a.department_id <> ALL (SELECT DISTINCT department_id
FROM departments
WHERE location_id IN (1400, 1700));

示例 2

返回其他工种中比 job_id 为’IT_PROG’工种任意工资低的员工的员工号、姓名、job_id、salary
/返回其他工种中比job_id为’IT_PROG’工种任一工资低的员工的员工号、姓名、job_id、salary/
/①查询job_id为’IT_PROG’部门任-工资/
SELECT DISTINCT salary
FROM employees
WHERE job_id = ‘IT_PROG’;

/②查询员工号、姓名、job_id、salary,slary<①的任意一个/
SELECT
last_name,
employee_id,
job_id,
salary
FROM employees
WHERE salary < ANY (SELECT DISTINCT salary
FROM employees
WHERE job_id = ‘IT_PROG’) AND job_id != ‘IT_PROG’;

/或者/
SELECT
last_name,
employee_id,
job_id,
salary
FROM employees
WHERE salary < (SELECT max(salary)
FROM employees
WHERE job_id = ‘IT_PROG’) AND job_id != ‘IT_PROG’;

示例 3

返回其他工种中比 job_id 为’IT_PROG’部门所有工资低的员工的员工号、姓名、job_id、salary
/返回其他工种中比job_id为’IT_PROG’部门所有工资低的员工的员工号、姓名、job_id、salary/
SELECT
last_name,
employee_id,
job_id,
salary
FROM employees
WHERE salary < ALL (SELECT DISTINCT salary
FROM employees
WHERE job_id = ‘IT_PROG’) AND job_id != ‘IT_PROG’;

/或者/
SELECT
last_name,
employee_id,
job_id,
salary
FROM employees
WHERE salary < (SELECT min(salary)
FROM employees
WHERE job_id = ‘IT_PROG’) AND job_id != ‘IT_PROG’;

行子查询(子查询结果集一行多列)

示例

查询员工编号最小并且工资最高的员工信息,3 种方式。
/查询员工编号最小并且工资最高的员工信息/
/①查询最小的员工编号/
SELECT min(employee_id)
FROM employees;
/②查询最高工资/
SELECT max(salary)
FROM employees;
/③方式1:查询员工信息/
SELECT
FROM employees a
WHERE a.employee_id = (SELECT min(employee_id)
FROM employees)
AND salary = (SELECT *max
(salary)
FROM employees);

/方式2/
SELECT
FROM employees a
WHERE (a.employee_id, a.salary) = (SELECT
min(employee_id),
max(salary)
FROM employees);
/
方式3/
SELECT

FROM employees a
WHERE (a.employee_id, a.salary) in (SELECT
min(employee_id),
max(salary)
FROM employees);

方式 1 比较常见,方式 2、3 更简洁。

exists 后面(也叫做相关子查询)

  1. 语法:exists(完整的查询语句)。
  2. exists 查询结果:1 或 0,exists 查询的结果用来判断子查询的结果集中是否有值。
  3. 一般来说,能用 exists 的子查询,绝对都能用 in 代替,所以 exists 用的少。
  4. 和前面的查询不同,这先执行主查询,然后主查询查询的结果,在根据子查询进行过滤,子查询中涉及到主查询中用到的字段,所以叫相关子查询。

    示例 1

    简单示例
    mysql> SELECT exists(SELECT employee_id
    FROM employees
    WHERE salary = 300000) AS ‘exists返回1或者0’;
    +———————————+
    | exists返回1或者0 |
    +———————————+
    | 0 |
    +———————————+
    1 row in set (0.00 sec)

示例 2

查询所有员工的部门名称
/exists入门案例/
SELECT exists(SELECT employee_id
FROM employees
WHERE salary = 300000) AS ‘exists返回1或者0’;

/查询所有员工部门名/
SELECT department_name
FROM departments a
WHERE exists(SELECT 1
FROM employees b
WHERE a.department_id = b.department_id);

/使用in实现/
SELECT department_name
FROM departments a
WHERE a.department_id IN (SELECT department_id
FROM employees);

示例 3

查询没有员工的部门
/查询没有员工的部门/
/exists实现/
SELECT
FROM departments a
WHERE NOT exists(SELECT 1
FROM employees b
WHERE a.department_id = b.department_id AND b.department_id IS NOT NULL);
/
in的方式/
SELECT

FROM departments a
WHERE a.department_id NOT IN (SELECT department_id
FROM employees b
WHERE b.department_id IS NOT NULL);

上面脚本中有b.department_id IS NOT NULL,为什么,有大坑,向下看。

NULL 的大坑

示例 1

使用not in的方式查询没有员工的部门,如下:
SELECT
FROM departments a
WHERE a.department_id NOT *IN
(SELECT department_id
FROM employees b);

运行结果:
mysql> SELECT
-> FROM departments a
-> WHERE a.department_id NOT IN (SELECT department_id
-> FROM employees b);
Empty *set
(0.00 sec)

not in 的情况下,子查询中列的值为 NULL 的时候,外查询的结果为空。
建议:建表是,列不允许为空。

总结

  1. 本文中讲解了常见的子查询,请大家务必多练习
  2. 注意 in、any、some、any 的用法
  3. 字段值为 NULL 的时候,not in 查询有大坑,这个要注意
  4. 建议创建表的时候,列不允许为空

    十三、细说NULL导致的神坑,让人防不胜防

    比较运算符中使用 NULL

    认真看下面的效果
    mysql> select 1>NULL;
    +————+
    | 1>NULL |
    +————+
    | NULL |
    +————+
    1 row in set (0.00 sec)

mysql> select 1+————+
| 1+————+
| NULL |
+————+
1 row in set (0.00 sec)

mysql> select 1<>NULL;
+————-+
| 1<>NULL |
+————-+
| NULL |
+————-+
1 row in set (0.00 sec)

mysql> select 1>NULL;
+————+
| 1>NULL |
+————+
| NULL |
+————+
1 row in set (0.00 sec)

mysql> select 1+————+
| 1+————+
| NULL |
+————+
1 row in set (0.00 sec)

mysql> select 1>=NULL;
+————-+
| 1>=NULL |
+————-+
| NULL |
+————-+
1 row in set (0.00 sec)

mysql> select 1<=NULL;
+————-+
| 1<=NULL |
+————-+
| NULL |
+————-+
1 row in set (0.00 sec)

mysql> select 1!=NULL;
+————-+
| 1!=NULL |
+————-+
| NULL |
+————-+
1 row in set (0.00 sec)

mysql> select 1<>NULL;
+————-+
| 1<>NULL |
+————-+
| NULL |
+————-+
1 row in set (0.00 sec)

mysql> select NULL=NULL,NULL!=NULL;
+—————-+——————+
| NULL=NULL | NULL!=NULL |
+—————-+——————+
| NULL | NULL |
+—————-+——————+
1 row in set (0.00 sec)

mysql> select 1 in (null),1 not in (null),null in (null),null not in (null);
+——————-+————————-+————————+——————————+
| 1 in (null) | 1 not in (null) | null in (null) | null not in (null) |
+——————-+————————-+————————+——————————+
| NULL | NULL | NULL | NULL |
+——————-+————————-+————————+——————————+
1 row in set (0.00 sec)

mysql> select 1=any(select null),null=any(select null);
+——————————+———————————-+
| 1=any(select null) | null=any(select null) |
+——————————+———————————-+
| NULL | NULL |
+——————————+———————————-+
1 row in set (0.00 sec)

mysql> select 1=all(select null),null=all(select null);
+——————————+———————————-+
| 1=all(select null) | null=all(select null) |
+——————————+———————————-+
| NULL | NULL |
+——————————+———————————-+
1 row in set (0.00 sec)

结论:任何值和 NULL 使用运算符(>、<、>=、<=、!=、<>)或者(in、not in、any/some、all)比较时,返回值都为 NULL,NULL 作为布尔值的时候,不为 1 也不为 0。

准备数据

mysql> create table test1(a int,b int);
Query OK, 0 rows affected (0.01 sec)

mysql> insert into test1 values (1,1),(1,null),(null,null);
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0

mysql> select from test1;
+———+———+
| a | b |
+———+———+
| 1 | 1 |
| 1 | NULL |
| NULL | NULL |
+———+———+
3 rows in *set
(0.00 sec)

上面 3 条数据,认真看一下,特别是注意上面 NULL 的记录。

IN、NOT IN 和 NULL 比较

IN 和 NULL 比较

mysql> select from test1;
+———+———+
| a | b |
+———+———+
| 1 | 1 |
| 1 | NULL |
| NULL | NULL |
+———+———+
3 rows in *set
(0.00 sec)

mysql> select from test1 where a in (null);
Empty *set
(0.00 sec)

mysql> select from test1 where a in (null,1);
+———+———+
| a | b |
+———+———+
| 1 | 1 |
| 1 | NULL |
+———+———+
2 rows in *set
(0.00 sec)

结论:当 IN 和 NULL 比较时,无法查询出为 NULL 的记录。

NOT IN 和 NULL 比较

mysql> select from test1 where a not in (1);
Empty *set
(0.00 sec)

mysql> select from test1 where a not in (null);
Empty *set
(0.00 sec)

mysql> select from test1 where a not in (null,2);
Empty *set
(0.00 sec)

mysql> select from test1 where a not in (2);
+———+———+
| a | b |
+———+———+
| 1 | 1 |
| 1 | NULL |
+———+———+
2 rows in *set
(0.00 sec)

结论:当 NOT IN 后面有 NULL 值时,不论什么情况下,整个 sql 的查询结果都为空。

EXISTS、NOT EXISTS 和 NULL 比较

mysql> select from test2;
+———+———+
| a | b |
+———+———+
| 1 | 1 |
| 1 | NULL |
| NULL | NULL |
+———+———+
3 rows in *set
(0.00 sec)

mysql> select from test1 t1 where exists (select from test2 t2 where t1.a = t2.a);
+———+———+
| a | b |
+———+———+
| 1 | 1 |
| 1 | NULL |
+———+———+
2 rows in set (0.00 sec)

mysql> select from test1 t1 where not exists (select from test2 t2 where t1.a = t2.a);
+———+———+
| a | b |
+———+———+
| NULL | NULL |
+———+———+
1 row in set (0.00 sec)

上面我们复制了表 test1 创建了表 test2。
查询语句中使用 exists、not exists 对比 test1.a=test2.a,因为=不能比较 NULL,结果和预期一致。

判断 NULL 只能用 IS NULL、IS NOT NULL

mysql> select 1 is not null;
+———————-+
| 1 is not null |
+———————-+
| 1 |
+———————-+
1 row in set (0.00 sec)

mysql> select 1 is null;
+—————-+
| 1 is null |
+—————-+
| 0 |
+—————-+
1 row in set (0.00 sec)

mysql> select null is null;
+———————+
| null is null |
+———————+
| 1 |
+———————+
1 row in set (0.00 sec)

mysql> select null is not null;
+—————————+
| null is not null |
+—————————+
| 0 |
+—————————+
1 row in set (0.00 sec)

看上面的效果,返回的结果为 1 或者 0。
结论:判断是否为空只能用 IS NULL、IS NOT NULL。

聚合函数中 NULL 的坑

示例

mysql> select count(a),count(b),count() from test1;
+—————+—————+—————+
| count(a) | count(b) | count(
) |
+—————+—————+—————+
| 2 | 1 | 3 |
+—————+—————+—————+
1 row in set (0.00 sec)

count(a)返回了 2 行记录,a 字段为 NULL 的没有统计出来。
count(b)返回了 1 行记录,为 NULL 的 2 行记录没有统计出来。
count(*)可以统计所有数据,不论字段的数据是否为 NULL。

再继续看

mysql> select from test1 where a is null;
+———+———+
| a | b |
+———+———+
| NULL | NULL |
+———+———+
1 row in *set
(0.00 sec)

mysql> select count(a) from test1 where a is null;
+—————+
| count(a) |
+—————+
| 0 |
+—————+
1 row in set (0.00 sec)

上面第 1 个 sql 使用 is null 查询出了结果,第 2 个 sql 中 count(a)返回的是 0 行。
结论:count(字段)无法统计字段为 NULL 的值,count(*)可以统计值为 null 的行。

NULL 不能作为主键的值

mysql> create table test3(a int primary key,b int);
Query OK, 0 rows affected (0.01 sec)

mysql> insert into test3 values (null,1);
ERROR 1048 (23000): Column ‘a’ cannot be null

上面我们创建了一个表test3,字段a未指定不能为空,插入了一条 NULL 的数据,报错原因:a 字段的值不能为NULL,我们看一下表的创建语句:
mysql> show create table test3;
+———-+——————+
| Table | Create Table |
+———-+——————+
| test3 | CREATE TABLE test3 (
a int(11) NOT NULL,
b int(11) DEFAULT NULL,
PRIMARY KEY (a)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
+———-+——————+
1 row in set (0.00 sec)

从上面的脚本可以看出,当字段为主键的时候,字段会自动设置为not null。
结论:当字段为主键的时候,字段会自动设置为 not null。
看了上面这些还是比较晕,NULL 的情况确实比较难以处理,容易出错,最有效的方法就是避免使用 NULL。所以,强烈建议创建字段的时候字段不允许为 NULL,设置一个默认值。

总结

  • NULL 作为布尔值的时候,不为 1 也不为 0
  • 任何值和 NULL 使用运算符(>、<、>=、<=、!=、<>)或者(in、not in、any/some、all),返回值都为 NULL
  • 当 IN 和 NULL 比较时,无法查询出为 NULL 的记录
  • 当 NOT IN 后面有 NULL 值时,不论什么情况下,整个 sql 的查询结果都为空
  • 判断是否为空只能用 IS NULL、IS NOT NULL
  • count(字段)无法统计字段为 NULL 的值,count(*)可以统计值为 null 的行
  • 当字段为主键的时候,字段会自动设置为 not null
  • NULL 导致的坑让人防不胜防,强烈建议创建字段的时候字段不允许为 NULL,给个默认值

    十四、事务详解

    什么是事务?

    数据库中的事务是指对数据库执行一批操作,这些操作最终要么全部执行成功,要么全部失败,不会存在部分成功的情况。
    举个例子
    比如 A 用户给 B 用户转账 100 操作,过程如下:
    1.从A账户扣100
    2.给B账户加100

如果在事务的支持下,上面最终只有 2 种结果:

  1. 操作成功:A 账户减少 100;B 账户增加 100
  2. 操作失败:A、B 两个账户都没有发生变化

如果没有事务的支持,可能出现错:A 账户减少了 100,此时系统挂了,导致 B 账户没有加上 100,而 A 账户凭空少了 100。

事务的几个特性(ACID)

原子性(Atomicity)

事务的整个过程如原子操作一样,最终要么全部成功,或者全部失败,这个原子性是从最终结果来看的,从最终结果来看这个过程是不可分割的。

一致性(Consistency)

一个事务必须使数据库从一个一致性状态变换到另一个一致性状态。
首先回顾一下一致性的定义。所谓一致性,指的是数据处于一种有意义的状态,这种状态是语义上的而不是语法上的。最常见的例子是转帐。例如从帐户 A 转一笔钱到帐户 B 上,如果帐户 A 上的钱减少了,而帐户 B 上的钱却没有增加,那么我们认为此时数据处于不一致的状态。
从这段话的理解来看,所谓一致性,即,从实际的业务逻辑上来说,最终结果是对的、是跟程序员的所期望的结果完全符合的

隔离性(Isolation)

一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

持久性(Durability)

一个事务一旦提交,他对数据库中数据的改变就应该是永久性的。当事务提交之后,数据会持久化到硬盘,修改是永久性的。

Mysql 中事务操作

mysql 中事务默认是隐式事务,执行 insert、update、delete 操作的时候,数据库自动开启事务、提交或回滚事务。
是否开启隐式事务是由变量autocommit控制的。
所以事务分为隐式事务显式事务

隐式事务

事务自动开启、提交或回滚,比如 insert、update、delete 语句,事务的开启、提交或回滚由 mysql 内部自动控制的。
查看变量autocommit是否开启了自动提交
mysql> show variables like ‘autocommit’;
+———————-+———-+
| Variable_name | Value |
+———————-+———-+
| autocommit | ON |
+———————-+———-+
1 row in set, 1 warning (0.00 sec)

autocommit为 ON 表示开启了自动提交。

显式事务

事务需要手动开启、提交或回滚,由开发者自己控制。
2 种方式手动控制事务:
方式 1:
语法:
//设置不自动提交事务
set autocommit=0;
//执行事务操作
commit|rollback;

示例 1:提交事务操作,如下:
mysql> create table test1 (a int);
Query OK, 0 rows affected (0.01 sec)

mysql> select from test1;
Empty *set
(0.00 sec)

mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into test1 values(1);
Query OK, 1 row affected (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> select from test1;
+———+
| a |
+———+
| 1 |
+———+
1 row in *set
(0.00 sec)

示例 2:回滚事务操作,如下:
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into test1 values(2);
Query OK, 1 row affected (0.00 sec)

mysql> rollback;
Query OK, 0 rows affected (0.00 sec)

mysql> select from test1;
+———+
| a |
+———+
| 1 |
+———+
1 row in *set
(0.00 sec)

可以看到上面数据回滚了。
我们把autocommit还原回去:
mysql> set autocommit=1;
Query OK, 0 rows affected (0.00 sec)

方式 2:
语法:
start transaction;//开启事务
//执行事务操作
commit|rollback;

示例 1:提交事务操作,如下:
mysql> select from test1;
+———+
| a |
+———+
| 1 |
+———+
1 row in *set
(0.00 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into test1 values (2);
Query OK, 1 row affected (0.00 sec)

mysql> insert into test1 values (3);
Query OK, 1 row affected (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> select from test1;
+———+
| a |
+———+
| 1 |
| 2 |
| 3 |
+———+
3 rows in *set
(0.00 sec)

上面成功插入了 2 条数据。
示例 2:回滚事务操作,如下:
mysql> select from test1;
+———+
| a |
+———+
| 1 |
| 2 |
| 3 |
+———+
3 rows in *set
(0.00 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> delete from test1;
Query OK, 3 rows affected (0.00 sec)

mysql> rollback;
Query OK, 0 rows affected (0.00 sec)

mysql> select from test1;
+———+
| a |
+———+
| 1 |
| 2 |
| 3 |
+———+
3 rows in *set
(0.00 sec)

上面事务中我们删除了test1的数据,显示删除了 3 行,最后回滚了事务。

savepoint 关键字

在事务中我们执行了一大批操作,可能我们只想回滚部分数据,怎么做呢?
我们可以将一大批操作分为几个部分,然后指定回滚某个部分。可以使用savepoin来实现,效果如下:
先清除test1表数据:
mysql> delete from test1;
Query OK, 3 rows affected (0.00 sec)

mysql> select from test1;
Empty *set
(0.00 sec)

演示savepoint效果,认真看:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into test1 values (1);
Query OK, 1 row affected (0.00 sec)

mysql> savepoint part1;//设置一个保存点
Query OK, 0 rows affected (0.00 sec)

mysql> insert into test1 values (2);
Query OK, 1 row affected (0.00 sec)

mysql> rollback to part1;//将savepint = part1的语句到当前语句之间所有的操作回滚
Query OK, 0 rows affected (0.00 sec)

mysql> commit;//提交事务
Query OK, 0 rows affected (0.00 sec)

mysql> select from test1;
+———+
| a |
+———+
| 1 |
+———+
1 row in *set
(0.00 sec)

从上面可以看出,执行了 2 次插入操作,最后只插入了 1 条数据。
savepoint需要结合rollback to sp1一起使用,可以将保存点sp1到rollback to之间的操作回滚掉。

只读事务

表示在事务中执行的是一些只读操作,如查询,但是不会做 insert、update、delete 操作,数据库内部对只读事务可能会有一些性能上的优化。
用法如下:
start transaction read only;

示例:
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> start transaction read only;
Query OK, 0 rows affected (0.00 sec)

mysql> select from test1;
+———+
| a |
+———+
| 1 |
| 1 |
+———+
2 rows in *set
(0.00 sec)

mysql> delete from test1;
ERROR 1792 (25006): Cannot execute statement in a READ ONLY transaction.
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> select from test1;
+———+
| a |
+———+
| 1 |
| 1 |
+———+
2 rows in *set
(0.00 sec)

只读事务中执行 delete 会报错。

事务中的一些问题

这些问题主要是基于数据在多个事务中的可见性来说的。

脏读

一个事务在执行的过程中读取到了其他事务还没有提交的数据。这个还是比较好理解的。

读已提交

从字面上我们就可以理解,即一个事务操作过程中可以读取到其他事务已经提交的数据。
事务中的每次读取操作,读取到的都是数据库中其他事务已提交的最新的数据(相当于当前读)

可重复读

一个事务操作中对于一个读取操作不管多少次,读取到的结果都是一样的。

幻读

脏读、不可重复读、可重复读、幻读,其中最难理解的是幻读
以 mysql 为例:
幻读在可重复读的模式下才会出现,其他隔离级别中不会出现
幻读现象例子:
可重复读模式下,比如有个用户表,手机号码为主键,有两个事物进行如下操作
事务 A 操作如下:1、打开事务 2、查询号码为 X 的记录,不存在 3、插入号码为 X 的数据,插入报错(为什么会报错,先向下看) 4、查询号码为 X 的记录,发现还是不存在(由于是可重复读,所以读取记录 X 还是不存在的)
事物 B 操作:在事务 A 第 2 步操作时插入了一条 X 的记录,所以会导致 A 中第 3 步插入报错(违反了唯一约束)
上面操作对 A 来说就像发生了幻觉一样,明明查询 X(A 中第二步、第四步)不存在,但却无法插入成功
幻读可以这么理解:事务中后面的操作(插入号码 X)需要上面的读取操作(查询号码 X 的记录)提供支持,但读取操作却不能支持下面的操作时产生的错误,就像发生了幻觉一样。
如果还是理解不了的,继续向下看,后面后详细的演示。

事务的隔离级别

当多个事务同时进行的时候,如何确保当前事务中数据的正确性,比如 A、B 两个事物同时进行的时候,A 是否可以看到 B 已提交的数据或者 B 未提交的数据,这个需要依靠事务的隔离级别来保证,不同的隔离级别中所产生的效果是不一样的。
事务隔离级别主要是解决了上面多个事务之间数据可见性及数据正确性的问题。
隔离级别分为 4 种:

  1. 读未提交:READ-UNCOMMITTED
  2. 读已提交:READ-COMMITTED
  3. 可重复读:REPEATABLE-READ
  4. 串行:SERIALIZABLE

上面 4 中隔离级别越来越强,会导致数据库的并发性也越来越低。

查看隔离级别

mysql> show variables like ‘transaction_isolation’;
+———————————-+————————+
| Variable_name | Value |
+———————————-+————————+
| transaction_isolation | READ-COMMITTED |
+———————————-+————————+
1 row in set, 1 warning (0.00 sec)

隔离级别的设置

分 2 步骤,修改文件、重启 mysql,如下:
修改 mysql 中的 my.init 文件,我们将隔离级别设置为:READ-UNCOMMITTED,如下:
# 隔离级别设置,READ-UNCOMMITTED读未提交,READ-COMMITTED读已提交,REPEATABLE-READ可重复读,SERIALIZABLE串行
transaction-isolation=READ-UNCOMMITTED

以管理员身份打开 cmd 窗口,重启 mysql,如下:
C:\Windows\system32>net stop mysql
mysql 服务正在停止..
mysql 服务已成功停止。

C:\Windows\system32>net start mysql
mysql 服务正在启动 .
mysql 服务已经启动成功。

各种隔离级别中会出现的问题

隔离级别 脏读 不可重复读 幻读
READ-UNCOMMITTED
READ-COMMITTED
REPEATABLE-READ
SERIALIZABLE

表格中和网上有些不一样,主要是幻读这块,幻读只会在可重复读级别中才会出现,其他级别下不存在。
下面我们来演示一下,各种隔离级别中可见性的问题,开启两个窗口,叫做 A、B 窗口,两个窗口中登录 mysql。

READ-UNCOMMITTED:读未提交

将隔离级别置为READ-UNCOMMITTED:
# 隔离级别设置,READ-UNCOMMITTED读未提交,READ-COMMITTED读已提交,REPEATABLE-READ可重复读,SERIALIZABLE串行
transaction-isolation=READ-UNCOMMITTED

重启 mysql:
C:\Windows\system32>net stop mysql
mysql 服务正在停止..
mysql 服务已成功停止。

C:\Windows\system32>net start mysql
mysql 服务正在启动 .
mysql 服务已经启动成功。

查看隔离级别:
mysql> show variables like ‘transaction_isolation’;
+———————————-+————————+
| Variable_name | Value |
+———————————-+————————+
| transaction_isolation | READ-UNCOMMITTED |
+———————————-+————————+
1 row in set, 1 warning (0.00 sec)

先清空 test1 表数据:
delete from test1;
select * from test1;

按时间顺序在 2 个窗口中执行下面操作:

时间 窗口 A 窗口 B
T1 start transaction;

| | T2 | select * from test1; |

| | T3 |

| start transaction; | | T4 |

| insert into test1 values (1); | | T5 |

| select from test1; | | T6 | select from test1; |

| | T7 |

| commit; | | T8 | commit; |

|

A 窗口如下:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select from test1;
Empty *set
(0.00 sec)

mysql> select from test1;
+———+
| a |
+———+
| 1 |
+———+
1 row in *set
(0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

B 窗口如下:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into test1 values (1);
Query OK, 1 row affected (0.00 sec)

mysql> select from test1;
+———+
| a |
+———+
| 1 |
+———+
1 row in *set
(0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

看一下:
T2-A:无数据,T6-A:有数据,T6 时刻 B 还未提交,此时 A 已经看到了 B 插入的数据,说明出现了脏读
T2-A:无数据,T6-A:有数据,查询到的结果不一样,说明不可重复读
结论:读未提交情况下,可以读取到其他事务还未提交的数据,多次读取结果不一样,出现了脏读、不可重复读

READ-COMMITTED:读已提交

将隔离级别置为READ-COMMITTED
# 隔离级别设置,READ-UNCOMMITTED读未提交,READ-COMMITTED读已提交,REPEATABLE-READ可重复读,SERIALIZABLE串行
transaction-isolation=READ-COMMITTED

重启 mysql:
C:\Windows\system32>net stop mysql
mysql 服务正在停止..
mysql 服务已成功停止。

C:\Windows\system32>net start mysql
mysql 服务正在启动 .
mysql 服务已经启动成功。

查看隔离级别:
mysql> show variables like ‘transaction_isolation’;
+———————————-+————————+
| Variable_name | Value |
+———————————-+————————+
| transaction_isolation | READ-COMMITTED |
+———————————-+————————+
1 row in set, 1 warning (0.00 sec)

先清空 test1 表数据:
delete from test1;
select * from test1;

按时间顺序在 2 个窗口中执行下面操作:

时间 窗口 A 窗口 B
T1 start transaction;

| | T2 | select * from test1; |

| | T3 |

| start transaction; | | T4 |

| insert into test1 values (1); | | T5 |

| select from test1; | | T6 | select from test1; |

| | T7 |

| commit; | | T8 | select * from test1; |

| | T9 | commit; |

|

A 窗口如下:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select from test1;
Empty *set
(0.00 sec)

mysql> select from test1;
Empty *set
(0.00 sec)

mysql> select from test1;
+———+
| a |
+———+
| 1 |
+———+
1 row in *set
(0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

B 窗口如下:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into test1 values (1);
Query OK, 1 row affected (0.00 sec)

mysql> select from test1;
+———+
| a |
+———+
| 1 |
+———+
1 row in *set
(0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

看一下:
T5-B:有数据,T6-A 窗口:无数据,A 看不到 B 的数据,说明没有脏读
T6-A 窗口:无数据,T8-A:看到了 B 插入的数据,此时 B 已经提交了,A 看到了 B 已提交的数据,说明可以读取到已提交的数据
T2-A、T6-A:无数据,T8-A:有数据,多次读取结果不一样,说明不可重复读
结论:读已提交情况下,无法读取到其他事务还未提交的数据,可以读取到其他事务已经提交的数据,多次读取结果不一样,未出现脏读,出现了读已提交、不可重复读。

REPEATABLE-READ:可重复读

将隔离级别置为REPEATABLE-READ
# 隔离级别设置,READ-UNCOMMITTED读未提交,READ-COMMITTED读已提交,REPEATABLE-READ可重复读,SERIALIZABLE串行
transaction-isolation=REPEATABLE-READ

重启 mysql:
C:\Windows\system32>net stop mysql
mysql 服务正在停止..
mysql 服务已成功停止。

C:\Windows\system32>net start mysql
mysql 服务正在启动 .
mysql 服务已经启动成功。

查看隔离级别:
mysql> show variables like ‘transaction_isolation’;
+———————————-+————————+
| Variable_name | Value |
+———————————-+————————+
| transaction_isolation | REPEATABLE-READ |
+———————————-+————————+
1 row in set, 1 warning (0.00 sec)

先清空 test1 表数据:
delete from test1;
select * from test1;

按时间顺序在 2 个窗口中执行下面操作:

时间 窗口 A 窗口 B
T1 start transaction;

| | T2 | select * from test1; |

| | T3 |

| start transaction; | | T4 |

| insert into test1 values (1); | | T5 |

| select from test1; | | T6 | select from test1; |

| | T7 |

| commit; | | T8 | select * from test1; |

| | T9 | commit; |

| | T10 | select * from test1; |

|

A 窗口如下:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select from test1;
Empty *set
(0.00 sec)

mysql> select from test1;
Empty *set
(0.00 sec)

mysql> select from test1;
Empty *set
(0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> select from test1;
+———+
| a |
+———+
| 1 |
| 1 |
+———+
2 rows in *set
(0.00 sec)

B 窗口如下:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into test1 values (1);
Query OK, 1 row affected (0.00 sec)

mysql> select from test1;
+———+
| a |
+———+
| 1 |
| 1 |
+———+
2 rows in *set
(0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

看一下:
T2-A、T6-A 窗口:无数据,T5-B:有数据,A 看不到 B 的数据,说明没有脏读
T8-A:无数据,此时 B 已经提交了,A 看不到 B 已提交的数据,A 中 3 次读的结果一样都是没有数据的,说明可重复读
结论:可重复读情况下,未出现脏读,未读取到其他事务已提交的数据,多次读取结果一致,即可重复读。

幻读演示

幻读只会在REPEATABLE-READ(可重复读)级别下出现,需要先把隔离级别改为可重复读。
将隔离级别置为REPEATABLE-READ
# 隔离级别设置,READ-UNCOMMITTED读未提交,READ-COMMITTED读已提交,REPEATABLE-READ可重复读,SERIALIZABLE串行
transaction-isolation=REPEATABLE-READ

重启 mysql:
C:\Windows\system32>net stop mysql
mysql 服务正在停止..
mysql 服务已成功停止。

C:\Windows\system32>net start mysql
mysql 服务正在启动 .
mysql 服务已经启动成功。

查看隔离级别:
mysql> show variables like ‘transaction_isolation’;
+———————————-+————————+
| Variable_name | Value |
+———————————-+————————+
| transaction_isolation | REPEATABLE-READ |
+———————————-+————————+
1 row in set, 1 warning (0.00 sec)

准备数据:
mysql> create table t_user(id int primary key,name varchar(16) unique key);
Query OK, 0 rows affected (0.01 sec)

mysql> insert into t_user values (1,’路人甲Java’),(2,’路人甲Java’);
ERROR 1062 (23000): Duplicate entry ‘路人甲Java’ for key ‘name’

mysql> select from t_user;
Empty *set
(0.00 sec)

上面我们创建 t_user 表,name 添加了唯一约束,表示 name 不能重复,否则报错。
按时间顺序在 2 个窗口中执行下面操作:

时间 窗口 A 窗口 B
T1 start transaction;

| | T2 |

| start transaction; | | T3 |

| — 插入路人甲Java
insert into t_user values (1,’路人甲 Java’); | | T4 |

| select from t_user; | | T5 | — 查看路人甲Java是否存在
select
from t_user where name=’路人甲 Java’; |

| | T6 |

| commit; | | T7 | — 插入路人甲Java
insert into t_user values (2,’路人甲 Java’); |

| | T8 | — 查看路人甲Java是否存在
select * from t_user where name=’路人甲 Java’; |

| | T9 | commit; |

|

A 窗口如下:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select from t_user where name=’路人甲Java’;
Empty *set
(0.00 sec)

mysql> insert into t_user values (2,’路人甲Java’);
ERROR 1062 (23000): Duplicate entry ‘路人甲Java’ for key ‘name’
mysql> select from t_user where name=’路人甲Java’;
Empty *set
(0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

B 窗口如下:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into t_user values (1,’路人甲Java’);
Query OK, 1 row affected (0.00 sec)

mysql> select from t_user;
+——+———————-+
| id | name |
+——+———————-+
| 1 | 路人甲Java |
+——+———————-+
1 row in *set
(0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

看一下:
A 想插入数据路人甲Java,插入之前先查询了一下(T5 时刻)该用户是否存在,发现不存在,然后在 T7 时刻执行插入,报错了,报数据已经存在了,因为 T6 时刻B已经插入了路人甲Java。
然后 A 有点郁闷,刚才查的时候不存在的,然后 A 不相信自己的眼睛,又去查一次(T8 时刻),发现路人甲Java还是不存在的。
此时 A 心里想:数据明明不存在啊,为什么无法插入呢?这不是懵逼了么,A 觉得如同发生了幻觉一样。

SERIALIZABLE:串行

SERIALIZABLE 会让并发的事务串行执行(多个事务之间读写、写读、写写会产生互斥,效果就是串行执行,多个事务之间的读读不会产生互斥)。
读写互斥:事务 A 中先读取操作,事务 B 发起写入操作,事务 A 中的读取会导致事务 B 中的写入处于等待状态,直到 A 事务完成为止。
表示我开启一个事务,为了保证事务中不会出现上面说的问题(脏读、不可重复读、读已提交、幻读),那么我读取的时候,其他事务有修改数据的操作需要排队等待,等待我读取完成之后,他们才可以继续。
写读、写写也是互斥的,读写互斥类似。
这个类似于 java 中的java.util.concurrent.lock.ReentrantReadWriteLock类产生的效果。
下面演示读写互斥的效果。
将隔离级别置为SERIALIZABLE
# 隔离级别设置,READ-UNCOMMITTED读未提交,READ-COMMITTED读已提交,REPEATABLE-READ可重复读,SERIALIZABLE串行
transaction-isolation=SERIALIZABLE

重启 mysql:
C:\Windows\system32>net stop mysql
mysql 服务正在停止..
mysql 服务已成功停止。

C:\Windows\system32>net start mysql
mysql 服务正在启动 .
mysql 服务已经启动成功。

查看隔离级别:
mysql> show variables like ‘transaction_isolation’;
+———————————-+———————+
| Variable_name | Value |
+———————————-+———————+
| transaction_isolation | SERIALIZABLE |
+———————————-+———————+
1 row in set, 1 warning (0.00 sec)

先清空 test1 表数据:
delete from test1;
select * from test1;

按时间顺序在 2 个窗口中执行下面操作:

时间 窗口 A 窗口 B
T1 start transaction;

| | T2 | select * from test1; |

| | T3 |

| start transaction; | | T4 |

| insert into test1 values (1); | | T5 | commit; |

| | T6 |

| commit; |

按时间顺序运行上面的命令,会发现 T4-B 这样会被阻塞,直到 T5-A 执行完毕。
上面这个演示的是读写互斥产生的效果,大家可以自己去写一下写读、写写互斥的效果。
可以看出来,事务只能串行执行了。串行情况下不存在脏读、不可重复读、幻读的问题了。

关于隔离级别的选择

  1. 需要对各种隔离级别产生的现象非常了解,然后选择的时候才能游刃有余
  2. 隔离级别越高,并发性也低,比如最高级别SERIALIZABLE会让事物串行执行,并发操作变成串行了,会导致系统性能直接降低。

    十五、视图

    需求背景

    电商公司领导说:给我统计一下:当月订单总金额、订单量、男女订单占比等信息,我们啪啦啪啦写了一堆很复杂的 sql,然后发给领导。
    这样一大片 sql,发给领导,你们觉得好么?
    如果领导只想看其中某个数据,还需要修改你发来的 sql,领导日后想新增其他的统计指标,你又会发送一大坨 sql 给领导,对于领导来说这个 sql 看起来很复杂,难以维护。
    实际上领导并不关心你是怎么实现的,他关心的只是这些指标,并且方便查看、查询,而你却把复杂的实现都发给了领导。
    那我们有什么办法隐藏这些细节,只暴露简洁的结果呢?
    数据库已经帮我们想到了:使用视图来解决这个问题。

    什么是视图

    概念

    视图是在 mysql5 之后出现的,是一种虚拟表,行和列的数据来自于定义视图时使用的一些表中,视图的数据是在使用视图的时候动态生成的,视图只保存了 sql 的逻辑,不保存查询的结果

    使用场景

    多个地方使用到同样的查询结果,并且该查询结果比较复杂的时候,我们可以使用视图来隐藏复杂的实现细节。

    视图和表的区别

    |

    | 语法 | 实际中是否占用物理空间 | 使用 | | —- | —- | —- | —- | | 视图 | create view | 只是保存了 sql 的逻辑 | 增删改查,实际上我们只使用查询 | | 表 | create table | 保存了数据 | 增删改查 |

视图的好处

  • 简化复杂的 sql 操作,不用知道他的实现细节
  • 隔离了原始表,可以不让使用视图的人接触原始的表,从而保护原始数据,提高了安全性

    准备测试数据

    测试数据比较多,放在我的个人博客上了。
    浏览器中打开链接:http://www.itsoku.com/article/196
    mysql 中执行里面的javacode2018_employees库部分的脚本。
    成功创建javacode2018_employees库及 5 张表,如下:
表名 描述
departments 部门表
employees 员工信息表
jobs 职位信息表
locations 位置表(部门表中会用到)
job_grades 薪资等级表

创建视图

语法

create view 视图名
as
查询语句;

视图的使用步骤

  • 创建视图
  • 对视图执行查询操作

    案例 1

    查询姓名中包含 a 字符的员工名、部门、工种信息
    /案例1:查询姓名中包含a字符的员工名、部门、工种信息/
    /①创建视图myv1/
    CREATE VIEW myv1
    AS
    SELECT
    t1.last_name,
    t2.department_name,
    t3.job_title
    FROM employees t1, departments t2, jobs t3
    WHERE t1.department_id = t2.department_id
    AND t1.job_id = t3.job_id;

/②使用视图/
SELECT * FROM myv1 a where a.last_name like ‘a%’;

效果如下:
mysql> SELECT FROM myv1 a where a.last_name like ‘a%’;
+—————-+————————-+———————————+
| last_name | department_name | job_title |
+—————-+————————-+———————————+
| Austin | IT | Programmer |
| Atkinson | Shi | Stock Clerk |
| Ande | Sal | Sales Representative |
| Abel | Sal | Sales Representative |
+—————-+————————-+———————————+
4 rows in *set
(0.00 sec)

上面我们创建了一个视图:myv1,我们需要看员工姓名、部门、工种信息的时候,不用关心这个视图内部是什么样的,只需要查询视图就可以了,sql 简单多了。

案例 2

案例 2:查询各部门的平均工资级别
/案例2:查询各部门的平均工资级别/
/①创建视图myv1/
CREATE VIEW myv2
AS
SELECT
t1.department_id 部门id,
t1.ag 平均工资,
t2.grade_level 工资级别
FROM (SELECT
department_id,
AVG(salary) ag
FROM employees
GROUP BY department_id)
t1, job_grades t2
WHERE t1.ag BETWEEN t2.lowest_sal AND t2.highest_sal;

/②使用视图/
SELECT * FROM myv2;

效果:
mysql> SELECT FROM myv2;
+—————+———————+———————+
| 部门id | 平均工资 | 工资级别 |
+—————+———————+———————+
| NULL | 7000.000000 | C |
| 10 | 4400.000000 | B |
| 20 | 9500.000000 | C |
| 30 | 4150.000000 | B |
| 40 | 6500.000000 | C |
| 50 | 3475.555556 | B |
| 60 | 5760.000000 | B |
| 70 | 10000.000000 | D |
| 80 | 8955.882353 | C |
| 90 | 19333.333333 | E |
| 100 | 8600.000000 | C |
| 110 | 10150.000000 | D |
+—————+———————+———————+
12 rows in *set
(0.00 sec)

修改视图

2 种方式。

方式 1

如果该视图存在,就修改,如果不存在,就创建新的视图。
create or replace view 视图名
as
查询语句;

示例

CREATE OR REPLACE VIEW myv3
AS
SELECT
job_id,
AVG(salary) javg
FROM employees
GROUP BY job_id;

方式 2

alter view 视图名
as
查询语句;

示例

ALTER VIEW myv3
AS
SELECT *
FROM employees;

删除视图

语法

drop view 视图名1 [,视图名2] [,视图名n];

可以同时删除多个视图,多个视图名称之间用逗号隔开。

示例

mysql> drop view myv1,myv2,myv3;
Query OK, 0 rows affected (0.00 sec)

查询视图结构

/方式1/
desc 视图名称;
/方式2/
show create view 视图名称;

如:
mysql> desc myv1;
+————————-+——————-+———+——-+————-+———-+
| Field | Type | Null | Key | Default | Extra |
+————————-+——————-+———+——-+————-+———-+
| last_name | varchar(25) | YES | | NULL | |
| department_name | varchar(3) | YES | | NULL | |
| job_title | varchar(35) | YES | | NULL | |
+————————-+——————-+———+——-+————-+———-+
3 rows in set (0.00 sec)

mysql> show create view myv1;
+———+—————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-+———————————+———————————+
| View | Create View | character_set_client | collation_connection |
+———+—————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-+———————————+———————————+
| myv1 | CREATE ALGORITHM=UNDEFINED DEFINER=root@localhost SQL SECURITY DEFINER VIEW myv1 AS select t1.last_name AS last_name,t2.department_name AS department_name,t3.job_title AS job_title from ((employees t1 join departments t2) join jobs t3) where ((t1.department_id = t2.department_id) and (t1.job_id = t3.job_id)) | utf8 | utf8_general_ci |
+———+—————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-+———————————+———————————+
1 row in set (0.00 sec)

show create view显示了视图的创建语句。

更新视图【基本不用】

视图的更新是更改视图中的数据,而不是更改视图中的 sql 逻辑。
当对视图进行更新后,也会对原始表的数据进行更新。
为了防止对原始表的数据产生更新,可以为视图添加只读权限,只允许读视图,不允许对视图进行更新。
一般情况下,极少对视图进行更新操作。

示例

CREATE OR REPLACE VIEW myv4
AS
SELECT last_name,email
from employees;

/插入/
insert into myv4 VALUES (‘路人甲Java’,’javacode2018@163.com’);
SELECT * from myv4 where email like ‘javacode2018%’;

/修改/
UPDATE myv4 SET last_name = ‘刘德华’ WHERE last_name = ‘路人甲Java’;
SELECT * from myv4 where email like ‘javacode2018%’;

/删除/
DELETE FROM myv4 where last_name = ‘刘德华’;
SELECT * from myv4 where email like ‘javacode2018%’;

注意:视图的更新我们一般不使用,了解即可。

总结

  1. 了解视图的用途及与表的区别。
  2. 掌握视图的创建、使用、修改、删除。

    十六、变量

    变量分类

  • 系统变量
  • 自定义变量

    系统变量

    概念

    系统变量由系统定义的,不是用户定义的,属于 mysql 服务器层面的。

    系统变量分类

  • 全局变量

  • 会话变量

    使用步骤

    查看系统变量

    //1.查看系统所有变量
    show [global | session] variables;
    //查看全局变量
    show global variables;
    //查看会话变量
    show session variables;
    show variables;

上面使用了 show 关键字

查看满足条件的系统变量

通过 like 模糊匹配指定的变量
//查看满足条件的系统变量(like模糊匹配)
show [global|session] like ‘%变量名%’;

上面使用了 show 和 like 关键字。

查看指定的系统变量

//查看指定的系统变量的值
select @@[global.|session.]系统变量名称;

注意select和@@关键字,global 和 session 后面有个.符号。

赋值

//方式1
set [global|session] 系统变量名=值;

//方式2
set @@[global.|session.]系统变量名=值;

注意:
上面使用中介绍的,全局变量需要添加 global 关键字,会话变量需要添加 session 关键字,如果不写,默认为 session 级别。
全局变量的使用中用到了@@关键字,后面会介绍自定义变量,自定义变量中使用了一个@符号,这点需要和全局变量区分一下。

全局变量

作用域

mysql 服务器每次启动都会为所有的系统变量设置初始值。
我们为系统变量赋值,针对所有会话(连接)有效,可以跨连接,但不能跨重启,重启之后,mysql 服务器会再次为所有系统变量赋初始值。

示例

查看所有全局变量

/查看所有全局变量/
show global variables;

查看包含’tx’字符的变量

/查看包含tx字符的变量/
mysql> show global variables like ‘%tx%’;
+———————-+————————-+
| Variable_name | Value |
+———————-+————————-+
| tx_isolation | REPEATABLE-READ |
| tx_read_only | OFF |
+———————-+————————-+
2 rows in set, 1 warning (0.00 sec)

/查看指定名称的系统变量的值,如查看事务默认自动提交设置/
mysql> select @@global.autocommit;
+——————————-+
| @@global.autocommit |
+——————————-+
| 0 |
+——————————-+
1 row in set (0.00 sec)

为某个变量赋值

/为某个系统变量赋值/
set global autocommit=0;
set @@global.autocommit=1;

mysql> set global autocommit=0;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@global.autocommit;
+——————————-+
| @@global.autocommit |
+——————————-+
| 0 |
+——————————-+
1 row in set (0.00 sec)

mysql> set @@global.autocommit=1;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@global.autocommit;
+——————————-+
| @@global.autocommit |
+——————————-+
| 1 |
+——————————-+
1 row in set (0.00 sec)

会话变量

作用域

针对当前会话(连接)有效,不能跨连接。
会话变量是在连接创建时由 mysql 自动给当前会话设置的变量。

示例

查看所有会话变量

/①查看所有会话变量/
show session variables;

查看满足条件的会话变量

/②查看满足条件的步伐会话变量/
/查看包含char字符变量名的会话变量/
show session variables like ‘%char%’;

查看指定的会话变量的值

/③查看指定的会话变量的值/
/查看事务默认自动提交的设置/
select @@autocommit;
select @@session.autocommit;
/查看事务隔离级别/
select @@tx_isolation;
select @@session.tx_isolation;

为某个会话变量赋值

/④为某个会话变量赋值/
set @@session.tx_isolation=’read-uncommitted’;
set @@tx_isolation=’read-committed’;
set session tx_isolation=’read-committed’;
set tx_isolation=’read-committed’;

效果:
mysql> select @@tx_isolation;
+————————+
| @@tx_isolation |
+————————+
| READ-COMMITTED |
+————————+
1 row in set, 1 warning (0.00 sec)

mysql> set tx_isolation=’read-committed’;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> select @@tx_isolation;
+————————+
| @@tx_isolation |
+————————+
| READ-COMMITTED |
+————————+
1 row in set, 1 warning (0.00 sec)

自定义变量

概念

变量由用户自定义的,而不是系统提供的。

使用

使用步骤:
1. 声明
2. 赋值
3. 使用(查看、比较、运算)

分类

  • 用户变量
  • 局部变量

    用户变量

    作用域

    针对当前会话(连接)有效,作用域同会话变量。
    用户变量可以在任何地方使用也就是既可以在 begin end 里面使用,也可以在他外面使用。

    使用

    声明并初始化(要求声明时必须初始化)

    /方式1/
    set @变量名=值;
    /方式2/
    set @变量名:=值;
    /方式3/
    select @变量名:=值;

注意:
上面使用了@符合,而上面介绍全局变量使用了 2 个@符号,这点注意区分一下。
set 中=号前面冒号是可选的,select 方式=前面必须有冒号

赋值(更新变量的值)

/方式1:这块和变量的声明一样/
set @变量名=值;
set @变量名:=值;
select @变量名:=值;

/方式2/
select 字段 into @变量名 from 表;

注意上面 select 的两种方式。

使用

select @变量名;

综合示例

/set方式创建变量并初始化/
set @username=’路人甲java’;
/select into方式创建变量/
select ‘javacode2018’ into @gzh;
select count(*) into @empcount from employees;

/select :=方式创建变量/
select @first_name:=’路人甲Java’,@email:=’javacode2018@163.com’;
/使用变量/
insert into employees (first_name,email) values (@first_name,@email);

局部变量

作用域

declare 用于定义局部变量变量,在存储过程和函数中通过 declare 定义变量在 begin…end 中,且在语句之前。并且可以通过重复定义多个变量
declare 变量的作用范围同编程里面类似,在这里一般是在对应的 begin 和 end 之间。在 end 之后这个变量就没有作用了,不能使用了。这个同编程一样。

使用

声明

declare 变量名 变量类型;
declare 变量名 变量类型 [default 默认值];

赋值

/方式1/
set 局部变量名=值;
set 局部变量名:=值;
select 局部变量名:=值;

/方式2/
select 字段 into 局部变量名 from 表;

注意:局部变量前面没有@符号

使用(查看变量的值)

select 局部变量名;

示例

/创建表test1/
drop table IF EXISTS test1;
create table test1(a int PRIMARY KEY,b int);

/声明脚本的结束符为 /
DELIMITER
DROP PROCEDURE IF EXISTS proc1;
CREATE PROCEDURE proc1()
BEGIN
/声明了一个局部变量/
DECLARE v_a int;

select ifnull(max(a),0)+1 into v_a from test1;
select @v_b:=v_a2;
insert into *test1
(a,b) select v_a,@v_b;
end $$

/声明脚本的结束符为;/
DELIMITER ;

/调用存储过程/
call proc1();
/查看结果/
select * from test1;

代码中使用到了存储过程,关于存储过程的详解下章节介绍。

delimiter 关键字

我们写 sql 的时候,mysql 怎么判断 sql 是否已经结束了,可以去执行了?
需要一个结束符,当 mysql 看到这个结束符的时候,表示可以执行前面的语句了,mysql 默认以分号为结束符。
当我们创建存储过程或者自定义函数的时候,写了很大一片 sql,里面包含了很多分号,整个创建语句是一个整体,需要一起执行,此时我们就不可用用分号作为结束符了。
那么我们可以通过delimiter关键字来自定义结束符。
用法:
delimiter 分隔符

上面示例的效果

mysql> /创建表test1/
mysql> drop table IF EXISTS test1;
Query OK, 0 rows affected (0.01 sec)

mysql> create table test1(a int PRIMARY KEY,b int);
Query OK, 0 rows affected (0.01 sec)

mysql>
mysql> /声明脚本的结束符为 /
mysql> DELIMITER
mysql> DROP PROCEDURE IF EXISTS proc1;
-> CREATE PROCEDURE proc1()
-> BEGIN
-> /声明了一个局部变量/
-> DECLARE v_a int;
->
-> select ifnull(max(a),0)+1 into v_a from test1;
-> select @v_b:=v_a2;
-> insert into test1(a,b) select v_a,@v_b;
-> end $$
Query OK, 0 rows *affected
(0.00 sec)

Query OK, 0 rows affected (0.00 sec)

mysql>
mysql> /声明脚本的结束符为;/
mysql> DELIMITER ;
mysql>
mysql> /调用存储过程/
mysql> call proc1();
+——————-+
| @v_b:=v_a2 |
+——————-+
| 2 |
+——————-+
1 row in *set
(0.00 sec)

Query OK, 1 row affected (0.01 sec)

mysql> /查看结果/
mysql> select from test1;
+—-+———+
| a | b |
+—-+———+
| 1 | 2 |
+—-+———+
1 row in *set
(0.00 sec)

用户变量和局部变量对比

|

作用域 定义位置 语法
用户变量 当前会话 会话的任何地方 加@符号,不用指定类型
局部变量 定义他的 begin end 之间 begin end 中的第一句话 不加@符号,要指定类型

总结

  • 本文对系统变量和自定义变量的使用做了详细的说明,知识点比较细,可以多看几遍,加深理解
  • 系统变量可以设置系统的一些配置信息,数据库重启之后会被还原
  • 会话变量可以设置当前会话的一些配置信息,对当前会话起效
  • declare 创建的局部变量常用于存储过程和函数的创建中
  • 作用域:全局变量对整个系统有效、会话变量作用于当前会话、用户变量作用于当前会话、局部变量作用于 begin end 之间
  • 注意全局变量中用到了@@,用户变量变量用到了@,而局部变量没有这个符号
  • delimiter关键字用来声明脚本的结束符

    十七、存储过程 & 自定义函数

    需求背景介绍

    线上程序有时候出现问题导致数据错误的时候,如果比较紧急,我们可以写一个存储来快速修复这块的数据,然后再去修复程序,这种方式我们用到过不少。
    存储过程相对于 java 程序对于 java 开发来说,可能并不是太好维护以及阅读,所以不建议在程序中去调用存储过程做一些业务操作。
    关于自定义函数这块,若 mysql 内部自带的一些函数无法满足我们的需求的时候,我们可以自己开发一些自定义函数来使用。
    所以建议大家掌握 mysql 中存储过程和自定义函数这块的内容。

    本文内容

  • 详解存储过程的使用

  • 详解自定义函数的使用

    准备数据

    /建库javacode2018/
    drop database if exists javacode2018;
    create database javacode2018;

/切换到javacode2018库/
use javacode2018;

/建表test1/
DROP TABLE IF EXISTS t_user;
CREATE TABLE t_user (
id INT NOT NULL PRIMARY KEY COMMENT ‘编号’,
age SMALLINT UNSIGNED NOT NULL COMMENT ‘年龄’,
name VARCHAR(16) NOT NULL COMMENT ‘姓名’
) COMMENT ‘用户表’;

存储过程

概念

一组预编译好的 sql 语句集合,理解成批处理语句。
好处:

  • 提高代码的重用性
  • 简化操作
  • 减少编译次数并且减少和数据库服务器连接的次数,提高了效率。

    创建存储过程

    create procedure 存储过程名([参数模式] 参数名 参数类型)
    begin
    存储过程体
    end

参数模式有 3 种:
in:该参数可以作为输入,也就是该参数需要调用方传入值。
out:该参数可以作为输出,也就是说该参数可以作为返回值。
inout:该参数既可以作为输入也可以作为输出,也就是说该参数需要在调用的时候传入值,又可以作为返回值。
参数模式默认为 IN。
一个存储过程可以有多个输入、多个输出、多个输入输出参数。

调用存储过程

call 存储过程名称(参数列表);

注意:调用存储过程关键字是call。

删除存储过程

drop procedure [if exists] 存储过程名称;

存储过程只能一个个删除,不能批量删除。
if exists:表示存储过程存在的情况下删除。

修改存储过程

存储过程不能修改,若涉及到修改的,可以先删除,然后重建。

查看存储过程

show create procedure 存储过程名称;

可以查看存储过程详细创建语句。

示例

示例 1:空参列表

创建存储过程
/设置结束符为 $/
DELIMITER $
/如果存储过程存在则删除/
DROP PROCEDURE IF EXISTS proc1;
/创建存储过程proc1/
CREATE PROCEDURE proc1()
BEGIN
INSERT INTO t_user VALUES (1,30,’路人甲Java’);
INSERT INTO t_user VALUES (2,50,’刘德华’);
END $

/将结束符置为;/
DELIMITER ;

delimiter 用来设置结束符,当 mysql 执行脚本的时候,遇到结束符的时候,会把结束符前面的所有语句作为一个整体运行,存储过程中的脚本有多个 sql,但是需要作为一个整体运行,所以此处用到了 delimiter。
mysql 默认结束符是分号。
上面存储过程中向 t_user 表中插入了 2 条数据。
调用存储过程:
CALL proc1();

验证效果:
mysql> select from t_user;
+——+——-+———————-+
| id | age | name |
+——+——-+———————-+
| 1 | 30 | 路人甲Java |
| 2 | 50 | 刘德华 |
+——+——-+———————-+
2 rows in *set
(0.00 sec)

存储过程调用成功,test1 表成功插入了 2 条数据。

示例 2:带 in 参数的存储过程

创建存储过程:
/设置结束符为 $/
DELIMITER $
/如果存储过程存在则删除/
DROP PROCEDURE IF EXISTS proc2;
/创建存储过程proc2/
CREATE PROCEDURE proc2(id int,age int,in name varchar(16))
BEGIN
INSERT INTO t_user VALUES (id,age,name);
END $

/将结束符置为;/
DELIMITER ;

调用存储过程:
/创建了3个自定义变量/
SELECT @id:=3,@age:=56,@name:=’张学友’;
/调用存储过程/
CALL proc2(@id,@age,@name);

验证效果:
mysql> select from t_user;
+——+——-+———————-+
| id | age | name |
+——+——-+———————-+
| 1 | 30 | 路人甲Java |
| 2 | 50 | 刘德华 |
| 3 | 56 | 张学友 |
+——+——-+———————-+
3 rows in *set
(0.00 sec)

张学友插入成功。

示例 3:带 out 参数的存储过程

创建存储过程:
delete a from t_user a where a.id = 4;
/如果存储过程存在则删除/
DROP PROCEDURE IF EXISTS proc3;
/设置结束符为 $/
DELIMITER $
/创建存储过程proc3/
CREATE PROCEDURE proc3(id int,age int,in name varchar(16),out user_count int,out max_id INT)
BEGIN
INSERT INTO t_user VALUES (id,age,name);
/查询出t_user表的记录,放入user_count中,max_id用来存储t_user中最小的id/
SELECT COUNT(),*max(id) into user_count,max_id from t_user;
END $

/将结束符置为;/
DELIMITER ;

proc3 中前 2 个参数,没有指定参数模式,默认为 in。
调用存储过程:
/创建了3个自定义变量/
SELECT @id:=4,@age:=55,@name:=’郭富城’;
/调用存储过程/
CALL proc3(@id,@age,@name,@user_count,@max_id);

验证效果:
mysql> select @user_count,@max_id;
+——————-+————-+
| @user_count | @max_id |
+——————-+————-+
| 4 | 4 |
+——————-+————-+
1 row in set (0.00 sec)

示例 4:带 inout 参数的存储过程

创建存储过程:
/如果存储过程存在则删除/
DROP PROCEDURE IF EXISTS proc4;
/设置结束符为 $/
DELIMITER $
/创建存储过程proc4/
CREATE PROCEDURE proc4(INOUT a int,INOUT b int)
BEGIN
SET a = a2;
select b
2 into b;
END $

/将结束符置为;/
DELIMITER ;

调用存储过程:
/创建了2个自定义变量/
set @a=10,@b:=20;
/调用存储过程/
CALL proc4(@a,@b);

验证效果:
mysql> SELECT @a,@b;
+———+———+
| @a | @b |
+———+———+
| 20 | 40 |
+———+———+
1 row in set (0.00 sec)

上面的两个自定义变量@a、@b 作为入参,然后在存储过程内部进行了修改,又作为了返回值。

示例 5:查看存储过程

mysql> show create procedure proc4;
+———-+———-+———-+———-+———-+———-+
| Procedure | sql_mode | Create Procedure | character_set_client | collation_connection | Database Collation |
+———-+———-+———-+———-+———-+———-+
| proc4 | ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION | CREATE DEFINER=root@localhost PROCEDURE proc4(INOUT a int,INOUT b int)
BEGIN
SET a = a2;
select b
2 into b;
END | utf8 | utf8_general_ci | utf8_general_ci |
+———-+———-+———-+———-+———-+———-+
1 row in set (0.00 sec)

函数

概念

一组预编译好的 sql 语句集合,理解成批处理语句。类似于 java 中的方法,但是必须有返回值。

创建函数

create function 函数名(参数名称 参数类型)
returns 返回值类型
begin
函数体
end

参数是可选的。
返回值是必须的。

调用函数

select 函数名(实参列表);

删除函数

drop function [if exists] 函数名;

查看函数详细

show create function 函数名;

示例

示例 1:无参函数

创建函数:
/删除fun1/
DROP FUNCTION IF EXISTS fun1;
/设置结束符为 $/
DELIMITER $
/创建函数/
CREATE FUNCTION fun1()
returns INT
BEGIN
DECLARE max_id int DEFAULT 0;
SELECT max(id) INTO max_id FROM t_user;
return max_id;
END $
/设置结束符为;/
DELIMITER ;

调用看效果:
mysql> SELECT fun1();
+————+
| fun1() |
+————+
| 4 |
+————+
1 row in set (0.00 sec)

示例 2:有参函数

创建函数:
/删除函数/
DROP FUNCTION IF EXISTS get_user_id;
/设置结束符为 $/
DELIMITER $
/创建函数/
CREATE FUNCTION get_user_id(v_name VARCHAR(16))
returns INT
BEGIN
DECLARE r_id int;
SELECT id INTO r_id FROM t_user WHERE name = v_name;
return r_id;
END $
/设置结束符为;/
DELIMITER ;

运行看效果:
mysql> SELECT get_user_id(name) from t_user;
+—————————-+
| get_user_id(name) |
+—————————-+
| 1 |
| 2 |
| 3 |
| 4 |
+—————————-+
4 rows in set (0.00 sec)

存储过程和函数的区别

存储过程的关键字为procedure,返回值可以有多个,调用时用call一般用于执行比较复杂的的过程体、更新、创建等语句
函数的关键字为function返回值必须有一个,调用用select,一般用于查询单个值并返回。

|

存储过程 函数
返回值 可以有 0 个或者多个 必须有一个
关键字 procedure function
调用方式 call select

十八、流程控制语句详解

准备数据

/建库javacode2018/
drop database if exists javacode2018;
create database javacode2018;

/切换到javacode2018库/
use javacode2018;

/创建表:t_user/
DROP TABLE IF EXISTS t_user;
CREATE TABLE t_user(
id int PRIMARY KEY COMMENT ‘编号’,
sex TINYINT not null DEFAULT 1 COMMENT ‘性别,1:男,2:女’,
name VARCHAR(16) not NULL DEFAULT ‘’ COMMENT ‘姓名’
)COMMENT ‘用户表’;

/插入数据/
INSERT INTO t_user VALUES
(1,1,’路人甲Java’),(2,1,’张学友’),(3,2,’王祖贤’),(4,1,’郭富城’),(5,2,’李嘉欣’);

SELECT * FROM t_user;

DROP TABLE IF EXISTS test1;
CREATE TABLE test1 (a int not null);

DROP TABLE IF EXISTS test2;
CREATE TABLE test2 (a int not null,b int NOT NULL );

if 函数

语法

if(条件表达式,值1,值2);

if 函数有 3 个参数。
当参数 1 为 true 的时候,返回值1,否则返回值2。

示例

需求:查询t_user表数据,返回:编号、性别(男、女)、姓名。
分析一下:数据库中性别用数字表示的,我们需要将其转换为(男、女),可以使用 if 函数。
mysql> SELECT id 编号,if(sex=1,’男’,’女’) 性别,name 姓名 FROM t_user;
+————+————+———————-+
| 编号 | 性别 | 姓名 |
+————+————+———————-+
| 1 | 男 | 路人甲Java |
| 2 | 男 | 张学友 |
| 3 | 女 | 王祖贤 |
| 4 | 男 | 郭富城 |
| 5 | 女 | 李嘉欣 |
+————+————+———————-+
5 rows in set (0.00 sec)

CASE 结构

2 种用法。

第 1 种用法

类似于 java 中的 switch 语句。
case 表达式
when 值1 then 结果1或者语句1(如果是语句需要加分号)
when 值2 then 结果2或者语句2

else 结果n或者语句n
end [case] (如果是放在begin end之间需要加case,如果在select后则不需要)

示例 1:select 中使用

查询t_user表数据,返回:编号、性别(男、女)、姓名。
/写法1:类似于java中的if else/
SELECT id 编号,(CASE sex WHEN 1 THEN ‘男’ ELSE ‘女’ END) 性别,name 姓名 FROM t_user;
/写法2:类似于java中的if else if/
SELECT id 编号,(CASE sex WHEN 1 then ‘男’ WHEN 2 then ‘女’ END) 性别,name 姓名 FROM t_user;

示例 2:begin end 中使用

写一个存储过程,接受 3 个参数:id,性别(男、女),姓名,然后插入到 t_user 表
创建存储过程:
/删除存储过程proc1/
DROP PROCEDURE IF EXISTS proc1;
/s删除id=6的记录/
DELETE FROM t_user WHERE id=6;
/声明结束符为 $/
DELIMITER $
/创建存储过程proc1/
CREATE PROCEDURE proc1(id int,sex_str varchar(8),name varchar(16))
BEGIN
/声明变量v_sex用于存放性别/
DECLARE v_sex TINYINT UNSIGNED;
/根据sex_str的值来设置性别/
CASE sex_str
when ‘男’ THEN
SET v_sex = 1;
WHEN ‘女’ THEN
SET v_sex = 2;
END CASE ;
/插入数据/
INSERT INTO t_user VALUES (id,v_sex,name);
END $
/结束符置为;/
DELIMITER ;

调用存储过程:
CALL proc1(6,’男’,’郭富城’);

查看效果:
mysql> select from t_user;
+——+——-+———————-+
| id | sex | name |
+——+——-+———————-+
| 1 | 1 | 路人甲Java |
| 2 | 1 | 张学友 |
| 3 | 2 | 王祖贤 |
| 4 | 1 | 郭富城 |
| 5 | 2 | 李嘉欣 |
| 6 | 1 | 郭富城 |
+——+——-+———————-+
6 rows in *set
(0.00 sec)

示例 3:函数中使用

需求:写一个函数,根据 t_user 表 sex 的值,返回男女
创建函数:
/删除存储过程proc1/
DROP FUNCTION IF EXISTS fun1;
/声明结束符为 $/
DELIMITER $
/创建存储过程proc1/
CREATE FUNCTION fun1(sex TINYINT UNSIGNED)
RETURNS varchar(8)
BEGIN
/声明变量v_sex用于存放性别/
DECLARE v_sex VARCHAR(8);
CASE sex
WHEN 1 THEN
SET v_sex:=’男’;
ELSE
SET v_sex:=’女’;
END CASE;
RETURN v_sex;
END $
/结束符置为;/
DELIMITER ;

看一下效果:
mysql> select sex, fun1(sex) 性别,name FROM t_user;
+——-+————+———————-+
| sex | 性别 | name |
+——-+————+———————-+
| 1 | 男 | 路人甲Java |
| 1 | 男 | 张学友 |
| 2 | 女 | 王祖贤 |
| 1 | 男 | 郭富城 |
| 2 | 女 | 李嘉欣 |
| 1 | 男 | 郭富城 |
+——-+————+———————-+
6 rows in set (0.00 sec)

第 2 种用法

类似于 java 中多重 if 语句。
case
when 条件1 then 结果1或者语句1(如果是语句需要加分号)
when 条件2 then 结果2或者语句2

else 结果n或者语句n
end [case] (如果是放在begin end之间需要加case,如果是在select后面case可以省略)

这种写法和 1 中的类似,大家用上面这种语法实现第 1 中用法中的 3 个示例,贴在留言中。

if 结构

if 结构类似于 java 中的 if..else if…else 的语法,如下:
if 条件语句1 then 语句1;
elseif 条件语句2 then 语句2;

else 语句n;
end if;

只能使用在 begin end 之间。

示例

写一个存储过程,实现用户数据的插入和新增,如果 id 存在,则修改,不存在则新增,并返回结果
/删除id=7的记录/
DELETE FROM t_user WHERE id=7;
/删除存储过程/
DROP PROCEDURE IF EXISTS proc2;
/声明结束符为 $/
DELIMITER $
/创建存储过程/
CREATE PROCEDURE proc2(v_id int,v_sex varchar(8),v_name varchar(16),OUT result TINYINT)
BEGIN
DECLARE v_count TINYINT DEFAULT 0;/用来保存user记录的数量/
/根据v_id查询数据放入v_count中/
select count(id) into v_count from t_user where id = v_id;
/v_count>0表示数据存在,则修改,否则新增/
if v_count>0 THEN
BEGIN
DECLARE lsex TINYINT;
select if(lsex=’男’,1,2) into lsex;
update t_user set sex = lsex,name = v_name where id = v_id;
/获取update影响行数/
select ROW_COUNT() INTO result;
END;
else
BEGIN
DECLARE lsex TINYINT;
select if(lsex=’男’,1,2) into lsex;
insert into t_user VALUES (v_id,lsex,v_name);
select 0 into result;
END;
END IF;
END $
/结束符置为;/
DELIMITER ;

看效果:
mysql> SELECT FROM t_user;
+——+——-+———————-+
| id | sex | name |
+——+——-+———————-+
| 1 | 1 | 路人甲Java |
| 2 | 1 | 张学友 |
| 3 | 2 | 王祖贤 |
| 4 | 1 | 郭富城 |
| 5 | 2 | 李嘉欣 |
| 6 | 1 | 郭富城 |
+——+——-+———————-+
6 rows in *set
(0.00 sec)

mysql> CALL proc2(7,’男’,’黎明’,@result);
Query OK, 1 row affected (0.00 sec)

mysql> SELECT @result;
+————-+
| @result |
+————-+
| 0 |
+————-+
1 row in set (0.00 sec)

mysql> SELECT FROM t_user;
+——+——-+———————-+
| id | sex | name |
+——+——-+———————-+
| 1 | 1 | 路人甲Java |
| 2 | 1 | 张学友 |
| 3 | 2 | 王祖贤 |
| 4 | 1 | 郭富城 |
| 5 | 2 | 李嘉欣 |
| 6 | 1 | 郭富城 |
| 7 | 2 | 黎明 |
+——+——-+———————-+
7 rows in *set
(0.00 sec)

mysql> CALL proc2(7,’男’,’梁朝伟’,@result);
Query OK, 1 row affected (0.00 sec)

mysql> SELECT @result;
+————-+
| @result |
+————-+
| 1 |
+————-+
1 row in set (0.00 sec)

mysql> SELECT FROM t_user;
+——+——-+———————-+
| id | sex | name |
+——+——-+———————-+
| 1 | 1 | 路人甲Java |
| 2 | 1 | 张学友 |
| 3 | 2 | 王祖贤 |
| 4 | 1 | 郭富城 |
| 5 | 2 | 李嘉欣 |
| 6 | 1 | 郭富城 |
| 7 | 2 | 梁朝伟 |
+——+——-+———————-+
7 rows in *set
(0.00 sec)

循环

mysql 中循环有 3 种写法

  1. while:类似于 java 中的 while 循环
  2. repeat:类似于 java 中的 do while 循环
  3. loop:类似于 java 中的 while(true)死循环,需要在内部进行控制。

    循环控制

    对循环内部的流程进行控制,如:

    结束本次循环

    类似于 java 中的continue
    iterate 循环标签;

退出循环

类似于 java 中的break
leave 循环标签;

下面我们分别介绍 3 种循环的使用。

while 循环

类似于 java 中的 while 循环。

语法

[标签:]while 循环条件 do
循环体
end while [标签];

标签:是给 while 取个名字,标签和iterate、leave结合用于在循环内部对循环进行控制:如:跳出循环、结束本次循环。
注意:这个循环先判断条件,条件成立之后,才会执行循环体,每次执行都会先进行判断。

示例 1:无循环控制语句

根据传入的参数 v_count 向 test1 表插入指定数量的数据。
/删除test1表记录/
DELETE FROM test1;
/删除存储过程/
DROP PROCEDURE IF EXISTS proc3;
/声明结束符为 $/
DELIMITER $
/创建存储过程/
CREATE PROCEDURE proc3(v_count int)
BEGIN
DECLARE i int DEFAULT 1;
a:WHILE i<=v_count DO
INSERT into test1 values (i);
SET i=i+1;
END WHILE;
END $
/结束符置为;/
DELIMITER ;

见效果:
mysql> CALL proc3(5);
Query OK, 1 row affected (0.01 sec)

mysql> SELECT from test1;
+—-+
| a |
+—-+
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
+—-+
5 rows in *set
(0.00 sec)

示例 2:添加 leave 控制语句

根据传入的参数 v_count 向 test1 表插入指定数量的数据,当插入超过 10 条,结束。
/删除存储过程/
DROP PROCEDURE IF EXISTS proc4;
/声明结束符为 $/
DELIMITER $
/创建存储过程/
CREATE PROCEDURE proc4(v_count int)
BEGIN
DECLARE i int DEFAULT 1;
a:WHILE i<=v_count DO
INSERT into test1 values (i);
/判断i=10,离开循环a/
IF i=10 THEN
LEAVE a;
END IF;

  1. SET i=i+1;<br /> END WHILE;<br /> END $<br />/*结束符置为;*/<br />DELIMITER ;

见效果:
mysql> DELETE FROM test1;
Query OK, 20 rows affected (0.00 sec)

mysql> CALL proc4(20);
Query OK, 1 row affected (0.02 sec)

mysql> SELECT from test1;
+——+
| a |
+——+
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
| 6 |
| 7 |
| 8 |
| 9 |
| 10 |
+——+
10 rows in *set
(0.00 sec)

示例 3:添加 iterate 控制语句

根据传入的参数 v_count 向 test1 表插入指定数量的数据,只插入偶数数据。
/删除test1表记录/
DELETE FROM test1;
/删除存储过程/
DROP PROCEDURE IF EXISTS proc5;
/声明结束符为 $/
DELIMITER $
/创建存储过程/
CREATE PROCEDURE proc5(v_count int)
BEGIN
DECLARE i int DEFAULT 0;
a:WHILE i<=v_count DO
SET i=i+1;
/如果i不为偶数,跳过本次循环/
IF i%2!=0 THEN
ITERATE a;
END IF;
/插入数据/
INSERT into test1 values (i);
END WHILE;
END $
/结束符置为;/
DELIMITER ;

见效果:
mysql> DELETE FROM test1;
Query OK, 5 rows affected (0.00 sec)

mysql> CALL proc5(10);
Query OK, 1 row affected (0.01 sec)

mysql> SELECT from test1;
+——+
| a |
+——+
| 2 |
| 4 |
| 6 |
| 8 |
| 10 |
+——+
5 rows in *set
(0.00 sec)

示例 4:嵌套循环

test2 表有 2 个字段(a,b),写一个存储过程(2 个参数:v_a_count,v_b_count),使用双重循环插入数据,数据条件:a 的范围[1,v_a_count]、b 的范围[1,v_b_count]所有偶数的组合。
/删除存储过程/
DROP PROCEDURE IF EXISTS proc8;
/声明结束符为 $/
DELIMITER $
/创建存储过程/
CREATE PROCEDURE proc8(v_a_count int,v_b_count int)
BEGIN
DECLARE v_a int DEFAULT 0;
DECLARE v_b int DEFAULT 0;

  1. a:WHILE v_a<=v_a_count DO<br /> SET v_a=v_a+1;<br /> SET v_b=0;
  2. b:WHILE v_b<=v_b_count DO
  3. SET v_b=v_b+1;<br /> IF v_a%2!=0 THEN<br /> ITERATE a;<br /> END IF;
  4. IF v_b%2!=0 THEN<br /> ITERATE b;<br /> END IF;
  5. INSERT INTO test2 **VALUES** (v_a,v_b);
  6. END WHILE b;
  7. END WHILE a;<br /> END $<br />/*结束符置为;*/<br />DELIMITER ;

代码中故意将ITERATE a;放在内层循环中,主要让大家看一下效果。
见效果:
mysql> DELETE FROM test2;
Query OK, 6 rows affected (0.00 sec)

mysql> CALL proc8(4,6);
Query OK, 1 row affected (0.01 sec)

mysql> SELECT from test2;
+—-+—-+
| a | b |
+—-+—-+
| 2 | 2 |
| 2 | 4 |
| 2 | 6 |
| 4 | 2 |
| 4 | 4 |
| 4 | 6 |
+—-+—-+
6 rows in *set
(0.00 sec)

repeat 循环

语法

[标签:]repeat
循环体;
until 结束循环的条件 end repeat [标签];

repeat 循环类似于 java 中的 do…while 循环,不管如何,循环都会先执行一次,然后再判断结束循环的条件,不满足结束条件,循环体继续执行。这块和 while 不同,while 是先判断条件是否成立再执行循环体。

示例 1:无循环控制语句

根据传入的参数 v_count 向 test1 表插入指定数量的数据。
/删除存储过程/
DROP PROCEDURE IF EXISTS proc6;
/声明结束符为 $/
DELIMITER $
/创建存储过程/
CREATE PROCEDURE proc6(v_count int)
BEGIN
DECLARE i int DEFAULT 1;
a:REPEAT
INSERT into test1 values (i);
SET i=i+1;
UNTIL i>v_count END REPEAT;
END $
/结束符置为;/
DELIMITER ;

见效果:
mysql> DELETE FROM test1;
Query OK, 1 row affected (0.00 sec)

mysql> CALL proc6(5);
Query OK, 1 row affected (0.01 sec)

mysql> SELECT from test1;
+—-+
| a |
+—-+
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
+—-+
5 rows in *set
(0.00 sec)

repeat 中iterate和leave用法和 while 中类似,这块的示例算是给大家留的作业,写好的发在留言区,谢谢。

loop 循环

语法

[标签:]loop
循环体;
end loop [标签];

loop 相当于一个死循环,需要在循环体中使用iterate或者leave来控制循环的执行。

示例 1:无循环控制语句

根据传入的参数 v_count 向 test1 表插入指定数量的数据。
/删除存储过程/
DROP PROCEDURE IF EXISTS proc7;
/声明结束符为 $/
DELIMITER $
/创建存储过程/
CREATE PROCEDURE proc7(v_count int)
BEGIN
DECLARE i int DEFAULT 0;
a:LOOP
SET i=i+1;
/当i>v_count的时候退出循环/
IF i>v_count THEN
LEAVE a;
END IF;
INSERT into test1 values (i);
END LOOP a;
END $
/结束符置为;/
DELIMITER ;

见效果:
mysql> DELETE FROM test1;
Query OK, 5 rows affected (0.00 sec)

mysql> CALL proc7(5);
Query OK, 1 row affected (0.01 sec)

mysql> SELECT from test1;
+—-+
| a |
+—-+
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
+—-+
5 rows in *set
(0.00 sec)

loop 中iterate和leave用法和 while 中类似,这块的示例算是给大家留的作业,写好的发在留言区,谢谢。

总结

  1. 本文主要介绍了 mysql 中控制流语句的使用,请大家下去了多练习,熟练掌握
  2. if 函数常用在 select 中
  3. case 语句有 2 种写法,主要用在 select、begin end 中,select 中 end 后面可以省略 case,begin end 中使用不能省略 case
  4. if 语句用在 begin end 中
  5. 3 种循环体的使用,while 类似于 java 中的 while 循环,repeat 类似于 java 中的 do while 循环,loop 类似于 java 中的死循环,都用于 begin end 中
  6. 循环中体中的控制依靠leave和iterate,leave类似于 java 中的break可以退出循环,iterate类似于 java 中的 continue 可以结束本次循环

    十九、游标

    需求背景

    当我们需要对一个 select 的查询结果进行遍历处理的时候,如何实现呢?
    此时我们需要使用游标,通过游标的方式来遍历 select 查询的结果集,然后对每行数据进行处理。

    本篇内容

  • 游标定义
  • 游标作用
  • 游标使用步骤
  • 游标执行过程详解
  • 单游标示例
  • 嵌套游标示例

    准备数据

    创建库:javacode2018
    创建表:test1、test2、test3
    /建库javacode2018/
    drop database if exists javacode2018;
    create database javacode2018;

/切换到javacode2018库/
use javacode2018;

DROP TABLE IF EXISTS test1;
CREATE TABLE test1(a int,b int);
INSERT INTO test1 VALUES (1,2),(3,4),(5,6);

DROP TABLE IF EXISTS test2;
CREATE TABLE test2(a int);
INSERT INTO test2 VALUES (100),(200),(300);

DROP TABLE IF EXISTS test3;
CREATE TABLE test3(b int);
INSERT INTO test3 VALUES (400),(500),(600);

游标定义

游标(Cursor)是处理数据的一种方法,为了查看或者处理结果集中的数据,游标提供了在结果集中一次一行遍历数据的能力。
游标只能在存储过程和函数中使用。

游标的作用

如 sql:
select a,b from test1;

上面这个查询返回了 test1 中的数据,如果我们想对这些数据进行遍历处理,此时我们就可以使用游标来进行操作。
游标相当于一个指针,这个指针指向 select 的第一行数据,可以通过移动指针来遍历后面的数据。

游标的使用步骤

声明游标:这个过程只是创建了一个游标,需要指定这个游标需要遍历的 select 查询,声明游标时并不会去执行这个 sql。
打开游标:打开游标的时候,会执行游标对应的 select 语句。
遍历数据:使用游标循环遍历 select 结果中每一行数据,然后进行处理。
关闭游标:游标使用完之后一定要关闭。

游标语法

声明游标

DECLARE 游标名称 CURSOR FOR 查询语句;

一个 begin end 中只能声明一个游标。

打开游标

open 游标名称;

遍历游标

fetch 游标名称 into 变量列表;

取出当前行的结果,将结果放在对应的变量中,并将游标指针指向下一行的数据。
当调用 fetch 的时候,会获取当前行的数据,如果当前行无数据,会引发 mysql 内部的NOT FOUND错误。

关闭游标

close 游标名称;

游标使用完毕之后一定要关闭。

单游标示例

写一个函数,计算 test1 表中 a、b 字段所有的和。
创建函数:
/删除函数/
DROP FUNCTION IF EXISTS fun1;
/声明结束符为 $/
DELIMITER $
/创建函数/
CREATE FUNCTION fun1(v_max_a int)
RETURNS int
BEGIN
/用于保存结果/
DECLARE v_total int DEFAULT 0;
/创建一个变量,用来保存当前行中a的值/
DECLARE v_a int DEFAULT 0;
/创建一个变量,用来保存当前行中b的值/
DECLARE v_b int DEFAULT 0;
/创建游标结束标志变量/
DECLARE v_done int DEFAULT FALSE;
/创建游标/
DECLARE cur_test1 CURSOR FOR SELECT a,b from test1 where a<=v_max_a;
/设置游标结束时v_done的值为true,可以v_done来判断游标是否结束了/
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done=TRUE;
/设置v_total初始值/
SET v_total = 0;
/打开游标/
OPEN cur_test1;
/使用Loop循环遍历游标/
a:LOOP
/先获取当前行的数据,然后将当前行的数据放入v_a,v_b中,如果当前行无数据,v_done会被置为true/
FETCH cur_test1 INTO v_a, v_b;
/通过v_done来判断游标是否结束了,退出循环/
if v_done THEN
LEAVE a;
END IF;
/对v_total值累加处理/
SET v_total = v_total + v_a + v_b;
END LOOP;
/关闭游标/
CLOSE cur_test1;
/返回结果/
RETURN v_total;
END $
/结束符置为;/
DELIMITER ;

上面语句执行过程中可能有问题,解决方式如下。
错误信息:Mysql 创建函数出现 This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA
This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary
mysql 的设置默认是不允许创建函数
解决办法 1:
执行:
SET GLOBAL log_bin_trust_function_creators = 1;
不过 重启了 就失效了
注意:有主从复制的时候 从机必须要设置 不然会导致主从同步失败
解决办法 2:
在 my.cnf 里面设置
log-bin-trust-function-creators=1
不过这个需要重启服务
见效果:
mysql> SELECT a,b FROM test1;
+———+———+
| a | b |
+———+———+
| 1 | 2 |
| 3 | 4 |
| 5 | 6 |
+———+———+
3 rows in set (0.00 sec)

mysql> SELECT fun1(1);
+————-+
| fun1(1) |
+————-+
| 3 |
+————-+
1 row in set (0.00 sec)

mysql> SELECT fun1(2);
+————-+
| fun1(2) |
+————-+
| 3 |
+————-+
1 row in set (0.00 sec)

mysql> SELECT fun1(3);
+————-+
| fun1(3) |
+————-+
| 10 |
+————-+
1 row in set (0.00 sec)

游标过程详解

以上面的示例代码为例,咱们来看一下游标的详细执行过程。
游标中有个指针,当打开游标的时候,才会执行游标对应的 select 语句,这个指针会指向 select 结果中第一行记录
当调用fetch 游标名称时,会获取当前行的数据,如果当前行无数据,会触发NOT FOUND异常。
当触发NOT FOUND异常的时候,我们可以使用一个变量来标记一下,如下代码:
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done=TRUE;

当游标无数据触发NOT FOUND异常的时候,将变量v_down的值置为TURE,循环中就可以通过v_down的值控制循环的退出。
如果当前行有数据,则将当前行数据存到对应的变量中,并将游标指针指向下一行数据,如下语句:
fetch 游标名称 into 变量列表;

嵌套游标

写个存储过程,遍历 test2、test3,将 test2 中的 a 字段和 test3 中的 b 字段任意组合,插入到 test1 表中。
创建存储过程:
/删除存储过程/
DROP PROCEDURE IF EXISTS proc1;
/声明结束符为 $/
DELIMITER $
/创建存储过程/
CREATE PROCEDURE proc1()
BEGIN
/创建一个变量,用来保存当前行中a的值/
DECLARE v_a int DEFAULT 0;
/创建游标结束标志变量/
DECLARE v_done1 int DEFAULT FALSE;
/创建游标/
DECLARE cur_test1 CURSOR FOR SELECT a FROM test2;
/设置游标结束时v_done1的值为true,可以v_done1来判断游标cur_test1是否结束了/
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done1=TRUE;
/打开游标/
OPEN cur_test1;
/使用Loop循环遍历游标/
a:LOOP
FETCH cur_test1 INTO v_a;
/通过v_done1来判断游标是否结束了,退出循环/
if v_done1 THEN
LEAVE a;
END IF;

  1. BEGIN<br /> /*创建一个变量,用来保存当前行中b的值*/<br /> DECLARE v_b **int** DEFAULT 0;<br /> /*创建游标结束标志变量*/<br /> DECLARE v_done2 **int** DEFAULT FALSE;<br /> /*创建游标*/<br /> DECLARE cur_test2 CURSOR FOR SELECT b FROM test3;<br /> /*设置游标结束时v_done1的值为true,可以v_done1来判断游标cur_test2是否结束了*/<br /> DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done2=TRUE;
  2. /*打开游标*/<br /> OPEN cur_test2;<br /> /*使用Loop循环遍历游标*/<br /> b:LOOP<br /> FETCH cur_test2 INTO v_b;<br /> /*通过v_done1来判断游标是否结束了,退出循环*/<br /> **if** v_done2 THEN<br /> LEAVE b;<br /> END IF;
  3. /*将v_a、v_b插入test1表中*/<br /> INSERT INTO test1 **VALUES** (v_a,v_b);<br /> END LOOP b;<br /> /*关闭cur_test2游标*/<br /> CLOSE cur_test2;<br /> END;
  4. END LOOP;<br /> /*关闭游标cur_test1*/<br /> CLOSE cur_test1;<br /> END $<br />/*结束符置为;*/<br />DELIMITER ;

见效果:
mysql> DELETE FROM test1;
Query OK, 9 rows affected (0.00 sec)

mysql> SELECT FROM test1;
Empty *set
(0.00 sec)

mysql> CALL proc1();
Query OK, 0 rows affected (0.02 sec)

mysql> SELECT from test1;
+———+———+
| a | b |
+———+———+
| 100 | 400 |
| 100 | 500 |
| 100 | 600 |
| 200 | 400 |
| 200 | 500 |
| 200 | 600 |
| 300 | 400 |
| 300 | 500 |
| 300 | 600 |
+———+———+
9 rows in *set
(0.00 sec)

成功插入了 9 条数据。

总结

  1. 游标用来对查询结果进行遍历处理
  2. 游标的使用过程:声明游标、打开游标、遍历游标、关闭游标
  3. 游标只能在存储过程和函数中使用
  4. 一个 begin end 中只能声明一个游标
  5. 掌握单个游标及嵌套游标的使用
  6. 大家下去了多练习一下,熟练掌握游标的使用

    二十、异常处理详解

    需求背景

    我们在写存储过程的时候,可能会出现下列一些情况:

  7. 插入的数据违反唯一约束,导致插入失败

  8. 插入或者更新数据超过字段最大长度,导致操作失败
  9. update 影响行数和期望结果不一致

遇到上面各种异常情况的时,可能需要我们能够捕获,然后可能需要回滚当前事务。
本文主要围绕异常处理这块做详细的介绍。
此时我们需要使用游标,通过游标的方式来遍历 select 查询的结果集,然后对每行数据进行处理。

本篇内容

  • 异常分类详解
  • 内部异常详解
  • 外部异常详解
  • 掌握乐观锁解决并发修改数据出错的问题
  • update 影响行数和期望结果不一致时的处理

    准备数据

    创建库:javacode2018
    创建表:test1,test1 表中的 a 字段为主键。
    /建库javacode2018/
    drop database if exists javacode2018;
    create database javacode2018;

/切换到javacode2018库/
use javacode2018;

DROP TABLE IF EXISTS test1;
CREATE TABLE test1(a int PRIMARY KEY);

异常分类

我们将异常分为 mysql 内部异常和外部异常

mysql 内部异常

当我们执行一些 sql 的时候,可能违反了 mysql 的一些约束,导致 mysql 内部报错,如插入数据违反唯一约束,更新数据超时等,此时异常是由 mysql 内部抛出的,我们将这些由 mysql 抛出的异常统称为内部异常。

外部异常

当我们执行一个 update 的时候,可能我们期望影响 1 行,但是实际上影响的不是 1 行数据,这种情况:sql 的执行结果和期望的结果不一致,这种情况也我们也把他作为外部异常处理,我们将 sql 执行结果和期望结果不一致的情况统称为外部异常。

Mysql 内部异常

示例 1

test1 表中的 a 字段为主键,我们向 test1 表同时插入 2 条数据,并且放在一个事务中执行,最终要么都插入成功,要么都失败。

创建存储过程:

/删除存储过程/
DROP PROCEDURE IF EXISTS proc1;
/声明结束符为 $/
DELIMITER $
/创建存储过程/
CREATE PROCEDURE proc1(a1 int,a2 int)
BEGIN
START TRANSACTION;
INSERT INTO test1(a) VALUES (a1);
INSERT INTO test1(a) VALUES (a2);
COMMIT;
END $
/结束符置为;/
DELIMITER ;

上面存储过程插入了两条数据,a 的值都是 1。

验证结果:

mysql> DELETE FROM test1;
Query OK, 0 rows affected (0.00 sec)

mysql> CALL proc1(1,1);
ERROR 1062 (23000): Duplicate entry ‘1’ for key ‘PRIMARY’
mysql> SELECT from test1;
+—-+
| a |
+—-+
| 1 |
+—-+
1 row in *set
(0.00 sec)

上面先删除了 test1 表中的数据,然后调用存储过程proc1,由于 test1 表中的 a 字段是主键,插入第二条数据时违反了 a 字段的主键约束,mysql 内部抛出了异常,导致第二条数据插入失败,最终只有第一条数据插入成功了。
上面的结果和我们期望的不一致,我们希望要么都插入成功,要么失败。
那我们怎么做呢?我们需要捕获上面的主键约束异常,然后发现有异常的时候执行rollback回滚操作,改进上面的代码,看下面示例 2。

示例 2

我们对上面示例进行改进,捕获上面主键约束异常,然后进行回滚处理,如下:

创建存储过程:

/删除存储过程/
DROP PROCEDURE IF EXISTS proc2;
/声明结束符为 $/
DELIMITER $
/创建存储过程/
CREATE PROCEDURE proc2(a1 int,a2 int)
BEGIN
/声明一个变量,标识是否有sql异常/
DECLARE hasSqlError int DEFAULT FALSE;
/在执行过程中出任何异常设置hasSqlError为TRUE/
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET hasSqlError=TRUE;

  1. /*开启事务*/<br /> START TRANSACTION;<br /> INSERT INTO **test1**(a) **VALUES** (a1);<br /> INSERT INTO **test1**(a) **VALUES** (a2);
  2. /*根据hasSqlError判断是否有异常,做回滚和提交操作*/<br /> IF hasSqlError THEN<br /> ROLLBACK;<br /> ELSE<br /> COMMIT;<br /> END IF;<br /> END $<br />/*结束符置为;*/<br />DELIMITER ;

上面重点是这句:

DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET hasSqlError=TRUE;

当有 sql 异常的时候,会将变量hasSqlError的值置为TRUE。

模拟异常情况:

mysql> DELETE FROM test1;
Query OK, 2 rows affected (0.00 sec)

mysql> CALL proc2(1,1);
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT from test1;
Empty *set
(0.00 sec)

上面插入了 2 条一样的数据,插入失败,可以看到上面test1表无数据,和期望结果一致,插入被回滚了。

模拟正常情况:

mysql> DELETE FROM test1;
Query OK, 0 rows affected (0.00 sec)

mysql> CALL proc2(1,2);
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT from test1;
+—-+
| a |
+—-+
| 1 |
| 2 |
+—-+
2 rows in *set
(0.00 sec)

上面插入了 2 条不同的数据,最终插入成功。

外部异常

外部异常不是由 mysql 内部抛出的错误,而是由于 sql 的执行结果和我们期望的结果不一致的时候,我们需要对这种情况做一些处理,如回滚操作。

示例 1

我们来模拟电商中下单操作,按照上面的步骤来更新账户余额。

电商中有个账户表和订单表,如下:

DROP TABLE IF EXISTS t_funds;
CREATE TABLE t_funds(
user_id INT PRIMARY KEY COMMENT ‘用户id’,
available DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT ‘账户余额’
) COMMENT ‘用户账户表’;
DROP TABLE IF EXISTS t_order;
CREATE TABLE t_order(
id int PRIMARY KEY AUTO_INCREMENT COMMENT ‘订单id’,
price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT ‘订单金额’
) COMMENT ‘订单表’;
delete from t_funds;
/插入一条数据,用户id为1001,余额为1000/
INSERT INTO t_funds (user_id,available) VALUES (1001,1000);

下单操作涉及到操作上面的账户表,我们用存储过程来模拟实现:

/删除存储过程/
DROP PROCEDURE IF EXISTS proc3;
/声明结束符为 $/
DELIMITER $
/创建存储过程/
CREATE PROCEDURE proc3(v_user_id int,v_price decimal(10,2),OUT v_msg varchar(64))
a:BEGIN
DECLARE v_available DECIMAL(10,2);

  1. /*1.查询余额,判断余额是否够*/<br /> select a.available into v_available from t_funds a where a.user_id = v_user_id;<br /> **if** v_available<=v_price THEN<br /> SET v_msg='账户余额不足!';<br /> /*退出*/<br /> LEAVE a;<br /> END IF;
  2. /*模拟耗时5秒*/<br /> SELECT **sleep**(5);
  3. /*2.余额减去price*/<br /> SET v_available = v_available - v_price;
  4. /*3.更新余额*/<br /> START TRANSACTION;<br /> UPDATE t_funds SET available = v_available WHERE user_id = v_user_id;
  5. /*插入订单明细*/<br /> INSERT INTO **t_order** (price) **VALUES** (v_price);
  6. /*提交事务*/<br /> COMMIT;<br /> SET v_msg='下单成功!';<br /> END $<br />/*结束符置为;*/<br />DELIMITER ;

上面过程主要分为 3 步骤:验证余额、修改余额变量、更新余额。

开启 2 个 cmd 窗口,连接 mysql,同时执行下面操作:

USE javacode2018;
CALL proc3(1001,100,@v_msg);
select @v_msg;

然后执行:

mysql> SELECT FROM t_funds;
+————-+—————-+
| user_id | available |
+————-+—————-+
| 1001 | 900.00 |
+————-+—————-+
1 row in *set
(0.00 sec)

mysql> SELECT FROM t_order;
+——+————+
| id | price |
+——+————+
| 1 | 100.00 |
| 2 | 100.00 |
+——+————+
2 rows in *set
(0.00 sec)

上面出现了非常严重的错误:下单成功了 2 次,但是账户只扣了 100。
上面过程是由于 2 个操作并发导致的,2 个窗口同时执行第一步的时候看到了一样的数据(看到的余额都是 1000),然后继续向下执行,最终导致结果出问题了。
上面操作我们可以使用乐观锁来优化。
乐观锁的过程:用期望的值和目标值进行比较,如果相同,则更新目标值,否则什么也不做。
乐观锁类似于 java 中的 cas 操作,这块需要了解的可以点击:详解 CAS
我们可以在资金表t_funds添加一个version字段,表示版本号,每次更新数据的时候+1,更新数据的时候将 version 作为条件去执行 update,根据 update 影响行数来判断执行是否成功,优化上面的代码,见示例 2

示例 2

对示例 1 进行优化。

创建表:

DROP TABLE IF EXISTS t_funds;
CREATE TABLE t_funds(
user_id INT PRIMARY KEY COMMENT ‘用户id’,
available DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT ‘账户余额’,
version INT DEFAULT 0 COMMENT ‘版本号,每次更新+1’
) COMMENT ‘用户账户表’;

DROP TABLE IF EXISTS t_order;
CREATE TABLE t_order(
id int PRIMARY KEY AUTO_INCREMENT COMMENT ‘订单id’,
price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT ‘订单金额’
)COMMENT ‘订单表’;
delete from t_funds;
/插入一条数据,用户id为1001,余额为1000/
INSERT INTO t_funds (user_id,available) VALUES (1001,1000);

创建存储过程:

/删除存储过程/
DROP PROCEDURE IF EXISTS proc4;
/声明结束符为 $/
DELIMITER $
/创建存储过程/
CREATE PROCEDURE proc4(v_user_id int,v_price decimal(10,2),OUT v_msg varchar(64))
a:BEGIN
/保存当前余额/
DECLARE v_available DECIMAL(10,2);
/保存版本号/
DECLARE v_version INT DEFAULT 0;
/保存影响的行数/
DECLARE v_update_count INT DEFAULT 0;

  1. /*1.查询余额,判断余额是否够*/<br /> select a.available,a.version into v_available,v_version from t_funds a where a.user_id = v_user_id;<br /> **if** v_available<=v_price THEN<br /> SET v_msg='账户余额不足!';<br /> /*退出*/<br /> LEAVE a;<br /> END IF;
  2. /*模拟耗时5秒*/<br /> SELECT **sleep**(5);
  3. /*2.余额减去price*/<br /> SET v_available = v_available - v_price;
  4. /*3.更新余额*/<br /> START TRANSACTION;<br /> UPDATE t_funds SET available = v_available WHERE user_id = v_user_id AND version = v_version;<br /> /*获取上面update影响行数*/<br /> select **ROW_COUNT**() INTO v_update_count;
  5. IF v_update_count=1 THEN<br /> /*插入订单明细*/<br /> INSERT INTO **t_order** (price) **VALUES** (v_price);<br /> SET v_msg='下单成功!';<br /> /*提交事务*/<br /> COMMIT;<br /> ELSE<br /> SET v_msg='下单失败,请重试!';<br /> /*回滚事务*/<br /> ROLLBACK;<br /> END IF;<br /> END $<br />/*结束符置为;*/<br />DELIMITER ;

ROW_COUNT()可以获取更新或插入后获取受影响行数。将受影响行数放在v_update_count中。
然后根据v_update_count是否等于 1 判断更新是否成功,如果成功则记录订单信息并提交事务,否则回滚事务。

验证结果:开启 2 个 cmd 窗口,连接 mysql,执行下面操作:

use javacode2018;
CALL proc4(1001,100,@v_msg);
select @v_msg;

窗口 1 结果:

mysql> CALL proc4(1001,100,@v_msg);
+—————+
| sleep(5) |
+—————+
| 0 |
+—————+
1 row in set (5.00 sec)

Query OK, 0 rows affected (5.00 sec)

mysql> select @v_msg;
+———————-+
| @v_msg |
+———————-+
| 下单成功! |
+———————-+
1 row in set (0.00 sec)

窗口 2 结果:

mysql> CALL proc4(1001,100,@v_msg);
+—————+
| sleep(5) |
+—————+
| 0 |
+—————+
1 row in set (5.00 sec)

Query OK, 0 rows affected (5.01 sec)

mysql> select @v_msg;
+————————————-+
| @v_msg |
+————————————-+
| 下单失败,请重试! |
+————————————-+
1 row in set (0.00 sec)

可以看到第一个窗口下单成功了,窗口 2 下单失败了。
再看一下 2 个表的数据:
mysql> SELECT FROM t_funds;
+————-+—————-+————-+
| user_id | available | version |
+————-+—————-+————-+
| 1001 | 900.00 | 0 |
+————-+—————-+————-+
1 row in *set
(0.00 sec)

mysql> SELECT FROM t_order;
+——+————+
| id | price |
+——+————+
| 1 | 100.00 |
+——+————+
1 row in *set
(0.00 sec)

也正常。

总结

  1. 异常分为 Mysql 内部异常和外部异常
  2. 内部异常由 mysql 内部触发,外部异常是 sql 的执行结果和期望结果不一致导致的错误
  3. sql 内部异常捕获方式DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET hasSqlError=TRUE;

  4. ROW_COUNT()可以获取 mysql 中 insert 或者 update 影响的行数

  5. 掌握使用乐观锁(添加版本号)来解决并发修改数据可能出错的问题
  6. begin end前面可以加标签,LEAVE 标签可以退出对应的 begin end,可以使用这个来实现 return 的效果

    二十一、什么是索引?

    来看一个问题

    路人在搞计算机之前,是负责小区建设规划的,上级领导安排路人负责一个万人小区建设规划,并提了一个要求:可以快速通过户主姓名找到户主的房子;让路人出个好的解决方案。

    方案 1

    刚开始路人没什么经验,实在想不到什么好办法。
    路人告诉领导:你可以去敲每户的门,然后开门之后再去询问房主姓名,是否和需要找的人姓名一致。
    领导一听郁闷了:我敲你的头,1 万户,我一个个找,找到什么时候了?你明天不用来上班了。
    这里面涉及到的时间有:走到每户的门口耗时、敲门等待开门耗时、询问户主获取户主姓名耗时、将户主姓名和需要查找的姓名对比是否一致耗时。
    加入要找的人刚好在最后一户,领导岂不是要疯掉了,需要重复 1 万次上面的操作。
    上面是最原始,最耗时的做法,可能要找的人根本不在这个小区,白费力的找了 1 万次,岂不是要疯掉。

    方案 2

    路人灵机一动,想到了一个方案:

  7. 给所有的户主制定一个编号,从 1-10000,户主将户号贴在自家的门口

  8. 路人自己制作了一个户主和户号对应的表格,我们叫做:户主目录表,共 1 万条记录,如下: | 户主姓名 | 房屋编号 | | —- | —- | | 刘德华 | 00001 | | 张学友 | 00002 | | 路人 | 00888 | | 路人甲 java | 10000 |

此时领导要查找路人甲Java时,过程如下:

  1. 按照姓名在户主目录表查找路人甲Java,找到对应的编号:10000
  2. 然后从第一户房子开始找,查看其门口户号是否是 10000,直到找到为止

路人告诉领导,这个方案比方案 1 有以下好处:

  1. 如果要找的人不在这个小区,通过户主目录表就确定,不需要第二步了
  2. 步骤 2 中不需要再去敲每户的门以及询问户主的姓名了,只需对比一下门口的户号就可以了,比方案 1 省了不少时间。

领导笑着说,不错不错,有进步,不过我找路人甲Java还是需要挨家挨户看门牌号 1 万次啊!。。。。。你再去想想吧,看看是否还有更好的办法来加快查找速度。
路人下去了苦思冥想,想出了方案 3。

方案 3

方案 2 中第 2 步最坏的情况还是需要找 1 万次。
路人去上海走了一圈,看了那边小区搞的不错,很多小区都是搞成一栋一栋的,每栋楼里面有 100 户,路人也决定这么搞。
路人告诉领导:

  1. 将 1 万户划分为 100 栋楼,每栋楼有 25 层,每层有 4 户人家,总共 1 万户
  2. 给每栋楼一个编号,范围是[001,100],将栋号贴在每栋楼最显眼的位置
  3. 给每栋楼中的每层一个编号,编号范围是[01,25],将层号贴在每层楼最显眼的位置
  4. 户号变为:栋号-楼层-层中编号,如路人甲Java户号是:100-20-04,贴在每户门口

户主目录表还是有 1 万条记录,如下:

户主姓名 房屋编号
刘德华 001-08-04
张学友 022-18-01
路人 088-25-04
路人甲 java 100-25-04

此时领导要查找路人甲Java时,过程如下:

  1. 按照姓名在户主目录表查找路人甲Java,找到对应的编号是100-25-04,将编号分解,得到:栋号(100)、楼层(25)、楼号(04)
  2. 从第一栋开始找,看其栋号是否是 100,直到找到编号为 100 为止,这个过程需要找 100 次,然后到了第 100 栋楼下
  3. 从 100 栋的第一层开始向上走,走到每层看其编号是否为 25,直到走到第 25 层,这个过程需要匹配 25 次
  4. 在第 25 层依次看看户号是否为100-25-04,匹配了 4 次,找到了路人甲Java

此方案分析:

  1. 查找户主目录表1 万次,不过这个是在表格中,不用动身走路去找,只需要动动眼睛对比一下数字,速度还是比较快的
  2. 将方案 2 中的第 2 步优化为上面的2/3/4步骤,上面最坏需要匹配 129 次(栋 100+层 25+楼号 4 次),相对于方案 2 的 1 万次好多了

领导拍拍路人的肩膀:小伙子,去过上海的人确实不一样啊,这次方案不错,不过第一步还是需要很多次,能否有更好的方案呢?
路人下去了又想了好几天,突然想到了我们常用的字典,可以按照字典的方式对方案 3 中第一步做优化,然后提出了方案 4。

方案 4

对户主表进行改造,按照姓的首字母(a-z)制作 26 个表格,叫做:姓氏户主表,每个表格中保存对应姓氏首字母及所有户主和户号。如下:

| 姓首字母:A |

| | —- | —- | | 姓名 | 户号 | | 阿三 | 010-16-01 | | 阿郎 | 017-11-04 | | 啊啊 | 008-08-02 |

| 姓首字母:L |

| | —- | —- | | 姓名 | 户号 | | 刘德华 | 011-16-01 | | 路人 | 057-11-04 | | 路人甲 Java | 048-08-02 |

现在查找户号步骤如下:

  1. 通过姓名获取姓对应的首字母
  2. 在 26 个表格中找到对应姓的表格,如路人甲Java,对应L表
  3. 在 L 表中循环遍历,找到路人甲Java的户号
  4. 根据户号按照方案 3 中的(2/3/4)步骤找对应的户主

    理想情况:

    1 万户主的姓氏分配比较均衡,那么每个姓氏下面分配 385 户(10000/26) ,那么找到某个户主,最多需要:26 次+385 次 = 410 次,相对于 1 万次少了很多。

    最坏的情况:

    1 万个户主的姓氏都是一样的,导致这 1 万个户主信息都位于同一个姓氏户主表,此时查询又变为了 1 万多次。不过出现姓氏一样的情况比较低。
    如果担心姓氏不足以均衡划分户主信息,那么也可以通过户主姓名的笔画数来划分,或者其他方法,主要是将用户信息划分为不同的区,可以快速过滤一些不相关的户主。
    上面几个方案为了快速检索到户主,用到了一些数据结构,通过这些数据结构对户主的信息进行组织,从而可以快速过滤掉一些不相关的户主,减少查找次数,快速定位到户主的房子。

    索引是什么?

    通过上面的示例,我们可以概况一下索引的定义:索引是依靠某些数据结构和算法来组织数据,最终引导用户快速检索出所需要的数据。
    索引有 2 个特点:

  5. 通过数据结构和算法来对原始的数据进行一些有效的组织

  6. 通过这些有效的组织,可以引导使用者对原始数据进行快速检索

mysql 为了快速检索数据,也用到了一些好的数据结构和算法,来组织表中的数据,加快检索效率。

二十二、索引原理解密

背景

使用 mysql 最多的就是查询,我们迫切的希望 mysql 能查询的更快一些,我们经常用到的查询有:

  1. 按照 id 查询唯一一条记录
  2. 按照某些个字段查询对应的记录
  3. 查找某个范围的所有记录(between and)
  4. 对查询出来的结果排序

mysql 的索引的目的是使上面的各种查询能够更快。

预备知识

什么是索引?

上一篇中有详细的介绍,可以过去看一下:什么是索引?
索引的本质:通过不断地缩小想要获取数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺序的事件,也就是说,有了这种索引机制,我们可以总是用同一种查找方式来锁定数据。

磁盘中数据的存取

以机械硬盘来说,先了解几个概念。
扇区:磁盘存储的最小单位,扇区一般大小为 512Byte。
磁盘块:文件系统与磁盘交互的的最小单位(计算机系统读写磁盘的最小单位),一个磁盘块由连续几个()扇区组成,块一般大小一般为 4KB。
磁盘读取数据:磁盘读取数据靠的是机械运动,每次读取数据花费的时间可以分为寻道时间、旋转延迟、传输时间三个部分,寻道时间指的是磁臂移动到指定磁道所需要的时间,主流磁盘一般在 5ms 以下;旋转延迟就是我们经常听说的磁盘转速,比如一个磁盘 7200 转,表示每分钟能转 7200 次,也就是说 1 秒钟能转 120 次,旋转延迟就是 1/120/2 = 4.17ms;传输时间指的是从磁盘读出或将数据写入磁盘的时间,一般在零点几毫秒,相对于前两个时间可以忽略不计。那么访问一次磁盘的时间,即一次磁盘 IO 的时间约等于 5+4.17 = 9ms 左右,听起来还挺不错的,但要知道一台 500 -MIPS 的机器每秒可以执行 5 亿条指令,因为指令依靠的是电的性质,换句话说执行一次 IO 的时间可以执行 40 万条指令,数据库动辄十万百万乃至千万级数据,每次 9 毫秒的时间,显然是个灾难。

mysql 中的页

mysql 中和磁盘交互的最小单位称为页,页是 mysql 内部定义的一种数据结构,默认为 16kb,相当于 4 个磁盘块,也就是说 mysql 每次从磁盘中读取一次数据是 16KB,要么不读取,要读取就是 16KB,此值可以修改的。

数据检索过程

我们对数据存储方式不做任何优化,直接将数据库中表的记录存储在磁盘中,假如某个表只有一个字段,为 int 类型,int 占用 4 个 byte,每个磁盘块可以存储 1000 条记录,100 万的记录需要 1000 个磁盘块,如果我们需要从这 100 万记录中检索所需要的记录,需要读取 1000 个磁盘块的数据(需要 1000 次 io),每次 io 需要 9ms,那么 1000 次需要 9000ms=9s,100 条数据随便一个查询就是 9 秒,这种情况我们是无法接受的,显然是不行的。

我们迫切的需求是什么?

我们迫切需要这样的数据结构和算法:

  1. 需要一种数据存储结构:当从磁盘中检索数据的时候能,够减少磁盘的 io 次数,最好能够降低到一个稳定的常量值
  2. 需要一种检索算法:当从磁盘中读取磁盘块的数据之后,这些块中可能包含多条记录,这些记录被加载到内存中,那么需要一种算法能够快速从内存多条记录中快速检索出目标数据

我们来找找,看是否能够找到这样的算法和数据结构。
我们看一下常见的检索算法和数据结构。

循环遍历查找

从一组无序的数据中查找目标数据,常见的方法是遍历查询,n 条数据,时间复杂度为 O(n),最快需要 1 次,最坏的情况需要 n 次,查询效率不稳定。

二分法查找

二分法查找也称为折半查找,用于在一个有序数组中快速定义某一个需要查找的数据。
原理是:
先将一组无序的数据排序(升序或者降序)之后放在数组中,此处用升序来举例说明:用数组中间位置的数据 A 和需要查找的数据 F 对比,如果 A=F,则结束查找;如果 AF,则将查找范围缩小至数组中 A 数据左边的部分,继续按照上面的方法直到找到 F 为止。
示例:
从下列有序数字中查找数字 9,过程如下
[1,2,3,4,5,6,7,8,9]
第 1 次查找:[1,2,3,4,5,6,7,8,9]中间位置值为 5,9>5,将查找范围缩小至 5 右边的部分:[6、7、8、9]
第 2 次查找:[6、7、8、9]中间值为 8,9>8 ,将范围缩小至 8 右边部分:[9]
第 3 次查找:在[9]中查找 9,找到了。
可以看到查找速度是相当快的,每次查找都会使范围减半,如果我们采用顺序查找,上面数据最快需要 1 次,最多需要 9 次,而二分法查找最多只需要 3 次,耗时时间也比较稳定。
二分法查找时间复杂度是:O(logN)(N 为数据量),100 万数据查找最多只需要 20 次(=1048576‬)
二分法查找数据的优点:定位数据非常快,前提是:目标数组是有序的。

有序数组

如果我们将 mysql 中表的数据以有序数组的方式存储在磁盘中,那么我们定位数据步骤是:

  1. 取出目标表的所有数据,存放在一个有序数组中
  2. 如果目标表的数据量非常大,从磁盘中加载到内存中需要的内存也非常大

步骤取出所有数据耗费的 io 次数太多,步骤 2 耗费的内存空间太大,还有新增数据的时候,为了保证数组有序,插入数据会涉及到数组内部数据的移动,也是比较耗时的,显然用这种方式存储数据是不可取的。

链表

链表相当于在每个节点上增加一些指针,可以和前面或者后面的节点连接起来,就像一列火车一样,每节车厢相当于一个节点,车厢内部可以存储数据,每个车厢和下一节车厢相连。
链表分为单链表和双向链表。

单链表

每个节点中有持有指向下一个节点的指针,只能按照一个方向遍历链表,结构如下:
//单项链表
class Node1{
private Object data;//存储数据
private Node1 nextNode;//指向下一个节点
}

双向链表

每个节点中两个指针,分别指向当前节点的上一个节点和下一个节点,结构如下:
//双向链表
class Node2{
private Object data;//存储数据
private Node1 prevNode;//指向上一个节点
private Node1 nextNode;//指向下一个节点
}

链表的优点:

  1. 可以快速定位到上一个或者下一个节点
  2. 可以快速删除数据,只需改变指针的指向即可,这点比数组好

    链表的缺点:

  3. 无法向数组那样,通过下标随机访问数据

  4. 查找数据需从第一个节点开始遍历,不利于数据的查找,查找时间和无需数据类似,需要全遍历,最差时间是 O(N)

    二叉查找树

    二叉树是每个结点最多有两个子树的树结构,通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。二叉树常被用于实现二叉查找树和二叉堆。二叉树有如下特性:
    1、每个结点都包含一个元素以及 n 个子树,这里 0≤n≤2。2、左子树和右子树是有顺序的,次序不能任意颠倒,左子树的值要小于父结点,右子树的值要大于父结点。
    数组[20,10,5,15,30,25,35]使用二叉查找树存储如下:
    image.png
    每个节点上面有两个指针(left,rigth),可以通过这 2 个指针快速访问左右子节点,检索任何一个数据最多只需要访问 3 个节点,相当于访问了 3 次数据,时间为 O(logN),和二分法查找效率一样,查询数据还是比较快的。
    但是如果我们插入数据是有序的,如[5,10,15,20,30,25,35],那么结构就变成下面这样:
    image.png
    二叉树退化为了一个链表结构,查询数据最差就变为了 O(N)。
    二叉树的优缺点:

  5. 查询数据的效率不稳定,若树左右比较平衡的时,最差情况为 O(logN),如果插入数据是有序的,退化为了链表,查询时间变成了 O(N)

  6. 数据量大的情况下,会导致树的高度变高,如果每个节点对应磁盘的一个块来存储一条数据,需 io 次数大幅增加,显然用此结构来存储数据是不可取的

    平衡二叉树(AVL 树)

    平衡二叉树是一种特殊的二叉树,所以他也满足前面说到的二叉查找树的两个特性,同时还有一个特性:
    它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。
    平衡二叉树相对于二叉树来说,树的左右比较平衡,不会出现二叉树那样退化成链表的情况,不管怎么插入数据,最终通过一些调整,都能够保证树左右高度相差不大于 1。
    这样可以让查询速度比较稳定,查询中遍历节点控制在 O(logN)范围内
    如果数据都存储在内存中,采用 AVL 树来存储,还是可以的,查询效率非常高。不过我们的数据是存在磁盘中,用过采用这种结构,每个节点对应一个磁盘块,数据量大的时候,也会和二叉树一样,会导致树的高度变高,增加了 io 次数,显然用这种结构存储数据也是不可取的。

    B-树

    B杠树,千万不要读作 B 减树了,B-树在是平衡二叉树上进化来的,前面介绍的几种树,每个节点上面只有一个元素,而 B-树节点中可以放多个元素,主要是为了降低树的高度。
    一棵 m 阶的 B-Tree 有如下特性【特征描述的有点绕,看不懂的可以跳过,看后面的图】:

  7. 每个节点最多有 m 个孩子,m 称为 b 树的阶

  8. 除了根节点和叶子节点外,其它每个节点至少有 Ceil(m/2)个孩子
  9. 若根节点不是叶子节点,则至少有 2 个孩子
  10. 所有叶子节点都在同一层,且不包含其它关键字信息
  11. 每个非终端节点包含 n 个关键字(健值)信息
  12. 关键字的个数 n 满足:ceil(m/2)-1 <= n <= m-1
  13. ki(i=1,…n)为关键字,且关键字升序排序
  14. Pi(i=1,…n)为指向子树根节点的指针。P(i-1)指向的子树的所有节点关键字均小于 ki,但都大于 k(i-1)

B-Tree 结构的数据可以让系统高效的找到数据所在的磁盘块。为了描述 B-Tree,首先定义一条记录为一个二元组[key, data] ,key 为记录的键值,对应表中的主键值,data 为一行记录中除主键外的数据。对于不同的记录,key 值互不相同。
B-Tree 中的每个节点根据实际情况可以包含大量的关键字信息和分支,如下图所示为一个 3 阶的 B-Tree:
image.png
每个节点占用一个盘块的磁盘空间,一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址。两个键将数据划分成的三个范围域,对应三个指针指向的子树的数据的范围域。以根节点为例,关键字为 17 和 35,P1 指针指向的子树的数据范围为小于 17,P2 指针指向的子树的数据范围为 17~35,P3 指针指向的子树的数据范围为大于 35。
模拟查找关键字 29 的过程:

  1. 根据根节点找到磁盘块 1,读入内存。【磁盘 I/O 操作第 1 次】
  2. 比较关键字 29 在区间(17,35),找到磁盘块 1 的指针 P2
  3. 根据 P2 指针找到磁盘块 3,读入内存。【磁盘 I/O 操作第 2 次】
  4. 比较关键字 29 在区间(26,30),找到磁盘块 3 的指针 P2
  5. 根据 P2 指针找到磁盘块 8,读入内存。【磁盘 I/O 操作第 3 次】
  6. 在磁盘块 8 中的关键字列表中找到关键字 29

分析上面过程,发现需要 3 次磁盘 I/O 操作,和 3 次内存查找操作,由于内存中的关键字是一个有序表结构,可以利用二分法快速定位到目标数据,而 3 次磁盘 I/O 操作是影响整个 B-Tree 查找效率的决定因素。
B-树相对于 avl 树,通过在节点中增加节点内部数据的个数来减少磁盘的 io 操作。
上面我们说过 mysql 是采用页方式来读写数据,每页是 16KB,我们用 B-树来存储 mysql 的记录,每个节点对应 mysql 中的一页(16KB),假如每行记录加上树节点中的 1 个指针占 160Byte,那么每个节点可以存储 1000(16KB/160byte)条数据,树的高度为 3 的节点大概可以存储(第一层 1000+第二层 +第三层 )10 亿条记录,是不是非常惊讶,一个高度为 3 个 B-树大概可以存储 10 亿条记录,我们从 10 亿记录中查找数据只需要 3 次 io 操作可以定位到目标数据所在的页,而页内部的数据又是有序的,然后将其加载到内存中用二分法查找,是非常快的。
可以看出使用 B-树定位某个值还是很快的(10 亿数据中 3 次 io 操作+内存中二分法),但是也是有缺点的:B-不利于范围查找,比如上图中我们需要查找[15,36]区间的数据,需要访问 7 个磁盘块(1/2/7/3/8/4/9),io 次数又上去了,范围查找也是我们经常用到的,所以 b-树也不太适合在磁盘中存储需要检索的数据。

b+树

image.png

b+树的特征

  1. 每个结点至多有 m 个子女
  2. 除根结点外,每个结点至少有[m/2]个子女,根结点至少有两个子女
  3. 有 k 个子女的结点必有 k 个关键字
  4. 父节点中持有访问子节点的指针
  5. 父节点的关键字在子节点中都存在(如上面的 1/20/35 在每层都存在),要么是最小值,要么是最大值,如果节点中关键字是升序的方式,父节点的关键字是子节点的最小值
  6. 最底层的节点是叶子节点
  7. 除叶子节点之外,其他节点不保存数据,只保存关键字和指针
  8. 叶子节点包含了所有数据的关键字以及 data,叶子节点之间用链表连接起来,可以非常方便的支持范围查找

    b+树与 b-树的几点不同

  9. b+树中一个节点如果有 k 个关键字,最多可以包含 k 个子节点(k 个关键字对应 k 个指针);而 b-树对应 k+1 个子节点(多了一个指向子节点的指针)

  10. b+树除叶子节点之外其他节点值存储关键字和指向子节点的指针,而 b-树还存储了数据,这样同样大小情况下,b+树可以存储更多的关键字
  11. b+树叶子节点中存储了所有关键字及 data,并且多个节点用链表连接,从上图中看子节点中数据从左向右是有序的,这样快速可以支撑范围查找(先定位范围的最大值和最小值,然后子节点中依靠链表遍历范围数据)

    B-Tree 和 B+Tree 该如何选择?

  12. B-Tree 因为非叶子结点也保存具体数据,所以在查找某个关键字的时候找到即可返回。而 B+Tree 所有的数据都在叶子结点,每次查找都得到叶子结点。所以在同样高度的 B-Tree 和 B+Tree 中,B-Tree 查找某个关键字的效率更高。

  13. 由于 B+Tree 所有的数据都在叶子结点,并且结点之间有指针连接,在找大于某个关键字或者小于某个关键字的数据的时候,B+Tree 只需要找到该关键字然后沿着链表遍历就可以了,而 B-Tree 还需要遍历该关键字结点的根结点去搜索。
  14. 由于 B-Tree 的每个结点(这里的结点可以理解为一个数据页)都存储主键+实际数据,而 B+Tree 非叶子结点只存储关键字信息,而每个页的大小有限是有限的,所以同一页能存储的 B-Tree 的数据会比 B+Tree 存储的更少。这样同样总量的数据,B-Tree 的深度会更大,增大查询时的磁盘 I/O 次数,进而影响查询效率。

    Mysql 的存储引擎和索引

    mysql 内部索引是由不同的引擎实现的,主要说一下 InnoDB 和 MyISAM 这两种引擎中的索引,这两种引擎中的索引都是使用 b+树的结构来存储的。

    InnoDB 中的索引

    Innodb 中有 2 种索引:主键索引(聚集索引)辅助索引(非聚集索引)
    主键索引:每个表只有一个主键索引,b+树结构,叶子节点同时保存了主键的值也数据记录,其他节点只存储主键的值。
    辅助索引:每个表可以有多个,b+树结构,叶子节点保存了索引字段的值以及主键的值,其他节点只存储索引指端的值。

    MyISAM 引擎中的索引

    B+树结构,MyISM 使用的是非聚簇索引,非聚簇索引的两棵 B+树看上去没什么不同,节点的结构完全一致只是存储的内容不同而已,主键索引 B+树的节点存储了主键,辅助键索引 B+树存储了辅助键。表数据存储在独立的地方,这两颗 B+树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别。由于索引树是独立的,通过辅助键检索无需访问主键的索引树。
    如下图:为了更形象说明这两种索引的区别,我们假想一个表存储了 4 行数据。其中 Id 作为主索引,Name 作为辅助索引,图中清晰的显示了聚簇索引和非聚簇索引的差异。
    image.png
    我们看一下上图中数据检索过程。

    InnoDB 数据检索过程

    如果需要查询 id=14 的数据,只需要在左边的主键索引中检索就可以了。
    如果需要搜索 name=’Ellison’的数据,需要 2 步:

  15. 先在辅助索引中检索到 name=’Ellison’的数据,获取 id 为 14

  16. 再到主键索引中检索 id 为 14 的记录

辅助索引这个查询过程在 mysql 中叫做回表

MyISAM 数据检索过程

  1. 在索引中找到对应的关键字,获取关键字对应的记录的地址
  2. 通过记录的地址查找到对应的数据记录

我们用的最多的是 innodb 存储引擎,所以此处主要说一下 innodb 索引的情况,innodb 中最好是采用主键查询,这样只需要一次索引,如果使用辅助索引检索,涉及到回表操作,比主键查询要耗时一些。
innodb 中辅助索引为什么不像 myisam 那样存储记录的地址?
表中的数据发生变更的时候,会影响其他记录地址的变化,如果辅助索引中记录数据的地址,此时会受影响,而主键的值一般是很少更新的,当页中的记录发生地址变更的时候,对辅助索引是没有影响的。
我们来看一下 mysql 中页的结构,页是真正存储记录的地方,对应 B+树中的一个节点,也是 mysql 中读写数据的最小单位,页的结构设计也是相当有水平的,能够加快数据的查询。

页结构

mysql 中页是 innodb 中存储数据的基本单位,也是 mysql 中管理数据的最小单位,和磁盘交互的时候都是以页来进行的,默认是 16kb,mysql 中采用 b+树存储数据,页相当于 b+树中的一个节点。
页的结构如下图:
image.png
每个 Page 都有通用的头和尾,但是中部的内容根据 Page 的类型不同而发生变化。Page 的头部里有我们关心的一些数据,下图把 Page 的头部详细信息显示出来:
image.png
我们重点关注和数据组织结构相关的字段:Page 的头部保存了两个指针,分别指向前一个 Page 和后一个 Page,根据这两个指针我们很容易想象出 Page 链接起来就是一个双向链表的结构,如下图:
image.png
再看看 Page 的主体内容,我们主要关注行数据和索引的存储,他们都位于 Page 的 User Records 部分,User Records 占据 Page 的大部分空间,User Records 由一条一条的 Record 组成。在一个 Page 内部,单链表的头尾由固定内容的两条记录来表示,字符串形式的”Infimum”代表开头,”Supremum”代表结尾,这两个用来代表开头结尾的 Record 存储在 System Records 的,Infinum、Supremum 和 User Records 组成了一个单向链表结构。最初数据是按照插入的先后顺序排列的,但是随着新数据的插入和旧数据的删除,数据物理顺序会变得混乱,但他们依然通过链表的方式保持着逻辑上的先后顺序,如下图:
image.png
把 User Record 的组织形式和若干 Page 组合起来,就看到了稍微完整的形式。
基础知识 - 图15
基础知识 - 图16
innodb 为了快速查找记录,在页中定义了一个称之为 page directory 的目录槽(slots),每个槽位占用两个字节(用于保存指向记录的地址),page directory 中的多个 slot 组成了一个有序数组(可用于二分法快速定位记录,向下看),行记录被 Page Directory 逻辑的分成了多个块,块与块之间是有序的,能够加速记录的查找,如下图:
基础知识 - 图17
看上图,每个行记录的都有一个 n_owned 的区域(图中粉色区域),n_owned 标识所属的 slot 这个这个块有多少条数据,伪记录 Infimum 的 n_owned 值总是 1,记录 Supremum 的 n_owned 的取值范围为[1,8],其他用户记录 n_owned 的取值范围[4,8],并且只有每个块中最大的那条记录的 n_owned 才会有值,其他的用户记录的 n_owned 为 0。

数据检索过程

在 page 中查询数据的时候,先通过 b+树中查询方法定位到数据所在的页,然后将页内整体加载到内存中,通过二分法在 page directory 中检索数据,缩小范围,比如需要检索 7,通过二分法查找到 7 位于 slot2 和 slot3 所指向的记录中间,然后从 slot3 指向的记录 5 开始向后向后一个个找,可以找到记录 7,如果里面没有 7,走到 slot2 向的记录 8 结束。
n_owned 范围控制在[4,8]内,能保证每个 slot 管辖的范围内数据量控制在[4,8]个,能够加速目标数据的查找,当有数据插入的时候,page directory 为了控制每个 slot 对应块中记录的个数([4,8]),此时 page directory 中会对 slot 的数量进行调整。

对 page 的结构总结一下

  1. b+树中叶子页之间用双向链表连接的,能够实现范围查找
  2. 页内部的记录之间是采用单向链表连接的,方便访问下一条记录
  3. 为了加快页内部记录的查询,对页内记录上加了个有序的稀疏索引,叫页目录(page directory)

整体上来说 mysql 中的索引用到了 b+树,链表,二分法查找,做到了快速定位目标数据,快速范围查找。
参考资料:
Jeremy Cole的一些文章
https://blog.jcole.us/2013/01/10/btree-index-structures-in-innodb/
https://blog.jcole.us/innodb/

二十三、索引管理

索引分类

分为聚集索引非聚集索引

聚集索引

每个表有且一定会有一个聚集索引,整个表的数据存储在聚集索引中,mysql 索引是采用 B+树结构保存在文件中,叶子节点存储主键的值以及对应记录的数据,非叶子节点不存储记录的数据,只存储主键的值。当表中未指定主键时,mysql 内部会自动给每条记录添加一个隐藏的 rowid 字段(默认 4 个字节)作为主键,用 rowid 构建聚集索引。
聚集索引在 mysql 中又叫主键索引

非聚集索引(辅助索引)

也是 b+树结构,不过有一点和聚集索引不同,非聚集索引叶子节点存储字段(索引字段)的值以及对应记录主键的值,其他节点只存储字段的值(索引字段)。
每个表可以有多个非聚集索引。

mysql 中非聚集索引分为

单列索引

即一个索引只包含一个列。

多列索引(又称复合索引)

即一个索引包含多个列。

唯一索引

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

数据检索的过程

看一张图:
image.png
上面的表中有 2 个索引:id 作为主键索引,name 作为辅助索引。
innodb 我们用的最多,我们只看图中左边的 innodb 中数据检索过程:
如果需要查询 id=14 的数据,只需要在左边的主键索引中检索就可以了。
如果需要搜索 name=’Ellison’的数据,需要 2 步:

  1. 先在辅助索引中检索到 name=’Ellison’的数据,获取 id 为 14
  2. 再到主键索引中检索 id 为 14 的记录

辅助索引相对于主键索引多了第二步。

索引管理

创建索引

方式 1:

create [unique] index 索引名称 on 表名(列名[(length)]);

方式 2:

alter 表名 add [unique] index 索引名称 on (列名[(length)]);

如果字段是 char、varchar 类型,length 可以小于字段实际长度,如果是 blog、text 等长文本类型,必须指定 length。
[unique]:中括号代表可以省略,如果加上了 unique,表示创建唯一索引。
如果 table 后面只写一个字段,就是单列索引,如果写多个字段,就是复合索引,多个字段之间用逗号隔开。

删除索引

drop index 索引名称 on 表名;

查看索引

查看某个表中所有的索引信息如下:
show index from 表名;

索引修改

可以先删除索引,再重建索引。

示例

准备 200 万数据

/建库javacode2018/
DROP DATABASE IF EXISTS javacode2018;
CREATE DATABASE javacode2018;
USE javacode2018;

/建表test1/
DROP TABLE IF EXISTS test1;
CREATE TABLE test1 (
id INT NOT NULL COMMENT ‘编号’,
name VARCHAR(20) NOT NULL COMMENT ‘姓名’,
sex TINYINT NOT NULL COMMENT ‘性别,1:男,2:女’,
email VARCHAR(50)
);

/准备数据/
DROP PROCEDURE IF EXISTS proc1;
DELIMITER $
CREATE PROCEDURE proc1()
BEGIN
DECLARE i INT DEFAULT 1;
START TRANSACTION;
WHILE i <= 2000000 DO
INSERT INTO test1 (id, name, sex, email) VALUES (i,concat(‘javacode’,i),if(mod(i,2),1,2),concat(‘javacode’,i,’@163.com’));
SET i = i + 1;
if i%10000=0 THEN
COMMIT;
START TRANSACTION;
END IF;
END WHILE;
COMMIT;
END $

DELIMITER ;
CALL proc1();
SELECT count(*) FROM test1;

上图中使用存储过程循环插入了 200 万记录,表中有 4 个字段,除了 sex 列,其他列的值都是没有重复的,表中还未建索引。
插入的 200 万数据中,id,name,email 的值都是没有重复的。

无索引我们体验一下查询速度

mysql> select from test1 a where a.id = 1;
+——+—————-+——-+—————————-+
| id | name | sex | email |
+——+—————-+——-+—————————-+
| 1 | javacode1 | 1 | javacode1@163.com |
+——+—————-+——-+—————————-+
1 row in *set
(0.77 sec)

上面我们按 id 查询了一条记录耗时 770 毫秒,我们在 id 上面创建个索引感受一下速度。

创建索引

我们在 id 上面创建一个索引,感受一下:
mysql> create index idx1 on test1 (id);
Query OK, 0 rows affected (2.82 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> select from test1 a where a.id = 1;
+——+—————-+——-+—————————-+
| id | name | sex | email |
+——+—————-+——-+—————————-+
| 1 | javacode1 | 1 | javacode1@163.com |
+——+—————-+——-+—————————-+
1 row in *set
(0.00 sec)

上面的查询是不是非常快,耗时 1 毫秒都不到。
我们在 name 上也创建个索引,感受一下查询的神速,如下:
mysql> create unique index idx2 on test1(name);
Query OK, 0 rows affected (9.67 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> select from test1 where name = ‘javacode1’;
+——+—————-+——-+—————————-+
| id | name | sex | email |
+——+—————-+——-+—————————-+
| 1 | javacode1 | 1 | javacode1@163.com |
+——+—————-+——-+—————————-+
1 row in *set
(0.00 sec)

查询快如闪电,有没有,索引是如此的神奇。

创建索引并指定长度

通过 email 检索一下数据
mysql> select from test1 a where a.email = ‘javacode1000085@163.com’;
+————-+————————-+——-+————————————-+
| id | name | sex | email |
+————-+————————-+——-+————————————-+
| 1000085 | javacode1000085 | 1 | javacode1000085@163.com |
+————-+————————-+——-+————————————-+
1 row in *set
(1.28 sec)

耗时 1 秒多,回头去看一下插入数据的 sql,我们可以看到所有的 email 记录,每条记录的前面 15 个字符是不一样的,结尾是一样的(都是@163.com),通过前面 15 个字符就可以定位一个 email 了,那么我们可以对 email 创建索引的时候指定一个长度为 15,这样相对于整个 email 字段更短一些,查询效果是一样的,这样一个页中可以存储更多的索引记录,命令如下:
mysql> create index idx3 on test1 (email(15));
Query OK, 0 rows affected (7.67 sec)
Records: 0 Duplicates: 0 Warnings: 0

然后看一下查询效果:
mysql> select from test1 a where a.email = ‘javacode1000085@163.com’;
+————-+————————-+——-+————————————-+
| id | name | sex | email |
+————-+————————-+——-+————————————-+
| 1000085 | javacode1000085 | 1 | javacode1000085@163.com |
+————-+————————-+——-+————————————-+
1 row in *set
(0.00 sec)

耗时不到 1 毫秒,神速。

查看表中的索引

我们看一下 test1 表中的所有索引,如下:
mysql> show index from test1;
+———-+——————+—————+———————+——————-+—————-+——————-+—————+————+———+——————+————-+———————-+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+———-+——————+—————+———————+——————-+—————-+——————-+—————+————+———+——————+————-+———————-+
| test1 | 0 | idx2 | 1 | name | A | 1992727 | NULL | NULL | | BTREE | | |
| test1 | 1 | idx1 | 1 | id | A | 1992727 | NULL | NULL | | BTREE | | |
| test1 | 1 | idx3 | 1 | email | A | 1992727 | 15 | NULL | YES | BTREE | | |
+———-+——————+—————+———————+——————-+—————-+——————-+—————+————+———+——————+————-+———————-+
3 rows in set (0.00 sec)

可以看到 test1 表中 3 个索引的详细信息(索引名称、类型,字段)。

删除索引

我们删除 idx1,然后再列出 test1 表所有索引,如下:
mysql> drop index idx1 on test1;
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> show index from test1;
+———-+——————+—————+———————+——————-+—————-+——————-+—————+————+———+——————+————-+———————-+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+———-+——————+—————+———————+——————-+—————-+——————-+—————+————+———+——————+————-+———————-+
| test1 | 0 | idx2 | 1 | name | A | 1992727 | NULL | NULL | | BTREE | | |
| test1 | 1 | idx3 | 1 | email | A | 1992727 | 15 | NULL | YES | BTREE | | |
+———-+——————+—————+———————+——————-+—————-+——————-+—————+————+———+——————+————-+———————-+
2 rows in set (0.00 sec)

本篇主要是 mysql 中索引管理相关一些操作,属于基础知识,希望大家掌握。
下篇文章介绍:

  1. 一个表应该创建哪些索引?
  2. 有索引时 sql 应该怎么写?
  3. 我的 sql 为什么不走索引?需要知道内部原理
  4. where 条件涉及多个字段多个索引时怎么走?
  5. 多表连接查询、子查询,怎么去利用索引,内部过程是什么样的?
  6. like 查询中前面有%的时候为何不走索引?
  7. 字段中使用函数的时候为什么不走索引?
  8. 字符串查询使用数字作为条件的时候为什么不走索引?
  9. 索引区分度、索引覆盖、最左匹配、索引排序又是什么?原理是什么?

关于上面各种索引选择的问题,我们会深入其原理,让大家知道为什么是这样?而不是只去记录一些优化规则,而不知道其原因,知道其原理用的时候跟得心应手一些。

二十四、如何正确的使用索引?

前言

学习索引,主要是写出更快的 sql,当我们写 sql 的时候,需要明确的知道 sql 为什么会走索引?为什么有些 sql 不走索引?sql 会走那些索引,为什么会这么走?我们需要了解其原理,了解内部具体过程,这样使用起来才能更顺手,才可以写出更高效的 sql。本篇我们就是搞懂这些问题。
读本篇文章之前,需要先了解一些知识:

  1. 什么是索引?
  2. 索引原理解密
  3. MySQL 索引管理

上面 3 篇文章没有读过的最好去读一下,不然后面的内容会难以理解。

先来回顾一些知识

本篇文章我们以 innodb 存储引擎为例来做说明。
mysql 采用 b+树的方式存储索引信息。

b+树结构如下:

基础知识 - 图19
说一下 b+树的几个特点:

  1. 叶子节点(最下面的一层)存储关键字(索引字段的值)信息及对应的 data,叶子节点存储了所有记录的关键字信息
  2. 其他非叶子节点只存储关键字的信息及子节点的指针
  3. 每个叶子节点相当于 mysql 中的一页,同层级的叶子节点以双向链表的形式相连
  4. 每个节点(页)中存储了多条记录,记录之间用单链表的形式连接组成了一条有序的链表,顺序是按照索引字段排序的
  5. b+树中检索数据时:每次检索都是从根节点开始,一直需要搜索到叶子节点

InnoDB 的数据是按数据页为单位来读写的。也就是说,当需要读取一条记录的时候,并不是将这个记录本身从磁盘读取出来,而是以页为单位,将整个也加载到内存中,一个页中可能有很多记录,然后在内存中对页进行检索。在 innodb 中,每个页的大小默认是 16kb。

Mysql 中索引分为

聚集索引(主键索引)

每个表一定会有一个聚集索引,整个表的数据存储以 b+树的方式存在文件中,b+树叶子节点中的 key 为主键值,data 为完整记录的信息;非叶子节点存储主键的值。
通过聚集索引检索数据只需要按照 b+树的搜索过程,即可以检索到对应的记录。

非聚集索引

每个表可以有多个非聚集索引,b+树结构,叶子节点的 key 为索引字段字段的值,data 为主键的值;非叶子节点只存储索引字段的值。
通过非聚集索引检索记录的时候,需要 2 次操作,先在非聚集索引中检索出主键,然后再到聚集索引中检索出主键对应的记录,该过程比聚集索引多了一次操作。
索引怎么走,为什么有些查询不走索引?为什么使用函数了数据就不走索引了?
这些问题可以先放一下,我们先看一下 b+树检索数据的过程,这个属于原理的部分,理解了 b+树各种数据检索过程,上面的问题就都可以理解了。

通常说的这个查询走索引了是什么意思?

当我们对某个字段的值进行某种检索的时候,如果这个检索过程中,我们能够快速定位到目标数据所在的页,有效的降低页的 io 操作,而不需要去扫描所有的数据页的时候,我们认为这种情况能够有效的利用索引,也称这个检索可以走索引,如果这个过程中不能够确定数据在那些页中,我们认为这种情况下索引对这个查询是无效的,此查询不走索引。

b+树中数据检索过程

唯一记录检索

image.png
如上图,所有的数据都是唯一的,查询 105 的记录,过程如下:

  1. 将 P1 页加载到内存
  2. 在内存中采用二分法查找,可以确定 105 位于[100,150)中间,所以我们需要去加载 100 关联 P4 页
  3. 将 P4 加载到内存中,采用二分法找到 105 的记录后退出

    查询某个值的所有记录

    image.png
    如上图,查询 105 的所有记录,过程如下:

  4. 将 P1 页加载到内存

  5. 在内存中采用二分法查找,可以确定 105 位于[100,150)中间,100 关联 P4 页
  6. 将 P4 加载到内存中,采用二分法找到最有一个小于 105 的记录,即 100,然后通过链表从 100 开始向后访问,找到所有的 105 记录,直到遇到第一个大于 100 的值为止

    范围查找

    image.png
    数据如上图,查询[55,150]所有记录,由于页和页之间是双向链表升序结构,页内部的数据是单项升序链表结构,所以只用找到范围的起始值所在的位置,然后通过依靠链表访问两个位置之间所有的数据即可,过程如下:

  7. 将 P1 页加载到内存

  8. 内存中采用二分法找到 55 位于 50 关联的 P3 页中,150 位于 P5 页中
  9. 将 P3 加载到内存中,采用二分法找到第一个 55 的记录,然后通过链表结构继续向后访问 P3 中的 60、67,当 P3 访问完毕之后,通过 P3 的 nextpage 指针访问下一页 P4 中所有记录,继续遍历 P4 中的所有记录,直到访问到 P5 中的 150 为止。

    模糊匹配

    image.png
    数据如上图。

    查询以f开头的所有记录

    过程如下:

  10. 将 P1 数据加载到内存中

  11. 在 P1 页的记录中采用二分法找到最后一个小于等于 f 的值,这个值是 f,以及第一个大于 f 的,这个值是 z,f 指向叶节点 P3,z 指向叶节点 P6,此时可以断定以 f 开头的记录可能存在于[P3,P6)这个范围的页内,即 P3、P4、P5 这三个页中
  12. 加载 P3 这个页,在内部以二分法找到第一条 f 开头的记录,然后以链表方式继续向后访问 P4、P5 中的记录,即可以找到所有已 f 开头的数据

    查询包含f的记录

    包含的查询在 sql 中的写法是%f%,通过索引我们还可以快速定位所在的页么?
    可以看一下上面的数据,f 在每个页中都存在,我们通过 P1 页中的记录是无法判断包含 f 的记录在那些页的,只能通过 io 的方式加载所有叶子节点,并且遍历所有记录进行过滤,才可以找到包含 f 的记录。
    所以如果使用了%值%这种方式,索引对查询是无效的。

    最左匹配原则

    当 b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+树是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较 name 来确定下一步的所搜方向,如果 name 相同再依次比较 age 和 sex,最后得到检索的数据;但当(20,F)这样的没有 name 的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候 name 就是第一个比较因子,必须要先根据 name 来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用 name 来指定搜索方向,但下一个字段 age 的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是 F 的数据了, 这个是非常重要的性质,即索引的最左匹配特性。
    来一些示例我们体验一下。
    下图中是 3 个字段(a,b,c)的联合索引,索引中数据的顺序是以a asc,b asc,c asc这种排序方式存储在节点中的,索引先以 a 字段升序,如果 a 相同的时候,以 b 字段升序,b 相同的时候,以 c 字段升序,节点中每个数据认真看一下。
    image.png

    查询 a=1 的记录

    由于页中的记录是以a asc,b asc,c asc这种排序方式存储的,所以 a 字段是有序的,可以通过二分法快速检索到,过程如下:

  13. 将 P1 加载到内存中

  14. 在内存中对 P1 中的记录采用二分法找,可以确定 a=1 的记录位于{1,1,1}和{1,5,1}关联的范围内,这两个值子节点分别是 P2、P4
  15. 加载叶子节点 P2,在 P2 中采用二分法快速找到第一条 a=1 的记录,然后通过链表向下一条及下一页开始检索,直到在 P4 中找到第一个不满足 a=1 的记录为止

    查询 a=1 and b=5 的记录

    方法和上面的一样,可以确定 a=1 and b=5 的记录位于{1,1,1}和{1,5,1}关联的范围内,查找过程和 a=1 查找步骤类似。

    查询 b=1 的记录

    这种情况通过 P1 页中的记录,是无法判断 b=1 的记录在那些页中的,只能加锁索引树所有叶子节点,对所有记录进行遍历,然后进行过滤,此时索引是无效的。

    按照 c 的值查询

    这种情况和查询 b=1 也一样,也只能扫描所有叶子节点,此时索引也无效了。

    按照 b 和 c 一起查

    这种也是无法利用索引的,也只能对所有数据进行扫描,一条条判断了,此时索引无效。

    按照[a,c]两个字段查询

    这种只能利用到索引中的 a 字段了,通过 a 确定索引范围,然后加载 a 关联的所有记录,再对 c 的值进行过滤。

    查询 a=1 and b>=0 and c=1 的记录

    这种情况只能先确定 a=1 and b>=0 所在页的范围,然后对这个范围的所有页进行遍历,c 字段在这个查询的过程中,是无法确定 c 的数据在哪些页的,此时我们称 c 是不走索引的,只有 a、b 能够有效的确定索引页的范围。
    类似这种的还有>、<、between and,多字段索引的情况下,mysql 会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配。
    上面说的各种情况,大家都多看一下图中数据,认真分析一下查询的过程,基本上都可以理解了。
    上面这种查询叫做最左匹配原则。

    索引区分度

    我们看 2 个有序数组
    [1,2,3,4,5,6,7,8,8,9,10]
    [1,1,1,1,1,8,8,8,8,8]
    上面 2 个数组是有序的,都是 10 条记录,如果我需要检索值为 8 的所有记录,那个更快一些?
    咱们使用二分法查找包含 8 的所有记录过程如下:先使用二分法找到最后一个小于 8 的记录,然后沿着这条记录向后获取下一个记录,和 8 对比,知道遇到第一个大于 8 的数字结束,或者到达数组末尾结束。
    采用上面这种方法找到 8 的记录,第一个数组中更快的一些。因为第二个数组中含有 8 的比例更多的,需要访问以及匹配的次数更多一些。
    这里就涉及到数据的区分度问题:
    索引区分度 = count(distint 记录) / count(记录)
    当索引区分度高的时候,检索数据更快一些,索引区分度太低,说明重复的数据比较多,检索的时候需要访问更多的记录才能够找到所有目标数据。
    当索引区分度非常小的时候,基本上接近于全索引数据的扫描了,此时查询速度是比较慢的。
    第一个数组索引区分度为 1,第二个区分度为 0.2,所以第一个检索更快的一些。
    所以我们创建索引的时候,尽量选择区分度高的列作为索引。

    正确使用索引

    准备 400 万测试数据

    /建库javacode2018/
    DROP DATABASE IF EXISTS javacode2018;
    CREATE DATABASE javacode2018;
    USE javacode2018;
    /建表test1/
    DROP TABLE IF EXISTS test1;
    CREATE TABLE test1 (
    id INT NOT NULL COMMENT ‘编号’,
    name VARCHAR(20) NOT NULL COMMENT ‘姓名’,
    sex TINYINT NOT NULL COMMENT ‘性别,1:男,2:女’,
    email VARCHAR(50)
    );

/准备数据/
DROP PROCEDURE IF EXISTS proc1;
DELIMITER $
CREATE PROCEDURE proc1()
BEGIN
DECLARE i INT DEFAULT 1;
START TRANSACTION;
WHILE i <= 4000000 DO
INSERT INTO test1 (id, name, sex, email) VALUES (i,concat(‘javacode’,i),if(mod(i,2),1,2),concat(‘javacode’,i,’@163.com’));
SET i = i + 1;
if i%10000=0 THEN
COMMIT;
START TRANSACTION;
END IF;
END WHILE;
COMMIT;
END $

DELIMITER ;
CALL proc1();

上面插入的 400 万数据,除了 sex 列,其他列的值都是没有重复的。

无索引检索效果

400 万数据,我们随便查询几个记录看一下效果。
按照 id 查询记录
mysql> select from test1 where id = 1;
+——+—————-+——-+—————————-+
| id | name | sex | email |
+——+—————-+——-+—————————-+
| 1 | javacode1 | 1 | javacode1@163.com |
+——+—————-+——-+—————————-+
1 row in *set
(1.91 sec)

id=1 的数据,表中只有一行,耗时近 2 秒,由于 id 列无索引,只能对 400 万数据进行全表扫描。

主键检索

test1 表中没有明确的指定主键,我们将 id 设置为主键:
mysql> alter table test1 modify id int not null primary key;
Query OK, 0 rows affected (10.93 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> show index from test1;
+———-+——————+—————+———————+——————-+—————-+——————-+—————+————+———+——————+————-+———————-+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+———-+——————+—————+———————+——————-+—————-+——————-+—————+————+———+——————+————-+———————-+
| test1 | 0 | PRIMARY | 1 | id | A | 3980477 | NULL | NULL | | BTREE | | |
+———-+——————+—————+———————+——————-+—————-+——————-+—————+————+———+——————+————-+———————-+
1 row in set (0.00 sec)

id 被置为主键之后,会在 id 上建立聚集索引,随便检索一条我们看一下效果:
mysql> select from test1 where id = 1000000;
+————-+————————-+——-+————————————-+
| id | name | sex | email |
+————-+————————-+——-+————————————-+
| 1000000 | javacode1000000 | 2 | javacode1000000@163.com |
+————-+————————-+——-+————————————-+
1 row in *set
(0.00 sec)

这个速度很快,这个走的是上面介绍的唯一记录检索。

between and 范围检索

mysql> select count() from test1 where id between 100 and 110;
+—————+
| count(
) |
+—————+
| 11 |
+—————+
1 row in set (0.00 sec)

速度也很快,id 上有主键索引,这个采用的上面介绍的范围查找可以快速定位目标数据。
但是如果范围太大,跨度的 page 也太多,速度也会比较慢,如下:
mysql> select count() from test1 where id between 1 and 2000000;
+—————+
| count(
) |
+—————+
| 2000000 |
+—————+
1 row in set (1.17 sec)

上面 id 的值跨度太大,1 所在的页和 200 万所在页中间有很多页需要读取,所以比较慢。
所以使用 between and 的时候,区间跨度不要太大。

in 的检索

in 方式检索数据,我们还是经常用的。
平时我们做项目的时候,建议少用表连接,比如电商中需要查询订单的信息和订单中商品的名称,可以先查询查询订单表,然后订单表中取出商品的 id 列表,采用 in 的方式到商品表检索商品信息,由于商品 id 是商品表的主键,所以检索速度还是比较快的。
通过 id 在 400 万数据中检索 100 条数据,看看效果:
mysql> select from test1 a where a.id in (100000, 100001, 100002, 100003, 100004, 100005, 100006, 100007, 100008, 100009, 100010, 100011, 100012, 100013, 100014, 100015, 100016, 100017, 100018, 100019, 100020, 100021, 100022, 100023, 100024, 100025, 100026, 100027, 100028, 100029, 100030, 100031, 100032, 100033, 100034, 100035, 100036, 100037, 100038, 100039, 100040, 100041, 100042, 100043, 100044, 100045, 100046, 100047, 100048, 100049, 100050, 100051, 100052, 100053, 100054, 100055, 100056, 100057, 100058, 100059, 100060, 100061, 100062, 100063, 100064, 100065, 100066, 100067, 100068, 100069, 100070, 100071, 100072, 100073, 100074, 100075, 100076, 100077, 100078, 100079, 100080, 100081, 100082, 100083, 100084, 100085, 100086, 100087, 100088, 100089, 100090, 100091, 100092, 100093, 100094, 100095, 100096, 100097, 100098, 100099);
+————+————————+——-+————————————+
| id | name | sex | email |
+————+————————+——-+————————————+
| 100000 | javacode100000 | 2 | javacode100000@163.com |
| 100001 | javacode100001 | 1 | javacode100001@163.com |
| 100002 | javacode100002 | 2 | javacode100002@163.com |
…….
| 100099 | javacode100099 | 1 | javacode100099@163.com |
+————+————————+——-+————————————+
100 rows in *set
(0.00 sec)

耗时不到 1 毫秒,还是相当快的。
这个相当于多个分解为多个唯一记录检索,然后将记录合并。

多个索引时查询如何走?

我们在 name、sex 两个字段上分别建个索引
mysql> create index idx1 on test1(name);
Query OK, 0 rows affected (13.50 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> create index idx2 on test1(sex);
Query OK, 0 rows affected (6.77 sec)
Records: 0 Duplicates: 0 Warnings: 0

看一下查询:
mysql> select from test1 where name=’javacode3500000’ and sex=2;
+————-+————————-+——-+————————————-+
| id | name | sex | email |
+————-+————————-+——-+————————————-+
| 3500000 | javacode3500000 | 2 | javacode3500000@163.com |
+————-+————————-+——-+————————————-+
1 row in *set
(0.00 sec)

上面查询速度很快,name 和 sex 上各有一个索引,觉得上面走哪个索引?
有人说 name 位于 where 第一个,所以走的是 name 字段所在的索引,过程可以解释为这样:

  1. 走 name 所在的索引找到javacode3500000对应的所有记录
  2. 遍历记录过滤出 sex=2 的值

我们看一下name=’javacode3500000’检索速度,确实很快,如下:
mysql> select from test1 where name=’javacode3500000’;
+————-+————————-+——-+————————————-+
| id | name | sex | email |
+————-+————————-+——-+————————————-+
| 3500000 | javacode3500000 | 2 | javacode3500000@163.com |
+————-+————————-+——-+————————————-+
1 row in *set
(0.00 sec)

走 name 索引,然后再过滤,确实可以,速度也很快,果真和 where 后字段顺序有关么?我们把 name 和 sex 的顺序对调一下,如下:
mysql> select from test1 where sex=2 and name=’javacode3500000’;
+————-+————————-+——-+————————————-+
| id | name | sex | email |
+————-+————————-+——-+————————————-+
| 3500000 | javacode3500000 | 2 | javacode3500000@163.com |
+————-+————————-+——-+————————————-+
1 row in *set
(0.00 sec)

速度还是很快,这次是不是先走sex索引检索出数据,然后再过滤 name 呢?我们先来看一下sex=2查询速度:
mysql> select count(id) from test1 where sex=2;
+—————-+
| count(id) |
+—————-+
| 2000000 |
+—————-+
1 row in set (0.36 sec)

看上面,查询耗时 360 毫秒,200 万数据,如果走 sex 肯定是不行的。
我们使用 explain 来看一下:
mysql> explain select from test1 where sex=2 and name=’javacode3500000’;
+——+——————-+———-+——————+———+———————-+———+————-+———-+———+—————+——————-+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+——+——————-+———-+——————+———+———————-+———+————-+———-+———+—————+——————-+
| 1 | SIMPLE | test1 | NULL | ref | idx1,idx2 | idx1 | 62 | *const
| 1 | 50.00 | Using where |
+——+——————-+———-+——————+———+———————-+———+————-+———-+———+—————+——————-+
1 row in set, 1 warning (0.00 sec)

possible_keys:列出了这个查询可能会走两个索引(idx1、idx2)
实际上走的却是 idx1(key 列:实际走的索引)。
当多个条件中有索引的时候,并且关系是 and 的时候,会走索引区分度高的,显然 name 字段重复度很低,走 name 查询会更快一些。

模糊查询

看两个查询
mysql> select count() from test1 a where a.name like ‘javacode1000%’;
+—————+
| count(
) |
+—————+
| 1111 |
+—————+
1 row in set (0.00 sec)

mysql> select count() from test1 a where a.name like ‘%javacode1000%’;
+—————+
| count(
) |
+—————+
| 1111 |
+—————+
1 row in set (1.78 sec)

上面第一个查询可以利用到 name 字段上面的索引,下面的查询是无法确定需要查找的值所在的范围的,只能全表扫描,无法利用索引,所以速度比较慢,这个过程上面有说过。

回表

当需要查询的数据在索引树中不存在的时候,需要再次到聚集索引中去获取,这个过程叫做回表,如查询:
mysql> select from test1 where name=’javacode3500000’;
+————-+————————-+——-+————————————-+
| id | name | sex | email |
+————-+————————-+——-+————————————-+
| 3500000 | javacode3500000 | 2 | javacode3500000@163.com |
+————-+————————-+——-+————————————-+
1 row in *set
(0.00 sec)

上面查询是*,由于 name 列所在的索引中只有name、id两个列的值,不包含sex、email,所以上面过程如下:

  1. 走 name 索引检索javacode3500000对应的记录,取出 id 为3500000
  2. 在主键索引中检索出id=3500000的记录,获取所有字段的值

    索引覆盖

    查询中采用的索引树中包含了查询所需要的所有字段的值,不需要再去聚集索引检索数据,这种叫索引覆盖。
    我们来看一个查询:
    select id,name from test1 where name=’javacode3500000’;

name 对应 idx1 索引,id 为主键,所以 idx1 索引树叶子节点中包含了 name、id 的值,这个查询只用走 idx1 这一个索引就可以了,如果 select 后面使用,还需要一次回表获取 sex、email 的值。
所以写 sql 的时候,尽量避免使用
,*可能会多一次回表操作,需要看一下是否可以使用索引覆盖来实现,效率更高一些。

索引下推

简称 ICP,Index Condition Pushdown(ICP)是 MySQL 5.6 中新特性,是一种在存储引擎层使用索引过滤数据的一种优化方式,ICP 可以减少存储引擎访问基表的次数以及 MySQL 服务器访问存储引擎的次数。
举个例子来说一下:
我们需要查询 name 以javacode35开头的,性别为 1 的记录数,sql 如下:
mysql> select count(id) from test1 a where name like ‘javacode35%’ and sex = 1;
+—————-+
| count(id) |
+—————-+
| 55556 |
+—————-+
1 row in set (0.19 sec)

过程:

  1. 走 name 索引检索出以 javacode35 的第一条记录,得到记录的 id
  2. 利用 id 去主键索引中查询出这条记录 R1
  3. 判断 R1 中的 sex 是否为 1,然后重复上面的操作,直到找到所有记录为止。

上面的过程中需要走 name 索引以及需要回表操作。
如果采用 ICP 的方式,我们可以这么做,创建一个(name,sex)的组合索引,查询过程如下:

  1. 走(name,sex)索引检索出以 javacode35 的第一条记录,可以得到(name,sex,id),记做 R1
  2. 判断 R1.sex 是否为 1,然后重复上面的操作,知道找到所有记录为止

这个过程中不需要回表操作了,通过索引的数据就可以完成整个条件的过滤,速度比上面的更快一些。

数字使字符串类索引失效

mysql> insert into test1 (id,name,sex,email) values (4000001,’1’,1,’javacode2018@163.com’);
Query OK, 1 row affected (0.00 sec)

mysql> select from test1 where name = ‘1’;
+————-+———+——-+———————————+
| id | name | sex | email |
+————-+———+——-+———————————+
| 4000001 | 1 | 1 | javacode2018@163.com |
+————-+———+——-+———————————+
1 row in *set
(0.00 sec)

mysql> select * from test1 where name = 1;
+————-+———+——-+———————————+
| id | name | sex | email |
+————-+———+——-+———————————+
| 4000001 | 1 | 1 | javacode2018@163.com |
+————-+———+——-+———————————+
1 row in set, 65535 warnings (3.30 sec)

上面 3 条 sql,我们插入了一条记录。
第二条查询很快,第三条用 name 和 1 比较,name 上有索引,name 是字符串类型,字符串和数字比较的时候,会将字符串强制转换为数字,然后进行比较,所以第二个查询变成了全表扫描,只能取出每条数据,将 name 转换为数字和 1 进行比较。
数字字段和字符串比较什么效果呢?如下:
mysql> select from test1 where id = ‘4000000’;
+————-+————————-+——-+————————————-+
| id | name | sex | email |
+————-+————————-+——-+————————————-+
| 4000000 | javacode4000000 | 2 | javacode4000000@163.com |
+————-+————————-+——-+————————————-+
1 row in *set
(0.00 sec)

mysql> select from test1 where id = 4000000;
+————-+————————-+——-+————————————-+
| id | name | sex | email |
+————-+————————-+——-+————————————-+
| 4000000 | javacode4000000 | 2 | javacode4000000@163.com |
+————-+————————-+——-+————————————-+
1 row in *set
(0.00 sec)

id 上面有主键索引,id 是 int 类型的,可以看到,上面两个查询都非常快,都可以正常利用索引快速检索,所以如果字段是数组类型的,查询的值是字符串还是数组都会走索引。

函数使索引无效

mysql> select a.name+1 from test1 a where a.name = ‘javacode1’;
+—————+
| a.name+1 |
+—————+
| 1 |
+—————+
1 row in set, 1 warning (0.00 sec)

mysql> select from test1 a where concat(a.name,’1’) = ‘javacode11’;
+——+—————-+——-+—————————-+
| id | name | sex | email |
+——+—————-+——-+—————————-+
| 1 | javacode1 | 1 | javacode1@163.com |
+——+—————-+——-+—————————-+
1 row in *set
(2.88 sec)

name 上有索引,上面查询,第一个走索引,第二个不走索引,第二个使用了函数之后,name 所在的索引树是无法快速定位需要查找的数据所在的页的,只能将所有页的记录加载到内存中,然后对每条数据使用函数进行计算之后再进行条件判断,此时索引无效了,变成了全表数据扫描。
结论:索引字段使用函数查询使索引无效。

运算符使索引无效

mysql> select from test1 a where id = 2 - 1;
+——+—————-+——-+—————————-+
| id | name | sex | email |
+——+—————-+——-+—————————-+
| 1 | javacode1 | 1 | javacode1@163.com |
+——+—————-+——-+—————————-+
1 row *in
set (0.00 sec)

mysql> select from test1 a where id+1 = 2;
+——+—————-+——-+—————————-+
| id | name | sex | email |
+——+—————-+——-+—————————-+
| 1 | javacode1 | 1 | javacode1@163.com |
+——+—————-+——-+—————————-+
1 row *in
set (2.41 sec)

id 上有主键索引,上面查询,第一个走索引,第二个不走索引,第二个使用运算符,id 所在的索引树是无法快速定位需要查找的数据所在的页的,只能将所有页的记录加载到内存中,然后对每条数据的 id 进行计算之后再判断是否等于 1,此时索引无效了,变成了全表数据扫描。
结论:索引字段使用了函数将使索引无效。

使用索引优化排序

我们有个订单表 t_order(id,user_id,addtime,price),经常会查询某个用户的订单,并且按照 addtime 升序排序,应该怎么创建索引呢?我们来分析一下。
在 user_id 上创建索引,我们分析一下这种情况,数据检索的过程:

  1. 走 user_id 索引,找到记录的的 id
  2. 通过 id 在主键索引中回表检索出整条数据
  3. 重复上面的操作,获取所有目标记录
  4. 在内存中对目标记录按照 addtime 进行排序

我们要知道当数据量非常大的时候,排序还是比较慢的,可能会用到磁盘中的文件,有没有一种方式,查询出来的数据刚好是排好序的。
我们再回顾一下 mysql 中 b+树数据的结构,记录是按照索引的值排序组成的链表,如果将 user_id 和 addtime 放在一起组成联合索引(user_id,addtime),这样通过 user_id 检索出来的数据自然就是按照 addtime 排好序的,这样直接少了一步排序操作,效率更好,如果需 addtime 降序,只需要将结果翻转一下就可以了。

总结一下使用索引的一些建议

  1. 在区分度高的字段上面建立索引可以有效的使用索引,区分度太低,无法有效的利用索引,可能需要扫描所有数据页,此时和不使用索引差不多
  2. 联合索引注意最左匹配原则:必须按照从左到右的顺序匹配,mysql 会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如 a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d 是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d 的顺序可以任意调整
  3. 查询记录的时候,少使用*,尽量去利用索引覆盖,可以减少回表操作,提升效率
  4. 有些查询可以采用联合索引,进而使用到索引下推(IPC),也可以减少回表操作,提升效率
  5. 禁止对索引字段使用函数、运算符操作,会使索引失效
  6. 字符串字段和数字比较的时候会使索引无效
  7. 模糊查询’%值%’会使索引无效,变为全表扫描,但是’值%’这种可以有效利用索引
  8. 排序中尽量使用到索引字段,这样可以减少排序,提升查询效率

    二十五、sql中的where条件在数据库中提取与应用浅析

    问题描述

    一条 SQL,在数据库中是如何执行的呢?相信很多人都会对这个问题比较感兴趣。当然,要完整描述一条 SQL 在数据库中的生命周期,这是一个非常巨大的问题,涵盖了 SQL 的词法解析、语法解析、权限检查、查询优化、SQL 执行等一系列的步骤,简短的篇幅是绝对无能为力的。因此,本文挑选了其中的部分内容,也是我一直都想写的一个内容,做重点介绍:
    给定一条 SQL,如何提取其中的 where 条件?where 条件中的每个子条件,在 SQL 执行的过程中有分别起着什么样的作用?
    通过本文的介绍,希望读者能够更好地理解查询条件对于 SQL 语句的影响;撰写出更为优质的 SQL 语句;更好地理解一些术语,例如:MySQL 5.6 中一个重要的优化——Index Condition Pushdown,究竟 push down 了什么?
    本文接下来的内容,安排如下:

  9. 简单介绍关系型数据库中数据的组织形式

  10. 给定一条 SQL,如何提取其中的 where 条件
  11. 最后做一个小的总结

    关系型数据库中的数据组织

    关系型数据库中,数据组织涉及到两个最基本的结构:表与索引。表中存储的是完整记录,一般有两种组织形式:堆表(所有的记录无序存储),或者是聚簇索引表(所有的记录,按照记录主键进行排序存储)。索引中存储的是完整记录的一个子集,用于加速记录的查询速度,索引的组织形式,一般均为 B+树结构。
    有了这些基本知识之后,接下来让我们创建一张测试表,为表新增几个索引,然后插入几条记录,最后看看表的完整数据组织、存储结构是怎么样的。(注意:下面的实例,使用的表的结构为堆表形式,这也是 Oracle/DB2/PostgreSQL 等数据库采用的表组织形式,而不是 InnoDB 引擎所采用的聚簇索引表。其实,表结构采用何种形式并不重要,最重要的是理解下面章节的核心,在任何表结构中均适用)
    create table t1 (a int primary key, b int, c int, d int, e varchar(20));
    create index idx_t1_bcd on t1(b, c, d);

insert into t1 values (4,3,1,1,’d’);
insert into t1 values (1,1,1,1,’a’);
insert into t1 values (8,8,8,8,’h’):
insert into t1 values (2,2,2,2,’b’);
insert into t1 values (5,2,3,5,’e’);
insert into t1 values (3,3,2,2,’c’);
insert into t1 values (7,4,5,5,’g’);
insert into t1 values (6,6,4,4,’f’);

t1 表的存储结构如下图所示(只画出了 idx_t1_bcd 索引与 t1 表结构,没有包括 t1 表的主键索引):
image.png
简单分析一下上图,idx_t1_bcd 索引上有[b,c,d]三个字段(注意:若是 InnoDB 类的聚簇索引表,idx_t1_bcd 上还会包括主键 a 字段),不包括[a,e]字段。idx_t1_bcd 索引,首先按照 b 字段排序,b 字段相同,则按照 c 字段排序,以此类推。记录在索引中按照[b,c,d]排序,但是在堆表上是乱序的,不按照任何字段排序。

SQL 的 where 条件提取

在有了以上的 t1 表之后,接下来就可以在此表上进行 SQL 查询了,获取自己想要的数据。例如,考虑以下的一条 SQL:
select * from t1 where b >= 2 and b < 8 and c > 1 and d != 4 and e != ‘a’;

一条比较简单的 SQL,一目了然就可以发现 where 条件使用到了[b,c,d,e]四个字段,而 t1 表的 idx_t1_bcd 索引,恰好使用了[b,c,d]这三个字段,那么走 idx_t1_bcd 索引进行条件过滤,应该是一个不错的选择。接下来,让我们抛弃数据库的思想,直接思考这条 SQL 的几个关键性问题:

此 SQL,覆盖索引 idx_t1_bcd 上的哪个范围?

起始范围:记录[2,2,2]是第一个需要检查的索引项。索引起始查找范围由 b >= 2,c > 1 决定。
终止范围:记录[8,8,8]是第一个不需要检查的记录,而之前的记录均需要判断。索引的终止查找范围由 b < 8 决定;

在确定了查询的起始、终止范围之后,SQL 中还有哪些条件可以使用索引 idx_t1_bcd 过滤?

根据 SQL,固定了索引的查询范围[(2,2,2),(8,8,8))之后,此索引范围中并不是每条记录都是满足 where 查询条件的。例如:(3,1,1)不满足 c > 1 的约束;(6,4,4)不满足 d != 4 的约束。而 c,d 列,均可在索引 idx_t1_bcd 中过滤掉不满足条件的索引记录的。
因此,SQL 中还可以使用 c > 1 and d != 4 条件进行索引记录的过滤。

在确定了索引中最终能够过滤掉的条件之后,还有哪些条件是索引无法过滤的?

此问题的答案显而易见,e != ‘a’这个查询条件,无法在索引 idx_t1_bcd 上进行过滤,因为索引并未包含 e 列。e 列只在堆表上存在,为了过滤此查询条件,必须将已经满足索引查询条件的记录回表,取出表中的 e 列,然后使用 e 列的查询条件 e != ‘a’进行最终的过滤。
在理解以上的问题解答的基础上,做一个抽象,可总结出一套放置于所有 SQL 语句而皆准的 where 查询条件的提取规则:

所有 SQL 的 where 条件,均可归纳为 3 大类

  • Index Key (First Key & Last Key)
  • Index Filter
  • Table Filter

接下来,让我们来详细分析这 3 大类分别是如何定义,以及如何提取的。

1.Index Key

用于确定 SQL 查询在索引中的连续范围(起始范围+结束范围)的查询条件,被称之为 Index Key。由于一个范围,至少包含一个起始与一个终止,因此 Index Key 也被拆分为 Index First Key 和 Index Last Key,分别用于定位索引查找的起始,以及索引查询的终止条件。

Index First Key

用于确定索引查询的起始范围。提取规则:从索引的第一个键值开始,检查其在 where 条件中是否存在,若存在并且条件是=、>=,则将对应的条件加入 Index First Key 之中,继续读取索引的下一个键值,使用同样的提取规则;若存在并且条件是>,则将对应的条件加入 Index First Key 中,同时终止 Index First Key 的提取;若不存在,同样终止 Index First Key 的提取。
针对上面的 SQL,应用这个提取规则,提取出来的 Index First Key 为(b >= 2, c > 1)。由于 c 的条件为 >,提取结束,不包括 d。

Index Last Key

Index Last Key 的功能与 Index First Key 正好相反,用于确定索引查询的终止范围。提取规则:从索引的第一个键值开始,检查其在 where 条件中是否存在,若存在并且条件是=、<=,则将对应条件加入到 Index Last Key 中,继续提取索引的下一个键值,使用同样的提取规则;若存在并且条件是 < ,则将条件加入到 Index Last Key 中,同时终止提取;若不存在,同样终止 Index Last Key 的提取。
针对上面的 SQL,应用这个提取规则,提取出来的 Index Last Key 为(b < 8),由于是 < 符号,因此提取 b 之后结束。

2.Index Filter

在完成 Index Key 的提取之后,我们根据 where 条件固定了索引的查询范围,但是此范围中的项,并不都是满足查询条件的项。在上面的 SQL 用例中,(3,1,1),(6,4,4)均属于范围中,但是又均不满足 SQL 的查询条件。
Index Filter 的提取规则:同样从索引列的第一列开始,检查其在 where 条件中是否存在:若存在并且 where 条件仅为 =,则跳过第一列继续检查索引下一列,下一索引列采取与索引第一列同样的提取规则;若 where 条件为 >=、>、<、<= 其中的几种,则跳过索引第一列,将其余 where 条件中索引相关列全部加入到 Index Filter 之中;若索引第一列的 where 条件包含 =、>=、>、<、<= 之外的条件,则将此条件以及其余 where 条件中索引相关列全部加入到 Index Filter 之中;若第一列不包含查询条件,则将所有索引相关条件均加入到 Index Filter 之中。
针对上面的用例 SQL,索引第一列只包含 >=、< 两个条件,因此第一列可跳过,将余下的 c、d 两列加入到 Index Filter 中。因此获得的 Index Filter 为 c > 1 and d != 4 。

3.Table Filter

Table Filter 是最简单,最易懂,也是提取最为方便的。提取规则:所有不属于索引列的查询条件,均归为 Table Filter 之中。
同样,针对上面的用例 SQL,Table Filter 就为 e != ‘a’。

Index Key/Index Filter/Table Filter 小结

SQL 语句中的 where 条件,使用以上的提取规则,最终都会被提取到 Index Key (First Key & Last Key),Index Filter 与 Table Filter 之中。
Index First Key,只是用来定位索引的起始范围,因此只在索引第一次 Search Path(沿着索引 B+树的根节点一直遍历,到索引正确的叶节点位置)时使用,一次判断即可;
Index Last Key,用来定位索引的终止范围,因此对于起始范围之后读到的每一条索引记录,均需要判断是否已经超过了 Index Last Key 的范围,若超过,则当前查询结束;
Index Filter,用于过滤索引查询范围中不满足查询条件的记录,因此对于索引范围中的每一条记录,均需要与 Index Filter 进行对比,若不满足 Index Filter 则直接丢弃,继续读取索引下一条记录;
Table Filter,则是最后一道 where 条件的防线,用于过滤通过前面索引的层层考验的记录,此时的记录已经满足了 Index First Key 与 Index Last Key 构成的范围,并且满足 Index Filter 的条件,回表读取了完整的记录,判断完整记录是否满足 Table Filter 中的查询条件,同样的,若不满足,跳过当前记录,继续读取索引的下一条记录,若满足,则返回记录,此记录满足了 where 的所有条件,可以返回给前端用户。

结语

在读完、理解了以上内容之后,详细大家对于数据库如何提取 where 中的查询条件,如何将 where 中的查询条件提取为 Index Key,Index Filter,Table Filter 有了深刻的认识。以后在撰写 SQL 语句时,可以对照表的定义,尝试自己提取对应的 where 条件,与最终的 SQL 执行计划对比,逐步强化自己的理解。
同时,我们也可以回答文章开始提出的一个问题:MySQL 5.6 中引入的 Index Condition Pushdown,究竟是将什么 Push Down 到索引层面进行过滤呢?对了,答案是 Index Filter。在 MySQL 5.6 之前,并不区分 Index Filter 与 Table Filter,统统将 Index First Key 与 Index Last Key 范围内的索引记录,回表读取完整记录,然后返回给 MySQL Server 层进行过滤。而在 MySQL 5.6 之后,Index Filter 与 Table Filter 分离,Index Filter 下降到 InnoDB 的索引层面进行过滤,减少了回表与返回 MySQL Server 层的记录交互开销,提高了 SQL 的执行效率。

二十六、如何使用MySQL实现分布式锁

分布式锁的功能

  1. 分布式锁使用者位于不同的机器中,锁获取成功之后,才可以对共享资源进行操作
  2. 锁具有重入的功能:即一个使用者可以多次获取某个锁
  3. 获取锁有超时的功能:即在指定的时间内去尝试获取锁,超过了超时时间,如果还未获取成功,则返回获取失败
  4. 能够自动容错,比如:A 机器获取锁 lock1 之后,在释放锁 lock1 之前,A 机器挂了,导致锁 lock1 未释放,结果会 lock1 一直被 A 机器占有着,遇到这种情况时,分布式锁要能够自动解决,可以这么做:持有锁的时候可以加个持有超时时间,超过了这个时间还未释放的,其他机器将有机会获取锁

    预备技能:乐观锁

    通常我们修改表中一条数据过程如下:
    t1:select获取记录R1
    t2:对R1进行编辑
    t3:update R1

我们来看一下上面的过程存在的问题:
如果 A、B 两个线程同时执行到 t1,他们俩看到的 R1 的数据一样,然后都对 R1 进行编辑,然后去执行 t3,最终 2 个线程都会更新成功,后面一个线程会把前面一个线程 update 的结果给覆盖掉,这就是并发修改数据存在的问题。
我们可以在表中新增一个版本号,每次更新数据时候将版本号作为条件,并且每次更新时候版本号+1,过程优化一下,如下:
t1:打开事务start transaction
t2:select获取记录R1,声明变量v=R1.version
t3:对R1进行编辑
t4:执行更新操作
update R1 set version = version + 1 where user_id=#user_id# and version = #v#;
t5:t4中的update会返回影响的行数,我们将其记录在count中,然后根据count来判断提交还是回滚
if(count==1){
//提交事务
commit;
}else{
//回滚事务
rollback;
}

上面重点在于步骤 t4,当多个线程同时执行到 t1,他们看到的 R1 是一样的,但是当他们执行到 t4 的时候,数据库会对 update 的这行记录加锁,确保并发情况下排队执行,所以只有第一个的 update 会返回 1,其他的 update 结果会返回 0,然后后面会判断 count 是否为 1,进而对事务进行提交或者回滚。可以通过 count 的值知道修改数据是否成功了。
上面这种方式就乐观锁。我们可以通过乐观锁的方式确保数据并发修改过程中的正确性。

使用 mysql 实现分布式锁

建表

我们创建一个分布式锁表,如下
DROP DATABASE IF EXISTS javacode2018;
CREATE DATABASE javacode2018;
USE javacode2018;
DROP TABLE IF EXISTS t_lock;
create table t_lock(
lock_key varchar(32) PRIMARY KEY NOT NULL COMMENT ‘锁唯一标志’,
request_id varchar(64) NOT NULL DEFAULT ‘’ COMMENT ‘用来标识请求对象的’,
lock_count INT NOT NULL DEFAULT 0 COMMENT ‘当前上锁次数’,
timeout BIGINT NOT NULL DEFAULT 0 COMMENT ‘锁超时时间’,
version INT NOT NULL DEFAULT 0 COMMENT ‘版本号,每次更新+1’
)COMMENT ‘锁信息表’;

分布式锁工具类:

package com.itsoku.sql;

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

import java.sql.;
import java.util.Objects;
import java.util.UUID;
*import
java.util.concurrent.TimeUnit;

/
工作10年的前阿里P7分享Java、算法、数据库方面的技术干货!坚信用技术改变命运,让家人过上更体面的生活!
喜欢的请关注公众号:路人甲Java
*/
@Slf4j
public class LockUtils** {

  1. //将requestid保存在该变量中<br /> **static** ThreadLocal<String> requestIdTL = **new** ThreadLocal<>();
  2. /**<br /> * 获取当前线程requestid<br /> *<br /> * **@return**<br /> */<br /> **public** **static** String **getRequestId**() {<br /> String requestId = requestIdTL.get();<br /> **if** (requestId == **null** || "".equals(requestId)) {<br /> requestId = UUID.randomUUID().toString();<br /> requestIdTL.set(requestId);<br /> }<br /> log.info("requestId:{}", requestId);<br /> **return** requestId;<br /> }
  3. /**<br /> * 获取锁<br /> *<br /> * **@param** lock_key 锁key<br /> * **@param** locktimeout(毫秒) 持有锁的有效时间,防止死锁<br /> * **@param** gettimeout(毫秒) 获取锁的超时时间,这个时间内获取不到将重试<br /> * **@return**<br /> */<br /> **public** **static** **boolean** **lock**(String lock_key, **long** locktimeout, **int** gettimeout) **throws** Exception {<br /> log.info("start");<br /> **boolean** lockResult = **false**;<br /> String request_id = getRequestId();<br /> **long** starttime = System.currentTimeMillis();<br /> **while** (**true**) {<br /> LockModel lockModel = LockUtils.get(lock_key);<br /> **if** (Objects.isNull(lockModel)) {<br /> //插入一条记录,重新尝试获取锁<br /> LockUtils.insert(LockModel.builder().lock_key(lock_key).request_id("").lock_count(0).timeout(0L).version(0).build());<br /> } **else** {<br /> String reqid = lockModel.getRequest_id();<br /> //如果reqid为空字符,表示锁未被占用<br /> **if** ("".equals(reqid)) {<br /> lockModel.setRequest_id(request_id);<br /> lockModel.setLock_count(1);<br /> lockModel.setTimeout(System.currentTimeMillis() + locktimeout);<br /> **if** (LockUtils.update(lockModel) == 1) {<br /> lockResult = **true**;<br /> **break**;<br /> }<br /> } **else** **if** (request_id.equals(reqid)) {<br /> //如果request_id和表中request_id一样表示锁被当前线程持有者,此时需要加重入锁<br /> lockModel.setTimeout(System.currentTimeMillis() + locktimeout);<br /> lockModel.setLock_count(lockModel.getLock_count() + 1);<br /> **if** (LockUtils.update(lockModel) == 1) {<br /> lockResult = **true**;<br /> **break**;<br /> }<br /> } **else** {<br /> //锁不是自己的,并且已经超时了,则重置锁,继续重试<br /> **if** (lockModel.getTimeout() < System.currentTimeMillis()) {<br /> LockUtils.resetLock(lockModel);<br /> } **else** {<br /> //如果未超时,休眠100毫秒,继续重试<br /> **if** (starttime + gettimeout > System.currentTimeMillis()) {<br /> TimeUnit.MILLISECONDS.sleep(100);<br /> } **else** {<br /> **break**;<br /> }<br /> }<br /> }<br /> }<br /> }<br /> log.info("end");<br /> **return** lockResult;<br /> }
  4. /**<br /> * 释放锁<br /> *<br /> * **@param** lock_key<br /> * **@throws** Exception<br /> */<br /> **public** **static** **void** **unlock**(String lock_key) **throws** Exception {<br /> //获取当前线程requestId<br /> String requestId = getRequestId();<br /> LockModel lockModel = LockUtils.get(lock_key);<br /> //当前线程requestId和库中request_id一致 && lock_count>0,表示可以释放锁<br /> **if** (Objects.nonNull(lockModel) && requestId.equals(lockModel.getRequest_id()) && lockModel.getLock_count() > 0) {<br /> **if** (lockModel.getLock_count() == 1) {<br /> //重置锁<br /> resetLock(lockModel);<br /> } **else** {<br /> lockModel.setLock_count(lockModel.getLock_count() - 1);<br /> LockUtils.update(lockModel);<br /> }<br /> }<br /> }
  5. /**<br /> * 重置锁<br /> *<br /> * **@param** lockModel<br /> * **@return**<br /> * **@throws** Exception<br /> */<br /> **public** **static** **int** **resetLock**(LockModel lockModel) **throws** Exception {<br /> lockModel.setRequest_id("");<br /> lockModel.setLock_count(0);<br /> lockModel.setTimeout(0L);<br /> **return** LockUtils.update(lockModel);<br /> }
  6. /**<br /> * 更新lockModel信息,内部采用乐观锁来更新<br /> *<br /> * **@param** lockModel<br /> * **@return**<br /> * **@throws** Exception<br /> */<br /> **public** **static** **int** **update**(LockModel lockModel) **throws** Exception {<br /> **return** exec(conn -> {<br /> String sql = "UPDATE t_lock SET request_id = ?,lock_count = ?,timeout = ?,version = version + 1 WHERE lock_key = ? AND version = ?";<br /> PreparedStatement ps = conn.prepareStatement(sql);<br /> **int** colIndex = 1;<br /> ps.setString(colIndex++, lockModel.getRequest_id());<br /> ps.setInt(colIndex++, lockModel.getLock_count());<br /> ps.setLong(colIndex++, lockModel.getTimeout());<br /> ps.setString(colIndex++, lockModel.getLock_key());<br /> ps.setInt(colIndex++, lockModel.getVersion());<br /> **return** ps.executeUpdate();<br /> });<br /> }
  7. **public** **static** LockModel **get**(String lock_key) **throws** Exception {<br /> **return** exec(conn -> {<br /> String sql = "select * from t_lock t WHERE t.lock_key=?";<br /> PreparedStatement ps = conn.prepareStatement(sql);<br /> **int** colIndex = 1;<br /> ps.setString(colIndex++, lock_key);<br /> ResultSet rs = ps.executeQuery();<br /> **if** (rs.next()) {<br /> **return** LockModel.builder().<br /> lock_key(lock_key).<br /> request_id(rs.getString("request_id")).<br /> lock_count(rs.getInt("lock_count")).<br /> timeout(rs.getLong("timeout")).<br /> version(rs.getInt("version")).build();<br /> }<br /> **return** **null**;<br /> });<br /> }
  8. **public** **static** **int** **insert**(LockModel lockModel) **throws** Exception {<br /> **return** exec(conn -> {<br /> String sql = "insert into t_lock (lock_key, request_id, lock_count, timeout, version) VALUES (?,?,?,?,?)";<br /> PreparedStatement ps = conn.prepareStatement(sql);<br /> **int** colIndex = 1;<br /> ps.setString(colIndex++, lockModel.getLock_key());<br /> ps.setString(colIndex++, lockModel.getRequest_id());<br /> ps.setInt(colIndex++, lockModel.getLock_count());<br /> ps.setLong(colIndex++, lockModel.getTimeout());<br /> ps.setInt(colIndex++, lockModel.getVersion());<br /> **return** ps.executeUpdate();<br /> });<br /> }
  9. **public** **static** <T> T **exec**(SqlExec<T> sqlExec) **throws** Exception {<br /> Connection conn = getConn();<br /> **try** {<br /> **return** sqlExec.exec(conn);<br /> } **finally** {<br /> closeConn(conn);<br /> }<br /> }
  10. @FunctionalInterface<br /> **public** **interface** **SqlExec**<**T**> {<br /> T **exec**(Connection conn) **throws** Exception;<br /> }
  11. @Getter<br /> @Setter<br /> @Builder<br /> **public** **static** **class** **LockModel** {<br /> **private** String lock_key;<br /> **private** String request_id;<br /> **private** Integer lock_count;<br /> **private** Long timeout;<br /> **private** Integer version;<br /> }
  12. **private** **static** **final** String url = "jdbc:mysql://localhost:3306/javacode2018?useSSL=false"; //数据库地址<br /> **private** **static** **final** String username = "root"; //数据库用户名<br /> **private** **static** **final** String password = "root123"; //数据库密码<br /> **private** **static** **final** String driver = "com.mysql.jdbc.Driver"; //mysql驱动
  13. /**<br /> * 连接数据库<br /> *<br /> * **@return**<br /> */<br /> **public** **static** Connection **getConn**() {<br /> Connection conn = **null**;<br /> **try** {<br /> Class.forName(driver); //加载数据库驱动<br /> **try** {<br /> conn = DriverManager.getConnection(url, username, password); //连接数据库<br /> } **catch** (SQLException e) {<br /> e.printStackTrace();<br /> }<br /> } **catch** (ClassNotFoundException e) {<br /> e.printStackTrace();<br /> }<br /> **return** conn;<br /> }
  14. /**<br /> * 关闭数据库链接<br /> *<br /> * **@return**<br /> */<br /> **public** **static** **void** **closeConn**(Connection conn) {<br /> **if** (conn != **null**) {<br /> **try** {<br /> conn.close(); //关闭数据库链接<br /> } **catch** (SQLException e) {<br /> e.printStackTrace();<br /> }<br /> }<br /> }<br />}

上面代码中实现了文章开头列的分布式锁的所有功能,大家可以认真研究下获取锁的方法:lock,释放锁的方法:unlock。

测试用例

package com.itsoku.sql;

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

import static com.itsoku.sql.LockUtils.lock;
import static com.itsoku.sql.LockUtils.unlock;

/
工作10年的前阿里P7分享Java、算法、数据库方面的技术干货!坚信用技术改变命运,让家人过上更体面的生活!
喜欢的请关注公众号:路人甲Java
*/
@Slf4j
public class LockUtilsTest** {

  1. //测试重复获取和重复释放<br /> @Test<br /> **public** **void** **test1**() **throws** Exception {<br /> String lock_key = "key1";<br /> **for** (**int** i = 0; i < 10; i++) {<br /> lock(lock_key, 10000L, 1000);<br /> }<br /> **for** (**int** i = 0; i < 9; i++) {<br /> unlock(lock_key);<br /> }<br /> }
  2. //获取之后不释放,超时之后被thread1获取<br /> @Test<br /> **public** **void** **test2**() **throws** Exception {<br /> String lock_key = "key2";<br /> lock(lock_key, 5000L, 1000);<br /> Thread thread1 = **new** Thread(() -> {<br /> **try** {<br /> **try** {<br /> lock(lock_key, 5000L, 7000);<br /> } **finally** {<br /> unlock(lock_key);<br /> }<br /> } **catch** (Exception e) {<br /> e.printStackTrace();<br /> }<br /> });<br /> thread1.setName("thread1");<br /> thread1.start();<br /> thread1.join();<br /> }<br />}

test1方法测试了重入锁的效果。
test2测试了主线程获取锁之后一直未释放,持有锁超时之后被thread1获取到了。

留给大家一个问题

上面分布式锁还需要考虑一个问题:比如 A 机会获取了 key1 的锁,并设置持有锁的超时时间为 10 秒,但是获取锁之后,执行了一段业务操作,业务操作耗时超过 10 秒了,此时机器 B 去获取锁时可以获取成功的,此时会导致 A、B 两个机器都获取锁成功了,都在执行业务操作,这种情况应该怎么处理?大家可以思考一下,可以加我微信:itsoku,我们一起讨论一下。

二十七、MySQL如何确保数据不丢失的?有几点我们可以借鉴

本篇文章我们先来看一下 mysql 是如何确保数据不丢失的,通过本文我们可以了解 mysql 内部确保数据不丢失的原理,学习里面优秀的设计要点,然后我们再借鉴这些优秀的设计要点进行实践应用,加深理解。

预备知识

  1. mysql 内部是使用 b+树的结构将数据存储在磁盘中,b+树中节点对应 mysql 中的页,mysql 和磁盘交互的最小单位为页,页默认情况下为 16kb,表中的数据记录存储在 b+树的叶子节点中,当我们需要修改、删除、插入数据时,都需要按照页来对磁盘进行操作。
  2. 磁盘顺序写比随机写效率要高很多,通常我们使用的是机械硬盘,机械硬盘写数据的时候涉及磁盘寻道、磁盘旋转寻址、数据写入的时间,耗时比较长,如果是顺序写,省去了寻道和磁盘旋转的时间,效率会高几个数量级。
  3. 内存中数据读写操作比磁盘中数据读写操作速度高好多个数量级。

    mysql 确保数据不丢失原理分析

    我们来思考一下,下面这条语句的执行过程是什么样的:
    start transaction;
    update t_user set name = ‘路人甲Java’ where user_id = 666;
    commit;

按照正常的思路,通常过程如下:

  1. 找到 user_id=666 这条记录所在的页 p1,将 p1 从磁盘加载到内存中
  2. 在内存中对 p1 中 user_id=666 这条记录信息进行修改
  3. mysql 收到 commit 指令
  4. 将 p1 页写入磁盘
  5. 给客户端返回更新成功

上面过程可以确保数据被持久化到了磁盘中。
我们将需求改一下,如下:
start transaction;
update t_user set name = ‘路人甲Java’ where user_id = 666;
update t_user set name = ‘javacode2018’ where user_id = 888;
commit;

来看一下处理过程:

  1. 找到 user_id=666 这条记录所在的页 p1,将 p1 从磁盘加载到内存中
  2. 在内存中对 p1 中 user_id=666 这条记录信息进行修改
  3. 找到 user_id=888 这条记录所在的页 p2,将 p2 从磁盘加载到内存中
  4. 在内存中对 p2 中 user_id=888 这条记录信息进行修改
  5. mysql 收到 commit 指令
  6. 将 p1 页写入磁盘
  7. 将 p2 页写入磁盘
  8. 给客户端返回更新成功

上面过程我们看有什么问题

  1. 假如 6 成功之后,mysql 宕机了,此时 p1 修改已写入磁盘,但是 p2 的修改还未写入磁盘,最终导致 user_id=666 的记录被修改成功了,user_id=888 的数据被修改失败了,数据是有问题的
  2. 上面 p1 和 p2 可能位于磁盘的不同位置,涉及到磁盘随机写的问题,导致整个过程耗时也比较长

上面问题可以归纳为 2 点:无法确保数据可靠性、随机写导致耗时比较长。
关于上面问题,我们看一下 mysql 是如何优化的,mysql 内部引入了一个 redo log,这是一个文件,对于上面 2 条更新操作,mysql 实现如下:
mysql 内部有个 redo log buffer,是内存中一块区域,我们将其理解为数组结构,向 redo log 文件中写数据时,会先将内容写入 redo log buffer 中,后续会将这个 buffer 中的内容写入磁盘中的 redo log 文件,这个 redo log buffer 是整个 mysql 中所有连接共享的内存区域,可以被重复使用。

  1. mysql 收到 start transaction 后,生成一个全局的事务编号 trx_id,比如 trx_id=10
  2. user_id=666 这个记录我们就叫 r1,user_id=888 这个记录叫 r2
  3. 找到 r1 记录所在的数据页 p1,将其从磁盘中加载到内存中
  4. 在内存中找到 r1 在 p1 中的位置,然后对 p1 进行修改(这个过程可以描述为:将 p1 中的 pos_start1 到 pos_start2 位置的值改为 v1),这个过程我们记为 rb1(内部包含事务编号 trx_id),将 rb1 放入 redo log buffer 数组中,此时 p1 的信息在内存中被修改了,和磁盘中 p1 的数据不一样了
  5. 找到 r2 记录所在的数据页 p2,将其从磁盘中加载到内存中
  6. 在内存中找到 r2 在 p2 中的位置,然后对 p2 进行修改(这个过程可以描述为:将 p2 中的 pos_start1 到 pos_start2 位置的值改为 v2),这个过程我们记为 rb2(内部包含事务编号 trx_id),将 rb2 放入 redo log buffer 数组中,此时 p2 的信息在内存中被修改了,和磁盘中 p2 的数据不一样了
  7. 此时 redo log buffer 数组中有 2 条记录[rb1,rb2]
  8. mysql 收到 commit 指令
  9. 将 redo log buffer 数组中内容写入到 redo log 文件中,写入的内容:1.start trx=10;
    2.写入rb1
    3.写入rb2
    4.end trx=10;

  10. 返回给客户端更新成功。

上面过程执行完毕之后,数据是这样的:

  1. 内存中 p1、p2 页被修改了,还未同步到磁盘中,此时内存中数据页和磁盘中数据页是不一致的,此时内存中数据页我们称为脏页
  2. 对 p1、p2 页修改被持久到磁盘中的 redolog 文件中了,不会丢失

认真看一下上面过程中第 9 步骤,一个成功的事务记录在 redo log 中是有 start 和 end 的,redo log 文件中如果一个 trx_id 对应 start 和 end 成对出现,说明这个事务执行成功了,如果只有 start 没有 end 说明是有问题的。
那么对 p1、p2 页的修改什么时候会同步到磁盘中呢?
redo log 是 mysql 中所有连接共享的文件,对 mysql 执行 insert、delete 和上面 update 的过程类似,都是先在内存中修改页数据,然后将修改过程持久化到 redo log 所在的磁盘文件中,然后返回成功。redo log 文件是有大小的,需要重复利用的(redo log 有多个,多个之间采用环形结构结合几个变量来做到重复利用,这块知识不做说明,有兴趣的可以去网上找一下),当 redo log 满了,或者系统比较闲的时候,会对 redo log 文件中的内容进行处理,处理过程如下:

  1. 读取 redo log 信息,读取一个完整的 trx_id 对应的信息,然后进行处理
  2. 比如读取到了 trx_id=10 的完整内容,包含了 start end,表示这个事务操作是成功的,然后继续向下
  3. 判断 p1 在内存中是否存在,如果存在,则直接将 p1 信息写到 p1 所在的磁盘中;如果 p1 在内存中不存在,则将 p1 从磁盘加载到内存,通过 redo log 中的信息在内存中对 p1 进行修改,然后将其写到磁盘中上面的 update 之后,p1 在内存中是存在的,并且 p1 是已经被修改过的,可以直接刷新到磁盘中。如果上面的 update 之后,mysql 宕机,然后重启了,p1 在内存中是不存在的,此时系统会读取 redo log 文件中的内容进行恢复处理。
  4. 将 redo log 文件中 trx_id=10 的占有的空间标记为已处理,这块空间会被释放出来可以重复利用了
  5. 如果第 2 步读取到的 trx_id 对应的内容没有 end,表示这个事务执行到一半失败了(可能是第 9 步骤写到一半宕机了),此时这个记录是无效的,可以直接跳过不用处理

上面的过程做到了:数据最后一定会被持久化到磁盘中的页中,不会丢失,做到了可靠性。
并且内部采用了先把页的修改操作先在内存中进行操作,然后再写入了 redo log 文件,此处 redo log 是按顺序写的,使用到了 io 的顺序写,效率会非常高,相对于用户来说响应会更快。
对于将数据页的变更持久化到磁盘中,此处又采用了异步的方式去读取 redo log 的内容,然后将页的变更刷到磁盘中,这块的设计也非常好,异步刷盘操作!
但是有一种情况,当一个事务 commit 的时候,刚好发现 redo log 不够了,此时会先停下来处理 redo log 中的内容,然后在进行后续的操作,遇到这种情况时,整个事务响应会稍微慢一些。
mysql 中还有一个 binlog,在事务操作过程中也会写 binlog,先说一下 binlog 的作用,binlog 中详细记录了对数据库做了什么操作,算是对数据库操作的一个流水,这个流水也是相当重要的,主从同步就是使用 binlog 来实现的,从库读取主库中 binlog 的信息,然后在从库中执行,最后,从库就和主库信息保持同步一致了。还有一些其他系统也可以使用 binlog 的功能,比如可以通过 binlog 来实现 bi 系统中 etl 的功能,将业务数据抽取到数据仓库,阿里提供了一个 java 版本的项目:canal,这个项目可以模拟从库从主库读取 binlog 的功能,也就是说可以通过 java 程序来监控数据库详细变化的流水,这个大家可以脑洞大开一下,可以做很多事情的,有兴趣的朋友可以去研究一下;所以 binlog 对 mysql 来说也是相当重要的,我们来看一下系统如何确保 redo log 和 binlog 在一致性的,都写入成功的。
还是以 update 为例:
start transaction;
update t_user set name = ‘路人甲Java’ where user_id = 666;
update t_user set name = ‘javacode2018’ where user_id = 888;
commit;

一个事务中可能有很多操作,这些操作会写很多 binlog 日志,为了加快写的速度,mysql 先把整个过程中产生的 binlog 日志先写到内存中的 binlog cache 缓存中,后面再将 binlog cache 中内容一次性持久化到 binlog 文件中。一个事务的 binlog 是不能被拆开的,因此不论这个事务多大,也要确保一次性写入。这就涉及到了 binlog cache 的保存问题。系统给 binlog cache 分配了一片内存,每个线程一个,参数 binlog_cache_size 用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。
过程如下:

  1. mysql 收到 start transaction 后,生成一个全局的事务编号 trx_id,比如 trx_id=10
  2. user_id=666 这个记录我们就叫 r1,user_id=888 这个记录叫 r2
  3. 找到 r1 记录所在的数据页 p1,将其从磁盘中加载到内存中
  4. 在内存中对 p1 进行修改
  5. 将 p1 修改操作记录到 redo log buffer 中
  6. 将 p1 修改记录流水记录到 binlog cache 中
  7. 找到 r2 记录所在的数据页 p2,将其从磁盘中加载到内存中
  8. 在内存中对 p2 进行修改
  9. 将 p2 修改操作记录到 redo log buffer 中
  10. 将 p2 修改记录流水记录到 binlog cache 中
  11. mysql 收到 commit 指令
  12. 将 redo log buffer 携带 trx_id=10 写入到 redo log 文件,持久化到磁盘,这步操作叫做 redo log prepare,内容如下1.start trx=10; 2.写入 rb1 3.写入 rb2 4.prepare trx=10;注意上面是 prepare 了,不是之前说的 end 了。
  13. 将 binlog cache 携带 trx_id=10 写入到 binlog 文件,持久化到磁盘
  14. 向 redo log 中写入一条数据:end trx=10;表示 redo log 中这个事务完成了,这步操作叫做 redo log commit
  15. 返回给客户端更新成功

我们来分析一下上面过程可能出现的一些情况:
步骤 10 操作完成后,mysql 宕机了
宕机之前,所有修改都位于内存中,mysql 重启之后,内存修改还未同步到磁盘,对磁盘数据没有影响,所以无影响。
步骤 12 执行完毕之后,mysql 宕机了
此时 redo log prepare 过程是写入 redo log 文件了,但是 binlog 写入失败了,此时 mysql 重启之后会读取 redo log 进行恢复处理,查询到 trx_id=10 的记录是 prepare 状态,会去 binlog 中查找 trx_id=10 的操作在 binlog 中是否存在,如果不存在,说明 binlog 写入失败了,此时可以将此操作回滚
步骤 13 执行完毕之后,mysql 宕机
此时 redo log prepare 过程是写入 redo log 文件了,但是 binlog 写入失败了,此时 mysql 重启之后会读取 redo log 进行恢复处理,查询到 trx_id=10 的记录是 prepare 状态,会去 binlog 中查找 trx_id=10 的操作在 binlog 是存在的,然后接着执行上面的步骤 14 和 15.

做一个总结

上面的过程设计比较好的地方,有 2 点
日志先行,io 顺序写,异步操作,做到了高效操作
对数据页,先在内存中修改,然后使用 io 顺序写的方式持久化到 redo log 文件;然后异步去处理 redo log,将数据页的修改持久化到磁盘中,效率非常高,整个过程,其实就是 MySQL 里经常说到的 WAL 技术,WAL 的全称是 Write-Ahead Logging,它的关键点就是先写日志,再写磁盘。
两阶段提交确保 redo log 和 binlog 一致性
为了确保 redo log 和 binlog 一致性,此处使用了二阶段提交技术,redo log 和 binlog 的写分了 3 步走:

  1. 携带 trx_id,redo log prepare 到磁盘
  2. 携带 trx_id,binlog 写入磁盘
  3. 携带 trx_id,redo log commit 到磁盘

上面 3 步骤,可以确保同一个 trx_id 关联的 redo log 和 binlog 的可靠性。
关于上面 2 点优秀的设计,我们平时开发的过程中也可以借鉴,下面举 2 个常见的案例来学习一下。

案例:电商中资金账户高频变动解决方案

电商中有账户表和账户流水表,2 个表结构如下:
drop table IF EXISTS t_acct;
create table t_acct(
acct_id int primary key NOT NULL COMMENT ‘账户id’,
balance decimal(12,2) NOT NULL COMMENT ‘账户余额’,
version INT NOT NULL DEFAULT 0 COMMENT ‘版本号,每次更新+1’
)COMMENT ‘账户表’;

drop table IF EXISTS t_acct_data;
create table t_acct_data(
id int AUTO_INCREMENT PRIMARY KEY COMMENT ‘编号’,
acct_id int primary key NOT NULL COMMENT ‘账户id’,
price DECIMAL(12,2) NOT NULL COMMENT ‘交易额’,
open_balance decimal(12,2) NOT NULL COMMENT ‘期初余额’,
end_balance decimal(12,2) NOT NULL COMMENT ‘期末余额’
) COMMENT ‘账户流水表’;

INSERT INTO t_acct(acct_id, balance, version) VALUES (1,10000,0);

上面向账户表 t_acct 插入了一条数据,余额为 10000,当我们下单成功或者充值的时候,会对上面 2 个表进行操作,会修改 t_acct 的数据,顺便向 t_acct_data 表写一条流水,这个 t_acct_data 表有个期初和期末的流水,关系如下:
end_balance = open_balance + price;
open_balance为操作业务时,t_acct表的balance的值。

如给账户 1 充值 100,过程如下:
t1:开启事务:start transaction;
t2:R1 = (select from t_acct where acct_id = 1);
t3:创建几个变量
v_balance = R1.balance;
t4:update t_acct set balnce = v_balance+100,version = version + 1 where acct_id = 1;
t5:insert into t_acct_data(acct_id,price,open_balnace,end_balance)
*values
(1,100,#v_balance#,#v_balance+100#)
t6:提交事务:commit;

分析一下上面过程存在的问题:
我们开启 2 个线程【thread1、thread2】模拟分别充值 100,正常情况下数据应该是这样的:
t_acct表记录:
(1,10200,1);
t_acct_data表产生2条数据:
(1,100,10000,10100);
(2,100,10100,10200);

但是当 2 个线程同时执行到 t2 的时候获取 R1 记录信息是一样的,变量 v_balance 的值也一样的,最后执行完成之后,数据变成了下面这样:
t_acct表:1,10200
t_acct_data表产生2条数据:
1,100,10000,10100;
2,100,10100,10100;

导致t_acct_data产生的 2 条数据是一样的,这种情况是有问题的,这就是并发导致的问题。
上篇文章中有说到乐观锁可以解决这种并发问题,有兴趣的可以去看一下,过程如下:
t1:打开事务start transaction
t2:R1 = (select from t_acct where acct_id = 1);
t3:创建几个变量
v_version = R1.version;
v_balance = R1.balance;
v_open_balance = v_balance;
v_balance = R1.balance + 100;
v_open_balance = v_balance;
t3:对R1进行编辑
t4:执行更新操作
int count = (update t_acct set balance = #v_balance#,version = version + 1 where acct_id = 1 and version = #v_version#);
t5:if(count==1){
//向t_acct_data表写入数据
insert into t_acct_data(acct_id,price,open_balnace,end_balance) values (1,100,#v_open_balance#,#v_open_balance#)
//提交事务
commit;
}*else
{
//回滚事务
rollback;
}

上面的过程中,如果 2 个线程同时执行到 t2 看到的 R1 数据是一样的,但是最后走到 t4 的时候会被数据库加锁,2 个线程的 update 在 mysql 中会排队执行,最后只有一个 update 的结果返回的影响行数是 1,然后根据 t5,会有一个会被回滚,另外一个被提交,避免了并发导致的问题。
我们分析一下上面过程会有什么问题?
刚才上面也提到了,并发量大的时候,只有部分会成功,比如 10 个线程同时执行到 t2 的时候,其中只有 1 个会成功,其他 9 个都会失败,并发量大的情况下失败的概率比较高,这个大家可以并发测试一下,失败率很高,下面我们继续优化。
分析一下问题主要出现在写 t_acct_data 上面,如果没有这个表的操作,我们直接用一个 update 就完成了操作,速度是非常快的,上面我们学到的了 mysql 中先写日志,然后异步刷盘的方式,此处我们也可以采用这种思路,先记录一条交易日志,然后异步根据交易日志将交易流水写到t_acct_data表中。
那我们继续优化,新增一个账户操作日志表:
drop table IF EXISTS t_acct_log;
create table t_acct_log(
id INT AUTO_INCREMENT PRIMARY KEY COMMENT ‘编号’,
acct_id int primary key NOT NULL COMMENT ‘账户id’,
price DECIMAL(12,2) NOT NULL COMMENT ‘交易额’,
status SMALLINT NOT NULL DEFAULT 0 COMMENT ‘状态,0:待处理,1:处理成功’
) COMMENT ‘账户操作日志表’;

顺便对 t_acct 标做一下改造,新增一个字段old_balance,新结构如下:
drop table IF EXISTS t_acct;
create table t_acct(
acct_id int primary key NOT NULL COMMENT ‘账户id’,
balance decimal(12,2) NOT NULL COMMENT ‘账户余额’,
old_balance decimal(12,2) NOT NULL COMMENT ‘账户余额(老的值)’,
version INT NOT NULL DEFAULT 0 COMMENT ‘版本号,每次更新+1’
)COMMENT ‘账户表’;

INSERT INTO t_acct(acct_id, balance,old_balance,version) VALUES (1,10000,10000,0);

新增了一个 old_balance 字段,这个字段的值刚开始的时候和 balance 的值是一致的,后面会在 job 中进行改变,可以先向下看,后面有解释
假设账户 v_acct_id 交易金额为 v_price,过程如下:
t1.开启事务:start transaction;
t2.insert into t_acct_log(acct_id,price,status) values (#v_acct_id#,#v_price#,0)
t3.int count = (update t_acct set balnce = v_balance+#v_price#,version = version+1 where acct_id = #v_acct_id# and v_balance+#v_price#>=0);
t6.if(count==1){
//提交事务
commit;
}else{
//回滚事务
rollback;
}

可以看到上面没有记录流水了,变成插入了一条日志t_acct_log,后面我们异步根据t_acct_log的数据来生成t_acct_data记录。
上面这个操作支撑并发操作还是比较高的,测试了一下每秒 500 笔,并且都成功了,效率非常高。
新增一个 job,查询 t_acct_log 中状态为 0 的记录,然后遍历进行一个个处理,处理过程如下:
假设t_acct_log中当前需要处理的记录为L1
t1:打开事务start transaction
t2:创建变量
v_price = L1.price;
v_acct_id = L1.acct_id;
t3:R1 = (select from t_acct where acct_id = #v_acct_id#);
t4:创建几个变量
v_old_balance = R1.old_balance;
v_open_balance = v_old_balance;
v_old_balance = R1.old_balance + v_price;
v_open_balance = v_old_balance;
t5:int count = (update t_acct set old_balance = #v_old_balance#,version = version + 1 where acct_id = #v_acct_id# and version = #v_version#);
t6:*if
(count==1){
//更新t_acct_log的status置为1
count = (update t_acct_log set status=1 where status=0 and id = #L1.id#);
}

if(count==1){
//提交事务
commit;
}else{
//回滚事务
rollback;
}

上面 t5 中 update 条件中加了version,t6 中的 update 条件中加了status=0的操作,主要是为了防止并发操作修改可能会出错的问题。
上面 t_acct_log 中所有 status=0 的记录被处理完毕之后,t_acct 表中的 balance 和 old_balance 会变为一致。
上面这种方式采用了先写账户操作日志,然后异步对日志进行操作,在生成流水,借鉴了 mysql 中的设计,大家也可以学习学习。

案例 2:跨库转账问题

此处我们使用 mysql 上面介绍的二阶段提交来解决。
如从 A 库的 T1 表转 100 到 B 库的 T1 表。
我们创建一个 C 库,在 C 库新增一个转账订单表,如:
drop table IF EXISTS t_transfer_order;
create table t_transfer_order(
id int NOT NULL AUTO_INCREMENT primary key COMMENT ‘账户id’,
from_acct_id int NOT NULL COMMENT ‘转出方账户’,
to_acct_id int NOT NULL COMMENT ‘转入方账户’,
price decimal(12,2) NOT NULL COMMENT ‘转账金额’,
addtime int COMMENT ‘入库时间(秒)’,
status SMALLINT NOT NULL DEFAULT 0 COMMENT ‘状态,0:待处理,1:转账成功,2:转账失败’,
version INT NOT NULL DEFAULT 0 COMMENT ‘版本号,每次更新+1’
) COMMENT ‘转账订单表’;

A、B 库加 3 张表,如:
drop table IF EXISTS t_acct;
create table t_acct(
acct_id int primary key NOT NULL COMMENT ‘账户id’,
balance decimal(12,2) NOT NULL COMMENT ‘账户余额’,
version INT NOT NULL DEFAULT 0 COMMENT ‘版本号,每次更新+1’
)COMMENT ‘账户表’;

drop table IF EXISTS t_order;
create table t_order(
transfer_order_id int primary key NOT NULL COMMENT ‘转账订单id’,
price decimal(12,2) NOT NULL COMMENT ‘转账金额’,
status SMALLINT NOT NULL DEFAULT 0 COMMENT ‘状态,1:转账成功,2:转账失败’,
version INT NOT NULL DEFAULT 0 COMMENT ‘版本号,每次更新+1’
) COMMENT ‘转账订单表’;

drop table IF EXISTS t_transfer_step_log;
create table t_transfer_step_log(
id int primary key NOT NULL COMMENT ‘账户id’,
transfer_order_id int NOT NULL COMMENT ‘转账订单id’,
step SMALLINT NOT NULL COMMENT ‘转账步骤,0:正向操作,1:回滚操作’,
UNIQUE KEY (transfer_order_id,step)
) COMMENT ‘转账步骤日志表’;

t_transfer_step_log表用于记录转账日志操作步骤的,transfer_order_id,step上加了唯一约束,表示每个步骤只能执行一次,可以确保步骤的幂等性。
定义几个变量:
v_from_acct_id:转出方账户
v_to_acct_id:转入方账户
v_price:交易金额
整个转账流程如下:
每个步骤都有返回值,返回值是数组类型的,含义是:0:处理中(结果未知),1:成功,2:失败
step1:创建转账订单,订单状态为0,表示处理中
C1:start transaction;
C2:insert into t_transfer_order(from_acct_id,to_acct_id,price,addtime,status,version)
values(#v_from_acct_id#,#v_to_acct_id#,#v_price#,0,unix_timestamp(now()));
C3:获取刚才insert成功的订单id,放在变量v_transfer_order_id中
C4:commit;

step2:A库操作如下
A1:AR1 = (select from t_order where transfer_order_id = #v_transfer_order_id#);
A2:if(AR1!=null){
return AR1.status==1?1:2;
}
A3:start transaction;
A4:AR2 = (select 1 from t_acct where acct_id = #v_from_acct_id#);
A5:if(AR2.balance //表示余额不足,那转账肯定是失败了,插入一个转账失败订单
insert into t_order (transfer_order_id,price,status) values (#transfer_order_id#,#v_price#,2);
commit;
//返回失败的状态2
return 2;
}else{
//通过乐观锁 & balance - #v_price# >= 0更新账户资金,防止并发操作
int count = (update t_acct set balance = balance - #v_price#, version = version + 1 where acct_id = #v_from_acct_id# and balance - #v_price# >= 0 and version = #AR2.version#);
//count为1表示上面的更新成功
if(count==1){
//插入转账成功订单,状态为1
insert into t_order (transfer_order_id,price,status) values (#transfer_order_id#,#v_price#,1);
//插入步骤日志
insert into t_transfer_step_log (transfer_order_id,step) values (#v_transfer_order_id#,1);
commit;
return 1;
}else{
//插入转账失败订单,状态为2
insert into t_order (transfer_order_id,price,status) values (#transfer_order_id#,#v_price#,2);
commit;
*return
2;
}
}

step3:
if(step2的结果==1){
//表示A库中扣款成功了
执行step4;
}else if(step2的结果==2){
//表示A库中扣款失败了
执行step6;
}

step4:对B库进行操作,如下:
B1:BR1 = (select from t_order where transfer_order_id = #v_transfer_order_id#);
B2:if(BR1!=null){
return BR1.status==1?1:2;
}else{
执行B3;
}
B3:start transaction;
B4:BR2 = (select 1 from t_acct where acct_id = #v_to_acct_id#);
B5:int count = (update t_acct set balance = balance + #v_price#, version = version + 1 where acct_id = #v_to_acct_id# and version = #BR2.version#);
if(count==1){
//插入订单,状态为1
insert into t_order (transfer_order_id,price,status) values (#transfer_order_id#,#v_price#,1);
//插入日志
insert into t_transfer_step_log (transfer_order_id,step) values (#v_transfer_order_id#,1);
commit;
return 1;
}else{
//进入到此处说明有并发,返回0
rollback;
*return
0;
}

step5:
if(step4的结果==1){
//表示B库中加钱成功了
执行step7;
}

step6:对C库操作(转账失败,将订单置为失败)
C1:AR1 = (select 1 from t_transfer_order where id = #v_transfer_order_id#);
C2:if(AR1.status==1 || AR1.status=2){
return AR1.status=1?”转账成功”:”转账失败”;
}
C3:start transaction;
C4:int count = (udpate t_transfer_order set status = 2,version = version+1 where id = #v_transfer_order_id# and version = version + #AR1.version#)
C5:if(count==1){
commit;
return “转账失败”;
}else{
rollback;
return “处理中”;
}

step7:对C库操作(转账成功,将订单置为成功)
C1:AR1 = (select 1 from t_transfer_order where id = #v_transfer_order_id#);
C2:if(AR1.status==1 || AR1.status=2){
return AR1.status=1?”转账成功”:”转账失败”;
}
C3:start transaction;
C4:int count = (udpate t_transfer_order set status = 1,version = version+1 where id = #v_transfer_order_id# and version = version + #AR1.version#)
C5:if(count==1){
commit;
return “转账成功”;
}else{
rollback;
return “处理中”;
}

还需要新增一个补偿的 job,处理 C 库中状态为 0 的超过 10 分钟的转账订单订单,过程如下:
while(true){
List list = select from t_transfer_order where status = 0 and addtime+1060 if(list为空){
//插叙无记录,退出循环
break;
}
//循环遍历list进行处理
for(Object r:list){
//调用上面的steap2进行处理,最终订单状态会变为1或者2
}
}

说一下:这个 job 的处理有不好的地方,可能会死循环,这个留给大家去思考一下,如何解决?欢迎加我微信 itsoku 交流