20-bo‘lim

Amaliy loyiha - onlayn do'kon bazasi

O'rganilgan barcha bilimlarni birlashtirib, to'liq onlayn do'kon ma'lumotlar bazasini noldan loyihalash.

🕑 15 daqiqa o‘qish 📄 954 so‘z 👁 5 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Talablar
  2. Bosqich 1 - mohiyatlarni ajratish
  3. Bosqich 2 - jadvallarni yaratish
  4. Kategoriyalar - o'ziga bog'lanish
  5. Mahsulotlar
  6. Rasmlar
  7. Mijozlar va manzillar
  8. Buyurtmalar
  9. Sharhlar va holat tarixi
  10. Bosqich 3 - test ma'lumotlari
  11. Bosqich 4 - buyurtma qabul qilish
  12. Bosqich 5 - trigger bilan tarixni yozish
  13. Bosqich 6 - katalog so'rovlari
  14. Bosqich 7 - admin hisobotlari
  15. Bosqich 8 - PHP qatlami
  16. Bosqich 9 - tekshiruv
  17. Loyiha tekshiruv ro'yxati
  18. Yakuniy xulosa

Yigirma bo'lim davomida o'rgangan hamma narsani bitta loyihada birlashtiramiz: loyihalash, indekslar, so'rovlar, tranzaksiyalar, xavfsizlik va hisobotlar.

Talablar #

Loyiha shartlari

Onlayn do'kon uchun baza kerak. U quyidagilarni qo'llab-quvvatlashi lozim:

  1. Mahsulotlar kategoriyalarga bo'linadi (kategoriya ichida kategoriya bo'lishi mumkin)
  2. Har bir mahsulotda narx, ombor qoldig'i va bir nechta rasm bor
  3. Mijozlar ro'yxatdan o'tadi va bir nechta manzil saqlashi mumkin
  4. Buyurtma bir nechta mahsulotdan iborat; narx buyurtma vaqtida qotib qoladi
  5. Mijoz mahsulotga bir marta sharh va baho qoldiradi
  6. Buyurtma holati o'zgarishi tarixi saqlanadi
  7. Admin uchun savdo hisobotlari kerak

Bosqich 1 - mohiyatlarni ajratish #

Talablardan otlarni ajratamiz: kategoriya, mahsulot, rasm, mijoz, manzil, buyurtma, buyurtma elementi, sharh, holat tarixi.

kategoriyalar id, nomi, slug ota_id mahsulotlar id, nomi, narx zaxira, faol kategoriya_id rasmlar id, yol, tartib mahsulot_id mijozlar id, ism, email parol_xesh buyurtmalar id, raqam, holat jami, sana mijoz_id, manzil_id buyurtma_elementlari buyurtma_id mahsulot_id soni, narx sharhlar mijoz_id, mahsulot_id, baho 1:N 1:N 1:N 1:N
Do'kon bazasining asosiy jadvallari va bog'liqliklari

Bosqich 2 - jadvallarni yaratish #

SQL
DROP DATABASE IF EXISTS dokon_loyiha;
CREATE DATABASE dokon_loyiha
    CHARACTER SET utf8mb4
    COLLATE utf8mb4_unicode_ci;

USE dokon_loyiha;

Kategoriyalar - o'ziga bog'lanish #

