Найти строки, где целочисленная последовательность содержит данную подпоследовательность

9

проблема

Примечание: я имею в виду математические последовательности , а не механизм последовательностей в PostgreSQL .

У меня есть таблица, представляющая последовательности целых чисел. Определение таково:

CREATE TABLE sequences
(
  id serial NOT NULL,
  title character varying(255) NOT NULL,
  date date NOT NULL,
  sequence integer[] NOT NULL,
  CONSTRAINT "PRIM_KEY_SEQUENCES" PRIMARY KEY (id)
);

Моя цель - найти строки, используя данную подпоследовательность. То есть строки, где sequenceполе представляет собой последовательность, которая содержит данную подпоследовательность (в моем случае последовательность упорядочена).

пример

Предположим, что таблица содержит следующие данные:

+----+-------+------------+-------------------------------+
| id | title |    date    |           sequence            |
+----+-------+------------+-------------------------------+
|  1 | BG703 | 2004-12-24 | {1,3,17,25,377,424,242,1234}  |
|  2 | BG256 | 2005-05-11 | {5,7,12,742,225,547,2142,223} |
|  3 | BD404 | 2004-10-13 | {3,4,12,5698,526}             |
|  4 | BK956 | 2004-08-17 | {12,4,3,17,25,377,456,25}     |
+----+-------+------------+-------------------------------+

Поэтому, если данная подпоследовательность есть {12, 742, 225, 547}, я хочу найти строку 2.

Точно так же, если данная подпоследовательность есть {3, 17, 25, 377}, я хочу найти строку 1 и строку 4.

Наконец, если заданная подпоследовательность есть {12, 4, 3, 25, 377}, то строки не возвращаются.

исследования

Во-первых, я не совсем уверен, что представлять последовательности с типом данных массива целесообразно. Хотя это похоже на ситуацию; Боюсь, это усложняет управление. Возможно, лучше представлять последовательности по-разному, используя модель отношений с другой таблицей.

Точно так же я думаю о расширении последовательностей с помощью unnestфункции массива, а затем добавляю критерии поиска. Тем не менее, количество членов в последовательности, являющейся переменной, я не вижу, как это сделать.

Я знаю, что также возможно вырезать мою последовательность в подпоследовательности, используя subarrayфункцию модуля intarray, но я не понимаю, как это мне пригодится для моего поиска.

Ограничения

Даже если в данный момент моя модель все еще разрабатывается, предполагается, что таблица будет состоять из множества последовательностей, от 50 000 до 300 000 строк. Так что у меня сильное ограничение производительности.

В моем примере я использовал относительно маленькие целые числа. На практике, возможно, что эти целые числа становятся намного больше, вплоть до переполнения bigint. В такой ситуации, я думаю, лучше всего хранить числа в виде строк (поскольку нет необходимости выполнять эти последовательности математических операций). Однако, выбирая это решение, это делает невозможным использование модуля intarray , упомянутого выше.

mlpo
источник
Если они могут переполниться, bigintвы должны использовать их numericкак тип для хранения. Это намного медленнее и занимает гораздо больше места, хотя.
Крейг Рингер
@CraigRinger Почему использовать, numericа не строку ( textнапример)? Мне не нужно выполнять математические операции над своими последовательностями.
mlpo
2
Потому что он более компактен и во многих отношениях быстрее textи не позволяет вам хранить поддельные нечисловые данные. Зависит от того, что если вы выполняете только ввод-вывод, вам может потребоваться, чтобы текст уменьшал объем обработки ввода-вывода.
Крейг Рингер
@CraigRinger Действительно, тип более последовательный. Что касается производительности, я проверю, когда я нашел способ сделать свой поиск.
mlpo
2
@CraigRinger Может сработать, если порядок не имеет значения. Но здесь последовательности упорядочены. Пример: SELECT ARRAY[12, 4, 3, 17, 25, 377, 456, 25] @> ARRAY[12, 4, 3, 25, 377];вернет true, потому что заказ не рассматривается этим оператором.
mlpo

Ответы:

3

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

Вот пример для массивов int4. ( Общий вариант массива и соответствующий скрипт SQL ).

Datum
_int_sequence_contained(PG_FUNCTION_ARGS)
{
    return DirectFunctionCall2(_int_contains_sequence,
                               PG_GETARG_DATUM(1),
                               PG_GETARG_DATUM(0));
}

Datum
_int_contains_sequence(PG_FUNCTION_ARGS)
{
    ArrayType  *a = PG_GETARG_ARRAYTYPE_P(0);
    ArrayType  *b = PG_GETARG_ARRAYTYPE_P(1);
    int         na, nb;
    int32      *pa, *pb;
    int         i, j;

    na = ArrayGetNItems(ARR_NDIM(a), ARR_DIMS(a));
    nb = ArrayGetNItems(ARR_NDIM(b), ARR_DIMS(b));
    pa = (int32 *) ARR_DATA_PTR(a);
    pb = (int32 *) ARR_DATA_PTR(b);

    /* The naive searching algorithm. Replace it with a better one if your arrays are quite large. */
    for (i = 0; i <= na - nb; ++i)
    {
        for (j = 0; j < nb; ++j)
            if (pa[i + j] != pb[j])
                break;

        if (j == nb)
            PG_RETURN_BOOL(true);
    }

    PG_RETURN_BOOL(false);
}
CREATE FUNCTION _int_contains_sequence(_int4, _int4)
RETURNS bool
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT IMMUTABLE;

