Skip to content

Transactions, Isolation Levels và SQL Injection Prevention 🔒

Ba kịch bản phá tan ngày nghỉ cuối tuần của bạn:

  1. Server crash giữa chừng chuyển tiền: Tài khoản A đã bị trừ 1 triệu, nhưng tài khoản B chưa được cộng — tiền bốc hơi giữa không trung.
  2. Hai user mua cùng món hàng cuối cùng đồng thời — cả hai đều thành công, tồn kho xuống -1. Warehouse gọi lên hỏi: "Hàng đâu?"
  3. Hacker gõ '; DROP TABLE users; -- vào form đăng nhập — và toàn bộ bảng users biến mất trong tích tắc.

Ba vấn đề, ba giải pháp: Transactions giải quyết (1), Isolation Levels giải quyết (2), và SQL Injection Prevention giải quyết (3). Bài này dạy bạn cả ba — bởi vì trong thực tế, chúng luôn đi cùng nhau.


PART 1: ACID — Bốn Trụ Cột Của Mọi Transaction

ACID qua ví dụ chuyển tiền

Bạn chuyển 1.000.000₫ từ tài khoản A sang tài khoản B. Đây là thao tác hai bước — trừ A rồi cộng B:

sql
BEGIN;
    UPDATE accounts SET balance = balance - 1000000 WHERE id = 'A';
    -- 💥 Server crash ở đây thì sao?
    UPDATE accounts SET balance = balance + 1000000 WHERE id = 'B';
COMMIT;

Nếu server crash sau dòng đầu tiên — tiền biến mất? Không. Đây là lúc ACID bảo vệ bạn.


A — Atomicity (Nguyên tử): Tất cả hoặc Không gì cả

Transaction là một đơn vị không thể chia nhỏ. Giống như nguyên tử trong vật lý — bạn không thể chỉ thực hiện "nửa transaction".

  • Nếu mọi thứ OK → COMMIT → cả hai UPDATE được lưu vĩnh viễn
  • Nếu bất kỳ bước nào fail → tự động ROLLBACK → database quay về trạng thái trước BEGIN

Khi server crash sau dòng UPDATE accounts ... WHERE id = 'A', database tự động rollback toàn bộ transaction khi khởi động lại. Tiền trở về tài khoản A — không mất một đồng nào.

C — Consistency (Nhất quán): Từ hợp lệ đến hợp lệ

Database luôn chuyển từ trạng thái hợp lệ sang trạng thái hợp lệ khác. Nếu bạn có constraint CHECK (balance >= 0), thì cho dù có 1000 transaction chạy đồng thời, không bao giờ balance xuống dưới 0.

sql
ALTER TABLE accounts ADD CONSTRAINT positive_balance CHECK (balance >= 0);
-- Giờ đây: UPDATE accounts SET balance = balance - 999999999 WHERE id = 'A';
-- → Lỗi! Violates CHECK constraint. Transaction bị rollback.

I — Isolation (Độc lập): Không ai thấy trạng thái "dở dang"

Trong lúc transaction đang chạy — A đã bị trừ nhưng B chưa được cộng — không transaction nào khác nhìn thấy trạng thái trung gian này. Từ góc nhìn bên ngoài, transaction hoặc chưa xảy ra, hoặc đã xảy ra hoàn toàn.

D — Durability (Bền vững): COMMIT là lời hứa "sắt đá"

Khi database trả về COMMIT thành công, dữ liệu đã được ghi an toàn — sống sót qua mất điện, crash hệ thống, thậm chí hỏng ổ cứng (nếu có replication). Database dùng WAL (Write-Ahead Log) để đảm bảo: ghi log trước, ghi data sau. Nếu crash xảy ra, WAL sẽ "replay" để khôi phục.

💡 Ghi nhớ ACID

Nghĩ ACID như hợp đồng giữa bạn và database: "Tôi đảm bảo dữ liệu của bạn luôn đúng, luôn toàn vẹn, và không bao giờ ở trạng thái nửa vời."


