欢迎光临
我们一直在努力

Day 20 MySQL_DDL_DML教程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

文章目录

    • 一、先聊版本:为什么是 8.4
    • 二、一键安装 MySQL 8.4(附加固脚本)
      • 2.1 脚本核心逻辑(节选)
      • 2.2 装完怎么连
    • 三、SQL 语句的五大分类(先把地图看懂)
    • 四、DDL —— 数据定义语言
      • 4.1 它到底在管什么
      • 4.2 数据库级操作
      • 4.3 表级操作:建一张规范的表
      • 4.4 修改表结构:ALTER TABLE
      • 4.5 删除表:DROP vs TRUNCATE
    • 五、DML —— 数据操纵语言
      • 5.1 它到底在管什么
      • 5.2 INSERT:插入数据
      • 5.3 UPDATE:修改数据
      • 5.4 DELETE:删除数据
      • 5.5 事务里验证 DML 可回滚
    • 六、DQL —— 数据查询语言
      • 6.1 它到底在管什么
      • 6.2 最基础的查询:投影 + 别名
      • 6.3 WHERE:把条件写全
      • 6.4 ORDER BY + LIMIT:排序与分页
      • 6.5 聚合 + GROUP BY + HAVING
      • 6.6 JOIN:多表联查
      • 6.7 子查询
      • 6.8 CASE WHEN:在查询里做条件分支
      • 6.9 窗口函数(MySQL 8.0+ / 8.4 完全支持)
      • 6.10 EXPLAIN:查询慢了先看它
      • 6.11 DQL 执行顺序(背下来,写 SQL 不再懵)
    • 七、DCL —— 数据控制语言
      • 10.1 它到底在管什么
      • 10.2 创建用户:'user'@'host' 是两部分
      • 10.3 授权 GRANT + 查看权限
      • 10.4 实测权限:允许的和被拒的都看一眼
      • 10.5 回收权限 REVOKE
      • 10.6 角色 ROLE:8.0+ / 8.4 推荐的批量授权方式
      • 10.7 改密、删用户,以及 FLUSH PRIVILEGES 的真相
      • 10.8 生产权限配置建议
    • 八、TCL —— 事务控制语言
      • 8.1 它到底在管什么
      • 8.2 先看 autocommit:默认每条 SQL 都自动提交
      • 8.3 完整事务过程:转账(代码 + 结果)
      • 8.4 ROLLBACK:出错了整体撤销
      • 8.5 SAVEPOINT:只回滚其中一部分
      • 8.6 隔离级别:并发事务之间隔多远
      • 8.7 DDL / DCL 会隐式提交事务(最容易翻车的一条)
      • 8.8 事务排错与最佳实践
    • 九、五大类核心对比(一张表记住)
    • 十、综合实战:从 0 到 1 走一遍(五类串起来)
      • 10.1 建库建表(DDL)
      • 10.2 插数据(DML)
      • 10.3 查 + 改(DML)
      • 10.4 改结构(DDL)
      • 10.5 统计(DQL)
      • 10.6 补上 DCL 和 TCL:一条业务线的最后一环
    • 十一、生产避坑清单(血泪版)
    • 十二、速查表

环境:Ubuntu 22.04 / 24.04,MySQL 8.4 LTS 本篇三条主线:① 一键安装 8.4 并加固;② 把 SQL 五大类(DDL / DML / DQL / DCL / TCL)全部讲透,每条命令都给「代码 → 执行结果 → 解释」;③ 一次业务场景把五类串起来跑一遍。


一、先聊版本:为什么是 8.4

MySQL 的版本线其实很清晰,别一上来就纠结:

版本定位现状建议
5.7 老一代 已停止官方支持,新项目别用
8.0 LTS 长期支持 存量项目主力,仍可用
8.4 LTS 长期支持(当前推荐) 新装首选,支持到 2032 年左右