SQL
CREATE TABLE kategoriyalar (
    id     INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    ota_id INT UNSIGNED NULL,
    nomi   VARCHAR(120) NOT NULL,
    slug   VARCHAR(120) NOT NULL,
    tartib SMALLINT UNSIGNED NOT NULL DEFAULT 0,

    UNIQUE KEY uq_slug (slug),
    KEY idx_ota (ota_id),

    CONSTRAINT fk_kategoriya_ota
        FOREIGN KEY (ota_id) REFERENCES kategoriyalar(id)
        ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
O'ziga murojaat qiluvchi kalit

ota_id shu jadvalning o'ziga ishora qiladi. Bu daraxtsimon tuzilma hosil qiladi: "Elektronika" → "Kompyuterlar" → "Noutbuklar".

Ildiz kategoriyada ota_id qiymati NULL bo'ladi.

Mahsulotlar #

SQL
CREATE TABLE mahsulotlar (
    id            INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    kategoriya_id INT UNSIGNED NOT NULL,
    nomi          VARCHAR(200) NOT NULL,
    slug          VARCHAR(200) NOT NULL,
    tavsif        TEXT NULL,
    narx          DECIMAL(12,2) NOT NULL,
    eski_narx     DECIMAL(12,2) NULL,
    zaxira        INT NOT NULL DEFAULT 0,
    faol          BOOLEAN NOT NULL DEFAULT TRUE,
    yaratilgan    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    yangilangan   TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    UNIQUE KEY uq_slug (slug),
    KEY idx_kategoriya_faol (kategoriya_id, faol),
    KEY idx_narx (narx),
    FULLTEXT KEY ft_qidiruv (nomi, tavsif),

    CONSTRAINT fk_mahsulot_kategoriya
        FOREIGN KEY (kategoriya_id) REFERENCES kategoriyalar(id)
        ON DELETE RESTRICT,

    CONSTRAINT chk_narx   CHECK (narx > 0),
    CONSTRAINT chk_zaxira CHECK (zaxira >= 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DECIMAL, FLOAT emas

Pul uchun har doim DECIMAL(12,2). FLOAT da 0.1 + 0.2 aniq 0.3 bermaydi va hisob-kitobda tiyinlar yo'qoladi.

Rasmlar #

SQL
CREATE TABLE rasmlar (
    id          INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    mahsulot_id INT UNSIGNED NOT NULL,
    yol         VARCHAR(255) NOT NULL,
    tartib      TINYINT UNSIGNED NOT NULL DEFAULT 0,
    asosiy      BOOLEAN NOT NULL DEFAULT FALSE,

    KEY idx_mahsulot (mahsulot_id, tartib),

    CONSTRAINT fk_rasm_mahsulot
        FOREIGN KEY (mahsulot_id) REFERENCES mahsulotlar(id)
        ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Nima uchun bu yerda CASCADE?

Mahsulot o'chsa, uning rasmlari keraksiz bo'lib qoladi - shuning uchun ON DELETE CASCADE.

Lekin buyurtmalarda hech qachon CASCADE ishlatmang: mahsulot o'chirilgani uchun mijozning buyurtma tarixi yo'qolib ketmasligi kerak.

Mijozlar va manzillar #

SQL
CREATE TABLE mijozlar (
    id         INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    ism        VARCHAR(80) NOT NULL,
    familiya   VARCHAR(80) NOT NULL,
    email      VARCHAR(150) NOT NULL,
    telefon    VARCHAR(20) NULL,
    parol_xesh VARCHAR(255) NOT NULL,
    faol       BOOLEAN NOT NULL DEFAULT TRUE,
    royxatdan  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    UNIQUE KEY uq_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE manzillar (
    id       INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    mijoz_id INT UNSIGNED NOT NULL,
    viloyat  VARCHAR(60) NOT NULL,
    shahar   VARCHAR(60) NOT NULL,
    kocha    VARCHAR(200) NOT NULL,
    indeks   VARCHAR(10) NULL,
    asosiy   BOOLEAN NOT NULL DEFAULT FALSE,

    KEY idx_mijoz (mijoz_id),

    CONSTRAINT fk_manzil_mijoz
        FOREIGN KEY (mijoz_id) REFERENCES mijozlar(id)
        ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Buyurtmalar #

SQL
CREATE TABLE buyurtmalar (
    id              INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    raqam           VARCHAR(20) NOT NULL,
    mijoz_id        INT UNSIGNED NOT NULL,
    manzil_id       INT UNSIGNED NULL,
    holat           ENUM('yangi','tolangan','yigilmoqda','yolda','yetkazilgan','bekor')
                    NOT NULL DEFAULT 'yangi',
    mahsulot_summa  DECIMAL(12,2) NOT NULL DEFAULT 0,
    yetkazish_narxi DECIMAL(12,2) NOT NULL DEFAULT 0,
    jami            DECIMAL(12,2) NOT NULL DEFAULT 0,
    izoh            VARCHAR(500) NULL,
    yaratilgan      TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    UNIQUE KEY uq_raqam (raqam),
    KEY idx_mijoz_sana (mijoz_id, yaratilgan),
    KEY idx_holat (holat),

    CONSTRAINT fk_buyurtma_mijoz
        FOREIGN KEY (mijoz_id) REFERENCES mijozlar(id)
        ON DELETE RESTRICT,
    CONSTRAINT fk_buyurtma_manzil
        FOREIGN KEY (manzil_id) REFERENCES manzillar(id)
        ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE buyurtma_elementlari (
    id            INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    buyurtma_id   INT UNSIGNED NOT NULL,
    mahsulot_id   INT UNSIGNED NOT NULL,
    mahsulot_nomi VARCHAR(200) NOT NULL,
    narx          DECIMAL(12,2) NOT NULL,
    soni          SMALLINT UNSIGNED NOT NULL,

    KEY idx_buyurtma (buyurtma_id),
    KEY idx_mahsulot (mahsulot_id),

    CONSTRAINT fk_element_buyurtma
        FOREIGN KEY (buyurtma_id) REFERENCES buyurtmalar(id)
        ON DELETE CASCADE,
    CONSTRAINT fk_element_mahsulot
        FOREIGN KEY (mahsulot_id) REFERENCES mahsulotlar(id)
        ON DELETE RESTRICT,

    CONSTRAINT chk_soni CHECK (soni > 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Narx nusxasi - ataylab qilingan denormalizatsiya

buyurtma_elementlari jadvalida narx va mahsulot_nomi takrorlanadi. Bu normalizatsiya qoidasini buzayotgandek ko'rinadi, lekin to'g'ri qaror.

Sabab: mahsulot narxi ertaga o'zgarishi mumkin. Agar biz JOIN orqali joriy narxni olsak, o'tgan yilgi chek noto'g'ri summa ko'rsatadi.

Buyurtma - vaqt kesimidagi hujjat. Undagi ma'lumot muzlatilishi shart.

Sharhlar va holat tarixi #

SQL
CREATE TABLE sharhlar (
    id           INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    mahsulot_id  INT UNSIGNED NOT NULL,
    mijoz_id     INT UNSIGNED NOT NULL,
    baho         TINYINT UNSIGNED NOT NULL,
    matn         TEXT NULL,
    tasdiqlangan BOOLEAN NOT NULL DEFAULT FALSE,
    yaratilgan   TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    UNIQUE KEY uq_mijoz_mahsulot (mijoz_id, mahsulot_id),
    KEY idx_mahsulot_tasdiq (mahsulot_id, tasdiqlangan),

    CONSTRAINT fk_sharh_mahsulot
        FOREIGN KEY (mahsulot_id) REFERENCES mahsulotlar(id) ON DELETE CASCADE,
    CONSTRAINT fk_sharh_mijoz
        FOREIGN KEY (mijoz_id) REFERENCES mijozlar(id) ON DELETE CASCADE,

    CONSTRAINT chk_baho CHECK (baho BETWEEN 1 AND 5)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE holat_tarixi (
    id          INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    buyurtma_id INT UNSIGNED NOT NULL,
    eski_holat  VARCHAR(20) NULL,
    yangi_holat VARCHAR(20) NOT NULL,
    ozgartirdi  VARCHAR(80) NULL,
    vaqt        TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    KEY idx_buyurtma_vaqt (buyurtma_id, vaqt),

    CONSTRAINT fk_tarix_buyurtma
        FOREIGN KEY (buyurtma_id) REFERENCES buyurtmalar(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
UNIQUE KEY uq_mijoz_mahsulot

Bu bitta satr "har bir mijoz har bir mahsulotga faqat bitta sharh yozadi" qoidasini baza darajasida kafolatlaydi. PHP dagi tekshiruvni chetlab o'tish mumkin, bazadagini esa - yo'q.

Bosqich 3 - test ma'lumotlari #

SQL
INSERT INTO kategoriyalar (ota_id, nomi, slug, tartib) VALUES
(NULL, 'Elektronika',  'elektronika',  1),
(NULL, 'Kiyim',        'kiyim',        2),
(1,    'Kompyuterlar', 'kompyuterlar', 1),
(1,    'Telefonlar',   'telefonlar',   2),
(3,    'Noutbuklar',   'noutbuklar',   1);

INSERT INTO mahsulotlar (kategoriya_id, nomi, slug, narx, zaxira, tavsif) VALUES
(5, 'Lenovo IdeaPad 3',   'lenovo-ideapad-3', 6500000, 12, 'Kundalik ish uchun qulay noutbuk'),
(5, 'HP Pavilion 15',     'hp-pavilion-15',   8200000,  5, 'Kuchli protsessor va katta ekran'),
(4, 'Samsung Galaxy A54', 'samsung-a54',      4300000, 30, 'Yaxshi kamera, uzoq batareya'),
(4, 'Xiaomi Redmi 13',    'xiaomi-redmi-13',  2100000, 45, 'Arzon va ishonchli smartfon'),
(3, 'Klaviatura Logitech','klaviatura-logi',   250000, 80, 'Simsiz klaviatura');

INSERT INTO mijozlar (ism, familiya, email, telefon, parol_xesh) VALUES
('Husanboy', 'Qodirov',  '[email protected]', '+998901112233', '$2y$10$xeshnamunasi1'),
('Jasur',   'Toshmatov', '[email protected]',   '+998902223344', '$2y$10$xeshnamunasi2'),
('Malika',  'Yusupova',  '[email protected]',  '+998903334455', '$2y$10$xeshnamunasi3');

INSERT INTO manzillar (mijoz_id, viloyat, shahar, kocha, asosiy) VALUES
(1, 'Toshkent',  'Toshkent',  'Amir Temur ko''chasi, 15', TRUE),
(2, 'Samarqand', 'Samarqand', 'Registon ko''chasi, 7',    TRUE),
(3, 'Farg''ona', 'Qo''qon',   'Istiqlol ko''chasi, 22',   TRUE);
Apostrofni ikkilantirish

O'zbekcha matnda ko'cha so'zidagi apostrof SQL satrini erta yopib qo'yadi. Yechim - uni ikki marta yozish: 'ko''cha'. PHP dan yozganda esa tayyorlangan so'rov buni o'zi hal qiladi.

Bosqich 4 - buyurtma qabul qilish #

SQL
DELIMITER //

CREATE PROCEDURE buyurtma_yaratish(
    IN  p_mijoz_id     INT UNSIGNED,
    IN  p_manzil_id    INT UNSIGNED,
    IN  p_mahsulot_id  INT UNSIGNED,
    IN  p_soni         SMALLINT UNSIGNED,
    OUT p_buyurtma_id  INT UNSIGNED
)
BEGIN
    DECLARE v_narx   DECIMAL(12,2);
    DECLARE v_nomi   VARCHAR(200);
    DECLARE v_zaxira INT;

    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        RESIGNAL;
    END;

    START TRANSACTION;

    SELECT narx, nomi, zaxira INTO v_narx, v_nomi, v_zaxira
    FROM mahsulotlar
    WHERE id = p_mahsulot_id AND faol = TRUE
    FOR UPDATE;

    IF v_zaxira IS NULL OR v_zaxira < p_soni THEN
        SIGNAL SQLSTATE '45000'
            SET MESSAGE_TEXT = 'Omborda yetarli mahsulot yoq';
    END IF;

    INSERT INTO buyurtmalar (raqam, mijoz_id, manzil_id, mahsulot_summa, jami)
    VALUES (
        CONCAT('B-', DATE_FORMAT(NOW(), '%Y%m%d'), '-',
               LPAD(FLOOR(RAND() * 10000), 4, '0')),
        p_mijoz_id, p_manzil_id, v_narx * p_soni, v_narx * p_soni
    );

    SET p_buyurtma_id = LAST_INSERT_ID();

    INSERT INTO buyurtma_elementlari
        (buyurtma_id, mahsulot_id, mahsulot_nomi, narx, soni)
    VALUES
        (p_buyurtma_id, p_mahsulot_id, v_nomi, v_narx, p_soni);

    UPDATE mahsulotlar SET zaxira = zaxira - p_soni WHERE id = p_mahsulot_id;

    INSERT INTO holat_tarixi (buyurtma_id, eski_holat, yangi_holat, ozgartirdi)
    VALUES (p_buyurtma_id, NULL, 'yangi', 'tizim');

    COMMIT;
END //

DELIMITER ;
SQL
CALL buyurtma_yaratish(1, 1, 1, 2, @id);

SELECT raqam, holat, jami FROM buyurtmalar WHERE id = @id;
SELECT zaxira FROM mahsulotlar WHERE id = 1;
Natija
+-----------------+-------+-------------+
| raqam           | holat | jami        |
+-----------------+-------+-------------+
| B-20260615-4821 | yangi | 13000000.00 |
+-----------------+-------+-------------+

+--------+
| zaxira |
+--------+
|     10 |
+--------+
SELECT ... FOR UPDATE

Bu qator zaxira qiymatini qulflaydi. Ikki mijoz bir vaqtda oxirgi mahsulotni sotib olmoqchi bo'lsa, ikkinchisi birinchisi tugaguncha kutadi - va zaxira manfiy bo'lib qolmaydi.

Bunday holat "poyga sharti" (race condition) deyiladi va bu eng ko'p uchraydigan ombor xatosi.

Bosqich 5 - trigger bilan tarixni yozish #

SQL
DELIMITER //

CREATE TRIGGER buyurtma_holati_ozgardi
AFTER UPDATE ON buyurtmalar
FOR EACH ROW
BEGIN
    IF NEW.holat <> OLD.holat THEN
        INSERT INTO holat_tarixi (buyurtma_id, eski_holat, yangi_holat)
        VALUES (NEW.id, OLD.holat, NEW.holat);
    END IF;
END //

DELIMITER ;
SQL
UPDATE buyurtmalar SET holat = 'tolangan'   WHERE id = 1;
UPDATE buyurtmalar SET holat = 'yigilmoqda' WHERE id = 1;
UPDATE buyurtmalar SET holat = 'yolda'      WHERE id = 1;

SELECT eski_holat, yangi_holat, vaqt FROM holat_tarixi WHERE buyurtma_id = 1;
Natija
+------------+-------------+---------------------+
| eski_holat | yangi_holat | vaqt                |
+------------+-------------+---------------------+
| NULL       | yangi       | 2026-06-15 10:12:04 |
| yangi      | tolangan    | 2026-06-15 10:14:31 |
| tolangan   | yigilmoqda  | 2026-06-15 11:02:18 |
| yigilmoqda | yolda       | 2026-06-15 14:40:55 |
+------------+-------------+---------------------+

Bosqich 6 - katalog so'rovlari #

Mahsulot kartochkasi uchun kerak bo'ladigan hamma narsa bitta so'rovda:

SQL
SELECT
    m.id,
    m.nomi,
    m.narx,
    m.eski_narx,
    k.nomi                              AS kategoriya,
    r.yol                               AS rasm,
    ROUND(COALESCE(AVG(s.baho), 0), 1)  AS ortacha_baho,
    COUNT(s.id)                         AS sharhlar_soni,
    CASE WHEN m.zaxira > 0 THEN 'bor' ELSE 'tugagan' END AS holati
FROM mahsulotlar m
JOIN      kategoriyalar k ON k.id = m.kategoriya_id
LEFT JOIN rasmlar       r ON r.mahsulot_id = m.id AND r.asosiy = TRUE
LEFT JOIN sharhlar      s ON s.mahsulot_id = m.id AND s.tasdiqlangan = TRUE
WHERE m.faol = TRUE
GROUP BY m.id, m.nomi, m.narx, m.eski_narx, k.nomi, r.yol, m.zaxira
ORDER BY m.yaratilgan DESC
LIMIT 12;

Kategoriya daraxti - rekursiv so'rov bilan:

SQL
WITH RECURSIVE daraxt AS (
    SELECT id, ota_id, nomi, 0 AS daraja, CAST(nomi AS CHAR(500)) AS yol
    FROM kategoriyalar
    WHERE ota_id IS NULL

    UNION ALL

    SELECT k.id, k.ota_id, k.nomi, d.daraja + 1,
           CONCAT(d.yol, ' > ', k.nomi)
    FROM kategoriyalar k
    JOIN daraxt d ON d.id = k.ota_id
)
SELECT CONCAT(REPEAT('    ', daraja), nomi) AS kategoriya, yol
FROM daraxt
ORDER BY yol;
Natija
+--------------------+------------------------------------------+
| kategoriya         | yol                                      |
+--------------------+------------------------------------------+
| Elektronika        | Elektronika                              |
|     Kompyuterlar   | Elektronika > Kompyuterlar               |
|         Noutbuklar | Elektronika > Kompyuterlar > Noutbuklar  |
|     Telefonlar     | Elektronika > Telefonlar                 |
| Kiyim              | Kiyim                                    |
+--------------------+------------------------------------------+
WITH RECURSIVE

MySQL 8.0 va MariaDB 10.2 dan boshlab ishlaydi. Daraxtsimon ma'lumotni bitta so'rovda aylanib chiqish imkonini beradi - PHP da tsikl yozish shart emas.

Bosqich 7 - admin hisobotlari #

SQL
-- Oylik savdo
SELECT
    DATE_FORMAT(yaratilgan, '%Y-%m') AS oy,
    COUNT(*)                         AS buyurtmalar,
    SUM(jami)                        AS tushum,
    ROUND(AVG(jami))                 AS ortacha_chek
FROM buyurtmalar
WHERE holat <> 'bekor'
GROUP BY oy
ORDER BY oy DESC;

-- Eng ko'p sotilgan 10 ta mahsulot
SELECT
    be.mahsulot_nomi,
    SUM(be.soni)           AS sotilgan,
    SUM(be.soni * be.narx) AS tushum
FROM buyurtma_elementlari be
JOIN buyurtmalar b ON b.id = be.buyurtma_id
WHERE b.holat <> 'bekor'
GROUP BY be.mahsulot_id, be.mahsulot_nomi
ORDER BY sotilgan DESC
LIMIT 10;

-- Faol mijozlar reytingi
SELECT
    CONCAT(mj.ism, ' ', mj.familiya) AS mijoz,
    COUNT(b.id)                      AS buyurtmalar,
    SUM(b.jami)                      AS jami_xarid,
    MAX(b.yaratilgan)                AS oxirgi_xarid
FROM mijozlar mj
JOIN buyurtmalar b ON b.mijoz_id = mj.id AND b.holat <> 'bekor'
GROUP BY mj.id, mj.ism, mj.familiya
HAVING jami_xarid > 1000000
ORDER BY jami_xarid DESC;

-- Zaxirasi tugayotgan mahsulotlar
SELECT nomi, zaxira
FROM mahsulotlar
WHERE faol = TRUE AND zaxira <= 5
ORDER BY zaxira;

-- Bir marta ham sotilmagan mahsulotlar
SELECT m.nomi, m.narx
FROM mahsulotlar m
LEFT JOIN buyurtma_elementlari be ON be.mahsulot_id = m.id
WHERE be.id IS NULL;

Doimiy kerak bo'ladigan hisobotni VIEW ga aylantiramiz:

SQL
CREATE VIEW savdo_xulosasi AS
SELECT
    DATE(b.yaratilgan)   AS sana,
    COUNT(DISTINCT b.id) AS buyurtmalar,
    SUM(be.soni)         AS mahsulotlar,
    SUM(be.soni * be.narx) AS tushum
FROM buyurtmalar b
JOIN buyurtma_elementlari be ON be.buyurtma_id = b.id
WHERE b.holat <> 'bekor'
GROUP BY DATE(b.yaratilgan);

SELECT * FROM savdo_xulosasi ORDER BY sana DESC LIMIT 30;
SUM(b.jami) bu yerda noto'g'ri bo'lardi

JOIN dan keyin har bir buyurtma element soniga qarab takrorlanadi. Agar SUM(b.jami) yozsak, uch elementli buyurtma summasi uch marta qo'shiladi.

Shuning uchun element darajasidagi SUM(be.soni * be.narx) ishlatildi. Bu JOIN va GROUP BY birga ishlaganda eng ko'p qilinadigan xato.

Bosqich 8 - PHP qatlami #

PHP
<?php
class BuyurtmaXizmati
{
    public function __construct(private PDO $pdo) {}

    public function yarating(int $mijozId, int $manzilId, array $savat): int
    {
        if ($savat === []) {
            throw new InvalidArgumentException('Savat bo\'sh.');
        }

        $this->pdo->beginTransaction();

        try {
            $raqam = 'B-' . date('Ymd') . '-'
                   . str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT);

            $stmt = $this->pdo->prepare(
                'INSERT INTO buyurtmalar (raqam, mijoz_id, manzil_id)
                 VALUES (?, ?, ?)'
            );
            $stmt->execute([$raqam, $mijozId, $manzilId]);
            $buyurtmaId = (int) $this->pdo->lastInsertId();

            $summa = $this->elementlarniQoshing($buyurtmaId, $savat);

            $stmt = $this->pdo->prepare(
                'UPDATE buyurtmalar SET mahsulot_summa = ?, jami = ? WHERE id = ?'
            );
            $stmt->execute([$summa, $summa, $buyurtmaId]);

            $this->pdo->commit();

            return $buyurtmaId;
        } catch (Throwable $xato) {
            $this->pdo->rollBack();
            throw $xato;
        }
    }

    private function elementlarniQoshing(int $buyurtmaId, array $savat): float
    {
        $olish = $this->pdo->prepare(
            'SELECT nomi, narx, zaxira FROM mahsulotlar
             WHERE id = ? AND faol = TRUE FOR UPDATE'
        );
        $qoshish = $this->pdo->prepare(
            'INSERT INTO buyurtma_elementlari
                (buyurtma_id, mahsulot_id, mahsulot_nomi, narx, soni)
             VALUES (?, ?, ?, ?, ?)'
        );
        $kamaytirish = $this->pdo->prepare(
            'UPDATE mahsulotlar SET zaxira = zaxira - ? WHERE id = ?'
        );

        $summa = 0.0;

        foreach ($savat as $mahsulotId => $soni) {
            $olish->execute([$mahsulotId]);
            $mahsulot = $olish->fetch();

            if ($mahsulot === false) {
                throw new RuntimeException("Mahsulot topilmadi: {$mahsulotId}");
            }

            if ((int) $mahsulot['zaxira'] < $soni) {
                throw new RuntimeException(
                    "Omborda yetarli emas: {$mahsulot['nomi']}"
                );
            }

            $qoshish->execute([
                $buyurtmaId, $mahsulotId, $mahsulot['nomi'], $mahsulot['narx'], $soni,
            ]);
            $kamaytirish->execute([$soni, $mahsulotId]);

            $summa += (float) $mahsulot['narx'] * $soni;
        }

        return $summa;
    }

    public function holatniOzgartiring(int $buyurtmaId, string $yangiHolat): void
    {
        $ruxsatEtilgan = [
            'yangi', 'tolangan', 'yigilmoqda', 'yolda', 'yetkazilgan', 'bekor',
        ];

        if (!in_array($yangiHolat, $ruxsatEtilgan, true)) {
            throw new InvalidArgumentException('Noto\'g\'ri holat.');
        }

        $stmt = $this->pdo->prepare('UPDATE buyurtmalar SET holat = ? WHERE id = ?');
        $stmt->execute([$yangiHolat, $buyurtmaId]);
        // Tarix triggerda avtomatik yoziladi
    }
}

Bosqich 9 - tekshiruv #

SQL
-- Bog'liqliklar to'g'ri qurilganmi?
SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = 'dokon_loyiha'
  AND REFERENCED_TABLE_NAME IS NOT NULL
ORDER BY TABLE_NAME;

-- Indekslar
SELECT TABLE_NAME, INDEX_NAME,
       GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS ustunlar
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = 'dokon_loyiha'
GROUP BY TABLE_NAME, INDEX_NAME
ORDER BY TABLE_NAME;

-- Jadval hajmlari
SELECT
    TABLE_NAME,
    TABLE_ROWS,
    ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024, 1) AS hajm_kb
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'dokon_loyiha'
ORDER BY hajm_kb DESC;

-- So'rov rejasini tekshirish
EXPLAIN SELECT * FROM mahsulotlar WHERE kategoriya_id = 5 AND faol = TRUE;
Yetim qatorlarni qidiring

Tashqi kalitlar to'g'ri qo'yilgan bo'lsa, natija bo'sh bo'lishi shart:

SQL
SELECT be.id FROM buyurtma_elementlari be
LEFT JOIN buyurtmalar b ON b.id = be.buyurtma_id
WHERE b.id IS NULL;

Agar qator qaytsa - demak biror joyda FOREIGN KEY unutilgan.

Loyiha tekshiruv ro'yxati #

TalabQanday bajarildi
Ichma-ich kategoriyakategoriyalar.ota_id o'ziga bog'lanish
Bir nechta rasmAlohida rasmlar jadvali, 1:N
Bir nechta manzilAlohida manzillar jadvali, 1:N
Buyurtmada narx qotib qolishibuyurtma_elementlari.narx nusxasi
Bitta mijoz - bitta sharhUNIQUE (mijoz_id, mahsulot_id)
Baho 1-5 oralig'idaCHECK (baho BETWEEN 1 AND 5)
Zaxira manfiy bo'lmasligiCHECK va FOR UPDATE qulfi
Holat tarixiAFTER UPDATE trigger
Buyurtma yaxlitligiSTART TRANSACTION / COMMIT
Tez katalogKompozit va FULLTEXT indekslar
HisobotlarGROUP BY so'rovlari va VIEW
XavfsizlikTayyorlangan so'rovlar, password_hash
Yakuniy topshiriq

Bazani kengaytiring:

  1. Chegirma kuponlari jadvalini qo'shing: kod, foiz yoki summa, amal qilish muddati, maksimal ishlatilish soni.
  2. Buyurtmaga kupon_id qo'shing va jami ni chegirmani hisobga olib hisoblang.
  3. Sevimlilar jadvalini yarating (mijoz va mahsulot orasida N:M).
  4. Savat jadvalini qo'shing - ro'yxatdan o'tmagan foydalanuvchi uchun sessiya identifikatori bilan.
  5. Mahsulot uchun ortacha_baho ustunini qo'shing va sharh qo'shilganda uni yangilovchi trigger yozing.
  6. Har kuni kechasi 30 kundan eski bo'sh savatlarni o'chiruvchi EVENT yarating.
  7. Butun bazadan mysqldump bilan zaxira oling.
  8. PHP da mahsulot katalogini sahifalab chiqaruvchi sahifa yozing.

Yakuniy xulosa #

Yigirma bo'lim davomida siz quyidagilarni o'rgandingiz:

  • Ma'lumotlar bazasi nima va relyatsion model qanday ishlaydi
  • CREATE TABLE, ma'lumot turlari va cheklovlar
  • INSERT, SELECT, UPDATE, DELETE amallari
  • WHERE, ORDER BY, LIMIT bilan ma'lumot tanlash
  • GROUP BY va agregat funksiyalar bilan hisobotlar
  • Tashqi kalitlar va jadvallar orasidagi 1:1, 1:N, N:M bog'liqliklar
  • Barcha JOIN turlari va ichki so'rovlar
  • Indekslar va EXPLAIN bilan tezlikni oshirish
  • Normalizatsiya va ataylab qilinadigan denormalizatsiya
  • Tranzaksiyalar, ACID va qulflar
  • VIEW, protsedura, funksiya, trigger va hodisalar
  • Xavfsizlik: huquqlar, SQL inyeksiya, parol xeshlash
  • Zaxira olish, migratsiyalar va PDO orqali PHP dan ishlash
Keyingi qadam

Endi bu bilimlarni haqiqiy loyihada qo'llang. O'zingiz uchun kichik tizim yozing: shaxsiy kutubxona, xarajatlar hisobi yoki kichik do'kon.

Amaliyotsiz SQL unutiladi. Har kuni bitta so'rov yozing - bir oydan keyin farqni sezasiz.

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.