javaweb-Day07-mysql

TJCcc 发布于 17 天前 25 次阅读


1. 概述 (Overview)

1.1 什么是 MySQL?

MySQL 是目前最流行的开源关系型数据库管理系统(RDBMS)。它基于 C/S 架构,使用 SQL(结构化查询语言) 进行交互。

1.2 核心存储引擎 (后端必知)

  • InnoDB(默认,重中之重):支持事务(ACID)行级锁MVCC(多版本并发控制)外键。适合高并发写操作。
  • MyISAM(旧版常用):不支持事务,只支持表锁,适合读多写少的静态表(现已逐渐被 InnoDB 取代)。

1.3 SQL 执行顺序(物理层 vs 逻辑层)

  • 物理层连接器(接受客户端链接,校验账号密码从而建立连接) -> 查询缓存(8.0移除) -> 解析器(语法检查并解析) -> 优化器(分析多种执行方案,选开销最小的那种丢给执行器) -> 执行器(拿到优化器给的方案,调用存储引擎接口干活) -> 存储引擎(真正去磁盘读数据,返回给执行器)。
  • 逻辑层(SQL编写顺序)SELECT -> FROM -> JOIN -> WHERE -> GROUP BY -> HAVING -> ORDER BY -> LIMIT
    > 后端铁律:牢记执行顺序才能优化查询,尤其是 WHEREHAVING 的区别。

1.4 MySQL 数据类型 (Data Types) —— 设计表结构的灵魂

后端铁律:数据类型选错,索引失效、存储浪费、计算溢出接踵而至。永远不要用 VARCHAR 存主键,用 FLOAT 存金额!

1. 数值类型 (Numeric Types)

类型占用空间范围 (有符号)应用场景后端硬核提醒
TINYINT1 Byte-128 ~ 127状态码 (status)、性别、布尔值INT 省空间,且性能更好。status 用 0/1/2 就够了。
INT4 Bytes±21亿自增主键 ID、普通数字绝大多数业务主键够用,别上来就 BIGINT
BIGINT8 Bytes±9.22×10¹⁸雪花算法 ID、大厂流水号、时间戳分布式 ID 必用,防止溢出。
DECIMAL(M,D)变长高精度金额、价格、工资绝对不要用 FLOAT/DOUBLE(有精度丢失,钱会算错)。DECIMAL(10,2) 表示总长 10 位,2 位小数。
UNSIGNED-非负agestock将负数范围转为正数,但需注意:UNSIGNED INT 在运算中一旦减到负数会直接报错(BIGINT UNSIGNED 溢出)。

2. 字符串类型 (String Types)

类型最大长度存储特点应用场景后端硬核提醒
CHAR(N)255 字符固定长度,不足补空格手机号(11位)、身份证号、MD5摘要查询极快,适合长度固定的数据。
VARCHAR(N)65,535 字符可变长度,加 1-2 字节记录长度用户名、标题、邮箱、JSON 串日常使用率最高。注意:VARCHAR(255) 和 VARCHAR(10) 磁盘存储开销一样(都存实际长度),但 内存临时表排序时,会按 255 分配内存,极其浪费!所以按需设短
TEXT65,535 字符存大文本,不能设置默认值文章内容、富文本、备注查询会创建临时表,性能低于 VARCHAR。能用 VARCHAR 就不用 TEXT。
BLOB65,535 字节二进制数据图片、文件二进制流强烈不建议把图片/文件存 DB,存 OSS/MinIO 路径即可(VARCHAR)。

3. 日期与时间类型 (Date & Time Types) —— 坑最多的地方

类型占用空间范围应用场景后端硬核提醒
DATE3 Bytes'1000-01-01' ~ '9999-12-31'生日、入职日期仅存日期,不带时间。
TIME3 Bytes-838:59:59 ~ 838:59:59时长、时间段很少单独用。
DATETIME8 Bytes'1000-01-01 00:00:00' ~ '9999-12-31 23:59:59'业务时间(订单创建时间)与时区无关,存的是字面值。你存 2026-08-06 12:00:00,查出来就是这个,不管用户在哪。
TIMESTAMP4 Bytes'1970-01-01 08:00:01' UTC ~ '2038-01-19 11:14:07' UTC审计字段 (created_at, updated_at)受时区影响,存的是 UTC 时间,查询时会自动转为当前 session 时区。但有 2038 年问题(Y2K38),远期系统慎用!
BIGINT (存时间戳)8 Bytes无限制跨语言/跨时区系统虽然可读性差,但兼容性最好,且计算差值方便。

