"天下武功,唯快不破!" ------ SQL优化大师
📚 目录
- 第一章:SQL优化到底是个啥?
- 第二章:索引------数据库的"任意门"
- 第三章:EXPLAIN------SQL界的"透视眼"
- 第四章:查询优化的十八般武艺
- 第五章:表设计优化------打好地基
- 第六章:高级优化技巧
- 第七章:实战案例大解析
- 第八章:常见坑点与避坑指南
第一章:SQL优化到底是个啥?
1.1 引言:你的查询为啥这么慢?😭
想象一下这个场景:
你打开一个网页,点击查询按钮,然后...⏳
等啊等...☕
等啊等...🍵
等啊等...🥱
终于,30秒后,页面显示了结果。这时你的内心:
╔════════════════════════════╗
║ 这是1999年的网速吗??? ║
╚════════════════════════════╝
恭喜你,你遇到了SQL慢查询!这就像:
- 🏃 别人的查询:博尔特百米冲刺(0.1秒)
- 🐌 你的查询:蜗牛爬行(30秒)
1.2 SQL优化是什么?
简单来说,SQL优化就是:
让你的数据库查询从"蜗牛"变"猎豹"的魔法!
具体包括:
- ✅ 让查询速度飞起来
- ✅ 减少服务器资源消耗
- ✅ 提升用户体验
- ✅ 让老板给你加薪(最重要!💰)
1.3 为什么要学SQL优化?
生活比喻时间!🎭
场景一:超市购物
- ❌ 没优化:进超市后,从第一排开始逐个货架找鸡蛋(全表扫描)
- ✅ 优化后:直接看指示牌,去蛋类专区(索引查询)
场景二:图书馆找书
- ❌ 没优化:从第一本书开始翻,一本本找《SQL优化指南》
- ✅ 优化后:用检索系统,输入书名,直接定位
看到了吗?这就是优化的威力!
1.4 SQL性能问题的"罪魁祸首"
erlang
性能杀手榜单 🏆
━━━━━━━━━━━━━━━━━━━━━━━━
👑 冠军:没有索引(占比40%)
🥈 亚军:全表扫描(占比30%)
🥉 季军:SELECT *(占比15%)
4️⃣ 殿军:不合理的JOIN(占比10%)
5️⃣ 其他:各种小毛病(占比5%)
第二章:索引------数据库的"任意门"🚪
2.1 索引到底是个啥?
生活比喻:字典查字
假设你要在字典里找"优化"这个词:
方法A(无索引):
erlang
第1页:阿...不是
第2页:阿爸...不是
第3页:阿姨...不是
...
第800页:优化...找到了!😭
耗时:10分钟
方法B(有索引):
markdown
1. 看拼音索引:you在第780-820页
2. 翻到第780页
3. 找到"优化"
耗时:10秒
这就是索引的威力!
2.2 索引的数据结构:B+树
不要被"B+树"吓到,我用生活例子解释:
css
[总目录]
/ | \
[A-H] [I-P] [Q-Z]
/ \ / \ / \
[A-D][E-H][I-L][M-P][Q-T][U-Z]
| | | | | |
数据 数据 数据 数据 数据 数据
比喻:公司组织架构
- 顶层:总裁(根节点)
- 中层:部门经理(中间节点)
- 底层:员工(叶子节点/数据)
找一个员工,不需要遍历所有人,只需:总裁→对应部门→找到员工
时间复杂度:
- 无索引:O(n) ------ 线性查找
- 有索引:O(log n) ------ 树查找
用人话说:
- 100万条数据,无索引要查100万次
- 有索引只需要查20次左右!
2.3 索引的类型
2.3.1 主键索引(PRIMARY KEY)
sql
CREATE TABLE users (
id INT PRIMARY KEY, -- 主键索引,自动创建
name VARCHAR(50)
);
特点:
- 🔑 唯一性:每个值都不同
- ⚡ 最快:直接定位
- 📌 不能为NULL
生活比喻: 身份证号,独一无二!
2.3.2 唯一索引(UNIQUE INDEX)
sql
CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(100),
UNIQUE INDEX idx_email (email) -- 唯一索引
);
特点:
- ✨ 唯一性:值不能重复
- 🆗 可以为NULL(但只能有一个NULL)
生活比喻: 手机号,每个人一个,但你可以暂时没有手机
2.3.3 普通索引(INDEX)
sql
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT,
INDEX idx_age (age) -- 普通索引
);
特点:
- 🌈 最常用
- 🔄 值可以重复
- 💡 可以为NULL
生活比喻: 年龄,很多人可以同岁
2.3.4 联合索引(复合索引)
sql
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT,
city VARCHAR(50),
INDEX idx_age_city (age, city) -- 联合索引
);
最左前缀原则(重要!)
假设索引是 (age, city):
ini
✅ 能用上索引的查询:
WHERE age = 25 -- 用到了age
WHERE age = 25 AND city = '北京' -- 用到了age和city
WHERE age > 20 AND city = '上海' -- 用到了age和city
❌ 用不上索引的查询:
WHERE city = '北京' -- 跳过了age,用不上!
生活比喻:查电话簿
电话簿按照"姓氏→名字"排序:
李明
李娜
王刚
王丽
张伟
张芳
- ✅ 找"姓李的":很快,按姓氏找
- ✅ 找"姓李名明的":更快,两个都用上
- ❌ 找"名字叫明的":GG,要全扫描,因为没有姓氏
2.4 创建索引的正确姿势
2.4.1 什么情况下该建索引?
vbnet
✅ 应该建索引:
├── WHERE条件中经常出现的列
├── JOIN连接条件的列
├── ORDER BY排序的列
├── GROUP BY分组的列
└── DISTINCT去重的列
❌ 不该建索引:
├── 表数据太少(几百条)
├── 频繁更新的列(写多读少)
├── 重复值太多的列(如性别:男/女)
└── 很少被查询的列
2.4.2 索引命名规范
sql
-- 好的命名 ✅
INDEX idx_user_age -- 普通索引
UNIQUE INDEX uk_user_email -- 唯一索引
INDEX idx_order_user_date -- 联合索引
-- 差的命名 ❌
INDEX index1 -- 啥意思?
INDEX abc -- 谁看得懂?
命名规则:
- 普通索引:
idx_表名_列名
- 唯一索引:
uk_表名_列名
- 主键:
pk_表名
2.5 索引的代价:没有免费的午餐
索引不是越多越好!
sql
📊 索引的代价:
┌──────────────────────────────┐
│ 优点 👍 vs 缺点 👎 │
├──────────────────────────────┤
│ 查询快 vs 占空间 │
│ 排序快 vs 写入慢 │
│ JOIN快 vs 更新慢 │
│ vs 维护成本高 │
└──────────────────────────────┘
生活比喻:书的目录
- 📖 目录多:找内容快,但书更厚,翻阅麻烦
- 📕 目录少:书薄,但找内容慢
经验法则:
- 一个表的索引数量:3-5个为宜
- 联合索引优于多个单列索引
2.6 查看索引
sql
-- 查看表的所有索引
SHOW INDEX FROM users;
-- 查看创建表的语句(含索引)
SHOW CREATE TABLE users;
2.7 删除索引
sql
-- 删除索引
DROP INDEX idx_user_age ON users;
-- 删除主键
ALTER TABLE users DROP PRIMARY KEY;
第三章:EXPLAIN------SQL界的"透视眼"👁️
3.1 EXPLAIN是什么?
EXPLAIN就像:
- 🩺 医生的X光机:看透身体内部
- 🔍 侦探的放大镜:找出真相
- 🎮 游戏的调试模式:看到隐藏数据
它能告诉你:
- SQL怎么执行的?
- 用了哪些索引?
- 扫描了多少行?
- 性能瓶颈在哪?
3.2 EXPLAIN的基本用法
sql
-- 在查询前加上EXPLAIN
EXPLAIN SELECT * FROM users WHERE age = 25;
输出示例:
sql
+----+-------------+-------+------+---------------+----------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+----------+---------+-------+------+-------+
| 1 | SIMPLE | users | ref | idx_age | idx_age | 5 | const | 100 | NULL |
+----+-------------+-------+------+---------------+----------+---------+-------+------+-------+
看晕了?别怕,我一个个讲!
3.3 EXPLAIN字段详解
3.3.1 id:查询序列号
bash
┌─────────────────────────────┐
│ id越大,越先执行 │
│ id相同,从上往下执行 │
└─────────────────────────────┘
示例:
sql
EXPLAIN
SELECT * FROM users WHERE id IN (
SELECT user_id FROM orders WHERE amount > 1000
);
ini
id=2: 先执行子查询orders表
id=1: 再执行主查询users表
生活比喻: 就像做饭的步骤:
- 先洗菜(id=2)
- 再炒菜(id=1)
3.3.2 select_type:查询类型
sql
常见类型:
┌────────────────────────────────────┐
│ SIMPLE - 简单查询(最常见) │
│ PRIMARY - 主查询 │
│ SUBQUERY - 子查询 │
│ DERIVED - 派生表(FROM子查询)│
│ UNION - UNION查询 │
└────────────────────────────────────┘
3.3.3 type:访问类型(⭐超重要!)
这是性能的关键指标!从最优到最差:
sql
性能排行榜:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🏆 system ━━ 表只有一行(系统表)
🥇 const ━━ 主键或唯一索引查询
🥈 eq_ref ━━ 唯一索引查询
🥉 ref ━━ 非唯一索引查询
4️⃣ range ━━ 范围查询(BETWEEN、>、<)
5️⃣ index ━━ 索引全扫描
6️⃣ ALL ━━ 全表扫描(💀危险!)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
生活比喻:找人的方式
ini
type = const: "找身份证号123456的人"
→ 唯一,瞬间定位 ⚡
type = ref: "找所有25岁的人"
→ 多个结果,但很快 🏃
type = range: "找20-30岁的人"
→ 范围查找,还行 🚶
type = index: "看遍所有人的身份证"
→ 慢,但比ALL好 🐌
type = ALL: "看遍所有人的所有信息"
→ 最慢!💀
优化目标:
sql
✅ 至少达到 range 级别
✅ 最好达到 ref 级别
✅ 理想达到 const 级别
❌ 避免 index 和 ALL
3.3.4 possible_keys:可能用到的索引
sql
EXPLAIN SELECT * FROM users WHERE age = 25 AND city = '北京';
-- 结果显示:
possible_keys: idx_age, idx_city, idx_age_city
意思是:MySQL发现有3个索引可以用
生活比喻: 去某地有3条路可选(高速、国道、小路)
3.3.5 key:实际使用的索引
sql
key: idx_age_city
意思是:MySQL最终选了联合索引
生活比喻: 3条路里,选了高速(最快的)
⚠️ 重要情况:
ini
如果 key = NULL:没用索引!危险!🚨
3.3.6 key_len:索引长度
用来判断联合索引用了几列
sql
-- 假设索引是 (age, city)
-- age: INT (4字节) + 是否为NULL (1字节) = 5字节
-- city: VARCHAR(50) (50*3字节UTF8) + 长度 (2字节) + 是否为NULL (1字节) = 153字节
key_len = 5: 只用了age
key_len = 158: 用了age和city(5+153)
3.3.7 ref:索引的哪部分被使用
sql
ref: const -- 常量
ref: db.t1.id -- 其他表的列
ref: func -- 函数结果
3.3.8 rows:估计扫描行数(⭐重要!)
ini
rows = 10: 扫描10行,很快 ✅
rows = 1000: 扫描1000行,还行 🆗
rows = 100000: 扫描10万行,慢 ⚠️
rows = 1000000: 扫描百万行,GG 💀
优化目标:rows越小越好!
3.3.9 Extra:额外信息(⭐重要!)
sql
好的信息 ✅:
├── Using index ━━ 覆盖索引,不用回表,最快!
├── Using index condition ━━ 索引条件下推
└── Using where ━━ 使用WHERE过滤
需要注意的信息 ⚠️:
├── Using filesort ━━ 文件排序,考虑加索引
├── Using temporary ━━ 使用临时表,性能差
└── Using join buffer ━━ JOIN缓冲,考虑加索引
危险信息 ❌:
├── Using where; Using temporary; Using filesort
└── Full table scan
3.4 EXPLAIN实战案例
案例1:可怕的全表扫描
sql
EXPLAIN SELECT * FROM users WHERE name LIKE '%张%';
结果:
vbnet
type: ALL
rows: 1000000
Extra: Using where
诊断:
- 😱 type=ALL:全表扫描!
- 😱 rows=100万:要扫描100万行!
- 😱 没用索引:LIKE以%开头,索引失效
生活比喻: 在整个城市找"名字里有张字的人",需要挨家挨户问
解决方案:
sql
-- 方案1:改变查询方式
WHERE name LIKE '张%' -- 以"张"开头,可以用索引
-- 方案2:使用全文索引
ALTER TABLE users ADD FULLTEXT INDEX ft_name (name);
SELECT * FROM users WHERE MATCH(name) AGAINST('张');
案例2:索引失效的坑
sql
EXPLAIN SELECT * FROM users WHERE age + 1 = 26;
结果:
yaml
type: ALL
possible_keys: idx_age
key: NULL
诊断:
- 😱 possible_keys有索引,但key=NULL:索引失效!
- 😱 原因:列上有函数运算(age+1)
生活比喻: 图书馆按原书名排序,你要找"书名+序号=26的书" 图书管理员:我不会算术!只能一本本找!
解决方案:
sql
-- 把运算放在常量侧
WHERE age = 25 -- 正确✅
案例3:完美的索引使用
sql
EXPLAIN SELECT name, age FROM users WHERE age = 25;
-- 建立覆盖索引
CREATE INDEX idx_age_name ON users(age, name);
结果:
vbnet
type: ref
key: idx_age_name
rows: 100
Extra: Using index
诊断:
- ✅ type=ref:很好!
- ✅ key用上了索引
- ✅ rows只有100行
- ✅ Extra=Using index:覆盖索引,不用回表!
生活比喻: 目录上直接有答案,不用翻正文!
第四章:查询优化的十八般武艺
4.1 避免SELECT *
❌ 错误示范:
sql
SELECT * FROM users WHERE id = 1;
问题:
bash
表结构:
┌────┬──────┬─────┬──────┬────────┬────────────────┬─────────────┐
│ id │ name │ age │ city │ email │ address │ description │
├────┴──────┴─────┴──────┴────────┴────────────────┴─────────────┤
│ 你只需要name和age │
│ 却把所有列都查出来了! │
└─────────────────────────────────────────────────────────────────┘
✅ 正确示范:
sql
SELECT name, age FROM users WHERE id = 1;
好处:
- 减少网络传输
- 减少内存占用
- 可能使用覆盖索引(不用回表)
生活比喻:
sql
去超市买鸡蛋和牛奶:
SELECT *: 把超市所有商品都搬回家 🏪🏠
(包括你不需要的东西)
SELECT name, age: 只买鸡蛋和牛奶 🥚🥛
(精准高效)
4.2 避免在WHERE中使用函数
❌ 错误示范:
sql
-- 问题1:列上使用函数
SELECT * FROM orders WHERE YEAR(order_date) = 2024;
-- 问题2:类型转换
SELECT * FROM users WHERE id = '123'; -- id是INT类型
-- 问题3:计算操作
SELECT * FROM products WHERE price * 0.8 < 100;
索引失效原理:
scss
索引存储的是原始值:
2024-01-15
2024-02-20
2024-03-10
...
查询要YEAR(order_date):
需要对每行计算YEAR()
索引就无法使用了!
✅ 正确示范:
sql
-- 正确1:把函数放在常量侧
SELECT * FROM orders
WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01';
-- 正确2:类型匹配
SELECT * FROM users WHERE id = 123;
-- 正确3:移动计算
SELECT * FROM products WHERE price < 125; -- 100/0.8=125
生活比喻:
arduino
场景:图书馆找书
错误方式:
馆员:"帮我找出版年份-2000=24的书"
图书管理员:"我只能按出版年份找,不会算术!"
结果:要逐本计算 😭
正确方式:
馆员:"帮我找2024年出版的书"
图书管理员:"好的,马上找到!"
结果:直接定位 ✅
4.3 避免使用OR
❌ 错误示范:
sql
SELECT * FROM users WHERE age = 25 OR age = 30;
问题:
- OR可能导致索引失效
- 即使使用索引,也要扫描两次
✅ 正确示范:
sql
-- 方案1:使用IN
SELECT * FROM users WHERE age IN (25, 30);
-- 方案2:使用UNION(数据量大时)
SELECT * FROM users WHERE age = 25
UNION ALL
SELECT * FROM users WHERE age = 30;
性能对比:
yaml
OR: 扫描不确定,可能全表
IN: 走索引,一次扫描
UNION ALL: 走索引,两次精准扫描(无去重)
UNION: 走索引,两次扫描+去重
生活比喻:
arduino
任务:找出班级里叫"小明"或"小红"的同学
OR方式:
从第一个同学开始问:
"你叫小明吗?或者叫小红吗?"
每个人都要问一遍 😓
IN方式:
直接看花名册,找小明和小红
精准定位 ✅
4.4 小心使用LIKE
❌ 错误示范:
sql
-- 索引失效的情况
SELECT * FROM users WHERE name LIKE '%张%'; -- 前后都有%
SELECT * FROM users WHERE name LIKE '%张'; -- 前面有%
为什么失效?
sql
索引是按顺序排列的:
张三
张四
李四
王五
赵六
LIKE '张%': 可以用索引,从"张"开始找 ✅
LIKE '%张%': 不知道从哪开始,只能全扫 ❌
✅ 正确示范:
sql
-- 可以使用索引
SELECT * FROM users WHERE name LIKE '张%';
-- 需要模糊查询时,使用全文索引
ALTER TABLE users ADD FULLTEXT INDEX ft_name (name);
SELECT * FROM users WHERE MATCH(name) AGAINST('张');
生活比喻:
sql
场景:字典查字
LIKE '张%':
"找以'张'开头的字"
直接翻到zhang那页 ✅
LIKE '%张%':
"找包含'张'的字"
每个字都要看一遍 😭
4.5 使用LIMIT分页优化
❌ 错误示范:
sql
-- 深度分页问题
SELECT * FROM orders ORDER BY id LIMIT 100000, 10;
问题:
markdown
要查第100000页的10条数据:
1. 先扫描100010行
2. 取最后10行
3. 丢弃前100000行
天哪!扫了10万行,只用10行!💀
✅ 优化方案:
方案1:使用上一页的最大ID
sql
-- 假设上一页最大id是99999
SELECT * FROM orders
WHERE id > 99999
ORDER BY id
LIMIT 10;
方案2:延迟关联
sql
SELECT * FROM orders o
INNER JOIN (
SELECT id FROM orders ORDER BY id LIMIT 100000, 10
) t ON o.id = t.id;
方案3:使用业务主键
sql
-- 记录上次查询的最后一个时间戳
SELECT * FROM orders
WHERE create_time > '2024-01-01 10:00:00'
ORDER BY create_time
LIMIT 10;
生活比喻:
场景:在一本厚书里翻到第1000页
错误方式:
从第1页开始,一页页翻
翻到第999页:累死了 😭
正确方式:
看书签,直接翻到第1000页附近 ✅
4.6 优化JOIN查询
4.6.1 JOIN的类型
scss
┌─────────────────────────────────┐
│ LEFT JOIN (左连接) │
│ │
│ ●●●●●● │
│ ●●●●●● ●●● │
│ ●●●●●● │
│ 左表 右表 │
│ (全部) (匹配) │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ INNER JOIN (内连接) │
│ │
│ ●●●●●● │
│ ●●●●●● ●●● │
│ ●●●●●● │
│ ●● │
│ (交集) │
└─────────────────────────────────┘
4.6.2 JOIN优化技巧
技巧1:小表驱动大表
sql
-- ❌ 错误:大表驱动小表
SELECT * FROM orders o -- 100万行
INNER JOIN users u ON o.user_id = u.id -- 1万行
-- ✅ 正确:小表驱动大表
SELECT * FROM users u -- 1万行
INNER JOIN orders o ON u.id = o.user_id -- 100万行
原理:
scss
大表驱动小表:
for (100万次) {
去小表找匹配 (1万次)
}
总共:100万 × 查找时间
小表驱动大表:
for (1万次) {
去大表找匹配 (100万次,但有索引!)
}
总共:1万 × 索引查找时间(极快)
生活比喻:
scss
任务:找出两个班级的同学配对
错误方式:
大班(100人)的每个同学,
去小班(10人)里找匹配
要问100次
正确方式:
小班(10人)的每个同学,
去大班(100人)里找匹配
只要问10次 ✅
技巧2:确保JOIN列有索引
sql
-- 在外键列建索引
CREATE INDEX idx_user_id ON orders(user_id);
-- 然后JOIN
SELECT * FROM users u
INNER JOIN orders o ON u.id = o.user_id;
技巧3:避免在JOIN条件中使用函数
sql
-- ❌ 错误
SELECT * FROM users u
INNER JOIN orders o ON DATE(o.create_time) = u.register_date;
-- ✅ 正确
SELECT * FROM users u
INNER JOIN orders o ON o.create_time >= u.register_date
AND o.create_time < DATE_ADD(u.register_date, INTERVAL 1 DAY);
4.7 避免隐式类型转换
❌ 错误示范:
sql
CREATE TABLE users (
id INT PRIMARY KEY,
phone VARCHAR(11)
);
-- 查询时用数字
SELECT * FROM users WHERE phone = 13800138000; -- phone是VARCHAR
发生了什么?
scss
MySQL自动转换为:
WHERE CAST(phone AS SIGNED) = 13800138000
等于在phone列上加了函数!
索引失效!💀
✅ 正确示范:
sql
-- 类型匹配
SELECT * FROM users WHERE phone = '13800138000';
记忆口诀:
sql
VARCHAR配字符串 '123'
INT配数字 123
DATE配日期 '2024-01-01'
类型对上,索引飞!
4.8 优化COUNT查询
不同COUNT的区别:
sql
-- 方式1:统计所有行
SELECT COUNT(*) FROM users;
-- 方式2:统计id列
SELECT COUNT(id) FROM users;
-- 方式3:统计非NULL的name
SELECT COUNT(name) FROM users;
-- 方式4:统计去重后的name
SELECT COUNT(DISTINCT name) FROM users;
性能对比:
scss
COUNT(*) > COUNT(id) > COUNT(name) > COUNT(DISTINCT name)
最快 最慢
为什么COUNT(*)最快?
sql
COUNT(*): MySQL优化过,直接读索引树的行数
COUNT(id): 要判断id是否为NULL
COUNT(name): 要判断name是否为NULL
COUNT(DISTINCT name): 还要去重
大表COUNT优化:
方案1:使用近似值
sql
-- 从统计信息获取
EXPLAIN SELECT * FROM users;
-- 看rows字段,大概的行数
方案2:单独维护计数表
sql
CREATE TABLE table_count (
table_name VARCHAR(50) PRIMARY KEY,
row_count INT
);
-- 插入时+1,删除时-1
-- 查询时直接读这个表
SELECT row_count FROM table_count WHERE table_name = 'users';
生活比喻:
scss
统计体育馆人数:
COUNT(*):
看电子计数器,瞬间知道 ⚡
逐个数:
站门口数进来的人 😓
去重后数:
还要记住谁来过,避免重复 💀
4.9 使用UNION ALL代替UNION
sql
-- ❌ 慢:会去重
SELECT name FROM users WHERE age = 25
UNION
SELECT name FROM users WHERE age = 30;
-- ✅ 快:不去重
SELECT name FROM users WHERE age = 25
UNION ALL
SELECT name FROM users WHERE age = 30;
性能差异:
sql
UNION: 查询 + 排序 + 去重
UNION ALL: 查询
如果确定没有重复数据,用UNION ALL!
生活比喻:
sql
任务:合并两个班的学生名单
UNION:
合并后,检查有没有重复的学生
发现小明在两个班都有,去掉一个 😓
UNION ALL:
直接合并,不管重复 ✅
(如果确定没重复,这样最快)
4.10 批量操作代替循环单条
❌ 错误示范:
sql
-- 插入1000条数据,执行1000次
for (int i = 0; i < 1000; i++) {
INSERT INTO users VALUES (i, 'name' + i);
}
✅ 正确示范:
sql
-- 批量插入,一次搞定
INSERT INTO users VALUES
(1, 'name1'),
(2, 'name2'),
(3, 'name3'),
...
(1000, 'name1000');
性能对比:
yaml
单条插入: 1000次网络往返 + 1000次事务
批量插入: 1次网络往返 + 1次事务
速度差异:10-100倍!
生活比喻:
任务:搬1000块砖
单条方式:
一次搬一块,往返1000次 😭
批量方式:
用小推车,一次搬100块,往返10次 ✅
第五章:表设计优化------打好地基
5.1 选择合适的数据类型
原则:更小更好
sql
┌──────────────────────────────────┐
│ 存储年龄: │
├──────────────────────────────────┤
│ ❌ BIGINT - 8字节,太浪费 │
│ ❌ INT - 4字节,还是大 │
│ ✅ TINYINT - 1字节,完美! │
│ (范围:0-255,够用了) │
└──────────────────────────────────┘
常见类型选择:
sql
-- ❌ 错误示例
CREATE TABLE users (
id BIGINT, -- 太大
age INT, -- 太大
gender VARCHAR(50), -- 太大
is_vip TEXT, -- 什么鬼
salary DOUBLE -- 会有精度问题
);
-- ✅ 正确示例
CREATE TABLE users (
id INT UNSIGNED, -- 42亿够用
age TINYINT UNSIGNED, -- 0-255
gender CHAR(1), -- '男'/'女'
is_vip TINYINT(1), -- 0/1
salary DECIMAL(10,2) -- 精确的钱
);
类型选择指南:
sql
整数类型:
┌─────────────┬───────────┬─────────────────┐
│ 类型 │ 字节 │ 适用场景 │
├─────────────┼───────────┼─────────────────┤
│ TINYINT │ 1字节 │ 年龄、状态 │
│ SMALLINT │ 2字节 │ 编号 │
│ MEDIUMINT │ 3字节 │ 大编号 │
│ INT │ 4字节 │ 主键、外键 │
│ BIGINT │ 8字节 │ 超大数据 │
└─────────────┴───────────┴─────────────────┘
字符类型:
┌─────────────┬─────────────────────────┐
│ CHAR(n) │ 固定长度,如手机号 │
│ VARCHAR(n) │ 可变长度,如姓名 │
│ TEXT │ 长文本,如文章 │
└─────────────┴─────────────────────────┘
时间类型:
┌─────────────┬─────────────────────────┐
│ DATE │ 日期 '2024-01-01' │
│ DATETIME │ 日期时间(8字节) │
│ TIMESTAMP │ 时间戳(4字节,推荐!) │
└─────────────┴─────────────────────────┘
金额类型:
┌─────────────┬─────────────────────────┐
│ DECIMAL │ 精确小数,存钱用这个! │
│ DOUBLE │ 浮点数,会有误差,慎用 │
└─────────────┴─────────────────────────┘
生活比喻:
makefile
存储"性别":
BIGINT: 像用卡车运一个苹果 🚛🍎
VARCHAR(50): 像用小货车运一个苹果 🚐🍎
CHAR(1): 像用篮子刚好装一个苹果 🧺🍎 ✅
5.2 避免NULL值
为什么要避免NULL?
vbscript
NULL的问题:
1. 占用额外空间存储NULL标记
2. 索引效率降低
3. 查询逻辑复杂(需要IS NULL判断)
4. 聚合函数会忽略NULL
❌ 错误示范:
sql
CREATE TABLE users (
id INT,
name VARCHAR(50) DEFAULT NULL,
age INT DEFAULT NULL,
city VARCHAR(50) DEFAULT NULL
);
-- 查询复杂
SELECT * FROM users WHERE age IS NULL OR age >= 18;
✅ 正确示范:
sql
CREATE TABLE users (
id INT NOT NULL,
name VARCHAR(50) NOT NULL DEFAULT '',
age INT NOT NULL DEFAULT 0,
city VARCHAR(50) NOT NULL DEFAULT ''
);
-- 查询简单
SELECT * FROM users WHERE age = 0 OR age >= 18;
生活比喻:
arduino
问:"你今年多少岁?"
NULL回答:"我没有年龄"
程序员:"什么鬼???" 💀
默认值回答:"0岁"
程序员:"哦,还没出生或没填" ✅
5.3 合理使用反范式设计
三范式 vs 反范式
规范化(三范式):
sql
-- 订单表
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
product_id INT
);
-- 用户表
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50)
);
-- 查询需要JOIN
SELECT o.*, u.name
FROM orders o
JOIN users u ON o.user_id = u.id;
反范式(冗余设计):
sql
-- 订单表(冗余用户名)
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
user_name VARCHAR(50), -- 冗余!
product_id INT
);
-- 查询直接搞定
SELECT * FROM orders WHERE user_name = '张三';
权衡:
scss
┌──────────────┬────────────────┬────────────────┐
│ │ 规范化(三范式)│ 反范式 │
├──────────────┼────────────────┼────────────────┤
│ 存储空间 │ 小 ✅ │ 大 ❌ │
│ 数据一致性 │ 高 ✅ │ 需要维护 ⚠️ │
│ 查询性能 │ 慢(需要JOIN) ❌ │ 快 ✅ │
│ 更新性能 │ 快 ✅ │ 慢 ❌ │
└──────────────┴────────────────┴────────────────┘
使用场景:
diff
✅ 适合反范式:
- 读多写少的字段
- 经常JOIN查询的字段
- 高并发查询场景
❌ 不适合反范式:
- 频繁更新的字段
- 数据一致性要求高
- 存储空间紧张
生活比喻:
三范式:
你的联系方式统一存在通讯录里
每次打电话要先查通讯录 📖📞
反范式:
你把常用的联系人手机号背下来
打电话直接拨 📞✅
5.4 分库分表
当数据量达到千万、亿级别时,单表扛不住了!
垂直分表
bash
原表(列太多):
┌────┬──────┬─────┬──────┬─────────┬─────────────┐
│ id │ name │ age │ city │ address │ description │
└────┴──────┴─────┴──────┴─────────┴─────────────┘
↓ 拆分
┌────┬──────┬─────┬──────┐ ┌────┬─────────┬─────────────┐
│ id │ name │ age │ city │ │ id │ address │ description │
└────┴──────┴─────┴──────┘ └────┴─────────┴─────────────┘
常用字段表 详情字段表
水平分表
scss
原表(行太多):
users (1亿条数据)
↓ 拆分
users_0 (2500万)
users_1 (2500万)
users_2 (2500万)
users_3 (2500万)
分片策略:
sql
-- 按用户ID分片(取模)
user_id % 4 = 0 → users_0
user_id % 4 = 1 → users_1
user_id % 4 = 2 → users_2
user_id % 4 = 3 → users_3
生活比喻:
diff
垂直分表:
原来所有东西都堆在一个房间
现在:
- 卧室放床和衣服(常用)
- 储藏室放杂物(不常用)
水平分表:
图书馆书太多:
- 一层放A-F开头的书
- 二层放G-M开头的书
- 三层放N-Z开头的书
第六章:高级优化技巧
6.1 查询缓存(MySQL 5.7及以前)
⚠️ 注意:MySQL 8.0已移除查询缓存
sql
-- 查看缓存配置
SHOW VARIABLES LIKE 'query_cache%';
-- 开启缓存
SET GLOBAL query_cache_type = ON;
SET GLOBAL query_cache_size = 134217728; -- 128MB
缓存原理:
sql
第一次查询:
SQL → 执行 → 结果 → 缓存起来
第二次相同查询:
SQL → 发现缓存命中 → 直接返回结果 ⚡
失效情况:
scss
❌ 表数据变了:缓存失效
❌ SQL差一个空格:不命中
❌ 使用NOW()等函数:不缓存
生活比喻:
ini
背乘法口诀表:
第一次:1×1=1, 2×2=4...慢慢算
以后:直接背出来 ✅
但如果老师改了规则,乘法口诀表就得重新背
6.2 覆盖索引
什么是覆盖索引?
索引里已经包含了所有需要的列,不用回表查询!
sql
-- 建立联合索引
CREATE INDEX idx_age_name ON users(age, name);
-- 查询只要age和name
SELECT age, name FROM users WHERE age = 25;
执行过程:
markdown
普通查询(需要回表):
1. 去索引树找到满足条件的主键id
2. 拿着id回到数据表查name
3. 返回结果
覆盖索引(不需要回表):
1. 去索引树找到满足条件的记录
2. 索引里已经有name,直接返回
3. 省略了回表步骤!⚡
判断方法:
sql
EXPLAIN SELECT age, name FROM users WHERE age = 25;
-- 如果Extra显示:Using index
-- 恭喜你!用上了覆盖索引!
生活比喻:
markdown
查电话簿:
需要回表:
1. 在目录里找到"张三" - 第203页
2. 翻到第203页看详细信息
覆盖索引:
1. 目录里就有电话号码,直接看 ✅
6.3 前缀索引
当字段很长时,可以只索引前几个字符
sql
-- 原始:索引完整的email
CREATE INDEX idx_email ON users(email);
-- 优化:只索引前10个字符
CREATE INDEX idx_email_prefix ON users(email(10));
选择前缀长度:
sql
-- 查看选择性
SELECT
COUNT(DISTINCT LEFT(email, 5)) / COUNT(*) AS sel5,
COUNT(DISTINCT LEFT(email, 10)) / COUNT(*) AS sel10,
COUNT(DISTINCT LEFT(email, 15)) / COUNT(*) AS sel15
FROM users;
-- 选择选择性最高且最短的长度
权衡:
vbnet
优点:
✅ 索引更小
✅ 查询更快
✅ 占用更少内存
缺点:
❌ 不能用于ORDER BY
❌ 不能用于GROUP BY
❌ 选择性可能降低
生活比喻:
sql
给书编号:
完整索引:
《如何在30天内学会SQL优化并成为数据库大师》
太长了!
前缀索引:
《如何在30天内学会...》
够用了,也更短 ✅
6.4 索引下推(ICP)
MySQL 5.6+的优化特性
sql
CREATE INDEX idx_age_name ON users(age, name);
-- 查询
SELECT * FROM users WHERE age > 20 AND name LIKE '张%';
没有ICP:
markdown
1. 使用索引找到所有age>20的记录(比如1万条)
2. 回表1万次,读取完整数据
3. 在Server层过滤name LIKE '张%'
有ICP:
markdown
1. 使用索引找到age>20的记录
2. 在索引中直接过滤name LIKE '张%'(剩下100条)
3. 只回表100次
性能提升:
回表次数:10000 → 100
提升:100倍!
查看是否使用:
sql
EXPLAIN ...
-- Extra显示:Using index condition
生活比喻:
arduino
招聘:要求年龄25-35岁,名字叫"张*"
没有ICP:
先把所有25-35岁的人叫来面试(1000人)
面试时再问名字,筛选姓张的(10人)
有ICP:
直接从简历(索引)中筛选:
25-35岁 + 姓张(10人)
只让这10人来面试 ✅
6.5 分区表
将大表按规则分成多个子表
sql
-- 按年份分区
CREATE TABLE orders (
id INT,
user_id INT,
order_date DATE,
amount DECIMAL(10,2)
)
PARTITION BY RANGE (YEAR(order_date)) (
PARTITION p2021 VALUES LESS THAN (2022),
PARTITION p2022 VALUES LESS THAN (2023),
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025)
);
查询优化:
sql
-- 只查2023年的数据
SELECT * FROM orders WHERE order_date >= '2023-01-01'
AND order_date < '2024-01-01';
-- MySQL只扫描p2023分区!
分区策略:
sql
RANGE分区: 按范围(如日期、ID)
LIST分区: 按列表值(如地区)
HASH分区: 按哈希值
KEY分区: 类似HASH,MySQL自动处理
生活比喻:
diff
图书馆分区:
- A区:2020年的书
- B区:2021年的书
- C区:2022年的书
- D区:2023年的书
找2023年的书,直接去D区
不用翻其他区 ✅
第七章:实战案例大解析
案例1:慢查询优化------从30秒到0.1秒
背景
某电商订单表,1000万数据,查询超时
原始SQL:
sql
SELECT * FROM orders
WHERE user_id = 12345
AND status = 'paid'
AND DATE(create_time) = '2024-01-15';
问题诊断:
sql
EXPLAIN SELECT ...;
结果:
vbnet
type: ALL
rows: 10000000
Extra: Using where
问题分析:
ini
😱 type=ALL:全表扫描
😱 rows=1000万:要扫描全表
😱 DATE(create_time):索引失效
😱 SELECT *:查了不需要的列
优化步骤:
Step 1:创建索引
sql
CREATE INDEX idx_user_status_time
ON orders(user_id, status, create_time);
Step 2:去掉函数
sql
-- 把DATE(create_time) = '2024-01-15'
-- 改为范围查询
WHERE create_time >= '2024-01-15 00:00:00'
AND create_time < '2024-01-16 00:00:00'
Step 3:只查需要的列
sql
SELECT id, order_no, amount, create_time
优化后的SQL:
sql
SELECT id, order_no, amount, create_time
FROM orders
WHERE user_id = 12345
AND status = 'paid'
AND create_time >= '2024-01-15 00:00:00'
AND create_time < '2024-01-16 00:00:00';
验证效果:
sql
EXPLAIN SELECT ...;
结果:
vbnet
type: ref
key: idx_user_status_time
rows: 10
Extra: Using index condition
性能对比:
优化前:30秒,扫描1000万行
优化后:0.1秒,扫描10行
提升:300倍!🚀
案例2:分页查询优化------深度分页问题
背景
用户列表分页,第1000页加载超慢
原始SQL:
sql
SELECT * FROM users ORDER BY id LIMIT 999000, 1000;
问题:
markdown
要获取第1000页(每页1000条):
1. MySQL扫描100万行
2. 取最后1000行
3. 前面99.9万行白扫描了!💀
优化方案一:使用ID范围
sql
-- 记录上一页的最大ID(假设是999000)
SELECT * FROM users
WHERE id > 999000
ORDER BY id
LIMIT 1000;
性能:
优化前:扫描100万行
优化后:扫描1000行
提升:1000倍!
缺点:
- 需要记录上一页的ID
- 不能跳页
优化方案二:延迟关联
sql
SELECT * FROM users u
INNER JOIN (
SELECT id FROM users ORDER BY id LIMIT 999000, 1000
) t ON u.id = t.id;
原理:
bash
子查询只查id,走覆盖索引
主查询用id关联,精准定位
性能:
优化前:扫描完整行100万次
优化后:索引扫描100万次 + 回表1000次
提升:10倍以上
案例3:JOIN优化------订单用户查询
背景
查询订单及其用户信息
原始SQL:
sql
SELECT o.*, u.name, u.phone
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.status = 'paid'
AND u.city = '北京';
问题诊断:
sql
EXPLAIN SELECT ...;
发现:
markdown
1. orders表全表扫描
2. user_id没有索引
3. city没有索引
优化步骤:
Step 1:创建索引
sql
CREATE INDEX idx_status ON orders(status);
CREATE INDEX idx_user_id ON orders(user_id);
CREATE INDEX idx_city ON users(city);
Step 2:改写SQL(先过滤再JOIN)
sql
SELECT o.*, u.name, u.phone
FROM (
SELECT * FROM orders WHERE status = 'paid'
) o
INNER JOIN (
SELECT * FROM users WHERE city = '北京'
) u ON o.user_id = u.id;
Step 3:如果只需要部分字段,使用覆盖索引
sql
-- 创建覆盖索引
CREATE INDEX idx_id_name_phone ON users(id, name, phone);
SELECT o.id, o.order_no, u.name, u.phone
FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE o.status = 'paid' AND u.city = '北京';
性能对比:
优化前:扫描百万行,耗时5秒
优化后:扫描千行,耗时0.05秒
提升:100倍!
案例4:COUNT优化------统计大表数据
背景
统计订单总数,超慢
原始SQL:
sql
SELECT COUNT(*) FROM orders WHERE status = 'paid';
问题:
表有1000万行,即使有索引,也要扫描很久
优化方案一:使用近似值
sql
-- 从统计信息获取
EXPLAIN SELECT * FROM orders WHERE status = 'paid';
-- 看rows字段
优化方案二:单独维护计数表
sql
-- 创建计数表
CREATE TABLE order_stats (
status VARCHAR(20) PRIMARY KEY,
count INT DEFAULT 0
);
-- 插入初始值
INSERT INTO order_stats VALUES ('paid', 0);
-- 订单插入时,计数+1(用触发器)
CREATE TRIGGER tr_order_insert
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
UPDATE order_stats
SET count = count + 1
WHERE status = NEW.status;
END;
-- 查询时直接读计数表
SELECT count FROM order_stats WHERE status = 'paid';
性能对比:
优化前:扫描1000万行,5秒
优化后:读1行,0.001秒
提升:5000倍!
第八章:常见坑点与避坑指南
8.1 索引失效的十大场景
sql
🚨 索引失效现场 🚨
1️⃣ 列上有函数或运算
WHERE YEAR(create_time) = 2024 ❌
WHERE create_time >= '2024-01-01' ✅
2️⃣ 类型不匹配
WHERE phone = 13800138000 ❌ (phone是VARCHAR)
WHERE phone = '13800138000' ✅
3️⃣ LIKE以%开头
WHERE name LIKE '%张%' ❌
WHERE name LIKE '张%' ✅
4️⃣ 使用OR连接
WHERE age = 25 OR age = 30 ❌
WHERE age IN (25, 30) ✅
5️⃣ 不等于判断
WHERE age != 25 ❌
WHERE age > 25 OR age < 25 ⚠️ (看情况)
6️⃣ IS NOT NULL
WHERE name IS NOT NULL ❌
建表时用NOT NULL + DEFAULT ✅
7️⃣ NOT IN / NOT EXISTS
WHERE id NOT IN (1,2,3) ❌
WHERE id NOT EXISTS (...) ❌
8️⃣ 联合索引不满足最左前缀
INDEX(age, city)
WHERE city = '北京' ❌ (跳过了age)
WHERE age = 25 ✅
9️⃣ 范围查询后面的索引列
INDEX(age, city, name)
WHERE age > 20 AND city = '北京' ⚠️ (city不走索引)
🔟 索引列参与运算
WHERE age + 1 = 26 ❌
WHERE age = 25 ✅
8.2 性能杀手榜
markdown
💀 性能杀手TOP 10 💀
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 全表扫描 (type=ALL)
2. SELECT *
3. 没有索引
4. 索引失效
5. 深度分页 (LIMIT 100000, 10)
6. 大量JOIN
7. 子查询
8. DISTINCT
9. 临时表 (Using temporary)
10. 文件排序 (Using filesort)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8.3 SQL优化口诀
sql
📜 SQL优化十字箴言 📜
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
索引加对,查询飞快
函数别碰,类型要对
小表驱动,别反着来
星号少用,按需查询
分页优化,别傻乎乎
JOIN有度,不要贪多
定期分析,清理维护
监控警告,问题早知
测试测试,反复测试
持续学习,不断进步
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8.4 开发规范建议
sql
✅ 强制规范:
├── 所有表必须有主键
├── 外键列必须有索引
├── 禁止SELECT *
├── 禁止在WHERE中对列使用函数
├── VARCHAR长度不超过5000
├── 单表列数不超过50
├── 单表索引不超过5个
└── 禁止使用TEXT/BLOB做主键
⚠️ 推荐规范:
├── 表名、字段名使用小写
├── 避免使用保留字
├── 创建时间、更新时间字段
├── 逻辑删除代替物理删除
├── 金额使用DECIMAL类型
├── 使用UNSIGNED存储非负数
└── 合理使用NOT NULL DEFAULT
🔍 性能规范:
├── 单表数据不超过500万行
├── 单次批量插入不超过5000条
├── 事务控制在2秒内完成
├── 慢查询阈值设为1秒
└── 定期ANALYZE TABLE更新统计
总结:SQL优化的修炼之路
🎯 优化的三个境界
sql
第一境界:能用就行
├── 功能实现了,但慢
├── 没有索引,全靠运气
├── SELECT * 遍地跑
└── 出了问题不知道咋办
第二境界:追求性能
├── 知道建索引
├── 会用EXPLAIN分析
├── 注意查询优化
└── 能解决常见性能问题
第三境界:炉火纯青
├── 懂原理,知道为什么
├── 能预见问题并避免
├── 设计阶段就考虑性能
├── 持续监控和优化
└── 经验丰富,游刃有余
🚀 优化步骤总结
sql
┌─────────────────────────────────┐
│ SQL优化标准流程 │
├─────────────────────────────────┤
│ 1. 发现问题 │
│ └── 监控慢查询日志 │
│ │
│ 2. 分析问题 │
│ └── EXPLAIN查看执行计划 │
│ │
│ 3. 定位问题 │
│ ├── 有没有索引? │
│ ├── 索引有没有用上? │
│ ├── 扫描了多少行? │
│ └── 有没有临时表/文件排序? │
│ │
│ 4. 解决问题 │
│ ├── 加索引 │
│ ├── 改SQL │
│ ├── 改表结构 │
│ └── 拆表 │
│ │
│ 5. 验证效果 │
│ ├── 再次EXPLAIN │
│ ├── 测试查询时间 │
│ └── 观察线上效果 │
│ │
│ 6. 持续监控 │
│ └── 保持警惕,及时发现新问题 │
└─────────────────────────────────┘
📚 学习资源推荐
sql
📖 书籍推荐:
├── 《高性能MySQL》 - 必读圣经
├── 《MySQL技术内幕:InnoDB存储引擎》
├── 《数据库索引设计与优化》
└── 《SQL性能调优》
🌐 在线资源:
├── MySQL官方文档
├── 掘金/思否等技术社区
├── GitHub优秀项目源码
└── 各大厂技术博客
🛠️ 工具推荐:
├── pt-query-digest(慢查询分析)
├── MySQLTuner(配置优化)
├── Percona Toolkit(性能工具集)
└── Navicat/DataGrip(GUI工具)
后记:优化永无止境
SQL优化是一个持续的过程,没有最好,只有更好。
记住这三句话:
┌─────────────────────────────────────┐
│ │
│ 1. 没有绝对的最优解 │
│ 只有最适合当前场景的方案 │
│ │
│ 2. 过早优化是万恶之源 │
│ 但不优化是更大的恶 │
│ │
│ 3. 实践出真知 │
│ 理论+实践=成功 │
│ │
└─────────────────────────────────────┘
最后,送给大家一句话:
"慢即是快,快即是慢。 打好基础,步步为营, 总有一天,你的SQL会像火箭一样快!" 🚀
附录:常用SQL优化命令速查
sql
-- ============ 索引管理 ============
-- 创建索引
CREATE INDEX idx_name ON table_name(column_name);
CREATE UNIQUE INDEX uk_name ON table_name(column_name);
CREATE INDEX idx_name ON table_name(col1, col2); -- 联合索引
-- 查看索引
SHOW INDEX FROM table_name;
-- 删除索引
DROP INDEX idx_name ON table_name;
-- ============ 性能分析 ============
-- 查看执行计划
EXPLAIN SELECT ...;
EXPLAIN FORMAT=JSON SELECT ...; -- JSON格式
-- 查看实际执行
EXPLAIN ANALYZE SELECT ...; -- MySQL 8.0+
-- 查看慢查询配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 1秒
-- ============ 统计信息 ============
-- 更新统计信息
ANALYZE TABLE table_name;
-- 查看表统计
SHOW TABLE STATUS LIKE 'table_name';
-- 查看索引统计
SHOW INDEX FROM table_name;
-- ============ 优化命令 ============
-- 优化表(碎片整理)
OPTIMIZE TABLE table_name;
-- 检查表
CHECK TABLE table_name;
-- 修复表
REPAIR TABLE table_name;
-- ============ 性能监控 ============
-- 查看当前连接
SHOW PROCESSLIST;
-- 杀死慢查询
KILL 线程ID;
-- 查看InnoDB状态
SHOW ENGINE INNODB STATUS;
-- 查看表大小
SELECT
table_name,
table_rows,
ROUND(data_length/1024/1024, 2) AS 'data_mb',
ROUND(index_length/1024/1024, 2) AS 'index_mb'
FROM information_schema.tables
WHERE table_schema = 'your_database';
全文完
感谢阅读!如果这篇指南对你有帮助,请给个⭐!
有问题欢迎讨论,让我们一起进步!💪
markdown
_____ ____ _ ___ _ _ _ _ _
/ ____| _ \ | | / _ \ | | (_) (_) | | (_)
| (___ | |_) | | | | | | |_ __ | |_ _ _ __ ___ _ ______ _| |_ _ ___ _ __
\___ \| _ < | | | | | | '_ \| __| | '_ ` _ \| |_ / _` | __| |/ _ \| '_ \
____) | |_) | | |____ | |_| | |_) | |_| | | | | | | |/ / (_| | |_| | (_) | | | |
|_____/|____/ |______| \___/| .__/ \__|_|_| |_| |_|_/___\__,_|\__|_|\___/|_| |_|
| |
|_|
🎉 让数据库飞起来! 🚀