CREATE FUNCTION _int_sequence_contained(_int4, _int4)
RETURNS bool
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT IMMUTABLE;

CREATE OPERATOR @@> (
  LEFTARG = _int4,
  RIGHTARG = _int4,
  PROCEDURE = _int_contains_sequence,
  COMMUTATOR = '<@@',
  RESTRICT = contsel,
  JOIN = contjoinsel
);

CREATE OPERATOR <@@ (
  LEFTARG = _int4,
  RIGHTARG = _int4,
  PROCEDURE = _int_sequence_contained,
  COMMUTATOR = '@@>',
  RESTRICT = contsel,
  JOIN = contjoinsel
);

Теперь вы можете фильтровать строки, как это.

SELECT * FROM sequences WHERE sequence @@> '{12, 742, 225, 547}'

Я провел небольшой эксперимент, чтобы выяснить, насколько быстрее это решение.

CREATE TEMPORARY TABLE sequences AS
SELECT array_agg((random() * 10)::int4) AS sequence, g1 AS id
FROM generate_series(1, 100000) g1
  CROSS JOIN generate_series(1, 30) g2
GROUP BY g1;
EXPLAIN ANALYZE SELECT * FROM sequences
WHERE        translate(cast(sequence as text), '{}',',,')
 LIKE '%' || translate(cast('{1,2,3,4}'as text), '{}',',,') || '%'

"Seq Scan on sequences  (cost=0.00..7869.42 rows=28 width=36) (actual time=2.487..334.318 rows=251 loops=1)"
"  Filter: (translate((sequence)::text, '{}'::text, ',,'::text) ~~ '%,1,2,3,4,%'::text)"
"  Rows Removed by Filter: 99749"
"Planning time: 0.104 ms"
"Execution time: 334.365 ms"
EXPLAIN ANALYZE SELECT * FROM sequences WHERE sequence @@> '{1,2,3,4}'

"Seq Scan on sequences  (cost=0.00..5752.01 rows=282 width=36) (actual time=0.178..20.792 rows=251 loops=1)"
"  Filter: (sequence @@> '{1,2,3,4}'::integer[])"
"  Rows Removed by Filter: 99749"
"Planning time: 0.091 ms"
"Execution time: 20.859 ms"

Так что это примерно в 16 раз быстрее. Если этого недостаточно, вы можете добавить поддержку индексов GIN или GiST, но это будет гораздо более сложной задачей.

Slonopotamus
источник
Звучит интересно, но я использую либо строки, либо тип numericдля представления моих данных, потому что они могут переполниться bigint. Возможно, было бы хорошо отредактировать ваш ответ, чтобы он соответствовал ограничениям вопроса. Во всяком случае, я сделаю сравнительное представление, которое я опубликую здесь.
mlpo
Я не уверен, стоит ли вставлять в ответы большие блоки кода, поскольку они должны быть минимальными и проверяемыми. Общая версия массива этой функции в четыре раза длиннее и довольно громоздка. Я также проверил это с, numericи textулучшение варьировалось от 20 до 50 раз в зависимости от длины массивов.
Слонопотам
Да, однако необходимо, чтобы ответы отвечали на вопросы :-). Здесь мне кажется, что ответ, соответствующий ограничениям, интересен (потому что этот аспект является частью вопроса). Тем не менее, возможно, нет необходимости предлагать общую версию. Просто версия со строками или numeric.
mlpo
В любом случае, я добавил версию для универсальных массивов, так как она будет почти одинаковой для любого типа данных переменной длины. Но если вы действительно беспокоитесь о производительности, вам следует придерживаться таких типов данных, как bigint.
Slonopotamus
Я хотел бы сделать это. Проблема в том, что некоторые из моих последовательностей выходят далеко за рамки bigint, поэтому, похоже, у меня нет выбора. Но если у вас есть идея, мне интересно :).
mlpo
1

Вы можете легко найти подпоследовательность, когда вы приводите массивы к строкам и заменяете фигурные скобки запятыми:

translate(cast(sequence as varchar(10000)), '{}',',,')

{1,3,17,25,377,424,242,1234} -> ',1,3,17,25,377,424,242,1234,'

Сделайте то же самое для массива, который вы ищете, и добавьте ведущий и трейлинг %:

'%' || translate(cast(searchedarray as varchar(10000)), '{}',',,') || '%'

{3, 17, 25, 377} -> '%,3,17,25,377,%'

Теперь вы сравниваете это, используя LIKE:

WHERE        translate(cast(sequence      as varchar(10000)), '{}',',,')
 LIKE '%' || translate(cast(searchedarray as varchar(10000)), '{}',',,') || '%'

Редактировать:

Скрипка снова работает.

Если массивы нормализованы в одну строку для каждого значения, вы можете применить логику на основе набора:

CREATE TABLE sequences
( id int NOT NULL,
  n int not null,
  val numeric not null
);

insert into sequences values(  1, 1,1     );
insert into sequences values(  1, 2,3     );
insert into sequences values(  1, 3,17    );
insert into sequences values(  1, 4,25    );
insert into sequences values(  1, 5,377   );
insert into sequences values(  1, 6,424   );
insert into sequences values(  1, 7,242   );
insert into sequences values(  1, 8,1234  );
insert into sequences values(  2, 1,5     );
insert into sequences values(  2, 2,7     );
insert into sequences values(  2, 3,12    );
insert into sequences values(  2, 4,742   );
insert into sequences values(  2, 5,225   );
insert into sequences values(  2, 6,547   );
insert into sequences values(  2, 7,2142  );
insert into sequences values(  2, 8,223   );
insert into sequences values(  3, 1,3     );
insert into sequences values(  3, 2,4     );
insert into sequences values(  3, 3,12    );
insert into sequences values(  3, 4,5698  );
insert into sequences values(  3, 5,526   );          
insert into sequences values(  4, 1,12    );
insert into sequences values(  4, 2,4     );
insert into sequences values(  4, 3,3     );
insert into sequences values(  4, 4,17    );
insert into sequences values(  4, 5,25    );
insert into sequences values(  4, 6,377   );
insert into sequences values(  4, 7,456   );
insert into sequences values(  4, 8,25    );
insert into sequences values(  5, 1,12    );
insert into sequences values(  5, 2,4     );
insert into sequences values(  5, 3,3     );
insert into sequences values(  5, 4,17    );
insert into sequences values(  5, 5,17    );
insert into sequences values(  5, 6,25    );
insert into sequences values(  5, 7,377   );
insert into sequences values(  5, 8,456   );
insert into sequences values(  5, 9,25    );

nдолжен быть последовательным, без дубликатов, без пробелов. Теперь присоединяйтесь к общим значениям и используйте тот факт, что последовательности являются последовательными :-)

with searched (n,val) as (
  VALUES
   ( 1,3  ),
   ( 2,17 ),
   ( 3,25 ),
   ( 4,377)
)
select seq.id, 
   -- this will return the same result if the values from both tables are in the same order
   -- it's a meaningless dummy, but the same meaningless value for sequential rows 
   seq.n - s.n as dummy,
   seq.val,
   seq.n,
   s.n 
from sequences as seq join searched as s
on seq.val = s.val
order by seq.id, dummy, seq.n;

Наконец, подсчитайте количество строк с одинаковой фиктивной переменной и проверьте, правильное ли это число:

with searched (n,val) as (
  VALUES
   ( 1,3  ),
   ( 2,17 ),
   ( 3,25 ),
   ( 4,377)
)
select distinct seq.id
from sequences as seq join searched as s
on seq.val = s.val
group by 
   seq.id,
   seq.n - s.n
having count(*) = (select count(*) from searched)
;

Попробуйте указатель на последовательности (val, id, n).

dnoeth
источник
Я также рассмотрел это решение позже. Но я вижу несколько проблем, которые кажутся довольно надоедливыми: во-первых, я боюсь, что это решение очень неэффективно, мы должны привести каждый массив каждой строки перед созданием шаблона поиска. Можно рассмотреть сохранение последовательностей в TEXTполе ( varcharна мой взгляд, это плохая идея, последовательности могут быть длинными, так как числа, поэтому размер довольно непредсказуем), чтобы избежать приведения; но по-прежнему невозможно использовать индексы для улучшения производительности (более того, использование строкового поля не кажется разумным, см. комментарий @CraigRinger выше).
mlpo
@mlpo: Каковы ваши ожидания производительности? Чтобы иметь возможность использовать индекс, вы должны нормализовать последовательность в одну строку для каждого значения, применить реляционное деление и, наконец, проверить правильность порядка. В вашем примере 25существует дважды в id=4, это на самом деле возможно? Сколько совпадений существует в среднем / максимальном для искомой последовательности?
dnoeth
Последовательность может содержать несколько раз одно и то же число. Например {1, 1, 1, 1, 12, 2, 2, 12, 12, 1, 1, 5, 4}вполне возможно. Что касается количества совпадений, используемые подпоследовательности обычно считаются ограничивающими количество результатов. Однако некоторые последовательности очень похожи, и иногда может быть интересно использовать подпоследовательность, более короткую, чтобы получить больше результатов. Я предполагаю, что число совпадений для большинства случаев составляет от 0 до 100. Всегда существует вероятность того, что подпоследовательность иногда совпадает с большим количеством последовательностей, когда она короткая или очень распространенная.
mlpo
@mlpo: я добавил решение, основанное на множествах, и мне было бы очень интересно сравнить производительность :-)
dnoeth
@ypercube: Это было просто быстро добавить , чтобы вернуть более значимый результат :-) Ok, это ужасно, я изменю it.l
dnoeth