SQL HowTo: nulis daur ulang nalika langsung ing pitakon, utawa "Telung langkah dhasar"

Secara periodik, tugas nggoleki data sing gegandhengan nggunakake sekumpulan tombol muncul. nganti entuk jumlah rekaman sing dibutuhake.

Conto paling "urip nyata" kanggo nampilake 20 masalah paling tuwa, kadhaptar ing dhaptar karyawan (contone, ing siji divisi). Kanggo macem-macem "dashboard" manajemen kanthi ringkesan ringkes babagan wilayah kerja, topik sing padha dibutuhake asring.

SQL HowTo: nulis daur ulang nalika langsung ing pitakon, utawa "Telung langkah dhasar"

Ing artikel iki, kita bakal ndeleng implementasine ing PostgreSQL saka solusi "naif" kanggo masalah kasebut, algoritma "pinter" lan rumit banget. "loop" ing SQL kanthi kondisi metu saka data sing ditemokake, sing bisa migunani kanggo pangembangan umum lan digunakake ing kasus liyane sing padha.

Ayo dadi njupuk set data test saka artikel sadurunge. Kanggo nyegah cathetan sing ditampilake saka "mlumpat" saka wektu kanggo wektu nalika nilai diurutake pas, nggedhekake indeks subyek kanthi nambahake kunci utama. Ing wektu sing padha, iki bakal langsung menehi keunikan lan njamin manawa urutan ngurutake ora ambigu:

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

Kaya sing dirungokake, kaya sing ditulis

Pisanan, ayo gawe sketsa versi panjalukan sing paling gampang, ngliwati ID para pemain. array minangka parameter input:

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: nulis daur ulang nalika langsung ing pitakon, utawa "Telung langkah dhasar"
[deleng ing explain.tensor.ru]

A little sedih - kita mung dhawuh 20 cathetan, nanging Index Scan bali menyang kita 960 larik, sing banjur uga kudu diurutake ... Coba maca kurang.

unnest + ARRAY

Wawasan pisanan sing bakal mbantu kita yaiku yen kita butuh mung 20 diurutake cathetan, banjur mung maca ora luwih saka 20 diurutake ing urutan padha kanggo saben kuncine. apik, indeks cocok (id_pemilik, tanggal_tugas, id) kita duwe.

Ayo nggunakake mekanisme sing padha kanggo ngekstrak lan "nyebar menyang kolom" cathetan tabel integral, ing artikel pungkasan. Kita uga bisa ngetrapake lempitan menyang array nggunakake fungsi kasebut 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: nulis daur ulang nalika langsung ing pitakon, utawa "Telung langkah dhasar"
[deleng ing explain.tensor.ru]

Oh, wis luwih apik! 40% luwih cepet lan 4.5 kaping kurang data Aku kudu maca.

Materialisasi rekaman tabel liwat CTEAyo kula tarik manungsa waΓ© kanggo kasunyatan sing ing sawetara kasus Usaha kanggo langsung nggarap lapangan rekaman sawise nggoleki ing subquery, tanpa "bungkus" ing CTE, bisa nyebabake "multiply" InitPlan sebanding karo jumlah kolom sing padha:

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

Rekaman sing padha "digoleki" kaping 4 ... Nganti PostgreSQL 11, prilaku iki kedadeyan kanthi rutin, lan solusi kasebut yaiku "bungkus" ing CTE, sing minangka watesan mutlak kanggo pangoptimal ing versi kasebut.

Akumulator rekursif

Ing versi sadurunge, total kita maca 200 larik marga saka dibutuhake 20. Ora 960, nanging malah kurang - iku bisa?

Ayo padha nyoba nggunakake kawruh sing kita butuhake gunggunge 20 cathetan. Yaiku, kita bakal ngulang maca data mung nganti tekan jumlah sing dibutuhake.

Langkah 1: Daftar Miwiti

Temenan, dhaptar "target" saka 20 cathetan kudu diwiwiti karo cathetan "pisanan" kanggo salah sawijining kunci owner_id. Mulane, pisanan kita bakal nemokake kuwi "pisanan banget" kanggo saben tombol lan ditambahake menyang dhaptar, ngurutake ing urutan sing dikarepake - (task_date, id).

SQL HowTo: nulis daur ulang nalika langsung ing pitakon, utawa "Telung langkah dhasar"

Langkah 2: Temokake entri "sabanjure".

Saiki yen kita njupuk entri pisanan saka dhaftar kita lan miwiti "langkah" luwih ing sadawane indeks ngreksa tombol owner_id, banjur kabeh cathetan ketemu persis sing sabanjurΓ© ing pilihan asil. Mesthi, mung nganti kita nyabrang tombol bokong entri kapindho ing dhaftar.

Yen ternyata kita "nyabrang" rekaman kapindho, banjur entri pungkasan sing diwaca kudu ditambahake menyang dhaptar tinimbang sing pisanan (karo owner_id padha), sawise iku maneh ngurutake dhaftar maneh.

SQL HowTo: nulis daur ulang nalika langsung ing pitakon, utawa "Telung langkah dhasar"

Yaiku, kita mesthi ngerteni manawa dhaptar ora duwe luwih saka siji entri kanggo saben tombol (yen entri entek lan ora "nyebrang", mula entri pisanan saka dhaptar bakal ilang lan ora ana sing ditambahake. ), lan padha tansah diurutake ing urutan munggah tombol aplikasi (task_date, id).

SQL HowTo: nulis daur ulang nalika langsung ing pitakon, utawa "Telung langkah dhasar"

Langkah 3: Filter lan "ngembangake" cathetan

Ing sawetara larik saka pilihan rekursif kita, sawetara cathetan rv sing diduplikasi - pisanan kita temokake kayata "nyebrang tapel wates entri 2nd dhaftar", lan banjur sulih minangka 1st saka dhaftar. Dadi kedadeyan pisanan kudu disaring.

Pitakonan pungkasan sing nggegirisi

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: nulis daur ulang nalika langsung ing pitakon, utawa "Telung langkah dhasar"
[deleng ing explain.tensor.ru]

Mangkono, kita perdagangan 50% saka data diwaca kanggo 20% wektu eksekusi. Sing, yen sampeyan duwe alasan kanggo pracaya maca bisa njupuk wektu dawa (contone, data asring ora ing cache, lan sampeyan kudu pindhah menyang disk kanggo iku), banjur ing cara iki sampeyan bisa gumantung kurang ing maca. .

Ing kasus apa wae, wektu eksekusi dadi luwih apik tinimbang ing pilihan pisanan "naif". Nanging endi saka 3 pilihan iki kanggo sampeyan.

Source: www.habr.com

Add a comment