使用 PL/pgSQL 在 PostgreSQL 中将多个字段作为记录返回

Posted

技术标签:

【中文标题】使用 PL/pgSQL 在 PostgreSQL 中将多个字段作为记录返回【英文标题】:Return multiple fields as a record in PostgreSQL with PL/pgSQL 【发布时间】:2011-05-31 15:58:55 【问题描述】:

我正在使用 PL/pgSQL 编写一个 SP。 我想返回一条记录,该记录由几个不同表中的字段组成。可能看起来像这样:

CREATE OR REPLACE FUNCTION get_object_fields(name text)
  RETURNS RECORD AS $$
BEGIN
  -- fetch fields f1, f2 and f3 from table t1
  -- fetch fields f4, f5 from table t2
  -- fetch fields f6, f7 and f8 from table t3
  -- return fields f1 ... f8 as a record
END
$$ language plpgsql; 

如何将不同表中的字段作为单个记录中的字段返回?

[编辑]

我意识到我上面给出的例子有点过于简单了。我需要检索的一些字段将保存为正在查询的数据库表中的单独行,但我想以“扁平化”记录结构返回它们。

下面的代码应该有助于进一步说明:

CREATE TABLE user (id int, school_id int, name varchar(32));

CREATE TYPE my_type AS (
  user1_id   int,
  user1_name varchar(32),
  user2_id   int,
  user2_name varchar(32)
);

CREATE OR REPLACE FUNCTION get_two_users_from_school(schoolid int)
  RETURNS my_type AS $$
DECLARE
  result my_type;
  temp_result user;
BEGIN
  -- for purpose of this question assume 2 rows returned
  SELECT id, name INTO temp_result FROM user where school_id = schoolid LIMIT 2;
  -- Will the (pseudo)code below work?:
  result.user1_id := temp_result[0].id ;
  result.user1_name := temp_result[0].name ;
  result.user2_id := temp_result[1].id ;
  result.user2_name := temp_result[1].name ;
  return result ;
END
$$ language plpgsql

【问题讨论】:

确实调查了返回多条记录,例如returns setof my_type @nate:它不是返回集合的函数。我需要在 SP 内获取记录,然后从函数返回的数据中从检索集中的单个记录中检索字段 - 听起来比实际复杂 - 请参阅上面的代码 【参考方案1】:
CREATE TABLE users(user_id int, school_id int, name text);
insert into users values (1, 10,'alice')
,(5, 10,'boy')
,(13, 10,'cassey')
,(17, 10,'delores')
,(4, 11,'elaine');

我将 user_id 设置为任意 int。函数输入参数为school_id。因此,如果school_id 为 10,您希望得到以下结果:

 user_id | name  | user_id | name
---------+-------+---------+------
       1 | alice |       5 | boy

所以你的查询应该是这样的:

with a as (
select u1.user_id,
     u1.name from  users u1 
        where school_id = 10 order by user_id limit 1),
b as      
(select u2.user_id,u2.name from users u2 
        where school_id = 10  order by user_id limit 1 offset 1 )
select * from a  cross JOIN b ;

所以让我们将查询包装到 plpgsql 函数中。

CREATE OR REPLACE FUNCTION 
    get_object_fields2(_school_id int)
  RETURNS TABLE (user1_id   int
               , user1_name text
               , user2_id   int
               , user2_name text) 
               LANGUAGE plpgsql AS 
$func$
DECLARE countu integer;
BEGIN
    countu := (
        select count(*) from users where school_id = _school_id);
    IF countu >= 2 THEN
        RETURN QUERY
            with a as (
            select u1.user_id,
                u1.name from  users u1 
                where school_id = _school_id 
                    order by user_id limit 1),
            b as(
                select u2.user_id,u2.name from users u2 
                where school_id = _school_id 
                    order by user_id limit 1 offset 1 )
            select * from a  cross JOIN b;
    elseif countu = 1 then
    return query
      select u1.user_id, u1.name,u1.user_id, u1.name
        from  users u1 where school_id = _school_id; 
    else 
        RAISE EXCEPTION 'not found';
    end if;
