SQL HowTo: tulis while-loop langsung di kueri, atau "Elementary three-way"

Secara berkala, tugas mencari data terkait dengan sekumpulan kunci muncul, sampai kita mendapatkan jumlah total catatan yang diperlukan.

Contoh yang paling "nyata" adalah dengan menampilkan 20 masalah tertua, terdaftar dalam daftar karyawan (misalnya, dalam departemen yang sama). Untuk berbagai "dasbor" manajerial dengan ringkasan singkat bidang pekerjaan, topik serupa cukup sering diperlukan.

SQL HowTo: tulis while-loop langsung di kueri, atau "Elementary three-way"

Dalam artikel ini, kami akan mempertimbangkan implementasi PostgreSQL dari versi "naif" untuk memecahkan masalah seperti itu, algoritma yang "lebih cerdas" dan sangat kompleks "loop" di SQL dengan kondisi keluar dari data yang ditemukan, yang dapat berguna baik untuk pengembangan umum maupun untuk digunakan dalam kasus serupa lainnya.

Mari kita ambil kumpulan data pengujian artikel sebelumnya. Agar record keluaran tidak β€œmelompat” dari waktu ke waktu ketika nilai yang diurutkan cocok, perluas indeks subjek dengan menambahkan kunci utama. Pada saat yang sama, ini akan segera memberinya keunikan, dan menjamin keunikan urutannya:

CREATE INDEX ON task(owner_id, task_date, id);
-- Π° старый - ΡƒΠ΄Π°Π»ΠΈΠΌ
DROP INDEX task_owner_id_task_date_idx;

Sebagaimana yang didengar, demikianlah yang tertulis

Pertama, mari kita buat sketsa versi permintaan yang paling sederhana, dengan meneruskan ID pelakunya array sebagai masukan:

SELECT
  *
FROM
  task
WHERE
  owner_id = ANY('{1,2,4,8,16,32,64,128,256,512}'::integer[])
ORDER BY
  task_date, id
LIMIT 20;

SQL HowTo: tulis while-loop langsung di kueri, atau "Elementary three-way"
[lihat penjelasan.tensor.ru]

Agak menyedihkan - kami hanya memesan 20 catatan, dan Index Scan mengembalikan kami String 960, yang kemudian juga harus disortir... Dan mari kita coba membaca lebih sedikit.

tidak bersarang + ARRAY

Pertimbangan pertama yang akan membantu kita adalah jika kita membutuhkannya total 20 diurutkan catatan, cukup membaca tidak lebih dari 20 diurutkan dalam urutan yang sama untuk masing-masing kunci. Bagus, indeks yang sesuai (owner_id, task_date, id) yang kita punya.

Mari kita gunakan mekanisme yang sama untuk mengekstraksi dan "mengubah menjadi kolom" entri tabel integral, seperti dalam artikel terakhir. Dan juga menerapkan konvolusi ke array menggunakan fungsi tersebut ARRAY():

WITH T AS (
  SELECT
    unnest(ARRAY(
      SELECT
        t
      FROM
        task t
      WHERE
        owner_id = unnest
      ORDER BY
        task_date, id
      LIMIT 20 -- ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡ΠΈΠ²Π°Π΅ΠΌ Ρ‚ΡƒΡ‚...
    )) r
  FROM
    unnest('{1,2,4,8,16,32,64,128,256,512}'::integer[])
)
SELECT
  (r).*
FROM
  T
ORDER BY
  (r).task_date, (r).id
LIMIT 20; -- ... ΠΈ Ρ‚ΡƒΡ‚ - Ρ‚ΠΎΠΆΠ΅

SQL HowTo: tulis while-loop langsung di kueri, atau "Elementary three-way"
[lihat penjelasan.tensor.ru]

Oh, ini sudah jauh lebih baik! 40% lebih cepat dan data 4.5 kali lebih sedikit harus membaca.

Perwujudan catatan tabel melalui CTESaya akan mencatatnya dalam beberapa kasus upaya untuk segera bekerja dengan bidang catatan setelah mencarinya di subkueri, tanpa "membungkus" dalam CTE, dapat menyebabkan InitPlan "perkalian". sebanding dengan jumlah bidang yang sama:

SELECT
  ((
    SELECT
      t
    FROM
      task t
    WHERE
      owner_id = 1
    ORDER BY
      task_date, id
    LIMIT 1
  ).*);

Result  (cost=4.77..4.78 rows=1 width=16) (actual time=0.063..0.063 rows=1 loops=1)
  Buffers: shared hit=16
  InitPlan 1 (returns $0)
    ->  Limit  (cost=0.42..1.19 rows=1 width=48) (actual time=0.031..0.032 rows=1 loops=1)
          Buffers: shared hit=4
          ->  Index Scan using task_owner_id_task_date_id_idx on task t  (cost=0.42..387.57 rows=500 width=48) (actual time=0.030..0.030 rows=1 loops=1)
                Index Cond: (owner_id = 1)
                Buffers: shared hit=4
  InitPlan 2 (returns $1)
    ->  Limit  (cost=0.42..1.19 rows=1 width=48) (actual time=0.008..0.009 rows=1 loops=1)
          Buffers: shared hit=4
          ->  Index Scan using task_owner_id_task_date_id_idx on task t_1  (cost=0.42..387.57 rows=500 width=48) (actual time=0.008..0.008 rows=1 loops=1)
                Index Cond: (owner_id = 1)
                Buffers: shared hit=4
  InitPlan 3 (returns $2)
    ->  Limit  (cost=0.42..1.19 rows=1 width=48) (actual time=0.008..0.008 rows=1 loops=1)
          Buffers: shared hit=4
          ->  Index Scan using task_owner_id_task_date_id_idx on task t_2  (cost=0.42..387.57 rows=500 width=48) (actual time=0.008..0.008 rows=1 loops=1)
                Index Cond: (owner_id = 1)
                Buffers: shared hit=4"
  InitPlan 4 (returns $3)
    ->  Limit  (cost=0.42..1.19 rows=1 width=48) (actual time=0.009..0.009 rows=1 loops=1)
          Buffers: shared hit=4
          ->  Index Scan using task_owner_id_task_date_id_idx on task t_3  (cost=0.42..387.57 rows=500 width=48) (actual time=0.009..0.009 rows=1 loops=1)
                Index Cond: (owner_id = 1)
                Buffers: shared hit=4

Catatan yang sama telah β€œdicari” 4 kali… Hingga PostgreSQL 11, perilaku ini terjadi secara teratur, dan solusinya adalah dengan β€œmembungkus” dalam CTE, yang merupakan batas tanpa syarat untuk pengoptimal dalam versi ini.

akumulator rekursif

Di versi sebelumnya, secara total, kita membaca String 200 demi yang diperlukan 20. Sudah bukan 960, tapi lebih sedikit lagi - apakah mungkin?

Mari kita coba gunakan ilmu yang kita butuhkan total 20 catatan. Artinya, kita akan mengulangi pengurangan data hanya sampai jumlah yang kita perlukan tercapai.

Langkah 1: Mulai Daftar

Jelasnya, daftar 20 entri "target" kita harus dimulai dengan entri "pertama" untuk salah satu kunci owner_id kita. Oleh karena itu, pertama-tama kita temukan yang seperti itu "pertama" untuk masing-masing kunci dan memasukkannya ke dalam daftar, mengurutkannya sesuai urutan yang kita inginkan - (tanggal_tugas, id).

SQL HowTo: tulis while-loop langsung di kueri, atau "Elementary three-way"

Langkah 2: temukan catatan "berikutnya".

Sekarang jika kita mengambil entri pertama dari daftar kita dan memulai "langkah" lebih jauh ke bawah indeks dengan menyimpan kunci owner_id, maka semua rekaman yang ditemukan hanyalah rekaman berikutnya dalam pilihan yang dihasilkan. Tentu saja hanya sampai kita melewati kunci yang diterapkan entri kedua dalam daftar.

Jika ternyata kita β€œmelewati” entri kedua, maka entri yang terakhir dibaca harus ditambahkan ke daftar, bukan yang pertama (dengan owner_id yang sama), setelah itu daftar diurutkan kembali.

SQL HowTo: tulis while-loop langsung di kueri, atau "Elementary three-way"

