Как написать комбинаторику функции в postgres?

у меня есть таблица PostgreSQL этой формы:

base_id int | mods smallint[]
     3      |   {7,15,48}

мне нужно заполнить таблицу такого вида:

combo_id int | base_id int | mods smallint[]
     1       |     3       |      
     2       |     3       |      {7}
     3       |     3       |      {7,15}   
     4       |     3       |      {7,48}   
     5       |     3       |      {7,15,48}
     6       |     3       |      {15}
     7       |     3       |      {15,48}
     8       |     3       |      {48}

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

но, я новичок Postgres и не могу для жизни меня понять, как это сделать, используя plpgsql. Этого не должно быть. особенно быстро; он будет запускаться только периодически на бэкэнде. Первая таблица содержит около 80 записей, и грубый расчет предполагает, что мы можем ожидать около 2600 записей для второй таблицы.

может кто-нибудь хотя бы указать мне в правильном направлении?

Edit: Craig: у меня есть PostgreSQL 9.0. Мне удалось успешно использовать UNNEST ():

FOR messvar IN SELECT * FROM UNNEST(mods) AS mod WHERE mod BETWEEN 0 AND POWER(2, @n) - 1
LOOP
    RAISE NOTICE '%', messvar;
END LOOP;

но тогда не знал, куда идти дальше.

Edit: для ссылка, я закончил использование решения Эрвина, с одной строкой, добавленной для добавления нулевого результата ('{}') к каждому набору, и особый случай, на который ссылается Эрвин, удален:

CREATE OR REPLACE FUNCTION f_combos(_arr integer[], _a integer[] DEFAULT '{}'::integer[], _z integer[] DEFAULT '{}'::integer[])
RETURNS SETOF integer[] LANGUAGE plpgsql AS
$BODY$
DECLARE
 i int;
 j int;
 _up int;
BEGIN
 IF array_length(_arr,1) > 0 THEN 
    _up := array_upper(_arr, 1);

    IF _a = '{}' AND _z = '{}' THEN RETURN QUERY SELECT '{}'::int[]; END IF;
    FOR i IN array_lower(_arr, 1) .. _up LOOP
       FOR j IN i .. _up  LOOP
          CASE j-i
          WHEN 0,1 THEN
             RETURN NEXT _a || _arr[i:j] || _z;
          ELSE
             RETURN NEXT _a || _arr[i:i] || _arr[j:j] || _z;
             RETURN QUERY SELECT *
             FROM f_combos(_arr[i+1:j-1], _a || _arr[i], _arr[j] || _z);
          END CASE;
       END LOOP;
    END LOOP;
 ELSE
    RETURN NEXT _arr;
 END IF;
END;
$BODY$

затем я использовал эту функцию для заполнения моей таблицы:

INSERT INTO e_ecosystem_modified (ide_ecosystem, modifiers) 
(SELECT ide_ecosystem, f_combos(modifiers) AS modifiers FROM e_ecosystem WHERE ecosystemgroup <> 'modifier' ORDER BY ide_ecosystem, modifiers);

из 79 строк в моей исходной таблице с максимум 7 элементами в массиве модификаторов запрос занял 250 мс, чтобы заполнить 2630 строк в моей выходной таблице. Фантастический.

2 ответов


после того, как я спал над ним, у меня была совершенно новая, более простая и быстрая идея:

CREATE OR REPLACE FUNCTION f_combos(_arr anyarray)
  RETURNS TABLE (combo anyarray) LANGUAGE plpgsql AS
$BODY$
BEGIN
    IF array_upper(_arr, 1) IS NULL THEN
        combo := _arr; RETURN NEXT; RETURN;
    END IF;

    CASE array_upper(_arr, 1)
--  WHEN 0 THEN -- does not exist
    WHEN 1 THEN
        RETURN QUERY VALUES ('{}'), (_arr);
    WHEN 2 THEN
        RETURN QUERY VALUES ('{}'), (_arr[1:1]), (_arr), (_arr[2:2]);
    ELSE
        RETURN QUERY
        WITH x AS (
            SELECT f.combo FROM f_combos(_arr[1:array_upper(_arr, 1)-1]) f
            )
        SELECT x.combo FROM x
        UNION ALL
        SELECT x.combo || _arr[array_upper(_arr, 1)] FROM x;
    END CASE;
END
$BODY$;

звоните:

SELECT * FROM f_combos('{1,2,3,4,5,6,7,8,9}'::int[]) ORDER BY 1;

512 строк, общее время выполнения: 2.899 ms

объяснить

  • лечить особые случаи с NULL и пустой массив.
  • построить комбинации для примитивного массива из двух.
  • любой более длинный массив разбивается на:
    • комбинации для одного и того же массива длины n-1
    • плюс все они объединены с элементом n .. рекурсивно.

очень просто, как только вы его получили.

  • работает для 1-мерных целых массивов, начиная с Нижний индекс 1 (см. ниже).
  • 2-3 раза быстрее, чем старое решение, Весы лучше.
  • работает любой тип элемента снова (с использованием полиморфных типов).
  • включает пустой массив в результат как показано в вопросе (и как @Craig указал мне в комментариях).
  • короче, элегантнее.

предполагается индексы массива начиная с 1 (по умолчанию). Если вы не уверены в своих значениях, вызовите такую функцию для нормализации:

SELECT * FROM  f_combos(_arr[array_lower(_arr, 1):array_upper(_arr, 1)]);

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

старое решение (медленнее)

CREATE OR REPLACE FUNCTION f_combos2(_arr int[], _a int[] = '{}', _z int[] = '{}')
 RETURNS SETOF int[] LANGUAGE plpgsql AS
$BODY$
DECLARE
   i int;
   j int;
   _up int;
BEGIN
   IF array_length(_arr,1) > 0 THEN 
      _up := array_upper(_arr, 1);

      FOR i IN array_lower(_arr, 1) .. _up LOOP
         FOR j IN i .. _up  LOOP
            CASE j-i
            WHEN 0,1 THEN
               RETURN NEXT _a || _arr[i:j] || _z;
            WHEN 2 THEN
               RETURN NEXT _a || _arr[i:i] || _arr[j:j] || _z;
               RETURN NEXT _a || _arr[i:j] || _z;
            ELSE
               RETURN NEXT _a || _arr[i:i] || _arr[j:j] || _z;
               RETURN QUERY SELECT *
               FROM f_combos2(_arr[i+1:j-1], _a || _arr[i], _arr[j] || _z);
            END CASE;
         END LOOP;
      END LOOP;
   ELSE
      RETURN NEXT _arr;
   END IF;
END;
$BODY$;

звоните:

SELECT * FROM f_combos2('{7,15,48}'::int[]) ORDER BY 1;

работает для одномерных целочисленных массивов. Это может быть дополнительно оптимизировано, но это, безусловно, не требуется для охвата этого вопроса.
ORDER BY чтобы наложить порядок, отображаемый в вопросе.

укажите NULL или пустой массив, как NULL упоминается в комментарии.

протестировано с PostgreSQL 9.1, но должно работать с любой наполовину современной версией. array_lower() и array_upper() существуют, по крайней мере, с PostgreSQL 7.4. Только параметры по умолчанию являются новыми в версии 8.4. Можно легко заменить.

производительность приличная.

SELECT DISTINCT * FROM f_combos('{1,2,3,4,5,6,7,8,9}'::int[]) ORDER BY 1;

511 строк, общее время выполнения: 7,729 МС

объяснение

он основывается на этом простая форма это только создает все комбинации соседних элементов:

CREATE FUNCTION f_combos(_arr int[])
  RETURNS SETOF int[] LANGUAGE plpgsql AS
$BODY$
DECLARE
   i  int;
   j  int;
  _up int;
BEGIN
   _up := array_upper(_arr, 1);

   FOR i in array_lower(_arr, 1) .. _up LOOP
      FOR j in i .. _up LOOP
         RETURN NEXT _arr[i:j];
      END LOOP;
   END LOOP;
END;
$BODY$;

но это не для суб-массивы с более чем двумя элементами. Итак:

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

  • для любого суб-массива с более чем 3 элементами я беру внешний два элементы на все комбинации внутренних элементов построенный той же функцией рекурсивно.


один подход - с рекурсивным CTE. Обновленная рекурсивная функция Erwin значительно быстрее и масштабируется лучше, поэтому это действительно полезно как интересный другой подход. Обновленная версия Erwin намного практичнее.

я попытался немного подсчитать подход (см. Конец), но без быстрого способа вырвать произвольные элементы из массива он оказался медленнее, чем любой рекурсивный подход.

