SQL & Database — Tư tưởng & Nguyên lý
7/52
Bài 7 / 52~16 phútTruy vấn cơ bảnLộ trình · chặng 10/21Miễn phí lượt xem

SELECT — projection + alias, vì sao SELECT * chậm gấp 17 lần

Projection ở executor layer + network/serialization overhead. Cú pháp column list, expression, alias. Vì sao Prisma select() và Spring @Query đều chọn explicit column.

TL;DR: SELECT * trả về mọi column của row; SELECT id, title chỉ trả về hai column. Cùng một row được tìm qua index lookup, sự khác biệt về tốc độ không nằm ở disk I/O mà nằm ở hai bước sau: serialize từng cell sang binary protocol và gửi qua network. Column ít = byte ít = nhanh hơn. Bốn lý do production code luôn dùng explicit column list: schema fragile, N+1 amplification, covering index miss, sensitive data leak.

Cùng query trên bảng tasks 100k row của TaskFlow — SELECT * FROM tasks WHERE id=42 mất 200ms. SELECT id, title FROM tasks WHERE id=42 mất 12ms. Cùng một row được trả về, khác ở chỗ query chọn bao nhiêu column — chênh nhau 17 lần. Nếu disk I/O không phải nguyên nhân (cả hai query đều tìm đúng một row bằng primary key index), thì vì sao?

Bài này giải thích cơ chế projection diễn ra ở executor layer, cost thực sự nằm ở network và serialization, cú pháp đầy đủ của SELECT (column list, expression, alias), và vì sao mọi ORM — Prisma, Spring Data JPA, SQLAlchemy — đều đặt explicit column là best practice.

1. Analogy — Lấy hàng từ kho

Hình dung kho hàng với mỗi sản phẩm được đóng trong thùng lớn gồm 50 ngăn. Bạn chỉ cần lấy 2 món: nhãn sản phẩm và giá. Có hai cách:

Cách A: Lấy cả thùng ra xe tải, chở về kho phân phối, mở ra, lấy 2 món cần, bỏ 48 món còn lại.

Cách B: Mở thùng tại chỗ, lấy đúng 2 món, để lại 48 món trong kho.

Cách A tốn xe tải to hơn, tốn thời gian bốc dỡ, tốn không gian tạm chứa — dù kết quả cuối cùng giống nhau.

Lấy hàng từ khoSELECT
Lấy cả thùng (mọi món)SELECT *
Chỉ lấy 2 món cầnSELECT id, title
Tốn xe tải lớn hơnNetwork bandwidth — nhiều byte hơn trên dây
Tốn thời gian bốc dỡ tại kho phân phốiSerialization — database convert mọi cell sang binary protocol
Khó kiểm hàng khi danh sách thay đổiApp fragile khi schema thêm column mới
Nhận đồ trộn lẫn — phải phân loại sauDeserialize tất cả rồi app mới biết cần gì
Chuẩn bị danh sách trước khi vào khoExplicit column list = predictable cost
💡 Cách nhớ

SELECT * = lấy cả thùng. SELECT id, title = lấy đúng 2 món. Cost không nằm ở việc tìm thùng (disk I/O) — cost nằm ở chở thùng về (network) và phân loại lại (serialization).

2. Cơ chế projection — diễn ra ở đâu

Hệ quản trị xử lý query theo pipeline. Với SELECT id, title FROM tasks WHERE id=42:

Pipeline chia hai khung: khung trên là bước 1 và 2 gồm index scan trên id để tìm hàng 42 rồi WHERE lọc hàng, hai query giống nhau ở đây; khung dưới là bước 3 và 4 gồm project giữ 2 cột hay giữ cả 12 cột rồi serialize sang binary protocol và đẩy qua network, chênh lệch nằm ở đây

Projection diễn ra ở bước 3 — sau khi scan và filter xong. Vì vậy:

  • SELECT *SELECT id, title đọc cùng lượng disk I/O để tìm row (cùng index lookup trên id).
  • Sự khác biệt nằm ở bước 3 và 4: project giữ bao nhiêu column → serialize bao nhiêu byte → gửi bao nhiêu qua network.