BEGIN / COMMIT / ROLLBACK — Ba câu lệnh quyền lực

Happy path — Đặt hàng thành công

sql
BEGIN;
    -- Bước 1: Tạo đơn hàng
    INSERT INTO orders (user_id, total) VALUES (1, 500000);

    -- Bước 2: Thêm chi tiết đơn hàng
    INSERT INTO order_items (order_id, product_id, qty)
    VALUES (currval('orders_id_seq'), 42, 2);

    -- Bước 3: Giảm tồn kho
    UPDATE products SET stock = stock - 2 WHERE id = 42;
COMMIT;
-- ✅ Cả 3 bước thành công → dữ liệu được lưu

Sad path — Rollback khi có lỗi

sql
BEGIN;
    UPDATE accounts SET balance = balance - 1000000 WHERE id = 'A';

    -- Kiểm tra: A có đủ tiền không?
    -- Nếu balance < 0 (trong application code):
    ROLLBACK;  -- ← Hoàn tác TẤT CẢ — balance A trở về giá trị ban đầu

    -- Nếu OK, tiếp tục:
    UPDATE accounts SET balance = balance + 1000000 WHERE id = 'B';
COMMIT;

⚠️ Autocommit — Cái bẫy của người mới

Hầu hết database (PostgreSQL, MySQL) mặc định chạy ở chế độ autocommit: mỗi câu SQL đơn lẻ tự động được wrap trong một transaction riêng. Nghĩa là nếu bạn chạy hai câu UPDATE riêng lẻ mà không có BEGIN, chúng là hai transaction độc lập — crash giữa chừng sẽ mất data.

Khi cần nhiều câu SQL phải thành công cùng nhau → luôn dùng BEGIN...COMMIT.


SAVEPOINT — Rollback một phần

Đôi khi bạn không muốn rollback toàn bộ transaction, chỉ muốn rollback một phần. SAVEPOINT cho phép bạn đặt "checkpoint" bên trong transaction.

sql
BEGIN;
    -- Tạo đơn hàng (bước này phải giữ lại)
    INSERT INTO orders (user_id, total) VALUES (1, 500000);

    SAVEPOINT before_payment;

    -- Thử trừ tiền bằng ví điện tử
    UPDATE wallets SET balance = balance - 500000 WHERE user_id = 1;
    -- Lỗi: ví không đủ tiền!

    ROLLBACK TO SAVEPOINT before_payment;
    -- → Order VẪN CÒN, chỉ phần payment bị rollback

    -- Thử phương thức thanh toán khác: thẻ tín dụng
    INSERT INTO credit_card_charges (user_id, amount) VALUES (1, 500000);
COMMIT;
-- ✅ Order được tạo + charge thẻ tín dụng thành công

💡 Khi nào dùng SAVEPOINT?

  • Khi một transaction có nhiều bước và bạn muốn retry một bước mà không phải làm lại từ đầu
  • Phổ biến trong: payment processing (thử nhiều phương thức), batch import (skip dòng lỗi)

PART 2: Isolation Levels — Kiểm Soát Đồng Thời

Vấn đề: Hai transaction chạy cùng lúc

Trong thực tế, database không chỉ phục vụ một transaction — mà hàng trăm, hàng nghìn transaction chạy đồng thời. Câu hỏi đặt ra: transaction này có nên nhìn thấy dữ liệu đang được thay đổi bởi transaction khác không?

Câu trả lời phụ thuộc vào isolation level mà bạn chọn.


Ba hiện tượng đọc nguy hiểm

1. Dirty Read — Đọc dữ liệu "bẩn"

Thời gian ──────────────────────────────────────────────────────►

T1: BEGIN; UPDATE products SET stock = 0; ............ ROLLBACK;

T2:         BEGIN; SELECT stock ──► 0 (DIRTY!)
            → T2 hành động dựa trên data SAI!
            → stock thực tế CHƯA TỪNG = 0 (T1 đã rollback)

T2 đọc giá trị mà T1 chưa commit — rồi T1 rollback. T2 đã ra quyết định dựa trên dữ liệu chưa bao giờ tồn tại thật sự. Đây là kiểu lỗi nguy hiểm nhất.

