【问题标题】:How to respect the order of an array in a PostgreSQL select sentence如何在 PostgreSQL 选择语句中尊重数组的顺序
【发布时间】:2020-11-26 18:42:59
【问题描述】:

这是我的(极其简化的)产品表和一些测试数据。

drop table if exists product cascade;

create table product (
  product_id  integer not null,
  reference   varchar,
  price       decimal(13,4),
  
  primary key (product_id)
);

insert into product (product_id, reference, price) values 
(1001, 'MX-232',    100.00),
(1011, 'AX-232',     20.00),
(1003, 'KKK 11',     11.00),
(1004, 'OXS SUPER',   0.35),
(1005, 'ROR-MOT',   200.00),
(1006, '234PPP',     30.50),
(1007, 'T555-NS',   110.25),
(1008, 'LM234-XS',  101.20),
(1009, 'MOTOR-22',   12.50),
(1010, 'MOTOR-11',   30.00),
(1002, 'XUL-XUL1',   40.00);

我在现实生活中,列出产品列是一项教学任务,充满了连接、case-when-end 子句等。另一方面,有大量的查询需要完成,如按品牌分类的产品、特色产品、产品名称、标签、范围或价格等。

我不想在每次执行查询时重复和维护复杂的产品列列表,因此,我目前的方法是在两个任务中打破查询流程:

  • 将查询封装在 select_products_by_xxx() 类型的函数中,返回 product_id 数组,正确选择和排序。
  • 将所有产品列复杂性封装在一个独特的函数list_products() 中,该函数以product_id array 作为参数。
  • 执行select * from list_products(select_products_by_xxx()) 以获得每个xxx 函数所需的结果。

例如,要以相反的顺序选择product_id(如果这是对应用程序有意义的选择),这样的函数就可以做到。

create or replace function select_products_by_inverse () 
returns int[]
as $$
  select 
    array_agg(product_id order by product_id desc)  
  from 
    product;
$$ language sql;

它可以作为测试工作

select * from select_products_by_inverse();

select_products_by_inverse                              |
--------------------------------------------------------|
{1011,1010,1009,1008,1007,1006,1005,1004,1003,1002,1001}|

为了封装查询的“列表”部分,我使用了这个函数(同样,为了示例的方便,非常简化并且没有任何连接或大小写)。

create or replace function list_products (
    tid int[]
) 
returns table (
  id        integer,
  reference varchar,
  price     decimal(13,4)
)
as $$
  select
    product_id,
    reference,
    price
  from
    product
  where
    product_id = any (tid);
$$ language sql;

它有效,但不尊重传递数组中产品的顺序。

select * from list_products(select_products_by_inverse());

id  |reference|price   |
----|---------|--------|
1001|MX-232   |100.0000|
1011|AX-232   | 20.0000|
1003|KKK 11   | 11.0000|
1004|OXS SUPER|  0.3500|
1005|ROR-MOT  |200.0000|
1006|234PPP   | 30.5000|
1007|T555-NS  |110.2500|
1008|LM234-XS |101.2000|
1009|MOTOR-22 | 12.5000|
1010|MOTOR-11 | 30.0000|
1002|XUL-XUL1 | 40.0000|

所以,问题是我传递了一个自定义的 product_id 有序数组,但 list_products() 函数不遵守数组内的顺序。

显然,我可以在list_products() 中包含一个order by 子句,但请记住,排序必须由select_products_by_xxx() 函数确定,以保持list_products() 的唯一性。

有什么想法吗?


编辑

@adamkg 解决方案简单有效:添加通用 order by 子句,如下所示:

order by array_position(tid, product_id);

但是,这意味着要订购两次产品:首先在select_products_by_xxx() 内,然后在list_products() 内。

explain 探索呈现以下结果:

QUERY PLAN                                                            |
----------------------------------------------------------------------|
Sort  (cost=290.64..290.67 rows=10 width=56)                          |
  Sort Key: (array_position(select_products_by_inverse(), product_id))|
  ->  Seq Scan on product  (cost=0.00..290.48 rows=10 width=56)       |
        Filter: (product_id = ANY (select_products_by_inverse()))     |

