Query optimizatsiya — N+1, join strategiyalari, qoidalar

~25 daq · PostgreSQL · Index va Performance

Foydalanuvchilar ro'yxati yuklandi — 100 foydalanuvchi. Har birining oxirgi buyurtmasini ko'rsatish kerak. Kod:

const users = await db.query('SELECT * FROM foydalanuvchilar');
for (const user of users) {
  const order = await db.query(
    'SELECT * FROM buyurtmalar WHERE foydalanuvchi_id = $1 LIMIT 1',
    [user.id]
  );
}

Bu 101 ta so'rov — 1 ta foydalanuvchilar uchun, 100 ta har biri uchun. Bu N+1 muammo.


N+1 muammo

N ta yozuv uchun N ta qo'shimcha so'rov — jadval o'sgan sari so'rovlar ham o'sadi.

Yechim — bitta JOIN:

SELECT
  f.id, f.ism,
  b.mahsulot AS oxirgi_buyurtma
FROM foydalanuvchilar f
LEFT JOIN LATERAL (
  SELECT mahsulot FROM buyurtmalar
  WHERE foydalanuvchi_id = f.id
  ORDER BY sana DESC
  LIMIT 1
) b ON TRUE;

101 ta o'rniga — 1 ta so'rov.


SELECT * — keraksiz ustunlar

-- ❌ — barcha ustunlar, kerakmaydiganlar ham
SELECT * FROM foydalanuvchilar;

-- ✅ — faqat keraklisi
SELECT id, ism, email FROM foydalanuvchilar;

Katta jadvallarda SELECT * ko'p ma'lumot uzatadi — tarmoq va xotira sarflanadi.


WHERE da indekssiz funksiya

-- ❌ — index ishlamaydi
WHERE EXTRACT(YEAR FROM yaratilgan) = 2024;

-- ✅ — range bilan, index ishlaydi
WHERE yaratilgan BETWEEN '2024-01-01' AND '2024-12-31';
-- ❌
WHERE LOWER(email) = 'jasur@mail.com';

-- ✅ variantlar:
-- 1. Functional index yaratish
CREATE INDEX ON foydalanuvchilar (LOWER(email));
-- 2. ILIKE ishlatish (kichik jadvalda)
WHERE email ILIKE 'jasur@mail.com';

JOIN tartibini optimallashtirish

PostgreSQL JOIN tartibini o'zi tanlaydi — lekin ba'zan yordam kerak:

-- Kichik jadvaldan boshlash yaxshi
SELECT *
FROM kichik_jadval k
JOIN katta_jadval  t ON k.id = t.kichik_id
WHERE k.tur = 'A';

WHERE sharti avval kichik jadvalga qo'llanadi — natija kichik bo'ladi, JOIN dan o'tishi tez.


LIMIT erta qo'yish

-- ❌ — million qator JOIN qilinadi, keyin 10 ta olinadi
SELECT f.ism, b.mahsulot
FROM foydalanuvchilar f
JOIN buyurtmalar b ON f.id = b.foydalanuvchi_id
LIMIT 10;

-- ✅ — avval 10 ta foydalanuvchi, keyin JOIN
SELECT f.ism, b.mahsulot
FROM (SELECT * FROM foydalanuvchilar LIMIT 10) f
JOIN buyurtmalar b ON f.id = b.foydalanuvchi_id;

COUNT(*) vs COUNT(ustun)

COUNT(*)       -- barcha qatorlar, NULL ham
COUNT(telefon) -- faqat NULL bo'lmagan telefon qatorlar
COUNT(DISTINCT email) -- noyob emaillar

COUNT(*) eng tez — biror ustunni tekshirmaydi.


Optimizatsiya qoidalari

1. N+1 muammosidan qoching    — JOIN bilan bitta so'rov
2. SELECT * ishlatmang        — faqat kerakli ustunlar
3. INDEX qo'llang             — WHERE, JOIN, ORDER BY ustunlariga
4. WHERE da funksiya ehtiyotkorlik — index ishlamasligi mumkin
5. LIMIT erta qo'ying         — kerakli qatorlarda JOIN
6. EXPLAIN ANALYZE            — sekin so'rovni tekshiring
7. ANALYZE                    — statistikani yangilang

Tez-tez uchraydigan xatolar

OR bilan index:

WHERE id = 1 OR id = 2 OR id = 3

-- ✅ IN bilan — index yaxshiroq ishlaydi
WHERE id IN (1, 2, 3)

NOT IN da NULL:

WHERE id NOT IN (SELECT foydalanuvchi_id FROM bloklangan);
-- bloklangan da NULL bo'lsa — hech nima qaytmaydi!

-- ✅ NOT EXISTS bilan
WHERE NOT EXISTS (
  SELECT 1 FROM bloklangan WHERE foydalanuvchi_id = f.id
);

Xulosa

N+1 muammo    → JOIN bilan bitta so'rovga birlashtirish
SELECT *      → faqat kerakli ustunlar
INDEX         → WHERE, JOIN ON, ORDER BY ustunlariga
WHERE funksiya → range yoki functional index
LIMIT         → JOIN dan oldin

EXPLAIN ANALYZE — sekin so'rovni tekshiring
ANALYZE jadval — statistikani yangilang

Keyingi dars

41-dars: VACUUM, ANALYZE, AUTOVACUUM — nima uchun kerak