Với bảng tasks có 12 column (bao gồm description TEXT có thể chứa vài KB), SELECT * có thể trả về row nặng hơn SELECT id, title hàng chục lần. Multiply với 100k row trong một batch query — chênh lệch rõ ràng.

Hầu hết hệ quản trị cung cấp công cụ xem kế hoạch thực thi (execution plan) để kiểm tra số byte trung bình mỗi row (width) được trả về — ví dụ EXPLAIN trong SQL Server / PostgreSQL, EXPLAIN hoặc DESCRIBE trong MySQL. Bài Module 6 — Storage & indexing sẽ giới thiệu cách đọc execution plan.

Thử ngẫmbảng tasks của bạn thêm cột attachments_blob nặng vài trăm KB. Query cũ dùng SELECT * chậm đi ở bước nào trong bốn bước trên, và bước tìm row bằng index có bị ảnh hưởng không?

3. Cú pháp đầy đủ

SQL
-- 1. Column list explicit (best practice cho app code)
SELECT id, title, status FROM tasks;

-- 2. Toan bo column -- dung cho exploration, khong dung trong app code
SELECT * FROM tasks;

-- 3. Expression trong SELECT -- tinh toan gia tri tu column
SELECT
    id,
    UPPER(title)                                        AS title_upper,
    EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - created_at)) AS age_seconds,
    CASE WHEN status = 'done' THEN 1 ELSE 0 END         AS is_done_int
FROM tasks;

-- 4. Alias -- doi ten column trong output
SELECT
    id          AS task_id,
    title       AS "Tieu de"   -- quote khi alias co space hoac trung keyword
FROM tasks;

-- 5. SELECT khong tu table -- constant query, ham utility
SELECT 1 + 1 AS sum, CURRENT_TIMESTAMP AS current_time;

Column trong SELECT list có thể là:

  • Tên column trực tiếp: id, title
  • Expression: UPPER(title), salary * 12, CASE WHEN status = 'done' THEN 1 ELSE 0 END
  • Constant: 1, 'hello', true
  • Function call: CURRENT_TIMESTAMP, LENGTH(title), COALESCE(assignee_id, 0)

4. Alias — quy tắc và pitfall

Alias với AS:

SQL
SELECT
    id          AS task_id,          -- doi ten column trong output
    title       AS task_title,
    status      AS current_status
FROM tasks;

AS là optional — SELECT id task_id cũng hoạt động — nhưng nên viết explicit để code dễ đọc.

Quote alias khi cần:

SQL
SELECT
    title       AS "Tieu de cong viec",  -- co space -> phai quote
    status      AS "order"               -- 'order' la keyword -> nen quote
FROM tasks;

Alias không được quote: hầu hết hệ quản trị tự lowercase (TaskId thành taskid). Alias được quote: preserve case chính xác.

Alias không dùng được trong WHERE cùng query:

SQL
-- SAI: alias annual_salary chua ton tai khi WHERE duoc tinh
SELECT salary * 12 AS annual_salary
FROM employees
WHERE annual_salary > 100000;
-- ERROR: column "annual_salary" does not exist

Lý do: thứ tự xử lý của SQL engine là FROM → WHERE → SELECT → ORDER BY. Alias trong SELECT chỉ tồn tại sau khi SELECT clause được evaluate — tức là sau khi WHERE đã chạy xong. Hai cách fix:

SQL
-- Fix 1: lap lai expression trong WHERE
SELECT salary * 12 AS annual_salary
FROM employees
WHERE salary * 12 > 100000;

-- Fix 2: wrap subquery (hoac CTE -- Module 4 cua khoa nay)
SELECT *
FROM (
    SELECT *, salary * 12 AS annual_salary FROM employees
) sub
WHERE annual_salary > 100000;
Pitfall — alias trong WHERE không hoạt động