阿里巴巴 Java 开发手册强制推荐DATETIME 优先于 TIMESTAMP,因为 TIMESTAMP 有存储范围限制(1970-2038)。如果表中有 create_timeupdate_time,强烈建议设默认值:DEFAULT CURRENT_TIMESTAMPON UPDATE CURRENT_TIMESTAMP


2. SQL 语言分类 (DDL, DML, DQL, DCL)

为了让你更好理解,我们构建一个电商系统的迷你库,包含两张表:users(用户表) 和 orders(订单表)。注意下面建表已经应用了上面的数据类型最佳实践。

-- 建表语句 (DDL)
CREATE DATABASE IF NOT EXISTS `ecommerce` DEFAULT CHARSET utf8mb4;
USE `ecommerce`;

-- 用户表 (应用数据类型规范)
CREATE TABLE `users` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID (INT够用)',
    `username` VARCHAR(20) NOT NULL COMMENT '用户名 (限制20字符,不设255)',
    `age` TINYINT UNSIGNED DEFAULT 0 COMMENT '年龄 (TINYINT省空间)',
    `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
    `status` TINYINT DEFAULT 1 COMMENT '状态: 0冻结, 1活跃 (用TINYINT)',
    `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间 (用DATETIME避开2038)',
    `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间 (自动更新)',
    PRIMARY KEY (`id`),
    UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 订单表
CREATE TABLE `orders` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `user_id` INT UNSIGNED NOT NULL COMMENT '下单用户ID',
    `amount` DECIMAL(10,2) NOT NULL COMMENT '订单金额 (必须DECIMAL)',
    `product_name` VARCHAR(200) COMMENT '商品名称',
    `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

2.1 DDL (Data Definition Language) —— 数据定义语言

作用:操作数据库对象(库、表、列、索引)的结构,隐式提交事务(不能回滚)

CREATE(创建):见上方建表。

ALTER(修改)

-- 添加列ALTER TABLE `users` ADD COLUMN `phone` CHAR(11) AFTER `email`; -- 手机号用CHAR  

-- 修改列类型
ALTER TABLE `users` MODIFY `age` TINYINT UNSIGNED; 

-- 修改列名及类型 
ALTER TABLE `users` CHANGE `phone` `mobile` VARCHAR(20); 

-- 删除列 
ALTER TABLE `users` DROP COLUMN `mobile`; 

-- 添加索引(常考) 
ALTER TABLE `orders` ADD INDEX `idx_amount` (`amount`);

DROP(删除)DROP TABLE users; (物理删除,不可恢复,线上禁用!)

TRUNCATE(清空)TRUNCATE TABLE orders; (重置自增ID,不可回滚,比 DELETE 快但危险)。

RENAME(重命名)RENAME TABLE users TO sys_users;


2.2 DML (Data Manipulation Language) —— 数据操纵语言

作用:对表中的数据进行增、删、改。会触发事务锁

INSERT(插入)


-- 标准插入 
INSERT INTO `users` (`username`, `age`, `email`) VALUES ('zhangsan', 25, 'zs@qq.com'); 

-- 批量插入(后端优化必用,减少IO次数) 
INSERT INTO `users` (`username`, `age`) VALUES ('lisi', 22), ('wangwu', 30); 

-- 插入并更新(存在则更新,不存在则插入)—— 常用防重 
INSERT INTO `users` (`username`, `age`) VALUES ('zhangsan', 26) ON DUPLICATE KEY UPDATE `age` = 26;

UPDATE(更新)

-- 一定要加 WHERE,除非你想删库跑路 UPDATE `users` SET `status` = 0 WHERE `username` = 'lisi'; 

-- 多列更新 UPDATE `users` SET `age` = 18, `email` = 'new@qq.com' WHERE `id` = 1;

DELETE(删除)

-- 逻辑删除优于物理删除(后端规范:增加 is_deleted 字段) DELETE FROM `users` WHERE `id` = 2; 

-- 推荐逻辑删除UPDATE users SET is_deleted = 1 WHERE id = 2;

2.3 DQL (Data Query Language) —— 数据查询语言(重点)