Artinya, kita selalu mendapatkan bahwa daftar tersebut tidak memiliki lebih dari satu entri untuk masing-masing kunci (jika entri sudah habis, dan kita belum "menyeberang", maka entri pertama akan hilang begitu saja dari daftar dan tidak ada yang akan ditambahkan. ), dan mereka selalu diurutkan dalam urutan kunci aplikasi (tanggal_tugas, id).

SQL HowTo: tulis while-loop langsung di kueri, atau "Elementary three-way"

Langkah 3: Memfilter dan Memperluas Catatan

Di bagian baris pilihan rekursif kami, beberapa catatan rv diduplikasi - pertama kita temukan seperti "melintasi batas entri ke-2 dalam daftar", dan kemudian kita menggantinya sebagai yang pertama dari daftar. Jadi kejadian pertama harus disaring.

Permintaan terakhir yang buruk

WITH RECURSIVE T AS (
  -- #1 : заносим Π² список "ΠΏΠ΅Ρ€Π²Ρ‹Π΅" записи ΠΏΠΎ ΠΊΠ°ΠΆΠ΄ΠΎΠΌΡƒ ΠΈΠ· ΠΊΠ»ΡŽΡ‡Π΅ΠΉ Π½Π°Π±ΠΎΡ€Π°
  WITH wrap AS ( -- "ΠΌΠ°Ρ‚Π΅Ρ€ΠΈΠ°Π»ΠΈΠ·ΡƒΠ΅ΠΌ" record'Ρ‹, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΎΠ±Ρ€Π°Ρ‰Π΅Π½ΠΈΠ΅ ΠΊ полям Π½Π΅ Π²Ρ‹Π·Ρ‹Π²Π°Π»ΠΎ умноТСния InitPlan/SubPlan
    WITH T AS (
      SELECT
        (
          SELECT
            r
          FROM
            task r
          WHERE
            owner_id = unnest
          ORDER BY
            task_date, id
          LIMIT 1
        ) r
      FROM
        unnest('{1,2,4,8,16,32,64,128,256,512}'::integer[])
    )
    SELECT
      array_agg(r ORDER BY (r).task_date, (r).id) list -- сортируСм список Π² Π½ΡƒΠΆΠ½ΠΎΠΌ порядкС
    FROM
      T
  )
  SELECT
    list
  , list[1] rv
  , FALSE not_cross
  , 0 size
  FROM
    wrap
UNION ALL
  -- #2 : Π²Ρ‹Ρ‡ΠΈΡ‚Ρ‹Π²Π°Π΅ΠΌ записи 1-Π³ΠΎ ΠΏΠΎ порядку ΠΊΠ»ΡŽΡ‡Π°, ΠΏΠΎΠΊΠ° Π½Π΅ ΠΏΠ΅Ρ€Π΅ΡˆΠ°Π³Π½Π΅ΠΌ Ρ‡Π΅Ρ€Π΅Π· запись 2-Π³ΠΎ
  SELECT
    CASE
      -- Ссли Π½ΠΈΡ‡Π΅Π³ΠΎ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ΠΎ для ΠΊΠ»ΡŽΡ‡Π° 1-ΠΉ записи
      WHEN X._r IS NOT DISTINCT FROM NULL THEN
        T.list[2:] -- ΡƒΠ±ΠΈΡ€Π°Π΅ΠΌ Π΅Π΅ ΠΈΠ· списка
      -- Ссли ΠΌΡ‹ НЕ пСрСсСкли ΠΏΡ€ΠΈΠΊΠ»Π°Π΄Π½ΠΎΠΉ ΠΊΠ»ΡŽΡ‡ 2-ΠΉ записи
      WHEN X.not_cross THEN
        T.list -- просто протягиваСм Ρ‚ΠΎΡ‚ ΠΆΠ΅ список Π±Π΅Π· ΠΌΠΎΠ΄ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΉ
      -- Ссли Π² спискС ΡƒΠΆΠ΅ Π½Π΅Ρ‚ 2-ΠΉ записи
      WHEN T.list[2] IS NULL THEN
        -- просто Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ пустой список
        '{}'
      -- пСрСсортировываСм ΡΠ»ΠΎΠ²Π°Ρ€ΡŒ, убирая 1-ю запись ΠΈ добавляя послСднюю ΠΈΠ· Π½Π°ΠΉΠ΄Π΅Π½Π½Ρ‹Ρ…
      ELSE (
        SELECT
          coalesce(T.list[2] || array_agg(r ORDER BY (r).task_date, (r).id), '{}')
        FROM
          unnest(T.list[3:] || X._r) r
      )
    END
  , X._r
  , X.not_cross
  , T.size + X.not_cross::integer
  FROM
    T
  , LATERAL(
      WITH wrap AS ( -- "ΠΌΠ°Ρ‚Π΅Ρ€ΠΈΠ°Π»ΠΈΠ·ΡƒΠ΅ΠΌ" record
        SELECT
          CASE
            -- Ссли всС-Ρ‚Π°ΠΊΠΈ "ΠΏΠ΅Ρ€Π΅ΡˆΠ°Π³Π½ΡƒΠ»ΠΈ" Ρ‡Π΅Ρ€Π΅Π· 2-ю запись
            WHEN NOT T.not_cross
              -- Ρ‚ΠΎ нуТная запись - пСрвая ΠΈΠ· спписка
              THEN T.list[1]
            ELSE ( -- Ссли Π½Π΅ пСрСсСкли, Ρ‚ΠΎ ΠΊΠ»ΡŽΡ‡ остался ΠΊΠ°ΠΊ Π² ΠΏΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π΅ΠΉ записи - отталкиваСмся ΠΎΡ‚ Π½Π΅Π΅
              SELECT
                _r
              FROM
                task _r
              WHERE
                owner_id = (rv).owner_id AND
                (task_date, id) > ((rv).task_date, (rv).id)
              ORDER BY
                task_date, id
              LIMIT 1
            )
          END _r
      )
      SELECT
        _r
      , CASE
          -- Ссли 2-ΠΉ записи ΡƒΠΆΠ΅ Π½Π΅Ρ‚ Π² спискС, Π½ΠΎ ΠΌΡ‹ Ρ…ΠΎΡ‚ΡŒ Ρ‡Ρ‚ΠΎ-Ρ‚ΠΎ нашли
          WHEN list[2] IS NULL AND _r IS DISTINCT FROM NULL THEN
            TRUE
          ELSE -- Π½ΠΈΡ‡Π΅Π³ΠΎ Π½Π΅ нашли ΠΈΠ»ΠΈ "ΠΏΠ΅Ρ€Π΅ΡˆΠ°Π³Π½ΡƒΠ»ΠΈ"
            coalesce(((_r).task_date, (_r).id) < ((list[2]).task_date, (list[2]).id), FALSE)
        END not_cross
      FROM
        wrap
    ) X
  WHERE
    T.size < 20 AND -- ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡ΠΈΠ²Π°Π΅ΠΌ Ρ‚ΡƒΡ‚ количСство
    T.list IS DISTINCT FROM '{}' -- ΠΈΠ»ΠΈ ΠΏΠΎΠΊΠ° список Π½Π΅ кончился
)
-- #3 : "Ρ€Π°Π·Π²ΠΎΡ€Π°Ρ‡ΠΈΠ²Π°Π΅ΠΌ" записи - порядок Π³Π°Ρ€Π°Π½Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½ ΠΏΠΎ ΠΏΠΎΡΡ‚Ρ€ΠΎΠ΅Π½ΠΈΡŽ
SELECT
  (rv).*
FROM
  T
WHERE
  not_cross; -- Π±Π΅Ρ€Π΅ΠΌ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ "Π½Π΅ΠΏΠ΅Ρ€Π΅ΡΠ΅ΠΊΠ°ΡŽΡ‰ΠΈΠ΅" записи

SQL HowTo: tulis while-loop langsung di kueri, atau "Elementary three-way"
[lihat penjelasan.tensor.ru]

Jadi, kita memperdagangkan 50% pembacaan data untuk 20% waktu eksekusi. Artinya, jika Anda memiliki alasan untuk percaya bahwa pembacaan bisa memakan waktu lama (misalnya, data sering kali tidak ada dalam cache, dan Anda harus masuk ke disk untuk itu), maka dengan cara ini Anda dapat bergantung pada membaca lebih sedikit.

Bagaimanapun, waktu eksekusi ternyata lebih baik daripada opsi pertama yang "naif". Namun yang mana dari 3 opsi ini yang akan digunakan terserah Anda.

Sumber: www.habr.com

Tambah komentar