【问题标题】:PostgreSQL parameterized Order By / Limit in table functionPostgreSQL 参数化 Order By / Limit in table 函数
【发布时间】:2011-12-29 16:34:20
【问题描述】:

我有一个执行简单 sql 选择语句的 sql 函数:

CREATE OR REPLACE FUNCTION getStuff(param character varying)
  RETURNS SETOF stuff AS
$BODY$
    select *
    from stuff
    where col = $1
$BODY$
  LANGUAGE sql;

现在我像这样调用这个函数:

select * from getStuff('hello');

如果我需要使用order bylimit 子句对结果进行排序和限制,我有哪些选择?

我猜是这样的查询:

select * from getStuff('hello') order by col2 limit 100;

效率不会很高,因为表 stuff 中的所有行都将由函数 getStuff 返回,然后才按限制排序和切片。

但即使我是对的,也没有简单的方法如何通过 sql 语言函数的参数传递顺序。只能传递值,不能传递部分 sql 语句。

另一种选择是以plpgsql 语言创建函数,可以在其中构造查询并通过EXECUTE 执行它。但这也不是一个很好的方法。

那么,还有其他方法可以实现吗? 或者你会选择什么选项?函数外的排序/限制,还是plpgsql?

我使用的是 postgresql 9.1。

编辑

我像这样修改了 CREATE FUNCTION 语句:

CREATE OR REPLACE FUNCTION getStuff(param character varying, orderby character varying)
  RETURNS SETOF stuff AS
$BODY$
    select t.*
    from stuff t
    where col = $1
    ORDER BY
        CASE WHEN $2 = 'parent' THEN t.parent END,
        CASE WHEN $2 = 'type' THEN t."type" END, 
        CASE WHEN $2 = 'title' THEN t.title END

$BODY$
  LANGUAGE sql;

这会抛出:

错误:CASE 类型字符不同且整数无法匹配 ŘÁDKA 13:当 $1 = 'parent' THEN t.parent

stuff 表如下所示:

CREATE TABLE stuff
    (
      id integer serial,
      "type" integer NOT NULL,
      parent integer,
      title character varying(100) NOT NULL,
      description text,
      CONSTRAINT "pkId" PRIMARY KEY (id),
    )

编辑2

我读错了 Dems 代码。我已将其更正为问题。这段代码对我有用。

【问题讨论】:

  • 为什么使用 PL/pgSQL 和 EXECUTE 不是一个好方法?在性能方面应该不会有太大的不同,是我能想到的唯一解决方案。
  • 使用 EXECUTE 会慢一些(因为正在进行额外的解析),但我怀疑你是否能够衡量差异。
  • @JoshuaBoshi:猜测性能影响通常不会奏效。
  • 好的:-) 随意写一个答案,我会接受,如果没有人会想出其他解决方案:-)
  • @JoshuaBoshi - 您必须针对您的特定情况对其进行测试。数据量、排序字段的允许组合、可用索引、数据碎片等都会产生影响。带有 EXECUTE 的动态 SQL 总是会产生一个执行等于或优于单个基于 CASE 的表达式的计划。但它感觉更混乱,因此更难维护。测试将显示性能差异。我经常重视维护而不是性能如果没有标记性能差异。

标签: sql database postgresql dynamic-sql plpgsql


【解决方案1】:

plpgsql 函数 对于稍微复杂一点的事情没有任何问题。唯一会降低性能的情况是嵌套 plpgsql 函数,因为查询计划器无法在外部查询的上下文中进一步优化包含的代码,这可能会或可能不会使其变慢。
稍后回答中的更多详细信息:

这比查询中的许多CASE 子句要简单得多:

CREATE OR REPLACE FUNCTION get_stuff(_param text, _orderby text, _limit int)
  RETURNS SETOF stuff AS
$func$
BEGIN
   RETURN QUERY EXECUTE '
      SELECT *
      FROM   stuff
      WHERE  col = $1
      ORDER  BY ' || quote_ident(_orderby) || ' ASC
      LIMIT  $2'
   USING _param, _limit;
END
$func$  LANGUAGE plpgsql;

呼叫:

SELECT * FROM get_stuff('hello', 'col2', 100);

注意事项

使用RETURN QUERY EXECUTE一次性返回查询结果。

使用 quote_ident() 作为标识符以防止 SQLi。
format() 了解更复杂的情况。见:

使用USING 子句传递参数值以避免再次强制转换、引用和SQLi。

注意不要在参数和列名之间产生命名冲突。在示例中,我在参数名称前加上了下划线 (_)。只是我个人的喜好。

编辑后的第二个函数无法工作,因为您只返回parent,而返回类型声明为SETOF stuff。您可以声明您喜欢的 any 返回类型,但实际返回值必须与声明匹配。您可能想为此使用RETURNS TABLE

【讨论】:

  • Brandstetter:哇,现在你已经向我学习了一些 plpgsql :-) 我不知道 RETURN QUERY EXECUTEUSING。这是一个非常优雅的解决方案,我现在不用担心 plpgsql 方法。非常感谢!
  • 如何添加ASC或DESC?
  • @erwin:我想像 _orderby 那样动态地进行操作。我想动态传递升序或降序。
  • @Sachin:提出一个包含相关细节的新问题。评论不是地方。如果您需要上下文,您可以随时链接到这个。有简单安全的解决方案。
  • 请检查这个问题。 stackoverflow.com/questions/49775242/…
【解决方案2】:

如果你的函数是stable(不修改数据库),查询规划器通常会inline它。因此,执行SELECT * FROM getStuff('x') LIMIT 10 将产生与限制在getStuff() 内一样的查询计划。

但是,您需要通过这样声明来告诉 PG 您的函数是稳定的:

CREATE OR REPLACE FUNCTION getStuff(param varchar)
RETURNS setof STUFF
LANGUAGE SQL
STABLE
AS $$ ... $$;

现在执行EXPLAIN SELECT * FROM getStuff('x') LIMIT 1 应该会产生与写出等效查询相同的查询计划。

内联也应该适用于函数外的ORDER BY 子句。但是如果你想参数化函数来确定排序依据,你可以这样做来控制排序方向:

CREATE FUNCTION sort_stuff(sort_col TEXT, sort_dir TEXT DEFAULT 'asc')
RETURNS SETOF stuff
LANGUAGE SQL
STABLE
AS $$
    SELECT *
    FROM stuff
    ORDER BY
      -- Simplified to NULL if not sorting in ascending order.
      CASE WHEN sort_dir = 'asc' THEN
          CASE sort_col
              -- Check for each possible value of sort_col.
              WHEN 'col1' THEN col1
              WHEN 'col2' THEN col2
              WHEN 'col3' THEN col3
              --- etc.
              ELSE NULL
          END
      ELSE
          NULL
      END
      ASC,

      -- Same as before, but for sort_dir = 'desc'
      CASE WHEN sort_dir = 'desc' THEN
          CASE sort_col
              WHEN 'col1' THEN col1
              WHEN 'col2' THEN col2
              WHEN 'col3' THEN col3
              ELSE NULL
          END
      ELSE
          NULL
      END
      DESC
$$;

只要sort_colsort_dir 在查询中保持不变,查询规划器就应该能够将看起来很冗长的查询简化为

SELECT *
FROM stuff
ORDER BY <sort_col> <sort_dir>

您可以使用EXPLAIN 进行验证。

【讨论】:

    【解决方案3】:

    至于ORDER BY,你可以试试这样的:

    SELECT
        <column list>
    FROM
        Stuff
    WHERE
        col1 = $1
    ORDER BY
        CASE $2
            WHEN 'col1' THEN col1
            WHEN 'col2' THEN col2
            WHEN 'col3' THEN col3
            ELSE col1  -- Or whatever your default should be
        END
    

    您可能需要进行一些数据类型转换,以便CASE 结果中的所有数据类型都匹配。请注意将数字转换为字符串 - 您必须在前面加上 0 才能使它们正确排序。日期/时间值也是如此。按年、月、日等格式排序。

    我在 SQL Server 中做过,但从未在 PostgreSQL 中做过,而且我在这台机器上没有 PostgreSQL 的副本,因此未经测试。

    【讨论】:

    • 为了避免数据类型问题...ORDER BY CASE WHEN $2 = 'a' THEN a END, CASE WHEN $2 = 'b' THEN b END, etc, etc。但请注意,这与我在 soulcheck 的回答中提到的优化问题相同。
    • 这不是我所指的数据类型问题。显然 $2 将始终是相同的数据类型。如果 a 列和 b 列是不同的数据类型,那么它可能会导致一些问题。
    • 我给出的示例通过将每个字段作为 ORDER BY 中的单独子句来处理这个问题。 CASE 语句产生ORDER BY null, null, x, null(例如),因此导致数据类型独立。
    • 感谢您的回答和 cmets。我正在使用 Dems 的 CASE 版本,但是当我尝试创建函数时,postgresql 抛出:ERROR: CASE types character varying and integer cannot be matched(在 THEN 之后的位置 - 很奇怪)。我将编辑问题并添加完整来源...
    • 正是我所说的问题。您需要确保将所有结果转换为相同的数据类型。
    【解决方案4】:

    使用 Format 函数即使是 ilike 花哨的运算符。

    CREATE OR REPLACE FUNCTION get_customer(
          _param text, _orderby text, _limit int)
          RETURNS SETOF customer AS
    $func$
    
    BEGIN
       RETURN QUERY EXECUTE format('
          SELECT *
          FROM   customer
          WHERE  first_name ilike ''%%%s%%''
          ORDER  BY  %I DESC
          LIMIT  %L',
          _param, _orderby, _limit );
    END
    $func$  LANGUAGE plpgsql;
    

    Format参考:https://www.postgresql.org/docs/current/functions-string.html

    【讨论】:

      【解决方案5】:

      您可以毫无问题地将限制值作为函数参数传递。至于排序,您可以将 ODER BY 与 CASE 语句结合使用。不幸的是,这不适用于

      ORDER BY CASE condition_variable
      WHEN 'asc' THEN column_name ASC
      ELSE column_name DESC
      END;
      

      【讨论】:

      • 1) 您需要将1 替换为a。 2) 这将阻止优化器使用索引等。它确实产生一个用于多种目的的查询,但是应该针对动态 sql 来检查性能开销是多少。 (每个查询只能创建一个计划,但不同的 order by 子句可能需要不同的计划才能有效。)
      • 是正确的,它根据a是否等于'asc'按第一列降序或升序排列。但无论如何我都会编辑以使其更清晰。
      • 这是不合法的。表达式中不能有 ASC 或 DESC。
      • @asnyder 是的,这是一个不起作用的例子;)非常老的答案,所以质量还有很多不足之处。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-02-21
      • 1970-01-01
      • 2015-12-02
      • 2011-04-06
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多