作用:从数据库中检索数据。后端 80% 的性能瓶颈在这里。

基础查询

-- 1. 简单查询与去重
SELECT username, age FROM users;
SELECT DISTINCT status FROM users; -- 去重

-- 2. 条件查询 (WHERE)
-- 注意:索引失效场景 (如 age+1 > 20 会使索引失效,应写成 age > 19)
SELECT * FROM users WHERE age > 20 AND status = 1;
SELECT * FROM users WHERE email IS NOT NULL; -- 判断空用 IS,不能用 = NULL
SELECT * FROM users WHERE username IN ('zhangsan', 'lisi');
-- 模糊查询 (以张开头,虽然以%开头会让索引失效,但业务中常需注意)
SELECT * FROM users WHERE username LIKE '张%'; 

分组与聚合 (GROUP BY + HAVING)

-- 聚合函数: COUNT, SUM, AVG, MAX, MIN
-- 需求:统计每个用户的订单总金额,且只显示订单总金额大于 100 的用户
SELECT 
    user_id, 
    COUNT(*) AS order_count, 
    SUM(amount) AS total_amount
FROM orders
WHERE created_at > '2025-01-01' -- 分组前的条件过滤
GROUP BY user_id
HAVING total_amount > 100      -- 分组后的条件过滤(这里用了别名,MySQL特有)
ORDER BY total_amount DESC
LIMIT 10;

后端硬核提醒WHERE 过滤行,HAVING 过滤组。能在 WHERE 中过滤的,绝不留到 HAVING(性能差异巨大)。

连接查询 (JOIN) —— 后端最易出错的地方

  • INNER JOIN(内连接):取交集。
  • LEFT JOIN(左连接):左表全量 + 右表匹配项(无则为 NULL)。
  • RIGHT JOIN(右连接):同左连接。
-- 需求:查询所有用户及其订单信息(没有下单的用户也要显示)
SELECT 
    u.id,
    u.username,
    o.id AS order_id,
    o.amount,
    o.product_name
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;

-- 需求:查询下单超过 2 次的用户信息 (子查询 + JOIN)
SELECT u.*, t.order_count
FROM users u
INNER JOIN (
    SELECT user_id, COUNT(*) AS order_count
    FROM orders
    GROUP BY user_id
    HAVING COUNT(*) > 2
) t ON u.id = t.user_id;

子查询 (Subquery)

  • 注意:MySQL 5.6 之后对子查询做了优化,但 EXISTSIN 有区别:
    • IN:先执行子查询(内表),适合子查询结果集较小。
    • EXISTS:先执行外表,适合外表较小,内表巨大的情况(因为走索引)。
-- 查询所有下过单的用户 (IN)
SELECT * FROM users WHERE id IN (SELECT DISTINCT user_id FROM orders);

-- 查询没有下过单的用户 (NOT EXISTS 效率通常高于 NOT IN)
SELECT * FROM users u 
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
  • 配合标量子查询常用的操作符有:= ,<> , > , >= , < , <=
  • 配合列子查询常用的操作符有:in , not in等
  • 配合行子查询常用的操作符有:= ,<> ,in , not in
  • 配合行表查询常用的操作符有:in,常作为临时表使用

窗口函数 (Window Functions) —— MySQL 8.0+ 杀手锏

超级好用!解决“每组取前N条”的难题,告别复杂的临时表。

-- 需求:查询每个用户金额最高的前 2 个订单
WITH ranked_orders AS (
    SELECT 
        *,
        ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY amount DESC) AS rn,
        RANK() OVER (PARTITION BY user_id ORDER BY amount DESC) AS rk, -- 有并列跳号
        DENSE_RANK() OVER (PARTITION BY user_id ORDER BY amount DESC) AS dr -- 有并列不跳号
    FROM orders
)
SELECT * FROM ranked_orders WHERE rn <= 2;

执行顺序图解 (极重要)

你在写 SQL 时脑子里必须过一遍这个执行顺序(注意是执行顺序不是编写顺序):
FROM -> ON -> JOIN -> WHERE -> GROUP BY -> HAVING -> SELECT -> DISTINCT -> ORDER BY -> LIMIT

经典案例(别名报错原因)

-- 错误写法:WHERE 执行在 SELECT 之前,所以不知道 alias
SELECT amount + 1 AS new_amount FROM orders WHERE new_amount > 100; -- 报错!