END
$func$;

【讨论】:

【参考方案2】:

返回单行

OUT parameters 更简单:

CREATE OR REPLACE FUNCTION get_object_fields(_school_id int
                                       , OUT user1_id   int
                                       , OUT user1_name varchar(32)
                                       , OUT user2_id   int
                                       , OUT user2_name varchar(32)) AS 
$func$
BEGIN
   SELECT INTO user1_id, user1_name
          u.id, u.name
   FROM   users u
   WHERE  u.school_id = _school_id
   LIMIT  1;  -- make sure query returns 1 row - better in a more deterministic way?

   user2_id := user1_id + 1; -- some calculation

   SELECT INTO user2_name
          u.name       
   FROM   users u
   WHERE  u.id = user2_id;
END
$func$  LANGUAGE plpgsql;

呼叫:

SELECT * FROM get_object_fields(1);

您不需要仅仅为了这个 plpgsql 函数而创建一个类型。如果您想将多个函数绑定到同一个复合类型,它可能很有用。否则,OUT 参数可以完成这项工作。

没有RETURN 语句。 OUT 参数自动返回,这种形式返回单行。 RETURN 是可选的。

由于OUT 参数在函数体内随处可见(并且可以像任何其他变量一样使用),请确保对同名的列进行表限定以避免命名冲突! (更好的是,使用不同的名称开头。)

更简单 - 也返回 0-n 行

通常,如果可以组合函数体中的查询,这会更简单、更快。您可以使用RETURNS TABLE()(自 Postgres 8.4 起,早在提出问题之前)返回 0-n 行。

上面的例子可以写成:

CREATE OR REPLACE FUNCTION get_object_fields2(_school_id int)
  RETURNS TABLE (user1_id   int
               , user1_name varchar(32)
               , user2_id   int
               , user2_name varchar(32)) AS 
$func$
BEGIN
   RETURN QUERY
   SELECT u1.id, u1.name, u2.id, u2.name
   FROM   users u1
   JOIN   users u2 ON u2.id = u1.id + 1
   WHERE  u1.school_id = _school_id
   LIMIT  1;  -- may be optional
END
$func$  LANGUAGE plpgsql;

呼叫:

SELECT * FROM get_object_fields2(1);

RETURNS TABLE 实际上与将一堆OUT 参数与RETURNS SETOF record 结合使用相同,只是更短。

主要区别:此函数可以返回 0、1 或多行,而第一个版本总是返回 1 行。 添加 LIMIT 1 就像演示的那样只允许 0 或 1 行。

RETURN QUERY 是直接从查询返回结果的简单方法。 您可以在单个函数中使用多个实例来向输出添加更多行。

dbfiddle here(同时演示)

不同的行类型

如果您的函数应该根据输入动态返回具有不同行类型的结果,请在此处阅读更多信息:

Refactor a PL/pgSQL function to return the output of various SELECT queries

【讨论】:

我在使用 Hasura 尝试此操作时收到 the function does not return a "COMPOSITE" type • the function does not return a SETOF table @marcellothearcane:这些功能按照宣传的方式工作。查看添加的fiddle。 谢谢,我认为这是 Hasura 的问题 - 我正在返回 SETOF <table>,这似乎有效!【参考方案3】:

您可以通过简单地使用返回查询作为返回记录集来实现这一点。

CREATE OR REPLACE FUNCTION schemaName.get_two_users_from_school(schoolid bigint)
 RETURNS SETOF record
 LANGUAGE plpgsql
AS $function$
begin

 return query
  SELECT id, name FROM schemaName.user where school_id = schoolid;

end;
$function$

并将此函数称为:select * from schemaName.get_two_users_from_school(schoolid) as x(a bigint, b varchar);

【讨论】:

【参考方案4】:

您需要定义一个新类型并定义您的函数以返回该类型。

CREATE TYPE my_type AS (f1 varchar(10), f2 varchar(10) /* , ... */ );

CREATE OR REPLACE FUNCTION get_object_fields(name text) 
RETURNS my_type 
AS 
$$

DECLARE
  result_record my_type;

BEGIN
  SELECT f1, f2, f3
  INTO result_record.f1, result_record.f2, result_record.f3
  FROM table1
  WHERE pk_col = 42;

  SELECT f3 
  INTO result_record.f3
  FROM table2
  WHERE pk_col = 24;

  RETURN result_record;

END
$$ LANGUAGE plpgsql; 

如果要返回多条记录,需要将函数定义为returns setof my_type


更新

另一种选择是使用 RETURNS TABLE() 而不是创建 Postgres 8.4 中引入的 TYPE

CREATE OR REPLACE FUNCTION get_object_fields(name text) 
  RETURNS TABLE (f1 varchar(10), f2 varchar(10) /* , ... */ )
...

【讨论】:

请查看修改后的问题 - 原来的问题对于我的实际需求来说过于简化了。谢谢 我不确定我是否理解“新”问题,但这一切都归结为找到检索所需结果的正确查询。拥有它们后,返回结果应该不是问题 新问题是我需要能够偏移到检索到的行并从检索到的集合中获取特定字段,以便我可以填充从 SP 返回的“扁平化”结构。新问题归结为以下两个问题:(1)。我使用什么数据类型从查询中接收一组行 (2)> 如何访问返回集中第 N 行的字段“f1”? 我建议为此打开一个新问题。简而言之:对于 1)您将使用光标来遍历结果并选择您需要的结果,对于 2)您保留一个计数器,例如使用标识特定行的 row_number()。甚至更好:只选择 that 行。但是所有这些都不会改变你的函数的签名。返回您指定的类型就足够了。 不要使用新的TYPE 来解决这个问题。只需使用RECORD 并将RECORD 中的成员别名。有关不涉及自定义 TYPEs 的更正确答案,请参阅 ***.com/questions/4547672/…。【参考方案5】:

您可以使用 OUT 参数和 CROSS JOIN 来做到这一点

CREATE OR REPLACE FUNCTION get_object_fields(my_name text, OUT f1 text, OUT f2 text)
AS $$
SELECT t1.name, t2.name
FROM  table1 t1 
CROSS JOIN table2 t2 
WHERE t1.name = my_name AND t2.name = my_name;
$$ LANGUAGE SQL;

然后将其用作表格:

select get_object_fields( 'Pending') ;
get_object_fields
-------------------
(Pending,code)
(1 row)

select * from get_object_fields( 'Pending');
f1    |   f
---------+---------
Pending | code
(1 row)

select (get_object_fields( 'Pending')).f1;
f1
---------
Pending
(1 row)

【讨论】:

【参考方案6】:

如果您有一个具有这种精确记录布局的表,请将其名称用作类型,否则您必须显式声明该类型:

CREATE OR REPLACE FUNCTION get_object_fields
        (
        name text
        )
RETURNS mytable
AS
$$
        DECLARE f1 INT;
        DECLARE f2 INT;
        …
        DECLARE f8 INT;
        DECLARE retval mytable;
        BEGIN
        -- fetch fields f1, f2 and f3 from table t1
        -- fetch fields f4, f5 from table t2
        -- fetch fields f6, f7 and f8 from table t3
                retval := (f1, f2, …, f8);
                RETURN retval;
        END
$$ language plpgsql; 

【讨论】:

【参考方案7】:

不要使用CREATE TYPE 来返回多态结果。改用和滥用RECORD type。看看吧:

CREATE FUNCTION test_ret(a TEXT, b TEXT) RETURNS RECORD AS $$
DECLARE 
  ret RECORD;
