Back to blog

SQL Cơ Bản: SELECT, JOIN và Subqueries

sqldatabasepostgresqlbackend
SQL Cơ Bản: SELECT, JOIN và Subqueries

Giới Thiệu

SQL đã 50 tuổi và vẫn là ngôn ngữ quan trọng nhất trong thế giới backend. Mọi database lớn — PostgreSQL, MySQL, SQLite, BigQuery, Snowflake, Redshift — đều nói SQL. Các ORM tạo ra nó đằng sau hậu trường. Biết SQL có nghĩa là bạn có thể kiểm tra những gì ORM của mình thực sự đang làm, tối ưu các slow queries và viết logic mà nếu làm trong application code sẽ rất khó khăn.

Bài này bao gồm SQL cơ bản: cách lấy chính xác dữ liệu bạn cần, cách kết hợp dữ liệu từ nhiều tables và cách tổng hợp nó thành các tóm tắt hữu ích. Tất cả ví dụ chạy trên PostgreSQL, nhưng cú pháp cũng hoạt động trong MySQL và SQLite (với các biến thể nhỏ được ghi chú khi khác nhau).

Yêu Cầu Trước: Bài này là một phần của Lộ Trình Học SQL & NoSQL. Biết tables và rows là gì là đủ để bắt đầu.

Những Gì Bạn Sẽ Học

✅ Viết SELECT queries từ đầu
✅ Filter, sắp xếp và phân trang kết quả
✅ Tổng hợp dữ liệu với GROUP BY và HAVING
✅ Hiểu mọi loại JOIN với sơ đồ minh họa
✅ Dùng subqueries trong WHERE và FROM
✅ Biết thứ tự thực thi thực sự của SQL
✅ Tránh các lỗi phổ biến trả về kết quả sai một cách âm thầm


Schema Mẫu

Tất cả ví dụ dùng một schema e-commerce đơn giản. Thiết lập một lần và theo dõi:

CREATE TABLE customers (
    id         SERIAL PRIMARY KEY,
    name       VARCHAR(100) NOT NULL,
    email      VARCHAR(255) NOT NULL UNIQUE,
    country    VARCHAR(50)  NOT NULL,
    created_at TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);
 
CREATE TABLE products (
    id       SERIAL PRIMARY KEY,
    name     VARCHAR(100)   NOT NULL,
    category VARCHAR(50)    NOT NULL,
    price    NUMERIC(10, 2) NOT NULL
);
 
CREATE TABLE orders (
    id          SERIAL PRIMARY KEY,
    customer_id INT            NOT NULL REFERENCES customers(id),
    total       NUMERIC(10, 2) NOT NULL,
    status      VARCHAR(20)    NOT NULL DEFAULT 'pending',
    created_at  TIMESTAMPTZ    NOT NULL DEFAULT NOW()
);
 
CREATE TABLE order_items (
    id         SERIAL PRIMARY KEY,
    order_id   INT            NOT NULL REFERENCES orders(id),
    product_id INT            NOT NULL REFERENCES products(id),
    quantity   INT            NOT NULL,
    unit_price NUMERIC(10, 2) NOT NULL
);

Thêm dữ liệu mẫu:

INSERT INTO customers (name, email, country) VALUES
    ('Alice Johnson', '[email protected]', 'US'),
    ('Bob Smith',     '[email protected]',   'UK'),
    ('Carol White',   '[email protected]', 'US'),
    ('David Lee',     '[email protected]', 'CA');
 
INSERT INTO products (name, category, price) VALUES
    ('Laptop Pro',    'Electronics', 1299.00),
    ('Wireless Mouse','Electronics',   49.99),
    ('Desk Chair',    'Furniture',    399.00),
    ('Standing Desk', 'Furniture',    699.00),
    ('USB Hub',       'Electronics',   29.99);
 