-- 正确写法:用 HAVING 或 子查询
SELECT amount + 1 AS new_amount FROM orders HAVING new_amount > 100; 
-- 或者套一层子查询 (推荐,逻辑清晰)
SELECT * FROM (SELECT amount + 1 AS new_amount FROM orders) t WHERE new_amount > 100;

2.4 DCL (Data Control Language) —— 数据控制语言

作用:管理数据库权限和安全性(通常由 DBA 或运维操作,后端了解即可)。

  • GRANT(授权)
    sql -- 授予 zhangjie 用户对 ecommerce 库所有表的 SELECT 和 INSERT 权限 GRANT SELECT, INSERT ON ecommerce.* TO 'zhangjie'@'localhost' IDENTIFIED BY 'password123';
  • REVOKE(收回)
    sql -- 收回 DELETE 权限 REVOKE DELETE ON ecommerce.* FROM 'zhangjie'@'localhost';
  • FLUSH PRIVILEGES(刷新权限):修改后立刻生效。

3. 事务 (Transaction) —— 保证数据一致性

3.1 ACID 四大特性

特性含义实现技术(InnoDB)
A (原子性)一组操作要么全部成功,要么全部失败Undo Log (回滚日志)
C (一致性)事务前后,数据库完整性约束不被破坏依赖 A, I, D 共同保证
I (隔离性)并发事务互不干扰锁 + MVCC
D (持久性)提交后数据永久保存,即使宕机Redo Log (重做日志,WAL技术)

3.2 并发事务带来的问题 (三大读现象)

  1. 脏读 (Dirty Read):读到其他事务未提交的数据。
  2. 不可重复读 (Non-Repeatable Read):同一事务内两次读取同一条记录,结果不一致(针对 Update/Delete)。
  3. 幻读 (Phantom Read):同一事务内两次查询,记录数不一致(针对 Insert)。

3.3 隔离级别 (Isolation Level) —— 面试高频

级别隔离级别脏读不可重复读幻读加锁实现
0READ UNCOMMITTED✅ 有✅ 有✅ 有几乎不加锁
1READ COMMITTED (RC)✅ 有✅ 有快照读(普通select无锁)
2REPEATABLE READ (RR)❌ (MVCC) / ✅ (Gap锁)MVCC + Next-Key Lock
3SERIALIZABLE所有读都加锁,并发极差

MySQL 默认隔离级别REPEATABLE READ (RR)。
阿里巴巴规范:通常推荐 READ COMMITTED (RC) 并配合 Binlog 格式为 ROW,因为 RR 的 Gap 锁容易导致死锁,且 RC 下不可重复读问题可以通过业务代码补偿。

3.4 MVCC (多版本并发控制) 核心原理

InnoDB 实现高性能非阻塞读的关键。

  • 隐藏字段:每行数据隐式包含 DB_TRX_ID(事务ID)和 DB_ROLL_PTR(回滚指针)。
  • Read View (读视图):在 RR 级别下,事务开启时创建 Read View,决定哪些版本的数据可见。
  • 一致性读 (快照读):普通 SELECT 不加锁,读的是 Undo Log 构建的历史版本快照,因此不会阻塞写操作。

3.5 事务操作实战

START TRANSACTION;  -- 或 BEGIN;

-- 模拟扣款:张三转 100 给李四
UPDATE account SET money = money - 100 WHERE name = 'zhangsan';
-- 异常模拟: 若此处报错,执行 ROLLBACK
UPDATE account SET money = money + 100 WHERE name = 'lisi';

-- 设置保存点 (嵌套事务常用)
SAVEPOINT sp1;
-- 做一些其他操作...
-- ROLLBACK TO SAVEPOINT sp1; -- 回滚到保存点,不中断整个事务

COMMIT;  -- 提交,持久化到磁盘 (两阶段提交)
-- ROLLBACK; -- 回滚所有操作,Undo Log 重放逆向 SQL

3.6 锁机制 (InnoDB)

  • 乐观锁:不加锁,通过版本号(Version)或时间戳实现(适合读多写少)。
    sql UPDATE goods SET stock = stock - 1, version = version + 1 WHERE id = 1 AND version = #{oldVersion};
  • 悲观锁:加锁(适合写多读少)。
    • 共享锁 (S锁)SELECT ... LOCK IN SHARE MODE;(读读可并发,读写互斥)。
    • 排他锁 (X锁)SELECT ... FOR UPDATE;(常用于手动控制行锁,配合事务)。

