首页
学习
活动
专区
圈层
工具
发布

MySQL 中 如果我 select * from 一个有 1000 万行的表,内存会飙升么?

java.lang.OutOfMemoryError: Java heap space

日志里只剩这一句,前面那条 SQL 很朴素:

select * from user_event_log;

表不复杂,1000 万行。 代码也不复杂,甚至看着还挺“正常”。

List<Map<String, Object>> rows = jdbcTemplate.queryForList(

      "select * from user_event_log"

);

这种代码我第一眼就不太信。不是 MySQL 多脆弱,是你把 1000 万行一次性塞进 Java 堆里,谁来都扛不住。

select * from 一个 1000 万行的表,内存会不会飙升?

得分开看。

MySQL 服务器那边,不是说你一执行select *,它就把 1000 万行全部一次性加载到内存里,然后再慢慢返回给你。InnoDB 会按页读取数据,读到的数据页会进 buffer pool,热点页会留下,冷数据会被淘汰。

所以 MySQL 的内存可能上涨,但通常不会因为一条普通全表查询直接把所有行“完整塞进内存”。

真正容易炸的,反而是 Java 应用。

尤其是这种写法:

public List<UserEvent> dumpAll() {

  return jdbcTemplate.query(

          "select * from user_event_log",

          (rs, rowNum) -> {

              UserEvent e = new UserEvent();

              e.setId(rs.getLong("id"));

              e.setUserId(rs.getLong("user_id"));

              e.setAction(rs.getString("action"));

              e.setPayload(rs.getString("payload"));

              e.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());

              return e;

          }

  );

}

这段代码的问题不在 SQL 语法,在返回值。

List<UserEvent>。

1000 万个对象,加上对象头、字段引用、字符串、时间对象、List 扩容数组,再加上 JDBC driver 内部缓冲。你以为查的是 1000 万行,JVM 看到的是一堆对象森林。

线上如果开了 GC 日志,差不多会看到这种味道:

[GC pause Young 1800M->1600M]

[Full GC 3900M->3850M]

java.lang.OutOfMemoryError: Java heap space

这时候别急着调大-Xmx。调大也只是晚死一点。

还有一个坑,很多人以为自己写了ResultSet就一定是流式读取。未必。

这种代码看起来比queryForList老实一点:

try (Connection conn = dataSource.getConnection();

   PreparedStatement ps = conn.prepareStatement(

           "select * from user_event_log");

   ResultSet rs = ps.executeQuery()) {

  while (rs.next()) {

      handle(rs);

  }

}

但在 MySQL JDBC 下,如果连接参数和 fetchSize 没配对,驱动可能还是会把结果集拉到客户端缓存起来。你代码是一行一行处理,底层不一定真是一行一行取。

我一般会这么写导出类任务,别追求优雅,先让它别炸。

public void exportUserEvent(Path file) throws Exception {

  String sql = """

          select id, user_id, action, created_at

          from user_event_log

          where id > ?

          order by id

          limit ?

          """;

  long lastId = 0L;

  int size = 5000;

  try (BufferedWriter writer = Files.newBufferedWriter(file)) {

      while (true) {

          int count = 0;

          try (Connection conn = dataSource.getConnection();

               PreparedStatement ps = conn.prepareStatement(sql)) {

              ps.setLong(1, lastId);

              ps.setInt(2, size);

              try (ResultSet rs = ps.executeQuery()) {

                  while (rs.next()) {

                      lastId = rs.getLong("id");

                      writer.write(lastId + ","

                              + rs.getLong("user_id") + ","

                              + rs.getString("action") + ","

                              + rs.getTimestamp("created_at"));

                      writer.newLine();

                      count++;

                  }

              }

          }

          if (count < size) {

              break;

          }

      }

  }

}

这里我不用limit offset。

select * from user_event_log limit 9000000, 5000;

这种分页越往后越慢。MySQL 得先跳过前面 900 万行,再给你 5000 行。你让它干这活,它当然喘。

按主键往后扫,至少执行计划比较干净:

explain

select id, user_id, action, created_at

from user_event_log

where id > 8800000

order by id

limit 5000;

看到type: range,走主键或索引,我心里才踏实一点。

如果你真要流式读,也可以这么干,但连接串要配合:

jdbc:mysql://127.0.0.1:3306/app

?useCursorFetch=true

&useServerPrepStmts=true

代码里再控制 fetchSize:

public void scanSlowly() throws SQLException {

  String sql = """

          select id, user_id, action, payload

          from user_event_log

          order by id

          """;

  try (Connection conn = dataSource.getConnection();

       PreparedStatement ps = conn.prepareStatement(

               sql,

               ResultSet.TYPE_FORWARD_ONLY,

               ResultSet.CONCUR_READ_ONLY)) {

      ps.setFetchSize(1000);

      try (ResultSet rs = ps.executeQuery()) {

          while (rs.next()) {

              consumeOneRow(

                      rs.getLong("id"),

                      rs.getLong("user_id"),

                      rs.getString("action"),

                      rs.getString("payload")

              );

          }

      }

  }

}

不过我不太喜欢在业务接口里这么扫。

因为一个大查询挂在那里,连接一直占着,事务如果没处理好,还可能拖住 undo 清理。你以为只是导个数据,数据库那边看着可能是一条长时间不结束的查询。

再说select *。

这个星号很讨厌。表一开始只有 5 个字段,大家都觉得无所谓。过几个月加了remark、extra_json、content_blob,老代码一点没改,查询的数据量翻几倍。

你查列表页,只要 4 个字段,就别写:

select * from user_event_log;

写清楚:

select id, user_id, action, created_at

from user_event_log

where created_at >= ?

order by id

limit 5000;

还有一种情况,MySQL 服务器内存也会明显动。

比如你查 1000 万行,还来个没索引的排序:

select *

from user_event_log

order by created_at desc;

如果created_at没合适索引,执行计划里可能出现:

Using filesort

这类 SQL 我一般先停一下,不急着跑。因为它不只是扫表,还要排序。内存不够就落磁盘临时文件,慢得很稳定。

比较靠谱的是加索引,或者把查询条件压窄:

create index idx_event_created_id

on user_event_log(created_at, id);

然后查的时候别贪:

select id, user_id, action, created_at

from user_event_log

where created_at >= '2026-05-01 00:00:00'

order by created_at, id

limit 5000;

所以这个问题最后不是一句“会”或者“不会”。

如果你在 MySQL 命令行里执行select * from 大表,MySQL 会边读边返回,客户端慢慢接,服务端不一定爆。

如果你在 Java 里用queryForList、MyBatis 直接返回List,那内存大概率会飙。不是 MySQL 飙,是你的 JVM 飙。

如果 SQL 里带没索引的排序、分组、大字段、临时表,那 MySQL 服务器也可能跟着难受。

我自己的处理顺序一般就三步:

先砍掉select *,只查需要的列。

再看是不是一次性装进 List,能分批就分批,能流式就流式。

最后看执行计划,尤其盯着全表扫描、filesort、临时表、大 offset。

1000 万行不可怕,可怕的是你把它当 1000 行写。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OQ7Nezt8Yc3l1DM9rcnqBfjg0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。
领券