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


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 , упомянутого выше.


Если они могут переполниться, 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, но это будет гораздо более сложной задачей.


Звучит интересно, но я использую либо строки, либо тип 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).


Я также рассмотрел это решение позже. Но я вижу несколько проблем, которые кажутся довольно надоедливыми: во-первых, я боюсь, что это решение очень неэффективно, мы должны привести каждый массив каждой строки перед созданием шаблона поиска. Можно рассмотреть сохранение последовательностей в 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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.