从多结果集到游标集合:DMS 如何迁移 SQL Server 存储过程

2026-08-05 43 预计阅读时间: 1 分钟
来源: cloud.google.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

SQL Server 允许一次存储过程调用连续返回多个表格结果,应用通常只需依次调用 NextResult() 就能读取。PostgreSQL 的执行模型不同:多个数据集需要通过显式游标管理,而且游标只能在所属事务中使用。

这项差异会直接影响大型迁移。面对数百个包含条件分支、嵌套调用和标量返回值的存储过程,逐个手工重写既慢,也容易漏掉某条执行路径。Google Cloud Database Migration Service(DMS)的处理方式,是先分析过程可能产生的结果集数量,再决定将它转换为 PostgreSQL PROCEDURE,还是返回 SETOF refcursorFUNCTION

转换决策不只看 SELECT 数量

DMS 重点判断两个问题:

SQL Server 例程行为 PostgreSQL 目标对象 返回机制
只有一个结果集 PROCEDURE 使用 INOUT refcursor
只有标量返回值 PROCEDURE 使用普通变量或输出参数跟踪
多个结果集 FUNCTION 返回 SETOF refcursor
结果集与标量返回值并存 FUNCTION 数据游标集合,加一个独立的标量游标
结果集数量受循环或条件控制 通常按多结果集处理 显式返回动态生成的游标集合

真正困难的地方在于,结果集不一定直接写在当前过程里。例如,一个患者汇总过程可能先返回基本信息,再调用检验结果过程两次,最后调用就诊记录过程。某个子过程是否执行,还可能取决于上一次调用的标量返回值。

因此,单纯统计当前过程中的 SELECT 并不可靠。DMS 会识别直接结果集、条件或循环中的动态结果集,以及被调用过程产生的间接结果集。

单结果集:用 INOUT refcursor 保留过程语义

对于固定返回一个结果集的子过程,转换结果仍然可以是 PostgreSQL PROCEDURE。下面是一个可直接改造的示例:

CREATE SCHEMA IF NOT EXISTS dbo;

CREATE TABLE IF NOT EXISTS dbo.doctor_visits (
    visit_id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    patient_id integer NOT NULL,
    doctor_name text NOT NULL,
    visit_date timestamp NOT NULL,
    visit_notes text
);

CREATE OR REPLACE PROCEDURE dbo.get_patient_doctor_visits(
    _patient_id integer,
    INOUT result_cursor refcursor
)
LANGUAGE plpgsql
AS $$
BEGIN
    OPEN result_cursor FOR
        SELECT visit_id, patient_id, doctor_name, visit_date, visit_notes
        FROM dbo.doctor_visits
        WHERE patient_id = _patient_id
          AND visit_date >= localtimestamp - interval '6 months'
        ORDER BY visit_date DESC;
END;
$$;

调用时必须显式开启事务,因为 refcursor 指向的 portal 会在事务结束时失效:

BEGIN;
CALL dbo.get_patient_doctor_visits(3, 'doctor_visits_cursor');
FETCH ALL FROM doctor_visits_cursor;
COMMIT;

给游标指定稳定名称,比依赖 <unnamed portal 1> 一类自动名称更适合测试脚本和应用集成。匿名 portal 的编号可能受到调用顺序、驱动实现和过程分支影响,不宜成为长期接口契约。

多结果集:函数返回 SETOF refcursor

当一个例程可能返回多个数据集时,单个 INOUT refcursor 已经不够。DMS 会将其组织为返回 SETOF refcursor 的 PL/pgSQL 函数,每打开一个游标,就通过 RETURN NEXT 把游标引用交给调用方。

下面的最小示例同时返回近期检验结果、异常检验结果和一个标量状态。运行前只需把表结构和字段名替换成项目中的实际对象:

CREATE TABLE IF NOT EXISTS dbo.lab_results (
    result_id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    patient_id integer NOT NULL,
    test_name text NOT NULL,
    test_date timestamp NOT NULL,
    is_exception boolean NOT NULL DEFAULT false
);

CREATE OR REPLACE FUNCTION dbo.get_patient_lab_results(
    _patient_id integer,
    _query_type integer
)
RETURNS SETOF refcursor
LANGUAGE plpgsql
AS $$
DECLARE
    rc refcursor;
    _records_found integer := 0;
BEGIN
    IF _query_type IN (1, 3) THEN
        rc := 'recent_lab_results';
        OPEN rc FOR
            SELECT result_id, patient_id, test_name, test_date, is_exception
            FROM dbo.lab_results
            WHERE patient_id = _patient_id
              AND test_date >= localtimestamp - interval '6 months'
            ORDER BY test_date DESC;
        RETURN NEXT rc;

        IF EXISTS (
            SELECT 1
            FROM dbo.lab_results
            WHERE patient_id = _patient_id
              AND test_date >= localtimestamp - interval '6 months'
        ) THEN
            _records_found := 1;
        END IF;
    END IF;

    IF _query_type IN (2, 3) THEN
        rc := 'exception_lab_results';
        OPEN rc FOR
            SELECT result_id, patient_id, test_name, test_date, is_exception
            FROM dbo.lab_results
            WHERE patient_id = _patient_id
              AND is_exception = true
              AND test_date >= localtimestamp - interval '2 years'
            ORDER BY test_date DESC;
        RETURN NEXT rc;

        IF EXISTS (
            SELECT 1
            FROM dbo.lab_results
            WHERE patient_id = _patient_id
              AND is_exception = true
              AND test_date >= localtimestamp - interval '2 years'
        ) THEN
            _records_found := 1;
        END IF;
    END IF;

    rc := 'return_value';
    OPEN rc FOR SELECT _records_found AS return_value;
    RETURN NEXT rc;