2. Non-Repeatable Read — Đọc hai lần, hai kết quả

Thời gian ──────────────────────────────────────────────────────►

T1: BEGIN; SELECT price ──► 100K ............. SELECT price ──► 150K
                                    │                    (Khác rồi!)
T2:                  BEGIN; UPDATE price = 150K; COMMIT;

T1 đọc price hai lần trong cùng một transaction — lần đầu là 100K, lần hai là 150K. Cùng một query, cùng một transaction, nhưng kết quả khác nhau. Với financial reports, đây là thảm họa.

3. Phantom Read — Dòng "ma" xuất hiện

Thời gian ──────────────────────────────────────────────────────►

T1: BEGIN; SELECT COUNT(*) WHERE status='active' ──► 100

T2:              BEGIN; INSERT new active user; COMMIT;

T1:        SELECT COUNT(*) WHERE status='active' ──► 101
           → Phantom row! Dòng mới xuất hiện giữa hai lần đọc

Lần đầu đếm 100, lần sau đếm 101 — có một dòng "ma" xuất hiện mà T1 không hề biết. Khác non-repeatable read ở chỗ: đây là dòng mới hoàn toàn, không phải dòng cũ bị sửa.


Bốn Isolation Levels

SQL standard định nghĩa 4 level, mỗi level cân bằng giữa tính đúng đắnhiệu năng:

LevelDirty ReadNon-Repeatable ReadPhantom ReadPerformance
READ UNCOMMITTED⚠️ Có thể xảy ra⚠️ Có thể xảy ra⚠️ Có thể xảy ra🚀 Nhanh nhất
READ COMMITTED✅ Chặn⚠️ Có thể xảy ra⚠️ Có thể xảy ra🟢 Tốt (Default PostgreSQL)
REPEATABLE READ✅ Chặn✅ Chặn⚠️ Có thể xảy ra🟡 Khá
SERIALIZABLE✅ Chặn✅ Chặn✅ Chặn🔴 Chậm nhất

Đọc từ trên xuống: càng xuống càng an toàn, nhưng càng chậm. Đây là trade-off kinh điển trong database engineering.

sql
-- Đặt isolation level cho transaction tiếp theo
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
    -- ...queries của bạn...
COMMIT;

-- Hoặc đặt cho toàn session (PostgreSQL)
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;

Thực tế: Chọn level nào?

READ COMMITTED (Default) — 90% use cases

Đủ dùng cho hầu hết web applications. Khi user xem danh sách sản phẩm, đọc bài viết, hay browse catalog — READ COMMITTED là quá đủ.

sql
-- PostgreSQL mặc định là READ COMMITTED
-- Bạn không cần SET gì cả — nó đã ở level này rồi
BEGIN;
    SELECT * FROM products WHERE category = 'electronics';
COMMIT;

REPEATABLE READ — Financial Reports

Khi bạn cần snapshot nhất quán — đọc nhiều bảng mà không muốn data thay đổi giữa chừng.

sql
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
    -- Báo cáo tài chính: đọc nhiều bảng, cần nhất quán
    SELECT SUM(amount) FROM revenue WHERE month = '2024-01';
    SELECT SUM(amount) FROM expenses WHERE month = '2024-01';
    -- Hai câu SELECT này thấy cùng "snapshot" của database
COMMIT;

SERIALIZABLE — Giao dịch tài chính quan trọng

Khi tuyệt đối không được sai — trading, accounting, booking hàng cuối cùng.

sql
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
    -- Kiểm tra tồn kho
    SELECT stock FROM products WHERE id = 42;
    -- Nếu stock > 0, tiến hành đặt hàng
    UPDATE products SET stock = stock - 1 WHERE id = 42;
    INSERT INTO orders (product_id, user_id) VALUES (42, 1);
COMMIT;
-- Nếu có conflict → database sẽ báo lỗi serialization
-- Application cần retry transaction

⚠️ SERIALIZABLE cần retry logic

