第33章:C语言与PostgreSQL基础:libpq库

第33章:C语言与PostgreSQL基础:libpq库

说实话,很多C程序员一提到数据库就发怵。总觉得那是Java、Python的天下。但我在嵌入式领域摸爬滚打这么多年,可以负责任地告诉你:C语言操作PostgreSQL,不仅可行,而且非常高效。libpq就是PostgreSQL官方提供的C语言接口,轻量、稳定、功能完整。今天我就带你把它吃透。

libpq 核心流程 ① PQconnectdb — 建立连接 ② PQexec — 执行SQL ③ PQgetvalue — 获取结果 ④ PQfinish — 清理资源 libpq 是 PostgreSQL 原生C接口, 同步/异步都支持。 每个步骤都要 检查返回值, 否则容易翻车。

1. libpq 是什么?为什么选它?

libpq 是 PostgreSQL 自带的 C 语言客户端库。说白了,它就是一座桥,让你的 C 程序能和 PostgreSQL 服务器对话。我最早接触它是在一个工业数据采集项目里,设备端只有 C 环境,还要实时写入传感器数据到数据库。当时试过 ODBC,太重了。换成 libpq,代码量直接砍半。

它的优点很突出:

  • 原生支持:PostgreSQL 官方维护,版本同步更新。
  • 轻量级:没有多余依赖,编译出来就几百 KB。
  • 功能完整:连接管理、SQL 执行、事务、异步查询,全都有。
  • 线程安全:每个连接对象独立,多线程场景下很友好。
编译时记得链接 -lpq,头文件是 #include <libpq-fe.h>。如果你用 CMake,可以加 find_package(PostgreSQL),省心很多。

2. PQconnectdb — 建立连接

连接数据库是第一步。函数原型很简单:

PGconn *PQconnectdb(const char *conninfo);

conninfo 是一个字符串,里面用 key=value 的形式传参数。我最常用的写法是:

PGconn *conn = PQconnectdb("host=127.0.0.1 port=5432 dbname=testdb user=myuser password=mypass");

注意,这个函数不会阻塞太久。它内部会尝试连接,但超时机制由 PostgreSQL 服务端控制。我个人习惯在连接后立刻检查状态:

if (PQstatus(conn) != CONNECTION_OK) {
    fprintf(stderr, "连接失败: %s", PQerrorMessage(conn));
    PQfinish(conn);
    return -1;
}

我曾经在一个项目中忘了检查连接状态,结果后续的 PQexec 全部返回空指针,排查了半天才发现是连接串里密码写错了。嗯,从那以后我每次连接完都会加一句检查,成了肌肉记忆。

conninfo 中不要硬编码敏感信息。生产环境建议从配置文件或环境变量读取。另外,如果连接失败,PQerrorMessage 会给出详细原因,一定要打出来看看。

3. PQexec — 执行SQL

连接建立后,就可以用 PQexec 执行 SQL 了。它的签名是:

PGresult *PQexec(PGconn *conn, const char *command);

command 可以是任何 SQL 语句:SELECT、INSERT、UPDATE、DELETE,甚至 CREATE TABLE。返回的 PGresult 指针包含了执行结果的所有信息。

举个例子,插入一条数据:

PGresult *res = PQexec(conn, "INSERT INTO sensors (id, value) VALUES (1, 23.5)");
if (PQresultStatus(res) != PGRES_COMMAND_OK) {
    fprintf(stderr, "INSERT 失败: %s", PQerrorMessage(conn));
    PQclear(res);
    // 处理错误
} else {
    printf("插入成功,影响行数: %s", PQcmdTuples(res));
}
PQclear(res);

你想想看,这里有个很容易踩的坑:PQexec 执行后,必须调用 PQclear 释放结果对象。否则内存会一直涨。我在做长连接服务时遇到过,跑了一周后内存占用飙升到几个 GB,最后定位到就是忘了 PQclear。血的教训。

对于 SELECT 查询,PQresultStatus 返回的是 PGRES_TUPLES_OK,而不是 PGRES_COMMAND_OK。这两个要区分开。

4. PQgetvalue — 获取查询结果

执行 SELECT 之后,怎么把数据取出来?PQgetvalue 就是干这个的。

char *PQgetvalue(const PGresult *res, int row, int col);

它返回指定行、指定列的文本值。注意,返回的是 char*,所有数据都以字符串形式给出。你需要自己转换成 int、float 等类型。

一个完整的查询示例:

PGresult *res = PQexec(conn, "SELECT id, name, temperature FROM sensors ORDER BY id");
if (PQresultStatus(res) != PGRES_TUPLES_OK) {
    fprintf(stderr, "查询失败: %s", PQerrorMessage(conn));
    PQclear(res);
    return;
}
int rows = PQntuples(res);
int cols = PQnfields(res);
for (int i = 0; i < rows; i++) {
    for (int j = 0; j < cols; j++) {
        printf("%s\t", PQgetvalue(res, i, j));
    }
    printf("\n");
}
PQclear(res);

这里有几个辅助函数很实用:

函数作用
PQntuples(res)返回结果集的行数
PQnfields(res)返回结果集的列数
PQfname(res, col)返回指定列的字段名
PQgetisnull(res, row, col)判断某个字段是否为 NULL

我个人习惯在遍历结果前,先用 PQnfieldsPQfname 打印一个表头,这样输出更清晰。另外,如果某个字段可能为 NULL,一定要用 PQgetisnull 判断,否则 PQgetvalue 会返回空字符串,容易混淆。

PQgetvalue 返回的字符串是内部缓冲区的指针,不要手动 free。它的生命周期到 PQclear 为止。如果你需要长期保存,记得用 strdup 复制一份。

5. 完整示例:从连接到查询

下面我把上面几个步骤串起来,写一个完整的 demo。这个程序连接本地数据库,查询 sensors 表并打印结果。

#include <stdio.h>
#include <libpq-fe.h>

int main() {
    // 1. 连接数据库
    PGconn *conn = PQconnectdb("host=127.0.0.1 port=5432 dbname=testdb user=myuser password=mypass");
    if (PQstatus(conn) != CONNECTION_OK) {
        fprintf(stderr, "连接失败: %s", PQerrorMessage(conn));
        PQfinish(conn);
        return 1;
    }
    printf("连接成功!\n");

    // 2. 执行查询
    PGresult *res = PQexec(conn, "SELECT id, name, value FROM sensors ORDER BY id");
    if (PQresultStatus(res) != PGRES_TUPLES_OK) {
        fprintf(stderr, "查询失败: %s", PQerrorMessage(conn));
        PQclear(res);
        PQfinish(conn);
        return 1;
    }

    // 3. 获取并打印结果
    int rows = PQntuples(res);
    int cols = PQnfields(res);
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%s", PQgetvalue(res, i, j));
            if (j < cols - 1) printf(", ");
        }
        printf("\n");
    }

    // 4. 清理
    PQclear(res);
    PQfinish(conn);
    return 0;
}

编译命令:

gcc -o pg_demo pg_demo.c -lpq

运行前确保 PostgreSQL 服务已启动,并且 testdb 数据库和 sensors 表已存在。如果一切顺利,你会看到类似这样的输出:

连接成功!
1, 温度传感器, 23.5
2, 湿度传感器, 68.2

6. 避坑指南与个人经验

最后,我把自己这些年用 libpq 踩过的坑总结一下,希望能帮你少走弯路。

  • 连接字符串格式:key=value 之间用空格分隔,不要加多余引号。我曾经因为写了 password="mypass" 导致连接失败,去掉引号就好了。
  • 错误处理不能偷懒:每个 PQexec 之后都要检查 PQresultStatus。不要假设 SQL 一定会成功。
  • 内存泄漏PGresult 用完后必须 PQclearPGconn 用完后必须 PQfinish。这是铁律。
  • NULL 处理:查询结果中如果某个字段是 NULL,PQgetvalue 返回空字符串 "",但 PQgetisnull 会返回 1。一定要用后者判断。
  • 多线程注意:每个线程使用独立的 PGconn 对象,不要共享连接。libpq 的库函数本身是线程安全的,但连接对象不是。
千万不要在生产代码中直接用 PQexec 拼接用户输入的 SQL 字符串。这会导致 SQL 注入攻击。请使用 PQexecParams 配合参数化查询,或者用 PQescapeStringConn 转义。安全无小事。

好了,关于 libpq 的基础用法就讲到这里。这三个函数——PQconnectdbPQexecPQgetvalue——是你和 PostgreSQL 打交道的三板斧。把它们练熟,大部分日常需求都能搞定。下次遇到更复杂的场景,比如异步查询、批量插入、二进制传输,我们再慢慢聊。

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