SQL HowTo: to'g'ridan-to'g'ri so'rovga yoki "Elementar uch tomonlama" vaqt oralig'ini yozing

Vaqti-vaqti bilan kalitlar to'plamidan foydalangan holda tegishli ma'lumotlarni qidirish vazifasi paydo bo'ladi. biz kerakli umumiy yozuvlar sonini olmaguncha.

Eng "haqiqiy hayot" misoli ko'rsatishdir 20 ta eng qadimgi muammolar, sanab o'tilgan xodimlar ro'yxatida (masalan, bitta bo'linma ichida). Ish sohalari haqida qisqacha ma'lumotga ega bo'lgan turli xil boshqaruv "boshqaruv paneli" uchun shunga o'xshash mavzu juda tez-tez talab qilinadi.

SQL HowTo: to'g'ridan-to'g'ri so'rovga yoki "Elementar uch tomonlama" vaqt oralig'ini yozing

Ushbu maqolada biz PostgreSQL-da bunday muammoning "sodda" yechimini, "aqlliroq" va juda murakkab algoritmni amalga oshirishni ko'rib chiqamiz. Topilgan ma'lumotlardan chiqish sharti bilan SQLda "loop", bu umumiy rivojlanish uchun ham, boshqa shunga o'xshash holatlarda foydalanish uchun ham foydali bo'lishi mumkin.

dan test ma'lumotlar to'plamini olaylik oldingi maqola. Vaqti-vaqti bilan tartiblangan qiymatlar bir-biriga to'g'ri kelganda ko'rsatilgan yozuvlarning "sakrashini" oldini olish uchun, asosiy kalitni qo'shish orqali mavzu indeksini kengaytiring. Shu bilan birga, bu darhol unga o'ziga xoslikni beradi va saralash tartibi aniq bo'lishini kafolatlaydi:

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

Qanday eshitilsa, shunday yoziladi

Birinchidan, ijrochilarning identifikatorlarini o'tkazib, so'rovning eng oddiy versiyasini chizamiz kirish parametri sifatida massiv:

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: to'g'ridan-to'g'ri so'rovga yoki "Elementar uch tomonlama" vaqt oralig'ini yozing
[express.tensor.ru saytiga qarang]

Biroz achinarli - biz atigi 20 ta yozuvga buyurtma berdik, ammo Index Scan uni bizga qaytarib berdi 960 qator, keyin ham saralanishi kerak edi... Keling, kamroq o'qishga harakat qilaylik.

unnest + ARRAY

Bizga yordam beradigan birinchi narsa, agar kerak bo'lsa faqat 20 ta saralangan yozuvlar, keyin shunchaki o'qing har biri uchun bir xil tartibda tartiblangan 20 tadan ko'p bo'lmagan kalit. Yaxshi, mos indeks (egasi_identifikatori, vazifa_sanasi, identifikatori) bizda mavjud.

Keling, olish va "ustunlarga yoyish" uchun xuddi shu mexanizmdan foydalanamiz integral jadval yozuvi, kabi oxirgi maqola. Funktsiyadan foydalanib, massivga katlamani ham qo'llashimiz mumkin 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: to'g'ridan-to'g'ri so'rovga yoki "Elementar uch tomonlama" vaqt oralig'ini yozing
[express.tensor.ru saytiga qarang]

Oh, allaqachon yaxshiroq! 40% tezroq va 4.5 baravar kam ma'lumot Men uni o'qishim kerak edi.

CTE orqali jadval yozuvlarini materiallashtirishE'tiboringizni shunga qarataman ba'zi hollarda Yozuvni pastki so'rovda qidirgandan so'ng, uni CTE-ga "o'ramasdan" darhol uning maydonlari bilan ishlashga urinish: InitPlanni "ko'paytirish" bir xil maydonlar soniga mutanosib:

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

Xuddi shu yozuv 4 marta "ko'zdan kechirildi" ... PostgreSQL 11gacha bu xatti-harakatlar muntazam ravishda sodir bo'ladi va yechim uni CTE-ga "o'rash" dir, bu esa ushbu versiyalarda optimallashtiruvchi uchun mutlaq chegaradir.