Ở SERIALIZABLE, database có thể abort transaction của bạn khi phát hiện conflict. Application phải có logic retry:

python
MAX_RETRIES = 3
for attempt in range(MAX_RETRIES):
    try:
        with db.transaction(isolation='serializable'):
            # ... business logic ...
            break
    except SerializationError:
        if attempt == MAX_RETRIES - 1:
            raise
        continue  # Thử lại

READ UNCOMMITTED — Gần như không bao giờ

Chỉ dùng khi bạn cần ước lượng nhanh trên dataset khổng lồ và chấp nhận kết quả không chính xác 100%.

sql
-- Ví dụ: Ước lượng số user online (sai 1-2% cũng OK)
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT COUNT(*) FROM sessions WHERE last_active > NOW() - INTERVAL '5 minutes';

Trong thực tế, PostgreSQL không thực sự support READ UNCOMMITTED — nó tự động nâng lên READ COMMITTED. MySQL thì có, nhưng bạn gần như không nên dùng.


PART 3: SQL Injection Prevention — Bảo Vệ Hệ Thống

SQL Injection — Hiểu để phòng

SQL Injection (SQLi) liên tục nằm trong OWASP Top 10 — danh sách lỗ hổng bảo mật web nguy hiểm nhất. Nguyên nhân gốc rễ: ghép chuỗi user input trực tiếp vào câu SQL.

Cuộc tấn công kinh điển: Bypass Authentication

python
# ❌ VULNERABLE: String concatenation
username = request.POST['username']  # User nhập: admin' --
password = request.POST['password']  # Bất kỳ gì cũng được

query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"

Câu SQL thực tế mà database nhận được:

sql
SELECT * FROM users WHERE username = 'admin' --' AND password = 'abc123'
--                                            ^^
--                        Dấu -- biến phần còn lại thành COMMENT!
--                        → Chỉ cần username = 'admin' là đăng nhập được

Hacker bypass hoàn toàn kiểm tra password. Chỉ cần biết username — hoặc thậm chí không cần:

sql
-- Input: ' OR 1=1 --
SELECT * FROM users WHERE username = '' OR 1=1 --' AND password = '...'
-- → OR 1=1 luôn TRUE → trả về TẤT CẢ users → đăng nhập bằng user đầu tiên (thường là admin)

Bobby Tables — Xóa sạch database

Input:  '; DROP TABLE users; --

Query:  SELECT * FROM users WHERE name = ''; DROP TABLE users; --'

Result: 💀 Câu SELECT chạy bình thường (trả về 0 rows)
        💀 Sau đó DROP TABLE users chạy tiếp
        💀 Toàn bộ bảng users bị XÓA VĨNH VIỄN

🚨 Đây không phải lý thuyết

SQL injection vẫn xảy ra hàng ngày trên production systems. Năm 2023, theo báo cáo của Imperva, 42% các web attacks liên quan đến SQL injection. Nếu bạn viết code web mà không biết phòng chống SQLi, bạn đang đặt toàn bộ dữ liệu người dùng vào rủi ro.


Phòng thủ #1: Parameterized Queries (Phòng tuyến chính)

Nguyên tắc: Tách biệt hoàn toàn câu SQLdữ liệu. Database nhận câu SQL trước, compile và tối ưu, rồi mới nhận data — data không bao giờ được interpret như SQL code.

python
# ✅ Python (psycopg2) — Parameterized query
cursor.execute(
    "SELECT * FROM users WHERE username = %s AND password = %s",
    (username, hashed_password)
)
# %s là placeholder — psycopg2 tự escape và truyền riêng
# Dù username = "admin' --" thì database vẫn tìm ĐÚNG chuỗi "admin' --"
javascript
// ✅ Node.js (pg) — Parameterized query
const result = await pool.query(
    'SELECT * FROM users WHERE username = $1 AND password = $2',
    [username, hashedPassword]
);
// $1, $2 là placeholder — pg library tự xử lý an toàn
java
// ✅ Java (PreparedStatement) — Parameterized query
PreparedStatement ps = conn.prepareStatement(
    "SELECT * FROM users WHERE username = ? AND password = ?"
);
ps.setString(1, username);
ps.setString(2, hashedPassword);
ResultSet rs = ps.executeQuery();
// ? là placeholder — JDBC driver đảm bảo an toàn
go
// ✅ Go (database/sql) — Parameterized query
row := db.QueryRow(
    "SELECT id, role FROM users WHERE username = $1 AND password = $2",
    username, hashedPassword,
)

