结果集处理:从数据库到内存的最后一公里
说实话,MySQL 的 C API 里,结果集处理这块是我当年踩坑最多的地方。你想想看,SQL 语句发出去,数据库吭哧吭哧查完了,数据怎么拿回来?这就是 mysql_store_result 和 mysql_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跳转) - 可以提前知道总行数
- 释放数据库端的资源很快
缺点也致命:
- 结果集有多大,客户端内存就要吃多少
- 大结果集下,内存占用是线性的
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_rows 和 mysql_affected_rows 返回的东西完全不一样。
| 函数 | 作用 | 适用场景 |
|---|---|---|
mysql_num_rows |
返回结果集中的行数 | 仅对 mysql_store_result 有效 |
mysql_affected_rows |
返回 INSERT/UPDATE/DELETE 影响的行数 | 非查询语句 |
为什么 mysql_num_rows 对 mysql_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_fetch_fields 动态获取字段信息,然后根据 type 决定如何格式化输出。我在做一个数据库导出工具时就是这么干的,省去了硬编码字段类型的麻烦。
知识体系总览
下面这张图把结果集处理的核心逻辑串起来了。我建议你把它记在脑子里,写代码时随时对照。
释放资源:别忘了擦屁股
无论你用哪种方式获取结果集,最后都要调用 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_result 或 mysql_use_result 成功返回的指针,最终都必须由 mysql_free_result 释放。这是铁律,没有例外。
实际项目中的选择策略
说了这么多,到底什么时候用哪个?我总结了一个简单的决策树:
- 结果集小于 10 万行 → 用
mysql_store_result。简单、安全、方便。 - 结果集大于 10 万行,且只需要顺序处理 → 用
mysql_use_result。省内存。 - 需要随机访问或多次遍历 → 只能用
mysql_store_result。 - 内存极度受限的嵌入式环境 → 优先
mysql_use_result,配合分页查询。
我个人习惯是:默认用 mysql_store_result,只有在明确知道结果集很大、且只需要顺序处理时,才切换到 mysql_use_result。因为前者代码更健壮,不容易出幺蛾子。
好了,结果集处理这块就聊到这儿。记住两个核心点:选对加载策略,别忘了释放资源。做到这两条,你在结果集处理上基本就不会翻车了。
公众号:蓝海资料掘金营,微信deep3321