1. 从 MySQL 内核角度:能不能一次读 1000W?

能读,但不是 “一次性加载到内存”。

InnoDB 处理 SELECT * FROM big_table 的真实流程:

  1. 服务器开启结果集流
  2. 每次从磁盘读取 1 个页(默认 16KB)
  3. 从页里解析出若干行数据
  4. 通过网络逐批发给客户端
  5. 读完一页再读下一页,直到 1000W 条全部读完发完

它不会:

  • 先把 1000W 条全 load 进内存
  • 也不会一次性拼成一个巨大数据包

所以技术上是可以读完 1000W 条的,只是时间问题。


2. 那为什么不能这么干?

虽然能读,但会触发生产级事故

(1)长时间占用连接,事务快照不释放

  • InnoDB 在 RR 隔离级别下,查询开始会生成一个一致性视图
  • 只要查询没结束,undo log 不能清理
  • 结果:回滚段膨胀、主从延迟飙升、更新语句被阻塞

(2)网络与客户端直接崩

  • 1000W 条持续往客户端吐数据
  • Navicat、JDBC 连接会OOM 崩溃
  • 网卡被打满,影响整个应用

(3)MySQL 本身被拖垮

  • 全表扫描,磁盘 IO 跑满
  • 大量数据进入网络缓冲区,内存占用飙升
  • 其他业务 SQL 排队、超时

3. 一句话区分:能读 vs 能用

  • 能不能一次性读取 1000W?,MySQL 支持流式读取超大结果集。

  • 能不能在生产上这么执行?绝对不能,等于自杀。


4. 那 MySQL 到底怎么安全读 1000W?

分批流式读取

sql

SELECT * FROM t WHERE id > ? LIMIT 1000;

循环执行,直到读完。 这才是 MySQL 处理大数据量的标准姿势。

5、千万级表直接执行 SELECT * 会发生什么?

在 MySQL 中对一张1000 万条数据的表执行不带任何条件、无 LIMIT 的 SELECT *,技术上 MySQL 可以完成全表读取,但会引发一系列生产级风险,绝非简单的 “查询慢”:

  1. 全表扫描,磁盘 IO 瞬间打满 InnoDB 会逐页(默认 16KB)从磁盘加载数据,不走任何索引,磁盘 IO 利用率接近 100%,服务器整体性能急剧下降。
  2. 结果集过大,极易引发内存溢出 MySQL JDBC 默认全量拉取结果集,驱动会尝试将 1000 万条数据全部加载到客户端内存,直接导致应用 OOM 崩溃;服务端若内存不足,还会生成磁盘临时表,进一步加剧性能损耗。
  3. 一致性视图长期持有,阻塞业务写入 InnoDB 默认 RR 隔离级别,大查询会长期持有一致性视图,导致 undo log 无法回收、回滚段膨胀,同时阻塞其他事务的写入操作,引发锁等待、接口超时。
  4. 网络带宽被占满,服务整体不可用 千万级数据持续通过网络传输至客户端,网卡流量打满,不仅当前查询卡死,还会影响同服务器其他业务的正常通信。
  5. 主从延迟飙升,数据同步异常 全表扫描占用大量数据库资源,导致主库 binlog 同步延迟,从库数据滞后,严重时破坏数据一致性。

简言之,生产环境直接 SELECT * 查 1000 万数据,等同于数据库 “雪崩”,会直接拖垮整个服务。

6、JDBC 开启流式读取,避免千万级数据客户端 OOM

