0

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

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 子句等。另一方面,有大量的查询需要完成,如按品牌的产品、特色产品、产品按标题、按标签、按范围或价格等。

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

  • 将查询封装在 type 的函数中select_products_by_xxx(),该函数返回product_id正确选择和排序的数组。
  • 将所有产品列的复杂性封装在一个list_products()以 aproduct_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_idlist_products()函数不尊重数组内的顺序。

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

任何想法?


编辑

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

order by array_position(tid, product_id);

但是,这意味着要订购两次产品:首先是 inside select_products_by_xxx(),然后是 inside 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子句和问题本身,似乎product正在对表进行完整扫描list_products()。由于可能有数千种产品,更好的方法是扫描传递的数组。
  • 可以将xxx函数重构为 returnsetof int而不是int[]. 但是,集合不能作为函数参数传递。
4

2 回答 2

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 函数的波动性

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

db<>在这里摆弄

请注意与数组中的重复项存在细微的逻辑差异。虽然product_idUNIQUE...

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

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

于 2020-08-06T16:07:02.890 回答
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)



于 2020-08-06T13:43:06.680 回答