BEGIN
  -- Arbitrary expression to change the first parameter
  IF LENGTH(a) < LENGTH(b) THEN
      SELECT TRUE, a || b, 'a shorter than b' INTO ret;
  ELSE
      SELECT FALSE, b || a INTO ret;
  END IF;
RETURN ret;
END;$$ LANGUAGE plpgsql;

请注意,它可以根据输入选择返回两个三个列。

test=> SELECT test_ret('foo','barbaz');
             test_ret             
----------------------------------
 (t,foobarbaz,"a shorter than b")
(1 row)

test=> SELECT test_ret('barbaz','foo');
             test_ret             
----------------------------------
 (f,foobarbaz)
(1 row)

这确实会对代码造成严重破坏,因此请务必使用一致的列数,但是对于返回可选错误消息以及返回操作成功的第一个参数来说非常方便。使用一致的列数重写:

CREATE FUNCTION test_ret(a TEXT, b TEXT) RETURNS RECORD AS $$
DECLARE 
  ret RECORD;
BEGIN
  -- Note the CASTING being done for the 2nd and 3rd elements of the RECORD
  IF LENGTH(a) < LENGTH(b) THEN
      ret := (TRUE, (a || b)::TEXT, 'a shorter than b'::TEXT);
  ELSE
      ret := (FALSE, (b || a)::TEXT, NULL::TEXT);
   END IF;
RETURN ret;
END;$$ LANGUAGE plpgsql;

几乎是史诗般的热度:

test=> SELECT test_ret('foobar','bar');
   test_ret    
----------------
 (f,barfoobar,)
(1 row)

test=> SELECT test_ret('foo','barbaz');
             test_ret             
----------------------------------
 (t,foobarbaz,"a shorter than b")
(1 row)

但是如何将其拆分为多行,以便您选择的 ORM 层可以将值转换为您选择的语言的本机数据类型?热度:

test=> SELECT a, b, c FROM test_ret('foo','barbaz') AS (a BOOL, b TEXT, c TEXT);
 a |     b     |        c         
---+-----------+------------------
 t | foobarbaz | a shorter than b
(1 row)

test=> SELECT a, b, c FROM test_ret('foobar','bar') AS (a BOOL, b TEXT, c TEXT);
 a |     b     | c 
---+-----------+---
 f | barfoobar | 
(1 row)

这是 PostgreSQL 中最酷和最未被充分利用的特性之一。请广而告之。

【讨论】:

确实很方便..我想知道如何再次拆分此记录...谢谢! @Sean:很好的答案,谢谢——我要试试这个。在最后一个示例中拆分结果非常酷。 @Sean:谢谢。我发现SELECT a, b, c ... 非常有用。我怎么能用这种方法来使用类似的东西:SELECT code, theFunction(code) from theTable ;,我是来自 table 而不是函数的SELECTing? @Sean: 1. 从 Postgres 9.2 开始,普通的 SQL 语句也被重新规划。 Details here. 2. 返回类型是否与EXECUTE独立。 3.答案中的技术仅适用于单行结果,不适用于SETOF record。 4.如果您的返回类型无论如何都是常量(就像在您的示例中一样),请使用众所周知的类型。使用匿名记录的想法仅在返回类型不同的极少数情况下才有意义。 查询结果为0的情况下会发生什么?该函数仍将返回一行(ret RECORD),即使它预期返回 0 行。

以上是关于使用 PL/pgSQL 在 PostgreSQL 中将多个字段作为记录返回的主要内容,如果未能解决你的问题,请参考以下文章

PostgreSQL-PL/pgSQL控制结构

如何在 PostgreSQL、PL/pgSQL 上执行匿名代码块切换 CASE 语句?

是否可以在 Spring Data JPA Repository 的 @Query 注释中使用 PostgreSQL 匿名块(PL/pgSQL)?

有没有为 PostgreSQL 开发的 PL/pgSQL 免费环境?

PostgreSQL vs Oracle:PL/pgSQL 的“编译时”检查

PostgreSQL:列名到数组 PL/pgSQL