MySQL 与 MariaDB 基础用法
本文合并自:03-MariaDB用法.md、2-2-MariaDB-MySQL用法.md、2-5-MySQL索引事务与备份恢复.md(约束与表设计部分)
适用版本:MariaDB 5.5 / 10.x,MySQL 5.5 ~ 5.7 / 8.0(CentOS / RHEL 6、7 系列) 覆盖内容:数据库基本概念、客户端连接、库表管理(DDL)、约束与表设计、数据操作与查询(DML / DQL)、用户与权限、导入导出、配置文件与常见排错。
范围说明(交叉引用)
- 安装部署(YUM / 二进制 / 源码安装、
mysql_secure_installation安全初始化、服务启停、数据目录迁移)详见11-02-MySQL安装部署.md。 - 系统结构(information_schema / performance_schema)与多表连接查询详见
11-03。 - 索引与事务详见
11-04。 - 备份恢复详解详见
11-05(本文只讲基础mysqldump用法)。 - 主从复制详见
11-06。
一、数据库基础与连接
1.1 数据库的基本概念
| 概念 | 说明 |
|---|---|
| 数据库(Database) | 按一定数据结构组织、存储和管理数据的仓库。在 MySQL 中一个数据库对应数据目录下的一个子目录 |
| 表(Table) | 数据库中存储数据的基本单位,是一张二维表格。在 MySQL 中一张表对应目录中的一个或多个文件 |
| 行(Row / 记录 / Record) | 表中的一条完整数据,横向的一条记录 |
| 列(Column / 字段 / Field) | 表中的一个属性,纵向的一列,每列有固定的数据类型 |
| 主键(Primary Key) | 唯一标识一行记录的列(或列组合),不允许重复、不允许为 NULL,一张表最多一个主键 |
| 外键(Foreign Key) | 一个表中的列引用另一个表的主键,用于建立表与表之间的关联关系,保证参照完整性 |
| 索引(Index) | 为加速查询而建立的数据结构,详见 11-04 |
| 视图(View) | 基于查询结果的虚拟表 |
| SQL | Structured Query Language,结构化查询语言,操作关系型数据库的标准语言 |
关系型数据库(RDBMS)
关系数据库建立在关系模型基础上,借助集合代数等数学方法处理数据。现实世界中的实体及实体之间的联系均用关系模型表示。关系模型由埃德加·科德(E.F.Codd)于 1970 年提出,并配合"科德十二定律"。SQL 就是基于关系数据库的标准查询语言。
关系模型即二维表格模型,一个关系型数据库由若干二维表以及表之间的联系组成。
关系型数据库的特点与优势:
- 数据以二维表的形式组织,结构清晰;
- 保持数据一致性(事务处理) —— 这是关系型数据库的最大优势,库中所有表都可以关联,且数据拥有一致性;
- 以标准化为前提,数据更新开销小(相同字段基本只存放一处);
- 可以进行 JOIN、子查询等复杂查询;
- 使用 SQL 作为统一的数据操作语言。
非关系型数据库(NoSQL)
不使用二维表模型,常见形态有键值型(Redis)、文档型(MongoDB)、列存储(HBase)、图数据库(Neo4j)。特点是结构灵活、水平扩展能力强,但通常不保证强一致性、不支持复杂 JOIN。
1.2 常见数据库对比
| 数据库 | 类型 | 说明 |
|---|---|---|
| MySQL | 关系型 | 开源数据库市场占有率第一 |
| MariaDB | 关系型 | MySQL 的衍生版本 / 分支,技术与 MySQL 相同,兼容 MySQL 协议与语法 |
| Oracle | 关系型 | 商业数据库市场占有率第一 |
| DB2 | 关系型 | IBM 商业数据库 |
| SQL Server(MSSQL) | 关系型 | 微软商业数据库 |
| Sybase | 关系型 | — |
| PostgreSQL | 关系型 | 功能强大的开源对象关系型数据库 |
| Access | 关系型 | 微软桌面数据库 |
| SQLite | 关系型 | 轻量级嵌入式数据库 |
| Informix | 关系型 | — |
| Visual FoxPro | 关系型 | 早期桌面数据库 |
| MongoDB / Redis | 非关系型 | NoSQL 数据库 |
| NoSQL | 非关系型 | 非关系型数据库的统称 |
MySQL / MariaDB 的特点
| 特性 | 说明 |
|---|---|
| 性能 | 将最大精力用于提高性能,注重查询与写入性能 |
| 可靠性 | 广泛用于 Web 2.0 网站与互联网业务 |
| 易用性 | 简单易用,部署与维护简单 |
| 可编程支持性 | 支持存储过程、触发器等;Oracle 使用 PL/SQL,SQL Server 使用 T-SQL,PostgreSQL 使用 PL/pgSQL |
数据库连接技术
| 技术 | 全称 | 提出方 |
|---|---|---|
| ODBC | Open Database Connectivity / Connector | 微软 |
| JDBC | Java Database Connectivity / Connector | Sun |
1.3 MySQL 与 MariaDB 的关系与分支历史
- MySQL 由 MySQL AB 公司开发,2008 年被 Sun 收购,2010 年 Oracle 收购 Sun 后 MySQL 归属 Oracle。
- MySQL 创始人 Monty Widenius 因担心 MySQL 闭源,基于 MySQL 源码创建了分支 MariaDB(名字取自其女儿 Maria),保持 GPL 开源。
- MariaDB 5.5 与 MySQL 5.5 在语法、协议、数据文件层面高度兼容;MariaDB 10.x 之后独立演进,加入了自己的特性(如 Aria 引擎、并行复制等),与 MySQL 5.7 / 8.0 的差异逐渐变大。
| 对比项 | MySQL | MariaDB |
|---|---|---|
| 服务名(CentOS 7) | mysqld |
mariadb |
| 默认包名 | mysql-server |
mariadb-server |
| CentOS 7 默认自带 | 否(默认提供 MariaDB) | 是 |
| CentOS 6 默认自带 | 是(mysql-server 5.1) |
否(需第三方源) |
| 密码字段(5.6 及之前 / MariaDB) | mysql.user.password |
mysql.user.password |
| 密码字段(MySQL 5.7+) | mysql.user.authentication_string |
MariaDB 10.4+ 使用 mysql.global_priv |
| 修改密码语法 | 5.7+ 推荐 ALTER USER |
支持 SET PASSWORD、ALTER USER(10.2+) |
| 默认字符集 | 8.0 起为 utf8mb4 |
10.6 起为 utf8mb4 |
| 客户端命令 | mysql、mysqldump、mysqladmin |
同名命令(MariaDB 10.5+ 另提供 mariadb、mariadb-dump 等别名) |
本文中凡未特别标注的语法,MySQL 与 MariaDB 通用。
CentOS 6 与 CentOS 7 的操作差异
| 操作 | CentOS 6 | CentOS 7 |
|---|---|---|
| 启动服务 | service mysqld start |
systemctl start mariadb |
| 重启服务 | service mysqld restart |
systemctl restart mariadb |
| 停止服务 | service mysqld stop |
systemctl stop mariadb |
| 开机自启 | chkconfig mysqld on |
systemctl enable mariadb |
| 查看状态 | service mysqld status |
systemctl status mariadb |
| 默认数据库 | MySQL 5.1 | MariaDB 5.5 |
| 查看监听端口 | netstat -ntlup \| grep 3306 |
ss -ntlup \| grep 3306(netstat 需装 net-tools) |
1.4 客户端连接
# 本地登录,明文密码(不推荐,会记入 history 且被 ps 看到)
mysql -u root -p123456
# -u 指定用户,-p 指定密码,-p 与密码之间不能有空格
# 交互输入密码(推荐),回车后隐式输入
mysql -u root -p
mysql -p
# 指定主机与端口,远程连接
mysql -u aa -h 10.0.0.61 -p123 -P 3307
# 指定 socket 文件(本地连接)
mysql -S /var/lib/mysql/mysql.sock -u root -p
# 登录后直接进入指定库
mysql -u root -p -D mydb
# 不进入交互式,直接执行 SQL 并返回结果
mysql -u root -p123456 -e "show databases;"
mysql -u root -p123456 mydb -e "select * from t1;"
常用选项一览
| 选项 | 全称 | 说明 |
|---|---|---|
-u |
--user |
指定数据库用户名 |
-p |
--password |
指定密码,紧跟密码无空格;单独 -p 则交互输入 |
-h |
--host |
指定服务器地址,默认 localhost |
-P |
--port |
指定端口,默认 3306(注意大写 P) |
-D |
--database |
指定连接后默认使用的库 |
-e |
--execute |
执行 SQL 后立即退出,适合脚本 |
-S |
--socket |
指定 socket 文件路径 |
-A |
--no-auto-rehash |
不预读表信息,登录更快 |
-E |
--vertical |
所有结果纵向显示(等价于每条语句加 \G) |
-N |
--skip-column-names |
不输出列名,便于脚本处理 |
-s |
--silent |
精简输出,用 Tab 分隔 |
-f |
--force |
出错继续执行后续语句 |
--default-character-set |
— | 指定客户端字符集,如 utf8mb4 |
socket 连接与 TCP/IP 连接的区别
| 方式 | 说明 |
|---|---|
| socket | mysql.sock 是 Unix 沿用的本地连接机制,走文件系统不走网络协议栈,速度较快 |
| TCP/IP | 用于远程连接,也可用于本地(使用本地 IP 127.0.0.1),相对稳定 |
- 当
-h省略或写localhost时,客户端使用 socket 连接; - 当
-h 127.0.0.1时,客户端使用 TCP/IP 连接; - 从优化角度,本地连接优先使用 socket(便于备份、性能更好)。
误删 socket 文件后本地登录会报错:
rm /var/lib/mysql/mysql.sock
mysql
ERROR 2002 (HY000): Can't connect to local MySQL server through
socket '/var/lib/mysql/mysql.sock' (2)
但 TCP/IP 仍然可以连接:
mysql -h 127.0.0.1
解决方法:重启 mariadb / mysqld 服务,socket 文件会重新生成。
pid 文件和 socket 文件都是在服务启动成功后才会生成。
1.5 退出、提示符与客户端内置命令
退出方式
exit -- 退出客户端
quit -- 等价于 exit
\q -- 等价于 exit
也可以按 Ctrl + D 退出(Ctrl + C 只中断当前语句,不退出客户端)。
提示符与常见符号含义
| 提示符 | 含义 |
|---|---|
mysql> |
等待输入新的 SQL 语句 |
-> |
语句还没有以 ; 结束,等待继续输入 |
'> |
有一个未闭合的单引号字符串 |
"> |
有一个未闭合的双引号字符串 |
`> |
有一个未闭合的反引号(标识符引用) |
/*> |
有一个未闭合的 /* */ 注释 |
遇到 ->、'> 等提示符卡住时,可输入 \c 取消当前语句,或补全引号后加 ; 结束。
客户端常用内置命令
| 命令 | 简写 | 说明 |
|---|---|---|
\G |
— | 将结果纵向显示(一行一个字段),代替语句末尾的 ; |
\c |
— | 取消当前正在输入的语句 |
\s |
status |
显示服务器状态摘要(版本、连接 id、当前库、字符集、socket、运行时间等) |
\h |
help |
显示帮助 |
\u 库名 |
use 库名 |
切换数据库 |
\T 文件 |
tee 文件 |
将会话输出记录到文件 |
\t |
notee |
停止记录输出 |
\. 文件 |
source 文件 |
执行 SQL 脚本文件 |
\! 命令 |
system 命令 |
执行 shell 命令 |
\p |
print |
打印当前输入的语句 |
\e |
edit |
用编辑器编辑当前语句 |
\r |
connect |
重新连接服务器 |
\W / \w |
warnings / nowarning |
每条语句后显示 / 不显示警告 |
-- 纵向显示表内容,字段多时更易读
SELECT * FROM table_name\G
-- 查看服务器状态摘要
\s
status
-- 在客户端里执行系统命令
\! ls -l /var/lib/mysql
1.6 批处理执行 SQL 文件
方式一:在客户端内部使用 source(或 \.):
source /root/init.sql;
\. /root/init.sql
方式二:在 shell 中重定向输入:
# 执行脚本
mysql -u root -p123456 < /root/init.sql
# 指定库执行脚本(脚本中没有 use 语句时)
mysql -u root -p123456 mydb < /root/init.sql
# 执行并把结果输出到文件
mysql -u root -p123456 mydb < query.sql > result.txt
# 出错继续执行
mysql -u root -p123456 -f mydb < /root/init.sql
区别:
source在已建立的会话中执行,路径相对于客户端所在机器;mysql < file是 shell 重定向,适合写在脚本、计划任务中。
1.7 SQL 语句分类
| 分类 | 全称 | 作用 | 常用语句 |
|---|---|---|---|
| DDL | Data Definition Language 数据定义语言 | 定义与修改库表结构 | CREATE、DROP、ALTER、TRUNCATE、RENAME |
| DML | Data Manipulation Language 数据操作语言 | 增删改表中数据 | INSERT、UPDATE、DELETE、REPLACE |
| DQL | Data Query Language 数据查询语言 | 查询数据 | SELECT、SHOW |
| DCL | Data Control Language 数据控制语言 | 用户与权限、事务控制 | GRANT、REVOKE、CREATE USER、COMMIT、ROLLBACK |
部分教材把
COMMIT/ROLLBACK/SAVEPOINT单独归为 TCL(事务控制语言),详见11-04。
1.8 SQL 书写规范
- 每条语句以分号
;结尾(\G结尾时不再需要;); - 关键字大小写不敏感,习惯上关键字大写、库表列名小写,便于阅读;
- 库名、表名的大小写敏感性取决于操作系统:Linux 下默认区分大小写,Windows 下不区分。可用
lower_case_table_names=1统一转小写存储; - 列名、别名不区分大小写;字符串内容是否区分大小写取决于校对集(
_ci结尾为不区分); - 一条语句可以跨多行输入,客户端以
;判断结束,未结束时提示符变为->; - 遇到与关键字冲突的库表列名,用反引号包裹,如
`order`; - 字符串与日期值必须用单引号(推荐)或双引号包裹,数字不加引号;
- 注释写法:
-- 单行注释(两个减号后必须有一个空格)
# 单行注释(MySQL 特有)
/* 多行
注释 */
SELECT 1 /*! MySQL 特有的内联注释,会被执行 */;
- 建议一行一个子句、缩进对齐,长 SQL 便于维护:
SELECT id,
name,
age
FROM student
WHERE age > 18
AND sex = 'M'
ORDER BY age DESC
LIMIT 10;
二、库与表的管理(DDL)
2.1 查看、创建与删除数据库
-- 查看所有数据库(两种写法等价)
SHOW DATABASES;
SHOW SCHEMAS;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| test |
+--------------------+
| 系统库 | 说明 |
|---|---|
information_schema |
元数据字典库(虚拟库),保存库、表、列、权限等信息,详见 11-03 |
mysql |
系统核心库,保存用户、权限、时区、计划任务等 |
performance_schema |
性能监控库(MySQL 5.5+ / MariaDB 5.5+) |
sys |
MySQL 5.7+ 提供的性能视图库 |
test |
早期版本自带的测试库,安全初始化时建议删除 |
-- 模糊查看数据库
SHOW DATABASES LIKE 'my%';
-- 创建数据库
CREATE DATABASE mydb;
-- 创建时判断是否已存在(推荐,避免脚本报错)
CREATE DATABASE IF NOT EXISTS mydb;
-- 创建时指定字符集与校对集(推荐)
CREATE DATABASE IF NOT EXISTS mydb
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_general_ci;
-- MariaDB 10.x / MySQL 8.0 也可写作
CREATE DATABASE mydb CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 查看建库语句
SHOW CREATE DATABASE mydb;
-- 修改数据库字符集
ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
-- 进入(切换)数据库,类似 cd
USE mydb;
USE information_schema;
-- 查看当前所在库,类似 pwd;未选库时返回 NULL
SELECT DATABASE();
-- 删除数据库(危险操作,不可恢复)
DROP DATABASE mydb;
DROP DATABASE IF EXISTS mydb;
一个数据库对应
/var/lib/mysql下的一个目录,数据库中的表对应目录中的文件。因此库名、表名要符合文件名规范,且删库等于删目录。
# 数据目录结构(RPM 安装默认 /var/lib/mysql/)
ls /var/lib/mysql/
| 文件/目录 | 说明 |
|---|---|
ibdata1 |
InnoDB 引擎共享表空间数据文件,information_schema 部分信息在此 |
ib_logfile0 / ib_logfile1 |
InnoDB redo 日志文件 |
mysql.sock |
本地连接使用的 socket 文件 |
mysql/ |
mysql 系统库目录 |
test/ |
test 库目录 |
库名/表名.frm |
表结构定义文件(MySQL 8.0 前) |
库名/表名.ibd |
InnoDB 独立表空间数据文件 |
库名/表名.MYD / .MYI |
MyISAM 的数据文件与索引文件 |
2.2 数据类型详解
2.2.1 整型
| 类型 | 字节 | 有符号范围 | 无符号(UNSIGNED)范围 |
|---|---|---|---|
TINYINT |
1 | -128 ~ 127 | 0 ~ 255 |
SMALLINT |
2 | -32768 ~ 32767 | 0 ~ 65535 |
MEDIUMINT |
3 | -8388608 ~ 8388607 | 0 ~ 16777215 |
INT / INTEGER |
4 | -2147483648 ~ 2147483647 | 0 ~ 4294967295 |
BIGINT |
8 | -2^63 ~ 2^63-1 | 0 ~ 2^64-1 |
-- UNSIGNED 表示无符号(不存负数),范围上限翻倍
CREATE TABLE t_int (
id INT UNSIGNED,
age TINYINT UNSIGNED,
flag TINYINT(1) -- 常用于布尔值,0 假 1 真;BOOL/BOOLEAN 是它的别名
);
INT(1)中的数字是显示宽度,不是存储长度;INT(1)依然占 4 字节,能存 2147483647。- 显示宽度只在配合
ZEROFILL(不足位数补零)时有意义,如INT(5) ZEROFILL存 12 显示00012。 - MySQL 8.0.17 起整型的显示宽度已被标记为废弃。
2.2.2 浮点与定点
| 类型 | 字节 | 说明 |
|---|---|---|
FLOAT(M,D) |
4 | 单精度浮点,约 7 位有效数字,存在精度误差 |
DOUBLE(M,D) |
8 | 双精度浮点,约 15 位有效数字,仍有精度误差 |
DECIMAL(M,D) / NUMERIC |
变长 | 定点数,以字符串形式精确存储,无精度误差 |
M为总位数(精度),D为小数位数(标度),M最大 65,D最大 30。DECIMAL(10,2)表示总共 10 位、其中 2 位小数,可存-99999999.99 ~ 99999999.99。
CREATE TABLE t_num (
price DECIMAL(10,2), -- 金额必须用 DECIMAL
weight FLOAT(7,3),
rate DOUBLE
);
-- 浮点误差示例
SELECT 0.1 + 0.2 = 0.3; -- FLOAT/DOUBLE 计算可能返回 0
SELECT CAST(0.1 AS DECIMAL(10,2));
类型选择建议:涉及金额、库存、计量等要求精确的场景必须用
DECIMAL;科学计算、允许误差的场景才用FLOAT/DOUBLE。
2.2.3 字符串
| 类型 | 最大长度 | 说明 |
|---|---|---|
CHAR(M) |
255 字符 | 定长字符串,不足部分用空格填充(取出时去掉尾部空格),M 省略时为 1 |
VARCHAR(M) |
65535 字节(行总限制) | 变长字符串,额外用 1~2 字节记录长度 |
TINYTEXT |
255 字节 | 短文本 |
TEXT |
65535 字节(约 64KB) | 长文本,不能有默认值,索引需指定前缀长度 |
MEDIUMTEXT |
16MB | 中等长文本 |
LONGTEXT |
4GB | 超长文本 |
TINYBLOB / BLOB / MEDIUMBLOB / LONGBLOB |
同上对应长度 | 二进制大对象,区分大小写、无字符集概念,用于存图片、文件 |
BINARY(M) / VARBINARY(M) |
同 CHAR/VARCHAR | 定长/变长二进制串 |
CHAR 与 VARCHAR 的区别
| 对比项 | CHAR | VARCHAR |
|---|---|---|
| 存储方式 | 定长,按 M 个字符固定分配 | 变长,按实际长度 + 长度前缀 |
| 空间 | 短内容浪费空间 | 更省空间 |
| 性能 | 定长检索更快,无碎片 | 变更长度时可能产生碎片 |
| 适用 | 长度固定的值:手机号、MD5、性别、身份证 | 长度不固定的值:姓名、地址、标题 |
| 尾部空格 | 取出时被截断 | 原样保留 |
VARCHAR(M)中 M 在 MySQL 4.1 之后表示字符数;一行所有列总长度不能超过 65535 字节,utf8mb4 下每字符最多 4 字节,所以单列上限约 16383 字符。- 超过 65535 字节的文本请用
TEXT系列;TEXT/BLOB存储在行外,不能设置默认值。
2.2.4 日期与时间
| 类型 | 字节 | 格式 | 范围 |
|---|---|---|---|
DATE |
3 | YYYY-MM-DD |
1000-01-01 ~ 9999-12-31 |
TIME |
3 | HH:MM:SS |
-838:59:59 ~ 838:59:59 |
DATETIME |
8 | YYYY-MM-DD HH:MM:SS |
1000-01-01 00:00:00 ~ 9999-12-31 23:59:59 |
TIMESTAMP |
4 | YYYY-MM-DD HH:MM:SS |
1970-01-01 00:00:01 UTC ~ 2038-01-19 03:14:07 UTC |
YEAR |
1 | YYYY |
1901 ~ 2155 |
DATETIME 与 TIMESTAMP 的差异
| 对比项 | DATETIME | TIMESTAMP |
|---|---|---|
| 占用空间 | 8 字节(5.6+ 为 5 字节 + 小数位) | 4 字节 |
| 时区 | 与时区无关,存什么读什么 | 存储时转 UTC,读取时按当前时区转换 |
| 范围 | 1000 ~ 9999 年 | 1970 ~ 2038 年(存在 2038 问题) |
| 自动更新 | MySQL 5.6+ / MariaDB 10.0+ 支持 | 一直支持 |
| NULL 处理 | 允许 NULL | 传统模式下 NULL 会被替换为当前时间 |
CREATE TABLE t_time (
d DATE,
t TIME,
dt DATETIME DEFAULT CURRENT_TIMESTAMP,
ct TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 插入时自动填当前时间
ut TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更新时自动刷新
y YEAR
);
INSERT INTO t_time (d, t, y) VALUES ('2026-08-22', '13:20:00', 2026);
-- 也支持无分隔符写法
INSERT INTO t_time (d, t, y) VALUES ('20260822', '132000', '2026');
- 一张表中可以有多个自动更新的
TIMESTAMP/DATETIME列(MySQL 5.6.5 之后)。 - 支持小数秒:
DATETIME(3)表示毫秒精度,最大(6)微秒。
2.2.5 枚举与集合
| 类型 | 说明 |
|---|---|
ENUM('a','b','c') |
单选,最多 65535 个成员,实际存储为 1~2 字节的整数下标(从 1 开始) |
SET('a','b','c') |
多选,最多 64 个成员,按位存储,1~8 字节 |
CREATE TABLE t_enum (
sex ENUM('M','F','U') NOT NULL DEFAULT 'U',
hobby SET('read','game','sport','music')
);
INSERT INTO t_enum (sex, hobby) VALUES ('M', 'read,game');
INSERT INTO t_enum (sex, hobby) VALUES ('F', 'music');
-- 按下标查询/插入
SELECT sex + 0 FROM t_enum; -- 查看枚举对应的数字
SELECT * FROM t_enum WHERE FIND_IN_SET('read', hobby);
注意:
ENUM的值与下标容易混淆(WHERE sex = 1会匹配第一个成员),并且新增成员必须ALTER TABLE,扩展性差。工程上更推荐用TINYINT+ 应用层字典,或单独建字典表。
2.2.6 类型选择建议
- 能用小类型不用大类型:年龄用
TINYINT UNSIGNED,状态用TINYINT; - 主键自增 ID 用
INT UNSIGNED,预计超过 42 亿行用BIGINT UNSIGNED; - 金额一律
DECIMAL,禁止FLOAT/DOUBLE; - 定长用
CHAR,不定长用VARCHAR,超长文本用TEXT; - 尽量
NOT NULL并给DEFAULT,NULL 会让索引与统计变复杂; - 图片、附件建议存文件系统或对象存储,库里只存路径,避免
BLOB膨胀; - 时间统一用
DATETIME(跨时区业务)或TIMESTAMP(需要时区自适应且不超 2038 年)。
2.3 创建表
完整语法
CREATE TABLE [IF NOT EXISTS] [库名.]表名 (
列名 数据类型 [列约束] [COMMENT '注释'],
...
[PRIMARY KEY (列名, ...)],
[UNIQUE KEY 索引名 (列名, ...)],
[KEY 索引名 (列名, ...)],
[CONSTRAINT 约束名 FOREIGN KEY (列名) REFERENCES 父表(列名)
[ON DELETE 动作] [ON UPDATE 动作]]
) [ENGINE=引擎] [DEFAULT CHARSET=字符集] [COLLATE=校对集]
[AUTO_INCREMENT=起始值] [COMMENT='表注释'];
最简示例(源文档示例)
CREATE DATABASE a;
SHOW DATABASES;
USE a;
CREATE TABLE a1(id INT(1), name CHAR(10));
INSERT INTO a1(id, name) VALUES(1, "测试库");
INSERT INTO a1(id, name) VALUES(2, "mysql库");
SELECT * FROM a1;
规范示例
CREATE TABLE IF NOT EXISTS student (
id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '学号,主键',
name VARCHAR(20) NOT NULL COMMENT '姓名',
sex ENUM('M','F') NOT NULL DEFAULT 'M' COMMENT '性别',
age TINYINT UNSIGNED DEFAULT 18 COMMENT '年龄',
email VARCHAR(50) DEFAULT NULL COMMENT '邮箱',
phone CHAR(11) DEFAULT NULL COMMENT '手机号',
score DECIMAL(5,2) DEFAULT 0.00 COMMENT '总分',
dept_id INT UNSIGNED DEFAULT NULL COMMENT '所属院系',
created DATETIME DEFAULT CURRENT_TIMESTAMP,
updated DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_phone (phone),
KEY idx_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
AUTO_INCREMENT=1 COMMENT='学生表';
列约束一览
| 约束 | 说明 |
|---|---|
NOT NULL |
该列不允许为 NULL |
NULL |
允许为 NULL(默认) |
DEFAULT 值 |
默认值,插入时未指定则使用;TEXT/BLOB 不支持 |
AUTO_INCREMENT |
自增,一张表只能有一个,且必须是索引列(通常是主键),只能用于整型 |
UNIQUE / UNIQUE KEY |
唯一约束,值不可重复,允许多个 NULL |
PRIMARY KEY |
主键,唯一 + 非空,一张表只能有一个 |
COMMENT '文本' |
列注释 |
UNSIGNED |
无符号(数值列) |
ZEROFILL |
不足显示宽度补零(数值列) |
CHECK (条件) |
检查约束,MySQL 8.0.16+ / MariaDB 10.2+ 真正生效,之前版本语法通过但不检查 |
-- 列级写法
CREATE TABLE t1 (
id INT PRIMARY KEY AUTO_INCREMENT,
code VARCHAR(20) UNIQUE NOT NULL,
price DECIMAL(10,2) NOT NULL DEFAULT 0.00 CHECK (price >= 0)
);
-- 自增相关
SHOW CREATE TABLE t1; -- 查看当前 AUTO_INCREMENT 值
ALTER TABLE t1 AUTO_INCREMENT = 1000; -- 重设自增起点
复合主键(联合主键)
-- 多列共同唯一标识一行,常用于中间关联表
CREATE TABLE student_course (
student_id INT UNSIGNED NOT NULL,
course_id INT UNSIGNED NOT NULL,
score DECIMAL(5,2) DEFAULT NULL,
PRIMARY KEY (student_id, course_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
复合主键中不能使用
AUTO_INCREMENT(InnoDB 下自增列必须是索引的最左列)。
外键 FOREIGN KEY 与级联动作
-- 父表
CREATE TABLE dept (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(30) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 子表,建表时定义外键
CREATE TABLE emp (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) NOT NULL,
dept_id INT UNSIGNED,
CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES dept(id)
ON DELETE SET NULL
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 建表后追加外键
ALTER TABLE emp
ADD CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES dept(id);
-- 删除外键(用约束名,不是列名)
ALTER TABLE emp DROP FOREIGN KEY fk_emp_dept;
| 级联动作 | 含义 |
|---|---|
RESTRICT |
默认。父表记录被引用时禁止删除/更新 |
NO ACTION |
与 RESTRICT 等效(MySQL 中即时检查) |
CASCADE |
父表删除/更新时,子表对应记录一起删除/更新 |
SET NULL |
父表删除/更新时,子表外键列置为 NULL(该列必须允许 NULL) |
SET DEFAULT |
InnoDB 不支持,语法可解析但不生效 |
外键使用前提与注意:
- 只有 InnoDB 支持外键,MyISAM 会忽略外键定义;
- 父子列的数据类型、长度、符号必须一致,父列必须是主键或唯一索引;
- 外键列会自动创建索引;
- 高并发系统常在应用层维护关联,不建物理外键,以减少锁与死锁;
- 临时关闭外键检查(导入数据时常用):
SET FOREIGN_KEY_CHECKS = 0;
-- 执行导入
SET FOREIGN_KEY_CHECKS = 1;
2.4 约束体系总览
约束(Constraint)是加在表或列上的规则,由数据库自身保证数据合法,比在应用代码里校验更可靠。一个库通常有多个写入入口(Web 应用、后台脚本、手工 SQL、数据导入),只有写在表结构里的规则才对所有入口一视同仁。
2.4.1 六类约束
| 约束 | 关键字 | 作用 | 一表可有几个 |
|---|---|---|---|
| 主键 | PRIMARY KEY |
唯一 + 非空,行的唯一标识 | 1 个(可为多列组成的复合主键) |
| 外键 | FOREIGN KEY |
引用另一张表的主键/唯一键,保证参照完整性 | 多个 |
| 唯一 | UNIQUE |
值不能重复,但允许多个 NULL |
多个 |
| 非空 | NOT NULL |
该列不允许 NULL |
多个 |
| 默认值 | DEFAULT |
插入时未指定则使用默认值 | 多个 |
| 检查 | CHECK |
值必须满足布尔表达式 | 多个 |
要点:
- 主键自动创建聚簇索引(InnoDB),
UNIQUE自动创建唯一索引,因此约束和索引是同一件事的两面,索引部分详见11-04。 UNIQUE列允许多行为NULL,因为 SQL 中NULL != NULL;主键不允许NULL。CHECK约束的版本陷阱:- MySQL 5.7 及之前:语法可以解析,但被静默忽略,完全不生效。
- MySQL 8.0.16 及之后:真正生效。
- MariaDB 10.2 及之后:真正生效。
- 所以在 MySQL 5.7 上写
CHECK只是自我安慰,需要校验请用触发器或应用层;在 MariaDB 10.2+ 上可以放心用。
2.4.2 建表时集中定义约束(完整示例)
下面这套 shop 库的 dept / emp 两张表把六类约束用了一遍,本节及后续几节的实验都基于它:
CREATE DATABASE IF NOT EXISTS shop DEFAULT CHARSET=utf8;
USE shop;
-- 部门表(父表)
CREATE TABLE dept (
deptno TINYINT NOT NULL,
dname VARCHAR(30) NOT NULL,
loc VARCHAR(30) DEFAULT 'Beijing',
PRIMARY KEY (deptno),
UNIQUE KEY uk_dname (dname)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 员工表(子表)
CREATE TABLE emp (
empno INT NOT NULL AUTO_INCREMENT,
ename VARCHAR(20) NOT NULL,
sex CHAR(1) NOT NULL DEFAULT 'm',
email VARCHAR(50) DEFAULT NULL,
hiredate DATE NOT NULL,
sal DECIMAL(10,2) NOT NULL DEFAULT 0.00,
deptno TINYINT DEFAULT NULL,
PRIMARY KEY (empno),
UNIQUE KEY uk_email (email),
KEY idx_deptno (deptno),
CONSTRAINT fk_emp_dept FOREIGN KEY (deptno)
REFERENCES dept (deptno)
ON DELETE SET NULL
ON UPDATE CASCADE,
CONSTRAINT ck_sal CHECK (sal >= 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;
读法:列定义部分只写列级属性(NOT NULL、DEFAULT、AUTO_INCREMENT),把主键、唯一键、普通索引、外键、CHECK 集中写在列定义之后,这样约束一目了然,也方便统一命名。
2.4.3 约束与索引的命名规范
命名约定团队统一后,能极大降低维护成本(看名字就知道对象类型,报错信息里的名字也能立刻定位):
| 对象 | 前缀 | 示例 |
|---|---|---|
| 主键 | 无(固定名 PRIMARY) |
— |
| 唯一索引 | uk_ |
uk_email |
| 普通索引 | idx_ |
idx_deptno |
| 联合索引 | idx_ + 列序 |
idx_deptno_sal |
| 外键 | fk_ |
fk_emp_dept |
| CHECK | ck_ |
ck_sal |
主键的名字在 MySQL / MariaDB 中固定为
PRIMARY,写CONSTRAINT pk_xxx PRIMARY KEY (...)时自定义名会被忽略。
2.5 约束的追加、删除与查看
2.5.1 建表后追加约束
-- 追加主键
ALTER TABLE emp ADD PRIMARY KEY (empno);
-- 追加唯一约束
ALTER TABLE emp ADD CONSTRAINT uk_email UNIQUE (email);
-- 等价写法
ALTER TABLE emp ADD UNIQUE KEY uk_email (email);
-- 追加非空(NOT NULL 只能通过 MODIFY,没有 ADD CONSTRAINT 写法)
ALTER TABLE emp MODIFY ename VARCHAR(20) NOT NULL;
-- 追加默认值 / 删除默认值
ALTER TABLE emp ALTER COLUMN sex SET DEFAULT 'm';
ALTER TABLE emp ALTER COLUMN sex DROP DEFAULT;
-- 追加外键
ALTER TABLE emp ADD CONSTRAINT fk_emp_dept
FOREIGN KEY (deptno) REFERENCES dept (deptno)
ON DELETE SET NULL ON UPDATE CASCADE;
-- 追加 CHECK(MySQL 8.0.16+ / MariaDB 10.2+ 才生效)
ALTER TABLE emp ADD CONSTRAINT ck_sal CHECK (sal >= 0);
追加
NOT NULL前必须先把已有的NULL值补掉(UPDATE emp SET ename='' WHERE ename IS NULL),否则严格模式下MODIFY会直接报错;非严格模式下会被静默转成空串或 0,更危险。同理,追加唯一约束前表中不能已有重复值,追加外键前子表里不能有父表中不存在的值。
2.5.2 复合唯一约束
多列组合唯一,单列本身可以重复,常用于"同一部门内姓名不可重复"这类业务规则:
ALTER TABLE emp ADD CONSTRAINT uk_dept_ename UNIQUE (deptno, ename);
-- 允许:同名但不同部门
INSERT INTO emp (ename,hiredate,sal,deptno) VALUES ('lisi','2020-01-01',5000,20);
INSERT INTO emp (ename,hiredate,sal,deptno) VALUES ('lisi','2020-01-01',5000,30);
-- 拒绝:部门 + 姓名完全相同
INSERT INTO emp (ename,hiredate,sal,deptno) VALUES ('lisi','2021-01-01',6000,20);
-- ERROR 1062 (23000): Duplicate entry '20-lisi' for key 'uk_dept_ename'
复合唯一约束的两个陷阱:
- 含
NULL的组合不判重:(NULL,'lisi')可以插入任意多次,因为只要有一列为NULL,唯一性判断结果就是"未知"而不是"重复"。要真正防重,参与列必须NOT NULL。 - 复合唯一索引同时也是一个联合索引,遵循最左前缀原则:
uk_dept_ename (deptno, ename)可以加速WHERE deptno=?与WHERE deptno=? AND ename=?,但加速不了单独的WHERE ename=?。列顺序要按查询习惯排。
2.5.3 删除约束
ALTER TABLE emp DROP PRIMARY KEY; -- 有自增列时会报错,需先去掉 AUTO_INCREMENT
ALTER TABLE emp DROP INDEX uk_email; -- 删唯一约束 = 删唯一索引
ALTER TABLE emp DROP FOREIGN KEY fk_emp_dept;-- 注意关键字是 FOREIGN KEY 而非 CONSTRAINT
ALTER TABLE emp DROP CHECK ck_sal; -- MySQL 8.0.16+
ALTER TABLE emp DROP CONSTRAINT ck_sal; -- MariaDB 10.2+
| 操作 | 语法要点 | 版本差异 |
|---|---|---|
| 删主键 | DROP PRIMARY KEY,不需要写名字 |
自增列必须先 MODIFY 去掉 AUTO_INCREMENT |
| 删唯一 | DROP INDEX 索引名 |
通用 |
| 删外键 | DROP FOREIGN KEY 约束名 |
通用;删外键不会删掉外键列上自动建的索引 |
| 删 CHECK | MySQL 用 DROP CHECK,MariaDB 用 DROP CONSTRAINT |
MySQL 8.0.16+ / MariaDB 10.2+;MySQL 8.0.19+ 也支持 DROP CONSTRAINT |
| 删非空 | MODIFY 列 类型 NULL |
通用,没有 DROP 写法 |
2.5.4 查看已有约束
SHOW CREATE TABLE emp\G
SELECT constraint_name, constraint_type
FROM information_schema.table_constraints
WHERE table_schema='shop' AND table_name='emp';
-- 只看外键的引用关系
SELECT constraint_name, table_name, column_name,
referenced_table_name, referenced_column_name
FROM information_schema.key_column_usage
WHERE table_schema='shop' AND referenced_table_name IS NOT NULL;
information_schema.table_constraints的constraint_type取值为PRIMARY KEY、UNIQUE、FOREIGN KEY、CHECK。NOT NULL与DEFAULT不在这张表里,它们属于列属性,要查information_schema.columns的IS_NULLABLE与COLUMN_DEFAULT。MySQL 8.0.16+ 另有information_schema.check_constraints专门存放CHECK表达式。
2.6 约束验证实验与常见报错编号
2.6.1 实验数据
沿用 2.4.2 建好的 shop.dept 与 shop.emp,先灌入一份小数据集,后面所有实验结果都以此为准:
INSERT INTO dept VALUES
(10,'ACCOUNTING','Beijing'),
(20,'RESEARCH','Shanghai'),
(30,'SALES','Guangzhou'),
(40,'OPERATIONS','Shenzhen'); -- 40 号部门没有员工
INSERT INTO emp (ename,sex,email,hiredate,sal,deptno) VALUES
('boss', 'm','boss@x.com', '1995-01-01',20000,10),
('zhangsan','m','zs@x.com', '1998-04-11',15000,20),
('lisi', 'f','ls@x.com', '2001-11-21',13000,20),
('wangwu', 'm','ww@x.com', '2005-06-01', 9000,30),
('zhaoliu', 'f','zl@x.com', '2010-03-15', 7000,NULL); -- 该员工没有部门
此时 dept 有 4 行(含无人的 40 号部门),emp 有 5 行(含无部门的 zhaoliu)。
2.6.2 逐条触发约束
-- 实验 1:违反主键唯一性
INSERT INTO dept VALUES (10,'HR','Wuhan');
-- ERROR 1062 (23000): Duplicate entry '10' for key 'PRIMARY'
-- 实验 2:违反唯一约束 uk_dname
INSERT INTO dept VALUES (50,'ACCOUNTING','Wuhan');
-- ERROR 1062 (23000): Duplicate entry 'ACCOUNTING' for key 'uk_dname'
-- 实验 3:违反 NOT NULL
INSERT INTO emp (ename,hiredate,deptno) VALUES (NULL,'2020-01-01',10);
-- ERROR 1048 (23000): Column 'ename' cannot be null
-- 实验 4:DEFAULT 生效(不写 sex 与 sal,取默认 'm' 与 0.00)
INSERT INTO emp (ename,hiredate,deptno) VALUES ('newbie','2020-01-01',10);
SELECT ename, sex, sal FROM emp WHERE ename='newbie';
-- newbie | m | 0.00
-- 实验 5:UNIQUE 允许多个 NULL(email 为 NULL 的行可以插入多条)
INSERT INTO emp (ename,email,hiredate,deptno) VALUES ('n1',NULL,'2020-02-01',10);
INSERT INTO emp (ename,email,hiredate,deptno) VALUES ('n2',NULL,'2020-02-01',10);
-- 两条都成功,因为 NULL != NULL
-- 实验 6:违反外键(父表没有 99 号部门)
INSERT INTO emp (ename,hiredate,deptno) VALUES ('ghost','2020-01-01',99);
-- ERROR 1452 (23000): Cannot add or update a child row: a foreign key
-- constraint fails (`shop`.`emp`, CONSTRAINT `fk_emp_dept` FOREIGN KEY
-- (`deptno`) REFERENCES `dept` (`deptno`) ON DELETE SET NULL ON UPDATE CASCADE)
-- 实验 7:ON DELETE SET NULL 生效(删部门,员工保留、deptno 置 NULL)
DELETE FROM dept WHERE deptno=30;
SELECT ename, deptno FROM emp WHERE ename='wangwu';
-- wangwu | NULL
-- 实验 8:ON UPDATE CASCADE 生效(改父键,子行跟着改)
UPDATE dept SET deptno=21 WHERE deptno=20;
SELECT ename, deptno FROM emp WHERE ename IN ('zhangsan','lisi');
-- zhangsan | 21 lisi | 21
-- 实验 9:违反 CHECK
INSERT INTO emp (ename,hiredate,sal,deptno) VALUES ('badsal','2020-01-01',-1,10);
-- MySQL 8.0.16+ : ERROR 3819 (HY000): Check constraint 'ck_sal' is violated.
-- MariaDB 10.2+ : ERROR 4025 (23000): CONSTRAINT `ck_sal` failed for `shop`.`emp`
-- MySQL 5.7 及之前:插入成功,CHECK 被静默忽略(务必亲手验证一次)
如果把外键改成默认的 RESTRICT(即不写 ON DELETE 子句),实验 7 会变成拒绝删除:
DELETE FROM dept WHERE deptno=10;
-- ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key
-- constraint fails (`shop`.`emp`, CONSTRAINT `fk_emp_dept` FOREIGN KEY
-- (`deptno`) REFERENCES `dept` (`deptno`))
2.6.3 报错编号对照表
| 编号 | 报错关键字 | 含义 | 处理方向 |
|---|---|---|---|
1062 |
Duplicate entry 'x' for key 'k' |
违反主键或唯一约束 | 报错里的 key 名就是被撞的索引名;用 INSERT IGNORE / REPLACE / ON DUPLICATE KEY UPDATE 按业务语义处理 |
1048 |
Column 'c' cannot be null |
违反 NOT NULL |
补默认值,或检查应用是否漏传字段 |
1364 |
Field 'c' doesn't have a default value |
列既 NOT NULL 又无默认值,而 INSERT 没给值 |
给列加 DEFAULT,或插入时显式赋值 |
1452 |
Cannot add or update a child row |
子表写入了父表中不存在的值 | 先插父表,或检查外键列取值 |
1451 |
Cannot delete or update a parent row |
父行被子行引用,RESTRICT 拒绝操作 |
先删子行,或改用 CASCADE / SET NULL |
3819 |
Check constraint 'ck' is violated(MySQL 8.0.16+) |
违反 CHECK |
修正数据或表达式 |
4025 |
CONSTRAINT 'ck' failed(MariaDB 10.2+) |
违反 CHECK |
同上 |
1005 + errno: 150 |
Can't create table ... (errno: 150) |
外键定义不合法(MySQL 5.5 / MariaDB 常见形态) | 见 2.7.1 的五条前提逐项排查 |
1215 |
Cannot add foreign key constraint |
同上(MySQL 5.6/5.7 的报法) | SHOW ENGINE INNODB STATUS\G 的 LATEST FOREIGN KEY ERROR 段有详细原因 |
3780 |
Referencing column and referenced column are incompatible(MySQL 8.0) |
外键两侧类型/字符集不一致 | 统一类型、长度、符号、字符集 |
SQLSTATE
23000是"完整性约束冲突"这一大类的统一状态码,应用层可以按它做统一捕获,再用具体错误号细分。
2.7 外键约束深入
2.3 已给出外键的基本写法,本节补齐建不出外键时的排查依据与参照动作的选型。
2.7.1 外键成立的硬性前提
以下五条缺一条就建不出外键,报 errno: 150(MySQL 5.6+ 常表现为 1215 Cannot add foreign key constraint):
- 父表与子表都必须是 InnoDB(MyISAM 解析外键语法但完全忽略)。
- 父表被引用的列必须是主键或唯一索引。
- 子表的外键列必须有索引(若没写,MySQL 会自动补建一个)。
- 两边列的数据类型、长度、有无符号必须一致(
INT对INT UNSIGNED也会失败)。 - 字符列的字符集与排序规则必须一致。
排查手段:
-- 查看最近一次外键错误的详细原因(最有效的排查方法)
SHOW ENGINE INNODB STATUS\G
-- 输出中找 LATEST FOREIGN KEY ERROR 段落
-- 核对两边列定义是否完全一致
SELECT table_name, column_name, column_type, character_set_name, collation_name
FROM information_schema.columns
WHERE table_schema='shop' AND column_name='deptno';
# 命令行下也可直接过滤
mysql -uroot -p -e "SHOW ENGINE INNODB STATUS\G" | grep -A 20 "LATEST FOREIGN KEY ERROR"
2.7.2 参照动作选型
父表行被删除、或父表主键被修改时,子表怎么办:
| 动作 | ON DELETE 语义 |
ON UPDATE 语义 |
适用场景 |
|---|---|---|---|
RESTRICT |
父行有子行引用则拒绝删除 | 拒绝修改 | 默认行为,最安全 |
NO ACTION |
在 InnoDB 中等同于 RESTRICT |
同 | 兼容 SQL 标准的写法 |
CASCADE |
删父行则级联删除子行 | 父键改值则子行跟着改 | 强依赖数据(如订单 → 订单明细) |
SET NULL |
子行外键列置为 NULL |
同 | 弱依赖(如员工 → 部门,部门撤销员工保留) |
SET DEFAULT |
InnoDB 不支持,语法可解析但不生效 | 同 | 不要使用 |
-- 不写任何 ON 子句时,默认相当于 RESTRICT
CONSTRAINT fk_item_order FOREIGN KEY (order_id)
REFERENCES orders (id) ON DELETE CASCADE ON UPDATE RESTRICT
选型要点:
- 用
SET NULL时,子表的外键列必须允许NULL,否则建表就会失败。 CASCADE的删除是静默的、不触发子表触发器的,一条DELETE可能连带删掉大量数据,且不会在慢日志里体现为大事务的原因,生产上要慎用;多层级联(A→B→C)尤其危险。- 级联更新意味着父键值可变,这与"主键应当不可变"的设计原则冲突。使用自增或业务无关的代理主键时,
ON UPDATE基本可以永远用RESTRICT。
2.7.3 临时关闭外键检查
导入数据、批量删表、恢复备份时必备(备份恢复详见 11-05):
SET FOREIGN_KEY_CHECKS=0;
-- ... 执行导入 / 删表 ...
SET FOREIGN_KEY_CHECKS=1;
- 该变量是会话级的,
SET后只影响当前连接;要全局生效用SET GLOBAL FOREIGN_KEY_CHECKS=0,但重启后失效。 mysqldump导出的 SQL 文件头部会自动写入这两行,所以恢复时无需关心父子表的导入顺序。- 关闭期间写入的违规数据不会在开启后被自动校验,务必自行核对,否则会留下"指向不存在父行"的脏数据。
生产权衡:外键保证了数据正确,但也带来额外的锁与检查开销,并且分库分表后无法跨库生效。互联网高并发场景常见做法是不建物理外键,由应用层保证参照完整性;内部管理系统、数据量不大的库建议保留外键。无论选哪种,团队内必须统一。相关设计讨论详见
11-03的"外键约束的使用与取舍"。
2.8 AUTO_INCREMENT 自增列详解
2.8.1 使用规则
- 一张表只能有一个自增列,且该列必须是索引(通常就是主键)的第一列。
- 自增列必须是整数类型,且强烈建议
UNSIGNED(INT UNSIGNED上限约 42.9 亿,INT只有 21.4 亿)。 - 插入时写
NULL、0或省略该列,都会取下一个自增值。
-- 显式指定起始值
CREATE TABLE t_seq (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(20),
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=1000;
INSERT INTO t_seq (name) VALUES ('a'),('b'),('c'); -- id = 1000,1001,1002
-- 查看当前计数器与最后一次插入的自增值
SHOW CREATE TABLE t_seq\G
SELECT LAST_INSERT_ID();
LAST_INSERT_ID()返回的是当前连接最近一次成功插入产生的第一个自增值,与其他连接无关,因此并发下也是安全的。一次插入多行时,它返回的是这批行中的第一个 id。
2.8.2 自增值不回收
这是最常被误解的一点:
DELETE FROM t_seq WHERE id=1002;
INSERT INTO t_seq (name) VALUES ('d'); -- id = 1003,不是 1002
| 操作 | 自增计数器 |
|---|---|
DELETE FROM t 删光数据 |
不重置,继续往后发号 |
事务 ROLLBACK |
不回退,序号出现空洞 |
INSERT 唯一键冲突失败 |
也可能已消耗序号 |
TRUNCATE TABLE t |
重置为 1(本质是重建表) |
| MySQL 5.7 InnoDB 重启 | 计数器丢失,重启后取 MAX(id)+1 |
| MySQL 8.0 InnoDB | 计数器持久化到 redo log,重启后不回退 |
MySQL 5.7 与 8.0 的差异会带来一个真实故障:5.7 上删除了表中 id 最大的几行后重启实例,自增计数器会退回到 MAX(id)+1,如果这些 id 已经被下游系统或日志引用,就会出现"新数据复用了旧 ID"的问题。MySQL 8.0 与 MariaDB 10.2+ 的持久化行为消除了这个坑,但升级前的实例必须留意。
手动调整:
ALTER TABLE t_seq AUTO_INCREMENT=5000; -- 只能调大,调小到已有最大值以下无效
结论:自增 ID 必然有空洞,不要把它当作「记录条数」或「连续序号」使用。需要连续编号请在查询时用变量或窗口函数生成;需要条数请用
COUNT(*)。
2.8.3 自增列的容量与选型
| 类型 | UNSIGNED 上限 |
适用 |
|---|---|---|
TINYINT UNSIGNED |
255 | 字典表、状态表 |
SMALLINT UNSIGNED |
65535 | 小型配置表 |
INT UNSIGNED |
约 42.9 亿 | 绝大多数业务表 |
BIGINT UNSIGNED |
约 1.8×10^19 | 日志、流水、消息等只增不减的大表 |
自增列写满会报 ERROR 1062 (23000): Duplicate entry '4294967295' for key 'PRIMARY'(计数器停在上限,反复尝试同一个值),此时只能 ALTER TABLE ... MODIFY id BIGINT UNSIGNED AUTO_INCREMENT 扩容,而这在大表上是一次锁表重建,代价极高。流水类大表一开始就用 BIGINT UNSIGNED,不要省这 4 个字节。
2.9 生成列与表设计规范
2.9.1 生成列(Generated Column)
生成列的值由表达式算出,不能直接写入,适合把"由其他列推导出来的字段"固化到表结构里,避免应用各处重复计算。
| 类型 | 关键字 | 存储 | 特点 |
|---|---|---|---|
| 虚拟列 | VIRTUAL(默认) |
不占磁盘,读取时计算 | 加列是纯元数据操作,很快;可建索引(索引本身会落盘) |
| 存储列 | STORED(MariaDB 也写作 PERSISTENT) |
落盘 | 写入时计算,读取更快,但改定义要重建表 |
版本支持:MySQL 5.7.6+、MariaDB 5.2+(MariaDB 早期语法为 PERSISTENT / VIRTUAL,10.2+ 起兼容 MySQL 的 STORED 写法)。
CREATE TABLE t_order (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
qty INT UNSIGNED NOT NULL DEFAULT 0,
-- 虚拟列:总价随 price/qty 变化,不落盘
total DECIMAL(12,2) AS (price * qty) VIRTUAL,
email VARCHAR(50) DEFAULT NULL,
-- 存储列:把邮箱域名固化下来并建索引,便于按域名统计
domain VARCHAR(30) AS (SUBSTRING_INDEX(email,'@',-1)) STORED,
PRIMARY KEY (id),
KEY idx_domain (domain)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO t_order (price,qty,email) VALUES (19.90,3,'zs@example.com');
SELECT price, qty, total, domain FROM t_order;
-- 19.90 | 3 | 59.70 | example.com
-- 生成列不可写入
UPDATE t_order SET total=1;
-- ERROR 3105 (HY000): The value specified for generated column 'total' in table 't_order' is not allowed.
使用限制:
- 生成列不能有
DEFAULT,不能是AUTO_INCREMENT,表达式中不能引用其他表、变量、存储函数或不确定函数(如NOW()、RAND())。 - 表达式可以引用同表的其他列,但被引用列必须定义在它之前(MySQL 要求)。
- MySQL 5.7+ 支持给生成列建索引,从而实现"函数索引"效果;MySQL 8.0.13+ 可以直接写函数索引
KEY ((SUBSTRING_INDEX(email,'@',-1)))),无需显式生成列。 - 生成列常与
CHECK配合做复杂校验,也常用来给 JSON 列中的字段建索引。
2.9.2 表设计规范清单
| 原则 | 说明 |
|---|---|
| 必有主键 | 每张 InnoDB 表都要有主键,最好是自增 BIGINT UNSIGNED 或短整型 |
| 主键短且单调递增 | 主键是聚簇索引,过长会撑大所有二级索引;随机主键(如 UUID)造成页分裂 |
字段尽量 NOT NULL |
NULL 使索引统计、比较、聚合都变复杂,用 0/'' 加 DEFAULT 代替 |
| 选最小够用的类型 | 状态用 TINYINT 而非 INT;定长短串用 CHAR,变长用 VARCHAR |
金额禁用 FLOAT/DOUBLE |
必须用 DECIMAL(10,2),浮点有精度误差 |
| 字符集统一 | 全库统一 utf8mb4(需存 emoji)或 utf8,避免连接时字符集不一致导致索引失效 |
| 少用大字段 | TEXT/BLOB 拆到独立表,避免影响主表行密度 |
落地检查项(建表评审时逐条过):
- 表名、列名统一小写下划线风格,不用 MySQL 保留字(
order、desc、key等,必须用时要加反引号); - 每列都有
COMMENT,表有COMMENT,字典类列在注释里写明取值含义; - 每张表都有
created/updated时间列,便于排查与增量同步; - 引擎统一
InnoDB,字符集统一utf8mb4,校对集全库一致; - 约束与索引按 2.4.3 的前缀命名,索引数量控制在 5 个以内,避免写放大;
- 拆表与冗余的取舍(范式与反范式)详见
11-03的"多表关系与拆表设计"。
2.10 查看表结构
-- 查看当前库所有表
SHOW TABLES;
SHOW TABLES FROM mysql;
SHOW TABLES LIKE 'stu%';
-- 查看表结构(等价写法)
DESC student;
DESCRIBE student;
SHOW COLUMNS FROM student;
EXPLAIN student; -- 旧版本亦可用于描述表结构
-- 查看某列
DESC student name;
SHOW COLUMNS FROM student LIKE 'na%';
-- 查看建表语句(含引擎、字符集、索引、注释,最全)
SHOW CREATE TABLE student;
SHOW CREATE TABLE student\G
-- 查看表状态(行数、引擎、数据大小、自增值等)
SHOW TABLE STATUS LIKE 'student'\G
-- 查看表上的索引
SHOW INDEX FROM student;
DESC 输出字段含义:
| 列 | 说明 |
|---|---|
Field |
列名 |
Type |
数据类型 |
Null |
是否允许 NULL |
Key |
索引类型:PRI 主键、UNI 唯一、MUL 普通索引 |
Default |
默认值 |
Extra |
附加信息,如 auto_increment、on update CURRENT_TIMESTAMP |
2.11 修改表(ALTER TABLE)
-- 添加列(默认加到最后)
ALTER TABLE student ADD address VARCHAR(100);
ALTER TABLE student ADD COLUMN address VARCHAR(100) NOT NULL DEFAULT '' COMMENT '地址';
-- 添加到第一列 / 指定列之后
ALTER TABLE student ADD sn CHAR(10) FIRST;
ALTER TABLE student ADD nickname VARCHAR(20) AFTER name;
-- 一次添加多列
ALTER TABLE student
ADD col1 INT,
ADD col2 VARCHAR(10);
-- 删除列
ALTER TABLE student DROP address;
ALTER TABLE student DROP COLUMN col1, DROP COLUMN col2;
-- 修改列类型/属性(列名不变)
ALTER TABLE student MODIFY name VARCHAR(50) NOT NULL COMMENT '姓名';
-- 修改列名 + 类型(CHANGE 必须写完整新定义)
ALTER TABLE student CHANGE name stu_name VARCHAR(50) NOT NULL;
-- 只改列名(MySQL 8.0+ / MariaDB 10.5+)
ALTER TABLE student RENAME COLUMN stu_name TO name;
-- 修改默认值 / 删除默认值
ALTER TABLE student ALTER COLUMN age SET DEFAULT 20;
ALTER TABLE student ALTER COLUMN age DROP DEFAULT;
-- 修改表名(三种写法)
ALTER TABLE student RENAME TO stu;
ALTER TABLE stu RENAME student;
RENAME TABLE student TO stu, stu2 TO student2;
-- 添加与删除索引
ALTER TABLE student ADD INDEX idx_age (age);
ALTER TABLE student ADD UNIQUE KEY uk_email (email);
ALTER TABLE student DROP INDEX idx_age;
CREATE INDEX idx_age ON student (age);
DROP INDEX idx_age ON student;
-- 添加与删除主键
ALTER TABLE student ADD PRIMARY KEY (id);
ALTER TABLE student DROP PRIMARY KEY;
-- 修改存储引擎
ALTER TABLE student ENGINE=InnoDB;
-- 修改表字符集(只改表默认,不改已有列)
ALTER TABLE student DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
-- 修改表及所有字符列的字符集(推荐,修复乱码用)
ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
-- 修改表注释
ALTER TABLE student COMMENT='学生信息表';
MODIFY与CHANGE的区别:MODIFY只改列的定义,CHANGE可以同时改列名和定义。改列类型时若已有数据不兼容会报错或被截断。
2.12 删除与清空表
-- 删除表(结构 + 数据全部消失)
DROP TABLE student;
DROP TABLE IF EXISTS student, teacher;
-- 清空表数据(保留结构)
TRUNCATE TABLE student;
TRUNCATE student;
-- 逐行删除所有数据(保留结构,可回滚)
DELETE FROM student;
TRUNCATE 与 DELETE 的区别
| 对比项 | TRUNCATE | DELETE |
|---|---|---|
| 语句类型 | DDL | DML |
| 实现方式 | 直接删除并重建表文件 | 逐行删除并写 undo / binlog |
| 速度 | 快(大表尤其明显) | 慢 |
| 事务 | 隐式提交,不能回滚 | 可回滚(InnoDB + 事务) |
| WHERE 条件 | 不支持 | 支持 |
| 自增值 | 重置为初始值(如 1) | 不重置(MyISAM 不重置,InnoDB 5.7 及之前重启后重置) |
| 触发器 | 不触发 DELETE 触发器 | 触发 |
| 返回受影响行数 | 返回 0 | 返回实际删除行数 |
| 外键被引用时 | 报错,不允许 | 按级联规则处理 |
三者对比记忆:
DELETE删数据(可选条件、可回滚)、TRUNCATE清空表(快、不可回滚)、DROP删除表本身(结构也没了)。
2.13 字符集与校对集
基本概念
- 字符集(Character Set):字符与二进制编码的映射规则,如
latin1、gbk、utf8、utf8mb4。 - 校对集(Collation):字符集内字符的比较与排序规则,命名以字符集开头,后缀含义:
_ci:case insensitive,不区分大小写(最常用);_cs:case sensitive,区分大小写;_bin:按二进制比较,区分大小写与重音。
utf8 与 utf8mb4 的区别
| 对比项 | utf8(MySQL 的 utf8 = utf8mb3) | utf8mb4 |
|---|---|---|
| 每字符最大字节 | 3 | 4 |
| 支持 emoji / 部分生僻字 | 不支持,插入会报错 Incorrect string value |
支持 |
| 索引单列最大长度(InnoDB 767 字节前缀) | 255 字符 | 191 字符 |
| 推荐程度 | 遗留系统 | 新项目一律使用 utf8mb4 |
MySQL 中的
utf8是"阉割版"的 UTF-8,只支持 1~3 字节字符,无法存储 emoji(4 字节)。MySQL 8.0 起默认字符集为utf8mb4,并把utf8标记为utf8mb3的别名(已废弃)。MariaDB 10.6 起默认也是utf8mb4。 常用校对集:utf8mb4_general_ci(快、兼容老版本)、utf8mb4_unicode_ci(排序更准确)、utf8mb4_0900_ai_ci(MySQL 8.0 默认)、utf8mb4_uca1400_ai_ci(MariaDB 11 新增)。
三级(实为四级)字符集
优先级:列 > 表 > 库 > 服务器实例。未显式指定时向上继承。
-- 查看支持的字符集与校对集
SHOW CHARACTER SET;
SHOW CHARACTER SET LIKE 'utf8%';
SHOW COLLATION LIKE 'utf8mb4%';
-- 查看服务器与连接相关字符集
SHOW VARIABLES LIKE 'character_set%';
SHOW VARIABLES LIKE 'collation%';
+--------------------------+-----------------------------+
| Variable_name | Value |
+--------------------------+-----------------------------+
| character_set_client | utf8mb4 -- 客户端发来的编码 |
| character_set_connection | utf8mb4 -- 连接使用的编码 |
| character_set_database | utf8mb4 -- 当前库默认编码 |
| character_set_results | utf8mb4 -- 返回结果的编码 |
| character_set_server | utf8mb4 -- 服务器默认编码 |
| character_set_system | utf8 -- 元数据编码,固定 |
+--------------------------+-----------------------------+
-- 查看某库 / 某表 / 某列的字符集
SHOW CREATE DATABASE mydb;
SHOW CREATE TABLE student;
SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA='mydb' AND TABLE_NAME='student';
-- 修改
ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE student MODIFY name VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
-- 会话级设置(三个 client/connection/results 一次搞定)
SET NAMES utf8mb4;
-- 等价于
SET character_set_client = utf8mb4;
SET character_set_connection = utf8mb4;
SET character_set_results = utf8mb4;
中文乱码的完整解决流程
- 确认操作系统 / 终端编码:
echo $LANG,建议en_US.UTF-8或zh_CN.UTF-8; - 确认服务端字符集:
SHOW VARIABLES LIKE 'character_set%';,character_set_server应为utf8mb4; - 修改配置文件永久生效:
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci
init_connect='SET NAMES utf8mb4'
# MySQL 5.5 之前用 default-character-set(5.5+ 在 [mysqld] 中已废弃)
[client]
default-character-set=utf8mb4
[mysql]
default-character-set=utf8mb4
- 重启服务:
systemctl restart mariadb(CentOS 6:service mysqld restart); - 库、表、列三级都要是 utf8mb4:新建时显式指定,已有的用
ALTER DATABASE/ALTER TABLE ... CONVERT TO; - 应用连接串指定编码,例如 JDBC:
jdbc:mysql://host:3306/db?useUnicode=true&characterEncoding=utf8mb4; - 已经乱码的历史数据:先用正确编码导出(
mysqldump --default-character-set=原编码),转码后再导入,不能靠改配置自动修复; - 插入 emoji 报
Incorrect string value: '\xF0\x9F...'说明目标列还是utf8(mb3),需转utf8mb4。
三、数据操作与查询(DML 与 DQL)
以下示例统一使用如下测试数据:
CREATE DATABASE IF NOT EXISTS school DEFAULT CHARSET utf8mb4;
USE school;
CREATE TABLE student (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(20) NOT NULL,
sex ENUM('M','F') NOT NULL DEFAULT 'M',
age TINYINT UNSIGNED DEFAULT NULL,
score DECIMAL(5,2) DEFAULT NULL,
dept VARCHAR(20) DEFAULT NULL,
email VARCHAR(50) DEFAULT NULL,
created DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生表';
INSERT INTO student (name, sex, age, score, dept, email) VALUES
('张三', 'M', 20, 88.50, '计算机', 'zs@abc.com'),
('李四', 'M', 22, 76.00, '计算机', 'ls@abc.com'),
('王五', 'F', 21, 92.00, '数学', 'ww@abc.com'),
('赵六', 'F', 23, 65.50, '数学', NULL),
('孙七', 'M', 20, 79.00, '物理', 'sq@abc.com'),
('周八', 'F', 24, NULL, '物理', NULL);
3.1 插入数据(INSERT)
-- 指定列插入单行
INSERT INTO student (name, sex, age) VALUES ('钱九', 'M', 19);
-- 全列插入(必须按建表顺序给全部列的值)
INSERT INTO student VALUES (NULL, '吴十', 'F', 20, 85.00, '化学', 'ws@abc.com', NOW());
-- 一次插入多行(推荐,性能远高于多条单行 INSERT)
INSERT INTO student (name, sex, age, score, dept) VALUES
('郑一', 'M', 21, 70.00, '化学'),
('王二', 'F', 22, 81.00, '化学');
-- SET 语法(MySQL / MariaDB 特有,只能插一行)
INSERT INTO student SET name='冯三', sex='M', age=20;
-- 忽略错误继续插入(主键冲突时跳过该行,只给 warning)
INSERT IGNORE INTO student (id, name) VALUES (1, '重复行');
-- 源文档示例
INSERT INTO a1(id, name) VALUES(1, "测试库");
INSERT INTO a1(id, name) VALUES(2, "mysql库");
INSERT ... SELECT:从查询结果插入
-- 把计算机系的学生复制到备份表(表必须已存在,列数与类型对应)
CREATE TABLE student_bak LIKE student;
INSERT INTO student_bak SELECT * FROM student WHERE dept='计算机';
-- 指定列
INSERT INTO student_bak (name, age) SELECT name, age FROM student WHERE age > 21;
-- 建表并复制数据(结构由查询结果推导,不带索引与约束)
CREATE TABLE student_copy AS SELECT * FROM student;
REPLACE INTO:存在则先删再插
REPLACE INTO student (id, name, sex, age) VALUES (1, '张三改', 'M', 25);
- 依据主键或唯一索引判断冲突;冲突时先 DELETE 旧行再 INSERT 新行;
- 未指定的列会被重置为默认值(这是与
ON DUPLICATE KEY UPDATE最大的差别); - 会触发 DELETE + INSERT 触发器,自增 ID 会变化,慎用。
INSERT ... ON DUPLICATE KEY UPDATE:存在则更新(UPSERT)
INSERT INTO student (id, name, age, score)
VALUES (1, '张三', 20, 90.00)
ON DUPLICATE KEY UPDATE
age = VALUES(age),
score = VALUES(score),
name = '张三';
-- MySQL 8.0.20+ 推荐用别名代替已废弃的 VALUES()
INSERT INTO student (id, name, age) VALUES (1, '张三', 21) AS new
ON DUPLICATE KEY UPDATE age = new.age;
-- 常用于计数累加
INSERT INTO counter (k, v) VALUES ('pv', 1)
ON DUPLICATE KEY UPDATE v = v + 1;
返回受影响行数:新插入为 1,更新为 2,值未变化为 0。
3.2 更新数据(UPDATE)
-- 基本语法
UPDATE 表名 SET 列1=值1 [, 列2=值2 ...] [WHERE 条件] [ORDER BY ...] [LIMIT n];
-- 单列更新
UPDATE student SET age = 21 WHERE id = 1;
-- 多列更新(一条语句改多个字段,逗号分隔,不是 AND)
UPDATE student SET age = 22, score = 95.50, dept = '计算机' WHERE name = '张三';
-- 基于原值计算
UPDATE student SET score = score + 5 WHERE dept = '数学';
-- 配合 ORDER BY + LIMIT,只更新前 N 行
UPDATE student SET score = 60 WHERE score IS NULL ORDER BY id LIMIT 1;
-- 危险:不写 WHERE 会更新全表所有行!
UPDATE student SET score = 0;
WHERE 的重要性:
UPDATE与DELETE不写WHERE会作用于全表,且默认自动提交、无法撤销。生产环境的铁律:
- 先用同样的
WHERE条件执行SELECT确认命中的行数与内容;- 操作前备份(
mysqldump或CREATE TABLE bak AS SELECT);- 显式事务操作,确认后再
COMMIT:
SET autocommit = 0; -- 或 START TRANSACTION;
UPDATE student SET score = 0 WHERE dept = '物理';
SELECT * FROM student WHERE dept = '物理'; -- 确认
COMMIT; -- 或 ROLLBACK; 撤销
SET autocommit = 1;
安全模式 sql_safe_updates
-- 开启后,UPDATE/DELETE 若未使用索引列作为 WHERE 条件(或无 WHERE、无 LIMIT)将直接报错
SET sql_safe_updates = 1;
SET GLOBAL sql_safe_updates = 1;
报错信息:
ERROR 1175 (HY000): You are using safe update mode and you tried to update
a table without a WHERE that uses a KEY column
解决:补上基于索引列的 WHERE,或加 LIMIT,或临时 SET sql_safe_updates=0;。
客户端启动时也可加参数:mysql -u root -p --safe-updates(等价 -U)。
3.3 删除数据(DELETE)
-- 按条件删除
DELETE FROM student WHERE id = 10;
DELETE FROM student WHERE dept = '化学' AND score < 60;
-- 排序 + 限量删除(删除分数最低的 2 行)
DELETE FROM student ORDER BY score ASC LIMIT 2;
-- 清空表(逐行删除,可回滚,自增值不重置)
DELETE FROM student;
-- 清空表(DDL,快,不可回滚,自增值重置)
TRUNCATE TABLE student;
DELETE只删除数据,不释放表空间(InnoDB 需OPTIMIZE TABLE或重建表回收);- 删除被外键引用的父表记录会报 1451 错误,见排错章节。
3.4 查询数据(SELECT)
基本结构
SELECT [DISTINCT] 列名/表达式 [AS 别名], ...
FROM 表名 [AS 别名]
[WHERE 行过滤条件]
[GROUP BY 分组列]
[HAVING 分组后过滤条件]
[ORDER BY 排序列 [ASC|DESC]]
[LIMIT [offset,] count];
执行顺序(与书写顺序不同,非常重要)
FROM → ON/JOIN → WHERE → GROUP BY → 聚合函数 → HAVING → SELECT(列与别名) → DISTINCT → ORDER BY → LIMIT
因此:
WHERE里不能使用SELECT中定义的列别名,也不能使用聚合函数;HAVING和ORDER BY可以使用SELECT中的列别名和聚合结果。
基础查询与列别名
-- 查看表中所有数据
SELECT * FROM student;
-- 只查需要的列(生产环境避免 SELECT *)
SELECT id, name, score FROM student;
-- 列别名,AS 可省略;别名含空格或中文时用引号
SELECT name AS 姓名, score AS 分数 FROM student;
SELECT name 姓名, score * 1.1 AS '加权分数' FROM student;
-- 表别名
SELECT s.name, s.score FROM student AS s;
-- 查询常量、表达式、函数(不依赖表)
SELECT 1 + 2;
SELECT NOW(), VERSION(), DATABASE(), USER();
-- 纵向显示(字段多时更易读)
SELECT * FROM student\G
-- 源文档示例
SELECT * FROM a1;
SELECT DATABASE();
SELECT USER();
DISTINCT 去重
-- 单列去重
SELECT DISTINCT dept FROM student;
-- 多列去重:多列组合完全相同才算重复
SELECT DISTINCT dept, sex FROM student;
-- 统计不同院系数量
SELECT COUNT(DISTINCT dept) FROM student;
DISTINCT必须紧跟在SELECT之后,作用于其后的所有列,不能只对某一列去重。
3.5 WHERE 条件
比较运算符
| 运算符 | 说明 |
|---|---|
= |
等于(NULL 参与时结果为 NULL) |
<> 或 != |
不等于 |
> >= < <= |
大于、大于等于、小于、小于等于 |
<=> |
NULL 安全等于,NULL <=> NULL 返回 1 |
BETWEEN a AND b |
在区间内,包含两端 |
NOT BETWEEN a AND b |
不在区间内 |
IN (v1, v2, ...) |
匹配列表中任一值 |
NOT IN (...) |
不在列表中 |
LIKE / NOT LIKE |
模糊匹配 |
REGEXP / RLIKE |
正则匹配 |
IS NULL / IS NOT NULL |
判断是否为 NULL |
SELECT * FROM student WHERE age = 20;
SELECT * FROM student WHERE score > 80;
SELECT * FROM student WHERE dept <> '计算机';
SELECT * FROM student WHERE name = '张三';
-- 区间:以下两句等价
SELECT * FROM student WHERE score BETWEEN 70 AND 90;
SELECT * FROM student WHERE score >= 70 AND score <= 90;
-- 日期区间
SELECT * FROM student WHERE created BETWEEN '2026-01-01' AND '2026-12-31 23:59:59';
-- IN / NOT IN
SELECT * FROM student WHERE dept IN ('计算机', '数学');
SELECT * FROM student WHERE dept NOT IN ('计算机', '数学');
SELECT * FROM student WHERE id IN (1, 3, 5, 7);
注意:
NOT IN的列表中若含 NULL,整个条件永远不成立(返回空集)。
LIKE 与通配符
| 通配符 | 说明 |
|---|---|
% |
匹配任意个(包括 0 个)字符 |
_ |
匹配恰好一个字符 |
ESCAPE |
指定转义符,用于匹配字面量 % 或 _ |
SELECT * FROM student WHERE name LIKE '张%'; -- 姓张
SELECT * FROM student WHERE name LIKE '%三'; -- 以三结尾
SELECT * FROM student WHERE name LIKE '%三%'; -- 含三
SELECT * FROM student WHERE name LIKE '张_'; -- 张 + 恰好一个字
SELECT * FROM student WHERE email LIKE '%@abc.com';
SELECT * FROM student WHERE name NOT LIKE '张%';
-- 匹配包含下划线的内容,用 ESCAPE 转义
SELECT * FROM student WHERE name LIKE '%\_%';
SELECT * FROM student WHERE name LIKE '%#_%' ESCAPE '#';
-- 正则匹配:以张或李开头
SELECT * FROM student WHERE name REGEXP '^(张|李)';
SELECT * FROM student WHERE email RLIKE '^[a-z]+@abc\\.com$';
性能提示:
LIKE '张%'(前缀匹配)可以使用索引;LIKE '%三'与LIKE '%三%'无法使用普通索引,会全表扫描,大表慎用,可考虑全文索引。
NULL 的特殊性
-- 正确写法
SELECT * FROM student WHERE score IS NULL;
SELECT * FROM student WHERE score IS NOT NULL;
-- 错误写法:永远查不到结果(NULL 与任何值比较都返回 NULL)
SELECT * FROM student WHERE score = NULL;
SELECT * FROM student WHERE score != NULL;
-- NULL 安全比较
SELECT * FROM student WHERE score <=> NULL;
NULL 的其他影响:
NULL表示"未知",不是 0 也不是空字符串'';- 任何算术运算与 NULL 参与结果都是 NULL:
SELECT 1 + NULL;返回 NULL; - 字符串拼接:
CONCAT('a', NULL)返回 NULL,需用IFNULL/COALESCE处理; COUNT(列)不统计 NULL,COUNT(*)统计所有行;SUM/AVG忽略 NULL,AVG的分母是非 NULL 行数;ORDER BY时 NULL 默认排在最前(升序);GROUP BY会把所有 NULL 归为同一组;- 唯一索引允许多个 NULL 值。
逻辑运算符与优先级
| 运算符 | 说明 |
|---|---|
AND / && |
逻辑与 |
OR / \|\| |
逻辑或 |
NOT / ! |
逻辑非 |
XOR |
逻辑异或 |
优先级从高到低:括号 > NOT > 比较运算符 > AND > XOR > OR。
-- AND 优先级高于 OR,含义完全不同,务必加括号
SELECT * FROM student WHERE dept='计算机' OR dept='数学' AND score > 80;
SELECT * FROM student WHERE (dept='计算机' OR dept='数学') AND score > 80;
-- 组合条件
SELECT * FROM student
WHERE sex = 'F'
AND age BETWEEN 20 AND 23
AND score IS NOT NULL
AND dept IN ('数学','物理');
3.6 排序(ORDER BY)与限量(LIMIT)
-- 升序(ASC 为默认,可省略)
SELECT * FROM student ORDER BY score;
SELECT * FROM student ORDER BY score ASC;
-- 降序
SELECT * FROM student ORDER BY score DESC;
-- 多列排序:先按 dept 升序,dept 相同再按 score 降序
SELECT * FROM student ORDER BY dept ASC, score DESC;
-- 按别名 / 表达式 / 列序号排序
SELECT name, score * 2 AS s2 FROM student ORDER BY s2 DESC;
SELECT name, score FROM student ORDER BY 2 DESC; -- 按第 2 列,可读性差不推荐
-- 按函数结果排序
SELECT name FROM student ORDER BY CHAR_LENGTH(name) DESC;
-- 随机排序取一行(大表性能差)
SELECT * FROM student ORDER BY RAND() LIMIT 1;
-- 自定义顺序
SELECT * FROM student ORDER BY FIELD(dept, '物理','数学','计算机');
LIMIT 与分页
-- 取前 3 行
SELECT * FROM student LIMIT 3;
-- LIMIT offset, count:跳过 offset 行,取 count 行(offset 从 0 开始)
SELECT * FROM student LIMIT 0, 5; -- 第 1 页,每页 5 条
SELECT * FROM student LIMIT 5, 5; -- 第 2 页
SELECT * FROM student LIMIT 10, 5; -- 第 3 页
-- 等价写法
SELECT * FROM student LIMIT 5 OFFSET 10;
-- 分页公式:LIMIT (页码-1)*每页条数, 每页条数
-- 第 n 页,每页 m 条 → LIMIT (n-1)*m, m
-- 取分数最高的 3 名
SELECT name, score FROM student ORDER BY score DESC LIMIT 3;
-- 取分数第 2 高到第 4 高
SELECT name, score FROM student ORDER BY score DESC LIMIT 1, 3;
注意:不加
ORDER BY时LIMIT返回的行是不确定的。深分页(LIMIT 1000000, 10)性能很差,应改用"记住上次最大 id"的方式:WHERE id > 上次最大id ORDER BY id LIMIT 10。
3.7 分组(GROUP BY)与聚合函数
聚合函数
| 函数 | 说明 |
|---|---|
COUNT(*) |
统计行数(包含 NULL 行) |
COUNT(列) |
统计该列非 NULL 的行数 |
COUNT(DISTINCT 列) |
统计该列不同的非 NULL 值个数 |
SUM(列) |
求和,忽略 NULL;无数据时返回 NULL |
AVG(列) |
求平均,忽略 NULL(分母为非 NULL 行数) |
MAX(列) |
最大值,可用于数字、字符串、日期 |
MIN(列) |
最小值 |
GROUP_CONCAT(列) |
将组内值拼接成字符串(MySQL / MariaDB 特有) |
-- 全表聚合(不分组时整表视为一组)
SELECT COUNT(*) FROM student;
SELECT COUNT(*) AS 总人数, AVG(score) AS 平均分, MAX(score) AS 最高分,
MIN(score) AS 最低分, SUM(score) AS 总分
FROM student;
-- COUNT(*) 与 COUNT(列) 的差异
SELECT COUNT(*) AS 行数, -- 6
COUNT(score) AS 有分数的人数, -- 5(周八 score 为 NULL)
COUNT(email) AS 有邮箱的人数, -- 4
COUNT(DISTINCT dept) AS 院系数 -- 3
FROM student;
COUNT(*)由优化器特殊处理,InnoDB 会选最小的可用索引扫描,通常最快;COUNT(1)与COUNT(*)性能基本一致;COUNT(列)需要判断 NULL,语义也不同,不要混用。
GROUP BY 分组统计
-- 按院系统计人数
SELECT dept, COUNT(*) AS 人数 FROM student GROUP BY dept;
-- 多个统计值
SELECT dept,
COUNT(*) AS 人数,
AVG(score) AS 平均分,
MAX(score) AS 最高分,
MIN(score) AS 最低分,
ROUND(AVG(age),1) AS 平均年龄
FROM student
GROUP BY dept
ORDER BY 平均分 DESC;
-- 多列分组:院系 + 性别的交叉统计
SELECT dept, sex, COUNT(*) AS 人数 FROM student GROUP BY dept, sex;
-- 先过滤再分组
SELECT dept, COUNT(*) FROM student WHERE score IS NOT NULL GROUP BY dept;
-- 组内拼接
SELECT dept, GROUP_CONCAT(name) AS 学生名单 FROM student GROUP BY dept;
SELECT dept, GROUP_CONCAT(name ORDER BY score DESC SEPARATOR ' | ') FROM student GROUP BY dept;
-- 带总计行(小计 + 合计)
SELECT dept, COUNT(*) FROM student GROUP BY dept WITH ROLLUP;
HAVING 与 WHERE 的区别
-- HAVING 对分组后的结果过滤,可以使用聚合函数与别名
SELECT dept, AVG(score) AS 平均分
FROM student
GROUP BY dept
HAVING AVG(score) > 75;
SELECT dept, COUNT(*) AS 人数
FROM student
GROUP BY dept
HAVING 人数 >= 2;
-- WHERE + HAVING 同时使用:先按行过滤,再分组,再按组过滤
SELECT dept, COUNT(*) AS 人数, AVG(score) AS 平均分
FROM student
WHERE age >= 20 -- 行级过滤
GROUP BY dept
HAVING AVG(score) > 70 -- 组级过滤
ORDER BY 平均分 DESC
LIMIT 5;
-- 错误:WHERE 中不能使用聚合函数
SELECT dept, COUNT(*) FROM student WHERE COUNT(*) > 1 GROUP BY dept;
-- ERROR 1111 (HY000): Invalid use of group function
| 对比项 | WHERE | HAVING |
|---|---|---|
| 执行时机 | 分组之前,逐行过滤 | 分组之后,逐组过滤 |
| 能否用聚合函数 | 不能 | 能 |
| 能否用列别名 | 不能 | 能 |
| 能否用索引 | 能 | 不能(作用于结果集) |
| 是否必须配合 GROUP BY | 否 | 通常配合,也可单独用(整表一组) |
| 性能 | 更优,应尽量把条件写在 WHERE | 只能过滤聚合结果 |
ONLY_FULL_GROUP_BY:MySQL 5.7+ 默认开启,
SELECT列表中的非聚合列必须出现在GROUP BY中,否则报 1055 错误。详见 4.4 节sql_mode。
3.8 常用函数
字符串函数
| 函数 | 说明 | 示例 |
|---|---|---|
CONCAT(s1,s2,...) |
拼接字符串,任一参数为 NULL 返回 NULL | CONCAT('a','-','b') → a-b |
CONCAT_WS(sep,s1,s2) |
用分隔符拼接,忽略 NULL | CONCAT_WS('-','a',NULL,'b') → a-b |
SUBSTRING(s,pos,len) |
截取子串,位置从 1 开始;负数从右数 | SUBSTRING('abcdef',2,3) → bcd |
SUBSTR / MID |
SUBSTRING 的别名 |
— |
LENGTH(s) |
字节长度 | LENGTH('中国') → 6(utf8) |
CHAR_LENGTH(s) |
字符长度 | CHAR_LENGTH('中国') → 2 |
UPPER(s) / UCASE |
转大写 | UPPER('abc') → ABC |
LOWER(s) / LCASE |
转小写 | LOWER('ABC') → abc |
TRIM(s) |
去掉首尾空格 | TRIM(' ab ') → ab |
LTRIM / RTRIM |
去左 / 右空格 | — |
TRIM(x FROM s) |
去掉首尾指定字符 | TRIM('x' FROM 'xxabxx') → ab |
REPLACE(s,a,b) |
把 s 中的 a 全替换为 b | REPLACE('a-b','-','+') → a+b |
LEFT(s,n) |
取左边 n 个字符 | LEFT('abcdef',3) → abc |
RIGHT(s,n) |
取右边 n 个字符 | RIGHT('abcdef',2) → ef |
LPAD(s,n,p) / RPAD |
左 / 右填充到 n 位 | LPAD('7',3,'0') → 007 |
INSTR(s,sub) / LOCATE(sub,s) |
子串首次出现位置,无则 0 | INSTR('abc','b') → 2 |
REVERSE(s) |
反转 | REVERSE('abc') → cba |
REPEAT(s,n) |
重复 n 次 | REPEAT('ab',2) → abab |
SPACE(n) |
n 个空格 | — |
FIND_IN_SET(s,list) |
在逗号列表中查找位置 | FIND_IN_SET('b','a,b,c') → 2 |
SELECT CONCAT(name, '(', dept, ')') AS 描述 FROM student;
SELECT name, CHAR_LENGTH(name) AS 字数, LENGTH(name) AS 字节数 FROM student;
SELECT UPPER(email), LEFT(email, INSTR(email,'@')-1) AS 用户名 FROM student WHERE email IS NOT NULL;
SELECT REPLACE(email, 'abc.com', 'xyz.com') FROM student WHERE email IS NOT NULL;
-- 手机号脱敏
SELECT CONCAT(LEFT('13812345678',3), '****', RIGHT('13812345678',4));
数值函数
| 函数 | 说明 | 示例 |
|---|---|---|
ROUND(x[,d]) |
四舍五入保留 d 位小数 | ROUND(3.456,2) → 3.46 |
CEIL(x) / CEILING(x) |
向上取整 | CEIL(3.1) → 4 |
FLOOR(x) |
向下取整 | FLOOR(3.9) → 3 |
TRUNCATE(x,d) |
直接截断,不四舍五入 | TRUNCATE(3.456,2) → 3.45 |
ABS(x) |
绝对值 | ABS(-5) → 5 |
MOD(x,y) / x % y |
取余 | MOD(10,3) → 1 |
POW(x,y) / POWER |
幂 | POW(2,10) → 1024 |
SQRT(x) |
平方根 | SQRT(9) → 3 |
RAND() |
0~1 随机数 | FLOOR(RAND()*100) 随机 0~99 |
GREATEST(...) / LEAST(...) |
最大 / 最小值 | GREATEST(1,9,5) → 9 |
FORMAT(x,d) |
千分位格式化为字符串 | FORMAT(1234567.891,2) → 1,234,567.89 |
SELECT name, ROUND(score, 0) AS 取整分, CEIL(score) AS 上取整, FLOOR(score) AS 下取整
FROM student WHERE score IS NOT NULL;
SELECT dept, ROUND(AVG(score), 2) AS 平均分 FROM student GROUP BY dept;
SELECT * FROM student WHERE MOD(id, 2) = 0; -- 取 id 为偶数的行
日期时间函数
| 函数 | 说明 | 示例结果 |
|---|---|---|
NOW() / SYSDATE() |
当前日期时间 | 2026-08-22 13:20:00 |
CURDATE() / CURRENT_DATE |
当前日期 | 2026-08-22 |
CURTIME() / CURRENT_TIME |
当前时间 | 13:20:00 |
CURRENT_TIMESTAMP |
等同 NOW() |
— |
YEAR/MONTH/DAY(d) |
取年 / 月 / 日 | YEAR('2026-08-22') → 2026 |
HOUR/MINUTE/SECOND(t) |
取时 / 分 / 秒 | — |
DATE(dt) / TIME(dt) |
取日期 / 时间部分 | — |
DAYOFWEEK(d) |
星期(1=周日) | — |
WEEKDAY(d) |
星期(0=周一) | — |
DAYOFYEAR(d) / WEEK(d) |
年内第几天 / 第几周 | — |
LAST_DAY(d) |
该月最后一天 | 2026-08-31 |
DATE_FORMAT(d, fmt) |
格式化输出 | 见下 |
STR_TO_DATE(s, fmt) |
字符串转日期 | — |
DATE_ADD(d, INTERVAL n unit) |
日期加 | — |
DATE_SUB(d, INTERVAL n unit) |
日期减 | — |
ADDDATE / SUBDATE |
同上别名 | — |
DATEDIFF(d1, d2) |
相差天数(d1-d2) | — |
TIMESTAMPDIFF(unit,d1,d2) |
按指定单位相差(d2-d1) | — |
TIMEDIFF(t1, t2) |
时间差 | — |
UNIX_TIMESTAMP([d]) |
转 Unix 时间戳(秒) | 1787000000 |
FROM_UNIXTIME(ts[,fmt]) |
时间戳转日期 | — |
DATE_FORMAT 常用格式符:
| 格式符 | 含义 | 格式符 | 含义 |
|---|---|---|---|
%Y |
4 位年 | %y |
2 位年 |
%m |
2 位月 | %c |
月(不补零) |
%d |
2 位日 | %e |
日(不补零) |
%H |
24 小时制时 | %h |
12 小时制时 |
%i |
分钟 | %s |
秒 |
%p |
AM/PM | %W |
星期名 |
%j |
年内天数 | %% |
百分号 |
SELECT NOW(), CURDATE(), CURTIME();
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s');
SELECT DATE_FORMAT(NOW(), '%Y年%m月%d日');
SELECT DATE_FORMAT(created, '%Y-%m') AS 月份, COUNT(*) FROM student GROUP BY 月份;
-- 日期加减
SELECT DATE_ADD(NOW(), INTERVAL 7 DAY); -- 7 天后
SELECT DATE_ADD(NOW(), INTERVAL 1 MONTH);
SELECT DATE_SUB(CURDATE(), INTERVAL 1 YEAR); -- 1 年前
SELECT DATE_ADD(NOW(), INTERVAL -3 HOUR); -- 3 小时前
-- 可用单位:SECOND MINUTE HOUR DAY WEEK MONTH QUARTER YEAR
-- 日期差
SELECT DATEDIFF('2026-12-31', '2026-08-22'); -- 131(天)
SELECT TIMESTAMPDIFF(MONTH, '2026-01-01', '2026-08-22'); -- 7(月)
SELECT TIMESTAMPDIFF(YEAR, '2000-05-01', CURDATE()) AS 年龄;
-- 时间戳互转
SELECT UNIX_TIMESTAMP(), UNIX_TIMESTAMP('2026-08-22 00:00:00');
SELECT FROM_UNIXTIME(1787000000, '%Y-%m-%d %H:%i:%s');
-- 常用查询:今天 / 最近 7 天 / 本月
SELECT * FROM student WHERE DATE(created) = CURDATE();
SELECT * FROM student WHERE created >= DATE_SUB(CURDATE(), INTERVAL 7 DAY);
SELECT * FROM student WHERE DATE_FORMAT(created, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m');
性能提示:对索引列使用函数(如
DATE(created) = ...)会导致索引失效,建议改为范围条件created >= '2026-08-22 00:00:00' AND created < '2026-08-23 00:00:00'。
条件判断函数
| 函数 | 说明 |
|---|---|
IF(expr, v1, v2) |
expr 为真返回 v1,否则 v2 |
IFNULL(v, default) |
v 为 NULL 时返回 default |
NULLIF(v1, v2) |
两者相等返回 NULL,否则返回 v1 |
COALESCE(v1, v2, ...) |
返回第一个非 NULL 的值 |
CASE ... WHEN ... THEN ... END |
多分支判断 |
-- IF
SELECT name, IF(score >= 60, '及格', '不及格') AS 结果 FROM student;
SELECT name, IF(sex='M','男','女') AS 性别 FROM student;
-- IFNULL / COALESCE:NULL 替换
SELECT name, IFNULL(score, 0) AS 分数, IFNULL(email, '未填写') AS 邮箱 FROM student;
SELECT name, COALESCE(email, phone, '无联系方式') FROM student;
SELECT AVG(IFNULL(score, 0)) AS 含零平均分 FROM student;
-- CASE 简单形式(等值判断)
SELECT name,
CASE sex
WHEN 'M' THEN '男'
WHEN 'F' THEN '女'
ELSE '未知'
END AS 性别
FROM student;
-- CASE 搜索形式(区间判断),成绩分级
SELECT name, score,
CASE
WHEN score IS NULL THEN '缺考'
WHEN score >= 90 THEN '优秀'
WHEN score >= 80 THEN '良好'
WHEN score >= 60 THEN '及格'
ELSE '不及格'
END AS 等级
FROM student;
-- CASE + 聚合实现行转列统计
SELECT dept,
COUNT(*) AS 总人数,
SUM(CASE WHEN sex='M' THEN 1 ELSE 0 END) AS 男生数,
SUM(CASE WHEN sex='F' THEN 1 ELSE 0 END) AS 女生数,
SUM(IF(score >= 80, 1, 0)) AS 高分人数
FROM student
GROUP BY dept;
其他常用函数
SELECT VERSION(); -- 数据库版本
SELECT DATABASE(); -- 当前库
SELECT USER(), CURRENT_USER(); -- 当前连接用户 / 认证用户
SELECT CONNECTION_ID(); -- 当前连接 ID
SELECT MD5('123456'); -- MD5 摘要
SELECT SHA1('123456'), SHA2('123456', 256);
SELECT PASSWORD('123'); -- 密码哈希(MySQL 5.7 废弃,8.0 移除;MariaDB 仍可用)
SELECT UUID(); -- 生成 UUID
SELECT CAST('123' AS SIGNED), CONVERT('123', SIGNED); -- 类型转换
SELECT LAST_INSERT_ID(); -- 最近一次自增值
SELECT ROW_COUNT(); -- 上一条语句影响的行数
多表连接查询(
INNER JOIN/LEFT JOIN/RIGHT JOIN/ 子查询 /UNION)详见11-03。
四、用户与权限、导入导出、运维基础
4.1 用户管理
用户的组成:用户名 + 主机名
MySQL 的用户标识是 '用户名'@'主机名',同名不同主机是两个不同的用户,可拥有不同密码和权限。
| 主机名写法 | 含义 |
|---|---|
localhost |
只允许本机通过 socket 连接(不匹配 127.0.0.1 的 TCP 连接) |
127.0.0.1 |
只允许本机通过 TCP/IP 回环地址连接 |
% |
允许任意主机连接(不含某些版本下的 localhost 特例) |
10.0.0.% |
允许 10.0.0.0/24 网段连接 |
10.0.0.61 |
只允许该 IP 连接 |
%.abc.com |
允许该域名下主机连接(需反解 DNS) |
空字符串 '' |
匿名用户,任意用户名,安全隐患,应删除 |
安全建议:生产环境禁止创建
'root'@'%';应用账号按最小网段授权,如'app'@'10.0.0.%'。
创建用户
-- 标准写法
CREATE USER 'test'@'localhost' IDENTIFIED BY '123456';
CREATE USER 'app'@'10.0.0.%' IDENTIFIED BY 'App@123456';
CREATE USER 'dev'@'%' IDENTIFIED BY 'Dev@123456';
CREATE USER IF NOT EXISTS 'test2'@'localhost' IDENTIFIED BY '123456';
-- 用密码哈希创建(不写明文)
CREATE USER 'test3'@'localhost' IDENTIFIED BY PASSWORD '*6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9';
-- MySQL 8.0 指定认证插件(老客户端不支持 caching_sha2_password 时)
CREATE USER 'old'@'%' IDENTIFIED WITH mysql_native_password BY '123456';
-- MySQL 5.7+ / MariaDB 10.4+ 密码策略
CREATE USER 'x'@'%' IDENTIFIED BY '123456' PASSWORD EXPIRE INTERVAL 90 DAY;
-- MySQL 5.6 及之前 / MariaDB:GRANT 可隐式创建用户(8.0 已不允许)
GRANT ALL ON mydb.* TO 'test'@'localhost' IDENTIFIED BY '123456';
查看用户
-- 查看所有数据库用户及授权信息(MariaDB / MySQL 5.6 及之前)
SELECT user, host, password FROM mysql.user;
-- MySQL 5.7+
SELECT user, host, authentication_string, plugin FROM mysql.user;
-- 通用写法
SELECT user, host FROM mysql.user;
-- 查看当前登录用户
SELECT USER();
+----------------+
| user() |
+----------------+
| root@localhost |
+----------------+
此用户是数据库用户,不是系统 root 用户,两者密码互不相关。
源文档中的典型输出(安全初始化前,存在匿名用户与无密码账号):
+------+----------------+------------------+
| user | host | password |
+------+----------------+------------------+
| root | localhost | 773359240eb9a1d9 |
| root | li.cluster.com | |
| root | 127.0.0.1 | |
| | localhost | |
| | li.cluster.com | |
+------+----------------+------------------+
其中 user 为空的两行是匿名用户,密码为空的行是无密码账号,都应通过
mysql_secure_installation或手工DROP USER清理。
删除与重命名用户
DROP USER 'test'@'localhost';
DROP USER IF EXISTS 'test'@'localhost';
DROP USER 'a'@'%', 'b'@'%';
-- 删除匿名用户
DROP USER ''@'localhost';
DELETE FROM mysql.user WHERE user=''; -- 老版本写法,之后必须 FLUSH PRIVILEGES
FLUSH PRIVILEGES;
-- 重命名用户(同时保留其权限)
RENAME USER 'test'@'localhost' TO 'test_new'@'10.0.0.%';
-- 锁定 / 解锁账号(MySQL 5.7.6+)
ALTER USER 'test'@'localhost' ACCOUNT LOCK;
ALTER USER 'test'@'localhost' ACCOUNT UNLOCK;
4.2 修改密码的多种方式
方式一:mysqladmin 命令行(在 shell 中执行)
# root 无密码时首次设置密码
mysqladmin -u root password "123"
# 已知原密码时修改密码(-p 后跟旧密码,password 后跟新密码)
mysqladmin -u root password '456' -p123
# 远程修改
mysqladmin -u root -h 10.0.0.61 -p123 password '456'
登录验证:
mysql -p123 # 明文登录
mysql -p # 回车后隐式输入密码
方式二:SET PASSWORD(在客户端内执行)
-- MariaDB / MySQL 5.7 及之前
SET PASSWORD FOR 'root'@'localhost' = PASSWORD('123');
-- 修改当前登录用户自己的密码
SET PASSWORD = PASSWORD('123');
-- MySQL 8.0(PASSWORD() 函数已移除)
SET PASSWORD FOR 'root'@'localhost' = '123';
SET PASSWORD = '123';
此方式修改后不需要刷新权限表。
方式三:ALTER USER(MySQL 5.7.6+ / MariaDB 10.2+ 推荐)
ALTER USER 'root'@'localhost' IDENTIFIED BY '123456';
ALTER USER 'test'@'%' IDENTIFIED BY 'New@123456';
-- 修改自己的密码
ALTER USER USER() IDENTIFIED BY '123456';
-- 指定认证插件
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456';
方式四:UPDATE 直接修改权限表(老方式,慎用)
MariaDB / MySQL 5.6 及之前使用 password 字段:
UPDATE mysql.user
SET password = PASSWORD("456")
WHERE user = "root" AND host = "localhost";
FLUSH PRIVILEGES;
MySQL 5.7 使用 authentication_string 字段:
UPDATE mysql.user
SET authentication_string = PASSWORD("新密码")
WHERE User = "test" AND Host = "localhost";
FLUSH PRIVILEGES;
修改权限表后必须执行
FLUSH PRIVILEGES;刷新权限表,否则不生效。 MySQL 8.0 中PASSWORD()函数已被移除,不能再用这种方式,请用ALTER USER。 MariaDB 10.4+ 权限已迁移到mysql.global_priv表,mysql.user变成视图,也应改用ALTER USER/SET PASSWORD。
方式五:配置文件免密登录(测试环境慎用)
[client]
password=123
socket=/var/lib/mysql/mysql.sock
加入 [client] 并写入密码后,直接执行 mysql 即可免密登录。生产环境应改用受权限保护的 ~/.my.cnf(chmod 600)或 mysql_config_editor:
mysql_config_editor set --login-path=local --host=localhost --user=root --password
mysql --login-path=local
4.3 忘记 root 密码的完整重置流程
方法一:通过配置文件 skip-grant-tables 跳过授权表
第一步:编辑配置文件:
vim /etc/my.cnf
在 [mysqld] 段下加入:
[mysqld]
skip-grant-tables
第二步:重启数据库:
# CentOS 7 / MariaDB
systemctl restart mariadb
# CentOS 7 / MySQL
systemctl restart mysqld
# CentOS 6
service mysqld restart
第三步:无密码登录后修改密码:
mysql
UPDATE mysql.user
SET password = PASSWORD("123")
WHERE user = "root" AND host = "localhost";
FLUSH PRIVILEGES;
MySQL 5.7 写法:
FLUSH PRIVILEGES; -- 跳过授权表时先刷新,才能执行 ALTER USER
ALTER USER 'root'@'localhost' IDENTIFIED BY '123456';
-- 或
UPDATE mysql.user SET authentication_string=PASSWORD('123456')
WHERE user='root' AND host='localhost';
FLUSH PRIVILEGES;
第四步:去掉 skip-grant-tables 配置,重新加载 / 重启服务,再使用新密码登录验证:
vim /etc/my.cnf # 删除 skip-grant-tables 这一行
systemctl restart mariadb
mysql -u root -p123456
方法二:启动时直接跳过授权表
mysqld_safe --defaults-file=/etc/my.cnf --skip-grant-tables &
之后 mysql 即可无密码登录,进去后同样执行 UPDATE + FLUSH PRIVILEGES 修改密码,然后正常重启服务。
更安全的做法是同时禁用网络连接,避免重置期间被人无密码连入:
mysqld_safe --skip-grant-tables --skip-networking &
注意:
skip-grant-tables状态下所有权限校验被关闭,任何人都能无密码登录并拥有全部权限,操作必须尽快完成,并确保防火墙关闭对外端口。
4.4 权限管理
GRANT 语法
GRANT 权限列表 ON 库.表 TO '用户'@'主机' [IDENTIFIED BY '密码'] [WITH GRANT OPTION];
权限级别
| 级别 | 写法 | 说明 |
|---|---|---|
| 全局级 | ON *.* |
所有库所有表,权限记录在 mysql.user |
| 数据库级 | ON mydb.* |
指定库的所有表,记录在 mysql.db |
| 表级 | ON mydb.t1 |
指定表,记录在 mysql.tables_priv |
| 列级 | ON mydb.t1 (col1, col2) |
指定列,记录在 mysql.columns_priv |
| 存储过程级 | ON PROCEDURE mydb.p1 |
记录在 mysql.procs_priv |
常用权限清单
| 权限 | 说明 |
|---|---|
SELECT |
查询数据 |
INSERT |
插入数据 |
UPDATE |
更新数据 |
DELETE |
删除数据 |
CREATE |
创建库和表 |
DROP |
删除库和表 |
ALTER |
修改表结构 |
INDEX |
创建 / 删除索引 |
CREATE TEMPORARY TABLES |
创建临时表 |
CREATE VIEW / SHOW VIEW |
创建 / 查看视图 |
CREATE ROUTINE / ALTER ROUTINE / EXECUTE |
存储过程相关 |
TRIGGER |
触发器 |
REFERENCES |
创建外键 |
LOCK TABLES |
锁表(备份需要) |
RELOAD |
FLUSH 操作(备份需要) |
PROCESS |
查看其他会话(SHOW PROCESSLIST) |
SUPER |
超级权限(KILL、SET GLOBAL、修改只读参数) |
REPLICATION SLAVE / REPLICATION CLIENT |
主从复制相关,详见 11-06 |
SHOW DATABASES |
查看所有库名 |
FILE |
读写服务器文件(LOAD DATA / INTO OUTFILE) |
GRANT OPTION |
把自己拥有的权限授予他人 |
ALL / ALL PRIVILEGES |
除 GRANT OPTION 外的全部权限 |
USAGE |
无任何权限,仅允许登录(创建用户后的默认权限) |
-- 只读账号(某库)
GRANT SELECT ON mydb.* TO 'reader'@'10.0.0.%' IDENTIFIED BY 'Read@123';
-- 应用账号(增删改查)
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app'@'10.0.0.%' IDENTIFIED BY 'App@123';
-- 表级、列级授权
GRANT SELECT, UPDATE ON mydb.student TO 'tea'@'localhost';
GRANT SELECT (id, name), UPDATE (score) ON mydb.student TO 'tea'@'localhost';
-- 全部权限(相当于管理员)
GRANT ALL PRIVILEGES ON *.* TO 'admin'@'10.0.0.%' IDENTIFIED BY 'Admin@123';
-- 允许该用户把权限再授予他人
GRANT ALL PRIVILEGES ON mydb.* TO 'dba'@'localhost' WITH GRANT OPTION;
-- 备份账号所需最小权限
GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, RELOAD, PROCESS ON *.* TO 'backup'@'localhost';
-- 授权后刷新(GRANT 会自动生效,直接改表才必须刷新)
FLUSH PRIVILEGES;
查看权限
SHOW GRANTS; -- 当前用户
SHOW GRANTS FOR 'app'@'10.0.0.%'; -- 指定用户
SHOW GRANTS FOR CURRENT_USER();
-- 从系统表查看
SELECT * FROM mysql.user WHERE user='app'\G -- 全局权限,Y/N 列
SELECT * FROM mysql.db WHERE user='app'\G -- 库级权限
SELECT * FROM mysql.tables_priv WHERE user='app'\G
SELECT * FROM mysql.columns_priv WHERE user='app'\G
-- information_schema 视图
SELECT * FROM information_schema.USER_PRIVILEGES;
SELECT * FROM information_schema.SCHEMA_PRIVILEGES;
回收权限(REVOKE)
-- 回收指定权限
REVOKE INSERT, UPDATE, DELETE ON mydb.* FROM 'app'@'10.0.0.%';
-- 回收全部权限(用户仍然存在,保留 USAGE)
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'app'@'10.0.0.%';
FLUSH PRIVILEGES;
REVOKE的ON子句必须与GRANT时的级别完全一致:库级授权不能用表级回收。
最小权限原则与安全建议
- 一个应用一个账号,只授予它真正需要的权限与库;
- 禁止应用账号拥有
DROP、ALTER、FILE、SUPER、GRANT OPTION; - 主机名精确到网段或 IP,避免
%; - 禁止 root 远程登录(
mysql_secure_installation中选择Disallow root login remotely? Y); - 删除匿名用户与
test库; - 密码复杂度足够,定期轮换,命令行避免明文密码(会进
history与ps); - 定期审计:
SELECT user, host FROM mysql.user;与SHOW GRANTS; - 生产库开启慢查询日志与错误日志监控,端口不直接暴露公网。
4.5 数据导入导出
mysqldump 基本用法(详细备份恢复策略见 11-05)
# 导出单库(含建库语句)
mysqldump -u root -p --databases mydb > /backup/mydb.sql
# 导出单库(不含建库语句,恢复时需先建库)
mysqldump -u root -p mydb > /backup/mydb.sql
# 导出单表 / 多表
mysqldump -u root -p mydb student > /backup/student.sql
mysqldump -u root -p mydb student teacher > /backup/two_tables.sql
# 导出所有库
mysqldump -u root -p --all-databases > /backup/all.sql
# 只导出表结构(不含数据)
mysqldump -u root -p -d mydb > /backup/mydb_schema.sql
# 只导出数据(不含建表语句)
mysqldump -u root -p -t mydb > /backup/mydb_data.sql
# 按条件导出部分数据
mysqldump -u root -p mydb student --where="score > 80" > /backup/high.sql
# InnoDB 一致性热备(不锁表)
mysqldump -u root -p --single-transaction --master-data=2 --databases mydb > /backup/mydb.sql
# 指定字符集,避免乱码
mysqldump -u root -p --default-character-set=utf8mb4 mydb > /backup/mydb.sql
# 远程导出并压缩
mysqldump -u root -p -h 10.0.0.61 mydb | gzip > /backup/mydb.sql.gz
常用选项:
| 选项 | 说明 |
|---|---|
-B / --databases |
导出时包含 CREATE DATABASE 与 USE |
-A / --all-databases |
导出所有库 |
-d / --no-data |
只要结构 |
-t / --no-create-info |
只要数据 |
--single-transaction |
基于事务的一致性备份(仅 InnoDB) |
-x / --lock-all-tables |
锁全部表(MyISAM 一致性备份) |
--master-data=2 |
在备份中以注释形式记录 binlog 位置 |
-R / --routines |
导出存储过程与函数 |
-E / --events |
导出事件 |
--triggers |
导出触发器(默认开启) |
--where= |
条件导出 |
--default-character-set= |
指定字符集 |
恢复数据
# 方式一:shell 重定向
mysql -u root -p < /backup/mydb.sql # 备份中含 CREATE DATABASE
mysql -u root -p mydb < /backup/mydb.sql # 备份中不含建库语句,需先建库
# 先建库再恢复
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS mydb DEFAULT CHARSET utf8mb4"
mysql -u root -p mydb < /backup/mydb.sql
# 压缩包恢复
gunzip < /backup/mydb.sql.gz | mysql -u root -p mydb
-- 方式二:客户端内 source
USE mydb;
SOURCE /backup/mydb.sql;
SELECT ... INTO OUTFILE 导出文本 / CSV
-- 导出为默认格式(Tab 分隔,\n 换行)
SELECT * FROM student
INTO OUTFILE '/var/lib/mysql-files/student.txt';
-- 导出为标准 CSV
SELECT * FROM student
INTO OUTFILE '/var/lib/mysql-files/student.csv'
FIELDS TERMINATED BY ','
OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n';
-- 带表头的 CSV(用 UNION 拼一行标题)
SELECT 'id','name','sex','age','score'
UNION ALL
SELECT id, name, sex, age, score FROM student
INTO OUTFILE '/var/lib/mysql-files/student_h.csv'
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n';
LOAD DATA INFILE 导入文本 / CSV
LOAD DATA INFILE '/var/lib/mysql-files/student.csv'
INTO TABLE student
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 LINES -- 跳过表头
(name, sex, age, score, dept); -- 指定列顺序
-- 从客户端本地文件导入(文件在客户端机器上)
LOAD DATA LOCAL INFILE '/tmp/student.csv' INTO TABLE student
FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n';
-- 冲突处理
LOAD DATA INFILE '...' IGNORE INTO TABLE student ...; -- 跳过冲突行
LOAD DATA INFILE '...' REPLACE INTO TABLE student ...; -- 替换冲突行
secure_file_priv 限制
SHOW VARIABLES LIKE 'secure_file_priv';
| 取值 | 含义 |
|---|---|
NULL |
完全禁止导入导出文件 |
空字符串 '' |
不限制目录(不安全) |
目录路径(如 /var/lib/mysql-files/) |
只允许在该目录读写(MySQL 5.7+ 默认) |
该参数只能在配置文件中修改,不能动态 SET:
[mysqld]
secure_file_priv=/var/lib/mysql-files/
报错与解决:
ERROR 1290 (HY000): The MySQL server is running with the --secure-file-priv
option so it cannot execute this statement
- 把导出路径改到
secure_file_priv指定的目录; - 目录必须对
mysql系统用户可写:chown mysql:mysql /var/lib/mysql-files; - 若开启了 SELinux,还需处理上下文或临时
setenforce 0; LOAD DATA LOCAL INFILE需要服务端local_infile=ON且客户端加--local-infile=1:
mysql --local-infile=1 -u root -p mydb
SET GLOBAL local_infile = 1;
- 也可以用 shell 直接把查询结果转成 CSV(不受
secure_file_priv限制):
mysql -u root -p -N -e "SELECT id,name,score FROM school.student" \
| sed 's/\t/,/g' > /tmp/student.csv
4.6 配置文件 my.cnf 入门
RPM 版配置文件默认为 /etc/my.cnf,读取顺序一般为:/etc/my.cnf → /etc/mysql/my.cnf → $MYSQL_HOME/my.cnf → --defaults-extra-file → ~/.my.cnf。
# 查看当前实例读取了哪些配置文件
mysqld --verbose --help | grep -A 1 'Default options'
mysql --help | grep -A 1 'Default options'
源文档中的完整示例
[mysqld]
datadir=/var/lib/mysql # 数据文件目录
socket=/var/lib/mysql/mysql.sock # 本地连接 socket 文件
user=mysql # 管理 MySQL 的系统用户
port=3306 # 监听端口
max_connections=300 # 永久修改最大连接数
[mysqld_safe]
log-error=/var/log/mysqld.log # 错误日志
pid-file=/var/run/mysqld/mysqld.pid # pid 文件
[client]
password=123
socket=/var/lib/mysql/mysql.sock
pid 文件和 socket 文件在服务启动成功后才会生成。
[mysqld]是服务端参数组,[client]是所有客户端通用参数组,[mysql]仅对mysql命令生效,[mysqldump]仅对导出命令生效。
常用参数说明
| 参数 | 说明 |
|---|---|
datadir |
数据目录,默认 /var/lib/mysql |
basedir |
程序安装目录 |
socket |
本地 socket 文件路径,客户端与服务端要一致 |
port |
监听端口,默认 3306 |
user |
运行 mysqld 的系统用户,通常为 mysql |
pid-file |
pid 文件路径 |
log-error |
错误日志路径,排错第一现场 |
character-set-server |
服务端默认字符集,建议 utf8mb4 |
collation-server |
服务端默认校对集,如 utf8mb4_general_ci |
max_connections |
最大并发连接数,默认 151 |
max_connect_errors |
单主机连接失败次数上限,超过则被拉黑(FLUSH HOSTS 解除) |
wait_timeout / interactive_timeout |
空闲连接超时秒数 |
bind-address |
监听地址。127.0.0.1 只本机可连;0.0.0.0 监听所有网卡 |
skip-networking |
完全禁用 TCP,只允许 socket 连接 |
skip-name-resolve |
不做 DNS 反解,加快连接(此时授权只能用 IP) |
sql_mode |
SQL 模式,控制严格程度 |
lower_case_table_names |
0 区分大小写(Linux 默认);1 表名转小写存储、比较不敏感;2 仅 macOS |
default_storage_engine |
默认存储引擎,建议 InnoDB |
innodb_buffer_pool_size |
InnoDB 缓冲池大小,一般设为物理内存 50%~70% |
slow_query_log / long_query_time |
慢查询日志开关与阈值 |
log_bin / server_id |
二进制日志与实例 ID(复制、恢复必需,详见 11-05、11-06) |
secure_file_priv |
限制导入导出目录 |
skip-grant-tables |
跳过权限校验(仅重置密码时临时使用) |
参数查看与动态修改
-- 模糊查询参数
SHOW VARIABLES LIKE "%connections";
SHOW VARIABLES LIKE '%connections%';
+----------------------+-------+
| Variable_name | Value |
+----------------------+-------+
| max_connections | 151 |
| max_user_connections | 0 |
+----------------------+-------+
-- 查看引擎相关参数
SHOW VARIABLES LIKE '%engine%';
+---------------------------+--------+
| Variable_name | Value |
+---------------------------+--------+
| default_storage_engine | InnoDB |
| engine_condition_pushdown | OFF |
| storage_engine | InnoDB |
+---------------------------+--------+
-- 查看数据目录
SHOW VARIABLES LIKE 'datadir';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| datadir | /var/lib/mysql/ |
+---------------+-----------------+
-- 动态修改(全局,对新连接生效)
SET GLOBAL max_connections = 300;
-- 只对当前会话生效
SET SESSION sql_safe_updates = 1;
SET sql_safe_updates = 1; -- 省略时默认 SESSION
-- 查看会话级 / 全局级参数
SHOW SESSION VARIABLES LIKE 'sql_mode';
SHOW GLOBAL VARIABLES LIKE 'sql_mode';
SELECT @@global.max_connections, @@session.sql_mode;
-- MySQL 8.0 持久化到配置文件(自动写入 mysqld-auto.cnf)
SET PERSIST max_connections = 500;
动态修改在重启后会恢复为默认值(如
max_connections恢复为 151)。要永久生效必须写入配置文件/etc/my.cnf,然后重启服务。
sql_mode 与 ONLY_FULL_GROUP_BY
SELECT @@sql_mode;
SHOW VARIABLES LIKE 'sql_mode';
MySQL 5.7 默认值:
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
| 模式 | 含义 |
|---|---|
ONLY_FULL_GROUP_BY |
SELECT 中的非聚合列必须出现在 GROUP BY 中 |
STRICT_TRANS_TABLES |
严格模式:非法值直接报错而不是截断或补默认值 |
NO_ZERO_DATE / NO_ZERO_IN_DATE |
不允许 0000-00-00 这类零日期 |
ERROR_FOR_DIVISION_BY_ZERO |
除零报错(否则返回 NULL) |
NO_AUTO_CREATE_USER |
禁止 GRANT 隐式创建用户(8.0 已移除该模式,行为固化) |
NO_ENGINE_SUBSTITUTION |
指定引擎不存在时报错,而不是替换为默认引擎 |
ANSI_QUOTES |
双引号视为标识符引用而非字符串 |
PIPES_AS_CONCAT |
\|\| 作为字符串拼接符(Oracle 风格) |
ONLY_FULL_GROUP_BY 带来的典型报错:
SELECT dept, name, COUNT(*) FROM student GROUP BY dept;
ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause
and contains nonaggregated column 'school.student.name' which is not
functionally dependent on columns in GROUP BY clause; this is incompatible
with sql_mode=only_full_group_by
三种解决方式:
-- 1)把非聚合列加入 GROUP BY(推荐)
SELECT dept, name, COUNT(*) FROM student GROUP BY dept, name;
-- 2)用聚合函数包裹(ANY_VALUE 表示"任取一个")
SELECT dept, ANY_VALUE(name), COUNT(*) FROM student GROUP BY dept;
SELECT dept, MAX(name), COUNT(*) FROM student GROUP BY dept;
-- 3)临时关闭该模式(会话级)
SET SESSION sql_mode = (SELECT REPLACE(@@sql_mode, 'ONLY_FULL_GROUP_BY', ''));
SET GLOBAL sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
永久修改写入配置文件:
[mysqld]
sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
MariaDB 10.x 默认不开启
ONLY_FULL_GROUP_BY,从 MySQL 迁移到 MariaDB 时同样 SQL 可能不再报错,但结果不确定,仍应写规范的 SQL。
4.7 常用运维语句
-- 状态统计(连接数、QPS 计算基础、慢查询数等)
SHOW STATUS;
SHOW GLOBAL STATUS LIKE 'Threads_connected'; -- 当前连接数
SHOW GLOBAL STATUS LIKE 'Max_used_connections'; -- 历史最大连接数
SHOW GLOBAL STATUS LIKE 'Uptime';
SHOW GLOBAL STATUS LIKE 'Questions'; -- 累计语句数
SHOW GLOBAL STATUS LIKE 'Slow_queries';
SHOW GLOBAL STATUS LIKE 'Com_select';
SHOW STATUS LIKE 'Innodb_row_lock%';
-- 参数
SHOW VARIABLES;
SHOW VARIABLES LIKE 'max_connections';
-- 当前会话与执行中的 SQL
SHOW PROCESSLIST; -- 只显示前 100 字符的 SQL
SHOW FULL PROCESSLIST; -- 显示完整 SQL
SELECT * FROM information_schema.PROCESSLIST WHERE COMMAND <> 'Sleep';
-- 杀掉会话 / 杀掉查询
KILL 123; -- 终止连接(id 取自 PROCESSLIST)
KILL QUERY 123; -- 只终止当前查询,保留连接
-- 批量生成 KILL 语句(清理长时间 Sleep 连接)
SELECT CONCAT('KILL ', id, ';')
FROM information_schema.PROCESSLIST
WHERE COMMAND='Sleep' AND TIME > 600;
-- 引擎与版本
SHOW ENGINES;
SHOW ENGINES\G
SHOW VARIABLES LIKE 'version%';
SELECT VERSION();
-- 权限与日志刷新
FLUSH PRIVILEGES; -- 刷新权限表
FLUSH TABLES; -- 关闭并重新打开所有表
FLUSH TABLES WITH READ LOCK; -- 全局读锁(备份用),UNLOCK TABLES 解锁
UNLOCK TABLES;
FLUSH LOGS; -- 切换日志
FLUSH HOSTS; -- 清空主机缓存(解除被拉黑的主机)
RESET QUERY CACHE;
-- 表维护
CHECK TABLE student;
REPAIR TABLE student; -- 仅 MyISAM
ANALYZE TABLE student; -- 更新统计信息
OPTIMIZE TABLE student; -- 整理碎片、回收空间
ALTER TABLE student ENGINE=InnoDB; -- InnoDB 通过重建表整理碎片
-- 查看各库大小(MB)
SELECT TABLE_SCHEMA AS 库名,
ROUND(SUM(DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS 大小MB
FROM information_schema.TABLES
GROUP BY TABLE_SCHEMA;
-- 查看某库中各表大小与行数
SELECT TABLE_NAME, TABLE_ROWS,
ROUND(DATA_LENGTH/1024/1024, 2) AS 数据MB,
ROUND(INDEX_LENGTH/1024/1024, 2) AS 索引MB
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'school'
ORDER BY DATA_LENGTH DESC;
存储引擎
MySQL / MariaDB 支持多种存储引擎:MyISAM、InnoDB、MERGE、MEMORY(HEAP)、BDB(BerkeleyDB)、EXAMPLE、FEDERATED、ARCHIVE、CSV、BLACKHOLE(MariaDB 另有 Aria、TokuDB、Spider 等)。
| 引擎 | 特点 | 适用场景 |
|---|---|---|
| MyISAM | 管理非事务表,表级锁,读性能高,提供高速存储检索和全文搜索;崩溃后需修复 | 读多写少,日志类;MySQL 5.5 之前的默认引擎 |
| InnoDB | 提供事务安全表,支持事务、行级锁、外键、崩溃自动恢复、聚簇索引 | 高并发、需要事务,绝大多数业务;MySQL 5.5 及之后的默认引擎 |
| MEMORY(HEAP) | 数据存于内存,重启丢失,表级锁 | 临时缓存、中间结果 |
| ARCHIVE | 高压缩存储,只支持 INSERT 与 SELECT,不支持索引更新 | 历史归档数据 |
| CSV | 数据以 CSV 文本存储,可被外部直接读取 | 数据交换 |
| BLACKHOLE | 写入即丢弃,但记录 binlog | 复制中转、测试 |
| FEDERATED | 访问远端 MySQL 表 | 跨实例访问 |
| MERGE | 把多个结构相同的 MyISAM 表当一张表用 | 分表聚合查询 |
MySQL 5.5 以前默认 MyISAM,5.5 以后默认 InnoDB。
InnoDB 与 MyISAM 详细对比
| 对比项 | InnoDB | MyISAM |
|---|---|---|
| 事务 | 支持(ACID) | 不支持 |
| 锁粒度 | 行级锁(也有表锁、间隙锁) | 表级锁 |
| 外键 | 支持 | 不支持(语法忽略) |
| 崩溃恢复 | 支持(redo / undo 日志) | 不支持,需 REPAIR TABLE |
| 索引结构 | 聚簇索引,数据与主键索引存一起 | 非聚簇,索引与数据分离 |
| 全文索引 | 5.6+ 支持 | 支持 |
COUNT(*) |
需要扫描统计 | 保存行数,COUNT(*) 极快 |
| 文件组成 | .frm + .ibd(或共享 ibdata1) |
.frm + .MYD + .MYI |
| 内存缓存 | innodb_buffer_pool 缓存数据与索引 |
key_buffer 只缓存索引 |
| 空间占用 | 较大 | 较小 |
| MVCC | 支持 | 不支持 |
| 适用 | 绝大多数 OLTP 业务 | 只读 / 归档 / 极简统计 |
-- 查看某张表使用的引擎
SHOW TABLE STATUS LIKE 'student'\G
SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA='school';
-- 转换引擎
ALTER TABLE student ENGINE=InnoDB;
索引、事务、锁与 MVCC 的原理详见
11-04。
4.8 服务与连通性基础检查
# 服务状态(CentOS 7)
systemctl status mariadb
systemctl start mariadb
systemctl restart mariadb
systemctl stop mariadb
systemctl enable mariadb
# 服务状态(CentOS 6)
service mysqld status
service mysqld restart
chkconfig mysqld on
# 端口监听检查
netstat -ntlup | grep 3306
ss -ntlup | grep 3306
tcp 0 0 0.0.0.0:3306 0.0.0.0:* LISTEN 3720/mysqld
# 进程与数据目录
ps -ef | grep mysqld
id mysql # 安装后系统自动创建的 mysql 用户
ls -l /var/lib/mysql/
tail -f /var/log/mysqld.log # 错误日志,排错第一现场
# 关闭数据库的几种方式
systemctl stop mariadb # 系统服务方式(推荐)
mysqladmin shutdown -p123 # 自带管理命令(推荐)
pkill mysql # 生产环境不建议,可能造成数据损坏
kill -9 <pid> # 严禁使用
uid=27(mysql) gid=27(mysql) groups=27(mysql)
安装部署、
mysql_secure_installation安全初始化、数据目录迁移的完整步骤详见11-02-MySQL安装部署.md。
4.9 排错清单
| 报错 / 现象 | 原因 | 解决办法 |
|---|---|---|
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES) |
密码错误 | 确认密码;确认是否连的目标实例;必要时用 skip-grant-tables 重置密码 |
ERROR 1045 ... (using password: NO) |
未提供密码 | 加 -p 输入密码 |
Access denied for user 'app'@'10.0.0.5' |
用户 host 不匹配 | SELECT user,host FROM mysql.user; 检查;按网段重新授权 |
ERROR 1044 (42000): Access denied for user ... to database 'x' |
有登录权限但无库权限 | GRANT 对应库权限后 FLUSH PRIVILEGES |
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock' (2) |
服务未启动 / socket 文件被删或路径不一致 | 启动或重启服务重新生成 socket;核对 [mysqld] 与 [client] 的 socket 路径;临时用 mysql -h 127.0.0.1 |
ERROR 2003 (HY000): Can't connect to MySQL server on 'x.x.x.x' (113/111) |
网络不通 / 防火墙 / bind-address / 服务未监听 |
检查 netstat -ntlup \| grep 3306、firewalld、iptables、bind-address=0.0.0.0、SELinux |
ERROR 1064 (42000): You have an error in your SQL syntax |
语法错误:关键字拼错、缺分号、引号不匹配、使用了保留字做名字 | 对照报错中 near '...' 定位;保留字用反引号包裹;检查中文全角标点 |
ERROR 1146 (42S02): Table 'db.t' doesn't exist |
表名错误 / 未选对库 / 大小写不一致 | SELECT DATABASE(); SHOW TABLES; 确认;Linux 下区分大小写,可设 lower_case_table_names=1 |
ERROR 1049 (42000): Unknown database 'x' |
库不存在 | SHOW DATABASES; 确认,或先建库 |
ERROR 1054 (42S22): Unknown column 'x' in 'field list' |
列名错误或表别名用错 | DESC 表名; 核对列名 |
ERROR 1062 (23000): Duplicate entry 'x' for key 'PRIMARY' |
主键 / 唯一键冲突 | 改用 INSERT IGNORE、REPLACE INTO 或 ON DUPLICATE KEY UPDATE |
ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails |
删除 / 更新被子表引用的父表记录 | 先删子表数据;或改外键为 ON DELETE CASCADE / SET NULL;或临时 SET FOREIGN_KEY_CHECKS=0 |
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails |
子表插入的外键值在父表不存在 | 先插父表记录;或该列置 NULL;导入数据时临时 SET FOREIGN_KEY_CHECKS=0 |
ERROR 1215 (HY000): Cannot add foreign key constraint |
父子列类型 / 长度 / 符号不一致,或父列无索引,或引擎不是 InnoDB | 统一列定义,父列建主键或唯一索引,改用 InnoDB |
ERROR 1055 (42000): ... incompatible with sql_mode=only_full_group_by |
SELECT 列表含非分组列 |
见 4.6 节三种解决办法 |
ERROR 1366 (HY000): Incorrect string value: '\xF0\x9F...' |
向 utf8(mb3) 列插入 emoji |
库表列改为 utf8mb4 |
ERROR 1406 (22001): Data too long for column |
严格模式下值超出列长度 | 加长列,或截断数据;不建议关闭严格模式 |
ERROR 1364 (HY000): Field 'x' doesn't have a default value |
严格模式下 NOT NULL 列无默认值又未赋值 |
插入时显式给值,或给列加 DEFAULT |
ERROR 1292 (22007): Incorrect date value: '0000-00-00' |
NO_ZERO_DATE 严格模式 |
使用合法日期或 NULL;必要时调整 sql_mode |
中文显示为 ??? 或乱码 |
客户端 / 连接 / 库表列字符集不一致 | 见 2.13 节乱码解决流程;SET NAMES utf8mb4 |
ERROR 1040 (HY000): Too many connections |
连接数超过 max_connections |
调大 max_connections;排查连接泄漏、清理 Sleep 连接、使用连接池 |
ERROR 1129 (HY000): Host 'x' is blocked because of many connection errors |
该主机连接失败次数超过 max_connect_errors |
mysqladmin flush-hosts 或 FLUSH HOSTS; |
ERROR 1290 (HY000): ... --secure-file-priv option |
导入导出目录受限 | 见 4.5 节 |
ERROR 1175 (HY000): You are using safe update mode |
开启了 sql_safe_updates |
补索引列 WHERE 或加 LIMIT,或临时关闭 |
ERROR 1093 (HY000): You can't specify target table 'x' for update in FROM clause |
同一语句中更新的表又出现在子查询 | 子查询再包一层:... IN (SELECT id FROM (SELECT ...) AS tmp) |
| 服务启动失败 | 配置错误、目录权限、端口占用、磁盘满 | 看 /var/log/mysqld.log;chown -R mysql:mysql /var/lib/mysql;df -h;netstat -ntlup \| grep 3306 |
| 数据目录迁移后启动失败 | 目录权限、SELinux 上下文、socket 路径不一致 | chown -R mysql:mysql /data;setenforce 0 或修复上下文;统一 [mysqld]/[client] 的 socket |
| 远程无法连接 | 防火墙、bind-address、用户 host 不为 % 或对应网段 |
逐项检查并授权 |
# 常用排错命令
tail -100 /var/log/mysqld.log
mysqladmin -u root -p ping # 探测服务是否存活
mysqladmin -u root -p status
mysqladmin -u root -p processlist
mysqladmin -u root -p flush-hosts
mysqladmin -u root -p variables | grep max_connections
telnet 10.0.0.61 3306 # 测试端口连通性
mysql -u root -p -e "SHOW ENGINE INNODB STATUS\G" | head -50
4.10 练习
说明:源文档未提供练习题,以下练习基于源文档的操作要点(建库建表、插入中文数据、查询、密码管理、参数修改、字符集)编写,答案与命令均可直接执行验证。
练习 1(源文档实验复刻):创建库 a,在其中创建表 a1(id, name),插入两条中文记录并查询。
CREATE DATABASE a;
SHOW DATABASES;
USE a;
CREATE TABLE a1(id INT(1), name CHAR(10));
INSERT INTO a1(id, name) VALUES(1, "测试库");
INSERT INTO a1(id, name) VALUES(2, "mysql库");
SELECT * FROM a1;
练习 2:查看当前数据库实例的默认存储引擎与数据目录。
SHOW VARIABLES LIKE '%engine%';
SHOW VARIABLES LIKE 'datadir';
练习 3:查看当前所在库、当前登录用户,并列出所有数据库用户。
SELECT DATABASE();
SELECT USER();
SELECT user, host, password FROM mysql.user; -- MariaDB / MySQL 5.6-
SELECT user, host, authentication_string FROM mysql.user; -- MySQL 5.7+
练习 4:为 root@localhost 设置密码 123,随后改为 456(分别用命令行与 SQL 两种方式)。
mysqladmin -u root password "123"
mysqladmin -u root password '456' -p123
SET PASSWORD FOR 'root'@'localhost' = PASSWORD('456');
-- 或
UPDATE mysql.user SET password=PASSWORD('456') WHERE user='root' AND host='localhost';
FLUSH PRIVILEGES;
练习 5:把最大连接数临时调整为 300,并说明如何永久生效。
SHOW VARIABLES LIKE '%connections';
SET GLOBAL max_connections = 300;
答案:动态修改重启后失效(回到 151),永久生效需在 /etc/my.cnf 的 [mysqld] 段写 max_connections=300 并重启服务。
练习 6:删除 /var/lib/mysql/mysql.sock 后本地 mysql 登录报什么错?如何绕过?如何恢复?
答案:报 ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock' (2);可用 mysql -h 127.0.0.1 走 TCP/IP 绕过;重启服务(systemctl restart mariadb)后 socket 文件会重新生成。
练习 7:忘记 root 密码,写出完整重置步骤。
答案:/etc/my.cnf 的 [mysqld] 加 skip-grant-tables → 重启服务 → 直接 mysql 无密码登录 → UPDATE mysql.user SET password=PASSWORD('123') WHERE user='root' AND host='localhost'; FLUSH PRIVILEGES;(MySQL 5.7+ 用 FLUSH PRIVILEGES; 后 ALTER USER)→ 删除 skip-grant-tables → 重启服务 → 用新密码登录验证。
练习 8:建库 school 并建表 student,要求 id 自增主键、name 非空、sex 枚举、score 两位小数、手机号唯一、字符集 utf8mb4、引擎 InnoDB。
CREATE DATABASE IF NOT EXISTS school DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;
USE school;
CREATE TABLE student (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(20) NOT NULL,
sex ENUM('M','F') NOT NULL DEFAULT 'M',
score DECIMAL(5,2) DEFAULT NULL,
phone CHAR(11) DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_phone (phone)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
练习 9:为 student 表添加 email VARCHAR(50) 列放在 name 之后,然后把它改名为 mail 并加长到 100。
ALTER TABLE student ADD email VARCHAR(50) AFTER name;
ALTER TABLE student CHANGE email mail VARCHAR(100);
DESC student;
练习 10:查询每个院系的人数与平均分,只保留平均分大于 75 的院系,按平均分降序输出前 3 名。
SELECT dept, COUNT(*) AS 人数, ROUND(AVG(score),2) AS 平均分
FROM student
GROUP BY dept
HAVING AVG(score) > 75
ORDER BY 平均分 DESC
LIMIT 3;
练习 11:查出所有没填邮箱的学生姓名,并把 NULL 显示为"未填写"。
SELECT name FROM student WHERE email IS NULL;
SELECT name, IFNULL(email, '未填写') AS 邮箱 FROM student;
答案要点:不能写 WHERE email = NULL,必须用 IS NULL。
练习 12:按每页 5 条分页,写出查询第 3 页的 SQL,并给出通用公式。
SELECT * FROM student ORDER BY id LIMIT 10, 5;
答案:公式为 LIMIT (页码-1)*每页条数, 每页条数,第 3 页每页 5 条即 LIMIT 10, 5。
练习 13:把 student 表所有物理系学生的分数加 5 分,要求先验证再执行且可回滚。
SELECT id, name, score FROM student WHERE dept='物理';
START TRANSACTION;
UPDATE student SET score = score + 5 WHERE dept='物理';
SELECT id, name, score FROM student WHERE dept='物理';
COMMIT; -- 确认无误提交;有问题则 ROLLBACK;
练习 14:为学生表按分数分级输出(>=90 优秀、>=80 良好、>=60 及格、其余不及格、NULL 缺考)。
SELECT name, score,
CASE
WHEN score IS NULL THEN '缺考'
WHEN score >= 90 THEN '优秀'
WHEN score >= 80 THEN '良好'
WHEN score >= 60 THEN '及格'
ELSE '不及格'
END AS 等级
FROM student;
练习 15:创建只能从 10.0.0.% 网段访问 school 库、且只有增删改查权限的用户 app,然后查看并回收它的删除权限。
CREATE USER 'app'@'10.0.0.%' IDENTIFIED BY 'App@123456';
GRANT SELECT, INSERT, UPDATE, DELETE ON school.* TO 'app'@'10.0.0.%';
SHOW GRANTS FOR 'app'@'10.0.0.%';
REVOKE DELETE ON school.* FROM 'app'@'10.0.0.%';
FLUSH PRIVILEGES;
练习 16:备份 school 库到 /backup/school.sql,然后删除库并从备份恢复。
mysqldump -u root -p --single-transaction --databases school > /backup/school.sql
mysql -u root -p -e "DROP DATABASE school;"
mysql -u root -p < /backup/school.sql
mysql -u root -p -e "SHOW DATABASES; SELECT COUNT(*) FROM school.student;"
练习 17:解释 DELETE、TRUNCATE、DROP 三者的区别。
答案:DELETE 属 DML,逐行删除数据,可带 WHERE、可回滚、触发触发器、不重置自增值;TRUNCATE 属 DDL,清空整表数据,速度快、不可回滚、重置自增值、不触发触发器;DROP 属 DDL,连表结构一起删除。
练习 18:插入 emoji 报 Incorrect string value 该如何处理?
答案:目标列字符集为 utf8(即 utf8mb3,最多 3 字节),无法存 4 字节 emoji。需把库、表、列都改为 utf8mb4:
ALTER DATABASE school CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
SET NAMES utf8mb4;
并在 /etc/my.cnf 中设置 character-set-server=utf8mb4,重启服务。
五、常用操作速查小结
| 操作 | 命令 / 语句 |
|---|---|
安装(详见 11-02) |
yum install mariadb* -y |
| 启动(CentOS 7 / 6) | systemctl restart mariadb / service mysqld restart |
安全初始化(详见 11-02) |
mysql_secure_installation |
| 检查监听 | netstat -ntlup \| grep 3306 |
| 登录 | mysql -u 用户 -p,远程 mysql -u u -h IP -P 端口 -p |
| 免交互执行 | mysql -u root -p -e "SQL" |
| 执行脚本 | source /path/f.sql 或 mysql -u root -p db < f.sql |
| 查看库 / 表 / 结构 | SHOW DATABASES; SHOW TABLES; DESC 表; SHOW CREATE TABLE 表; |
| 当前库 / 当前用户 | SELECT DATABASE(); SELECT USER(); |
| 建库(带字符集) | CREATE DATABASE IF NOT EXISTS db DEFAULT CHARSET utf8mb4; |
| 建表 | CREATE TABLE t(...) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| 改表 | ALTER TABLE t ADD/DROP/MODIFY/CHANGE ... |
| 删表 / 清表 | DROP TABLE t; / TRUNCATE t; / DELETE FROM t; |
| 增删改 | INSERT INTO、UPDATE ... WHERE、DELETE FROM ... WHERE |
| 查询 | SELECT 列 FROM 表 WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT ... |
| 纵向显示 | 语句结尾用 \G |
| 修改密码(无密码) | mysqladmin -u root password "123" |
| 修改密码(有密码) | mysqladmin -u root password '456' -p123 |
| 库内改密 | SET PASSWORD FOR 'root'@'localhost'=PASSWORD('123'); 或 ALTER USER ... IDENTIFIED BY '...' |
| 改权限表改密 | UPDATE mysql.user SET password=PASSWORD("...") ...; FLUSH PRIVILEGES; |
| 忘记密码 | [mysqld] 加 skip-grant-tables → 重启 → 改密 → 去掉配置 → 重启 |
| 建用户 / 授权 / 回收 | CREATE USER、GRANT ... ON db.* TO、REVOKE ... FROM |
| 查看权限 | SHOW GRANTS FOR 'u'@'h'; |
| 查看 / 改参数 | SHOW VARIABLES LIKE '...'; SET GLOBAL 参数=值;(永久改 /etc/my.cnf) |
| 查看状态 / 会话 | SHOW STATUS; SHOW FULL PROCESSLIST; KILL id; |
备份 / 恢复(详见 11-05) |
mysqldump -u root -p --databases db > f.sql / mysql -u root -p < f.sql |
| 导出 CSV | SELECT ... INTO OUTFILE '...' FIELDS TERMINATED BY ',' |
| 导入 CSV | LOAD DATA INFILE '...' INTO TABLE t ... |
| 关闭数据库 | mysqladmin shutdown -p(推荐)/ systemctl stop mariadb;不建议 pkill |
数据目录迁移(详见 11-02) |
停服务 → cp -r → chown mysql:mysql → 改 datadir/socket → 重启 |
此处评论已关闭