8.4 几个必须知道的变化(踩坑重灾区):

  • 默认字符集 utf8mb4,排序规则 utf8mb4_0900_ai_ci,中文/emoji 都不会乱码。
  • 默认认证插件 caching_sha2_password;mysql_native_password 在 8.4 默认被禁用,老客户端连不上就是这个原因。
  • 不再自带 test 库,安装完是干净的。
  • 默认存储引擎 InnoDB,支持事务、行锁、外键。
  • 一句话:新装就上 8.4 LTS,稳定、支持周期长、默认配置更安全。


    二、一键安装 MySQL 8.4(附加固脚本)

    手动装 MySQL 最烦的是:加官方源、选版本、弹紫灰色配置界面、装完还要一条条加固。这里用一份脚本一次搞定,分三个阶段:

    阶段1:安装 → 加官方 apt 源,装 mysql-server 8.4
    阶段2:安全加固 → 删匿名用户、禁远程 root、清 test 库、关 local-infile、限 bind-address
    阶段3:安全检测 → 逐项检查并输出 PASS / FAIL

    2.1 脚本核心逻辑(节选)

    #!/bin/bash
    # 功能:一键自动化安装 MySQL 并完成安全加固与安全检测
    # 适用:Ubuntu 20.04 / 22.04 / 24.04 (Debian 系)
    # 用法:sudo bash mysql_install_hardening.sh

    set -u # 只用 -u,防止未定义变量;不加 pipefail,避免查询失败直接退出

    MYSQL_VERSION="8.4"
    APT_CONFIG_VER="0.8.40"
    ALLOW_REMOTE="0" # 0=仅本机, 1=允许远程

    ① 加官方源 + 预置版本(关键:避开交互弹窗)

    DEB_FILE="mysql-apt-config_${APT_CONFIG_VER}-1_all.deb"
    wget -q "https://dev.mysql.com/get/${DEB_FILE}"

    # 8.4 的选项名为 mysql-8.4-lts
    PRESET_VER="mysql-8.4-lts"
    echo "mysql-apt-config mysql-apt-config/select-server select ${PRESET_VER}" | debconf-set-selections

    DEBIAN_FRONTEND=noninteractive dpkg -i "${DEB_FILE}"
    apt-get update -y

    执行结果(节选):

    ==> 正在下载 mysql-apt-config…
    Selecting previously unselected package mysql-apt-config.
    (Reading database … 182345 files and directories currently installed.)
    Preparing to unpack mysql-apt-config_0.8.40-1_all.deb …
    Unpacking mysql-apt-config (0.8.40-1) …
    Setting up mysql-apt-config (0.8.40-1) …

    DEBIAN_FRONTEND=noninteractive + debconf-set-selections 这两招,就是让安装全程无人工交互的秘诀。

    ② 校验源里真的有 8.4,再装

    SRC_VERS=$(apt-cache madison mysql-server | grep -oE '[0-9]+\\.[0-9]+' | sort -u)
    echo "${SRC_VERS}" | grep -q "${MYSQL_VERSION}" || { echo "源中无 8.4"; exit 1; }

    # 预置 root 密码,防止安装过程卡住
    echo "mysql-server mysql-server/root_password password ${MYSQL_ROOT_PWD}" | debconf-set-selections
    echo "mysql-server mysql-server/root_password_again password ${MYSQL_ROOT_PWD}" | debconf-set-selections

    DEBIAN_FRONTEND=noninteractive apt-get install -y mysql-server
    systemctl enable –now mysql

    执行结果(节选):

    [INFO] 版本校验通过:8.4 可用
    Setting up mysql-server-8.4 (8.4.5-1ubuntu24.04) …
    [INFO] MySQL 服务已启动

    ③ 安全加固(纯 SQL + 配置文件 + 防火墙)

    mysql -uroot <<EOF
    DELETE FROM mysql.user WHERE User='';
    DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost','127.0.0.1','::1');
    DROP DATABASE IF EXISTS test;
    DELETE FROM mysql.db WHERE Db='test' OR Db LIKE 'test\\_%';
    FLUSH PRIVILEGES;
    EOF

    再改配置文件 /etc/mysql/mysql.conf.d/mysqld.cnf:

    bind-address = 127.0.0.1 # 只监听本机,不暴露公网
    local-infile = 0 # 关闭本地文件导入,防注入读文件
    symbolic-links = 0 # 关闭符号链接,防越权

    ④ 最后自动体检

    ===== 阶段3:安全检测 =====
    [PASS] MySQL 服务正在运行
    [INFO] MySQL 版本:8.4.5
    [PASS] MySQL 仅监听本机 (127.0.0.1)
    [PASS] 无匿名用户
    [PASS] 不存在可远程登录的 root
    [PASS] test 库已删除
    [PASS] local-infile 已关闭

    ===== 检测结论 =====
    ✅ 全部安全检查通过,MySQL 已安装并加固完成。
    root 采用 auth_socket 免密,本机 sudo mysql 即可管理

    2.2 装完怎么连

    Ubuntu 下 root 默认走 auth_socket,本机直接 sudo 免密登录:

    sudo mysql # 直接进,不输密码
    mysql -uroot -p # 或在已设密码时用这种方式

    登录成功:

    Welcome to the MySQL monitor. Commands end with ; or \\g.
    Your MySQL connection id is 12
    Server version: 8.4.5 MySQL Community Server – GPL

    mysql>

    查看版本,确认装对了:

    SELECT VERSION();

    +———–+
    | VERSION() |
    +———–+
    | 8.4.5 |
    +———–+
    1 row in set (0.00 sec)


    三、SQL 语句的五大分类(先把地图看懂)

    写 SQL 之前,先在心里建立这张地图。很多人把 DDL 说成"数据控制语言",这是错的——控制语言是 DCL。

    分类全称中文管什么代表命令
    DDL Data Definition Language 数据定义语言 库、表、索引等结构 CREATE ALTER DROP TRUNCATE RENAME
    DML Data Manipulation Language 数据操纵语言 表里的数据行 INSERT UPDATE DELETE REPLACE
    DQL Data Query Language 数据查询语言 查询数据(常归入 DML) SELECT
    DCL Data Control Language 数据控制语言 权限、用户 GRANT REVOKE
    TCL Transaction Control Language 事务控制语言 事务提交/回滚 COMMIT ROLLBACK SAVEPOINT

    记忆口诀:D 定义结构、M 操纵数据、Q 查、C 控权限、T 控事务。

    本篇五大类全部讲,顺序按真实学习路径来:先 DDL 建容器 → DML 灌数据 → DQL 查数据 → DCL 配权限 → TCL 保事务。每一类都给「作用 + 完整代码 + 执行结果 + 逐条解释」。


    四、DDL —— 数据定义语言

    4.1 它到底在管什么

    DDL 管的是**"容器"本身**:数据库、表、字段、索引、约束。 它不关心表里有什么数据,只管这张表长什么样。

    特点(考试/面试常问):

    • 自动提交,执行即生效,不能 ROLLBACK;
    • 多数操作会隐式提交当前事务;
    • 对大表做 ALTER 可能锁表,生产要挑低峰期。

    4.2 数据库级操作

    ① 创建数据库(指定字符集,避免中文乱码)

    CREATE DATABASE shop
    DEFAULT CHARACTER SET utf8mb4
    DEFAULT COLLATE utf8mb4_0900_ai_ci;

    Query OK, 1 row affected (0.01 sec)

    ② 查看、切换、删除

    SHOW DATABASES; — 看有哪些库
    USE shop; — 切换库
    SELECT DATABASE(); — 确认当前在哪个库
    DROP DATABASE IF EXISTS shop; — 删库(危险!)

    mysql> USE shop;
    Database changed
    mysql> SELECT DATABASE();
    +————+
    | DATABASE() |
    +————+
    | shop |
    +————+
    1 row in set (0.00 sec)

    IF EXISTS 是好习惯:库不存在时不会报错中断脚本。

    4.3 表级操作:建一张规范的表

    这是 DDL 的重头戏。建表要把字段类型、约束、注释、引擎、字符集一次写清楚。

    CREATE TABLE users (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    username VARCHAR(32) NOT NULL COMMENT '用户名',
    password CHAR(64) NOT NULL COMMENT '密码哈希',
    age TINYINT UNSIGNED DEFAULT NULL COMMENT '年龄',
    balance DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '余额',
    status TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0禁用',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (id),
    UNIQUE KEY uk_username (username)
    ) ENGINE=InnoDB
    DEFAULT CHARSET=utf8mb4
    COMMENT='用户表';

    Query OK, 0 rows affected (0.02 sec)

    逐项解释(这部分最值钱):

    写法作用为什么这么写
    BIGINT UNSIGNED 大整数、无符号 主键用 BIGINT 防未来溢出;无符号翻倍上限
    VARCHAR(32) 变长字符串 用户名够用,别开 255 浪费
    CHAR(64) 定长字符串 存密码哈希(如 SHA-256 十六进制 64 位)刚好
    DECIMAL(10,2) 精确小数 金额绝不能用 FLOAT/DOUBLE,会丢精度
    DATETIME DEFAULT CURRENT_TIMESTAMP 默认当前时间 插入时不用手写创建时间
    PRIMARY KEY (id) 主键 唯一且非空,InnoDB 聚簇索引
    UNIQUE KEY uk_username 唯一索引 用户名不允许重复
    ENGINE=InnoDB 存储引擎 支持事务、行锁、外键
    CHARSET=utf8mb4 字符集 支持 emoji、生僻字
    COMMENT '…' 注释 团队协作、后期维护救命

    查看表结构与建表语句

    SHOW TABLES;
    DESC users;
    SHOW CREATE TABLE users\\G

    在这里插入图片描述

    SHOW CREATE TABLE users\\G 会把完整建表语句(含所有默认值、字符集)还原出来:

    在这里插入图片描述

    迁移、备份、排错时,SHOW CREATE TABLE 比 DESC 信息全得多。

    4.4 修改表结构:ALTER TABLE

    业务一变,表结构就得跟着改。ALTER 是 DDL 里最常用的。

    ① 加字段

    ALTER TABLE users
    ADD COLUMN email VARCHAR(128) DEFAULT NULL COMMENT '邮箱' AFTER username;

    Query OK, 0 rows affected (0.03 sec)
    Records: 0 Duplicates: 0 Warnings: 0

    AFTER username 指定新字段插在 username 后面,不写就默认加在最后。

    ② 改字段类型/属性

    — MODIFY:只改属性,不改字段名
    ALTER TABLE users MODIFY COLUMN age TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '年龄';

    — CHANGE:可同时改字段名 + 类型(必须重写完整定义)
    ALTER TABLE users CHANGE COLUMN email mail VARCHAR(150) DEFAULT NULL COMMENT '邮箱';

    Query OK, 0 rows affected (0.02 sec)
    Records: 0 Duplicates: 0 Warnings: 0

    CHANGE 必须把新字段的完整定义写一遍,漏了 NOT NULL/DEFAULT 就会被改掉,这是高频事故点。

    ③ 删字段

    ALTER TABLE users DROP COLUMN mail;

    Query OK, 0 rows affected (0.02 sec)
    Records: 0 Duplicates: 0 Warnings: 0

    ④ 加索引

    ALTER TABLE users ADD INDEX idx_status_created (status, created_at);

    Query OK, 0 rows affected (0.04 sec)
    Records: 0 Duplicates: 0 Warnings: 0

    联合索引 (status, created_at) 适合"按状态筛 + 按时间排"的查询。

    ⑤ 改表名

    RENAME TABLE users TO sys_users;
    — 或 ALTER TABLE users RENAME TO sys_users;

    Query OK, 0 rows affected (0.01 sec)

    4.5 删除表:DROP vs TRUNCATE

    这两个都是 DDL,但差别很大:

    TRUNCATE TABLE sys_users; — 清空数据,保留表结构
    DROP TABLE IF EXISTS sys_users; — 连表带结构一起删

    mysql> TRUNCATE TABLE sys_users;
    Query OK, 0 rows affected (0.03 sec)

    mysql> DROP TABLE IF EXISTS sys_users;
    Query OK, 0 rows affected (0.01 sec)

    对比TRUNCATEDROP
    表结构 保留 删除
    数据 全部清空 全部清空
    可否回滚 否(DDL 隐式提交) 否
    速度 快(重建表) 快
    自增计数器 重置为 1 —
    触发触发器 不触发 —

    生产铁律:DROP / TRUNCATE 执行前先 SELECT COUNT(*) 确认,重要表先备份。


    五、DML —— 数据操纵语言

    5.1 它到底在管什么

    DML 管的是表里的数据行:增、删、改。 和 DDL 最大的区别:DML 支持事务,可以 ROLLBACK 回滚(InnoDB 下)。

    5.2 INSERT:插入数据

    ① 单行插入(推荐写全字段名)

    INSERT INTO users (username, password, age, balance)
    VALUES ('zhangsan', SHA2('123456', 256), 25, 100.50);

    Query OK, 1 row affected (0.01 sec)

    ② 多行插入(一条语句插多条,效率高)

    INSERT INTO users (username, password, age, balance) VALUES
    ('lisi', SHA2('123456', 256), 30, 200.00),
    ('wangwu', SHA2('123456', 256), 28, 50.00),
    ('zhaoliu',SHA2('123456', 256), 35, 999.99);

    Query OK, 3 rows affected (0.01 sec)
    Records: 3 Duplicates: 0 Warnings: 0

    ③ 插入并查看结果

    SELECT id, username, age, balance, created_at FROM users;

    在这里插入图片描述

    id 自增、created_at 自动填当前时间,都不用我们管。

    ④ INSERT … SELECT:把查询结果直接插进去

    — 备份表:结构与 users 相同
    CREATE TABLE users_bak LIKE users;

    INSERT INTO users_bak (username, password, age, balance)
    SELECT username, password, age, balance FROM users WHERE status = 1;

    Query OK, 4 rows affected (0.01 sec)
    Records: 4 Duplicates: 0 Warnings: 0

    CREATE TABLE … LIKE 只复制结构不复制数据;INSERT … SELECT 才是搬数据。两个配合就是表备份的标准姿势。

    ⑤ REPLACE:有则替换、无则插入

    REPLACE INTO users (id, username, password, age, balance)
    VALUES (1, 'zhangsan', SHA2('654321', 256), 26, 300.00);

    Query OK, 2 rows affected (0.01 sec)

    注意 2 rows affected:REPLACE 本质是先删后插,所以是"删 1 行 + 插 1 行"。它会改变自增 id 之外的整行数据,慎用;更推荐 INSERT … ON DUPLICATE KEY UPDATE。

    5.3 UPDATE:修改数据

    ① 指定条件更新(务必带 WHERE)

    UPDATE users SET balance = balance + 50.00 WHERE username = 'lisi';

    Query OK, 1 row affected (0.01 sec)
    Rows matched: 1 Changed: 1 Warnings: 0

    Rows matched 是命中的行数,Changed 是实际变化行数。如果值没变,matched=1 但 changed=0。

    ② 多字段同时更新

    UPDATE users
    SET age = 31, status = 1
    WHERE username = 'lisi';

    Query OK, 1 row affected (0.00 sec)
    Rows matched: 1 Changed: 1 Warnings: 0

    ③ 批量更新(按条件命中多行)

    — 余额低于 100 的统一加 20
    UPDATE users SET balance = balance + 20.00 WHERE balance < 100;

    Query OK, 2 rows affected (0.01 sec)
    Rows matched: 2 Changed: 2 Warnings: 0

    ④ 验证修改结果

    SELECT username, age, balance, status FROM users;

    +———-+——+———+——–+
    | username | age | balance | status |
    +———-+——+———+——–+
    | zhangsan | 26 | 300.00 | 1 |
    | lisi | 31 | 250.00 | 1 |
    | wangwu | 28 | 70.00 | 1 |
    | zhaoliu | 35 | 1019.99 | 1 |
    +———-+——+———+——–+
    4 rows in set (0.00 sec)

    ⚠️ UPDATE 不带 WHERE = 全表更新,这是生产第一大事故。执行前先 SELECT 同样条件看看命中几行。

    5.4 DELETE:删除数据

    ① 按条件删除

    DELETE FROM users WHERE username = 'zhaoliu';

    Query OK, 1 row affected (0.01 sec)

    ② 删除前先确认(好习惯)

    — 先看会删掉什么
    SELECT * FROM users WHERE status = 0;
    — 确认无误再删
    DELETE FROM users WHERE status = 0;

    Query OK, 0 rows affected (0.00 sec)

    ③ DELETE 全部 vs TRUNCATE

    DELETE FROM users; — 逐行删,可回滚,慢,自增不重置
    TRUNCATE TABLE users; — 重建表,不可回滚,快,自增重置为 1

    mysql> DELETE FROM users;
    Query OK, 3 rows affected (0.01 sec)

    对比DELETE (DML)TRUNCATE (DDL)
    分类 DML DDL
    能否带 WHERE 能(可精准删) 不能(整表清空)
    可否回滚 能(InnoDB) 不能
    速度 慢(逐行 + 写日志) 快
    自增 id 不重置 重置为 1
    触发触发器 会 不会

    结论:要精准删几行用 DELETE … WHERE;要清空整表用 TRUNCATE。别用 DELETE FROM t 清大表,慢且占日志。

    5.5 事务里验证 DML 可回滚

    DML 支持事务,这是它和 DDL 最本质的区别:

    START TRANSACTION;
    DELETE FROM users WHERE username = 'wangwu';
    SELECT COUNT(*) FROM users; — 事务内看到已删(3 行)
    ROLLBACK;
    SELECT COUNT(*) FROM users; — 回滚后数据回来(4 行)

    mysql> START TRANSACTION;
    Query OK, 0 rows affected (0.00 sec)
    mysql> DELETE FROM users WHERE username = 'wangwu';
    Query OK, 1 row affected (0.00 sec)
    mysql> ROLLBACK;
    Query OK, 0 rows affected (0.00 sec)

    但同样的事换成 DDL 就不行了:

    START TRANSACTION;
    TRUNCATE TABLE users; — DDL 隐式提交,事务里的改动直接生效
    ROLLBACK; — 无法挽回


    六、DQL —— 数据查询语言

    6.1 它到底在管什么

    DQL 管的是**“把数据按条件捞出来看”**,核心命令只有一条:SELECT。

    三个特点:

    • 只读:不改变表里任何数据(所以很多教材把它归进 DML,但按功能单列更清楚);
    • 用得最多:线上 80% 以上的 SQL 都是查询语句;
    • 最容易写出慢 SQL:一条 SELECT * 全表扫,能把数据库拖垮。

    为了让下面每条查询的执行结果都能对得上,本节重新准备一份干净的演示数据(实际练习时在自己库里跑即可,不影响前面章节的理解)。

    准备数据(DDL + DML)

    USE shop;

    DROP TABLE IF EXISTS orders;
    DROP TABLE IF EXISTS users;

    CREATE TABLE users (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    username VARCHAR(32) NOT NULL,
    age TINYINT UNSIGNED NOT NULL,
    balance DECIMAL(10,2) NOT NULL DEFAULT 0.00,
    status TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0禁用',
    city VARCHAR(20) DEFAULT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    UNIQUE KEY uk_username (username)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

    INSERT INTO users (username, age, balance, status, city) VALUES
    ('zhangsan', 25, 100.50, 1, '北京'),
    ('lisi', 31, 250.00, 1, '上海'),
    ('wangwu', 28, 70.00, 1, '北京'),
    ('zhaoliu', 35, 1019.99, 1, '广州'),
    ('sunqi', 22, 0.00, 0, '上海');

    CREATE TABLE orders (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    user_id BIGINT UNSIGNED NOT NULL,
    order_no VARCHAR(32) NOT NULL,
    amount DECIMAL(10,2) NOT NULL,
    status TINYINT NOT NULL DEFAULT 0 COMMENT '0待付 1已付 2已发',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    KEY idx_user (user_id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

    INSERT INTO orders (user_id, order_no, amount, status) VALUES
    (1, 'NO001', 199.00, 1),
    (1, 'NO002', 59.90, 0),
    (2, 'NO003', 299.00, 1),
    (2, 'NO004', 88.00, 1),
    (3, 'NO005', 35.50, 2),
    (4, 'NO006', 499.00, 1);

    Query OK, 0 rows affected (0.02 sec)
    Query OK, 5 rows affected (0.01 sec)
    Records: 5 Duplicates: 0 Warnings: 0
    Query OK, 0 rows affected (0.02 sec)
    Query OK, 6 rows affected (0.01 sec)
    Records: 6 Duplicates: 0 Warnings: 0

    看一眼基准数据,后面所有查询结果都从这里推出来:

    SELECT id, username, age, balance, status, city FROM users;

    +—-+———-+—–+———+——–+——–+
    | id | username | age | balance | status | city |
    +—-+———-+—–+———+——–+——–+
    | 1 | zhangsan | 25 | 100.50 | 1 | 北京 |
    | 2 | lisi | 31 | 250.00 | 1 | 上海 |
    | 3 | wangwu | 28 | 70.00 | 1 | 北京 |
    | 4 | zhaoliu | 35 | 1019.99 | 1 | 广州 |
    | 5 | sunqi | 22 | 0.00 | 0 | 上海 |
    +—-+———-+—–+———+——–+——–+
    5 rows in set (0.00 sec)

    6.2 最基础的查询:投影 + 别名

    — 只取需要的列,并起中文别名(中文别名建议用反引号包起来)
    SELECT username AS `姓名`, age AS `年龄`, balance AS `余额`
    FROM users
    WHERE status = 1;

    +———-+——–+———+
    | 姓名 | 年龄 | 余额 |
    +———-+——–+———+
    | zhangsan | 25 | 100.50 |
    | lisi | 31 | 250.00 |
    | wangwu | 28 | 70.00 |
    | zhaoliu | 35 | 1019.99 |
    +———-+——–+———+
    4 rows in set (0.00 sec)

    为什么不写 SELECT *:只取需要的列,能走覆盖索引、少传数据、表结构变更后程序也不容易崩。生产里 SELECT * 是明确的坏习惯。

    6.3 WHERE:把条件写全

    ① 比较 + 逻辑组合

    SELECT username, age, balance FROM users
    WHERE age BETWEEN 25 AND 32
    AND city = '北京'
    AND status = 1;

    +———-+—–+———+
    | username | age | balance |
    +———-+—–+———+
    | zhangsan | 25 | 100.50 |
    | wangwu | 28 | 70.00 |
    +———-+—–+———+
    2 rows in set (0.00 sec)

    BETWEEN 25 AND 32 等价于 age >= 25 AND age <= 32(含两端)。

    ② IN / LIKE / NOT / IS NULL

    SELECT username, city FROM users
    WHERE city IN ('北京', '上海') ORDER BY id;

    +———-+——–+
    | username | city |
    +———-+——–+
    | zhangsan | 北京 |
    | lisi | 上海 |
    | wangwu | 北京 |
    | sunqi | 上海 |
    +———-+——–+
    4 rows in set (0.00 sec)

    SELECT username FROM users WHERE username LIKE 'z%'; — z 开头

    +———-+
    | username |
    +———-+
    | zhangsan |
    | zhaoliu |
    +———-+
    2 rows in set (0.00 sec)

    写法含义备注
    = / <> / != 等于、不等于 <> 是标准 SQL 写法
    BETWEEN a AND b 区间(含端点) 适合数值、日期
    IN (…) 命中列表任一值 比一堆 OR 清晰
    LIKE 'z%' 模糊匹配 % 任意多字符,_ 一个字符
    IS NULL / IS NOT NULL 判空 不能写 = NULL
    AND / OR / NOT 逻辑组合 多条件记得加括号

    索引大坑:WHERE age + 1 > 30、WHERE DATE(created_at) = '…' 这类对列做运算/函数,索引会失效;写成 age > 29 才能走索引。字符串字段传数字也会触发隐式类型转换导致全表扫。

    6.4 ORDER BY + LIMIT:排序与分页

    SELECT username, balance FROM users ORDER BY balance DESC LIMIT 3;

    +———-+———+
    | username | balance |
    +———-+———+
    | zhaoliu | 1019.99 |
    | lisi | 250.00 |
    | zhangsan | 100.50 |
    +———-+———+
    3 rows in set (0.00 sec)

    分页公式 LIMIT (页码-1)*每页条数, 每页条数:

    SELECT id, username FROM users ORDER BY id LIMIT 2, 2; — 第 2 页,每页 2 条

    +—-+———-+
    | id | username |
    +—-+———-+
    | 3 | wangwu |
    | 4 | zhaoliu |
    +—-+———-+
    2 rows in set (0.00 sec)

    深分页(LIMIT 100000, 20)会先扫过 10 万行再丢弃,很慢。优化思路:用上一页最大 id 做条件 WHERE id > 上一页最大id ORDER BY id LIMIT 20。

    6.5 聚合 + GROUP BY + HAVING

    SELECT city,
    COUNT(*) AS cnt,
    ROUND(AVG(balance), 2) AS avg_bal,
    SUM(balance) AS total
    FROM users
    GROUP BY city
    HAVING COUNT(*) >= 2
    ORDER BY total DESC;

    +——–+—–+———+——–+
    | city | cnt | avg_bal | total |
    +——–+—–+———+——–+
    | 上海 | 2 | 125.00 | 250.00 |
    | 北京 | 2 | 85.25 | 170.50 |
    +——–+—–+———+——–+
    2 rows in set (0.00 sec)

    解释:按城市分组后,上海(250.00 + 0.00 = 250.00)、北京(100.50 + 70.00 = 170.50)、广州(1019.99);HAVING COUNT(*) >= 2 把只有 1 人的广州滤掉;再按 total 倒序。

    常用聚合函数:COUNT() 计数、SUM() 求和、AVG() 平均、MAX()/MIN() 极值、GROUP_CONCAT() 把多行拼成一个字符串。

    WHERE 和 HAVING 别搞混(面试高频):

    对比WHEREHAVING
    作用时机 分组前过滤原始行 分组后过滤分组结果
    能否用聚合函数 不能 能(HAVING COUNT(*) > 1)
    能否用字段别名 不能 部分场景可以
    性能 先过滤少算数据,优先用 分组后才过滤

    6.6 JOIN:多表联查

    ① INNER JOIN 取两表都匹配的行

    SELECT u.username, o.order_no, o.amount
    FROM users u
    INNER JOIN orders o ON u.id = o.user_id
    WHERE o.status = 1
    ORDER BY o.amount DESC;

    +———-+———+——–+
    | username | order_no| amount |
    +———-+———+——–+
    | zhaoliu | NO006 | 499.00 |
    | lisi | NO003 | 299.00 |
    | zhangsan | NO001 | 199.00 |
    | lisi | NO004 | 88.00 |
    +———-+———+——–+
    4 rows in set (0.00 sec)

    u、o 是表别名,ON u.id = o.user_id 就是两表的关联条件;INNER JOIN 只返回两边都对得上的行。

    ② LEFT JOIN 找"没有订单的用户"

    SELECT u.username, COUNT(o.id) AS order_cnt
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
    GROUP BY u.id, u.username
    ORDER BY order_cnt;

    +———-+———–+
    | username | order_cnt |
    +———-+———–+
    | sunqi | 0 |
    | wangwu | 1 |
    | zhaoliu | 1 |
    | zhangsan | 2 |
    | lisi | 2 |
    +———-+———–+
    5 rows in set (0.00 sec)

    关键点:LEFT JOIN 保留左表全部行,右表没匹配上补 NULL;所以统计订单数要用 COUNT(o.id)(NULL 不计入),写成 COUNT(*) 会把 sunqi 算成 1。

    JOIN 类型结果
    INNER JOIN 只返回两表都匹配上的行
    LEFT JOIN 左表全保留 + 右表匹配部分(不匹配补 NULL)
    RIGHT JOIN 右表全保留(日常用 LEFT 改写即可)
    CROSS JOIN 笛卡尔积,行数 = 两表行数相乘,慎用

    6.7 子查询

    — 标量子查询:余额高于全表平均值的人
    SELECT username, balance FROM users
    WHERE balance > (SELECT AVG(balance) FROM users);

    +———-+———+
    | username | balance |
    +———-+———+
    | zhaoliu | 1019.99 |
    +———-+———+
    1 row in set (0.00 sec)

    全表平均 (100.50+250.00+70.00+1019.99+0.00)/5 = 288.098,只有 zhaoliu 超过。

    — IN 子查询:下过 200 元以上订单的用户
    SELECT username FROM users
    WHERE id IN (SELECT user_id FROM orders WHERE amount > 200);

    +———-+
    | username |
    +———-+
    | lisi |
    | zhaoliu |
    +———-+
    2 rows in set (0.00 sec)

    大表场景下,IN (子查询) 有时不如 JOIN 或 EXISTS 高效,用 EXPLAIN 看执行计划再定。

    6.8 CASE WHEN:在查询里做条件分支

    SELECT username, balance,
    CASE WHEN balance >= 500 THEN '高'
    WHEN balance >= 100 THEN '中'
    ELSE '低' END AS level
    FROM users ORDER BY balance DESC;

    +———-+———+——-+
    | username | balance | level |
    +———-+———+——-+
    | zhaoliu | 1019.99 | 高 |
    | lisi | 250.00 | 中 |
    | zhangsan | 100.50 | 中 |
    | wangwu | 70.00 | 低 |
    | sunqi | 0.00 | 低 |
    +———-+———+——-+
    5 rows in set (0.00 sec)

    常见用途:把状态码翻译成人话、按条件分类统计(配合 SUM(CASE WHEN … THEN 1 ELSE 0 END))。

    6.9 窗口函数(MySQL 8.0+ / 8.4 完全支持)

    SELECT username, city, balance,
    ROW_NUMBER() OVER (PARTITION BY city ORDER BY balance DESC) AS rn
    FROM users ORDER BY city, rn;

    +———-+——–+———+—-+
    | username | city | balance | rn |
    +———-+——–+———+—-+
    | zhangsan | 北京 | 100.50 | 1 |
    | wangwu | 北京 | 70.00 | 2 |
    | lisi | 上海 | 250.00 | 1 |
    | sunqi | 上海 | 0.00 | 2 |
    | zhaoliu | 广州 | 1019.99 | 1 |
    +———-+——–+———+—-+
    5 rows in set (0.00 sec)

    PARTITION BY city = 按城市分区,ORDER BY balance DESC = 区内排序,ROW_NUMBER() 给区内每行编号。

    窗口函数 vs GROUP BY:GROUP BY 会把多行压缩成一行(5 行变 3 行);窗口函数不压缩行数(还是 5 行),只是给每行多算一个值。排行榜、组内 Top N、累计求和都靠它。常用还有 RANK()、DENSE_RANK()、LAG()/LEAD()、SUM() OVER (…)。

    6.10 EXPLAIN:查询慢了先看它

    EXPLAIN SELECT * FROM users WHERE username = 'lisi';

    +—-+————-+——-+——-+—————+————-+———+——-+——+——-+
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
    +—-+————-+——-+——-+—————+————-+———+——-+——+——-+
    | 1 | SIMPLE | users | const | uk_username | uk_username | 130 | const | 1 | NULL |
    +—-+————-+——-+——-+—————+————-+———+——-+——+——-+
    1 row in set, 1 warning (0.00 sec)

    type=const 且 key=uk_username 说明命中唯一索引、只扫 1 行,非常健康。如果看到 type=ALL(全表扫描)或 Using filesort / Using temporary,就该考虑加索引或改写 SQL 了。

    6.11 DQL 执行顺序(背下来,写 SQL 不再懵)

    FROM → JOIN → WHERE → GROUP BY → HAVING
    → SELECT → DISTINCT → ORDER BY → LIMIT

    这也是为什么 WHERE 里不能用 SELECT 定义的别名,而 ORDER BY 可以——别名在 SELECT 阶段才产生。


    七、DCL —— 数据控制语言

    10.1 它到底在管什么

    DCL 管的是**“谁能连上来、能看哪个库、能干什么操作”**,核心命令:CREATE USER / GRANT / REVOKE / SHOW GRANTS。

    三个特点:

    • 只有管理员能执行(需要 CREATE USER 或 GRANT OPTION 权限);
    • 立即生效、隐式提交,不能 ROLLBACK;
    • 遵循最小权限原则:应用账号只给它需要的库、需要的表、需要的操作。

    顺带把草稿里那个错再钉一遍:"数据控制语言"是 DCL,不是 DDL。DDL 是"数据定义语言"。

    10.2 创建用户:‘user’@‘host’ 是两部分

    CREATE USER 'dev'@'localhost' IDENTIFIED BY 'Dev@123456';

    Query OK, 0 rows affected (0.01 sec)

    MySQL 的账号是 用户名 + 主机 的组合,'dev'@'localhost' 和 'dev'@'%' 是两个不同账号:

    host 写法含义
    localhost 只能本机连
    127.0.0.1 只能本机 TCP 连
    192.168.1.% 只允许该网段
    % 任意主机(生产慎用,必须配防火墙)

    看看 8.4 给新用户用的是哪个认证插件:

    SELECT user, host, plugin FROM mysql.user WHERE user = 'dev';

    +——+———–+———————–+
    | user | host | plugin |
    +——+———–+———————–+
    | dev | localhost | caching_sha2_password |
    +——+———–+———————–+
    1 row in set (0.00 sec)

    正是前面说的 8.4 默认插件——老版本客户端驱动不认它,就会报 Authentication plugin 'caching_sha2_password' cannot be loaded,升级驱动即可。

    10.3 授权 GRANT + 查看权限

    GRANT SELECT, INSERT, UPDATE ON shop.* TO 'dev'@'localhost';
    SHOW GRANTS FOR 'dev'@'localhost';

    Query OK, 0 rows affected (0.01 sec)

    +—————————————————————+
    | Grants for dev@localhost |
    +—————————————————————+
    | GRANT USAGE ON *.* TO `dev`@`localhost` |
    | GRANT SELECT, INSERT, UPDATE ON `shop`.* TO `dev`@`localhost` |
    +—————————————————————+
    2 rows in set (0.00 sec)

    GRANT USAGE 别紧张,它的意思是"没有任何权限,只有连接权",是创建用户后自带的底座。

    权限的作用范围(从大到小):

    写法范围例子
    ON *.* 所有库所有表 只给 DBA
    ON shop.* shop 库全部表 应用账号最常用
    ON shop.users 单表 精细化管控
    ON shop.users(username) 单列 极少用
    ON PROCEDURE shop.p1 存储过程/函数 特殊场景

    10.4 实测权限:允许的和被拒的都看一眼

    用新账号从命令行连进去操作(这一步最能理解 DCL 到底管住了什么):

    mysql -udev -p -e "SELECT * FROM shop.users LIMIT 1;" # SELECT 已授权
    mysql -udev -p -e "DELETE FROM shop.users WHERE id = 1;" # DELETE 未授权

    +—-+———-+—–+———+——–+——–+
    | id | username | age | balance | status | city |
    +—-+———-+—–+———+——–+——–+
    | 1 | zhangsan | 25 | 100.50 | 1 | 北京 |
    +—-+———-+—–+———+——–+——–+

    ERROR 1142 (42000) at line 1: DELETE command denied to user 'dev'@'localhost' for table 'users'

    一条查得到、一条被拒,这就是 DCL 的实际效果。

    10.5 回收权限 REVOKE

    REVOKE UPDATE ON shop.* FROM 'dev'@'localhost';
    SHOW GRANTS FOR 'dev'@'localhost';

    Query OK, 0 rows affected (0.01 sec)

    +——————————————————+
    | Grants for dev@localhost |
    +——————————————————+
    | GRANT USAGE ON *.* TO `dev`@`localhost` |
    | GRANT SELECT, INSERT ON `shop`.* TO `dev`@`localhost`|
    +——————————————————+
    2 rows in set (0.00 sec)

    UPDATE 没了,只剩 SELECT, INSERT。

    10.6 角色 ROLE:8.0+ / 8.4 推荐的批量授权方式

    给一堆人配同样的权限,别一条条 GRANT,用角色一次搞定:

    CREATE ROLE 'read_only';
    GRANT SELECT ON shop.* TO 'read_only';
    GRANT 'read_only' TO 'dev'@'localhost';
    SET DEFAULT ROLE 'read_only' TO 'dev'@'localhost';

    Query OK, 0 rows affected (0.01 sec)

    SHOW GRANTS FOR 'dev'@'localhost' USING 'read_only';

    +——————————————————–+
    | Grants for dev@localhost |
    +——————————————————–+
    | GRANT USAGE ON *.* TO `dev`@`localhost` |
    | GRANT SELECT, INSERT ON `shop`.* TO `dev`@`localhost` |
    | GRANT SELECT ON `shop`.* TO `read_only` |
    | GRANT `read_only`@`%` TO `dev`@`localhost` |
    +——————————————————–+
    4 rows in set (0.00 sec)

    注意:授予角色后必须 SET DEFAULT ROLE(或会话里 SET ROLE),否则用户登录后角色默认不激活,会出现"明明授权了却没权限"的经典困惑。

    10.7 改密、删用户,以及 FLUSH PRIVILEGES 的真相

    ALTER USER 'dev'@'localhost' IDENTIFIED BY 'New@123456';
    DROP USER IF EXISTS 'dev'@'localhost';

    Query OK, 0 rows affected (0.01 sec)
    Query OK, 0 rows affected (0.00 sec)

    很多人背错了:用 CREATE USER / GRANT / REVOKE / ALTER USER 改权限,MySQL 自动重载权限表,不需要再执行 FLUSH PRIVILEGES。只有当你直接 UPDATE mysql.user 这种手工改系统表的野路子,才需要 FLUSH PRIVILEGES 让它生效(第二章脚本里就属于这种情况)。

    10.8 生产权限配置建议

    场景建议
    应用账号 只给业务库的 SELECT/INSERT/UPDATE/DELETE,不给 DROP/ALTER
    报表/只读从库 只给 SELECT,最好再配只读实例
    管理员 root 禁止 % 远程,本机 auth_socket(第二章脚本已加固)
    密码 8.4 默认 validate_password 组件可开强密码策略
    定期审计 SELECT user, host FROM mysql.user; 查有没有遗留测试账号

    八、TCL —— 事务控制语言

    8.1 它到底在管什么

    TCL 管的是**“一组 SQL 要么全成功、要么全失败”**,核心命令:START TRANSACTION / COMMIT / ROLLBACK / SAVEPOINT / SET TRANSACTION。

    前提:只有 InnoDB 引擎支持事务(MyISAM 不支持,这也是 8.4 默认 InnoDB 的原因之一)。

    事务的四个特性 ACID:

    特性含义落到 MySQL 上
    A 原子性 一组操作不可分割 ROLLBACK 靠 undo log 撤销
    C 一致性 数据从一个合法状态到另一个 约束、主键、外键共同保证
    I 隔离性 并发事务互不干扰 隔离级别 + MVCC + 锁
    D 持久性 提交后掉电也不丢 redo log

    8.2 先看 autocommit:默认每条 SQL 都自动提交

    SELECT @@autocommit;

    +————–+
    | @@autocommit |
    +————–+
    | 1 |
    +————–+
    1 row in set (0.00 sec)

    autocommit=1 表示每执行一条 DML 就悄悄 COMMIT 一次,所以你直接 UPDATE 是回滚不掉的。想手动控制,两种写法:

    SET autocommit = 0; — 方式一:整个会话都进手动模式(别忘了改回来)
    START TRANSACTION; — 方式二:推荐,只把接下来这一组包成事务

    8.3 完整事务过程:转账(代码 + 结果)

    CREATE TABLE accounts (
    id INT PRIMARY KEY,
    name VARCHAR(20) NOT NULL,
    balance DECIMAL(10,2) NOT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

    INSERT INTO accounts VALUES (1, '张三', 1000.00), (2, '李四', 1000.00);

    Query OK, 0 rows affected (0.02 sec)
    Query OK, 2 rows affected (0.01 sec)
    Records: 2 Duplicates: 0 Warnings: 0

    START TRANSACTION;
    UPDATE accounts SET balance = balance – 200 WHERE id = 1; — 张三扣 200
    UPDATE accounts SET balance = balance + 200 WHERE id = 2; — 李四加 200
    SELECT * FROM accounts; — 事务内看:已变
    COMMIT;
    SELECT * FROM accounts; — 提交后:持久生效

    mysql> START TRANSACTION;
    Query OK, 0 rows affected (0.00 sec)
    mysql> UPDATE accounts SET balance = balance – 200 WHERE id = 1;
    Query OK, 1 row affected (0.00 sec)
    Rows matched: 1 Changed: 1 Warnings: 0
    mysql> UPDATE accounts SET balance = balance + 200 WHERE id = 2;
    Query OK, 1 row affected (0.00 sec)
    Rows matched: 1 Changed: 1 Warnings: 0

    mysql> SELECT * FROM accounts;
    +—-+——–+———+
    | id | name | balance |
    +—-+——–+———+
    | 1 | 张三 | 800.00 |
    | 2 | 李四 | 1200.00 |
    +—-+——–+———+
    2 rows in set (0.00 sec)

    mysql> COMMIT;
    Query OK, 0 rows affected (0.01 sec)

    mysql> SELECT * FROM accounts;
    +—-+——–+———+
    | id | name | balance |
    +—-+——–+———+
    | 1 | 张三 | 800.00 |
    | 2 | 李四 | 1200.00 |
    +—-+——–+———+
    2 rows in set (0.00 sec)

    关键点:两条 UPDATE 被包成一个整体。如果第二条失败(比如余额字段溢出、连接断开),整个事务回滚,绝不会出现"张三扣了钱、李四没收到"。

    8.4 ROLLBACK:出错了整体撤销

    START TRANSACTION;
    UPDATE accounts SET balance = balance – 500 WHERE id = 1;
    SELECT * FROM accounts; — 事务内:300 / 1200
    ROLLBACK;
    SELECT * FROM accounts; — 回滚后:恢复成 800 / 1200

    mysql> START TRANSACTION;
    Query OK, 0 rows affected (0.00 sec)
    mysql> UPDATE accounts SET balance = balance – 500 WHERE id = 1;
    Query OK, 1 row affected (0.00 sec)
    Rows matched: 1 Changed: 1 Warnings: 0
    mysql> SELECT * FROM accounts;
    +—-+——–+———+
    | 1 | 张三 | 300.00 |
    | 2 | 李四 | 1200.00 |
    +—-+——–+———+
    mysql> ROLLBACK;
    Query OK, 0 rows affected (0.01 sec)
    mysql> SELECT * FROM accounts;
    +—-+——–+———+
    | 1 | 张三 | 800.00 |
    | 2 | 李四 | 1200.00 |
    +—-+——–+———+
    2 rows in set (0.00 sec)

    8.5 SAVEPOINT:只回滚其中一部分

    START TRANSACTION;
    UPDATE accounts SET balance = balance – 100 WHERE id = 1; — 第①步:张三 700
    SAVEPOINT sp1; — 打个存档点
    UPDATE accounts SET balance = balance – 100 WHERE id = 2; — 第②步:李四 1100
    ROLLBACK TO SAVEPOINT sp1; — 只撤销第②步
    COMMIT; — 提交第①步
    SELECT * FROM accounts;

    mysql> SAVEPOINT sp1;
    Query OK, 0 rows affected (0.00 sec)
    mysql> ROLLBACK TO SAVEPOINT sp1;
    Query OK, 0 rows affected (0.00 sec)
    mysql> COMMIT;
    Query OK, 0 rows affected (0.01 sec)
    mysql> SELECT * FROM accounts;
    +—-+——–+———+
    | id | name | balance |
    +—-+——–+———+
    | 1 | 张三 | 700.00 |
    | 2 | 李四 | 1200.00 |
    +—-+——–+———+
    2 rows in set (0.00 sec)

    结果说明:张三的 700 保留了,李四仍是最初的 1200 —— 只回滚了存档点之后的操作。长事务里分阶段提交/撤销很好用。

    8.6 隔离级别:并发事务之间隔多远

    SELECT @@transaction_isolation;

    +————————-+
    | @@transaction_isolation |
    +————————-+
    | REPEATABLE-READ |
    +————————-+
    1 row in set (0.00 sec)

    MySQL InnoDB 默认是 REPEATABLE-READ(可重复读)。

    隔离级别脏读不可重复读幻读说明
    READ UNCOMMITTED 可能 可能 可能 几乎不用
    READ COMMITTED 避免 可能 可能 互联网常用,并发好
    REPEATABLE-READ 避免 避免 InnoDB 下基本避免 MySQL 默认
    SERIALIZABLE 避免 避免 避免 全串行,性能最差

    SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
    SELECT @@transaction_isolation;

    Query OK, 0 rows affected (0.00 sec)
    +————————-+
    | @@transaction_isolation |
    +————————-+
    | READ-COMMITTED |
    +————————-+
    1 row in set (0.00 sec)

    • 脏读:读到别人还没提交的数据;
    • 不可重复读:同一事务里两次读同一行,结果不一样(被别人 UPDATE 了);
    • 幻读:同一条件两次查询,多出来几行(被别人 INSERT 了)。

    注意 SET SESSION 只对当前连接生效,SET GLOBAL 才影响新连接(且不改当前会话)。

    8.7 DDL / DCL 会隐式提交事务(最容易翻车的一条)

    事务里只要插进一条 DDL(或多数 DCL),MySQL 会先悄悄 COMMIT 再执行它,前面的改动就此定局:

    START TRANSACTION;
    DELETE FROM accounts WHERE id = 1; — 想删了待会儿回滚
    ALTER TABLE accounts ADD COLUMN remark VARCHAR(50); — ⚠️ 隐式提交!
    ROLLBACK; — 已经来不及了
    SELECT * FROM accounts;

    mysql> DELETE FROM accounts WHERE id = 1;
    Query OK, 1 row affected (0.00 sec)
    mysql> ALTER TABLE accounts ADD COLUMN remark VARCHAR(50);
    Query OK, 0 rows affected (0.03 sec)
    Records: 0 Duplicates: 0 Warnings: 0
    mysql> ROLLBACK;
    Query OK, 0 rows affected (0.00 sec)
    mysql> SELECT * FROM accounts;
    +—-+——–+———+——–+
    | id | name | balance | remark |
    +—-+——–+———+——–+
    | 2 | 李四 | 1200.00 | NULL |
    +—-+——–+———+——–+
    1 row in set (0.00 sec)

    张三那行没回来——因为 ALTER 已经把事务提交了,后面的 ROLLBACK 等于空转。这也解释了第四章里"DDL 不能回滚"的真正原因。

    会触发隐式提交的命令(记个大概):CREATE / ALTER / DROP / TRUNCATE / RENAME 等所有 DDL,CREATE USER / GRANT / REVOKE / ALTER USER 等多数 DCL,以及 LOCK TABLES、LOAD DATA。

    8.8 事务排错与最佳实践

    SELECT @@innodb_lock_wait_timeout; — 锁等待超时,默认 50 秒
    SHOW ENGINE INNODB STATUS\\G — 查最近一次死锁详情

    +—————————-+
    | @@innodb_lock_wait_timeout |
    +—————————-+
    | 50 |
    +—————————-+
    1 row in set (0.00 sec)

    建议原因
    事务尽量短,尽早 COMMIT/ROLLBACK 长事务占着锁、拖住 undo log、容易锁等待
    别在事务里调外部接口 / 发消息 / 等人工 外部耗时不可控,会把数据库拖死
    多条 UPDATE 固定更新顺序 避免两个事务交叉加锁形成死锁
    批量操作分批提交(如每 1000 条一次) 单事务过大,回滚代价高
    明确写 START TRANSACTION,别依赖 autocommit=0 意图清晰,不怕连接复用串味
    大表 DDL 单独执行,不要塞进事务 反正会隐式提交,还容易误判

    九、五大类核心对比(一张表记住)

    维度DDL 定义DML 操纵DQL 查询DCL 控制TCL 事务
    管什么 库表字段索引结构 表里数据行 把数据查出来 用户与权限 一组 SQL 的整体成败
    代表命令 CREATE / ALTER / DROP / TRUNCATE / RENAME INSERT / UPDATE / DELETE / REPLACE SELECT / JOIN / GROUP BY / EXPLAIN CREATE USER / GRANT / REVOKE / SHOW GRANTS START TRANSACTION / COMMIT / ROLLBACK / SAVEPOINT
    是否改数据 改结构(TRUNCATE 会清数据) 改数据 不改(只读) 改权限表 不直接改,决定 DML 是否落地
    是否自动提交 是(隐式提交) 否(需显式 COMMIT) — 是(立即生效) 本身就是提交/回滚
    能否 ROLLBACK 不能 能(InnoDB) — 不能 —
    执行频率 低(发版时) 高(业务写) 最高(业务读) 极低(运维配置) 高(随业务事务)
    典型风险 误 DROP、大表 ALTER 锁表 忘写 WHERE 全表更新 SELECT * 全表扫、深分页 权限给太大、root 远程 长事务、死锁、DDL 隐式提交
    谁在写 DBA / 开发 业务代码 业务代码 + 数据分析 DBA 业务代码

    一条 SQL 的归类不用死记:看它动的是什么——动结构是 DDL、动行是 DML、只看不动是 DQL、动权限是 DCL、管这批操作的成败是 TCL。


    十、综合实战:从 0 到 1 走一遍(五类串起来)

    把五大类串起来,做一个「用户 + 订单」的小场景:DDL 建表 → DML 写数据 → DQL 查统计 → DCL 给只读账号 → TCL 用事务改状态,正好是一条完整业务线。

    10.1 建库建表(DDL)

    CREATE DATABASE IF NOT EXISTS shop DEFAULT CHARSET=utf8mb4;
    USE shop;

    — 本章独立演示,先清掉上一章的订单表,避免重名报错
    DROP TABLE IF EXISTS orders;

    CREATE TABLE orders (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
    order_no VARCHAR(32) NOT NULL COMMENT '订单号',
    amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '金额',
    status TINYINT NOT NULL DEFAULT 0 COMMENT '0待付 1已付 2已发',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    UNIQUE KEY uk_order_no (order_no),
    KEY idx_user_status (user_id, status)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

    Query OK, 0 rows affected (0.02 sec)

    10.2 插数据(DML)

    INSERT INTO orders (user_id, order_no, amount, status) VALUES
    (1, 'NO20261106001', 199.00, 1),
    (1, 'NO20261106002', 59.90, 0),
    (2, 'NO20261106003', 299.00, 2);

    Query OK, 3 rows affected (0.01 sec)
    Records: 3 Duplicates: 0 Warnings: 0

    10.3 查 + 改(DML)

    — 查用户 1 的所有订单
    SELECT order_no, amount, status FROM orders WHERE user_id = 1;

    +—————-+——–+——–+
    | order_no | amount | status |
    +—————-+——–+——–+
    | NO20261106001 | 199.00 | 1 |
    | NO20261106002 | 59.90 | 0 |
    +—————-+——–+——–+
    2 rows in set (0.00 sec)

    — 用户 1 的第二单付款成功
    UPDATE orders SET status = 1 WHERE order_no = 'NO20261106002';

    Query OK, 1 row affected (0.01 sec)
    Rows matched: 1 Changed: 1 Warnings: 0

    10.4 改结构(DDL)

    — 加一个备注字段
    ALTER TABLE orders ADD COLUMN remark VARCHAR(255) DEFAULT NULL COMMENT '备注';
    — 给已付款订单补备注
    UPDATE orders SET remark = '双11活动单' WHERE status = 1;
    — 查结果
    SELECT order_no, status, remark FROM orders;

    +—————-+——–+————–+
    | order_no | status | remark |
    +—————-+——–+————–+
    | NO20261106001 | 1 | 双11活动单 |
    | NO20261106002 | 1 | 双11活动单 |
    | NO20261106003 | 2 | NULL |
    +—————-+——–+————–+
    3 rows in set (0.00 sec)

    10.5 统计(DQL)

    SELECT status, COUNT(*) AS cnt, SUM(amount) AS total
    FROM orders
    GROUP BY status;

    +——–+—–+——–+
    | status | cnt | total |
    +——–+—–+——–+
    | 1 | 2 | 258.90 |
    | 2 | 1 | 299.00 |
    +——–+—–+——–+
    2 rows in set (0.00 sec)

    10.6 补上 DCL 和 TCL:一条业务线的最后一环

    DCL:给报表系统配一个只读账号

    CREATE USER IF NOT EXISTS 'report'@'localhost' IDENTIFIED BY 'Rep@123456';
    GRANT SELECT ON shop.* TO 'report'@'localhost';
    SHOW GRANTS FOR 'report'@'localhost';

    Query OK, 0 rows affected (0.01 sec)
    Query OK, 0 rows affected (0.01 sec)

    +————————————————–+
    | Grants for report@localhost |
    +————————————————–+
    | GRANT USAGE ON *.* TO `report`@`localhost` |
    | GRANT SELECT ON `shop`.* TO `report`@`localhost` |
    +————————————————–+
    2 rows in set (0.00 sec)

    报表只能查、不能改,就算脚本写错也毁不掉数据。

    TCL:把"改状态 + 写备注"包成一个事务

    START TRANSACTION;
    UPDATE orders SET status = 2 WHERE order_no = 'NO20261106001'; — 标记发货
    UPDATE orders SET remark = CONCAT(remark, ' / 已发货') WHERE order_no = 'NO20261106001';
    COMMIT;
    SELECT order_no, status, remark FROM orders WHERE order_no = 'NO20261106001';

    mysql> START TRANSACTION;
    Query OK, 0 rows affected (0.00 sec)
    mysql> COMMIT;
    Query OK, 0 rows affected (0.01 sec)

    +—————-+——–+———————-+
    | order_no | status | remark |
    +—————-+——–+———————-+
    | NO20261106001 | 2 | 双11活动单 / 已发货 |
    +—————-+——–+———————-+
    1 row in set (0.00 sec)

    两条 UPDATE 要么一起生效、要么一起回滚,绝不会出现"发货了但备注没写"的半截状态。

    这一趟五类分别在哪:

    步骤用到的分类代表命令
    建库建表、加字段 DDL CREATE DATABASE / CREATE TABLE / ALTER TABLE
    插入订单、改状态 DML INSERT / UPDATE
    查订单、分组统计 DQL SELECT / GROUP BY
    配只读报表账号 DCL CREATE USER / GRANT
    保证两步一起成功 TCL START TRANSACTION / COMMIT

    十一、生产避坑清单(血泪版)

  • DDL 不能回滚。DROP / TRUNCATE / 大表 ALTER 前,先备份或先确认。
  • UPDATE / DELETE 忘写 WHERE = 全表。执行前用同样条件 SELECT 一遍。
  • 金额用 DECIMAL,别用 FLOAT/DOUBLE,浮点会丢精度,对账能逼疯人。
  • 字符集统一 utf8mb4,别用 utf8(MySQL 的 utf8 只有 3 字节,存不了 emoji)。
  • 主键用 BIGINT UNSIGNED AUTO_INCREMENT,INT 上限 21 亿,业务涨得快会打满。
  • 大表 ALTER 会锁表,用 pt-online-schema-change 或在从库改再切换;或选业务低峰。
  • TRUNCATE 不能带 WHERE,想精准删只能用 DELETE。
  • 8.4 默认禁用 mysql_native_password,老客户端/老驱动连不上,要升级驱动或显式启用插件。
  • 索引不是越多越好,每个索引都拖慢写入、占空间,按查询加。
  • 建表必写 COMMENT,三个月后你会感谢自己。
  • 别在生产写 SELECT *,只取需要的列;大表务必带 WHERE + 合适索引,先看 EXPLAIN。
  • WHERE 里别对列做函数/运算(DATE(created_at)=…、age+1>30),索引会失效;字符串字段别传数字,隐式转换同样全表扫。
  • HAVING 不是 WHERE:分组前过滤用 WHERE(更早、更快),分组后过滤统计结果才用 HAVING;深分页用"上一页最大 id"代替大偏移 LIMIT。
  • 业务账号绝不用 root,权限给到库级就够,别开 DROP/ALTER;'user'@'%' 必须配合防火墙白名单;授予角色后记得 SET DEFAULT ROLE。
  • 事务要短:别在事务里调外部接口、别一次改几十万行;多条 UPDATE 固定顺序防死锁;记住 DDL/DCL 会隐式提交,别把它们塞进事务里指望回滚。

  • 十二、速查表

    目的命令分类
    建库 CREATE DATABASE db DEFAULT CHARSET utf8mb4; DDL
    建表 CREATE TABLE t (id INT PRIMARY KEY); DDL
    看表结构 DESC t; / SHOW CREATE TABLE t\\G —
    加字段 ALTER TABLE t ADD COLUMN c INT; DDL
    改字段 ALTER TABLE t MODIFY COLUMN c BIGINT; DDL
    删字段 ALTER TABLE t DROP COLUMN c; DDL
    加索引 ALTER TABLE t ADD INDEX idx(c); DDL
    清空表 TRUNCATE TABLE t; DDL
    删表 DROP TABLE t; DDL
    插数据 INSERT INTO t (a,b) VALUES (1,2); DML
    批量插 INSERT INTO t (a) VALUES (1),(2),(3); DML
    改数据 UPDATE t SET a=1 WHERE id=2; DML
    删数据 DELETE FROM t WHERE id=2; DML
    备份表 CREATE TABLE t_bak LIKE t; INSERT INTO t_bak SELECT * FROM t; DDL+DML
    条件查询 SELECT a,b FROM t WHERE a=1 ORDER BY id DESC LIMIT 10; DQL
    分组统计 SELECT c, COUNT(*) FROM t GROUP BY c HAVING COUNT(*)>1; DQL
    联表查询 SELECT … FROM a JOIN b ON a.id = b.aid; DQL
    看执行计划 EXPLAIN SELECT …; DQL
    建用户 CREATE USER 'u'@'localhost' IDENTIFIED BY 'pwd'; DCL
    授权 GRANT SELECT, INSERT ON db.* TO 'u'@'localhost'; DCL
    看权限 SHOW GRANTS FOR 'u'@'localhost'; DCL
    回收权限 REVOKE UPDATE ON db.* FROM 'u'@'localhost'; DCL
    开事务 START TRANSACTION; TCL
    提交 / 回滚 COMMIT; / ROLLBACK; TCL
    存档点 SAVEPOINT sp1; ROLLBACK TO SAVEPOINT sp1; TCL
    隔离级别 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; TCL

    千磨万击还坚劲,任尔东西南北风。
    —— 郑燮《竹石》

    数据库的基本功,靠的是一条条 SQL 敲出来的。 愿你扛住每一次报错,把 SQL 五大类练成肌肉记忆,稳扎稳打,前程似锦。

    如果这篇对你有帮助,点个赞、加个关注,秋@枫 每天一个 Linux / 数据库实战知识点,干货不错过。

    赞(0)
    未经允许不得转载:171主机测评 » Day 20 MySQL_DDL_DML教程
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址