рекурсивные комбинации CTE функция

CREATE OR REPLACE FUNCTION combinations(anyarray) RETURNS SETOF anyarray AS $$
WITH RECURSIVE
    items AS (
            SELECT row_number() OVER (ORDER BY item) AS rownum, item
            FROM (SELECT unnest() AS item) unnested
    ),
    q AS (
            SELECT 1 AS i, [1:0] arr
            UNION ALL
            SELECT (i+1), CASE x
                    WHEN 1 THEN array_append(q.arr,(SELECT item FROM items WHERE rownum = i))
                    ELSE q.arr END
            FROM generate_series(0,1) x CROSS JOIN q WHERE i <= array_upper(,1)
    )
SELECT q.arr AS mods
FROM q WHERE i = array_upper(,1)+1;
$$ LANGUAGE 'sql';

это полиморфная функция, поэтому она будет работать на массивы любого типа.

логика состоит в том, чтобы перебирать каждый элемент в незарегистрированном входном наборе, используя рабочую таблицу. Начните с пустого массива в рабочей таблице с номером поколения 1. Для каждой записи во входном наборе вставьте два новых массива в рабочую таблицу с увеличенным номером генерации. Один из них является копией входного массива предыдущего поколения, а другой - входной массив с добавленным к нему элементом (generation-number) из входного набора. Если число генерации превышает число элементов во входном наборе, верните последнее поколение.

использование

можно использовать combinations(smallint[]) функция для получения желаемых результатов, используя ее как функцию возврата набора в combinatin с row_number функции окна.

-- assuming table structure
regress=# \d comb
       Table "public.comb"
 Column  |    Type    | Modifiers 
---------+------------+-----------
 base_id | integer    | 
 mods    | smallint[] | 


SELECT base_id, row_number() OVER (ORDER BY mod) AS mod_id, mod 
FROM (SELECT base_id, combinations(mods) AS mod FROM comb WHERE base_id = 3) x
ORDER BY mod;

результаты

regress=# SELECT base_id, row_number() OVER (ORDER BY mod) AS mod_id, mod 
regress-# FROM (SELECT base_id, combinations(mods) AS mod FROM comb WHERE base_id = 3) x
regress-# ORDER BY mod;
 base_id | mod_id |    mod    
---------+--------+-----------
       3 |      1 | {}
       3 |      2 | {7}
       3 |      3 | {7,15}
       3 |      4 | {7,15,48}
       3 |      5 | {7,48}
       3 |      6 | {15}
       3 |      7 | {15,48}
       3 |      8 | {48}
(8 rows)

Time: 2.121 ms

массивы нулевых элементов дают нулевой результат. Если вы хотите combinations({}) чтобы вернуть один ряд {} тогда a UNION ALL С {} сделает работу.

теория

похоже, вы хотите k-комбинации для всех k в K-мультикомбинации, а не простые комбинации. См.количество комбинаций с повторением.

другими словами, вы хотите все K-комбинации элементов из вашего набора, для всех k от 0 до n, где n-размер набора.

связанные так вопрос: SQL-найти все возможные комбинации, который имеет действительно интересный ответ о подсчете бит.

побитовые операции существуют в Pg, поэтому немного рассчитывает подход должен быть возможно. Вы ожидаете, что он будет более эффективным, но поскольку он так медленно выбирает разбросанное подмножество элементов из массива, он на самом деле работает медленнее.

CREATE OR REPLACE FUNCTION bitwise_subarray(arr anyarray, elements integer)
RETURNS anyarray AS $$
SELECT array_agg([n+1]) 
FROM generate_series(0,array_upper(,1)-1) n WHERE (>>n) & 1 = 1;
$$ LANGUAGE sql;

COMMENT ON FUNCTION bitwise_subarray(anyarray,integer) IS 'Return the elements from  where the corresponding bit in  is set';

CREATE OR REPLACE FUNCTION comb_bits(anyarray) RETURNS SETOF anyarray AS $$ 
SELECT bitwise_subarray(, x) 
FROM generate_series(0,pow(2,array_upper(,1))::integer-1) x;
$$ LANGUAGE 'sql';

если бы вы могли найти более быстрый способ написать bitwise_subarray затем comb_bits было бы очень быстро. Как, скажем, небольшая функция расширения C, но я достаточно сумасшедший, чтобы написать один из них для ответа SO.