Cursor — satr-satr ishlash

~15 daq · MySQL · View, Stored Procedure, Trigger

Muammodan boshlaylik

SQL to'plamlar bilan ishlaydi: UPDATE mahsulotlar SET narx = narx * 1.1 — barcha qatorlar birdaniga. Bu tez va qulay.

Lekin ba'zan har bir qator uchun alohida, ketma-ket ish qilish kerak bo'ladi:

  • Har bir mijoz uchun shaxsiy hisobot jadvalini yaratish
  • Har bir qatorga oldingi qatorga bog'liq bo'lgan murakkab qoidani qo'llash
  • Har bir jadval uchun OPTIMIZE TABLE buyrug'ini dinamik bajarish

Stored procedure ichida natija to'plamini qatorma-qator aylanib chiqish uchun — cursor.

[!WARNING] Bu darsning eng muhim xulosasi oxirida: cursor — oxirgi chora. Deyarli har doim to'plamli (set-based) SQL ancha tez va sodda. Avval cursor qanday ishlashini, keyin nima uchun undan qochish kerakligini ko'ramiz.


Cursor ning to'rt bosqichi

DECLARE cur CURSOR FOR SELECT ...;   -- 1. E'lon qilish
OPEN cur;                             -- 2. Ochish (so'rov bajariladi)
FETCH cur INTO o'zgaruvchilar;        -- 3. Navbatdagi qatorni olish (siklda)
CLOSE cur;                            -- 4. Yopish

Qatorlar tugaganda FETCH "NOT FOUND" holatini qaytaradi. Uni ushlash uchun — handler.


To'liq misol

Har bir mijoz uchun sodiqlik darajasini hisoblab, alohida jadvalga yozamiz, har birini jurnalga qayd qilgan holda:

CREATE TABLE sodiqlik (
    mijoz_id  INT UNSIGNED PRIMARY KEY,
    daraja    VARCHAR(20) NOT NULL,
    hisoblangan DATETIME NOT NULL
);

DELIMITER //

CREATE PROCEDURE sodiqlik_hisobla()
BEGIN
    DECLARE v_tugadi  BOOLEAN DEFAULT FALSE;
    DECLARE v_mijoz   INT;
    DECLARE v_jami    BIGINT;
    DECLARE v_daraja  VARCHAR(20);

    -- 1. Cursor
    DECLARE cur CURSOR FOR
        SELECT m.id, COALESCE(SUM(q.soni * q.narx), 0)
        FROM mijozlar m
        LEFT JOIN buyurtmalar b        ON b.mijoz_id = m.id AND b.holat <> 'bekor'
        LEFT JOIN buyurtma_qatorlari q ON q.buyurtma_id = b.id
        GROUP BY m.id;

    -- Qatorlar tugaganda v_tugadi = TRUE
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_tugadi = TRUE;

    -- 2. Ochish
    OPEN cur;

    tsikl: LOOP
        -- 3. Navbatdagi qator
        FETCH cur INTO v_mijoz, v_jami;
        IF v_tugadi THEN
            LEAVE tsikl;
        END IF;

        SET v_daraja = CASE
            WHEN v_jami >= 20000000 THEN 'Oltin'
            WHEN v_jami >= 5000000  THEN 'Kumush'
            WHEN v_jami > 0         THEN 'Bronza'
            ELSE 'Yangi'
        END;

        INSERT INTO sodiqlik (mijoz_id, daraja, hisoblangan)
        VALUES (v_mijoz, v_daraja, NOW())
        AS y ON DUPLICATE KEY UPDATE daraja = y.daraja, hisoblangan = y.hisoblangan;
    END LOOP;

    -- 4. Yopish
    CLOSE cur;
END //

DELIMITER ;

CALL sodiqlik_hisobla();
SELECT * FROM sodiqlik;
+----------+--------+---------------------+
| mijoz_id | daraja | hisoblangan         |
+----------+--------+---------------------+
|        1 | Kumush | 2026-11-04 15:30:00 |
|        2 | Oltin  | 2026-11-04 15:30:00 |
|        3 | Yangi  | 2026-11-04 15:30:00 |
|        4 | Yangi  | 2026-11-04 15:30:00 |
+----------+--------+---------------------+

Muhim tafsilotlar

E'lon qilish tartibi qat'iy: avval o'zgaruvchilar, keyin cursor lar, keyin handler lar. Boshqa tartibda — sintaksis xatosi.

CONTINUE HANDLER FOR NOT FOUND — FETCH qator topa olmaganda xato emas, faqat bayroqni o'rnatadi va davom etadi. Siklda bayroqni tekshirib, LEAVE bilan chiqasiz.

tsikl: LOOP ... END LOOP — nomlangan sikl. LEAVE tsikl — undan chiqish (break), ITERATE tsikl — keyingi aylanishga o'tish (continue).

[!WARNING] Handler procedure ichidagi boshqa SELECT ... INTO lar uchun ham ishlaydi. Sikl ichida SELECT x INTO v FROM ... hech narsa topmasa — v_tugadi ham TRUE bo'lib qoladi va sikl erta tugaydi. Bunday holatda ichki SELECT dan oldin va keyin bayroqni tiklang yoki ichki so'rovni alohida BEGIN ... END blokiga, o'z handler i bilan oling.

MySQL cursor larining cheklovlari

  • Faqat o'qish — cursor orqali qatorni to'g'ridan-to'g'ri o'zgartirib bo'lmaydi
  • Faqat oldinga — orqaga qaytish yoki sakrash yo'q
  • Faqat stored procedure, function, trigger va event ichida

Xuddi shu ish — cursorsiz

Yuqoridagi butun procedure — bitta so'rov:

INSERT INTO sodiqlik (mijoz_id, daraja, hisoblangan)
SELECT * FROM (
    SELECT
        m.id,
        CASE
            WHEN COALESCE(SUM(q.soni * q.narx), 0) >= 20000000 THEN 'Oltin'
            WHEN COALESCE(SUM(q.soni * q.narx), 0) >= 5000000  THEN 'Kumush'
            WHEN COALESCE(SUM(q.soni * q.narx), 0) > 0         THEN 'Bronza'
            ELSE 'Yangi'
        END AS daraja,
        NOW() AS vaqt
    FROM mijozlar m
    LEFT JOIN buyurtmalar b        ON b.mijoz_id = m.id AND b.holat <> 'bekor'
    LEFT JOIN buyurtma_qatorlari q ON q.buyurtma_id = b.id
    GROUP BY m.id
) AS y
ON DUPLICATE KEY UPDATE daraja = y.daraja, hisoblangan = y.vaqt;
Cursor Set-based
100 000 mijoz 100 000 ta alohida INSERT 1 ta so'rov
Tezlik Soniyalar-daqiqalar Bir necha soniya ichida
Kod hajmi ~40 qator ~15 qator
Optimizator yordami Yo'q To'liq

Qachon cursor haqiqatan kerak

Cursor ni faqat quyidagi holatlarda ko'rib chiqing:

  1. Har bir qator uchun dinamik SQL — jadval nomi yoki buyruq qatorga bog'liq:
-- Har bir arxiv jadval uchun OPTIMIZE TABLE
DECLARE cur CURSOR FOR
    SELECT TABLE_NAME FROM information_schema.TABLES
    WHERE TABLE_SCHEMA = 'dokon' AND TABLE_NAME LIKE 'arxiv_%';
-- sikl ichida:
SET @sql = CONCAT('OPTIMIZE TABLE ', v_jadval);
PREPARE st FROM @sql; EXECUTE st; DEALLOCATE PREPARE st;
  1. Har bir qator uchun procedure chaqirish, uni to'plamli qilib bo'lmasa
  2. Har bir qator natijasi keyingisiga bog'liq — va window funksiyalar (29-dars) yordam bermasa
  3. Juda katta ishni bo'laklarga bo'lib, har bo'lakdan keyin COMMIT qilish

Boshqa barcha holatlarda — UPDATE ... JOIN, INSERT ... SELECT, CASE, window funksiyalar.


Nima uchun shunday?

Nima uchun SQL da "qatorma-qator" ishlash bunchalik sekin, dasturlash tillarida esa sikl — oddiy narsa?

SQL — deklarativ til: siz "nima" kerakligini aytasiz, baza "qanday" bajarishni o'zi hal qiladi — indekslarni, JOIN algoritmlarini, xotirani tanlaydi. To'plamli so'rovda optimizator butun vazifani ko'radi va eng samarali yo'lni topadi. Cursor bilan esa siz unga "qanday" ni o'zingiz aytasiz — bittadan, ketma-ket — va optimizator yordam bera olmaydi. Natija — har bir qator uchun alohida kichik so'rov, ya'ni N+1 muammosi (18-dars) bazaning ichida.


Xulosa

Eslab qoling:

  • Cursor: DECLARE ... CURSOR FOR → OPEN → FETCH ... INTO (siklda) → CLOSE
  • DECLARE CONTINUE HANDLER FOR NOT FOUND SET tugadi = TRUE + LEAVE
  • E'lon tartibi: o'zgaruvchilar → cursor lar → handler lar
  • MySQL cursor lari — faqat o'qish, faqat oldinga, faqat procedure ichida
  • Handler sikl ichidagi boshqa SELECT INTO larga ham ta'sir qiladi — ehtiyot bo'ling
  • Deyarli har doim set-based SQL tezroq va soddaroq
  • Cursor — dinamik SQL va to'plamli qilib bo'lmaydigan holatlar uchun

Keyingi dars

46-dars: Normalizatsiya — 1NF, 2NF, 3NF, BCNF