在 ORDER BY 中使用 CASE .. END 有意义吗?

joa*_*olo 6 postgresql order-by dynamic-sql functions postgresql-9.6

类似SELECT * FROM t ORDER BY case when _parameter='a' then column_a end, case when _parameter='b' then column_b end的查询是可能的,但是:这是一个好习惯吗?

在查询的 WHERE 部分使用参数是很常见的,在 SELECT 部分有一些计算列,但参数化 ORDER BY 子句并不常见。

假设我们有一个列出二手车的应用程序 (à la CraigsList)。汽车列表可以按价格或颜色排序。我们有一个函数,给定一定数量的参数(比如价格范围、颜色和排序标准),它会返回一组带有结果的记录。

为了具体起见,让我们假设cars都在下表中:

CREATE TABLE cars
(
  car_id serial NOT NULL PRIMARY KEY,  /* arbitrary anonymous key */
  make text NOT NULL,       /* unnormalized, for the sake of simplicity */
  model text NOT NULL,      /* unnormalized, for the sake of simplicity */
  year integer,             /* may be null, meaning unknown */
  euro_price numeric(12,2), /* may be null, meaning seller did not disclose */
  colour text               /* may be null, meaning unknown */
) ;
Run Code Online (Sandbox Code Playgroud)

该表将具有大多数列的索引...

CREATE INDEX cars_colour_idx
  ON cars (colour);
CREATE INDEX cars_price_idx
  ON cars (price);
/* etc. */
Run Code Online (Sandbox Code Playgroud)

并有一些商品枚举:

CREATE TYPE car_sorting_criteria AS ENUM
   ('price',
    'colour');
Run Code Online (Sandbox Code Playgroud)

...和一些示例数据

INSERT INTO cars.cars (make, model, year, euro_price, colour)

VALUES 
    ('Ford',   'Mondeo',   1990,  2000.00, 'green'),
    ('Audi',   'A3',       2005,  2500.00, 'golden magenta'),
    ('Seat',   'Ibiza',    2012, 12500.00, 'dark blue'),
    ('Fiat',   'Punto',    2014,     NULL, 'yellow'),
    ('Fiat',   '500',      2010,  7500.00, 'blueish'),
    ('Toyota', 'Avensis',  NULL,  9500.00, 'brown'), 
    ('Lexus',  'CT200h',   2012, 12500.00, 'dark whitish'), 
    ('Lexus',  'NX300h',   2013, 22500.00, NULL) ;
Run Code Online (Sandbox Code Playgroud)

我们将要进行的查询类型如下:

SELECT
    make, model, year, euro_price, colour
FROM
    cars.cars
WHERE
    euro_price between 7500 and 9500 
ORDER BY
    colour ;
Run Code Online (Sandbox Code Playgroud)

我们希望在函数中查询这种风格:

CREATE or REPLACE FUNCTION get_car_list
   (IN _colour    text, 
    IN _min_price numeric, 
    IN _max_price numeric, 
    IN _sorting_criterium car_sorting_criteria) 
RETURNS record AS
$BODY$
      SELECT
          make, model, year, euro_price, colour
      FROM
          cars
      WHERE
           euro_price between _min_price and _max_price
           AND colour = _colour
      ORDER BY
          CASE WHEN _sorting_criterium = 'colour' THEN
            colour
          END,
          CASE WHEN _sorting_criterium = 'price' THEN
            euro_price
          END 
$BODY$
LANGUAGE SQL ;
Run Code Online (Sandbox Code Playgroud)

代替这种方法,该函数中的 SQL 可以作为字符串动态生成(在 PL/pgSQL 中),然后执行。

我们可以感觉到任何一种方法的一些限制、优点和缺点:

  1. 在函数中,很难找到某个语句的查询计划(如果可能的话)。然而,当我们经常使用某些东西时,我们倾向于使用函数。
  2. 静态 SQL 中的错误将(主要)在函数编译或首次调用时捕获。
  3. 动态SQL中的错误只有在函数编译完成后才会被捕获(moslty),并且所有的执行路径都已经检查过(即:对函数执行的测试次数可能真的很高)。
  4. 像公开的那样的参数查询可能比动态生成的查询效率低;然而,执行者每次解析/制作查询树/决定都会有更难的工作(这可能会影响相反方向的效率)。

