Как соблюдать порядок массива в предложении PostgreSQL select - PullRequest
0 голосов
/ 06 августа 2020

Это моя (чрезвычайно упрощенная) таблица продуктов и некоторые тестовые данные.

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 и т. Д. c. С другой стороны, необходимо выполнить большое количество запросов, таких как продукты по брендам, избранные продукты, продукты по названию, по тегам, по диапазону или цене и т. Д. c.

Я не Я хочу повторять и поддерживать сложные списки столбцов продуктов каждый раз, когда я выполняю запрос, поэтому мой текущий подход разбивает процессы запросов на две задачи:

  • инкапсулировать запрос в функции типа select_products_by_xxx(), которые возвращают product_id массивы, правильно выбранные и упорядоченные.
  • инкапсулируют всю сложность столбца продукта в уникальную функцию list_products(), которая принимает product_id array в качестве параметра.
  • execute 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() не соблюдает порядок внутри массива.

Очевидно, я мог бы включить предложение order by в list_products() , но помните, что порядок должен определяться функциями 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 и самой проблемы, похоже, что внутри выполняется полное сканирование таблицы product list_products(). Так как продуктов могут быть тысячи, лучшим подходом было бы сканирование переданного массива.
  • Функции xxx можно отредактировать так, чтобы они возвращали setof int вместо int[]. Однако набор не может быть передан как параметр функции.

Ответы [ 2 ]

2 голосов
/ 06 августа 2020

Для длинных массивов вы обычно получаете (намного!) Более эффективные планы запросов с распаковкой массива и присоединением к основной таблице. В простых случаях это даже сохраняет исходный порядок массива без добавления ORDER BY. Строки обрабатываются по порядку. Но нет никаких гарантий, и порядок может быть нарушен дополнительными соединениями или параллельным выполнением et c. Чтобы убедиться, добавьте 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, чтобы вы могли вызывать функцию с массивом или списком идентификаторов (макс. По умолчанию 100 штук). См .:

Я бы объявил STABLE изменчивость функции .

Вы можете использовать LEFT JOIN вместо JOIN, чтобы убедиться, что все заданные идентификаторы возвращаются - со значениями NULL, если строка с данным идентификатором пропала.

db <> fiddle здесь

Обратите внимание на тонкие logi c разницу с дубликатами в массиве. В то время как product_id равно UNIQUE ...

  • unnest + left join возвращает ровно одну строку для каждого заданного идентификатора - с сохранением дубликатов в данных идентификаторах, если таковые имеются.
  • product_id = any (tid) складывает дубликаты. (Одна из причин, по которой это обычно приводит к более дорогим планам запросов.)

Если в данном массиве нет дубликатов, нет никакой разницы. Если могут быть дубликаты, и вы хотите их сложить, ваша задача неоднозначна, так как не определено, какую позицию оставить.

1 голос
/ 06 августа 2020

Вы очень близко, все, что вам нужно добавить, это 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)



...