PostgreSQL 中文参考文档
🌐 英文原版 官方 ↗

34.7. 使用描述符区域 #

34.7.1. 命名 SQL 描述符区
34.7.2. SQLDA 描述符区

SQL 描述符区域(descriptor area)是处理 SELECT、FETCH 或 DESCRIBE 语句结果的一种更完善的方法。 一个 SQL 描述符区域把一行数据的数据 与元数据项组合到一个数据结构中。 在执行动态 SQL 语句时,结果列的性质可能事先是未知的, 这时元数据特别有用。PostgreSQL 提供两种使用 描述符区域的方式:命名的 SQL 描述符区域 和 C 结构 SQLDA。

34.7.1. 命名 SQL 描述符区 #

一个命名的 SQL 描述符区域由一个头部(它包含关于整个描述符的信息) 和一个或多个条目描述符区域组成, 每个条目描述符区域基本上描述结果行中的一列。

在使用一个 SQL 描述符区域之前,你需要先分配一个:

EXEC SQL ALLOCATE DESCRIPTOR identifier;

该标识符用作描述符区域的“变量名”。 当你不再需要该描述符时,应当把它释放掉:

EXEC SQL DEALLOCATE DESCRIPTOR identifier;

要使用一个描述符区域,在 INTO 子句中 把它指定为存储目标,而不是罗列宿主变量:

EXEC SQL FETCH NEXT FROM mycursor INTO SQL DESCRIPTOR mydesc;

如果结果集为空,描述符区域仍然会包含来自查询的元数据, 即字段名。

对于尚未执行的预备查询, 可以使用 DESCRIBE 语句来获取结果集的元数据:

EXEC SQL BEGIN DECLARE SECTION;
char *sql_stmt = "SELECT * FROM table1";
EXEC SQL END DECLARE SECTION;

EXEC SQL PREPARE stmt1 FROM :sql_stmt;
EXEC SQL DESCRIBE stmt1 INTO SQL DESCRIPTOR mydesc;

在 PostgreSQL 9.0 之前,SQL 关键字是可选的, 因此使用 DESCRIPTOR 和 SQL DESCRIPTOR 都会生成命名的 SQL 描述符区域。 现在该关键字是必需的,省略 SQL 关键字 将生成 SQLDA 描述符区域,见 Section 34.7.2。

在 DESCRIBE 和 FETCH 语句中, INTO 和 USING 关键字的用法类似: 它们会把结果集和元数据放入一个描述符区域。

现在如何从描述符区域中取出数据呢?你可以把描述符区域 想象成一个具有命名字段的结构。要把头部中一个字段的值 检索出来并存入一个宿主变量,使用以下命令:

EXEC SQL GET DESCRIPTOR name :hostvar = field;

目前只定义了一个头部字段: COUNT,它说明存在多少个条目 描述符区域(即结果中包含多少列)。 该宿主变量需要是整数类型。要从条目描述符区域中 取出一个字段,使用以下命令:

EXEC SQL GET DESCRIPTOR name VALUE num :hostvar = field;

num 可以是一个字面整数, 也可以是一个包含整数的宿主变量。可能的字段有:

CARDINALITY (integer) #

结果集中行的数量

DATA #

实际的数据项(因此,该字段的数据类型取决于查询)

DATETIME_INTERVAL_CODE (integer) #

当 TYPE 为 9 时, DATETIME_INTERVAL_CODE 的取值为: 1 表示 DATE、 2 表示 TIME、 3 表示 TIMESTAMP、 4 表示 TIME WITH TIME ZONE,或 5 表示 TIMESTAMP WITH TIME ZONE。

DATETIME_INTERVAL_PRECISION (integer) #

not implemented

INDICATOR (integer) #

指示符(表示一个空值或值被截断)

KEY_MEMBER (integer) #

not implemented

LENGTH (integer) #

数据的字符长度

NAME (string) #

列的名称

NULLABLE (integer) #

not implemented

OCTET_LENGTH (integer) #

数据的字符表示的字节长度

PRECISION (integer) #

精度(对于 numeric 类型)

RETURNED_LENGTH (integer) #

数据的字符长度

RETURNED_OCTET_LENGTH (integer) #

数据的字符表示的字节长度

SCALE (integer) #

小数位数(对于 numeric 类型)