题:

如何“两全其美”(如果可能)?[效率+编译器检查+轻松调试+轻松优化]

注意:这将在 PostgreSQL 9.6 上运行。

Erw*_*ter 3

一般回答

首先,我想解决前提中的歧义:

在查询的 WHERE 部分中使用参数并在 SELECT 部分中使用一些计算列是很常见的,但参数化 ORDER BY 子句并不常见。

该部分中的计算列SELECT几乎与查询计划或性能无关。但“在WHERE部分中”是含糊不清的。

在子句中参数化是很常见的WHERE,这适用于准备好的语句。(PL/pgSQL 在内部使用准备好的语句。)无论提供的如何,通用查询计划通常都是有意义的。也就是说,除非表的数据分布非常不均匀,但自从 Postgres 9.2 PL/pgSQL 重新计划查询几次以测试通用计划是否足够好:

但在子句中参数化整个谓词(包括标识符)并不常见WHERE,这对于准备好的语句来说是不可能的。您需要使用动态 SQL EXECUTE,或者在客户端中组装查询字符串。

动态ORDER BY表达式介于两者之间。您可以使用CASE表达式来完成此操作,但这通常很难优化。Postgres 可能会使用带有 plain 的索引ORDER BY,但不能使用CASE隐藏最终排序顺序的表达式。规划者很聪明,但不是人工智能。根据查询的其余部分(ORDER BY可能与计划相关或不相关 - 它与您的示例相关),您可能始终得到次优的查询计划。另外,您还可以添加表达式
的次要成本。CASE在您的特定示例中也适用于多个无用的ORDER BY列。

通常,动态 SQL 对此EXECUTE更快或更快。

如果您在函数体中保持清晰易读的代码格式,那么可维护性就不应该成为问题。

修复演示功能

问题中的功能已损坏。返回类型定义为返回匿名记录:

RETURNS record AS
Run Code Online (Sandbox Code Playgroud)

但查询实际上返回一记录,它必须是:

RETURNS SETOF record AS
Run Code Online (Sandbox Code Playgroud)

但这仍然无济于事。您必须在每次调用时提供列定义列表。您的查询返回已知类型的列。相应地声明返回类型!我在这里猜测,使用返回列/表达式的实际数据类型:

RETURNS TABLE (make text, model text, year int, euro_price int, colour text) AS
Run Code Online (Sandbox Code Playgroud)

为了方便起见,我使用相同的列名称。子句中的列RETURNS TABLE是有效的OUT参数,在正文中的每个 SQL 语句中可见(但在 内部不可见EXECUTE)。因此,对函数体中的查询中的列进行表限定,以避免可能的命名冲突。演示函数的工作原理如下:

CREATE or REPLACE FUNCTION get_car_list (
    _colour            text, 
    _min_price         numeric, 
    _max_price         numeric, 
    _sorting_criterium car_sorting_criteria) 
  RETURNS TABLE (make text, model text, year int, euro_price numeric, colour text) AS
$func$
      SELECT c.make, c.model, c.year, c.euro_price, c.colour
      FROM   cars c
      WHERE  c.euro_price BETWEEN _min_price AND _max_price
      AND    c.colour = _colour
      ORDER  BY CASE WHEN _sorting_criterium = 'colour' THEN c.colour     END
              , CASE WHEN _sorting_criterium = 'price'  THEN c.euro_price END;
$func$  LANGUAGE sql;
Run Code Online (Sandbox Code Playgroud)

不要像Evan 在他的回答中RETURNS那样将函数声明中的关键字与 plpgsqlRETURN命令混淆。细节:

示例查询的一般难度

某些列进行谓词(更糟糕的是:范围谓词),对 中的其他ORDER BY列进行谓词,这已经很难优化。但你在评论中提到

实际结果集可能有 1000 行左右(因此,在服务器端以较小的块进行排序和分页)

因此,您将向这些查询添加LIMIT和,首先OFFSET返回n 个“最佳”匹配项。或者一些更智能的分页技术:

需要一个匹配的索引才能加快速度。我不明白这如何CASEORDER BY.

考虑: