MySQL 基础
这篇笔记把 MySQL 入门阶段需要掌握的内容串成一条线:先分清数据库的类型和 SQL 语句的分工,再用一个 school 示例库走完建库建表、增删改查、外键与索引、多表联查,最后收尾于视图、函数和存储过程。
Windows 下的安装过程可以参考之前的文章。
一、数据库分类
按数据组织方式,数据库大致分两类:
- 关系型数据库:MySQL、SQL Server、MariaDB、Oracle 等,数据以二维表的形式存放,表之间通过键关联。
- 非关系型数据库(NoSQL):MongoDB、Redis 等,不强制固定表结构。
非关系型数据库内部还可以细分为四种常见形态:
- 文档型
- key-value 型
- 列式数据库
- 图形数据库




本文之后的内容都围绕关系型数据库中的 MySQL 展开。
二、连接与使用 MySQL
客户端连接
1 | # 连接到 MySQL |
进入客户端后,语句结尾的符号决定结果的排版方式:
\g等价于分号,结果水平显示;\G结果垂直显示,字段很多的时候比表格好读。
修改当前用户密码:
1 | ALTER USER `root`@`localhost` IDENTIFIED BY 'password'; |
准备一份示例数据
练习时用真实数据集比手工插入几行更有意义,可以直接导入官方示例库 employees:
1 | # 示例数据下载地址 https://codeload.github.com/datacharmer/test_db/zip/master |
SQL 语句的四类分工
理解语句的分类,比死记语法更重要:
- DDL(数据定义语言):
CREATE/ALTER/DROP/TRUNCATE,操作的是数据库对象(库和表)本身。 - DML(数据操作语言):
INSERT/UPDATE/DELETE/SELECT,操作的是表里的数据。前三个也称为写,SELECT称为读。 - TCL(事务控制语句):
COMMIT/ROLLBACK,管理数据库中的事务。 - DCL(数据控制语句):
GRANT/REVOKE,控制数据的访问权限。
常用变量与路径
1 | SHOW VARIABLES LIKE '%datadir%'; -- 数据目录(配置文件里的 datadir) |
数据文件所在的目录可以直接在系统层面确认:
1 | sudo ls -lhtr /usr/local/mysql/data/ |
创建用户与授权
除了 localhost 上的管理任务,一般不推荐用 root 账号连接数据库执行业务语句。给应用单独建一个只拥有必要权限的账号:
1 | CREATE USER 'app'@'%' IDENTIFIED BY 'password'; |
三、库与表的管理(DDL)
层级关系是一条主线,后面所有操作都在这条线上:
数据库服务器 → 数据库 → 表(由列定义) → 行
一个数据库是许多表的集合,一台数据库服务器又可以容纳许多这样的数据库。
创建与切换数据库

1 | -- 新建数据库,库名和表名建议统一用反引号包裹 |
创建表


