Temukan baris di mana urutan bilangan bulat berisi urutan yang diberikan

9

Masalah

Catatan: Saya merujuk pada urutan matematis , bukan mekanisme urutan PostgreSQL .

Saya punya tabel yang mewakili urutan bilangan bulat. Definisi tersebut adalah:

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

Tujuan saya adalah menemukan baris menggunakan urutan yang diberikan. Dengan kata lain, baris di mana sequencebidang adalah urutan yang berisi urutan yang diberikan (dalam kasus saya, urutan tersebut dipesan).

Contoh

Misalkan tabel tersebut berisi data berikut:

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

Jadi jika yang diberikan berikutnya adalah {12, 742, 225, 547}, saya ingin mencari baris 2.

Demikian pula, jika urutan yang diberikan adalah {3, 17, 25, 377}, saya ingin mencari baris 1 dan baris 4.

Akhirnya, jika urutan yang diberikan adalah {12, 4, 3, 25, 377}, maka tidak ada baris yang dikembalikan.

Investigasi

Pertama, saya tidak sepenuhnya yakin bahwa merepresentasikan urutan dengan tipe data array adalah bijaksana. Meskipun ini sepertinya sesuai dengan situasi; Saya khawatir itu membuat penanganan lebih rumit. Mungkin lebih baik untuk mewakili urutan berbeda, menggunakan model hubungan dengan tabel lain.

Dengan cara yang sama, saya berpikir tentang memperluas urutan menggunakan unnestfungsi array dan kemudian menambahkan kriteria pencarian saya. Namun demikian, jumlah istilah dalam urutan menjadi variabel I tidak melihat bagaimana melakukan itu.

Saya tahu juga mungkin untuk memotong urutan saya di kemudian hari menggunakan subarrayfungsi modul intarray tapi saya tidak melihat bagaimana itu menguntungkan saya untuk pencarian saya.

Kendala

Bahkan jika saat ini model saya masih dikembangkan, tabel ini dimaksudkan untuk terdiri dari banyak urutan, antara 50.000 dan 300.000 baris. Jadi saya punya kendala kinerja yang kuat.

Dalam contoh saya, saya menggunakan bilangan bulat yang relatif kecil. Dalam praktiknya, dimungkinkan bahwa bilangan bulat ini menjadi jauh lebih besar, hingga meluap bigint. Dalam situasi seperti itu, saya pikir yang terbaik adalah menyimpan angka sebagai string (karena tidak perlu melakukan urutan operasi matematika ini). Namun, memilih solusi ini, ini membuat tidak mungkin untuk menggunakan modul intarray , yang disebutkan di atas.

mlpo
sumber
Jika mereka bisa meluap bigintAnda harus menggunakan numericsebagai tipe untuk menyimpannya. Ini jauh lebih lambat dan membutuhkan lebih banyak ruang.
Craig Ringer
@CraigRinger Mengapa menggunakan numericdan bukan string ( textmisalnya)? Saya tidak perlu melakukan operasi matematika pada urutan saya.
mlpo
2
Karena lebih ringkas dan dalam banyak hal lebih cepat daripada text, dan mencegah Anda menyimpan data non-numerik palsu. Tergantung, jika Anda hanya melakukan I / O, Anda mungkin ingin teks mengurangi pemrosesan I / O.
Craig Ringer
@CraigRinger Memang, tipe ini lebih konsisten. Mengenai kinerja, saya akan menguji ketika saya menemukan cara untuk melakukan pencarian saya.
mlpo
2
@CraigRinger Mungkin berhasil jika pesanan tidak menjadi masalah. Tapi di sini, urutannya dipesan. Contoh: SELECT ARRAY[12, 4, 3, 17, 25, 377, 456, 25] @> ARRAY[12, 4, 3, 25, 377];akan mengembalikan true, karena pesanan tidak dipertimbangkan oleh operator ini.
mlpo

Jawaban:

3

Jika Anda mencari peningkatan kinerja yang signifikan untuk jawaban dnoeth , pertimbangkan untuk menggunakan fungsi C asli dan membuat operator yang sesuai.

Berikut adalah contoh untuk array int4. ( Varian array generik dan skrip SQL yang sesuai ).

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

Sekarang Anda dapat memfilter baris seperti ini.

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

Saya telah melakukan percobaan kecil untuk menemukan seberapa cepat solusi ini.

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"

Jadi, ini sekitar 16 kali lebih cepat. Jika itu tidak cukup, Anda dapat menambahkan dukungan untuk indeks GIN atau GiST, tetapi ini akan menjadi tugas yang jauh lebih sulit.