TYPE (integer) #

该列数据类型的数字编码

在 EXECUTE、DECLARE 和 OPEN 语句中,INTO 和 USING 关键字的效果有所不同。描述符区也可以手工构建,用来为查询或 游标提供输入参数,而 USING SQL DESCRIPTOR name 就是把输入参数传入参数化查询的方式。构建一个命名 SQL 描述符区的语句如下:

EXEC SQL SET DESCRIPTOR name VALUE num field = :hostvar;

PostgreSQL 支持在一条 FETCH 语句中 检索多于一条的记录,并把数据存入宿主变量; 在这种情况下,宿主变量会被假定是一个数组。例如:

EXEC SQL BEGIN DECLARE SECTION;
int id[5];
EXEC SQL END DECLARE SECTION;

EXEC SQL FETCH 5 FROM mycursor INTO SQL DESCRIPTOR mydesc;

EXEC SQL GET DESCRIPTOR mydesc VALUE 1 :id = DATA;

34.7.2. SQLDA 描述符区 #

SQLDA 描述符区域是一个 C 语言结构, 它同样可以用来获取查询的结果集和元数据。 一个结构存储结果集中的一条记录。

EXEC SQL include sqlda.h;
sqlda_t         *mysqlda;

EXEC SQL FETCH 3 FROM mycursor INTO DESCRIPTOR mysqlda;

注意这里省略了 SQL 关键字。 Section 34.7.1 中关于 INTO 和 USING 关键字用法的段落在这里同样适用,另有一点补充。 在 DESCRIBE 语句中, 如果使用了 INTO 关键字, 则 DESCRIPTOR 关键字可以完全省略:

EXEC SQL DESCRIBE prepared_statement INTO mysqlda;

使用 SQLDA 的程序的一般流程如下:

  1. 准备一个查询,并为它声明一个游标。

  2. 为结果行声明一个 SQLDA。

  3. 为输入参数声明一个 SQLDA,并对它进行初始化(内存分配、参数设置)。

  4. 使用输入 SQLDA 打开一个游标。

  5. 从游标取回各行,并把它们存入一个输出 SQLDA。

  6. 把输出 SQLDA 中的值读入主变量(必要时进行转换)。

  7. 关闭游标。

  8. 释放为输入 SQLDA 分配的内存区域。

34.7.2.1. SQLDA 数据结构 #

SQLDA 使用三种数据结构类型:sqlda_t、 sqlvar_t 和 struct sqlname。

Tip

PostgreSQL 的 SQLDA 具有与 IBM DB2 Universal Database 中 类似的数据结构,因此关于 DB2 的 SQLDA 的一些技术信息 可以帮助你更好地理解 PostgreSQL 的 SQLDA。

34.7.2.1.1. sqlda_t Structure #

结构类型 sqlda_t 是实际的 SQLDA 的类型。 它保存一条记录。两个或更多个 sqlda_t 结构 可以通过 desc_next 字段中的指针 以链表的方式连接起来, 从而表示一个行的有序集合。因此, 当抓取到两行或更多行时,应用可以沿着每个 sqlda_t 节点中的 desc_next 指针读取它们。

sqlda_t 的定义是:

struct sqlda_struct
{
    char            sqldaid[8];
    long            sqldabc;
    short           sqln;
    short           sqld;
    struct sqlda_struct *desc_next;
    struct sqlvar_struct sqlvar[1];
};

typedef struct sqlda_struct sqlda_t;

各字段的含义是:

sqldaid #

它包含字面字符串 "SQLDA "。

sqldabc #

它包含已分配空间的大小(以字节为单位)。

sqln #

当它通过 USING 关键字被传入 OPEN、DECLARE 或 EXECUTE 语句时, 它包含一个参数化查询的输入参数的个数。 当它被用作 SELECT、 EXECUTE 或 FETCH 语句的输出时,它的值与 sqld 相同。

sqld #

它包含结果集中字段的数量。

desc_next #

如果查询返回了多于一条记录,则会返回多个链接在一起的 SQLDA 结构,desc_next 中保存着 指向列表中下一个条目的指针。

sqlvar #

这是结果集中各列组成的数组。

34.7.2.1.2. sqlvar_t Structure #

