13-bo‘lim
Tashqi kalitlar va referensial butunlik
Yetim qatorlar, ON DELETE va ON UPDATE qoidalari, CASCADE xavfi, yumshoq o'chirish va tashqi kalitsiz ishlash.
Ushbu bo‘lim mundarijasi
Tashqi kalit - jadvallar orasidagi bog'lanishni majburiy qiladi. Usiz sxema tez orada "yetim" qatorlar bilan to'lib ketadi.
Yetim qator muammosi #
CREATE TABLE mualliflar (
id INT PRIMARY KEY AUTO_INCREMENT,
ism VARCHAR(100) NOT NULL
);
-- Tashqi kalitsiz jadval
CREATE TABLE kitoblar_himoyasiz (
id INT PRIMARY KEY AUTO_INCREMENT,
sarlavha VARCHAR(200) NOT NULL,
muallif_id INT NOT NULL
);
INSERT INTO mualliflar (ism) VALUES ('Abdulla Qodiriy'), ('Cho''lpon');
INSERT INTO kitoblar_himoyasiz (sarlavha, muallif_id) VALUES
('O''tkan kunlar', 1),
('Kecha va kunduz', 2),
('Sirli kitob', 999); -- 999-muallif MAVJUD EMAS
SELECT k.sarlavha, k.muallif_id, COALESCE(m.ism, '(YETIM)') AS muallif
FROM kitoblar_himoyasiz k
LEFT JOIN mualliflar m ON m.id = k.muallif_id
ORDER BY k.id;
+-----------------+------------+-----------------+
| sarlavha | muallif_id | muallif |
+-----------------+------------+-----------------+
| O'tkan kunlar | 1 | Abdulla Qodiriy |
| Kecha va kunduz | 2 | Cho'lpon |
| Sirli kitob | 999 | (YETIM) |
+-----------------+------------+-----------------+
999 raqamli muallif yo'q, lekin ma'lumotlar bazasi buni
qabul qildi.
Oqibatlari:
| Muammo | Ko'rinishi |
|---|---|
INNER JOIN qatorni yo'qotadi | Kitob ro'yxatda ko'rinmaydi |
LEFT JOIN NULL beradi | "muallif: bo'sh" |
| Hisobotlar mos kelmaydi | Kitoblar soni turlicha chiqadi |
| Xato manbasini topib bo'lmaydi | Qachon va kim yozgani noma'lum |
Va eng yomoni - buni hech kim sezmaydi, toki mijoz "kitobim yo'qolib qoldi" demaguncha.
Tashqi kalit bunday qatorni jismonan yozdirmaydi:
INSERT INTO kitoblar (sarlavha, muallif_id) VALUES ('Sirli', 999);
-- ERROR 1452: Cannot add or update a child row
Xato darhol, yozish paytida chiqadi - oylar keyin emas.
Tashqi kalit bilan himoya #
CREATE TABLE kitoblar (
id INT PRIMARY KEY AUTO_INCREMENT,
sarlavha VARCHAR(200) NOT NULL,
muallif_id INT NOT NULL,
CONSTRAINT fk_kitob_muallif
FOREIGN KEY (muallif_id) REFERENCES mualliflar(id)
);
INSERT INTO kitoblar (sarlavha, muallif_id) VALUES
('O''tkan kunlar', 1),
('Mehrobdan chayon', 1),
('Kecha va kunduz', 2);
SELECT m.ism, k.sarlavha
FROM kitoblar k
JOIN mualliflar m ON m.id = k.muallif_id
ORDER BY k.id;
+-----------------+------------------+
| ism | sarlavha |
+-----------------+------------------+
| Abdulla Qodiriy | O'tkan kunlar |
| Abdulla Qodiriy | Mehrobdan chayon |
| Cho'lpon | Kecha va kunduz |
+-----------------+------------------+
-- Mavjud bo'lmagan muallif rad etiladi
INSERT INTO kitoblar (sarlavha, muallif_id) VALUES ('Sirli kitob', 999);
ERROR 1452 (23000): Cannot add or update a child row: a foreign key
constraint fails (`maktab`.`kitoblar`, CONSTRAINT `fk_kitob_muallif`
FOREIGN KEY (`muallif_id`) REFERENCES `mualliflar` (`id`))
-- Kitobi bor muallifni o'chirib bo'lmaydi
DELETE FROM mualliflar WHERE id = 1;
ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key
constraint fails (`maktab`.`kitoblar`, CONSTRAINT `fk_kitob_muallif`
FOREIGN KEY (`muallif_id`) REFERENCES `mualliflar` (`id`))
ON DELETE qoidalari #
CASCADE amalda #
CREATE TABLE buyurtmalar (
id INT PRIMARY KEY AUTO_INCREMENT,
mijoz VARCHAR(100) NOT NULL
);
CREATE TABLE buyurtma_qatorlari (
id INT PRIMARY KEY AUTO_INCREMENT,
buyurtma_id INT NOT NULL,
mahsulot VARCHAR(100) NOT NULL,
CONSTRAINT fk_qator_buyurtma
FOREIGN KEY (buyurtma_id) REFERENCES buyurtmalar(id)
ON DELETE CASCADE
);
INSERT INTO buyurtmalar (mijoz) VALUES ('Husanboy'), ('Malika');
INSERT INTO buyurtma_qatorlari (buyurtma_id, mahsulot) VALUES
(1, 'Noutbuk'), (1, 'Sichqoncha'), (2, 'Klaviatura');
SELECT
(SELECT COUNT(*) FROM buyurtmalar) AS buyurtmalar,
(SELECT COUNT(*) FROM buyurtma_qatorlari) AS qatorlar;
+-------------+----------+
| buyurtmalar | qatorlar |
+-------------+----------+
| 2 | 3 |
+-------------+----------+
-- Buyurtmani o'chirsak, uning qatorlari ham o'chadi
DELETE FROM buyurtmalar WHERE id = 1;
SELECT
(SELECT COUNT(*) FROM buyurtmalar) AS buyurtmalar,
(SELECT COUNT(*) FROM buyurtma_qatorlari) AS qatorlar;
+-------------+----------+
| buyurtmalar | qatorlar |
+-------------+----------+
| 1 | 1 |
+-------------+----------+
CASCADE - kuchli va xavfliCASCADE zanjirli ishlaydi. Uch darajali sxemani tasavvur
qiling:
foydalanuvchilar -> buyurtmalar -> qatorlar -> to'lovlar
Har bog'lanishda ON DELETE CASCADE bo'lsa, bitta buyruq:
DELETE FROM foydalanuvchilar WHERE id = 5;
to'rt jadvaldan ma'lumot o'chiradi. Va bu ogohlantirishsiz sodir bo'ladi.
Real hodisa naqshi: administrator "test foydalanuvchini" o'chiradi, keyin ma'lum bo'ladiki, u yerda ming yozuv bor edi.
CASCADE qachon to'g'ri:
| Holat | Nima uchun |
|---|---|
| Buyurtma → qatorlari | Qator buyurtmasiz ma'nosiz |
| Maqola → izohlari | Izoh maqolasiz ma'nosiz |
| Foydalanuvchi → sessiyalari | Sessiya yo'qolishi normal |
| Bog'lovchi jadval yozuvlari | Bog'lanish o'zi ma'no bermaydi |
CASCADE qachon xavfli:
| Holat | Nima uchun |
|---|---|
| Mijoz → buyurtmalari | Buyurtma moliyaviy hujjat |
| Xodim → hisobotlari | Tarix yo'qoladi |
| Mahsulot → sotuvlari | Statistika buziladi |
Qoida: bola qator mustaqil qiymatga ega bo'lsa - CASCADE
ishlatmang. RESTRICT qoldiring va o'chirishni ongli qiling.
SET NULL #
CREATE TABLE bolimlar (
id INT PRIMARY KEY AUTO_INCREMENT,
nom VARCHAR(50) NOT NULL
);
CREATE TABLE xodimlar (
id INT PRIMARY KEY AUTO_INCREMENT,
ism VARCHAR(100) NOT NULL,
bolim_id INT NULL,
CONSTRAINT fk_xodim_bolim
FOREIGN KEY (bolim_id) REFERENCES bolimlar(id)
ON DELETE SET NULL
);
INSERT INTO bolimlar (nom) VALUES ('IT'), ('Hisobot');
INSERT INTO xodimlar (ism, bolim_id) VALUES
('Husanboy', 1), ('Malika', 1), ('Kamola', 2);
-- Bo'limni o'chirsak, xodimlar QOLADI, lekin bo'limsiz
DELETE FROM bolimlar WHERE id = 1;
SELECT ism, COALESCE(CAST(bolim_id AS CHAR), '(bolimsiz)') AS bolim
FROM xodimlar
ORDER BY id;
+----------+------------+
| ism | bolim |
+----------+------------+
| Husanboy | (bolimsiz) |
| Malika | (bolimsiz) |
| Kamola | 2 |
+----------+------------+
SET NULL - ustun NULL ga ruxsat berishi shartbolim_id INT NULL, -- NOT NULL bo'lsa ishlamaydi
FOREIGN KEY (bolim_id) ... ON DELETE SET NULL
bolim_id NOT NULL bo'lsa, SET NULL qoidasini yaratishga
urinish xato beradi.
Bu mantiqiy: qiymatni NULL ga o'rnatib bo'lmasa, qoida
bajarilmaydi.
SET NULL o'rinli holatlar:
- Xodim → bo'lim (bo'lim yopilsa, xodim qoladi);
- Maqola → rukn (rukn o'chirilsa, maqola "ruknsiz" bo'ladi);
- Vazifa → mas'ul shaxs (xodim ketsa, vazifa qoladi).
Ya'ni: bog'lanish ixtiyoriy bo'lgan joyda.
ON UPDATE #
Bu bo'limda yaratilgan uchala tashqi kalit qoidasini bir joyda ko'ramiz:
SELECT
CONSTRAINT_NAME AS cheklov,
UPDATE_RULE AS yangilashda,
DELETE_RULE AS ochirishda
FROM information_schema.REFERENTIAL_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = DATABASE()
ORDER BY BINARY CONSTRAINT_NAME;
+-------------------+-------------+------------+
| cheklov | yangilashda | ochirishda |
+-------------------+-------------+------------+
| fk_kitob_muallif | RESTRICT | RESTRICT |
| fk_qator_buyurtma | RESTRICT | CASCADE |
| fk_xodim_bolim | RESTRICT | SET NULL |
+-------------------+-------------+------------+
ON UPDATE CASCADE kamdan-kam kerakON UPDATE CASCADE ota qatorning kaliti o'zgarganda bola
qatorlarni yangilaydi.
Lekin 3-bo'limda ko'rganimizdek, sun'iy kalit hech qachon
o'zgarmaydi. Shuning uchun sun'iy kalit ishlatsangiz,
ON UPDATE deyarli hech qachon ishga tushmaydi.
U faqat tabiiy kalit ishlatilganda kerak bo'ladi:
-- Valyuta kodi kalit bo'lsa
valyuta CHAR(3) PRIMARY KEY -- 'UZS', 'USD'
Kod o'zgarsa (kamdan-kam, lekin bo'ladi), bog'liq jadvallar avtomatik yangilanadi.
Standart qiymat - RESTRICT, ya'ni kalitni o'zgartirishga
yo'l qo'yilmaydi. Ko'p holatda bu to'g'ri.
Yumshoq o'chirish #
CREATE TABLE mijozlar (
id INT PRIMARY KEY AUTO_INCREMENT,
ism VARCHAR(100) NOT NULL,
ochirilgan DATETIME NULL DEFAULT NULL
);
CREATE TABLE buyurtmalar_yumshoq (
id INT PRIMARY KEY AUTO_INCREMENT,
mijoz_id INT NOT NULL,
summa DECIMAL(10,2) NOT NULL,
FOREIGN KEY (mijoz_id) REFERENCES mijozlar(id)
);
INSERT INTO mijozlar (ism) VALUES ('Husanboy'), ('Malika');
INSERT INTO buyurtmalar_yumshoq (mijoz_id, summa) VALUES
(1, 8000000), (1, 150000), (2, 300000);
-- O'chirish o'rniga belgilash
UPDATE mijozlar SET ochirilgan = '2026-09-10 12:00:00' WHERE id = 1;
-- Faol mijozlar
SELECT ism FROM mijozlar WHERE ochirilgan IS NULL ORDER BY id;
+--------+
| ism |
+--------+
| Malika |
+--------+
-- Buyurtmalar saqlanib qoldi - hisobot to'g'ri ishlaydi
SELECT m.ism, COUNT(b.id) AS buyurtmalar, SUM(b.summa) AS jami
FROM mijozlar m
JOIN buyurtmalar_yumshoq b ON b.mijoz_id = m.id
GROUP BY m.id, m.ism
ORDER BY m.id;
+----------+-------------+------------+
| ism | buyurtmalar | jami |
+----------+-------------+------------+
| Husanboy | 2 | 8150000.00 |
| Malika | 1 | 300000.00 |
+----------+-------------+------------+
Yumshoq o'chirish (ochirilgan ustuni) tarixni saqlaydi, lekin
har so'rovga shart qo'shadi:
WHERE ochirilgan IS NULL
Buni bitta joyda unutish - o'chirilgan yozuvning foydalanuvchiga ko'rinishi demakdir.
Muammolar:
| Muammo | Izoh |
|---|---|
| Har so'rovda filtr | Unutish oson |
UNIQUE buziladi | O'chirilgan email qayta ishlatilmaydi |
| Jadval o'sib boradi | Eski yozuvlar qolaveradi |
JOIN murakkablashadi | Har tomonda filtr kerak |
UNIQUE muammosining yechimi - kompozit UNIQUE:
UNIQUE (email, ochirilgan)
ochirilgan NULL bo'lgani uchun (11-bo'lim) bir nechta
o'chirilgan yozuv bir xil emailga ega bo'la oladi, faol
yozuvlar orasida esa email noyob qoladi.
Filtrni unutmaslik uchun ko'rinish (view) yarating:
CREATE VIEW faol_mijozlar AS
SELECT * FROM mijozlar WHERE ochirilgan IS NULL;
Kod mijozlar o'rniga faol_mijozlar bilan ishlaydi.
Qachon yumshoq o'chirish kerak: moliyaviy hujjatlar, audit talab qilinadigan ma'lumot, tiklash imkoniyati kerak bo'lgan joylar.
Qachon kerak emas: sessiyalar, kesh, vaqtinchalik yozuvlar, jurnal.
Tashqi kalitsiz ishlash #
Ba'zi jamoalar tashqi kalitlarni ataylab ishlatmaydi. Sabablari:
| Sabab | Baho |
|---|---|
| "Yozishni sekinlashtiradi" | To'g'ri, lekin farq juda kichik |
| "Migratsiya qiyinlashadi" | To'g'ri - vaqtincha o'chirish mumkin |
| "Bo'laklangan bazada ishlamaydi" | To'g'ri - sharding da cheklov |
| "ORM o'zi tekshiradi" | Noto'g'ri - u hamma yo'lni qamramaydi |
Birinchi sabab odatda o'lchanmagan. Tashqi kalit tekshiruvi indeks bo'yicha qidiruv - u mikrosekundlarda bajariladi.
To'rtinchi sabab eng xavfli. ORM faqat o'z kodi orqali
o'tgan yozuvlarni tekshiradi. Import skripti, admin paneli,
qo'lda UPDATE va boshqa xizmatlar uni chetlab o'tadi.
Haqiqiy istisnolar:
- Juda katta miqyosdagi taqsimlangan tizim (sharding);
- Analitik ombor (ma'lumot faqat o'qiladi);
- Vaqtinchalik yuklash jadvallari.
Bu holatlarda ham butunlikni muntazam tekshirish kerak:
SELECT COUNT(*) AS yetimlar
FROM kitoblar_himoyasiz k
LEFT JOIN mualliflar m ON m.id = k.muallif_id
WHERE m.id IS NULL;
Odatiy veb ilova uchun esa javob oddiy: tashqi kalitlarni qo'ying.
Yetimlarni topish #
SELECT k.id, k.sarlavha, k.muallif_id
FROM kitoblar_himoyasiz k
LEFT JOIN mualliflar m ON m.id = k.muallif_id
WHERE m.id IS NULL
ORDER BY k.id;
+----+-------------+------------+
| id | sarlavha | muallif_id |
+----+-------------+------------+
| 3 | Sirli kitob | 999 |
+----+-------------+------------+
- Tashqi kalitsiz jadvalga yetim qator kiriting.
LEFT JOINbilan uni toping.- Tashqi kalit qo'shib, xuddi shu yozuvni rad ettiring.
- Bolasi bor otani o'chirishga urinib ko'ring.
ON DELETE CASCADEbilan zanjirli o'chirishni sinang.CASCADEqachon xavfli ekanini uch misolda ayting.ON DELETE SET NULLbilan bo'limni o'chiring.SET NULLuchun ustun qanday bo'lishi kerakligini ayting.- Yumshoq o'chirishni amalga oshiring.
- Yumshoq o'chirishning to'rtta muammosini sanang.
Xulosa #
- Tashqi kalitsiz yetim qatorlar jimgina to'planadi.
- Xato yozish paytida chiqadi - oylar keyin emas.
- Standart qoida
RESTRICT- eng xavfsiz. CASCADEzanjirli ishlaydi - ehtiyot bo'ling.- Bola qator mustaqil qiymatga ega bo'lsa,
CASCADEishlatmang. SET NULLuchun ustunNULLga ruxsat berishi shart.ON UPDATEsun'iy kalitda deyarli kerak emas.- Yumshoq o'chirish tarixni saqlaydi, lekin har so'rovga filtr qo'shadi.
UNIQUEmuammosini kompozitUNIQUEhal qiladi.- ORM tashqi kalitni almashtirmaydi - u hamma yo'lni qamramaydi.
- Odatiy ilova uchun: tashqi kalitlarni qo'ying.
Keyingi bo'limda indekslar qanday ishlashini ko'ramiz.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
Xatolik topdingizmi?
Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.