INSERT INTO orders (customer_id, total, status) VALUES
    (1, 1348.99, 'completed'),
    (1,   399.00, 'completed'),
    (2,   699.00, 'pending'),
    (3,   129.97, 'completed'),
    (4,  1299.00, 'cancelled');
 
INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES
    (1, 1, 1, 1299.00),
    (1, 2, 1,   49.99),
    (2, 3, 1,  399.00),
    (3, 4, 1,  699.00),
    (4, 2, 3,   49.99),
    (5, 1, 1, 1299.00);

SELECT: Lấy Dữ Liệu

Câu lệnh SELECT là nền tảng của SQL. Dạng tối giản:

SELECT column1, column2
FROM table_name;

Chọn Các Cột Cụ Thể

Luôn chỉ chọn các cột bạn cần. SELECT * tiện lợi để khám phá nhưng lãng phí băng thông, làm queries dễ vỡ khi schema thay đổi và ngăn index-only scans.

-- Tệ: trả về mọi thứ, kể cả các cột bạn không dùng
SELECT * FROM customers;
 
-- Tốt: chỉ những gì bạn cần
SELECT id, name, email FROM customers;

Cột Được Tính Toán và Alias

Bạn có thể tính toán các giá trị mới trực tiếp trong SELECT và đặt tên dễ đọc cho cột bằng AS:

SELECT
    name,
    price,
    price * 1.1 AS price_with_tax,
    UPPER(category) AS category
FROM products;

DISTINCT: Loại Bỏ Bản Sao

DISTINCT loại bỏ các rows trùng lặp khỏi kết quả. Nó hoạt động trên toàn bộ row, không chỉ một cột:

-- Tất cả các quốc gia duy nhất trong bảng customers
SELECT DISTINCT country FROM customers;
 
-- Các kết hợp (category, country) duy nhất — không chỉ các category duy nhất
SELECT DISTINCT category FROM products;

WHERE: Lọc Rows

WHERE lọc các rows nào được đưa vào. Nó được đánh giá trước SELECT, vì vậy bạn có thể lọc trên các cột không có trong output.

Toán Tử So Sánh

SELECT name, price FROM products WHERE price > 100;
SELECT name, price FROM products WHERE price = 49.99;
SELECT name FROM products WHERE category != 'Furniture';

Toán Tử Logic: AND, OR, NOT

-- Electronics dưới $100
SELECT name, price
FROM products
WHERE category = 'Electronics' AND price < 100;
 
-- Electronics hoặc bất cứ thứ gì trên $500
SELECT name, price
FROM products
WHERE category = 'Electronics' OR price > 500;
 
-- Mọi thứ trừ Furniture
SELECT name FROM products
WHERE NOT category = 'Furniture';

Thứ tự ưu tiên toán tử: AND kết hợp chặt hơn OR. Dùng dấu ngoặc để rõ ràng:

-- Không có dấu ngoặc: category='Electronics' AND (price < 50 OR price > 1000)
-- Đây có thể KHÔNG phải điều bạn muốn:
SELECT name FROM products
WHERE category = 'Electronics' AND price < 50
   OR price > 1000;
 
-- Với dấu ngoặc — rõ ràng là electronics hoặc đắt tiền:
SELECT name FROM products
WHERE (category = 'Electronics' AND price < 50)
   OR price > 1000;

IN: Khớp Với Bất Kỳ Giá Trị Nào Trong Danh Sách

SELECT name FROM products
WHERE category IN ('Electronics', 'Furniture');
 
-- Tương đương với:
SELECT name FROM products
WHERE category = 'Electronics' OR category = 'Furniture';

NOT IN loại trừ các giá trị — nhưng cẩn thận: NOT IN không trả về rows nếu danh sách chứa NULL. Đây là một bug phổ biến:

-- Nếu bất kỳ status nào trong danh sách là NULL, điều này không trả về gì
SELECT * FROM orders WHERE status NOT IN ('pending', NULL); -- SAI
 