结构类型 sqlvar_t 保存一列的值 以及诸如类型和长度之类的元数据。该类型的定义是:

struct sqlvar_struct
{
    short          sqltype;
    short          sqllen;
    char          *sqldata;
    short         *sqlind;
    struct sqlname sqlname;
};

typedef struct sqlvar_struct sqlvar_t;

各字段的含义是:

sqltype #

包含该字段的类型标识符。取值见 ecpgtype.h 中的 enum ECPGttype。

sqllen #

包含该字段的二进制长度。例如,ECPGt_int 为 4 字节。

sqldata #

指向数据。数据的格式在 Section 34.4.4 中描述。

sqlind #

指向空指示符。0 表示非空,-1 表示空。

sqlname #

字段的名称。

34.7.2.1.3. struct sqlname 结构 #

一个 struct sqlname 结构保存一个列名。 它被用作 sqlvar_t 结构的一个成员。 该结构的定义是:

#define NAMEDATALEN 64

struct sqlname
{
        short           length;
        char            data[NAMEDATALEN];
};

各字段的含义是:

length #

包含该字段名的长度。

data #

包含实际的字段名称。

34.7.2.2. 使用 SQLDA 检索结果集 #

通过 SQLDA 检索查询结果集的一般步骤如下:

  1. 声明一个 sqlda_t 结构来接收结果集。

  2. 执行 FETCH/EXECUTE/DESCRIBE 命令处理查询,并指定已声明的 SQLDA。

  3. 查看 sqlda_t 结构的成员 sqln,检查结果集中记录的数量。

  4. 从 sqlda_t 结构的 sqlvar[0]、sqlvar[1] 等成员取得各列的值。

  5. 沿着 sqlda_t 结构的成员 desc_next 指针转到下一行(下一个 sqlda_t 结构)。

  6. 按需重复上述步骤。

下面是一个通过 SQLDA 检索结果集的示例。

首先,声明一个 sqlda_t 结构来接收结果集。

sqlda_t *sqlda1;

接下来,在一条命令中指定该 SQLDA。下面是一个 FETCH 命令的例子。

EXEC SQL FETCH NEXT FROM cur1 INTO DESCRIPTOR sqlda1;

沿着链表运行一个循环来检索各行。

sqlda_t *cur_sqlda;

for (cur_sqlda = sqlda1;
     cur_sqlda != NULL;
     cur_sqlda = cur_sqlda->desc_next)
{
    ...
}

在该循环内部,再运行另一个循环来检索该行的 每一列的数据(sqlvar_t 结构)。

for (i = 0; i < cur_sqlda->sqld; i++)
{
    sqlvar_t v = cur_sqlda->sqlvar[i];
    char *sqldata = v.sqldata;
    short sqllen  = v.sqllen;
    ...
}

要取得一个列的值,需要检查 sqlvar_t 结构的 成员 sqltype 的值。 然后,根据该列的类型,切换到一种合适的方式, 把数据从 sqlvar 字段 复制到一个宿主变量中。

char var_buf[1024];

switch (v.sqltype)
{
    case ECPGt_char:
        memset(&var_buf, 0, sizeof(var_buf));
        memcpy(&var_buf, sqldata, (sizeof(var_buf) <= sqllen ? sizeof(var_buf) - 1 : sqllen));
        break;

    case ECPGt_int: /* integer */
        memcpy(&intval, sqldata, sqllen);
        snprintf(var_buf, sizeof(var_buf), "%d", intval);
        break;

    ...
}

34.7.2.3. 使用 SQLDA 传递查询参数 #

使用 SQLDA 向预备查询传递输入参数的一般步骤是:

  1. 创建一个预备好的查询(预备语句)

  2. 把一个 sqlda_t 结构声明为输入 SQLDA。

  3. 为输入 SQLDA 分配内存区域(作为一个 sqlda_t 结构)。

  4. 在已分配的内存中设置(复制)输入值。

  5. 通过指定输入 SQLDA 打开一个游标。

下面是一个示例。

首先,创建一个已准备语句。

EXEC SQL BEGIN DECLARE SECTION;
char query[1024] = "SELECT d.oid, * FROM pg_database d, pg_stat_database s WHERE d.oid = s.datid AND (d.datname = ? OR d.oid = ?)";
EXEC SQL END DECLARE SECTION;