Slonopotamus
sumber
Kedengarannya menarik, namun saya menggunakan string atau tipe numericuntuk mewakili data saya karena mungkin meluap bigint. Mungkin sebaiknya mengedit jawaban Anda agar sesuai dengan batasan-batasan pertanyaan. Bagaimanapun, saya akan melakukan kinerja komparatif yang akan saya posting di sini.
mlpo
Saya tidak yakin apakah ini merupakan praktik yang baik untuk menempelkan blok kode yang besar ke dalam jawaban karena dianggap minimal dan dapat diverifikasi. Versi array generik dari fungsi ini empat kali lebih lama dan cukup rumit. Saya juga telah mengujinya dengan numericdan textdan peningkatan berkisar antara 20 hingga 50 kali tergantung pada panjang array.
Slonopotamus
Ya, namun perlu bahwa jawaban menjawab pertanyaan :-). Di sini, menurut saya jawaban yang sesuai dengan batasan itu menarik (karena aspek ini adalah bagian dari pertanyaan). Meskipun demikian, mungkin tidak perlu mengusulkan versi generik. Hanya versi dengan string atau numeric.
mlpo
Lagi pula, saya menambahkan versi untuk array generik karena akan hampir sama untuk semua tipe data panjang variabel. Tetapi jika Anda benar-benar peduli dengan kinerja, Anda harus tetap dengan tipe data ukuran tetap seperti bigint.
Slonopotamus
Aku akan senang untuk melakukan itu. Masalahnya adalah beberapa urutan saya meluap jauh bigint, jadi sepertinya saya tidak punya pilihan. Tetapi jika Anda punya ide, saya tertarik :).
mlpo
1

Anda dapat dengan mudah menemukan bagian berikutnya ketika Anda melemparkan array ke string dan mengganti tanda kurung keriting dengan koma:

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

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

Lakukan hal yang sama untuk array yang Anda cari dan tambahkan yang memimpin dan mengikuti %:

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

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

Sekarang Anda membandingkannya menggunakan LIKE:

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

Edit:

Fiddle bekerja lagi.

Jika array dinormalisasi menjadi satu baris per nilai, Anda dapat menerapkan logika berbasis set:

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

nharus berurutan, tidak ada duplikat, tidak ada kesenjangan. Sekarang gabung dengan nilai-nilai umum dan manfaatkan fakta bahwa urutannya berurutan :-)

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;

Terakhir hitung jumlah baris dengan boneka yang sama dan periksa apakah itu nomor yang benar:

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

Coba indeks pada urutan (val, id, n).

dnoeth
sumber
Saya juga mempertimbangkan solusi ini sesudahnya. Tetapi saya melihat beberapa masalah yang tampaknya cukup menyusahkan: pertama-tama saya khawatir bahwa solusi ini sangat tidak efisien, kita harus melemparkan setiap larik setiap baris sebelum membuat pola pencarian. Dimungkinkan untuk mempertimbangkan menyimpan urutan dalam TEXTbidang ( varcharadalah ide yang buruk menurut saya, urutan bisa panjang, karena jumlahnya, sehingga ukurannya agak tidak dapat diprediksi), untuk menghindari pemain; tetapi masih tidak memungkinkan untuk menggunakan indeks untuk meningkatkan kinerja (selanjutnya menggunakan bidang string tampaknya tidak selalu bijaksana, lihat komentar @CraigRinger di atas).
mlpo
@mlpo: Apa harapan kinerja Anda? Untuk dapat menggunakan indeks, Anda harus menormalkan urutan menjadi satu baris per nilai, menerapkan Divisi Relasional dan akhirnya memeriksa apakah urutannya benar. Dalam contoh Anda 25ada dua kali id=4, apakah ini sebenarnya mungkin? Berapa banyak kecocokan yang ada dalam rata-rata / maksimum untuk urutan yang dicari?
dnoeth
Urutan mungkin berisi beberapa kali angka yang sama. Contohnya {1, 1, 1, 1, 12, 2, 2, 12, 12, 1, 1, 5, 4}sangat mungkin. Mengenai jumlah pertandingan, urutan yang digunakan biasanya dianggap membatasi jumlah hasil. Namun, beberapa urutan sangat mirip, dan kadang-kadang bisa menarik untuk menggunakan urutan yang lebih pendek untuk mendapatkan hasil lebih banyak. Saya memperkirakan bahwa jumlah kecocokan untuk sebagian besar kasus adalah antara 0 dan 100. Dengan selalu ada kemungkinan bahwa kadang-kadang kecocokan berikutnya dengan banyak urutan ketika pendek atau sangat umum.
mlpo
@mlpo: Saya menambahkan solusi berbasis set dan saya akan sangat tertarik dengan beberapa perbandingan kinerja :-)
dnoeth
@ ypercube: Ini hanyalah tambahan cepat untuk mengembalikan hasil yang lebih bermakna :-) Ok, ini mengerikan, saya akan mengubahnya.l
dnoeth