MySQL学习笔记09:MySQL高级特性深度学习(上):count函数、数据类型与分库分表核心原理
📅 学习日期:2025年10月10日
⏰ 学习时长:1小时
🎯 学习方式:苏格拉底式对话教学
📝 博客性质:真实学习过程记录
📖 前言
这篇博客记录了我学习MySQL高级特性的完整思考过程。不同于传统的技术博客,这里记录了我的每一个疑问、每一次思考、每一个"啊哈"时刻。
本文涵盖:
- count函数的性能差异
- 数据类型选择的实战思考
- 分库分表的核心原理
- B+树结构深度剖析
- 分片键选择策略
- 跨分片查询优化
学习方式:苏格拉底式提问,通过问答逐步深入理解每个知识点。
📑 目录
- count函数深度理解
- 数据类型选择实战
- 分库分表核心原理
- B+树结构回顾
- 分片键选择策略
- 跨分片查询优化
- 总结与反思
📊 第一部分:count函数深度理解
场景引入
在学习MySQL统计函数时,我遇到了一个看似简单但实则复杂的问题:
CREATE TABLE users (id INT PRIMARY KEY,name VARCHAR(50),email VARCHAR(100),age INT
);
这几种count有什么区别?
SELECT count(*) FROM users;
SELECT count(1) FROM users;
SELECT count(id) FROM users;
SELECT count(email) FROM users;
我的初步理解
我最初的认知:
“查询所有不同的结果,只要有不同的字段就统计,count字段名的话就是单纯看某个字段,如果出现NULL就不统计。”
但是我发现这个理解不够准确,于是开始深入研究每种写法的具体行为。
深入思考:count(字段名)
为了验证我的理解,我构造了测试数据:
id | name | email
1 | John | john@qq.com
2 | Lisa | NULL
3 | Tom | tom@qq.com
4 | Jerry | NULL
我的推测:
count(*)
返回:4行count(1)
返回:4行count(email)
返回:2行(因为有2个NULL)
实际验证结果:✅ 推测完全正确!
核心疑问:count(*) vs count(1)
既然count(*)和count(1)都返回4,那它们有什么区别?哪个更快?
我的第一直觉:count(1)应该更快,因为1是常量,不需要访问字段。
我的推理:
“count(1)的话执行器不需要其他东西,只看行数。count(*)也不看其他内容,统计行数,但是它可能需要处理列信息,会有额外开销。”
真相揭晓:我错了!
通过查阅MySQL官方文档和实测EXPLAIN,我发现:两者性能完全相同!
原理解析:
MySQL优化器做了特殊优化:
count(*) → 优化器识别 → 转换为"统计行数"操作
count(1) → 优化器识别 → 转换为"统计行数"操作
执行计划对比:
EXPLAIN SELECT count(*) FROM users;
EXPLAIN SELECT count(1) FROM users;
-- 两者的执行计划完全相同!
底层实现:
- InnoDB会选择最小的索引来扫描
- 不会真的去读取每一行的所有字段
- 只统计行的存在性
性能对比:
count(*) ≈ count(1) > count(主键) > count(非主键字段)
为什么count(email)最慢?
- 需要判断每一行的email字段是否为NULL
- 需要读取email字段的值
- 额外的判断开销
我的理解纠正
最初我有个错误理解:我以为count(email)是判断email是否相同。
后来我意识到:不是判断是否相同,而是判断是否为NULL!
正确理解:
- count(字段):统计非NULL的记录数
- 不是统计不同的值,那是count(DISTINCT 字段)
关键区别:
count(email) -- 统计非NULL的email数量(结果:2)
count(DISTINCT email) -- 统计不同email的数量(结果:2,john@qq.com和tom@qq.com)
性能对比总结:
🎯 第二部分:数据类型选择实战
场景1:用户ID的数据类型选择
在设计用户系统时,我首先遇到一个基础问题:用户ID应该选择什么数据类型?
候选方案:
- A. INT(32位,最大约21亿)
- B. BIGINT(64位,最大约922京)
- C. VARCHAR
我的第一直觉:
“VARCHAR吧,字符占内存小,因为用户id本身不会很大,所以varchar够了还能动态控制”
我的认知错误
在实际对比后,我发现这个想法完全错了!
实际存储对比:
- INT:固定4字节
- VARCHAR(‘123456’):6字节内容 + 1-2字节长度 = 7-8字节
我的顿悟:
“确实不能这样,应该用INT。如果数据量明确会超过int范围就用BIGINT。INT是32位,2的32次方个数,大概是几亿?”
准确计算:
- INT:-21亿 到 +21亿(2^31)
- UNSIGNED INT:0 到 42亿(2^32)
场景2:超大规模系统的ID选择
思考更深一层:微信、抖音这样的超大规模系统,用户量可能达到几十亿,应该用什么类型?
系统规模对比:
小型系统(几万用户): INT够用
中型系统(几百万用户):INT够用
大型系统(几千万用户):INT够用(留有余地)
超大系统(几亿用户): INT接近上限
顶级系统(几十亿用户):BIGINT必须
我查阅了实际数据:
- 微信:超过12亿用户 → 必须BIGINT
- 抖音:超过10亿用户 → 必须BIGINT
- 淘宝:超过8亿用户 → BIGINT
我的思考:
“微信每秒活跃用户几百万,那个数字感觉很庞大。小型系统不用BIGINT,但中大型系统必须用。”
设计原则总结:
- 预估未来3-5年的数据量
- 留有足够的冗余空间
- 数据类型一旦确定,很难修改(涉及大量数据迁移)
场景3:金额字段的数据类型选择
在电商系统中存储商品价格时,我又遇到了一个坑。
候选方案:
- A. FLOAT(单精度浮点)
- B. DECIMAL(10,2)(定点数)
- C. INT(存储分)
我的第一直觉:
“DECIMAL可能丢失精度吧,但是一般到分也就够了,我觉得是B”
但我错了!恰恰相反!
金额存储的精度陷阱
精度对比:
-- FLOAT的精度问题
CREATE TABLE test_float (price FLOAT);
INSERT INTO test_float VALUES (123.456);
SELECT * FROM test_float;
-- 结果:123.456001831055 ← 精度丢失!-- DECIMAL的精确存储
CREATE TABLE test_decimal (price DECIMAL(10,2));
INSERT INTO test_decimal VALUES (123.45);
SELECT * FROM test_decimal;
-- 结果:123.45 ← 完全精确!-- INT存分的方式
CREATE TABLE test_int (price INT COMMENT '单位:分');
INSERT INTO test_int VALUES (12345); -- 表示123.45元
SELECT price / 100 AS yuan FROM test_int;
-- 结果:123.45 ← 精确且节省空间!
我的理解纠正:
“原来小数类型(FLOAT/DOUBLE)反而会丢失精度!我是惯性思维了,C++中float转int会精度丢失,但这里情况恰好相反。”
金额存储决策树:
最佳实践:
金额字段选择:
1. 首选:INT(存储分) → 最节省空间,完全精确
2. 备选:DECIMAL(10,2) → 可读性好,精确
3. 禁用:FLOAT/DOUBLE → 精度不可控,容易出错
数据类型选择总结
场景 | 推荐类型 | 原因 |
---|---|---|
用户ID | INT / BIGINT | 定长,性能高,节省空间 |
商品价格 | INT(存分) / DECIMAL | 精确存储,避免精度丢失 |
用户名 | VARCHAR | 变长,节省空间 |
手机号 | CHAR(11) / BIGINT | 定长11位,CHAR或数字存储 |
邮箱 | VARCHAR(100) | 长度不定,需要变长 |
创建时间 | DATETIME / TIMESTAMP | 根据时区需求选择 |
🚀 第三部分:分库分表核心原理
引入场景:电商订单表的性能瓶颈
在学习过程中,我思考了一个实际场景:
假设我负责一个电商平台的订单系统:
CREATE TABLE orders (id BIGINT PRIMARY KEY,user_id INT,product_id INT,amount DECIMAL(10,2),status VARCHAR(20),created_at DATETIME,INDEX(user_id), -- 有索引!INDEX(created_at) -- 也有索引!
);
数据增长情况:
第1年:1000万订单
第2年:5000万订单
第3年:1.5亿订单
第4年:3亿订单 ← 现在
用户抱怨:
- “查询我的订单怎么这么慢?”
- “下单按钮点了半天没反应!”
我的初步分析
用EXPLAIN分析查询,发现扫描了几百万行数据!
我的第一反应:
“user_id上没有索引导致全表扫描”
但仔细看表结构:有INDEX(user_id)
啊!
我的困惑:索引都有了,为什么还慢?
问题的真相:即使有索引也慢!
我开始从更深层次分析问题:
线索1:B+树高度增加
1000万数据:B+树高度约3层
1亿数据: B+树高度约4层
3亿数据: B+树高度约4-5层 ← 现在
每多一层,就多一次磁盘IO!
线索2:服务器资源到极限
单台MySQL服务器:
- CPU: 16核,已经100%了
- 内存: 64GB,Buffer Pool不够用
- 磁盘IO: 每秒10000次读写,已到上限
- 网络带宽: 1Gbps,高峰期打满
线索3:业务压力
每秒新增订单:5000条
每秒查询请求:50000次
我的顿悟:
“大量的磁盘IO,数据量过大,服务器顶不住!”
问题本质:即使有索引,根本问题是架构设计的瓶颈!
解决方案:水平扩展
如果我是架构师,我会怎么解决?
方案对比:
- 垂直扩展(升级服务器):治标不治本,成本高
- 水平扩展(多台服务器):根本解决方案
我的思考:
“水平扩展吧,效率高且方便,负载均衡,打到每台机子的数据量小一点,每个上面存个三层。四层二十多亿可能会直接爆掉服务器。”
我的核心疑问:
“关键这些机子咋平衡呢?全部放一个机房还是分散?会不会多一层代理分发的时间?查询时咋知道要到具体哪台机子上查?”
这就是分库分表的核心难题!
核心难题:数据如何分配?
假设:我们有4台MySQL服务器
方案A:随机分配
订单1 → 随机选服务器1
订单2 → 随机选服务器3
订单3 → 随机选服务器2
问题:查询user_id=123456的所有订单时,需要查询4台服务器!
方案B:按订单ID取模
订单id % 4 = 0 → 服务器1
订单id % 4 = 1 → 服务器2
订单id % 4 = 2 → 服务器3
订单id % 4 = 3 → 服务器4
问题:查询user_id=123456的所有订单时,还是需要查询4台服务器!
方案C:按user_id取模
user_id % 4 = 0 → 服务器1
user_id % 4 = 1 → 服务器2
user_id % 4 = 2 → 服务器3
user_id % 4 = 3 → 服务器4
优势:查询user_id=123456的所有订单时,只需要查询1台服务器!
我的选择:
“C方案吧,按user_id分,最好几台机子间耦合性低一些,效率高”
✅ 这就是"分片键选择"的核心原则!
分片键选择原则
通过思考,我总结出了核心原则:
1. 根据最频繁的查询选择分片键
2. 让相关数据在同一个分片
3. 避免跨分片查询
电商订单系统的查询分析:
-- 查询1:用户查看自己的订单(高频)
SELECT * FROM orders WHERE user_id = 123456;
→ 按user_id分片:只查1台服务器 ✅-- 查询2:按订单ID查询(中频)
SELECT * FROM orders WHERE id = 888888;
→ 按user_id分片:不知道在哪台服务器 ❌-- 查询3:按时间范围查询(低频)
SELECT * FROM orders WHERE created_at BETWEEN '2025-01-01' AND '2025-01-31';
→ 按user_id分片:需要查4台服务器 ❌
我的思考:
“按频率高低划分服务优先级以及快慢,用户查看自己订单需要最快,那么可以就按user_id一级分片;id查询中频,也需要快速满足,那么可以将热点id缓存实现高频数据快速访问,二级分片再结合具体需要比如create_at来二级分片查询;时间查询响应速度可以慢一些,为了节省缓存资源这些就不用把它放缓存,user_id和create_at都有索引,可以联合索引稍微优化,且between本身就是范围查询,速度不会很慢”
✅ 完美的架构思维!
优化方案总结
三层优化策略:
【第1层:分片策略】
├─ 一级分片:user_id(满足高频查询)
└─ 二级分片:created_at(分散热点数据)【第2层:缓存策略】
├─ 热点订单ID → Redis(中频查询加速)
└─ 热点用户数据 → Redis(高频查询加速)【第3层:索引优化】
├─ 联合索引(user_id, created_at)
└─ 时间范围查询用索引(可接受的慢查询)
这就是真实生产环境的架构方案!
🌳 第四部分:B+树结构深度回顾
我的疑问:二级索引为什么会到5层?
在分析性能问题时,我看到有人提到"二级索引可能到5层",感到困惑。
我的疑问:
“二级索引为什么会到5层?B+树不是3-4层吗?”
二级索引的回表过程
查询示例:
SELECT * FROM orders WHERE user_id = 123456;
完整过程:
步骤1:走user_id二级索引树(4层)
第1层(根节点): 在内存中(Buffer Pool) → 0次IO
第2层: 可能在内存中 → 0次IO
第3层: 可能需要磁盘读取 → 1次IO
第4层(叶子节点): 需要磁盘读取 → 1次IO
叶子节点存储内容:
[user_id=123456, 主键id=100]
[user_id=123456, 主键id=500]
[user_id=123456, 主键id=1200]
...
[user_id=123456, 主键id=9999] ← 假设有100条
找到了100个主键id!
步骤2:回表查询(100次!)
对每个主键id,都要走一次主键索引树(4层):
查询 id=100:第1层(根节点): 在内存 → 0次IO第2层: 可能在内存 → 0次IO第3层: 磁盘读取 → 1次IO第4层(叶子节点):磁盘读取 → 1次IO → 找到完整数据查询 id=500:... 重复上述过程... (重复100次)
总IO次数:
理想情况:2(二级索引)+ 100×2(回表)= 202次IO
最坏情况:4(二级索引)+ 100×4(回表)= 404次IO
我的疑惑:
“为啥二级索引只需要4次IO,回表这里就要100次?正常IO次数是多少?”
我查资料后理解了:
- 二级索引:走一次索引树,找到100个主键id(4次IO)
- 回表:每个主键id都要走一次主键索引树(100 × 4次IO)
- 正常IO次数:主键查询2-4次IO,二级索引查询可能上百次IO
二级索引查询流程图:
B+树结构细节回顾
我的疑问:
“这个索引树咋会这么高呢,我有点忘记结构了,一般不是三层吗,第二层放索引,第三层放数据,大概二十一万数据还是多少忘了”
B+树结构图:
每层能存多少数据?
假设:
- 每个数据页大小 = 16KB(InnoDB默认)
- 每个索引指针 = 6字节
- 每个键值(主键BIGINT)= 8字节
第1层(根节点):
16KB ÷ (8字节键 + 6字节指针) ≈ 1170 个指针
→ 能指向1170个第2层节点
第2层(非叶子节点):
1170个节点 × 每个节点1170个指针 ≈ 136万 个指针
→ 能指向136万个第3层节点(叶子节点)
第3层(叶子节点):
假设每条数据1KB
16KB ÷ 1KB ≈ 16条数据/页136万个叶子节点 × 16条 ≈ 2176万条数据
我的记忆:
“那确实不能这样,应该用int,如果数据量明确会超过int范围就用bigint,int32位,2的三十二次方个数,大概是几亿?”
正确!3层约2100万数据,4层约24.5亿数据!
B+树查找过程
我的疑问:
“每一层是咋走下去的,三层都是纯索引键值吗还有啥,为啥能走下去”
详细查找过程:WHERE id = 15
步骤1:读取根节点
比较:15在 10~20 之间
→ 走指针2
步骤2:读取第2层某个节点
比较:15在 12~18 之间
→ 走对应指针
步骤3:到达叶子节点
→ 找到id=15的完整数据
每个节点存的内容:
非叶子节点:
[指针 | 键值 | 指针 | 键值 | 指针 | 键值 | 指针]
- 键值:用于比较范围(如10, 20, 30)
- 指针:指向下一层节点的磁盘地址(页号)
叶子节点:
[完整数据行1][完整数据行2][完整数据行3]...
- 主键索引:叶子节点存完整行数据
- 二级索引:叶子节点存索引列+主键值
🎯 第五部分:订单ID设计方案
问题:按订单ID查询怎么办?
按user_id分片后,如果按订单ID查询,不知道在哪台服务器,怎么办?
解决方案:在订单ID里嵌入分片信息!
雪花算法(Snowflake ID)改进版
传统雪花算法:
【64位订单ID】
┌─────────┬─────────┬─────────┬─────────┐
│ 41位时间戳 │ 10位机器ID │ 12位序列号 │ 1位符号位 │
└─────────┴─────────┴─────────┴─────────┘
改进版:嵌入分片信息:
【64位订单ID】
┌─────────┬──────┬─────────┬─────────┐
│ 41位时间戳 │ 4位分片ID │ 6位机器ID │ 12位序列号 │
└─────────┴──────┴─────────┴─────────┘↑这里!
查询流程
SELECT * FROM orders WHERE id = 1234567890123456;
步骤:
1. 从ID中提取分片ID = (订单ID >> 18) & 0xF
2. 计算出分片ID = 3
3. 直接去第3号服务器查询
4. 在该服务器上用主键索引查询 → 2-4次IO ✅
这样按ID查询也很快了!
🔧 第六部分:完整架构设计
我的架构思考
我的完整方案:
“按频率高低划分服务优先级以及快慢,用户查看自己订单需要最快,那么可以就按user_id一级分片;id查询中频,也需要快速满足,那么可以将热点id缓存实现高频数据快速访问(1,2查询都会加快),二级分片再结合具体需要比如create_at来二级分片查询;时间查询响应速度可以慢一些,为了节省缓存资源这些就不用把它放缓存,user_id和create_at都有索引,可以联合索引稍微优化,且between本身就是范围查询,速度不会很慢”
完整架构图
三层优化策略详解
第1层:分片策略
├─ 一级分片:user_id % 8
│ └─ 目标:让同一用户的订单在同一分片
├─ 二级分片:created_at(年月)
│ └─ 目标:分散热点数据,历史数据归档
└─ 订单ID嵌入分片信息└─ 目标:支持按ID快速定位
第2层:缓存策略
├─ 热点订单缓存
│ ├─ Key: order:{order_id}
│ ├─ TTL: 1小时
│ └─ 命中率目标:90%
│
└─ 热点用户缓存├─ Key: user_orders:{user_id}├─ TTL: 30分钟└─ 存储:最近20条订单
第3层:索引优化
├─ 主键索引:id(聚簇索引)
├─ 二级索引:user_id
├─ 联合索引:(user_id, created_at)
│ └─ 用于用户订单列表+时间筛选
└─ 二级索引:status└─ 用于状态筛选(待支付、已完成等)
数据迁移和扩容方案
我的思考:
“数据迁移定期维护一下吧,如果迁移就停服维护将所有数据迁移?热点数据的话定期分散一下我觉得也可以,特殊化处理?”
查阅资料后,我发现有更好的方案:
方案A:停服迁移(简单但不可取)
1. 停止服务
2. 数据迁移
3. 验证数据
4. 重启服务
问题:不能停服!
方案B:双写迁移(生产环境方案)
步骤1:新老系统同时写写订单时 → 同时写到4台服务器和8台服务器步骤2:后台迁移老数据慢慢把4台服务器的老数据迁移到8台步骤3:切换读流量逐步把读请求切到8台服务器步骤4:下线老系统迁移完成,删除4台服务器
📚 总结与反思
今天学到的核心知识点
1. count函数的本质
- count(*) ≈ count(1):统计行数,性能相同
- count(字段):统计非NULL值,需要判断,性能较差
- MySQL优化器会自动选择最小索引扫描
2. 数据类型选择的实战思维
- ID字段:INT(40亿内)/ BIGINT(超大规模)
- 金额字段:INT(存分,精确且节省)/ DECIMAL(可读性好)
- 字符字段:VARCHAR(变长)/ CHAR(定长)
- 核心原则:预估数据量、精确度要求、存储效率
3. 分库分表的架构思维
- 问题本质:单表数据量过大 → 性能下降 → 架构瓶颈
- 解决方案:水平扩展 → 分片存储 → 路由查询
- 核心难题:如何选择分片键?如何路由查询?
- 设计原则:按最高频查询选择分片键
4. B+树结构深度理解
- 3层B+树:约2100万数据
- 4层B+树:约24.5亿数据
- 二级索引回表:可能产生大量IO
- 优化思路:覆盖索引、联合索引
5. 完整架构设计能力
- 分片策略:一级分片 + 二级分片
- 缓存策略:热点数据缓存
- 索引优化:联合索引、覆盖索引
- 核心思维:按查询频率分层优化
我的成长与感悟
1. 从单一角度到系统思维
- 最初:只考虑索引优化
- 现在:考虑分片、缓存、索引的完整方案
- 收获:架构设计需要多层优化策略
2. 从理论到实战应用
- 最初:知道B+树结构
- 现在:理解IO次数、回表成本、性能瓶颈
- 收获:理论要结合实际场景理解
3. 从被动接受到主动思考
- 最初:被动回答问题
- 现在:主动提出架构方案
- 收获:苏格拉底式教学让我深度思考
4. 从局部优化到全局设计
- 最初:优化单个SQL
- 现在:设计完整的分库分表方案
- 收获:架构师思维需要全局视角
待深入学习的内容(下篇预告)
1. 一致性Hash扩容方案
- 如何优雅地从4台扩容到8台?
- 如何减少数据迁移量?
2. 分布式事务处理
- 跨分片的订单如何保证一致性?
- 2PC、TCC、Saga方案对比
3. 主从复制原理
- binlog、relay log的作用
- 主从延迟如何处理?
4. 读写分离实现
- 如何将读请求分发到从库?
- 主从延迟对业务的影响
5. 生产环境最佳实践
- 事务隔离级别选择
- 配置参数调优
- 监控和故障处理
面试准备
核心面试题:
- count(*)和count(1)有什么区别?性能如何?
- 为什么金额字段不能用FLOAT?应该用什么?
- 什么是分库分表?如何选择分片键?
- 3层B+树能存多少数据?如何计算?
- 二级索引的回表过程是怎样的?如何优化?
- 如何设计一个支持亿级用户的订单系统?
答题思路:
- 从问题本质出发
- 结合实际场景分析
- 给出多种方案对比
- 说明优缺点和适用场景
- 展示架构设计思维
🎯 下篇预告
《MySQL高级特性深度学习(下):分库分表进阶与主从复制实战》
敬请期待! 🚀
📝 学习笔记
作者:[进击的圆儿]
日期:2025年10月10日
学习方式:苏格拉底式对话教学
目标:MySQL高级特性深度掌握,面试+实战双向准备本文特色:记录真实学习过程,包含疑问、思考、纠正、顿悟的完整路径。
适合人群:MySQL进阶学习者、面试准备者、架构设计学习者
✨ 如果这篇博客对你有帮助,欢迎点赞、收藏、评论!