EXEC SQL PREPARE stmt1 FROM :query;

接下来,为 SQLDA 分配内存, 并在 sqlda_t 结构的成员变量 sqln 中设置输入参数的个数。 当预备查询需要两个或更多输入参数时, 应用必须分配额外的内存空间, 其大小按(参数个数 - 1)* sizeof(sqlvar_t) 计算。 这里展示的例子为两个输入参数分配了内存空间。

sqlda_t *sqlda2;

sqlda2 = (sqlda_t *) malloc(sizeof(sqlda_t) + sizeof(sqlvar_t));
memset(sqlda2, 0, sizeof(sqlda_t) + sizeof(sqlvar_t));

sqlda2->sqln = 2; /* number of input variables */

内存分配完成后,把参数值存入 sqlvar[] 数组 (当 SQLDA 接收结果集时,使用的也是同一个数组)。 在这个例子中,输入参数是字符串类型的 "postgres" 和整数类型的 1。

sqlda2->sqlvar[0].sqltype = ECPGt_char;
sqlda2->sqlvar[0].sqldata = "postgres";
sqlda2->sqlvar[0].sqllen  = 8;

int intval = 1;
sqlda2->sqlvar[1].sqltype = ECPGt_int;
sqlda2->sqlvar[1].sqldata = (char *) &intval;
sqlda2->sqlvar[1].sqllen  = sizeof(intval);

通过打开一个游标并指定事先设置好的 SQLDA, 输入参数就会被传递给预备语句。

EXEC SQL OPEN cur1 USING DESCRIPTOR sqlda2;

最后,与用于接收查询结果的 SQLDA 不同, 输入 SQLDA 使用完之后必须显式释放已分配的内存空间。

free(sqlda2);

34.7.2.4. 使用 SQLDA 的示例应用 #

下面是一个示例程序,它说明了如何从系统目录中 获取由输入参数指定的数据库的访问统计信息。

这个应用以数据库 OID 为连接条件连接两个系统表 pg_database 和 pg_stat_database, 并抓取和显示由两个输入参数 (数据库 postgres 和 OID 1) 检索到的数据库统计信息。

首先,为输入声明一个 SQLDA,为输出声明一个 SQLDA。

EXEC SQL include sqlda.h;

sqlda_t *sqlda1; /* an output descriptor */
sqlda_t *sqlda2; /* an input descriptor  */

接下来,连接到数据库,准备一条语句,并为该已准备语句声明一个 游标。