-- Thay thế an toàn:
SELECT * FROM orders WHERE status != 'pending';

BETWEEN: Lọc Theo Khoảng

SELECT name, price FROM products
WHERE price BETWEEN 50 AND 500;
-- Tương đương: WHERE price >= 50 AND price <= 500 (bao gồm cả hai đầu)

LIKE và ILIKE: Khớp Pattern

% khớp với bất kỳ chuỗi ký tự nào. _ khớp chính xác một ký tự.

-- Tên bắt đầu bằng 'A'
SELECT name FROM customers WHERE name LIKE 'A%';
 
-- Tên chứa 'son'
SELECT name FROM customers WHERE name LIKE '%son%';
 
-- ILIKE không phân biệt hoa thường (đặc biệt của PostgreSQL)
SELECT name FROM customers WHERE name ILIKE 'alice%';

IS NULL / IS NOT NULL

NULL có nghĩa là "không biết" — nó không phải là một giá trị. Bạn không thể so sánh bằng =:

-- SAI: luôn không trả về rows nào
SELECT * FROM orders WHERE cancelled_at = NULL;
 
-- ĐÚNG:
SELECT * FROM orders WHERE cancelled_at IS NULL;
SELECT * FROM orders WHERE cancelled_at IS NOT NULL;

ORDER BY: Sắp Xếp Kết Quả

ORDER BY sắp xếp kết quả. Mặc định là tăng dần (ASC). Dùng DESC để đảo ngược.

-- Từ rẻ nhất đến đắt nhất
SELECT name, price FROM products ORDER BY price ASC;
 
-- Đắt nhất trước
SELECT name, price FROM products ORDER BY price DESC;
 
-- Sắp xếp theo category, sau đó theo price trong mỗi category
SELECT name, category, price
FROM products
ORDER BY category ASC, price DESC;

NULL ordering: Theo mặc định PostgreSQL sắp xếp NULL cuối cùng trong ASC, đầu tiên trong DESC. Kiểm soát điều này một cách rõ ràng:

ORDER BY created_at DESC NULLS LAST;

LIMIT và OFFSET: Phân Trang

-- 3 sản phẩm đầu tiên
SELECT name FROM products ORDER BY id LIMIT 3;
 
-- Sản phẩm 4-6 (bỏ qua 3 sản phẩm đầu)
SELECT name FROM products ORDER BY id LIMIT 3 OFFSET 3;

Cảnh báo: Phân trang bằng OFFSET chậm trên các tables lớn vì database phải quét và bỏ qua các rows đã skip. Với phân trang production, ưu tiên cursor-based pagination dùng ID cuối cùng đã thấy:

-- Trang 1
SELECT id, name FROM products ORDER BY id LIMIT 10;
 
-- Trang 2 (cursor-based — nhanh bất kể độ sâu)
SELECT id, name FROM products
WHERE id > 10  -- id cuối cùng từ trang trước
ORDER BY id LIMIT 10;

Aggregation: GROUP BY và HAVING

Các aggregation functions thu gọn nhiều rows thành một giá trị tóm tắt duy nhất.

Các Aggregate Functions

SELECT COUNT(*)           FROM orders;              -- tổng số orders
SELECT COUNT(DISTINCT customer_id) FROM orders;     -- số customers duy nhất đã đặt hàng
SELECT SUM(total)         FROM orders;              -- tổng doanh thu
SELECT AVG(price)         FROM products;            -- giá trung bình
SELECT MIN(price)         FROM products;            -- sản phẩm rẻ nhất
SELECT MAX(price)         FROM products;            -- sản phẩm đắt nhất

GROUP BY: Tổng Hợp Theo Nhóm

GROUP BY chia rows thành các nhóm và áp dụng aggregate functions cho từng nhóm độc lập.

-- Số lượng sản phẩm theo category
SELECT category, COUNT(*) AS product_count
FROM products
GROUP BY category;

Kết quả:

categoryproduct_count
Electronics3
Furniture2

Quy tắc GROUP BY: Mỗi cột trong SELECT phải nằm trong GROUP BY hoặc được bọc trong aggregate function. Query này không hợp lệ:

-- KHÔNG HỢP LỆ: 'name' không được aggregate hay trong GROUP BY
SELECT category, name, COUNT(*) FROM products GROUP BY category;

Các grouping hữu ích hơn:

-- Tổng doanh thu theo customer
SELECT customer_id, SUM(total) AS total_spent
FROM orders
GROUP BY customer_id
ORDER BY total_spent DESC;
 
-- Số lượng orders theo status
SELECT status, COUNT(*) AS count
FROM orders
GROUP BY status;

HAVING: Lọc Các Nhóm

WHERE lọc rows trước khi nhóm. HAVING lọc các nhóm sau khi tổng hợp.

-- Chỉ các categories có hơn 2 sản phẩm
SELECT category, COUNT(*) AS product_count
FROM products
GROUP BY category
HAVING COUNT(*) > 2;
 
-- Customers đã chi hơn $500 tổng cộng
SELECT customer_id, SUM(total) AS total_spent
FROM orders
GROUP BY customer_id
HAVING SUM(total) > 500;

Bạn không thể dùng column alias trong HAVING — nó chưa được tính toán ở giai đoạn đó:

-- KHÔNG HỢP LỆ trong hầu hết databases:
SELECT customer_id, SUM(total) AS total_spent
FROM orders
GROUP BY customer_id
HAVING total_spent > 500;  -- 'total_spent' không tồn tại ở đây
 
-- HỢP LỆ:
HAVING SUM(total) > 500;

Thứ Tự Thực Thi SQL

Đây là một trong những điều quan trọng nhất cần hiểu về SQL. Thứ tự bạn viết các mệnh đề không phải là thứ tự chúng thực thi:

Thứ tự thực thi:
  1. FROM       — xác định các bảng nguồn
  2. JOIN       — kết hợp với các bảng khác
  3. WHERE      — lọc các rows riêng lẻ
  4. GROUP BY   — nhóm các rows
  5. HAVING     — lọc các nhóm
  6. SELECT     — tính toán các cột output
  7. DISTINCT   — loại bỏ các rows trùng lặp
  8. ORDER BY   — sắp xếp kết quả
  9. LIMIT      — cắt bớt kết quả

Điều này giải thích một số hành vi khó hiểu:

  • Bạn không thể dùng SELECT alias trong WHERE — WHERE chạy trước SELECT
  • Bạn không thể dùng SELECT alias trong HAVING — cùng lý do
  • Bạn có thể dùng SELECT alias trong ORDER BY — ORDER BY chạy sau SELECT
  • WHERE lọc trước khi nhóm, HAVING lọc sau
-- Đây là lý do tại sao điều này thất bại:
SELECT total * 1.1 AS total_with_tax
FROM orders
WHERE total_with_tax > 500;  -- LỖI: cột chưa tồn tại
 
-- Sửa: lặp lại biểu thức
WHERE total * 1.1 > 500;

JOINs: Kết Hợp Các Tables

JOINs kết hợp các rows từ hai hoặc nhiều tables dựa trên một cột liên quan. Điều kiện join phổ biến nhất là quan hệ foreign key.

INNER JOIN

Chỉ trả về các rows có kết quả khớp trong cả hai tables.

SELECT
    o.id AS order_id,
    c.name AS customer_name,
    o.total,
    o.status
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id;

Chỉ các orders có customer khớp mới được trả về. Trong schema được thiết kế tốt với foreign key constraint, đây là mọi row. Nhưng nếu customer_id có thể là NULL hoặc tham chiếu đến customer đã bị xóa, INNER JOIN sẽ âm thầm bỏ qua các orders đó.

Table aliases (o cho orders, c cho customers) rất cần thiết khi join — chúng ngăn các tham chiếu cột mơ hồ và làm queries dễ đọc.

LEFT JOIN (LEFT OUTER JOIN)

Trả về tất cả rows từ bảng bên trái cộng với các rows khớp từ bảng bên phải. Các rows không có kết quả khớp nhận NULL cho tất cả các cột của bảng bên phải.

-- Tất cả customers, kể cả những người chưa có orders
SELECT
    c.name,
    c.email,
    COUNT(o.id) AS order_count,
    COALESCE(SUM(o.total), 0) AS total_spent
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name, c.email
ORDER BY total_spent DESC;

COALESCE(value, fallback) thay thế NULL bằng giá trị dự phòng. Ở đây nó biến NULL (customer không có orders) thành 0.

Tìm rows không có kết quả khớp — cách dùng LEFT JOIN kinh điển:

-- Customers chưa bao giờ đặt hàng
SELECT c.name, c.email
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL;

RIGHT JOIN (RIGHT OUTER JOIN)

Đối xứng với LEFT JOIN — tất cả rows từ bảng bên phải, cộng với các kết quả khớp từ bảng bên trái. RIGHT join hiếm gặp trong thực tế vì bạn luôn có thể viết lại chúng như LEFT JOIN bằng cách đổi thứ tự bảng.

-- Tất cả orders, kể cả những cái không có customer khớp
SELECT c.name, o.id, o.total
FROM customers c
RIGHT JOIN orders o ON o.customer_id = c.id;
 
-- LEFT JOIN tương đương (dễ đọc hơn):
SELECT c.name, o.id, o.total
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.id;

FULL OUTER JOIN

Trả về tất cả rows từ cả hai tables. Các rows không có kết quả khớp ở phía kia nhận NULL cho phía bị thiếu.

-- Tất cả customers VÀ tất cả orders, khớp khi có thể
SELECT c.name, o.id AS order_id, o.total
FROM customers c
FULL OUTER JOIN orders o ON o.customer_id = c.id;

Use cases: đối chiếu hai datasets, tìm rows thiếu ở một trong hai phía.

CROSS JOIN

Trả về tích Descartes — mọi row của bảng bên trái kết hợp với mọi row của bảng bên phải. Không có mệnh đề ON.

-- Mỗi customer được ghép với mỗi sản phẩm (hiếm khi hữu ích, nhưng hợp lệ)
SELECT c.name, p.name AS product
FROM customers c
CROSS JOIN products p;
-- 4 customers × 5 sản phẩm = 20 rows

Ứng dụng thực tế: tạo ra tất cả các tổ hợp cho bảng lịch trình, ghép kích thước với màu sắc, v.v.

Tóm Tắt JOIN

Join Hơn Hai Tables

Nối chuỗi các JOINs để kết hợp ba hoặc nhiều tables hơn:

-- Chi tiết đơn hàng: tên customer, tên sản phẩm, số lượng, đơn giá
SELECT
    c.name     AS customer,
    p.name     AS product,
    oi.quantity,
    oi.unit_price,
    oi.quantity * oi.unit_price AS line_total
FROM order_items oi
INNER JOIN orders   o ON oi.order_id   = o.id
INNER JOIN customers c ON o.customer_id = c.id
INNER JOIN products  p ON oi.product_id = p.id
ORDER BY c.name, o.id;

Subqueries

Subquery là một SELECT query được lồng bên trong một query khác. Chúng xuất hiện ở ba vị trí: WHERE, FROM và SELECT.

Subquery trong WHERE

Lọc dựa trên kết quả của một query khác:

-- Customers đã đặt ít nhất một order
SELECT name, email
FROM customers
WHERE id IN (
    SELECT DISTINCT customer_id FROM orders
);
 
-- Sản phẩm đắt hơn mức trung bình
SELECT name, price
FROM products
WHERE price > (
    SELECT AVG(price) FROM products
);

Query bên trong chạy trước và kết quả của nó được dùng bởi query bên ngoài.

Correlated Subquery

Correlated subquery tham chiếu các cột từ query bên ngoài. Nó chạy một lần cho mỗi row của query bên ngoài — điều này làm nó mạnh mẽ nhưng có thể chậm:

-- Ngày đặt hàng gần nhất của mỗi customer
SELECT
    c.name,
    (
        SELECT MAX(o.created_at)
        FROM orders o
        WHERE o.customer_id = c.id  -- tham chiếu đến 'c' của query bên ngoài
    ) AS last_order_date
FROM customers c;

Với các tables lớn, pattern này thường được diễn đạt tốt hơn như một JOIN hoặc window function (được đề cập trong DB-3). Nhưng với các datasets nhỏ thì hoàn toàn ổn.

EXISTS và NOT EXISTS

EXISTS kiểm tra xem một subquery có trả về bất kỳ rows nào không. Nó dừng ngay khi tìm thấy một kết quả khớp, làm cho nó hiệu quả hơn IN với các datasets lớn:

-- Customers đã đặt ít nhất một order (phiên bản EXISTS)
SELECT name
FROM customers c
WHERE EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.customer_id = c.id
);
 
-- Customers không có orders (NOT EXISTS)
SELECT name
FROM customers c
WHERE NOT EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.customer_id = c.id
);

SELECT 1 bên trong EXISTS là quy ước — các cột thực tế không quan trọng, chỉ quan trọng là có row nào khớp không.

Subquery trong FROM (Derived Table)

Subquery trong FROM hoạt động như một bảng tạm thời. Bạn phải đặt alias cho nó:

-- Trung bình của tổng theo từng customer
SELECT AVG(customer_total) AS avg_customer_spend
FROM (
    SELECT customer_id, SUM(total) AS customer_total
    FROM orders
    GROUP BY customer_id
) AS customer_totals;

Derived tables hữu ích cho các aggregation nhiều bước. Chúng đang được thay thế bởi CTEs trong SQL hiện đại (được đề cập trong DB-3), nhưng bạn sẽ gặp chúng thường xuyên trong code cũ.


Các Pattern Query Thực Tế

Các queries thực tế kết hợp nhiều mệnh đề. Đây là những patterns bạn sẽ viết thường xuyên.

Top N Theo Nhóm

Customers đã chi nhiều nhất:

SELECT
    c.name,
    SUM(o.total) AS total_spent
FROM customers c
INNER JOIN orders o ON o.customer_id = c.id
WHERE o.status = 'completed'
GROUP BY c.id, c.name
ORDER BY total_spent DESC
LIMIT 5;

Báo Cáo: Doanh Thu Theo Tháng

SELECT
    DATE_TRUNC('month', created_at) AS month,
    COUNT(*)                         AS order_count,
    SUM(total)                       AS revenue
FROM orders
WHERE status = 'completed'
GROUP BY DATE_TRUNC('month', created_at)
ORDER BY month;

Tìm Records Mồ Côi

-- Order items tham chiếu đến product không tồn tại (kiểm tra data integrity)
SELECT oi.id, oi.product_id
FROM order_items oi
LEFT JOIN products p ON oi.product_id = p.id
WHERE p.id IS NULL;

Aggregation Nhiều Điều Kiện Với FILTER

PostgreSQL hỗ trợ FILTER trên các aggregate functions — thay thế gọn gàng hơn cho CASE WHEN:

SELECT
    COUNT(*)                               AS total_orders,
    COUNT(*) FILTER (WHERE status = 'completed')  AS completed,
    COUNT(*) FILTER (WHERE status = 'pending')    AS pending,
    COUNT(*) FILTER (WHERE status = 'cancelled')  AS cancelled
FROM orders;

Không có FILTER, kết quả tương tự cần CASE WHEN:

SELECT
    COUNT(*) AS total_orders,
    SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
    SUM(CASE WHEN status = 'pending'   THEN 1 ELSE 0 END) AS pending,
    SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) AS cancelled
FROM orders;

Các Lỗi Phổ Biến

1. Lọc Trên JOIN Condition vs WHERE

-- Hai cách này KHÔNG tương đương:
 
-- Phiên bản A: LEFT JOIN với WHERE filter
-- Thực tế trở thành INNER JOIN — nulls bị lọc bỏ
SELECT c.name, o.total
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.status = 'completed';   -- loại bỏ customers không có orders
 
-- Phiên bản B: Filter trong JOIN condition (giữ tất cả customers)
SELECT c.name, o.total
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id AND o.status = 'completed';

Đặt filters trên bảng LEFT trong WHERE. Đặt filters trên bảng RIGHT trong JOIN condition nếu bạn muốn giữ các rows của bảng bên trái.

2. COUNT(*) vs COUNT(column)

COUNT(*)         -- đếm tất cả rows, kể cả NULLs
COUNT(column)    -- đếm rows mà column KHÔNG phải NULL
COUNT(DISTINCT column)  -- đếm các giá trị duy nhất không phải NULL

3. GROUP BY Ẩn Với Cột Aggregate và Không Aggregate

-- Điều này thất bại vì 'name' không trong GROUP BY:
SELECT name, category, COUNT(*) FROM products GROUP BY category;
 
-- Sửa: bao gồm tất cả các cột không được aggregate trong GROUP BY:
SELECT name, category, COUNT(*) FROM products GROUP BY name, category;
-- (nhưng điều này đánh bại mục đích — một row cho mỗi tổ hợp name+category)

4. LIKE Với Ký Tự Đại Diện Đầu Vô Hiệu Hóa Việc Dùng Index

-- Có thể dùng index trên 'name':
WHERE name LIKE 'Alice%'
 
-- Không thể dùng index trên 'name' — full table scan:
WHERE name LIKE '%Alice%'

Với tìm kiếm substring tùy ý, dùng full-text search của PostgreSQL hoặc extension pg_trgm thay thế.


Bài Tập

Thiết lập schema mẫu và giải quyết những bài này:

  1. Liệt kê tất cả sản phẩm trong category 'Electronics' được sắp xếp theo giá tăng dần.
  2. Tìm tổng số orders theo từng customer, kể cả customers có zero orders.
  3. Tìm tổng order trung bình chỉ cho completed orders.
  4. Liệt kê customers đã chi hơn $500 tổng cộng trên các completed orders.
  5. Tìm sản phẩm xuất hiện trong ít nhất một order (dùng cả IN và EXISTS — so sánh chúng).
  6. Với mỗi order, hiển thị tên customer, tổng order và số lượng sản phẩm phân biệt trong order đó.
  7. Tìm tháng có tổng doanh thu cao nhất.

Tóm Tắt Và Những Điểm Chính

Kết Quả Học Tập:
✅ SELECT lấy cột; WHERE lọc rows; ORDER BY sắp xếp; LIMIT phân trang
✅ GROUP BY tổng hợp rows thành nhóm; HAVING lọc các nhóm đó
✅ SQL thực thi theo thứ tự logic: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
✅ INNER JOIN chỉ trả về rows khớp; LEFT JOIN giữ tất cả rows bên trái
✅ Subqueries có thể xuất hiện trong WHERE, FROM hoặc SELECT
✅ EXISTS hiệu quả hơn IN với kết quả subquery lớn
✅ Hành vi NULL là nguồn gốc của nhiều bugs — luôn dùng IS NULL, không dùng = NULL


Tiếp Theo Là Gì

DB-3: Advanced SQL — Window Functions & CTEs →

Window functions và CTEs cho phép bạn viết các queries trước đây chỉ có thể thực hiện với các subqueries phức tạp hoặc code ở tầng ứng dụng: running totals, xếp hạng theo nhóm, recursive hierarchies và các transformations nhiều bước — tất cả trong SQL thuần túy.

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.