💡 Tại sao parameterized queries an toàn?

Database nhận câu SQL và data qua hai kênh riêng biệt. Câu SQL được compile thành execution plan trước, rồi data được "điền vào" chỗ placeholder. Dù user nhập '; DROP TABLE users; --, database chỉ coi đó là một chuỗi text bình thường để so sánh — không bao giờ thực thi như SQL code.


Phòng thủ #2: ORM — Tự động bảo vệ (nhưng đừng chủ quan)

ORM (Object-Relational Mapping) tự động dùng parameterized queries dưới hood. Nhưng — luôn có chữ "nhưng":

python
# ✅ Django ORM — tự động parameterized
User.objects.filter(username=username)
# → Tạo ra: SELECT ... WHERE username = %s với param riêng

# ✅ SQLAlchemy — tự động parameterized
session.query(User).filter(User.username == username).first()

# ❌ NHƯNG raw queries có thể VULNERABLE!
User.objects.raw(f"SELECT * FROM users WHERE name = '{name}'")  # ❌ XSS!
User.objects.raw("SELECT * FROM users WHERE name = %s", [name])  # ✅ Safe

# ❌ CŨNG VULNERABLE: extra() với string interpolation
User.objects.extra(where=[f"name = '{name}'"])  # ❌ Nguy hiểm!
javascript
// ✅ Sequelize (Node.js) — tự động parameterized
const user = await User.findOne({ where: { username } });

// ❌ NHƯNG raw queries CẦN CẨN THẬN:
sequelize.query(`SELECT * FROM users WHERE name = '${name}'`);     // ❌
sequelize.query('SELECT * FROM users WHERE name = ?', {
  replacements: [name]
});  // ✅

⚠️ ORM không phải lá chắn tuyệt đối

ORM bảo vệ bạn khi bạn dùng đúng cách. Mọi ORM đều cho phép raw queries — và đó là nơi SQL injection thường xảy ra. Quy tắc: mỗi khi viết raw SQL, phải dùng parameterized.


Phòng thủ #3: Defense in Depth — Nhiều tầng bảo vệ

Parameterized queries là phòng tuyến chính, nhưng hệ thống an toàn cần nhiều lớp:

TầngBiện phápVí dụ
1. CodeParameterized queriescursor.execute("...%s", [param])
2. ValidationInput validation (whitelist)Username chỉ cho phép [a-zA-Z0-9_]
3. PermissionsLeast privilegeApp DB user không có quyền DROP, ALTER
4. NetworkWAF (Web Application Firewall)Chặn request chứa pattern SQL injection
5. MonitoringSecurity audit & loggingPhát hiện pattern bất thường, alert
sql
-- Least Privilege: Tạo user riêng cho app, chỉ cho phép đọc/ghi
CREATE USER app_user WITH PASSWORD 'strong_random_password';
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
-- KHÔNG GRANT: DROP, ALTER, CREATE, TRUNCATE
-- → Kể cả khi bị injection, hacker KHÔNG THỂ DROP TABLE

🏋️ Bài tập nhanh

Đề bài: Viết transaction an toàn cho việc đặt hàng:

  1. Tạo order
  2. Thêm order items
  3. Giảm stock sản phẩm
  4. Nếu stock không đủ → rollback toàn bộ
💡 Xem gợi ý

Dùng CHECK constraint trên stock và bắt lỗi để ROLLBACK.

✅ Xem lời giải
sql
-- Đảm bảo stock không âm
ALTER TABLE products ADD CONSTRAINT positive_stock CHECK (stock >= 0);