SQL 里 -- 之后的内容是注释,不会被执行。下面建一张学生表:
1 | CREATE TABLE `students`( |
关于主键:PRIMARY KEY 是用来唯一定位记录的特殊索引,建议不要使用任何业务相关的字段作为主键。主键也可以事后单独添加:
1 | ALTER TABLE `students` ADD CONSTRAINT pk_id PRIMARY KEY(`id`); |
联合主键语法上支持,但实践中最好不要使用:
1 | CREATE TABLE mytable ( |
建表时显式指定存储引擎和字符集是个好习惯:
1 | CREATE TABLE IF NOT EXISTS `company`.`customers`( |
查看与克隆表结构
1 | SHOW TABLES; -- 查看当前库的所有表 |
修改表结构
1 | -- 在 students 表中,把 class id 加到 id 的后一列 |
这里的列名带空格,所以每次引用都必须用反引号包住。真实项目里更推荐 class_id 这样的写法,本文为了和后面的联查示例输出保持一致才沿用原名。
删除列用 DROP COLUMN:
1 | ALTER TABLE `students` DROP COLUMN `class id`; |
删除与清空
1 | TRUNCATE TABLE `customers`; -- 清空所有行最快的方式,属 DDL,无法通过日志恢复 |
四、数据的增删改查(DML)
插入数据

1 | -- 按列顺序整行插入,now() 取 MySQL 当前时间 |
查询数据
SELECT 各子句的书写顺序是固定的,不能颠倒:

1 | -- * 表示所有列,生产环境应尽量避免 |
字符串比较默认不区分大小写,加上 BINARY 可以强制区分:
1 | SELECT * FROM `students` WHERE BINARY `name` = 'imwl'; |
修改数据

WHERE 在这里非常重要,漏写就是改动整张表的数据:
1 | -- 危险:把所有人的性别都改成女 |
删除数据

1 | -- 删除 students 表中性别为女的数据 |
五、分组与聚合
WHERE 过滤行、GROUP BY 分组、HAVING 过滤分组结果、ORDER BY 排序、LIMIT 截断,顺序不能乱:
1 | -- WHERE 先过滤行,GROUP BY 按性别分组,HAVING 过滤分组结果,最后排序、截断 |

写这类语句时有两个容易踩的坑:
- 分组列要选得有意义。按主键
GROUP BY id等于每组只有一行,分组就白做了;HAVING后面也该写聚合条件(COUNT(*) > 1这种),直接写HAVING in_time是把 DATETIME 当布尔用,实际效果只是把in_time为NULL和零值的行滤掉,并不是在“过滤分组结果”。 - 没有
GROUP BY的聚合查询只有一行结果,再ORDER BY id已经没有意义,而且 5.7.5 起only_full_group_by默认开启,id既不在分组里也不是聚合函数,会直接报ERROR 1140;同理这时LIMIT 0,8也是多余的。
六、外键与表间关系
外键
在 students 表中,class id 这一列的值指向另一张表(class)的记录,这样的列称为外键。
先建出班级表:
1 | CREATE TABLE `class`( |
再把外键约束加上:
1 | ALTER TABLE `students` |
注意:删除外键约束并没有删除外键这一列,删掉列要用前面提到的 DROP COLUMN。
一对多、多对多与一对一
- 一对多:一个班级对应多个学生,也就是上面
students与class的关系。 - 多对多:通过一张中间表把两侧的主键都记下来,就定义出了多对多关系。
- 一对一:一个表的记录对应另一个表唯一的一条记录。
一对一有一个很实用的场景:把大表拆成两张一对一的表,让经常读取和不经常读取的字段分开。例如把用户表拆成用户基本信息表 user_info 和用户详细信息表 user_profiles,大部分请求只查 user_info,查询速度自然更快。
七、索引
想在查找记录时获得很快的速度,就需要索引。
创建与删除索引
1 | -- 名称为 sex search,建立在 sex 列上 |
索引的效率取决于索引列的值是否足够离散。像 sex 列,大约一半记录是男、一半是女,对它建索引基本没有意义。
唯一索引与唯一约束
如果 name 不会重复,可以建唯一索引,它既能加速查询,又能保证该列的值唯一:
1 | ALTER TABLE `students` ADD UNIQUE INDEX `uk_name`(`name`); |
索引名不能和已有的重名。上面创建组合索引时用掉了 search 这个名字,而删除时只删了 sex search,search 还在,所以这里换成 uk_name,否则会报 Duplicate key name 'search'。
如果只想要唯一性约束而不关心索引名,可以直接加约束:
1 | ALTER TABLE `students` ADD CONSTRAINT uni_name UNIQUE (`name`); |
约束要加在真正不该重复的列上。像 is_vaild 这种只有 0/1/NULL 的标志位就不能加唯一约束——那等于规定全表最多只有一行 is_vaild = 1。
索引小结
- 通过对表创建索引,可以提高查询速度;
- 通过创建唯一索引,可以保证某一列的值具有唯一性;
- 索引对于用户和应用程序来说都是透明的,SQL 不需要为了用索引而改写。
八、多表联查
笛卡尔积与投影查询



不加连接条件的多表查询会得到 M × N 行记录(M、N 为两个表各自的行数),结果集可能非常巨大,要小心使用。
内连接 INNER JOIN
1 | SELECT s.id, s.name, `s`.`class id`, s.nickname, s.sex, c.name, s.in_time, s.is_vaild |
1 | +----+---------+----------+-----------+------+--------------+---------------------+----------+ |
INNER JOIN 的写法可以拆成四步:
- 先确定主表,仍然使用
FROM <表1>的语法; - 再确定需要连接的表,使用
INNER JOIN <表2>; - 然后确定连接条件,使用
ON <条件...>,上例的条件是s.class id = c.id,表示students表的class id列与class表的id列相同的行需要连接; - 可选:再加上
WHERE子句、ORDER BY等子句。
外连接:LEFT / RIGHT / FULL
把查询写成这样:
1 | SELECT ... FROM tableA ??? JOIN tableB ON tableA.column1 = tableB.column2; |
把 tableA 看作左表、tableB 看作右表,??? 处填不同的关键字,结果集的范围就不同。
INNER JOIN 只选出两张表都存在的记录:

LEFT OUTER JOIN 选出左表存在的记录:

RIGHT OUTER JOIN 选出右表存在的记录:

FULL OUTER JOIN 则是选出左右表中出现过的全部记录:

不过 MySQL 到 8.0 / 8.4 都还不支持 FULL OUTER JOIN 这个语法,照着写会直接报语法错误——那是 SQL Server、Oracle、PostgreSQL 的写法。在 MySQL 里要用 LEFT JOIN 和 RIGHT JOIN 各查一遍再 UNION 起来模拟(UNION 会自动去重,正好去掉两边都命中的那部分):
1 | SELECT ... FROM tableA LEFT JOIN tableB ON tableA.column1 = tableB.column2 |
联查小结
JOIN查询需要先确定主表,然后把另一个表的数据“附加”到结果集上;INNER JOIN是最常用的一种,语法是SELECT ... FROM <表1> INNER JOIN <表2> ON <条件...>;JOIN查询仍然可以使用WHERE条件和ORDER BY排序。
九、视图、函数与存储过程
视图
视图是一条被起了名字的查询,用起来像表,但不真正存储数据。下面基于一张 player 表建立视图 player_above_avg_height,筛出身高高于平均值的球员:
1 | CREATE VIEW player_above_avg_height AS |
之后查询视图,效果等同于执行它背后的那条 SQL:
1 | SELECT * FROM player_above_avg_height; |
视图的价值在于把复杂查询收敛成一个名字,让上层 SQL 更易读。
函数
MySQL 自带大量内置函数,字符串、数值、日期、聚合都有:

内置函数不够用时可以自定义函数。函数必须有返回值,可以直接嵌在 SELECT 等表达式里调用:
1 | CREATE FUNCTION `add_num`(n INT) RETURNS INT |
1 | SELECT add_num(100); -- 5050 |
存储过程
存储过程同样把一段逻辑存在服务端,但它没有返回值,通过 IN / OUT 参数传递数据,并且要用 CALL 调用:
1 | CREATE PROCEDURE `add_num`(IN n INT) |
1 | CALL add_num(100); |
一句话区分:要一个值就写函数,要一段流程就写存储过程。