Tamsayı dizisinin belirli bir alt diziyi içerdiği satırları bulma


9

Sorun

Not: PostgreSQL'in dizi mekanizmasına değil, matematiksel dizilere atıfta bulunuyorum .

Tamsayı dizilerini temsil eden bir tablo var. Tanım:

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)
);

Amacım belirli bir alt diziyi kullanarak satırları bulmak. Yani, sequencealanın verilen alt diziyi içeren bir dizi olduğu satırlar (benim durumumda, dizi sıralanır).

Misal

Tablonun aşağıdaki verileri içerdiğini varsayalım:

+----+-------+------------+-------------------------------+
| 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}     |
+----+-------+------------+-------------------------------+

Yani verilen {12, 742, 225, 547}alt çizgi ise 2. satırı bulmak istiyorum.

Benzer şekilde, verilen {3, 17, 25, 377}alt çizgi ise , 1. satırı ve 4. satırı bulmak istiyorum.

Son olarak, verilen alt dizge ise {12, 4, 3, 25, 377}, döndürülen satır yoktur.

Araştırmalar

İlk olarak, bir dizi veri türü ile dizileri temsil tamamen akıllıca emin değilim. Bu duruma uygun görünse de; Korkarım daha karmaşık bir kullanım sağlar. Belki de dizileri başka bir tabloyla ilişki modeli kullanarak farklı şekilde temsil etmek daha iyidir.

Aynı şekilde, unnestdizi işlevini kullanarak dizileri genişletmek ve sonra arama ölçütlerimi eklemek düşünüyorum . Bununla birlikte, dizideki değişken olan terimlerin sayısı, bunun nasıl yapılacağını görmüyorum.

İntarray modülünün subarrayişlevini kullanarak sırayla dizimi kesmenin mümkün olduğunu biliyorum, ancak arama için bana nasıl fayda sağladığını göremiyorum.

Kısıtlamalar

Şu anda modelim hala geliştirilse bile, tablonun 50.000 ila 300.000 sıra arasında birçok diziden oluşması amaçlanmıştır. Bu yüzden güçlü bir performans kısıtlamam var.

Örneğimde nispeten küçük tamsayılar kullandım. Pratikte, bu tamsayıların taşmasına kadar çok daha büyük olması mümkündür bigint. Böyle bir durumda, en iyisinin sayıları dizeler olarak saklamak olduğunu düşünüyorum (çünkü bu matematiksel işlem dizilerini gerçekleştirmek gerekli değildir). Bununla birlikte, bu çözümü seçerek , yukarıda belirtilen intarray modülünün kullanılmasını imkansız hale getirir .


Taşabilirlerse , saklamak için tür olarak bigintkullanmalısınız numeric. Çok daha yavaş ve daha fazla yer kaplıyor.
Craig Ringer

@CraigRinger Neden numericbir dize kullanmıyoruz ( textörneğin)? Dizilerim üzerinde matematiksel işlemler yapmam gerekmiyor.
mlpo

2
Çünkü daha kompakt ve birçok yönden daha hızlı textve sahte sayısal olmayan verileri depolamanızı engelliyor. Bağımlıdır, yalnızca G / Ç yapıyorsanız metnin G / Ç işlemlerini azaltmasını isteyebilirsiniz.
Craig Ringer

@CraigRinger Gerçekten, tür daha tutarlıdır. Performansla ilgili olarak, aramamı yapmanın bir yolunu bulduğumu test edeceğim.
mlpo

2
@CraigRinger Sipariş önemli değilse işe yarayabilir. Ancak burada diziler sıralanmıştır. Örnek: SELECT ARRAY[12, 4, 3, 17, 25, 377, 456, 25] @> ARRAY[12, 4, 3, 25, 377];sipariş bu operatör tarafından dikkate alınmadığından true değerini döndürür.
mlpo

Yanıtlar:


3

Dnoeth'in cevabında önemli performans geliştirmeleri arıyorsanız , yerel bir C fonksiyonu kullanmayı ve uygun operatörü oluşturmayı düşünün.

İşte int4 dizileri için bir örnek. ( Genel dizi değişkeni ve karşılık gelen SQL betiği ).

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
);

Şimdi böyle satırları filtreleyebilirsiniz.

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

Bu çözümün ne kadar hızlı olduğunu bulmak için küçük bir deney yaptım.

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"

Yani, yaklaşık 16 kat daha hızlı. Yeterli değilse, GIN veya GiST dizinleri için destek ekleyebilirsiniz, ancak bu çok daha zor bir görev olacaktır.


Kulağa ilginç numericgelebilir , ancak taşma olabilir çünkü benim veri temsil etmek için dizeleri veya türü kullanın bigint. Yanıtınızı, sorunun kısıtlamalarına uyacak şekilde düzenlemek iyi olabilir. Her neyse, burada paylaşacağım karşılaştırmalı bir performans yapacağım.
mlpo