MySQL JDBC 默认全量拉取数据,想要实现服务端分批推送、客户端逐条处理的真正流式读取,需同时满足 4 个核心配置,缺一不可:

  1. JDBC URL 添加关键参数 在连接串中开启服务端游标并设置默认拉取条数:
    jdbc:mysql://ip:port/db?useCursorFetch=true&defaultFetchSize=1000
    
    • useCursorFetch=true:开启 MySQL 服务端游标,支持分批读取
    • defaultFetchSize=1000:设置单次拉取数据量(可根据业务调整)
  2. 设置 Statement 为只读、向前只进模式
    // 连接设为只读
    conn.setReadOnly(true);
    PreparedStatement pstmt = conn.prepareStatement(
        sql,
        ResultSet.TYPE_FORWARD_ONLY,  // 结果集只向前遍历
        ResultSet.CONCUR_READ_ONLY    // 只读模式
    );
    // 单次拉取1000条
    pstmt.setFetchSize(1000);
    
  3. 逐条处理结果集,不批量装载内存 避免将所有数据存入 List 等集合,直接边读边处理:

    java

    运行

    ResultSet rs = pstmt.executeQuery();
    while (rs.next()) {
        // 逐条解析、清洗、写入文件/目标库,不缓存全量数据
    }
    
  4. 关键注意事项
    • 仅支持 InnoDB 引擎,查询期间连接会被长期占用,建议使用独立连接而非连接池;
    • 不建议使用旧版 setFetchSize(Integer.MIN_VALUE) 一次性流式方案,易引发连接阻塞。

通过该方式,客户端内存仅维持单次拉取的千级数据量,即便读取 1000 万条数据也不会 OOM。

7、深度分页 LIMIT 5000000,10 为何远慢于主键分批查询?

1. 深度分页的执行缺陷

sql

-- 深度分页
SELECT * FROM table LIMIT 5000000,10;

MySQL 执行该语句时,必须先扫描并丢弃前 500 万条数据,逐行计数直到定位到第 5000001 条,再返回后续 10 条。这意味着大量 IO、CPU 资源浪费在无效扫描上,偏移量越大,耗时呈指数级增长。

2. 主键分批的执行优势

sql

-- 主键分批
SELECT * FROM table WHERE id > 5000000 LIMIT 10;

主键 id 是 InnoDB 聚簇索引,MySQL 可直接通过索引定位到 id > 5000000 的位置,仅读取 10 条数据即结束查询,耗时为毫秒级。

3. 核心差距总结

  • 深度分页:从头遍历 + 丢弃海量数据,越往后越慢;
  • 主键分批:索引直接定位,无无效扫描,性能相差几十倍甚至上百倍。

8、1000 万数据导出、清洗的生产安全方案

针对千万级数据的导出与清洗,核心原则是分批读取、控制压力、支持断点续跑,避免全表扫描拖垮数据库,推荐 4 种标准方案:

方案 1:主键分段分批(最通用、最稳妥)

  1. 先查询表最大 ID:SELECT MAX(id) FROM table
  2. lastId=0 开始循环查询,每次通过主键筛选分批:
    SELECT * FROM table WHERE id > ? LIMIT 1000;
    
  3. 每处理完一批,更新 lastId 为当前批次最大 ID,直至遍历完所有数据。 优势:无深度分页、对数据库压力小、可暂停 / 断点续跑。

方案 2:mysqldump 一致性导出

适合整表无锁导出,关键参数保障流式读取:

mysqldump -uroot -p 库名 表名 --where="id>0" --quick --single-transaction > data.sql
  • --quick:逐行读取数据,不加载至内存;
  • --single-transaction:基于 InnoDB 一致性快照,不加锁,不影响业务。

方案 3:专业工具 mydumper

比 mysqldump 更快,支持并行导出,适合超大数据量,可控制并发与限速,降低对数据库的影响。

方案 4:企业级 ETL 工具

使用 DataX、FlinkX、Kettle 等工具,内置主键分段、流式读取、限速重试等能力,无需手动开发,适合大规模数据同步与清洗。

严禁操作

  • 禁止直接 SELECT * 全表拉取;
  • 禁止使用深度分页 LIMIT 1000000,10 处理大数据;
  • 禁止将千万级数据一次性装载至内存集合;
  • 禁止在业务高峰期执行全表大数据操作。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