-- Transaction đặt hàng
BEGIN;
    -- Bước 1: Giảm stock TRƯỚC (để CHECK constraint bắt lỗi sớm)
    UPDATE products SET stock = stock - 2 WHERE id = 42;
    -- Nếu stock < 0 → CHECK constraint violation → tự động fail

    -- Bước 2: Tạo order
    INSERT INTO orders (user_id, total, status)
    VALUES (1, 500000, 'pending')
    RETURNING id;

    -- Bước 3: Thêm order items
    INSERT INTO order_items (order_id, product_id, qty, price)
    VALUES (currval('orders_id_seq'), 42, 2, 250000);
COMMIT;
-- Nếu bất kỳ bước nào fail → toàn bộ transaction tự động ROLLBACK
-- Stock, order, order_items — tất cả trở về trạng thái ban đầu

Tại sao giảm stock trước? Vì CHECK constraint sẽ bắt lỗi ngay lập tức nếu stock không đủ — không cần viết thêm logic kiểm tra trong application code. Đây là pattern fail fast — phát hiện lỗi sớm nhất có thể.


⚠️ Gotcha: Long-Running Transactions

sql
-- 🚫 Transaction mở quá lâu → lock giữ quá lâu
BEGIN;
SELECT * FROM products WHERE id = 1 FOR UPDATE;  -- Lock row này
-- ... developer đi uống cà phê 30 phút ...
-- Mọi transaction khác muốn UPDATE row id = 1 → CHỜ 30 phút!
COMMIT;

Hậu quả trong production:

  • Các transaction khác bị chờ (waiting) → timeout → user thấy lỗi
  • Hàng đợi lock ngày càng dài → hiệu ứng domino → cả hệ thống chậm
  • Worst case: deadlock — hai transaction chờ nhau mãi mãi

🚨 Quy tắc vàng

Giữ transaction càng NGẮN càng tốt. Thực hiện mọi computation, validation, API calls bên ngoài transaction. Chỉ wrap actual DB writes trong BEGIN...COMMIT.

python
# ✅ ĐÚNG: Tính toán bên ngoài, transaction chỉ chứa DB writes
total = calculate_order_total(items)         # Ngoài transaction
validated = validate_stock(items)            # Ngoài transaction
payment_token = charge_credit_card(total)    # Ngoài transaction

with db.transaction():                       # Transaction bắt đầu
    insert_order(user_id, total)             # Chỉ DB writes
    insert_order_items(order_id, items)      # Chỉ DB writes
    update_stock(items)                      # Chỉ DB writes
# Transaction kết thúc — giữ trong vài milliseconds

Ghi chú hiệu năng

Isolation Level vs Throughput

LevelThroughput ước tínhGhi chú
READ COMMITTED100% (baseline)Default, phù hợp hầu hết
REPEATABLE READ~80-90%Snapshot overhead
SERIALIZABLE~60-80%Conflict detection + retry overhead

SERIALIZABLE giảm 20-40% throughput so với READ COMMITTED. Chỉ dùng khi cần thiết.

Optimistic Locking — Thay thế SERIALIZABLE

Trong hệ thống high-concurrency, thay vì dùng SERIALIZABLE, nhiều team chọn optimistic locking với version column:

sql
-- Thêm cột version
ALTER TABLE products ADD COLUMN version INT DEFAULT 1;

-- Đọc sản phẩm
SELECT id, stock, version FROM products WHERE id = 42;
-- → stock = 10, version = 5

-- Cập nhật với version check
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 42 AND version = 5;
-- Nếu version đã bị transaction khác đổi → 0 rows affected → retry

Ưu điểm: không lock, không chờ, throughput cao hơn. Application tự retry khi conflict.

Công thức throughput

Connection pools + Short transactions + Đúng isolation level = High throughput
  • Connection pool: 20-50 connections cho hầu hết web apps
  • Transaction duration: Target < 100ms
  • Lock granularity: Row-level lock > Table-level lock

🚫 Anti-pattern: String Concatenation SQL