Minimal ve doğrulanabilir olması gerektiği için büyük kod bloklarını cevaplara yapıştırmanın iyi bir uygulama olup olmadığından emin değilim. Bu işlevin genel bir dizi sürümü dört kat daha uzun ve oldukça hantaldır. Ayrıca test ettim numericve textve dizilerin uzunluğuna bağlı olarak iyileştirme 20 ila 50 kat arasında değişti.
Slonopotamus

Evet, ancak cevapların soruları cevaplaması gerekir :-). Bana öyle geliyor ki, kısıtlamalara uygun bir cevap ilginçtir (çünkü bu husus sorunun bir parçasıdır). Bununla birlikte, genel bir versiyon önermek gerekli olmayabilir. Sadece dizeleri olan bir sürümü veya numeric.
mlpo

Her neyse, genel diziler için sürümü ekledim, çünkü değişken uzunluktaki veri türleri için neredeyse aynı olurdu. Ancak gerçekten performans konusunda endişeleriniz varsa, gibi sabit boyutlu veri türlerine bağlı kalmalısınız bigint.
Slonopotamus

Bunu yapmayı çok isterim. Sorun şu ki, bazı dizilerim çok ötesine taştı bigint, bu yüzden başka seçeneğim yok gibi görünüyor. Ama bir fikriniz varsa, ilgileniyorum :).
mlpo

1

Dizileri dizelere yayınladığınızda ve kıvırcık parantezleri virgülle değiştirdiğinizde sonraki diziyi kolayca bulabilirsiniz:

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

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

Aradığınız dizi için de aynısını yapın ve bir satır aralığı ve sonrakini ekleyin %:

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

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

Şimdi kullanarak karşılaştırın LIKE:

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

Düzenle:

Fiddle tekrar çalışıyor.

Diziler değer başına bir satıra normalleştirilmişse, set tabanlı mantık uygulayabilirsiniz:

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    );

nsıralı olmalı, kopya olmamalı, boşluk olmamalıdır. Şimdi ortak değerlere katılın ve dizilerin ardışık olduğu gerçeğini kullanı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;

Son olarak aynı mankenle satır sayısını sayın ve doğru sayı olup olmadığını kontrol edin:

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)
;

Diziler üzerinde bir dizin deneyin (val, id, n).


Daha sonra bu çözümü de düşündüm. Ama oldukça rahatsız edici görünen birkaç sorun görüyorum: her şeyden önce bu çözümün çok verimsiz olduğundan korkuyorum, bir arama deseni yapmadan önce her satırın her dizisini dökmeliyiz. Dizileri bir TEXTalanda depolamayı düşünmek mümkündür ( varcharbence kötü bir fikirdir, diziler sayılar kadar uzun olabilir, bu nedenle boyut oldukça tahmin edilemez); ancak performansları iyileştirmek için dizinleri kullanmak hala mümkün değildir (ayrıca bir dize alanı kullanmak mutlaka mantıklı görünmüyor, yukarıdaki @CraigRinger açıklamasına bakın).
mlpo

@mlpo: Performans beklentiniz nedir? Bir dizini kullanabilmek için diziyi değer başına bir satıra normalleştirmeli, bir İlişkisel Bölüm uygulamalı ve son olarak siparişin doğru olup olmadığını kontrol etmelisiniz. Senin örneğin yılında 25iki kez in var id=4, bu aslında mümkün mü? Aranan bir sıralama için ortalama / maksimum kaç eşleşme var?
dnoeth

Bir sekans aynı sayının birkaç katını içerebilir. Örneğin {1, 1, 1, 1, 12, 2, 2, 12, 12, 1, 1, 5, 4}oldukça mümkündür. Eşleşme sayısına ilişkin olarak, kullanılan alt dizilerin normalde sonuç sayısını sınırladığı düşünülmektedir. Bununla birlikte, bazı diziler çok benzerdir ve bazen daha fazla sonuç almak için daha kısa bir alt diziyi kullanmak ilginç olabilir. Vakaların çoğunluğu için eşleşme sayısının 0 ile 100 arasında olduğunu tahmin ediyorum. Her zaman kısa süreli veya çok yaygın olduğunda dizinin çok sayıda diziyle eşleşme olasılığı her zaman.
mlpo

@mlpo: Set tabanlı bir çözüm ekledim ve bazı performans karşılaştırmalarıyla çok ilgilenirim :-)
dnoeth

@ypercube: Bu daha anlamlı bir sonuç döndürmek için hızlı bir ekleme oldu :-) Tamam, korkunç, değiştireceğim. l
dnoeth
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.