# 📋 SQL CHEAT SHEET CHO TESTER

> **Dành cho:** Tester không biết lập trình
> **Mục đích:** Kiểm tra dữ liệu trong database nhanh chóng
> **Lưu ý chung:** Chỉ dùng câu lệnh `SELECT` — **KHÔNG** làm thay đổi dữ liệu

---

## 📌 MỤC LỤC

1. [SELECT WHERE — Lọc dữ liệu cơ bản](#1-select-where--lọc-dữ-liệu-cơ-bản)
2. [ORDER BY LIMIT — Tìm bản ghi mới nhất](#2-order-by-limit--tìm-bản-ghi-mới-nhất)
3. [COUNT và GROUP BY — Đếm theo nhóm](#3-count-và-group-by--đếm-theo-nhóm)
4. [INNER JOIN — Nối 2 bảng lại với nhau](#4-inner-join--nối-2-bảng-lại-với-nhau)
5. [IS NULL / IS NOT NULL — Tìm dữ liệu bị thiếu](#5-is-null--is-not-null--tìm-dữ-liệu-bị-thiếu)

---

## 1. SELECT WHERE — Lọc dữ liệu cơ bản

### 💡 Giải thích

`SELECT` dùng để **đọc dữ liệu** từ bảng.
`WHERE` dùng để **lọc** — chỉ lấy những dòng thỏa điều kiện.

> **Hình dung:** Giống như dùng bộ lọc trong Excel, chỉ hiện ra dòng bạn cần.

### 🗂️ Cấu trúc bảng `users` dùng trong ví dụ

| user_id | full_name | email | status | age | city |
|---------|-----------|-------|--------|-----|------|
| 1 | Nguyễn Văn A | a@gmail.com | active | 25 | Hà Nội |
| 2 | Trần Thị B | b@gmail.com | inactive | 30 | HCM |
| 3 | Lê Văn C | c@gmail.com | active | 17 | Đà Nẵng |

### ✅ Các câu query mẫu

```sql
-- =========================================
-- LẤY TẤT CẢ DỮ LIỆU trong bảng users
-- Dùng khi: muốn xem toàn bộ bảng
-- =========================================
SELECT *
FROM users;


-- =========================================
-- LẤY MỘT SỐ CỘT CỤ THỂ (không lấy hết)
-- Dùng khi: chỉ cần xem tên và email
-- =========================================
SELECT full_name, email, status
FROM users;


-- =========================================
-- LỌC THEO GIÁ TRỊ CHÍNH XÁC
-- Dùng khi: tìm tất cả user đang "active"
-- =========================================
SELECT *
FROM users
WHERE status = 'active';   -- Chú ý: giá trị chữ phải có dấu nháy đơn ''


-- =========================================
-- LỌC THEO SỐ (không cần dấu nháy đơn)
-- Dùng khi: tìm user có user_id = 5
-- =========================================
SELECT *
FROM users
WHERE user_id = 5;         -- Số thì KHÔNG cần dấu nháy đơn


-- =========================================
-- LỌC NHIỀU ĐIỀU KIỆN CÙNG LÚC — dùng AND
-- Dùng khi: tìm user active VÀ ở Hà Nội
-- Cả 2 điều kiện phải đúng thì mới lấy
-- =========================================
SELECT *
FROM users
WHERE status = 'active'
  AND city = 'Hà Nội';


-- =========================================
-- LỌC NHIỀU ĐIỀU KIỆN — dùng OR
-- Dùng khi: tìm user ở HCM HOẶC Hà Nội
-- Chỉ cần 1 điều kiện đúng là lấy
-- =========================================
SELECT *
FROM users
WHERE city = 'Hà Nội'
   OR city = 'HCM';


-- =========================================
-- LỌC THEO KHOẢNG GIÁ TRỊ — dùng BETWEEN
-- Dùng khi: tìm user có tuổi từ 18 đến 30
-- =========================================
SELECT *
FROM users
WHERE age BETWEEN 18 AND 30;  -- Bao gồm cả 18 và 30


-- =========================================
-- TÌM KIẾM THEO TỪ KHÓA — dùng LIKE
-- Dùng khi: tìm user có email chứa "gmail"
-- =========================================
SELECT *
FROM users
WHERE email LIKE '%gmail.com';
-- Dấu % = "bất kỳ ký tự nào"
-- '%gmail.com'  = kết thúc bằng gmail.com
-- 'Nguyễn%'    = bắt đầu bằng Nguyễn
-- '%Văn%'      = có chứa chữ Văn ở bất kỳ đâu


-- =========================================
-- LỌC THEO DANH SÁCH GIÁ TRỊ — dùng IN
-- Dùng khi: tìm user ở một trong nhiều thành phố
-- Thay thế cho viết nhiều OR
-- =========================================
SELECT *
FROM users
WHERE city IN ('Hà Nội', 'HCM', 'Đà Nẵng');


-- =========================================
-- LOẠI TRỪ — dùng NOT IN hoặc != hoặc <>
-- Dùng khi: tìm user KHÔNG phải inactive
-- =========================================
SELECT *
FROM users
WHERE status != 'inactive';   -- != và <> đều có nghĩa là "khác"
```

### ⚠️ Ghi chú lưu ý thường gặp

| Tình huống | Đúng ✅ | Sai ❌ |
|-----------|--------|-------|
| Giá trị là **chữ** | `WHERE status = 'active'` | `WHERE status = active` |
| Giá trị là **số** | `WHERE user_id = 5` | `WHERE user_id = '5'` |
| Tìm kiếm có phân biệt hoa thường | Tùy database (MySQL thường không phân biệt) | |
| Dùng `=` so sánh NULL | **KHÔNG dùng được** `WHERE email = NULL` | Xem mục 5 |

---

## 2. ORDER BY LIMIT — Tìm bản ghi mới nhất

### 💡 Giải thích

`ORDER BY` dùng để **sắp xếp** kết quả theo thứ tự tăng dần hoặc giảm dần.
`LIMIT` dùng để **giới hạn số dòng** trả về — không lấy hết bảng.

> **Hình dung:** Giống như sắp xếp email theo ngày mới nhất và chỉ đọc 10 email đầu.

### 🗂️ Cấu trúc bảng dùng trong ví dụ

**Bảng `orders`**

| order_id | user_id | total_amount | status | created_at |
|----------|---------|-------------|--------|------------|
| 101 | 1 | 250000 | completed | 2024-01-15 10:30:00 |
| 102 | 2 | 450000 | pending | 2024-01-20 14:00:00 |
| 103 | 1 | 100000 | cancelled | 2024-01-22 09:15:00 |

### ✅ Các câu query mẫu

```sql
-- =========================================
-- SẮP XẾP TĂNG DẦN — ASC (mặc định)
-- Dùng khi: xem order từ cũ nhất đến mới nhất
-- ASC = Ascending = tăng dần (A→Z, nhỏ→lớn, cũ→mới)
-- =========================================
SELECT *
FROM orders
ORDER BY created_at ASC;


-- =========================================
-- SẮP XẾP GIẢM DẦN — DESC
-- Dùng khi: xem order MỚI NHẤT lên đầu
-- DESC = Descending = giảm dần (Z→A, lớn→nhỏ, mới→cũ)
-- =========================================
SELECT *
FROM orders
ORDER BY created_at DESC;


-- =========================================
-- LẤY 1 BẢN GHI MỚI NHẤT
-- Dùng khi: kiểm tra order vừa tạo xong
-- =========================================
SELECT *
FROM orders
ORDER BY created_at DESC
LIMIT 1;              -- Chỉ lấy 1 dòng đầu tiên (= mới nhất)


-- =========================================
-- LẤY 10 BẢN GHI MỚI NHẤT
-- Dùng khi: xem 10 đơn hàng gần đây nhất
-- =========================================
SELECT *
FROM orders
ORDER BY created_at DESC
LIMIT 10;


-- =========================================
-- LẤY BẢN GHI MỚI NHẤT + LỌC THEO ĐIỀU KIỆN
-- Dùng khi: tìm 5 đơn hàng "pending" mới nhất
-- WHERE lọc trước, ORDER BY sắp xếp sau
-- =========================================
SELECT *
FROM orders
WHERE status = 'pending'
ORDER BY created_at DESC
LIMIT 5;


-- =========================================
-- TÌM GIÁ TRỊ LỚN NHẤT / NHỎ NHẤT
-- Dùng khi: tìm đơn hàng có giá trị cao nhất
-- =========================================
SELECT *
FROM orders
ORDER BY total_amount DESC   -- Sắp xếp tiền giảm dần
LIMIT 1;                     -- Lấy dòng đầu = đơn lớn nhất


-- =========================================
-- SẮP XẾP THEO NHIỀU CỘT
-- Dùng khi: sắp xếp theo status, nếu cùng status thì xếp theo ngày mới nhất
-- =========================================
SELECT *
FROM orders
ORDER BY status ASC,          -- Trước: sắp theo status A→Z
         created_at DESC;     -- Sau: cùng status thì mới nhất lên đầu


-- =========================================
-- BỎ QUA BẢN GHI ĐẦU — dùng OFFSET
-- Dùng khi: xem "trang 2" của dữ liệu
-- LIMIT 10 OFFSET 10 = bỏ 10 dòng đầu, lấy 10 dòng tiếp theo
-- =========================================
SELECT *
FROM orders
ORDER BY created_at DESC
LIMIT 10 OFFSET 10;    -- Trang 1: OFFSET 0 | Trang 2: OFFSET 10 | Trang 3: OFFSET 20
```

### ⚠️ Ghi chú lưu ý thường gặp

| Điều cần nhớ | Chi tiết |
|-------------|---------|
| Thứ tự viết câu lệnh | `SELECT` → `FROM` → `WHERE` → `ORDER BY` → `LIMIT` |
| Không có `ORDER BY` | Thứ tự dòng trả về **không đảm bảo** — không nên dựa vào |
| `LIMIT` không có `ORDER BY` | Lấy ngẫu nhiên — **không phải** bản ghi mới nhất |
| `DESC` vs `ASC` | Hay bị nhầm: muốn **mới nhất** thì dùng `DESC` |
| Tên cột thời gian | Hay gặp: `created_at`, `updated_at`, `order_date`, `created_date` |

---

## 3. COUNT và GROUP BY — Đếm theo nhóm

### 💡 Giải thích

`COUNT` dùng để **đếm số lượng** bản ghi.
`GROUP BY` dùng để **gom nhóm** các dòng có cùng giá trị lại, rồi tính toán trên từng nhóm.

> **Hình dung:** Giống như pivot table trong Excel — đếm xem mỗi nhóm có bao nhiêu cái.

### ✅ Các câu query mẫu

```sql
-- =========================================
-- ĐẾM TỔNG SỐ DÒNG trong bảng
-- Dùng khi: kiểm tra có bao nhiêu user trong hệ thống
-- =========================================
SELECT COUNT(*)  AS tong_so_user   -- AS = đặt tên cho cột kết quả
FROM users;
-- Kết quả: tong_so_user = 1500


-- =========================================
-- ĐẾM THEO ĐIỀU KIỆN — kết hợp với WHERE
-- Dùng khi: đếm bao nhiêu user đang active
-- =========================================
SELECT COUNT(*) AS so_user_active
FROM users
WHERE status = 'active';


-- =========================================
-- ĐẾM THEO TỪNG NHÓM — dùng GROUP BY
-- Dùng khi: xem mỗi status có bao nhiêu user
-- =========================================
SELECT
    status,           -- Cột để nhóm
    COUNT(*) AS so_luong  -- Đếm trong mỗi nhóm
FROM users
GROUP BY status;
-- Kết quả mẫu:
-- status    | so_luong
-- active    | 1200
-- inactive  | 250
-- banned    | 50


-- =========================================
-- ĐẾM ĐƠN HÀNG THEO TỪNG TRẠNG THÁI
-- Dùng khi: kiểm tra phân bổ đơn hàng
-- =========================================
SELECT
    status,
    COUNT(*) AS so_don_hang
FROM orders
GROUP BY status
ORDER BY so_don_hang DESC;  -- Nhóm nhiều nhất lên đầu


-- =========================================
-- ĐẾM SỐ ĐƠN HÀNG CỦA TỪNG USER
-- Dùng khi: xem user nào đặt nhiều đơn nhất
-- =========================================
SELECT
    user_id,
    COUNT(*) AS so_don
FROM orders
GROUP BY user_id
ORDER BY so_don DESC
LIMIT 10;   -- Top 10 user đặt nhiều đơn nhất


-- =========================================
-- TÍNH TỔNG — dùng SUM
-- Dùng khi: tính tổng doanh thu theo trạng thái đơn
-- =========================================
SELECT
    status,
    COUNT(*)            AS so_don,
    SUM(total_amount)   AS tong_tien   -- SUM = tính tổng
FROM orders
GROUP BY status;


-- =========================================
-- CÁC HÀM TÍNH TOÁN THƯỜNG DÙNG
-- =========================================
SELECT
    COUNT(*)              AS tong_so_don,       -- Đếm tất cả dòng
    COUNT(total_amount)   AS so_don_co_tien,    -- Đếm dòng KHÔNG NULL
    SUM(total_amount)     AS tong_doanh_thu,    -- Tổng cộng
    AVG(total_amount)     AS trung_binh_don,    -- Trung bình
    MIN(total_amount)     AS don_nho_nhat,      -- Giá trị nhỏ nhất
    MAX(total_amount)     AS don_lon_nhat       -- Giá trị lớn nhất
FROM orders
WHERE status = 'completed';


-- =========================================
-- LỌC NHÓM SAU KHI GROUP BY — dùng HAVING
-- Dùng khi: chỉ lấy những user có hơn 5 đơn hàng
-- HAVING giống WHERE nhưng dùng SAU GROUP BY
-- =========================================
SELECT
    user_id,
    COUNT(*) AS so_don
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 5     -- Lọc nhóm: chỉ lấy user có > 5 đơn
ORDER BY so_don DESC;


-- =========================================
-- PHÂN BIỆT WHERE vs HAVING
-- WHERE  = lọc TRƯỚC khi nhóm (lọc từng dòng)
-- HAVING = lọc SAU khi nhóm  (lọc từng nhóm)
-- =========================================
SELECT
    user_id,
    COUNT(*) AS so_don
FROM orders
WHERE status = 'completed'      -- Bước 1: Chỉ lấy đơn completed
GROUP BY user_id
HAVING COUNT(*) >= 3            -- Bước 2: Chỉ lấy user có >= 3 đơn completed
ORDER BY so_don DESC;
```

### ⚠️ Ghi chú lưu ý thường gặp

| Điều cần nhớ | Chi tiết |
|-------------|---------|
| `COUNT(*)` vs `COUNT(cột)` | `COUNT(*)` đếm tất cả dòng; `COUNT(email)` chỉ đếm dòng email **không phải NULL** |
| Cột trong `SELECT` | Nếu dùng `GROUP BY`, cột trong `SELECT` phải là: cột group hoặc hàm tính toán (COUNT, SUM...) |
| `WHERE` vs `HAVING` | `WHERE` lọc dòng đơn lẻ; `HAVING` lọc nhóm sau khi gom |
| `AS` đặt tên | Nên đặt tên cột kết quả cho dễ đọc: `COUNT(*) AS so_luong` |

---

## 4. INNER JOIN — Nối 2 bảng lại với nhau

### 💡 Giải thích

`INNER JOIN` dùng để **kết hợp dữ liệu từ 2 bảng** dựa trên một cột chung (thường là ID).
Kết quả chỉ trả về những dòng **có dữ liệu khớp ở CẢ 2 bảng**.

> **Hình dung:** Bảng `users` lưu thông tin người dùng. Bảng `orders` lưu đơn hàng nhưng chỉ có `user_id` chứ không có tên. JOIN giúp ghép tên người dùng vào đơn hàng.

### 🗂️ Quan hệ giữa các bảng

```
users                          orders
---------                      ----------
user_id  ◄────────────────►   user_id
full_name                      order_id
email                          total_amount
status                         status
                               created_at

orders                         products
---------                      ----------
product_id ◄─────────────►    product_id
order_id                       product_name
quantity                       price
```

### ✅ Các câu query mẫu

```sql
-- =========================================
-- JOIN CƠ BẢN: Ghép tên user vào đơn hàng
-- Dùng khi: muốn xem đơn hàng kèm tên người đặt
-- =========================================
SELECT
    orders.order_id,
    orders.total_amount,
    orders.status,
    users.full_name,         -- Lấy từ bảng users
    users.email              -- Lấy từ bảng users
FROM orders
INNER JOIN users
    ON orders.user_id = users.user_id;  -- Điều kiện nối: cột chung
-- ON = "nối theo điều kiện này"


-- =========================================
-- DÙNG ALIAS (bí danh) cho tên bảng
-- Viết tắt tên bảng cho gọn: orders → o, users → u
-- =========================================
SELECT
    o.order_id,
    o.total_amount,
    o.status        AS trang_thai_don,
    u.full_name     AS ten_khach_hang,
    u.email
FROM orders  AS o           -- o là tên tắt của orders
INNER JOIN users AS u       -- u là tên tắt của users
    ON o.user_id = u.user_id;


-- =========================================
-- JOIN + WHERE: Lọc kết quả sau khi nối
-- Dùng khi: xem đơn hàng "pending" cùng tên khách
-- =========================================
SELECT
    o.order_id,
    o.created_at,
    o.total_amount,
    u.full_name,
    u.email
FROM orders AS o
INNER JOIN users AS u
    ON o.user_id = u.user_id
WHERE o.status = 'pending'          -- Lọc theo bảng orders
ORDER BY o.created_at DESC;


-- =========================================
-- JOIN + WHERE lọc từ cả 2 bảng
-- Dùng khi: đơn hàng pending của user active
-- =========================================
SELECT
    o.order_id,
    o.total_amount,
    u.full_name,
    u.status    AS trang_thai_user
FROM orders AS o
INNER JOIN users AS u
    ON o.user_id = u.user_id
WHERE o.status = 'pending'
  AND u.status = 'active';


-- =========================================
-- JOIN 3 BẢNG: orders + users + products
-- Dùng khi: muốn xem đơn hàng, tên khách, tên sản phẩm
-- (Giả sử có bảng order_items nối orders và products)
-- =========================================
SELECT
    o.order_id,
    u.full_name         AS ten_khach,
    p.product_name      AS ten_san_pham,
    oi.quantity         AS so_luong
FROM orders AS o
INNER JOIN users AS u
    ON o.user_id = u.user_id
INNER JOIN order_items AS oi      -- Bảng trung gian
    ON o.order_id = oi.order_id
INNER JOIN products AS p
    ON oi.product_id = p.product_id;


-- =========================================
-- JOIN + COUNT: Đếm đơn hàng kèm tên user
-- Dùng khi: thống kê số đơn của mỗi khách hàng
-- =========================================
SELECT
    u.user_id,
    u.full_name,
    u.email,
    COUNT(o.order_id) AS tong_so_don
FROM users AS u
INNER JOIN orders AS o
    ON u.user_id = o.user_id
GROUP BY u.user_id, u.full_name, u.email
ORDER BY tong_so_don DESC;


-- =========================================
-- KIỂM TRA: User này có đơn hàng không?
-- Dùng khi: verify sau khi tạo đơn hàng mới
-- =========================================
SELECT
    u.full_name,
    o.order_id,
    o.status,
    o.created_at
FROM users AS u
INNER JOIN orders AS o
    ON u.user_id = o.user_id
WHERE u.email = 'a@gmail.com'    -- Tìm theo email cụ thể
ORDER BY o.created_at DESC;
```

### ⚠️ Ghi chú lưu ý thường gặp

| Điều cần nhớ | Chi tiết |
|-------------|---------|
| `INNER JOIN` bỏ qua dòng không khớp | User không có đơn hàng → **sẽ không xuất hiện** trong kết quả |
| Tên cột trùng nhau | Phải ghi rõ `tenbang.tencot`: `o.status` hoặc `u.status` để phân biệt |
| Dùng `LEFT JOIN` nếu muốn giữ lại tất cả | `LEFT JOIN` giữ tất cả dòng bảng trái dù không khớp |
| `ON` viết sai | Hay bị lỗi: nối sai cột → kết quả sai hoặc quá nhiều dòng |
| Kiểm tra kết quả | So sánh `COUNT(*)` trước và sau JOIN để phát hiện bất thường |

---

## 5. IS NULL / IS NOT NULL — Tìm dữ liệu bị thiếu

### 💡 Giải thích

`NULL` trong database nghĩa là **không có giá trị** — khác với số `0` hoặc chữ trống `''`.
`IS NULL` tìm dòng **bị thiếu dữ liệu**.
`IS NOT NULL` tìm dòng **đã có dữ liệu**.

> **Hình dung:** Như ô trống trong Excel — không phải số 0, không phải khoảng trắng, mà thực sự trống hoàn toàn.

### ✅ Các câu query mẫu

```sql
-- =========================================
-- TÌM DÒNG BỊ THIẾU DỮ LIỆU — IS NULL
-- Dùng khi: kiểm tra user chưa điền email
-- =========================================
SELECT *
FROM users
WHERE email IS NULL;      -- Tìm email bị trống/thiếu


-- =========================================
-- TÌM DÒNG ĐÃ CÓ DỮ LIỆU — IS NOT NULL
-- Dùng khi: chỉ lấy user đã có email
-- =========================================
SELECT *
FROM users
WHERE email IS NOT NULL;


-- =========================================
-- ĐẾM SỐ DÒNG BỊ THIẾU
-- Dùng khi: báo cáo "có bao nhiêu user chưa có email"
-- =========================================
SELECT COUNT(*) AS so_user_thieu_email
FROM users
WHERE email IS NULL;


-- =========================================
-- KIỂM TRA NHIỀU CỘT BỊ THIẾU CÙNG LÚC
-- Dùng khi: tìm user thiếu email HOẶC thiếu số điện thoại
-- =========================================
SELECT
    user_id,
    full_name,
    email,
    phone
FROM users
WHERE email IS NULL
   OR phone IS NULL;      -- Thiếu ít nhất 1 trong 2


-- =========================================
-- TÌM USER THIẾU CẢ 2 THÔNG TIN
-- Dùng khi: tìm user thiếu email VÀ thiếu số điện thoại
-- =========================================
SELECT *
FROM users
WHERE email IS NULL
  AND phone IS NULL;      -- Phải thiếu cả 2 mới lấy


-- =========================================
-- ĐẾM THỐNG KÊ: Bao nhiêu dòng có/không có dữ liệu
-- Dùng khi: báo cáo chất lượng dữ liệu tổng quan
-- =========================================
SELECT
    COUNT(*)                AS tong_so_user,
    COUNT(email)            AS so_co_email,        -- COUNT(cột) tự bỏ qua NULL
    COUNT(*) - COUNT(email) AS so_thieu_email,     -- Tính số thiếu
    COUNT(phone)            AS so_co_phone,
    COUNT(*) - COUNT(phone) AS so_thieu_phone
FROM users;


-- =========================================
-- TÌM ĐƠN HÀNG CHƯA ĐƯỢC XỬ LÝ (chưa có ngày xử lý)
-- Dùng khi: kiểm tra đơn hàng bị "treo" chưa ai xử lý
-- =========================================
SELECT
    order_id,
    user_id,
    total_amount,
    created_at,
    processed_at     -- Cột này NULL = chưa xử lý
FROM orders
WHERE processed_at IS NULL
ORDER BY created_at ASC;    -- Đơn cũ nhất lên đầu (tồn đọng lâu nhất)


-- =========================================
-- KẾT HỢP IS NULL VỚI ĐIỀU KIỆN KHÁC
-- Dùng khi: đơn hàng completed nhưng chưa có ngày giao
-- (Dữ liệu bất thường — cần kiểm tra)
-- =========================================
SELECT *
FROM orders
WHERE status = 'completed'
  AND delivered_at IS NULL;   -- Hoàn thành nhưng chưa có ngày giao → lỗi dữ liệu


-- =========================================
-- THAY THẾ NULL BẰNG GIÁ TRỊ KHÁC khi hiển thị
-- Dùng COALESCE(cột, 'giá trị thay thế')
-- Dùng khi: muốn hiển thị "Chưa cập nhật" thay vì NULL
-- =========================================
SELECT
    user_id,
    full_name,
    COALESCE(email, 'Chưa có email')     AS email,
    COALESCE(phone, 'Chưa có SĐT')      AS phone,
    COALESCE(city, 'Chưa cập nhật')     AS city
FROM users;


-- =========================================
-- LỌC KẾT QUẢ JOIN: User không có đơn hàng nào
-- Dùng khi: tìm user đăng ký nhưng chưa mua hàng lần nào
-- Cần dùng LEFT JOIN để giữ lại user không có đơn
-- =========================================
SELECT
    u.user_id,
    u.full_name,
    u.email,
    u.created_at AS ngay_dang_ky
FROM users AS u
LEFT JOIN orders AS o          -- LEFT JOIN: giữ tất cả user dù không có đơn
    ON u.user_id = o.user_id
WHERE o.order_id IS NULL       -- Chỉ lấy user không có đơn hàng nào
ORDER BY u.created_at DESC;
```

### ⚠️ Ghi chú lưu ý thường gặp

| Điều cần nhớ | Chi tiết |
|-------------|---------|
| **Không dùng `= NULL`** | `WHERE email = NULL` → **luôn sai**, không ra kết quả. Phải dùng `IS NULL` |
| NULL ≠ chuỗi rỗng | `''` (rỗng) và `NULL` là **khác nhau hoàn toàn** |
| NULL ≠ số 0 | `0` và `NULL` là **khác nhau hoàn toàn** |
| `COUNT(*)` vs `COUNT(cột)` | `COUNT(*)` đếm cả dòng NULL; `COUNT(email)` bỏ qua dòng email là NULL |
| Kiểm tra cả 2 | Nên check cả `IS NULL` lẫn `= ''` để chắc chắn |

---

## 🎯 BẢNG TỔNG HỢP NHANH

### Các từ khóa hay dùng

| Từ khóa | Ý nghĩa | Ví dụ |
|---------|---------|-------|
| `SELECT *` | Lấy tất cả cột | `SELECT * FROM users` |
| `WHERE` | Lọc theo điều kiện | `WHERE status = 'active'` |
| `AND` | Và — cả 2 điều kiện phải đúng | `WHERE a = 1 AND b = 2` |
| `OR` | Hoặc — 1 trong 2 điều kiện đúng | `WHERE a = 1 OR a = 2` |
| `LIKE '%x%'` | Chứa ký tự x | `WHERE name LIKE '%Nguyễn%'` |
| `IN (...)` | Trong danh sách | `WHERE city IN ('HN', 'HCM')` |
| `BETWEEN` | Trong khoảng | `WHERE age BETWEEN 18 AND 30` |
| `ORDER BY ... DESC` | Sắp xếp mới nhất trước | `ORDER BY created_at DESC` |
| `LIMIT n` | Chỉ lấy n dòng | `LIMIT 10` |
| `COUNT(*)` | Đếm số dòng | `SELECT COUNT(*) FROM orders` |
| `GROUP BY` | Gom nhóm | `GROUP BY status` |
| `INNER JOIN ... ON` | Nối 2 bảng | `JOIN users ON o.user_id = u.user_id` |
| `IS NULL` | Bị thiếu dữ liệu | `WHERE email IS NULL` |
| `IS NOT NULL` | Đã có dữ liệu | `WHERE email IS NOT NULL` |

### Thứ tự viết câu SQL

```sql
SELECT   ...      -- 1. Chọn cột nào
FROM     ...      -- 2. Từ bảng nào
JOIN     ...      -- 3. Nối với bảng nào (nếu cần)
  ON     ...      -- 4. Điều kiện nối
WHERE    ...      -- 5. Lọc điều kiện
GROUP BY ...      -- 6. Gom nhóm (nếu cần)
HAVING   ...      -- 7. Lọc nhóm (nếu cần)
ORDER BY ...      -- 8. Sắp xếp
LIMIT    ...      -- 9. Giới hạn số dòng
```

---

> ✅ **Nhớ:** Câu lệnh `SELECT` chỉ **ĐỌC** dữ liệu, hoàn toàn an toàn.
> ❌ **Tuyệt đối không** tự ý chạy `UPDATE`, `DELETE`, `DROP` trên database production.