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

Sherwin.Wei Lv7

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

回答重点

内存不会飙升。

因为 MySQL 在执行简单 SELECT * FROM 查询时,不会一次性将所有 1000 万行数据加载到内存中,而是通过逐批次处理的方式来控制内存使用。也就是说 MySQL 是边查边发送数据给客户端

分批的大小与 net_buffer_length 有关,默认 16384 字节(16 KB)。

所以实际上获取数据和发送数据的流程是这样的:

  • 获取一行,写入到 net_buffer 中。
  • 重复获取行,直到 net_buffer 写满,调用网络接口发出去。
  • 发送成功后 清空 net_buffer
  • 再继续取下一行,并写入 net_buffer,继续上述的操作。

这里还需要注意一点,发送的数据是需要被客户端读取的,如果客户端读取的慢,导致本地网络栈(socket send buffer)写满了,那么当前数据的写入会被暂停。

综上,**SELECT * FROM 一个有 1000 万行的表,不会导致内存飙升**。

扩展知识

注意 ORDER BY 和 GROUP BY

正常不需要排序和分组的查询不会占用过多的数据和影响 MySQL Server 的执行,但是如果涉及到分组和排序,那么就需要使用额外的内存(或外部空间)来处理全量数据,可能会占用额外的内存并影响查询的效率。

客户端的处理方式

虽然 MySQL 会分批返回数据,但是客户端需要做一定的处理,不能全量保存数据,否则可能会导致内存溢出。

客户端需要使用流式处理或者游标来查询。

  • 在编写查询代码时,使用流式读取方式(如 JDBC 中的 ResultSet.FETCH_SIZE),这会让数据库逐步返回数据,客户端按需处理。

JDBC 简单示例,重点就是 statement.setFetchSize(Integer.MIN_VALUE);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 获取数据库连接并开启流式处理
try (Connection connection = DriverManager.getConnection(URL, USER, PASSWORD)) {
// 禁用自动提交,避免结果集完全缓存到内存
connection.setAutoCommit(false);

// 创建一个可以进行流式处理的 Statement
try (PreparedStatement statement = connection.prepareStatement(query,
ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) {
// 设置流式处理的关键:MySQL 特定配置
statement.setFetchSize(Integer.MIN_VALUE);

// 执行查询
try (ResultSet resultSet = statement.executeQuery()) {
while (resultSet.next()) {
// 获取每行数据,这里假设有 "id" 和 "name" 两列
int id = resultSet.getInt("id");
String name = resultSet.getString("name");

// 处理数据(例如打印出来)
System.out.printf("ID: %d, Name: %s%n", id, name);
}
}
}
} catch (SQLException e) {
e.printStackTrace();
}

MyBatis 简单示例,重点就是 session.getConfiguration().setDefaultFetchSize(Integer.MIN_VALUE);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try (SqlSession session = sqlSessionFactory.openSession()) {
// 设置 fetchSize 为 Integer.MIN_VALUE 启用流式查询
session.getConfiguration().setDefaultFetchSize(Integer.MIN_VALUE);

// 获取 Mapper
LargeTableMapper mapper = session.getMapper(LargeTableMapper.class);

// 使用 ResultHandler 处理数据,逐行读取
mapper.selectAll(resultContext -> {
LargeTableRecord record = resultContext.getResultObject();
System.out.printf("ID: %d, Name: %s%n", record.getId(), record.getName());
// 可以在这里对数据进行处理,避免一次性加载到内存
});
}

MyBatis 游标查询示例,重点就是使用 Cursor 接口

1
2
3
4
5
6
7
8
9
10
11
12
try (SqlSession session = sqlSessionFactory.openSession()) {
// 获取 Mapper
LargeTableMapper mapper = session.getMapper(LargeTableMapper.class);

// 使用游标查询数据
try (Cursor<LargeTableRecord> cursor = mapper.selectAllWithCursor()) {
for (LargeTableRecord record : cursor) {
System.out.printf("ID: %d, Name: %s%n", record.getId(), record.getName());
// 可以在这里对数据进行处理,避免一次性加载到内存
}
}
}

一次性查询大量数据对 MySQL BufferPool 的影响

BufferPool 实现了一个冷热分区的 LRU

Comments