Contents

MySQL 基本入门

最近一段时间,需要做一个 信息管理系统,所以涉及到很多 数据库相关的知识,这里就专门恶补一下。之前有用到过 MySQL,但是用法太过基础,没有系统的去学习。这里参照着 《MySQL必知必会》做一些总结。

另外 MySQL 的一些基本操作,可以参考 : MySQL教程

主键

在mysql中,主键全称“主键约束”,是一个列或多列的组合,其值能唯一地标识表中的每一行。主键的作用是确定该数据的唯一性。主要用于和其他表的外键进行关联,以及本记录的修改与删除。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
> CREATE TABLE tb_emp ( 
    id INT(11) PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(25),
    salary FLOAT
);
Query OK, 0 rows affected
Time: 0.047s

> DESC tb_emp;
+--------+-------------+------+-----+---------+----------------+
| Field  | Type        | Null | Key | Default | Extra          |
+--------+-------------+------+-----+---------+----------------+
| id     | int         | NO   | PRI | <null>  | auto_increment |
| name   | varchar(25) | YES  |     | <null>  |                |
| salary | float       | YES  |     | <null>  |                |
+--------+-------------+------+-----+---------+----------------+
3 rows in set
Time: 0.006s

主键一般使用自增字段做主键,但是如果有多台服务器,可能会有主键重复的问题,需要考虑手动赋值的方式。


外键和连接

外键就是从表中用来引用主表中数据的那个公共字段。在关联关系 R 中,公众字段(字段 A)是表 A 的主键,所以表 A 是主表,表 B 是从表。表 B 中的公共字段(字段 A)是外键。

mysql1

在 MySQL 中,外键是通过外键约束来定义的。外键约束就是约束的一种,它必须在从表中定义,包括指明哪个是外键字段,以及外键字段所引用的主表中的主键字段是什么。

1
2
3
4
5
6
7
8
CREATE TABLE 从表名
(
  字段名 类型,
  ...
-- 定义外键约束,指出外键字段和参照的主表字段
CONSTRAINT 外键约束名
FOREIGN KEY (字段名) REFERENCES 主表名 (字段名)
)

如下所示,创建外键约束,先创建主表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
CREATE TABLE demo.importhead (
    listnumber INT PRIMARY KEY,
    supplierid INT,
    stocknumber INT,
    importtype INT,
    importquantity DECIMAL(10 , 3 ),
    importvalue DECIMAL(10 , 2 ),
    recorder INT,
    recordingdate DATETIME
);

再创建从表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

CREATE TABLE demo.importdetails
(
  listnumber INT,
  itemnumber INT,
  quantity DECIMAL(10,3),
  importprice DECIMAL(10,2),
  importvalue DECIMAL(10,2),
  -- 定义外键约束,指出外键字段和参照的主表字段
  CONSTRAINT fk_importdetails_importhead
  FOREIGN KEY (listnumber) REFERENCES importhead (listnumber)
);

创建好了外键,关联关系建立后,需要通过连接查询,这时需要用到连接,MySQL中有 内连接INNER JOIN)和外连接(OUTER JOIN)。

  • 内连接表示查询结果只返回符合连接条件的记录,这种连接方式比较常用;
  • 外连接则表示查询结果返回一个表中的所有记录,以及另一个表中满足连接条件的记录。

如下是一条内连接查询。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18

mysql> SELECT
    ->     a.transactionno,
    ->     a.itemnumber,
    ->     a.quantity,
    ->     a.price,
    ->     a.transdate,
    ->     b.membername
    -> FROM
    ->     demo.trans AS a
    ->         JOIN
    ->     demo.membermaster AS b ON (a.cardno = b.cardno);
+---------------+------------+----------+-------+---------------------+------------+
| transactionno | itemnumber | quantity | price | transdate           | membername |
+---------------+------------+----------+-------+---------------------+------------+
|             1 |          1 |    1.000 | 89.00 | 2020-12-01 00:00:00 | 张三       |
+---------------+------------+----------+-------+---------------------+------------+
1 row in set (0.00 sec)

时间函数

在MySQL中,时间函数是用来处理时间的,比如

  1. 统计一天之中不同时段的销售情况,就要获取时间值中的小时值,这里会用到函数 HOUR(),类似的还有 YEAR() 、MONTH()、Day()、HOUR()、MINUTE()、SECOND()
  2. 要计算与去年同期相比的增长率,就要用到函数 DATE_ADD()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
-- 用 DATE_ADD 函数,获取到 2020 年 12 月 10 日上一年的日期:2019 年 12 月 10 日。
mysql> SELECT DATE_ADD('2020-12-10', INTERVAL - 1 YEAR);
+-------------------------------------------+
| DATE_ADD('2020-12-10', INTERVAL - 1 YEAR) |
+-------------------------------------------+
| 2019-12-10                                |
+-------------------------------------------+
1 row in set (0.00 sec)

-- 获取去年的当月的月初
mysql> SELECT DATE_ADD(LAST_DAY(DATE_ADD(DATE_ADD('2020-12-10', INTERVAL - 1 YEAR),INTERVAL - 1 MONTH)),INTERVAL 1 DAY);
+-----------------------------------------------------------------------------------------------------------+
| DATE_ADD(LAST_DAY(DATE_ADD(DATE_ADD('2020-12-10', INTERVAL - 1 YEAR),INTERVAL - 1 MONTH)),INTERVAL 1 DAY) |
+-----------------------------------------------------------------------------------------------------------+
| 2019-12-01                                                                                                |
+-----------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
  1. 要计算今天是周几,需要用到函数 DAYOFWEEK()

  2. 获取时间中的小时、分钟等值,用到函数 EXTRACT()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

mysql> SELECT
    -> EXTRACT(HOUR FROM b.transdate) AS 时段,
    -> SUM(a.quantity) AS 数量,
    -> SUM(a.salesvalue) AS 金额
    -> FROM
    -> demo.transactiondetails a
    -> JOIN
    -> demo.transactionhead b ON (a.transactionid = b.transactionid)
    -> GROUP BY EXTRACT(HOUR FROM b.transdate)
    -> ORDER BY EXTRACT(HOUR FROM b.transdate);
+------+--------+--------+
| 时段 | 数量   | 金额   |
+------+--------+--------+
|    9 | 16.000 | 500.00 |
|   10 | 11.000 | 139.00 |
|   11 | 10.000 |  30.00 |
|   12 | 40.000 | 200.00 |
|   13 |  5.000 | 445.00 |
|   15 |  6.000 |  30.00 |
|   17 |  1.000 |   3.00 |
|   18 |  2.000 | 178.00 |
|   19 |  2.000 |   6.00 |
+------+--------+--------+
9 rows in set (0.00 sec)

详细的时间函数可以参考官方连接:https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_date-format


索引

索引是为了提升查询速度,就和一本书的目录一样。MySQL 支持单字段索引和组合索引,而单字段索引比较常用。

创建索引可以在建表时添加索引。也可以给已经存在的表添加索引。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
-- 建表时添加索引
CREATE TABLE `t_user` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `id_no` varchar(18) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '身份编号',
  `username` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  KEY `union_idx` (`id_no`,`username`,`age`),
  KEY `create_time_idx` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

-- 已存在表创建索引
CREATE INDEX 索引名 ON TABLE 表名 (字段);

上述表结构中有三个索引:

  • id:数据库主键索引
  • union_idx:id_no、username、age 构成的联合索引
  • create_time_idx:由create_time构成的普通索引

要知道索引是怎么起作用的,我们需要借助 MySQL 中的 EXPLAIN 这个关键字。EXPLAIN 关键字能够查看

1
2
3
4
5
6
7
explain select * from t_user where id_no = '1002';
+----+-------------+--------+------------+------+---------------+-----------+---------+-------+------+----------+--------+
| id | select_type | table  | partitions | type | possible_keys | key       | key_len | ref   | rows | filtered | Extra  |
+----+-------------+--------+------------+------+---------------+-----------+---------+-------+------+----------+--------+
| 1  | SIMPLE      | t_user | <null>     | ref  | union_idx     | union_idx | 75      | const | 1    | 100.0    | <null> |
+----+-------------+--------+------------+------+---------------+-----------+---------+-------+------+----------+--------+

  • table=t_user:显示这一行的数据是关于哪张表的。
  • type=ref:这列最重要,显示了连接使用了哪种类别,有无使用索引,是使用 expalin 命令分析瓶颈的关键项之一。

结果值从好到坏依次是: system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL 也就是说 type 记录了是否使用了索引还是全表扫描,const, eg_reg, ref, range, index, ALL 一般来说,得保证查询至少达到range级别,最好能达到ref,否则就可能会出现性能问题。

  • possible_key=union_idx:表示可以选择的索引是 union_idx 这个联合索引。

  • key=union_idx:表示实际选择的索引是 union_idx

  • key_len=75:MySQL 决定使用的键长度,在不损失精确性的情况下,长度越短越好。

  • ref=const:显示使用哪个列或常数与key一起从表中选择行。

  • rows=1:表示需要读取的记录数

  • filtered:

  • extra=NULL:包含MySQL解决查询的详细信息,也是关键参考项之一,显示了查询中MySQL的附加信息,关注 Using filesort 和 Using temporary。


组合索引

组合索引的多个字段是有序的,遵循左对齐的原则。比如我们创建的组合索引,排序的方式是 id_no、username、age。因此、筛选的条件也需要遵守从左到右,如果中断,那么断点后的条件就没有办法利用索引了。比如 id_no=1001 AND username=TOM1 AND age=11

如果只用组合索引的一部分,那效果就没有单字段索引的效果那么好了。


本节参考:15 个必知的MySQL索引失效的场景,别再踩坑了


触发器

实际的场景中,经常会遇到这样的情况,有2个或者多个相互关联的表,如商品信息和库存信息是分别存放在2个不同的数据表中,当新加入一条商品记录的时候,为了保证数据的完整性,必须同时在库存表中添加一条库存记录。

这样就需要维护两个一起触发的操作。很容易漏掉其中一步导致数据的缺失。

这个时候,可以创建一个触发器,让商品信息数据的插入操作自动触发库存数据的插入操作。这样一来,就不需要担心忘记添加库存数据而导致数据丢失。

1
2
3
4
5
6
7
8
9
-- 创建触发器的语法结构是
CREATE TRIGGER 触发器名称 {BEFORE|AFTER} {INSERT|UPDATE|DELETE}
ON 表名 FOR EACH ROW 表达式;

-- 查看触发器的语句是:
SHOW TRIGGERS\G;

-- 删除触发器的语法结构是:
DROP TRIGGER 触发器名称;
  • 表名:表示触发器监控的对象
  • INSERT|UPDATE|DELETE:表示触发的事件。INSERT 表示插入记录时触发;UPDATE 表示更新记录时触发;DELETE 表示删除记录时触发。
  • BEFORE|AFTER:表示触发的时间。BEFORE 表示在事件之前触发;AFTER 表示在事件之后触发。

实际小案例:超市会员的会员卡余额变动的明细,我们改变会员卡余额时总是忘去记录这笔交易,这个操作其实可以写到触发器中。

  1. 查询出编号为 2 的会员卡的储蓄金额为 200.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10

mysql> SELECT memberdeposit
-> FROM demo.membermaster
-> WHERE memberid = 2;
+---------------+
| memberdeposit |
+---------------+
| 200.00 |
+---------------+
1 row in set (0.00 sec)
  1. 该会员的储蓄值减去150
1
2
3
4
5
mysql> UPDATE demo.membermaster
-> SET memberdeposit = memberdeposit - 150
-> WHERE memberid = 2;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
  1. 读出会员编号2当前的生于储蓄金额
 1
 2
 3
 4
 5
 6
 7
 8
 9
10

mysql> SELECT memberdeposit
-> FROM demo.membermaster
-> WHERE memberid = 2;
+---------------+
| memberdeposit |
+---------------+
| 50.00 |
+---------------+
1 row in set (0.00 sec)
  1. 把会员编号和前面查询中获得的储蓄值的起始金额、储值余额、金额变化,写入该会员储蓄值历史表中
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
mysql> INSERT INTO demo.deposithist
-> (
-> memberid,
-> transdate,
-> oldvalue,
-> newvalue,
-> changedvalue
-> )
-> SELECT 2,NOW(),200,50,-150;
Query OK, 1 row affected (0.02 sec)
Records: 1 Duplicates: 0 Warnings: 0
  1. 这样,我们就完成了记录会员储蓄值金额的变动操作。
1
2
3
4
5
6
7
8
mysql> SELECT *
-> FROM demo.deposithist;
+----+----------+---------------------+----------+----------+--------------+
| id | memberid | transdate | oldvalue | newvalue | changedvalue |
+----+----------+---------------------+----------+----------+--------------+
| 1 | 2 | 2020-12-20 10:37:51 | 200.00 | 50.00 | -150.00 |
+----+----------+---------------------+----------+----------+--------------+
1 row in set (0.00 sec)

其实这4个独立的操作语句就可以改成触发器的模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

DELIMITER //
CREATE TRIGGER demo.upd_membermaster BEFORE UPDATE  -- 在更新前触发
ON demo.membermaster
FOR EACH ROW                              -- 表示每更新一条记录,触发一次
BEGIN                                     -- 开始程序体
IF (new.memberdeposit <> old.memberdeposit)  -- 如果储值金额有变化
THEN
INSERT INTO demo.deposithist
(
memberid,
transdate,
oldvalue,
newvalue,
changedvalue
)
SELECT
NEW.memberid,
NOW(),
OLD.memberdeposit,                  -- 更新前的储值金额
NEW.memberdeposit,                  -- 更新后的储值金额
NEW.memberdeposit-OLD.memberdeposit; -- 储值金额变化值
END IF;
END
//
DELIMITER ;