WHERE annual_salary > 100000 sau SELECT salary * 12 AS annual_salary luôn báo lỗi "column does not exist". SQL engine chạy WHERE trước SELECT — alias chưa tồn tại tại thời điểm WHERE được evaluate. Fix: lặp lại expression trong WHERE, hoặc dùng subquery/CTE.

Thử ngẫmORDER BY annual_salary (thay vì WHERE) trên cùng câu SELECT có báo lỗi tương tự không? Thứ tự xử lý FROM → WHERE → SELECT → ORDER BY trả lời câu này thế nào?

5. Vì sao SELECT * có hại trong production

Bốn lý do thực tế:

1. Schema fragile:

SQL
-- Schema hom nay: users(id, email, name, created_at)
SELECT * FROM users;  -- tra ve 4 column

-- Sau khi DBA them column: users(id, email, name, created_at, last_login_at, is_active, ...)
SELECT * FROM users;  -- dot nhien tra ve 7 column
-- App code parse theo thu tu column truoc -> parse sai hoac broken

Explicit column list không bị ảnh hưởng khi schema thêm column — chỉ trả về đúng những gì đã khai báo.

2. N+1 amplification:

Với lazy load ORM + SELECT *, mỗi entity fetch 50 column thay vì 5 column cần dùng. Multiply với 1000 entity trong một request — memory và network sử dụng gấp 10 lần không cần thiết.

3. Covering index miss:

Khi tất cả column cần thiết đã có trong index (covering index), hệ quản trị có thể trả về kết quả từ index mà không cần đọc bảng chính. SELECT * phá vỡ optimization này vì index không bao giờ chứa mọi column — database phải hit bảng mỗi row. Module 6 — Storage & indexing sẽ phân tích covering index chi tiết.

4. Sensitive data leak:

Pitfall — SELECT * rò rỉ dữ liệu nhạy cảm

SELECT * FROM users trả về cả password_hash, reset_token, payment_info — dù API endpoint chỉ cần idemail. Nếu code serialize toàn bộ query result thành JSON response, sensitive column bị expose mà không ai để ý. Explicit column list là lớp bảo vệ đầu tiên, đơn giản nhất.

6. Khi nào SELECT * vẫn ổn

SELECT * không phải lúc nào cũng sai — context quyết định:

ContextSELECT * có ổn không?Lý do
Ad-hoc exploration trong SQL client (LIMIT 5)ỔnKhông phải app code
Migration script chuyển full tableỔnCần tất cả column để copy
Backup scriptỔnExport toàn bộ dữ liệu
View definitionThường ổnView là alias, không phải app code
Production app code (REST API, service)Không ổnSchema fragile + performance + security
ORM findMany() không có selectKhông ổnTương đương SELECT *

Pattern ngắn gọn: production app code = explicit column. SQL client/migration/exploration = SELECT * ổn.

Thử ngẫmview active_tasks của bạn định nghĩa bằng SELECT *, rồi một service khác query lại SELECT * FROM active_tasks trong route handler. View còn nằm ở hàng "thường ổn" nữa không?

7. Applied — ORM convention

Lý do explicit column là best practice lan rộng sang mọi ORM:

Prisma:

TypeScript
// KHONG tot -- tuong duong SELECT *
const tasks = await prisma.task.findMany();

// Tot -- explicit column, type-safe, generated types match
const tasks = await prisma.task.findMany({
  select: {
    id: true,
    title: true,
    status: true,
  },
});
// tasks co kieu: { id: bigint; title: string; status: string }[]

Prisma select generate type narrowed — TypeScript biết chính xác những field nào có trong kết quả. Thêm select: { passwordHash: true } thì type tự xuất hiện — không thêm thì không có trong type, không thể truy cập nhầm.

Spring Data JPA:

Java
// JPQL explicit column -- DTO projection
@Query("SELECT u.id, u.email FROM User u WHERE u.isActive = true")
List<Object[]> findActiveUserProjection();

// Hoac dung DTO projection interface (Spring Data 2.x+)
public interface UserSummary {
    Long getId();
    String getEmail();
}
List<UserSummary> findByIsActive(boolean isActive);

node-postgres:

JavaScript
// Explicit column -- an toan va ro rang ve intent
const { rows } = await pool.query(
  'SELECT id, title, status FROM tasks WHERE assignee_id = $1',
  [userId]
);

SQLAlchemy:

Python
# Explicit column projection
results = session.query(User.id, User.email).filter(User.is_active == True).all()
# Khong: session.query(User).all()  -- equivalent SELECT *

Mọi ORM đều có cơ chế explicit projection vì lý do giống nhau: giảm byte truyền qua wire, tránh schema fragile, và tránh leak dữ liệu nhạy cảm qua serialization tự động.

8. Deep Dive — SELECT execution

📚 Deep Dive — SELECT execution

Ghi chú: Wikipedia cho cú pháp chuẩn agnostic; Use The Index Luke cho perf rationale; PG protocol chỉ khi dùng PostgreSQL và muốn deep optimization.

9. Liên hệ các bài khác

  • Bài 02 — WHERE + NULL three-valued logic: WHERE clause lọc row trước khi projection diễn ra — hiểu thứ tự xử lý (FROM → WHERE → SELECT) giải thích vì sao alias không dùng được trong WHERE.
  • Bài 03 — ORDER BY + pagination: sau khi projection, số column ảnh hưởng đến chi phí serialize mỗi page — explicit column đặc biệt quan trọng khi fetch nhiều page.
  • Module 6 — Storage & indexing: covering index — khi mọi column cần thiết đã có trong index, hệ quản trị không cần đọc bảng chính; SELECT * phá vỡ optimization này.
  • Module 4 — SQL nâng cao: CTE là cách portable để dùng alias trong WHERE — pattern fix thứ hai cho pitfall alias.

10. Tóm tắt

  • Projection chạy ở executor layer — sau scan và WHERE filter, trước serialize/network.
  • Cost chính nằm ở network và serialization, không phải disk I/O — lý do SELECT * vs SELECT id, title chênh nhiều dù cùng row.
  • Cú pháp SELECT: column list, expression (UPPER, EXTRACT, cast), alias với AS (quote khi có space hoặc reserved word).
  • Alias không dùng được trong WHERE cùng query — alias chưa tồn tại khi WHERE được evaluate. Fix: lặp lại expression hoặc dùng subquery/CTE.
  • SELECT * có 4 vấn đề production: schema fragile, N+1 amplification, covering index miss, sensitive data leak.
  • SELECT * ổn trong: SQL client exploration, migration script, backup, view definition — không phải app code.
  • Mọi ORM đều reflect tư duy này: Prisma select:{}, Spring DTO projection, SQLAlchemy query(Model.col).
  • Forward links: Module 6 — Storage & indexing (covering index) + Module 4 — Subquery & CTE (subquery/CTE) của khoá này.

11. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Vì sao SELECT * chậm hơn SELECT id, title dù cả hai đều tìm đúng một row bằng index lookup — disk I/O không tăng nhiều?
  2. Q2
    Phân biệt 4 trường hợp SELECT * ổn và ít nhất 4 trường hợp không ổn. Cho ví dụ cụ thể với TaskFlow.
  3. Q3
    Bạn viết `SELECT salary * 12 AS annual_salary FROM employees WHERE annual_salary > 100000` — error. Vì sao? Hai cách fix?
  4. Q4
    Prisma `select: { id: true, email: true }` thay vì `findMany()` tiết kiệm gì cụ thể? Đo bằng cách nào trong dự án thực?
  5. Q5
    Schema thay đổi: thêm column `last_login_at` vào bảng `users`. App code cũ `SELECT * FROM users` và `SELECT id, email FROM users` — cái nào bị ảnh hưởng và bị ảnh hưởng thế nào?

Bài tiếp theo: WHERE + NULL three-valued logic

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên

Bài tiếp theo

WHERE + NULL three-valued logic — vì sao age != 30 không trả NULL row