Знайдіть рядки, де ціла послідовність містить задану підряд


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.
Слонопотам

Я хотів би це зробити. Проблема полягає в тому, що деякі мої послідовності переповнюються далеко за межі 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: Це був лише швидкий додаток, щоб повернути більш значущий результат :-) Добре, це жахливо, я його зміню. l
dnoeth
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.