现在我想知道是否还有其他更好的方法来降低成本,保持功能之间的可分离性。

我看到了两个有希望的策略:

  • 至于explain 子句和问题本身,似乎正在list_products() 内部完成对表product 的完整扫描。由于可能有数千种产品,更好的方法是扫描传递的数组。
  • xxx 函数可以重构为返回setof int 而不是int[]。但是,集合不能作为函数参数传递。

【问题讨论】:

    标签: arrays postgresql sql-order-by postgresql-12 array-agg


    【解决方案1】:

    你很亲密,你只需要添加ORDER BY array_position(tid, product_id)

    testdb=# create or replace function list_products (
        tid int[]
    ) 
    returns table (
      id        integer,
      reference varchar,
      price     decimal(13,4)
    )
    as $$
      select
        product_id,
        reference,
        price
      from
        product
      where
        product_id = any (tid)
    -- add this:
    order by array_position(tid, product_id);
    $$ language sql;
    CREATE FUNCTION
    testdb=# select * from list_products(select_products_by_inverse());
      id  | reference |  price   
    ------+-----------+----------
     1011 | AX-232    |  20.0000
     1010 | MOTOR-11  |  30.0000
     1009 | MOTOR-22  |  12.5000
     1008 | LM234-XS  | 101.2000
     1007 | T555-NS   | 110.2500
     1006 | 234PPP    |  30.5000
     1005 | ROR-MOT   | 200.0000
     1004 | OXS SUPER |   0.3500
     1003 | KKK 11    |  11.0000
     1002 | XUL-XUL1  |  40.0000
     1001 | MX-232    | 100.0000
    (11 rows)
    
    
    
    

    【讨论】:

    • 哇!太棒了@adamkg!
    【解决方案2】:

    对于长数组,您通常会通过取消嵌套数组并加入主表来获得(非常!)更有效的查询计划。在简单的情况下,这甚至保留了数组的原始顺序而不添加ORDER BY。行按顺序处理。但是没有任何保证,并且顺序可能会因更多的连接或并行执行等而被破坏。可以肯定的是,添加WITH ORDINALITY

    CREATE OR REPLACE FUNCTION list_products (tid int[])  -- VARIADIC?
      RETURNS TABLE (
       id        integer,
       reference varchar,
       price     decimal(13,4)
       )
      LANGUAGE sql STABLE AS
    $func$
      SELECT product_id, p.reference, p.price
      FROM   unnest(tid) WITH ORDINALITY AS t(product_id, ord)
      JOIN   product p USING (product_id)  -- LEFT JOIN ?
      ORDER  BY t.ord
    $func$;
    

    快速、简单、安全。见:

    您可能想要加入修饰符VARIADIC,这样您就可以使用数组 ID 列表调用函数(默认情况下最多100 个项目)。见:

    我会声明STABLE function volatility

    您可以使用LEFT JOIN 而不是JOIN 来确保返回所有 给定ID - 如果具有给定ID 的行丢失,则返回NULL 值。

    db小提琴here

    注意数组中的与重复项的细微逻辑差异。而product_idUNIQUE ...

    • unnest + left join 为每个给定 ID 准确返回一行 - 保留给定 ID 中的重复项(如果有)。
    • product_id = any (tid) 折叠重复项。 (它通常会导致更昂贵的查询计划的原因之一。)

    如果给定数组中没有重复,则没有区别。如果可能有重复项并且您想要折叠它们,那么您的任务就是模棱两可的,因为未定义要保留哪个位置。

    【讨论】:

    • 很好的建议,一如既往@erwin-brandstetter。非常感谢
    • @coterobarros:我添加了一些可能感兴趣的功能。
    • @coterobarros:还有一些逻辑上的微妙之处。
    • 是的,我注意到任何带有重复项的折叠行为,因为我手动对其进行了测试。但是,真实案例应该始终没有产品重复,除非在编写查询时出现人为错误。
    • @coterobarros:仍然波动STABLE。该函数查看数据库的一致快照,并始终为同一语句中的同一输入返回相同的输出。 (而且它不会修改数据。)我在上面添加了指向手册详细信息的链接。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多