PostgreSQL - Bekerja dengan berbagai elemen

8

Saya mencari untuk memilih baris berdasarkan pada apakah kolom terkandung dalam daftar besar nilai yang saya berikan sebagai array integer.

Inilah pertanyaan yang saat ini saya gunakan:

SELECT item_id, other_stuff, ...
FROM (
    SELECT
        -- Partitioned row number as we only want N rows per id
        ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY start_date) AS r,
        item_id, other_stuff, ...
    FROM mytable
    WHERE
        item_id = ANY ($1) -- Integer array
        AND end_date > $2
    ORDER BY item_id ASC, start_date ASC, allowed ASC
) x
WHERE x.r <= 12

Tabel disusun sedemikian rupa:

    Column     |            Type             | Collation | Nullable | Default 
---------------+-----------------------------+-----------+----------+---------
 item_id       | integer                     |           | not null | 
 allowed       | boolean                     |           | not null | 
 start_date    | timestamp without time zone |           | not null | 
 end_date      | timestamp without time zone |           | not null | 
 ...


 Indexes:
    "idx_dtr_query" btree (item_id, start_date, allowed, end_date)
    ...

Saya datang dengan indeks ini setelah mencoba yang berbeda dan menjalankan EXPLAINkueri. Yang ini adalah yang paling efisien untuk query dan sortasi. Berikut ini penjelasan analisis kueri:

Subquery Scan on x  (cost=0.56..368945.41 rows=302230 width=73) (actual time=0.021..276.476 rows=168395 loops=1)
  Filter: (x.r <= 12)
  Rows Removed by Filter: 90275
  ->  WindowAgg  (cost=0.56..357611.80 rows=906689 width=73) (actual time=0.019..248.267 rows=258670 loops=1)
        ->  Index Scan using idx_dtr_query on mytable  (cost=0.56..339478.02 rows=906689 width=73) (actual time=0.013..130.362 rows=258670 loops=1)
              Index Cond: ((item_id = ANY ('{/* 15,000 integers */}'::integer[])) AND (end_date > '2018-03-30 12:08:00'::timestamp without time zone))
Planning time: 30.349 ms
Execution time: 284.619 ms

Masalahnya adalah bahwa array int dapat berisi hingga 15.000 elemen atau lebih dan permintaannya cukup lambat dalam kasus ini (sekitar 800 ms pada laptop saya, Dell XPS baru-baru ini).

Saya pikir melewati array int sebagai parameter bisa jadi lambat, dan mengingat daftar id dapat disimpan sebelumnya dalam database saya mencoba melakukan ini. Saya menyimpannya dalam array di tabel lain dan digunakan item_id = ANY (SELECT UNNEST(item_ids) FROM ...), yang lebih lambat dari pendekatan saya saat ini. Saya juga mencoba menyimpannya baris demi baris dan menggunakan item_id IN (SELECT item_id FROM ...), yang bahkan lebih lambat, bahkan dengan hanya baris yang relevan dengan test case saya di tabel.

Apakah ada cara yang lebih baik untuk melakukan ini?

Pembaruan: mengikuti komentar Evan , pendekatan lain yang saya coba: setiap item adalah bagian dari beberapa grup, jadi alih-alih meneruskan id item grup, saya mencoba menambahkan id grup di mytable:

    Column     |            Type             | Collation | Nullable | Default 
---------------+-----------------------------+-----------+----------+---------
 item_id       | integer                     |           | not null | 
 allowed       | boolean                     |           | not null | 
 start_date    | timestamp without time zone |           | not null | 
 end_date      | timestamp without time zone |           | not null | 
 group_ids     | integer[]                   |           | not null | 
 ...

 Indexes:
    "idx_dtr_query" btree (item_id, start_date, allowed, end_date)
    "idx_dtr_group_ids" gin (group_ids)
    ...

Kueri baru ($ 1 adalah id grup yang ditargetkan):

SELECT item_id, other_stuff, ...
FROM (
    SELECT
        -- Partitioned row number as we only want N rows per id
        ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY start_date) AS r,
        item_id, other_stuff, ...
    FROM mytable
    WHERE
        $1 = ANY (group_ids)
        AND end_date > $2
    ORDER BY item_id ASC, start_date ASC, allowed ASC
) x
WHERE x.r <= 12

Jelaskan analisis:

Subquery Scan on x  (cost=123356.60..137112.58 rows=131009 width=74) (actual time=811.337..1087.880 rows=172023 loops=1)
  Filter: (x.r <= 12)
  Rows Removed by Filter: 219726
  ->  WindowAgg  (cost=123356.60..132199.73 rows=393028 width=74) (actual time=811.330..1040.121 rows=391749 loops=1)
        ->  Sort  (cost=123356.60..124339.17 rows=393028 width=74) (actual time=811.311..868.127 rows=391749 loops=1)
              Sort Key: item_id, start_date, allowed
              Sort Method: external sort  Disk: 29176kB
              ->  Seq Scan on mytable (cost=0.00..69370.90 rows=393028 width=74) (actual time=0.105..464.126 rows=391749 loops=1)
                    Filter: ((end_date > '2018-04-06 12:00:00'::timestamp without time zone) AND (2928 = ANY (group_ids)))
                    Rows Removed by Filter: 1482567
Planning time: 0.756 ms
Execution time: 1098.348 ms

Mungkin ada ruang untuk perbaikan dengan indeks tetapi saya mengalami kesulitan memahami bagaimana postgres menggunakannya, jadi saya tidak yakin apa yang harus diubah.

Jukurrpa
sumber
Berapa banyak baris dalam "mytable"? Berapa banyak nilai "item_id" yang berbeda di sana?
Nick
Juga, bukankah seharusnya Anda memiliki kendala keunikan (mungkin indeks unik yang belum didefinisikan) di pada item_id di mytable? ... Diedit: oh, saya melihat "PARTISI OLEH item_id", jadi pertanyaan ini berubah menjadi "Apa kunci alami dan nyata untuk data Anda? Apa yang harus membentuk indeks unik di sana?"
Nick
Sekitar 12 juta baris mytable, dengan sekitar 500 ribu berbeda item_id. Tidak ada kunci unik alami yang nyata untuk tabel ini, data yang dihasilkan secara otomatis untuk acara yang berulang. Saya kira item_id+ start_date+ name(bidang tidak ditampilkan di sini) dapat merupakan semacam kunci.
Jukurrpa
Bisakah Anda memposting rencana eksekusi yang Anda dapatkan?
Colin 't Hart
Tentu, tambahkan penjelasan analisis pada pertanyaan.
Jukurrpa

Jawaban:

1

Apakah ada cara yang lebih baik untuk melakukan ini?

Ya, gunakan tabel temp. Tidak ada yang salah dengan membuat tabel temp diindeks ketika kueri Anda gila.

BEGIN;
  CREATE TEMP TABLE myitems ( item_id int PRIMARY KEY );
  INSERT INTO myitems(item_id) VALUES (1), (2); -- and on and on
  CREATE INDEX ON myitems(item_id);
COMMIT;

ANALYZE myitems;

SELECT item_id, other_stuff, ...
FROM (
  SELECT
      -- Partitioned row number as we only want N rows per id
      ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY start_date) AS r,
      item_id, other_stuff, ...
  FROM mytable
  INNER JOIN myitems USING (item_id)
  WHERE end_date > $2
  ORDER BY item_id ASC, start_date ASC, allowed ASC
) x
WHERE x.r <= 12;

Tetapi bahkan lebih baik dari itu ...

"500k berbeda item_id" ... "array int dapat berisi hingga 15.000 elemen"

Anda memilih 3% dari basis data Anda satu per satu. Saya harus bertanya-tanya apakah Anda lebih baik membuat grup / tag dll dalam skema itu sendiri. Saya tidak pernah secara pribadi harus mengirim 15.000 ID berbeda ke dalam kueri.

Evan Carroll
sumber
Hanya mencoba menggunakan tabel sementara dan lebih lambat, setidaknya dalam kasus 15.000 id. Sedangkan untuk membuat grup dalam skema itu sendiri maksud Anda tabel dengan id yang saya berikan sebagai argumen? Saya mencoba sesuatu seperti ini tetapi kinerjanya mirip atau lebih buruk daripada pendekatan saya saat ini. Saya akan memperbarui pertanyaan dengan lebih detail
Jukurrpa
Tidak, maksudku. Jika Anda memiliki 15.000 id biasanya Anda menyimpan sesuatu di ID, seperti apakah item tersebut adalah produk dapur, dan bukannya menyimpan group_id yang sesuai dengan "produk dapur", Anda mencoba mencari semua produk dapur oleh id mereka. (yang buruk untuk setiap alasan) Apa yang diwakili oleh 15.000 id itu? Mengapa tidak disimpan di baris itu sendiri?
Evan Carroll
Setiap item milik beberapa grup (biasanya 15-20 di antaranya), jadi saya mencoba menyimpannya sebagai int array di mytable tetapi tidak dapat menemukan cara untuk mengindeks ini dengan benar. Saya memperbarui pertanyaan dengan semua detailnya.
Jukurrpa