Đây là anti-pattern nguy hiểm nhất trong toàn bộ series — không phải về performance mà về security.

Ví dụ thực tế: Form đăng nhập

python
# ❌ ANTI-PATTERN: String concatenation trong login form
def login(request):
    username = request.POST['username']
    password = request.POST['password']

    # 💀 NGUY HIỂM: Ghép chuỗi trực tiếp
    query = f"""
        SELECT id, role FROM users
        WHERE username = '{username}'
        AND password_hash = '{hash(password)}'
    """
    user = db.execute(query).fetchone()

    if user:
        create_session(user)
        return redirect('/dashboard')

Hacker nhập: username = admin' OR 1=1 --

sql
-- Query thực tế:
SELECT id, role FROM users
WHERE username = 'admin' OR 1=1 --' AND password_hash = '...'
-- → OR 1=1 luôn TRUE → đăng nhập thành admin!

Fix: Parameterized query

python
# ✅ FIX: Parameterized — luôn luôn, không ngoại lệ
def login(request):
    username = request.POST['username']
    password = request.POST['password']

    query = "SELECT id, role FROM users WHERE username = %s AND password_hash = %s"
    user = db.execute(query, (username, hash(password))).fetchone()

    if user:
        create_session(user)
        return redirect('/dashboard')

Quy tắc đơn giản

Nếu câu SQL của bạn có bất kỳ biến nào từ user input — BẮT BUỘC dùng parameterized query. Không có ngoại lệ. Không có "nhưng trường hợp này đơn giản nên OK". Luôn luôn parameterized.


🎮 Playground

Chạy thử transaction với ROLLBACK và CHECK constraint:

sql
-- ============================================
-- 🧪 PLAYGROUND: Transactions & Constraints
-- ============================================

-- Tạo bảng accounts
CREATE TABLE accounts (
    id VARCHAR(10) PRIMARY KEY,
    name VARCHAR(50),
    balance BIGINT NOT NULL,
    CONSTRAINT positive_balance CHECK (balance >= 0)
);

-- Tạo bảng transfer_log
CREATE TABLE transfer_log (
    id SERIAL PRIMARY KEY,
    from_account VARCHAR(10),
    to_account VARCHAR(10),
    amount BIGINT,
    status VARCHAR(20),
    created_at TIMESTAMP DEFAULT NOW()
);

-- Insert dữ liệu mẫu
INSERT INTO accounts (id, name, balance) VALUES
('A', 'Nguyễn Văn An',   5000000),
('B', 'Trần Thị Bình',   3000000),
('C', 'Phạm Minh Châu',  1000000);

-- ============================================
-- 🧪 THỬ CÁC TRANSACTION DƯỚI ĐÂY
-- ============================================

-- 1. Chuyển tiền thành công: A → B, 1 triệu
BEGIN;
    UPDATE accounts SET balance = balance - 1000000 WHERE id = 'A';
    UPDATE accounts SET balance = balance + 1000000 WHERE id = 'B';
    INSERT INTO transfer_log (from_account, to_account, amount, status)
    VALUES ('A', 'B', 1000000, 'success');
COMMIT;

-- Kiểm tra kết quả
SELECT * FROM accounts;
-- A: 4,000,000 | B: 4,000,000 | C: 1,000,000

-- 2. Chuyển tiền thất bại: C → A, 2 triệu (C chỉ có 1 triệu!)
BEGIN;
    UPDATE accounts SET balance = balance - 2000000 WHERE id = 'C';
    -- → ERROR: CHECK constraint "positive_balance" violated
    -- → Transaction tự động fail, mọi thứ được rollback
COMMIT;

-- Kiểm tra: C vẫn nguyên 1 triệu
SELECT * FROM accounts WHERE id = 'C';

-- 3. Thử ROLLBACK thủ công
BEGIN;
    UPDATE accounts SET balance = balance - 500000 WHERE id = 'A';
    -- Ơ khoan, nhầm account rồi!
    ROLLBACK;
    -- → Hoàn tác — A vẫn nguyên

SELECT * FROM accounts WHERE id = 'A';
-- Balance vẫn = 4,000,000

