结果集处理:从数据库到内存的最后一公里

说实话,MySQL 的 C API 里,结果集处理这块是我当年踩坑最多的地方。你想想看,SQL 语句发出去,数据库吭哧吭哧查完了,数据怎么拿回来?这就是 mysql_store_resultmysql_use_result 要干的事。

这两个函数,说白了就是两种不同的「搬运策略」。一个是一次性全搬回来,一个是边用边搬。听起来简单,但选错了,轻则内存爆炸,重则查询卡死。我当年在一个嵌入式设备上就吃过这个亏——设备内存只有 64MB,我用 mysql_store_result 去拉一张百万行的表,结果程序直接 OOM 被内核杀了。

mysql_store_result:一次性全量加载

这个函数会把查询结果全部拉到客户端内存里。调用完,你就拥有了一个完整的结果集副本。

MYSQL_RES *result = mysql_store_result(mysql);
if (result == NULL) {
    fprintf(stderr, "mysql_store_result failed: %s\n", mysql_error(mysql));
    return -1;
}

// 现在可以随意访问结果集了
my_ulonglong num_rows = mysql_num_rows(result);
printf("查询到 %lu 行数据\n", (unsigned long)num_rows);

优点很明显

  • 可以随机访问行数据(用 mysql_data_seek 跳转)
  • 可以提前知道总行数
  • 释放数据库端的资源很快

缺点也致命

  • 结果集有多大,客户端内存就要吃多少
  • 大结果集下,内存占用是线性的
我的习惯:如果结果集行数确定小于 1 万行,或者你需要随机访问,就用 mysql_store_result。否则,考虑另一种方案。

mysql_use_result:流式逐行读取

这个函数恰恰相反——它不把数据拉到客户端,而是让数据库端「留着」,你一行一行去取。

MYSQL_RES *result = mysql_use_result(mysql);
if (result == NULL) {
    fprintf(stderr, "mysql_use_result failed: %s\n", mysql_error(mysql));
    return -1;
}

MYSQL_ROW row;
while ((row = mysql_fetch_row(result)) != NULL) {
    // 处理当前行
    printf("id=%s, name=%s\n", row[0], row[1]);
}

// 注意:必须取完所有行,或者调用 mysql_free_result 提前终止
mysql_free_result(result);

优点

  • 客户端内存占用极小,只缓存当前行
  • 适合超大结果集(比如导出几百万行数据)

缺点

  • 不能随机访问,只能顺序读取
  • 读取期间,数据库端的表可能被锁住(取决于存储引擎)
  • 必须取完所有行,否则会影响后续查询
我曾经踩过的坑:用 mysql_use_result 时,如果中途 break 退出循环,没有调用 mysql_free_result,那么后续的所有查询都会卡住。因为 MySQL 服务器还在等客户端把结果取完。嗯,那次排查了我整整一个下午。

行数获取:两个函数,两种含义

这里有个容易混淆的点。mysql_num_rowsmysql_affected_rows 返回的东西完全不一样。

函数 作用 适用场景
mysql_num_rows 返回结果集中的行数 仅对 mysql_store_result 有效
mysql_affected_rows 返回 INSERT/UPDATE/DELETE 影响的行数 非查询语句

为什么 mysql_num_rowsmysql_use_result 无效?因为数据还没全部取回来,客户端根本不知道总共有多少行。你想想看,流式读取怎么可能提前知道总行数?

// 正确用法:store_result 后获取行数
MYSQL_RES *res = mysql_store_result(mysql);
my_ulonglong rows = mysql_num_rows(res);
printf("总行数: %lu\n", (unsigned long)rows);

// 错误用法:use_result 后调用 mysql_num_rows
MYSQL_RES *res2 = mysql_use_result(mysql);
my_ulonglong rows2 = mysql_num_rows(res2);  // 永远返回 0!

字段信息:你得知道每一列是什么

光拿到行数据还不够,你还得知道每一列的名字、类型、长度。这就是 mysql_fetch_fields 的活。

MYSQL_RES *result = mysql_store_result(mysql);
unsigned int num_fields = mysql_num_fields(result);
MYSQL_FIELD *fields = mysql_fetch_fields(result);

for (unsigned int i = 0; i < num_fields; i++) {
    printf("字段 %d: 名称=%s, 类型=%d, 最大长度=%lu\n",
           i, fields[i].name, fields[i].type, fields[i].length);
}

MYSQL_FIELD 结构体里常用的成员:

  • name:字段名
  • type:字段类型(MYSQL_TYPE_LONG、MYSQL_TYPE_STRING 等枚举值)
  • length:字段最大长度
  • flags:标志位(比如是否允许 NULL、是否为主键)
  • table:所属表名(多表 JOIN 时很有用)
一个小技巧:如果你要写一个通用的查询工具(比如类似 mysql 命令行那种),可以用 mysql_fetch_fields 动态获取字段信息,然后根据 type 决定如何格式化输出。我在做一个数据库导出工具时就是这么干的,省去了硬编码字段类型的麻烦。

知识体系总览

下面这张图把结果集处理的核心逻辑串起来了。我建议你把它记在脑子里,写代码时随时对照。

MySQL 结果集处理核心流程 mysql_query() 选择? mysql_store_result() mysql_use_result() mysql_num_rows() 可用 mysql_num_rows() 返回 0 mysql_fetch_fields() 获取字段信息

释放资源:别忘了擦屁股

无论你用哪种方式获取结果集,最后都要调用 mysql_free_result 释放内存。这个函数会释放 MYSQL_RES 结构体以及它内部的所有数据。

MYSQL_RES *result = mysql_store_result(mysql);
if (result) {
    // ... 处理数据 ...
    mysql_free_result(result);  // 必须调用
}

我见过不少新手,程序跑着跑着内存越用越多,最后发现是忘了释放结果集。嗯,这个坑我也踩过——有一次线上服务跑了三天,内存从 200MB 涨到了 2GB,排查下来就是某个查询路径忘了 mysql_free_result

核心原则:每个 mysql_store_resultmysql_use_result 成功返回的指针,最终都必须由 mysql_free_result 释放。这是铁律,没有例外。

实际项目中的选择策略

说了这么多,到底什么时候用哪个?我总结了一个简单的决策树:

  1. 结果集小于 10 万行 → 用 mysql_store_result。简单、安全、方便。
  2. 结果集大于 10 万行,且只需要顺序处理 → 用 mysql_use_result。省内存。
  3. 需要随机访问或多次遍历 → 只能用 mysql_store_result
  4. 内存极度受限的嵌入式环境 → 优先 mysql_use_result,配合分页查询。

我个人习惯是:默认用 mysql_store_result,只有在明确知道结果集很大、且只需要顺序处理时,才切换到 mysql_use_result。因为前者代码更健壮,不容易出幺蛾子。

好了,结果集处理这块就聊到这儿。记住两个核心点:选对加载策略,别忘了释放资源。做到这两条,你在结果集处理上基本就不会翻车了。


公众号:蓝海资料掘金营,微信deep3321