END;
$$;

测试完整返回链时,函数调用和所有 FETCH 必须位于同一个事务:

BEGIN;

SELECT *
FROM dbo.get_patient_lab_results(3, 3);

FETCH ALL FROM recent_lab_results;
FETCH ALL FROM exception_lab_results;
FETCH ALL FROM return_value;

COMMIT;

这里的 return_value 不是 PostgreSQL 函数本身的普通整数返回值,而是一个只包含一行一列的结果集。这样做的目的,是把 SQL Server 的“多个表格结果加标量状态”统一映射到同一种游标序列中。

应用层不能继续照搬 NextResult

迁移后的数据访问层会先收到一组游标名称,然后逐个执行 FETCH ALL。游标和读取操作必须使用同一个数据库连接、同一个事务;连接池如果在两次语句之间归还或更换连接,后续 FETCH 就无法找到对应 portal。

可以这样用 Python 和 psycopg 3 验证函数输出:

import psycopg
from psycopg import sql

DSN = "postgresql://postgres:postgres@localhost:5432/app"

with psycopg.connect(DSN) as conn:
    with conn.transaction():
        with conn.cursor() as cur:
            cur.execute(
                "SELECT * FROM dbo.get_patient_lab_results(%s, %s)",
                (3, 3),
            )
            cursor_names = [row[0] for row in cur.fetchall()]

            results = {}
            for cursor_name in cursor_names:
                # Identifier 会正确引用游标名,避免把名称拼进 SQL。
                cur.execute(
                    sql.SQL("FETCH ALL FROM {}").format(
                        sql.Identifier(cursor_name)
                    )
                )
                columns = [column.name for column in cur.description]
                results[cursor_name] = [
                    dict(zip(columns, row)) for row in cur.fetchall()
                ]

print(results)

安装依赖并运行:

python -m pip install "psycopg[binary]"
python fetch_results.py

生产代码还应定义结果游标的稳定顺序、名称和字段模式。否则,业务分支改变后,调用方只能根据“第几个游标”猜测数据含义,回归测试也很难给出清晰错误。

DMS 如何推导嵌套过程的结果集数量

转换器需要从整个调用网络判断结果集规模,而不是孤立分析单个过程。其分析过程可以概括为四步:

  1. 扫描过程体,记录直接执行并向客户端返回的 SELECT
  2. 识别条件、循环和动态执行中的 SELECTEXEC;如果执行次数无法静态确定,就把结果集数量标记为动态。
  3. 根据过程之间的调用关系建立有向图。
  4. 在调用图上执行深度优先遍历,将子过程的固定数量或动态标记向上传播给调用者。

例如,主过程自身返回一个结果集,子过程固定返回两个结果集,另一个子过程只在条件满足时返回一个结果集,那么主过程不能简单标记为“四个结果集”。更准确的结论是:它具有动态结果集数量,应按多结果集函数生成代码。

调用图分析还能覆盖多层间接调用和递归关系。不过,自动转换保留的是结构和业务分支,不能证明每条分支的运行时语义都完全一致。动态 SQL、临时表、异常处理以及事务控制仍然需要专项验证。

上线前的验证清单

自动转换解决了批量重写问题,但应用协议已经发生变化。落地时至少检查这些事项:

  • 为每条主要条件分支准备测试数据,并核对结果集数量、顺序、列名和类型。
  • 验证“患者不存在”或“查询无记录”等早退路径是否仍返回预期的状态游标。
  • 确保函数调用与所有 FETCH 固定在同一个连接和事务中。
  • 优先使用稳定的显式游标名,不把匿名 portal 编号写死在生产代码中。
  • 检查重复调用和嵌套调用是否可能产生同名游标冲突。
  • 对标量状态游标建立明确契约,例如固定名称为 return_value、固定字段也为 return_value
  • 比较 SQL Server 与 PostgreSQL 在时间区间、布尔值、空值和排序方面的差异。
  • 对 DMS 生成的 PL/pgSQL 执行静态检查和分支覆盖测试,尤其关注变量命名、动态 SQL与递归调用。

DMS 能自动完成 PROCEDUREFUNCTION 的结构选择,并把嵌套结果集映射为确定的游标操作。迁移团队仍需把这次变化当作一次接口协议升级:数据库对象、连接池、数据访问层和 QA 脚本必须一起调整。只有端到端验证游标生命周期和每条执行路径,自动转换的代码才能稳定进入生产环境。


相关推荐