4. 索引 (Index) —— 性能调优的灵魂

后端铁律:索引是“空间换时间”的数据结构。不加索引的查询是全表扫描(Full Table Scan),加了索引是走 B+Tree 搜索

4.1 索引的数据结构 (B+Tree)

  • B+Tree 特性:非叶子节点只存 key(不存数据),叶子节点存数据并形成有序双向链表
  • 为什么选 B+Tree 而不是 Hash?
    • Hash 只支持等值查询(=IN),不支持范围查询(><BETWEEN),也不支持排序。
    • B+Tree 天然有序,范围查询极快。

4.2 索引分类

分类说明特点
聚集索引 (Clustered)InnoDB 中主键索引就是聚集索引叶子节点存储整行数据,表即索引。
二级索引 (Secondary)非主键索引叶子节点存储主键 ID,查询需要回表
联合索引 (Composite)多个字段组成的索引 (a,b,c)遵循最左前缀法则
覆盖索引 (Covering)查询字段刚好是索引字段无需回表,直接返回,性能炸裂。
唯一索引/普通索引区分约束唯一索引可加速查询,但影响插入速度(需检查唯一性)。

4.3 最左前缀法则 (实战高频考点)

联合索引 (name, age, sex)

SQL 示例是否命中索引原因
WHERE name = 'A'✅ 命中从最左列开始匹配
WHERE name = 'A' AND age = 18✅ 完全命中匹配了前两列
WHERE age = 18 AND name = 'A'✅ 命中MySQL 优化器会优化顺序
WHERE age = 18❌ 不命中跳过了第一列 name
WHERE name = 'A' AND sex = 1⚠️ 部分命中只用到了 namesex 由于跳过了 age 导致失效
WHERE name LIKE '张%'✅ 命中范围查询在右边,不影响最左
WHERE name LIKE '%张'❌ 不命中最左前缀被破坏(通配符在左边)

4.4 索引失效场景 (防坑指南)

  1. 隐式类型转换phone = 13800138000(phone 是 varchar) -> 索引失效。
  2. 对索引列做运算/函数WHERE age + 1 > 20 -> 失效;WHERE DATE(created_at) = '2023-01-01' -> 失效。
  3. 使用 !=<>:非等值查询大概率不走索引。
  4. OR 连接:如果 OR 两侧有一个字段无索引,整个查询不走索引。应改为 UNION
  5. 数据分布不均:如果表中 90% 都是 status=1,优化器认为全表扫描比走索引回表更快(会走全表扫描)。

4.5 执行计划 (EXPLAIN) —— 调优必备

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

重点关注字段:

  • type(重要)const > eq_ref > ref > range > index > ALL(尽量优化到 refrange)。
  • key:实际使用的索引。
  • rows:预估扫描的行数(越少越好)。
  • Extra:出现 Using filesort(文件排序,极差)或 Using temporary(临时表,极差)时需要立即优化;出现 Using index(覆盖索引,完美)。

4.6 索引下推 (ICP - Index Condition Pushdown)

MySQL 5.6 引入,将 WHERE 的过滤条件下推到存储引擎层,在索引遍历时就过滤掉不符合条件的记录,减少回表次数

比如联合索引 (name, age),查询 WHERE name LIKE '张%' AND age = 20,在 5.6 之前,先回表找到所有姓张的行再过滤年龄;5.6 之后,在索引树里直接把 age != 20 的剔除掉,再去回表。


总结

  1. 建表看类型:金额用 DECIMAL,状态用 TINYINT,时间用 DATETIME,字符串按需设长度(别一上来就是 255)。
  2. 写 SQL 前先看执行计划 (Explain)
  3. 多表关联尽量用 INNER JOIN,少用 LEFT JOIN,能过滤的先过滤(子查询或临时表)
  4. 深分页优化LIMIT 100000, 10 非常慢,应改为 WHERE id > 上次最大值 LIMIT 10(基于覆盖索引)。
  5. 索引不是越多越好,维护索引有成本(写入变慢,占用空间)。
  6. RR 级别下注意间隙锁导致的死锁,高频交易场景可考虑改为 RC 级别。
唯有极致沉淀,才能造就辉煌。
最后更新于 2026-08-09