🧪 Thử nghiệm thêm

  1. Thử chuyển đúng số dư còn lại của C (1 triệu) → có thành công không?
  2. Thử dùng SAVEPOINT: chuyển tiền lỗi → rollback to savepoint → chuyển số tiền nhỏ hơn
  3. Bỏ CHECK constraint rồi thử lại — thấy balance âm không?

📋 Tổng kết chương

ConceptGhi nhớ
ACIDAtomicity + Consistency + Isolation + Durability = dữ liệu luôn đúng
BEGIN/COMMITWrap nhiều SQL thành một đơn vị nguyên tử
ROLLBACKHoàn tác toàn bộ transaction khi có lỗi
SAVEPOINTCheckpoint bên trong transaction, rollback một phần
Dirty ReadĐọc data chưa commit → nguy hiểm nhất
READ COMMITTEDDefault level, đủ cho 90% use cases
SERIALIZABLEAn toàn tuyệt đối, nhưng chậm + cần retry logic
SQL InjectionGhép chuỗi user input vào SQL = thảm họa
Parameterized QueryPhòng tuyến chính chống injection — bắt buộc, không ngoại lệ
Least PrivilegeApp user chỉ có quyền tối thiểu cần thiết

🗺️ Phase 1 SQL Mastery — Bản đồ và lộ trình luyện tập

Dependency Map (Sơ đồ phụ thuộc)

    ┌──────────────────┐
    │ 01. Tư Duy       │
    │    Nền Tảng      │
    └────────┬─────────┘


    ┌──────────────────┐
    │ 02. SELECT/WHERE │
    │    ORDER BY      │
    └────────┬─────────┘


    ┌──────────────────┐
    │ 03. GROUP BY &   │
    │    Aggregation   │
    └────────┬─────────┘


    ┌──────────────────┐
    │ 04. JOIN         │
    │    Trực Giác     │
    └────────┬─────────┘


    ┌──────────────────┐
    │ 05. CTE &        │
    │    Window Funcs  │
    └────────┬─────────┘


    ┌──────────────────┐
    │ 06. Indexing,    │
    │    EXPLAIN, N+1  │
    └────────┬─────────┘


    ┌──────────────────┐
    │ 07. Transactions │◄── BẠN ĐANG Ở ĐÂY
    │    & Security    │
    └──────────────────┘

Suggested Practice Sequence

Sau bàiLuyện tậpLink
Bài 02SQL cơ bản: SELECT, WHERE, ORDER BY→ Basic Queries
Bài 03Aggregation: GROUP BY, HAVING→ Aggregations
Bài 04JOIN các loại→ Joins
Bài 05Window Functions + CTEs→ Window Functions / → CTEs
Bài 06Query Optimization→ Query Optimization
Bài 07Real-world Scenarios→ Real-world

Tổng kết Phase 1

Sau khi hoàn thành 7 bài trong Phase 1, bạn đã có:

  • Tư duy SQL đúng — declarative, set-based thinking
  • CRUD thành thạo — SELECT / WHERE / ORDER BY với best practices
  • Aggregation & GROUP BY — biến data thô thành business insights
  • JOIN mọi loại — với trực giác rõ ràng, không còn mơ hồ
  • CTE + Window Functions — vũ khí phỏng vấn và report phức tạp
  • Performance mindset — Index, EXPLAIN, N+1 problem
  • Transaction safety + SQL Injection prevention — code production-ready

Bạn không chỉ "biết SQL" — bạn đã có tư duy của một backend developer hiểu sâu về data layer. Đây là nền tảng mà mọi system design, mọi API optimization, và mọi cuộc phỏng vấn technical đều yêu cầu.

Tiếp theo: Khám phá các bài nâng cao trong sidebar hoặc bắt đầu luyện tập thực chiến tại Practice SQL.


📍 Trang tiếp theo

Đây là bài cuối cùng của Phase 1 — SQL Mastery Foundation. Bạn đã đi được một chặng đường dài từ tư duy nền tảng đến transaction và security.