SQL HowTo: kuandika kitanzi cha muda moja kwa moja kwenye hoja, au "Hatua tatu za Msingi"

Mara kwa mara, kazi ya kutafuta data inayohusiana kwa kutumia seti ya funguo hutokea. hadi tupate jumla ya rekodi zinazohitajika.

Mfano wa "maisha halisi" zaidi ni kuonyesha Matatizo 20 ya zamani zaidi, waliotajwa kwenye orodha ya wafanyakazi (kwa mfano, ndani ya mgawanyiko mmoja). Kwa "dashibodi" mbali mbali za usimamizi zilizo na muhtasari mfupi wa maeneo ya kazi, mada kama hiyo inahitajika mara nyingi.

SQL HowTo: kuandika kitanzi cha muda moja kwa moja kwenye hoja, au "Hatua tatu za Msingi"

Katika nakala hii tutaangalia utekelezaji katika PostgreSQL wa suluhisho la "kutojua" kwa shida kama hiyo, algorithm "ya busara" na ngumu sana. "kitanzi" katika SQL na hali ya kutoka kutoka kwa data iliyopatikana, ambayo inaweza kuwa na manufaa kwa maendeleo ya jumla na kwa matumizi katika kesi nyingine zinazofanana.

Wacha tuchukue seti ya data ya jaribio kutoka makala iliyopita. Ili kuzuia rekodi zilizoonyeshwa kutoka "kuruka" mara kwa mara wakati maadili yaliyopangwa yanafanana, panua faharasa ya mada kwa kuongeza ufunguo msingi. Wakati huo huo, hii itaipa upekee mara moja na kutuhakikishia kuwa mpangilio wa kupanga hauna utata:

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

Kama inavyosikika, ndivyo ilivyoandikwa

Kwanza, hebu tuchore toleo rahisi zaidi la ombi, kupitisha vitambulisho vya watendaji safu kama kigezo cha kuingiza:

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: kuandika kitanzi cha muda moja kwa moja kwenye hoja, au "Hatua tatu za Msingi"
[tazama kwenye explain.tensor.ru]

Inasikitisha kidogo - tuliagiza rekodi 20 pekee, lakini Index Scan iliturudishia Mistari 960, ambayo basi pia ilipaswa kupangwa ... Hebu tujaribu kusoma kidogo.

unnest + ARRAY

Jambo la kwanza ambalo litatusaidia ni ikiwa tunahitaji 20 tu zilizopangwa rekodi, basi soma tu si zaidi ya 20 zilizopangwa kwa mpangilio sawa kwa kila moja ufunguo. Nzuri, index inayofaa (kitambulisho_cha_mmiliki, tarehe_ya_kazi, kitambulisho) tunayo.

Wacha tutumie utaratibu huo huo wa kutoa na "kueneza kwenye safu wima" rekodi muhimu ya jedwali, kama katika makala ya mwisho. Tunaweza pia kutumia kukunja kwenye safu kwa kutumia chaguo la kukokotoa 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: kuandika kitanzi cha muda moja kwa moja kwenye hoja, au "Hatua tatu za Msingi"
[tazama kwenye explain.tensor.ru]

Oh, bora zaidi tayari! 40% kasi na 4.5 mara chini ya data Ilibidi niisome.

Uboreshaji wa rekodi za jedwali kupitia CTEHebu nitoe mawazo yako kwa ukweli kwamba katika baadhi ya kesi Jaribio la kufanya kazi mara moja na nyanja za rekodi baada ya kuitafuta katika subquery, bila "kuifunga" kwenye CTE, inaweza kusababisha "zidisha" InitPlan sawia na idadi ya nyanja hizi:

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

Rekodi sawa "iliangaliwa" mara 4 ... Hadi PostgreSQL 11, tabia hii hutokea mara kwa mara, na suluhisho ni "kuifunga" katika CTE, ambayo ni kikomo kabisa cha optimizer katika matoleo haya.

Kikusanyaji cha kujirudia

Katika toleo la awali, kwa jumla tunasoma Mistari 200 kwa ajili ya 20 zinazohitajika. Sio 960, lakini hata kidogo - inawezekana?

Hebu tujaribu kutumia ujuzi tunaohitaji jumla ya 20 kumbukumbu. Hiyo ni, tutarudia usomaji wa data tu hadi tufikie kiwango tunachohitaji.

Hatua ya 1: Orodha ya Kuanzia

Bila shaka, orodha yetu ya "lengo" ya rekodi 20 inapaswa kuanza na rekodi za "kwanza" za mojawapo ya vitufe vyetu vya owner_id. Kwa hivyo, kwanza tutapata vile "kwanza kabisa" kwa kila funguo na uiongeze kwenye orodha, ukiipanga kwa utaratibu tunaotaka - (task_date, id).

SQL HowTo: kuandika kitanzi cha muda moja kwa moja kwenye hoja, au "Hatua tatu za Msingi"

Hatua ya 2: Tafuta maingizo "yajayo".

Sasa ikiwa tutachukua ingizo la kwanza kutoka kwenye orodha yetu na kuanza "hatua" zaidi kwenye kielezo kuhifadhi ufunguo wa mmiliki_id, basi rekodi zote zilizopatikana ndizo zinazofuata katika uteuzi unaotokana. Bila shaka, tu mpaka tunavuka ufunguo wa kitako ingizo la pili kwenye orodha.

Ikiwa inageuka kuwa "tulivuka" rekodi ya pili, basi ingizo la mwisho lililosomwa linapaswa kuongezwa kwenye orodha badala ya la kwanza (pamoja na kitambulisho sawa cha mmiliki), baada ya hapo tunapanga upya orodha tena.

SQL HowTo: kuandika kitanzi cha muda moja kwa moja kwenye hoja, au "Hatua tatu za Msingi"

Hiyo ni, kila wakati tunapata kuwa orodha haina zaidi ya kiingilio kimoja kwa kila funguo (ikiwa maingizo yataisha na hatu "kuvuka", basi ingizo la kwanza kutoka kwenye orodha litatoweka tu na hakuna kitakachoongezwa. ), na wao daima zimepangwa kwa mpangilio wa kupanda wa ufunguo wa programu (task_tarehe, kitambulisho).

SQL HowTo: kuandika kitanzi cha muda moja kwa moja kwenye hoja, au "Hatua tatu za Msingi"

Hatua ya 3: chujio na "kupanua" rekodi

Katika baadhi ya safu mlalo za uteuzi wetu unaorudiwa, baadhi ya rekodi rv zimerudiwa - kwanza tunapata kama vile "kuvuka mpaka wa ingizo la 2 la orodha", na kisha uibadilishe kama ya 1 kutoka kwenye orodha. Kwa hivyo tukio la kwanza linahitaji kuchujwa.

Swali la mwisho la kutisha

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: kuandika kitanzi cha muda moja kwa moja kwenye hoja, au "Hatua tatu za Msingi"
[tazama kwenye explain.tensor.ru]

Hivyo, sisi iliuza 50% ya data iliyosomwa kwa 20% ya muda wa utekelezaji. Hiyo ni, ikiwa una sababu za kuamini kuwa kusoma kunaweza kuchukua muda mrefu (kwa mfano, data mara nyingi haipo kwenye cache, na unapaswa kwenda kwenye diski kwa hiyo), basi kwa njia hii unaweza kutegemea kidogo kusoma. .

Kwa hali yoyote, wakati wa utekelezaji uligeuka kuwa bora kuliko chaguo la kwanza la "naive". Lakini ni chaguo gani kati ya hizi 3 za kutumia ni juu yako.

Chanzo: mapenzi.com

Kuongeza maoni