int
main(void)
{
    EXEC SQL BEGIN DECLARE SECTION;
    char query[1024] = "SELECT d.oid,* FROM pg_database d, pg_stat_database s WHERE d.oid=s.datid AND ( d.datname=? OR d.oid=? )";
    EXEC SQL END DECLARE SECTION;

    EXEC SQL CONNECT TO testdb AS con1 USER testuser;
    EXEC SQL SELECT pg_catalog.set_config('search_path', '', false); EXEC SQL COMMIT;

    EXEC SQL PREPARE stmt1 FROM :query;
    EXEC SQL DECLARE cur1 CURSOR FOR stmt1;

接下来,把一些值放入输入 SQLDA 中作为输入参数。 为输入 SQLDA 分配内存,并把输入参数的个数设置到 sqln 中。把类型、值和值的长度 分别存入 sqlvar 结构中的 sqltype、sqldata 和 sqllen。

    /* Create SQLDA structure for input parameters. */
    sqlda2 = (sqlda_t *) malloc(sizeof(sqlda_t) + sizeof(sqlvar_t));
    memset(sqlda2, 0, sizeof(sqlda_t) + sizeof(sqlvar_t));
    sqlda2->sqln = 2; /* number of input variables */

    sqlda2->sqlvar[0].sqltype = ECPGt_char;
    sqlda2->sqlvar[0].sqldata = "postgres";
    sqlda2->sqlvar[0].sqllen  = 8;

    intval = 1;
    sqlda2->sqlvar[1].sqltype = ECPGt_int;
    sqlda2->sqlvar[1].sqldata = (char *)&intval;
    sqlda2->sqlvar[1].sqllen  = sizeof(intval);

设置好输入 SQLDA 之后,使用该输入 SQLDA 打开一个游标。

    /* Open a cursor with input parameters. */
    EXEC SQL OPEN cur1 USING DESCRIPTOR sqlda2;

从打开的游标把行抓取到输出 SQLDA 中。 (一般来说,你必须在循环中反复调用 FETCH, 才能抓取到结果集中的所有行。)

    while (1)
    {
        sqlda_t *cur_sqlda;

        /* Assign descriptor to the cursor  */
        EXEC SQL FETCH NEXT FROM cur1 INTO DESCRIPTOR sqlda1;

接下来,沿着 sqlda_t 结构的链表, 从 SQLDA 中检索被抓取的记录。

    for (cur_sqlda = sqlda1 ;
         cur_sqlda != NULL ;
         cur_sqlda = cur_sqlda->desc_next)
    {
        ...

读取第一条记录中的每一列。列数存储在 sqld 中, 第一列的实际数据存储在 sqlvar[0] 中, 这两者都是 sqlda_t 结构的成员。

        /* Print every column in a row. */
        for (i = 0; i < sqlda1->sqld; i++)
        {
            sqlvar_t v = sqlda1->sqlvar[i];
            char *sqldata = v.sqldata;
            short sqllen  = v.sqllen;

            strncpy(name_buf, v.sqlname.data, v.sqlname.length);
            name_buf[v.sqlname.length] = '\0';

现在,列数据保存在变量 v 中。 查看 v.sqltype 以了解该列的类型, 然后把每个数据复制到宿主变量中。

            switch (v.sqltype) {
                int intval;
                double doubleval;
                unsigned long long int longlongval;

                case ECPGt_char:
                    memset(&var_buf, 0, sizeof(var_buf));
                    memcpy(&var_buf, sqldata, (sizeof(var_buf) <= sqllen ? sizeof(var_buf)-1 : sqllen));
                    break;

                case ECPGt_int: /* integer */
                    memcpy(&intval, sqldata, sqllen);
                    snprintf(var_buf, sizeof(var_buf), "%d", intval);
                    break;

                ...

                default:
                    ...
            }

            printf("%s = %s (type: %d)
", name_buf, var_buf, v.sqltype);
        }

在处理完所有记录之后关闭游标,并断开与数据库的连接。

    EXEC SQL CLOSE cur1;
    EXEC SQL COMMIT;

    EXEC SQL DISCONNECT ALL;

完整的程序显示在 Example 34.1 中。

Example 34.1. SQLDA 示例程序

#include <stdlib.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

EXEC SQL include sqlda.h;

sqlda_t *sqlda1; /* descriptor for output */
sqlda_t *sqlda2; /* descriptor for input */

EXEC SQL WHENEVER NOT FOUND DO BREAK;
EXEC SQL WHENEVER SQLERROR STOP;

int
main(void)
{
    EXEC SQL BEGIN DECLARE SECTION;
    char query[1024] = "SELECT d.oid,* FROM pg_database d, pg_stat_database s WHERE d.oid=s.datid AND ( d.datname=? OR d.oid=? )";

    int intval;
    unsigned long long int longlongval;
    EXEC SQL END DECLARE SECTION;

    EXEC SQL CONNECT TO uptimedb AS con1 USER uptime;
    EXEC SQL SELECT pg_catalog.set_config('search_path', '', false); EXEC SQL COMMIT;

    EXEC SQL PREPARE stmt1 FROM :query;
    EXEC SQL DECLARE cur1 CURSOR FOR stmt1;

    /* Create an SQLDA structure for an input parameter */
    sqlda2 = (sqlda_t *)malloc(sizeof(sqlda_t) + sizeof(sqlvar_t));
    memset(sqlda2, 0, sizeof(sqlda_t) + sizeof(sqlvar_t));
    sqlda2->sqln = 2; /* a number of input variables */

    sqlda2->sqlvar[0].sqltype = ECPGt_char;
    sqlda2->sqlvar[0].sqldata = "postgres";
    sqlda2->sqlvar[0].sqllen  = 8;

    intval = 1;
    sqlda2->sqlvar[1].sqltype = ECPGt_int;
    sqlda2->sqlvar[1].sqldata = (char *) &intval;
    sqlda2->sqlvar[1].sqllen  = sizeof(intval);

    /* Open a cursor with input parameters. */
    EXEC SQL OPEN cur1 USING DESCRIPTOR sqlda2;

    while (1)
    {
        sqlda_t *cur_sqlda;

        /* Assign descriptor to the cursor  */
        EXEC SQL FETCH NEXT FROM cur1 INTO DESCRIPTOR sqlda1;

        for (cur_sqlda = sqlda1 ;
             cur_sqlda != NULL ;
             cur_sqlda = cur_sqlda->desc_next)
        {
            int i;
            char name_buf[1024];
            char var_buf[1024];

            /* Print every column in a row. */
            for (i=0 ; i<cur_sqlda->sqld ; i++)
            {
                sqlvar_t v = cur_sqlda->sqlvar[i];
                char *sqldata = v.sqldata;
                short sqllen  = v.sqllen;

                strncpy(name_buf, v.sqlname.data, v.sqlname.length);
                name_buf[v.sqlname.length] = '\0';

                switch (v.sqltype)
                {
                    case ECPGt_char:
                        memset(&var_buf, 0, sizeof(var_buf));
                        memcpy(&var_buf, sqldata, (sizeof(var_buf)<=sqllen ? sizeof(var_buf)-1 : sqllen) );
                        break;

                    case ECPGt_int: /* integer */
                        memcpy(&intval, sqldata, sqllen);
                        snprintf(var_buf, sizeof(var_buf), "%d", intval);
                        break;

                    case ECPGt_long_long: /* bigint */
                        memcpy(&longlongval, sqldata, sqllen);
                        snprintf(var_buf, sizeof(var_buf), "%lld", longlongval);
                        break;

                    default:
                    {
                        int i;
                        memset(var_buf, 0, sizeof(var_buf));
                        for (i = 0; i < sqllen; i++)
                        {
                            char tmpbuf[16];
                            snprintf(tmpbuf, sizeof(tmpbuf), "%02x ", (unsigned char) sqldata[i]);
                            strncat(var_buf, tmpbuf, sizeof(var_buf));
                        }
                    }
                        break;
                }

                printf("%s = %s (type: %d)\n", name_buf, var_buf, v.sqltype);
            }

            printf("\n");
        }
    }

    EXEC SQL CLOSE cur1;
    EXEC SQL COMMIT;

    EXEC SQL DISCONNECT ALL;

    return 0;
}

这个例子的输出看起来应该类似于下面这样 (某些数字会有所不同)。

oid = 1 (type: 1)
datname = template1 (type: 1)
datdba = 10 (type: 1)
encoding = 0 (type: 5)
datistemplate = t (type: 1)
datallowconn = t (type: 1)
dathasloginevt = f (type: 1)
datconnlimit = -1 (type: 5)
datfrozenxid = 379 (type: 1)
dattablespace = 1663 (type: 1)
datconfig =  (type: 1)
datacl = {=c/uptime,uptime=CTc/uptime} (type: 1)
datid = 1 (type: 1)
datname = template1 (type: 1)
numbackends = 0 (type: 5)
xact_commit = 113606 (type: 9)
xact_rollback = 0 (type: 9)
blks_read = 130 (type: 9)
blks_hit = 7341714 (type: 9)
tup_returned = 38262679 (type: 9)
tup_fetched = 1836281 (type: 9)
tup_inserted = 0 (type: 9)
tup_updated = 0 (type: 9)
tup_deleted = 0 (type: 9)

oid = 11511 (type: 1)
datname = postgres (type: 1)
datdba = 10 (type: 1)
encoding = 0 (type: 5)
datistemplate = f (type: 1)
datallowconn = t (type: 1)
dathasloginevt = f (type: 1)
datconnlimit = -1 (type: 5)
datfrozenxid = 379 (type: 1)
dattablespace = 1663 (type: 1)
datconfig =  (type: 1)
datacl =  (type: 1)
datid = 11511 (type: 1)
datname = postgres (type: 1)
numbackends = 0 (type: 5)
xact_commit = 221069 (type: 9)
xact_rollback = 18 (type: 9)
blks_read = 1176 (type: 9)
blks_hit = 13943750 (type: 9)
tup_returned = 77410091 (type: 9)
tup_fetched = 3253694 (type: 9)
tup_inserted = 0 (type: 9)
tup_updated = 0 (type: 9)
tup_deleted = 0 (type: 9)