🔥 爆款预警:全网最干的千万级大表无索引分页实战!同事小A踩过的坑你别再踩了!收藏=学会!
家人们谁懂啊!小A差点被生产环境坑惨!😱
同事小A最近在做电商数据同步项目,从6个MySQL源库拉数据。大部分表都有 update_time 索引,时间游标分页用得飞起~
但偏偏有一张表——订单明细表,1000万+有效记录,update_time 居然没索引! 更惨的是源库只读!找DBA加索引?直接被怼回来:"只读库,别想DDL!"
先看看小A踩过的坑有多惨烈👇
| 方案 | 结果 | 小A的心情 |
|---|---|---|
LIMIT 10000 OFFSET 5000000 | 直接卡死,CPU直接干到100% | 🤯 完全懵了 |
WHERE update_time > ? ORDER BY update_time | 第一页11分钟,小A差点睡着 | 😴 这太慢了,完全等不及 |
| 找DBA加索引 | "只读库,别搞事" | 💔 心碎了一地 |
OFFSET分页为什么这么慢?看完你就懂了!💡
先看这个SQL,是不是你平时写的?
SELECT * FROM order_detail
WHERE is_deleted = 0
ORDER BY update_time
LIMIT 10000 OFFSET 5000000;
MySQL执行过程:
1️⃣ 扫描所有满足 is_deleted = 0 的行(1000万条!)
2️⃣ 对这1000万行进行 filesort(全表扫描+临时文件排序)
3️⃣ 跳过前500万行(白干了!)
4️⃣ 返回1万行
核心问题:OFFSET 5000000意味着MySQL必须先把前面500万行全过一遍,即使你只要1万行!而且没有索引的话,排序本身就是灾难级别的慢!
救星来了!主键游标分页!🚀
游标分页(Cursor-based Pagination) 的核心思想:用上一批最后一条的主键值作为下一批的起点,不是用OFFSET跳过!
时间游标(有索引时很香)
-- 第一批
SELECT * FROM order_detail
WHERE is_deleted = 0 AND update_time > '2024-01-01 00:00:00'
ORDER BY update_time LIMIT 10000;
-- 第二批:用上一批最后一条的update_time!
SELECT * FROM order_detail
WHERE is_deleted = 0 AND update_time > '2024-03-15 12:30:00'
ORDER BY update_time LIMIT 10000;
前提:update_time 上有索引!这个索引必须有!
主键游标(无索引时的救命稻草!)
这就是我们的救星!利用InnoDB聚簇索引(主键索引)的特性:
-- 第一批
SELECT * FROM order_detail
WHERE is_deleted = 0 AND id > 0
ORDER BY id LIMIT 10000;
-- 第二批:用上一批最大的id!
SELECT * FROM order_detail
WHERE is_deleted = 0 AND id > 10000
ORDER BY id LIMIT 10000;
为什么这么快? ✨
- InnoDB的主键就是聚簇索引!数据按主键顺序物理存储!
ORDER BY id不需要额外排序!天然有序!WHERE id > 10000直接通过B+树定位!不需要扫描前面的行!
完整Python实现!直接拿走用!💻
def fetch_by_pk_cursor(src_conn, table_name, pk_field='id',
where_clause='is_deleted = 0',
batch_size=10000):
"""主键游标分页拉取全量数据"""
pk_cursor_value = 0 # 主键从0开始,确保不遗漏
total = 0
while True:
sql = f"""
SELECT * FROM {table_name}
WHERE {where_clause} AND {pk_field} > {pk_cursor_value}
ORDER BY {pk_field}
LIMIT {batch_size}
"""
batch = fetch_all(src_conn, sql)
if not batch:
break
# 更新游标:取本批最大主键值!
max_pk = max(row.get(pk_field, 0) for row in batch)
if max_pk > 0:
pk_cursor_value = max_pk
total += len(batch)
# 处理当前批次数据...
process_batch(batch)
# 进度日志
if total % 50000 == 0:
print(f" 🚀 已处理 {total} 条...")
print(f" ✅ {table_name}: 共 {total} 条")
return total
关键细节!别踩坑!⚠️
1. 游标值取本批最大主键!
max_pk = max(row.get(pk_field, 0) for row in batch)
if max_pk > 0:
pk_cursor_value = max_pk
别用 batch[-1] 的主键值! 因为虽然按主键排序,但如果某些记录被跳过(不满足WHERE条件),最后一条的主键不一定是最大的!
2. 起始值从0开始!
pk_cursor_value = 0 # 主键通常从1开始,0确保不遗漏第一条!
3. INSERT IGNORE保证幂等!
INSERT IGNORE INTO target_table (id, col1, col2, ...)
VALUES (?, ?, ?, ...)
全量拉取可能与已有数据重叠,INSERT IGNORE 遇到重复主键自动跳过!不会重复!
三种分页模式对比!一目了然!📊
| 分页模式 | SQL模板 | 适用场景 | 时间复杂度 |
|---|---|---|---|
| OFFSET分页 | LIMIT x OFFSET y | 小数据量 | O(n) 越往后越慢! |
| 时间游标分页 | WHERE time > ? ORDER BY time | 有时间索引 | O(log n) 恒定快! |
| 主键游标分页 | WHERE pk > ? ORDER BY pk | 有主键无时间索引 | O(log n) 恒定快! |
生产环境实测数据!惊呆了!🔥
| 方式 | 数据量 | 耗时 | 速率 |
|---|---|---|---|
| OFFSET分页 | 1000万 | 直接卡死! | 0条/秒 💀 |
| update_time游标分页 | 1000万 | 第一页11分钟! | 极慢 😴 |
| 主键游标分页 | 1000万 | 约42分钟 | 约4000条/秒 🚀🚀🚀 |
注意事项!记好这些!📝
1️⃣ 主键必须是递增的:UUID主键不适用!自增INT/BIGINT才行!
2️⃣ WHERE条件要慎重:is_deleted = 0 AND id > ? 中 is_deleted 没索引也没关系,因为id范围缩小了扫描量!
3️⃣ 不适合增量过滤:主键游标分页是全量拉取,增量过滤需要在下游完成!
4️⃣ 网络断连要重试:1000万条拉42分钟,期间可能断连,需要重连机制!
总结!收藏这一篇就够了!📌
遇到大表无索引的分页场景时:
- ❌ 别用OFFSET分页!越往后越慢!千万级直接卡死!
- ❌ 别用无索引字段排序!filesort全表扫描,等死你!
- ✅ 用主键游标分页!利用聚簇索引,O(log n)恒定快!
- ✅ 配合INSERT IGNORE!全量拉取+幂等写入!
💡 一句话总结:没有索引别慌!主键游标来帮忙!聚簇索引天然有序!比任何filesort都强!
家人们! 如果这篇文章对你有帮助,点赞👍+收藏⭐+转发👥 三连不迷路!评论区聊聊你遇到过的MySQL分页坑!👇
世局板块
1. 美伊局势再现“罗生门”。
5月29日,美伊局势陷入迷雾。美媒称双方已就60天停火备忘录达成一致,正待特朗普批准,但伊朗迅速否认。此外,伊朗宣称摧毁一架美军战机,同样遭美方否认。伊军方还解释称,此前阿巴斯港的巨响系对海峡非法船只的警告性射击。
2. 以军下令扩大加沙控制区。 以色列总理内塔尼亚胡28日表示,以军已控制加沙地带约60%区域,并下令将控制范围扩大至70%。他称正对哈马斯施压,并透露与特朗普保持密切沟通。内塔尼亚胡还证实,以军正深化在黎巴嫩的军事行动,打击真主党目标。
3. 美日西南诸岛联合军演。
5月29日,美日在日本西南诸岛举行联合军演,核心区域距台湾仅约110公里,并首次设立联合战术协调中心。日方称旨在强化区域威慑。我国防部严正警告,反对损害第三方利益的军事合作,指出日本右翼的新型军国主义倾向正成为东亚祸乱之源。
4. 欧盟批准援乌900亿欧元贷款。
当地时间5月28日,欧盟公告称已与乌克兰正式通过提供900亿欧元贷款的协议及谅解备忘录。根据安排,首批资金预计将于今年6月启动拨付。该协议旨在帮助乌克兰应对持续冲突带来的经济压力,标志着欧盟对乌长期支持迈出实质性一步。
5. 美国辛格伦火箭测试爆炸。
5月28日,美国蓝色起源公司的“辛格伦”重型火箭在佛罗里达州进行静态点火测试时突发剧烈爆炸,助推器完全损毁。所幸现场无人员伤亡。由于此前已因发射失败停飞,此次事故恐致复飞计划全面搁浅。目前,美国联邦航空管理局已介入调查。