Rekursiv akkumulyator

Oldingi versiyada jami biz o'qiymiz 200 qator talab uchun 20. 960 emas, balki undan ham kamroq - bu mumkinmi?

Keling, kerakli bilimlardan foydalanishga harakat qilaylik jami xnumx yozuvlar. Ya'ni, biz kerakli miqdorga yetguncha ma'lumotlarni o'qishni takrorlaymiz.

1-qadam: Boshlang'ich ro'yxat

Shubhasiz, 20 ta yozuvdan iborat "maqsadli" ro'yxatimiz egasi_id kalitlarimizdan biri uchun "birinchi" yozuvlardan boshlanishi kerak. Shuning uchun, birinchi navbatda, biz buni topamiz Har bir kalit uchun "juda birinchi" va biz xohlagan tartibda saralab, ro'yxatga qo'shing - (task_date, id).

SQL HowTo: to'g'ridan-to'g'ri so'rovga yoki "Elementar uch tomonlama" vaqt oralig'ini yozing

2-qadam: "Keyingi" yozuvlarni toping

Endi ro'yxatimizdan birinchi yozuvni olib, boshlasak Indeks bo'ylab "qadam" owner_id kalitini saqlab qolgan holda, barcha topilgan yozuvlar natijada olingan tanlovda aynan keyingisi bo'ladi. Albatta, faqat biz dumba kalitini kesib o'tgunimizcha ro'yxatdagi ikkinchi yozuv.

Agar biz ikkinchi rekordni "kesib o'tganimiz" aniqlansa ro'yxatga birinchi o'rniga oxirgi o'qilgan yozuv qo'shilishi kerak (xuddi shu owner_id bilan), shundan so'ng biz ro'yxatni yana saralaymiz.

SQL HowTo: to'g'ridan-to'g'ri so'rovga yoki "Elementar uch tomonlama" vaqt oralig'ini yozing

Ya'ni, biz har doim ro'yxatda har bir tugma uchun bittadan ortiq yozuv mavjud emasligini bilib olamiz (agar yozuvlar tugasa va biz "kesmasa" ro'yxatdagi birinchi yozuv shunchaki yo'qoladi va hech narsa qo'shilmaydi. ), va ular har doim tartiblangan ilova kalitining o'sish tartibida (task_date, id).

SQL HowTo: to'g'ridan-to'g'ri so'rovga yoki "Elementar uch tomonlama" vaqt oralig'ini yozing

3-qadam: yozuvlarni filtrlang va "kengaytiring"

Bizning rekursiv tanlovimizning ba'zi qatorlarida, ba'zi yozuvlar rv takrorlanadi - avval biz "ro'yxatning 2-chi yozuvining chegarasini kesib o'tish" kabi narsalarni topamiz va keyin uni ro'yxatdagi 1-o'ringa almashtiramiz. Shunday qilib, birinchi hodisani filtrlash kerak.

Qo'rqinchli yakuniy so'rov

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: to'g'ridan-to'g'ri so'rovga yoki "Elementar uch tomonlama" vaqt oralig'ini yozing
[express.tensor.ru saytiga qarang]

Shunday qilib, biz bajarilish vaqtining 50% ​​uchun ma'lumotlarni o'qishning 20% ni sotdi. Ya'ni, agar siz o'qish uzoq vaqt talab qilishi mumkinligiga ishonish uchun asoslaringiz bo'lsa (masalan, ma'lumotlar ko'pincha keshda bo'lmaydi va buning uchun siz diskka kirishingiz kerak), unda siz o'qishga kamroq bog'lanishingiz mumkin. .

Qanday bo'lmasin, ijro muddati "sodda" birinchi variantga qaraganda yaxshiroq bo'ldi. Ammo ushbu 3 variantdan qaysi birini ishlatish sizga bog'liq.

Manba: www.habr.com

a Izoh qo'shish