→ From Coder to Shipper
From Coder to Shipper · Dữ liệu

SQLite — dữ liệu, JOIN, chỉ mục

Dữ liệu thật của ứng dụng nằm trong cơ sở dữ liệu. SQLite giữ tất cả trong một file — cùng đi qua cách bảng hoạt động, cách chúng liên kết, và cách SQLite tìm nhanh dòng dữ liệu, mỗi truy vấn đặt cạnh kết quả thật.

← / → để chuyển slide · chuyển EN/VI bên dưới · 60 slide ví dụ

Toàn cảnh

SQLite: dữ liệu thật, trong một file

Người ship dành nhiều thời gian hiểu dữ liệu, không kém gì code.

Code = logic

Code của ứng dụng bạn quyết định điều gì xảy ra: quy tắc, luồng, UI. Đó là phần chỉ dẫn, không phải nội dung.

Dữ liệu = thứ thật

Người dùng, đơn hàng, sản phẩm — những thứ sống sót sau khi ứng dụng khởi động lại. Mất chúng là mất luôn cả doanh nghiệp.

Một file .db, không server

Bảng, chỉ mục, toàn bộ dữ liệu — một file như shop.db, chạy như thư viện trong tiến trình ứng dụng. Không cần cài đặt hay khởi động gì.

Copy là sao lưu

Sao lưu CSDL là copy một file. Chuyển sang máy khác là gửi một file.

Đúng cỡ, lớn lên êm ru

Một file chứa từ 1 dòng tới hàng triệu dòng. Lựa chọn mặc định cho prototype, app mobile/desktop và web app nhỏ.

Bạn sẽ xem nó mỗi ngày

Gỡ lỗi, xuất dữ liệu, sửa nhanh, 'vì sao dòng này sai?' — bạn sẽ đọc và viết dữ liệu này liên tục.

Bên trong

Trong file .db thực sự là gì

File không phải byte ngẫu nhiên — nó được tổ chức để nhanh.

Tất cả là các trang (page)

File là chuỗi các trang cỡ cố định (thường 4 KB). Bảng và chỉ mục nằm trong các trang này.

Dòng nằm trong B-tree

Mỗi bảng là một B-tree — cấu trúc cân bằng, đã sắp xếp, giúp SQLite nhảy tới dòng thay vì đọc cả file.

Chỉ mục là các B-tree bổ sung

Chỉ mục là một cây sắp xếp riêng, chỉ từ giá trị cột tới trang chứa dòng. Chi tiết ở Phần 4.

WAL giữ reader chạy

Write-ahead log cho phép reader vẫn đọc trong lúc có ghi — song song mượt mà cho app nhỏ.

Từ vựng

Bảng, dòng, cột — và schema

Từ vựng giống bảng tính, nhưng có cấu trúc — định nghĩa một lần, dùng ở khắp nơi.

Bảng (table)

Một tập hợp các thứ liên quan: bảng users, bảng orders.

Dòng (row)

Một bản ghi — một người dùng, một đơn hàng.

Cột (column)

Một thuộc tính, cùng loại giá trị ở mọi dòng: email, total, status.

Schema (sơ đồ dữ liệu)

Hình dạng dữ liệu — có những bảng nào, cột gì, cho phép gì. Khai báo một lần.

Một schema nhỏ

Ví dụ xuyên suốt: một cửa hàng nhỏ

schema.sql
-- shop.db — the whole database is this one file
CREATE TABLE users (
    id    INTEGER PRIMARY KEY,  -- unique per user
    name  TEXT NOT NULL,
    email TEXT NOT NULL UNIQUE
);

CREATE TABLE orders (
    id      INTEGER PRIMARY KEY,
    user_id INTEGER,            -- -> users.id  (foreign key)
    total   REAL,
    status  TEXT
);
users — schema trở thành dòng
idnameemail
1Anaana@ex.com
2Bobo@ex.com
3Cydcyd@ex.com
orders — mỗi dòng trỏ về users qua user_id
iduser_idtotalstatus
101120.00paid
11145.00shipped
12280.00paid

Chúng ta dùng cửa hàng này (users + orders + line_items) ở gần như mọi ví dụ bên dưới. Chú ý đường liên kết đã xuất hiện: orders.user_id trỏ tới users.id.

01
Phần 1

Cơ bản — năm động từ và WHERE

Năm động từ đáp ứng gần như mọi việc ứng dụng làm với dữ liệu. Bạn cần đọc hiểu trôi chảy; AI sẽ viết giúp bạn.

SQL cốt lõi

Năm động từ — CREATE + INSERT

create_insert.sql
CREATE TABLE users (id INTEGER PRIMARY KEY,
                    name TEXT, email TEXT);

INSERT INTO users (id, name, email) VALUES
  (1, 'Ana', 'ana@ex.com'),
  (2, 'Bo',  'bo@ex.com'),
  (3, 'Cyd', 'cyd@ex.com'),
  (4, 'Dee', 'dee@ex.com');
users
idnameemail
1Anaana@ex.com
2Bobo@ex.com
3Cydcyd@ex.com
4Deedee@ex.com

Tạo ra → thêm vào. INSERT thêm dòng vào bảng — mỗi dòng là một bản ghi hoàn chỉnh.

SQL cốt lõi

Năm động từ — SELECT, UPDATE, DELETE

select_update_delete.sql
SELECT name, email FROM users
WHERE id = 1;

UPDATE users SET name = 'Anna'
WHERE id = 1;

DELETE FROM users WHERE id = 4;
SELECT — kết quả
nameemail
Anaana@ex.com
users — sau UPDATE (dòng id=1 đổi tên)
idnameemail
1Annaana@ex.com
2Bobo@ex.com
3Cydcyd@ex.com
4Deedee@ex.com
users — sau DELETE (dòng id=4 biến mất)
idnameemail
1Annaana@ex.com
2Bobo@ex.com
3Cydcyd@ex.com

Đọc ra → sửa → xóa. Vòng lặp CREATE → INSERT → SELECT → UPDATE → DELETE là 95% SQL hằng ngày của ứng dụng.

Kiểu dữ liệu

Chọn kiểu cho mỗi cột

SQLite khá thoáng ('type affinity' — tương thích kiểu): không than vãn nếu bạn lưu kiểu lạ. Nhưng hãy chọn kiểu hợp lý: so sánh, sắp xếp và chỉ mục sẽ hoạt động như mong đợi.

KiểuDùng choGiá trị ví dụ
INTEGERid, số đếm, cờ, nămid = 1 · qty = 2
TEXTchuỗi: tên, email, trạng thái'Ana' · 'paid'
REALtiền, đo lường120.50
BLOBdữ liệu nhị phân: ảnh, filebyte của ảnh
TIMESTAMPthời gian (lưu dạng text/số)'2026-10-01 12:00'
Ràng buộc

Quy tắc mà CSDL tự thực thi

constraints.sql
CREATE TABLE users (
    id    INTEGER PRIMARY KEY,     -- auto id
    name  TEXT NOT NULL,           -- may not be empty
    email TEXT NOT NULL UNIQUE,    -- one email, ever
    age   INTEGER DEFAULT 30,      -- default when omitted
    vip   INTEGER NOT NULL DEFAULT 0
           CHECK (vip IN (0, 1))  -- only 0 or 1
);

Ràng buộc là hàng rào an toàn: chính CSDL từ chối dòng sai, nên ứng dụng không thể làm hỏng dữ liệu. Một lúc nữa bạn sẽ thấy các lần từ chối.

Ví dụ thực hành

Thêm dòng, đọc lại

Chính năm dòng sẽ dùng trong các ví dụ tiếp theo.

ops@vps — zsh
$ sqlite3 shop.db
sqlite>
$ INSERT INTO users (id, name, email) VALUES
$ (1,'Ana','ana@ex.com'), (2,'Bo','bo@ex.com'),
$ (3,'Cyd','cyd@ex.com'), (4,'Dee','dee@ex.com');
$ SELECT * FROM users;
1|Ana|ana@ex.com
2|Bo|bo@ex.com
3|Cyd|cyd@ex.com
4|Dee|dee@ex.com
SELECT

Đọc dữ liệu: ba dạng SELECT

select.sql
-- every row, every column
SELECT * FROM users;
-- only the columns you need
SELECT name, email FROM users;
-- cap the number of rows
SELECT name FROM users LIMIT 2;
LIMIT 2 — kết quả của truy vấn cuối
name
Ana
Bo

SELECT là động lực chính — phần lớn traffic của app là đọc. * tiện khi dùng CLI; trong code app, hãy liệt kê cột cần.

Câu WHERE

Lọc dữ liệu: các toán tử

WHERE là nơi diễn ra việc lọc — và (Phần 4) nơi chỉ mục khiến nó nhanh.

Toán tửCông dụngVí dụ
= !=khớp chính xác / khácWHERE status = 'paid'
< > <= >=so sánh số hoặc ngàyWHERE total > 100
LIKEmẫu text có ký tự đại diệnWHERE name LIKE 'A%'
IN NOT INgiá trị nằm (không) trong danh sáchWHERE status IN ('paid','shipped')
AND ORkết hợp các điều kiệnWHERE total > 50 AND status = 'paid'
Ví dụ thực hành

WHERE trên dữ liệu shop

where.sql
-- 4 users: Ana, Bo, Cyd, Dee
-- 4 orders: 10 (u1, 120.00, paid)  11 (u1, 45.00, shipped)
--           12 (u2, 80.00, paid)   13 (u4, 9.50, pending)

SELECT name FROM users
WHERE name = 'Bo';

SELECT * FROM orders
WHERE total > 50;
users — name = 'Bo'
name
Bo
orders — total > 50
iduser_idtotalstatus
101120.00paid
12280.00paid

Đọc mỗi truy vấn như một câu: 'cho tôi những dòng thỏa điều kiện'. Kết quả chỉ là các dòng khớp.

Mẫu LIKE

LIKE: mẫu có ký tự đặc biệt

MẫuÝ nghĩaSản phẩm khớp
k% bắt đầu bằng k keyboard
%r kết thúc bằng r keyboard
%r kết thúc bằng r monitor
m%e đầu m, cuối e, giữa tùy ý mouse
%hub kết thúc bằng 'hub' usb-c hub
_onitor một ký tự bất kỳ, rồi 'onitor' monitor

Mỗi dòng tô sáng = một sản phẩm khớp mẫu đó (trên cả 5 sản phẩm: keyboard, mouse, usb-c hub, monitor, adapter). % = chuỗi bất kỳ (kể cả rỗng) · _ = đúng một ký tự — cách làm truy vấn 'ô tìm kiếm'.

Ví dụ thực hành

Kết hợp điều kiện: AND, OR, IN

combine.sql
SELECT * FROM orders
WHERE status = 'paid' AND total > 100;

SELECT name FROM users
WHERE name = 'Bo' OR name = 'Dee';

SELECT * FROM orders
WHERE status IN ('paid','shipped');
status = 'paid' AND total > 100
iduser_idtotalstatus
101120.00paid
name = 'Bo' OR name = 'Dee'
name
Bo
Dee
status IN ('paid','shipped')
iduser_idtotalstatus
101120.00paid
11145.00shipped
12280.00paid

AND = chặt hơn (ít kết quả hơn), OR = rộng hơn. IN chỉ là cách viết gọn của chuỗi = OR = OR ...

Khóa & ràng buộc

Khóa chính, khóa ngoại, UNIQUE

Khóa là thứ khiến các dòng đáng tin cậy và các bảng liên kết được.

Khóa chính (PK)

Cột định danh mỗi dòng — thường là id. Duy nhất, không rỗng, dùng để tìm dòng.

Khóa ngoại (FK)

Cột trỏ tới PK của bảng khác — orders.user_id → users.id. Đây là cách các bảng liên kết.

UNIQUE (duy nhất)

Ràng buộc: không có hai dòng nào dùng chung giá trị này — ví dụ email, để không ai đăng ký trùng.

Ví dụ thực hành

Xem CSDL nói 'không'

Mọi thao tác ghi sai bên dưới đều bị SQLite từ chối — ứng dụng không bao giờ thấy dòng đó.

ops@vps — zsh
$ sqlite3 shop.db
sqlite>
$ INSERT INTO users (id, name, email) VALUES (1, 'Bo', 'bo@ex.com');
Error: UNIQUE constraint failed: users.id
$ INSERT INTO users (id, name, email) VALUES (5, 'X', 'ana@ex.com');
Error: UNIQUE constraint failed: users.email
$ INSERT INTO users (id, name) VALUES (6, 'NoEmail');
Error: NOT NULL constraint failed: users.email
$ PRAGMA foreign_keys = ON;
$ INSERT INTO orders (id, user_id, total) VALUES (99, 42, 10.0);
Error: FOREIGN KEY constraint failed
Định hình kết quả

ORDER BY và LIMIT

order.sql
-- biggest first
SELECT * FROM orders
ORDER BY total DESC;

-- first two, alphabetically
SELECT name FROM users
ORDER BY name LIMIT 2;

-- result: Ana, Bo
orders — ORDER BY total DESC
iduser_idtotalstatus
101120.00paid
12280.00paid
11145.00shipped
1349.50pending

ORDER BY sắp xếp (mặc định ASC tăng dần, DESC giảm dần); LIMIT giới hạn số dòng. Kết hợp thành 'top 10', '20 mới nhất', phân trang.

Định hình kết quả

Hàm tổng hợp: COUNT, SUM — và GROUP BY

group.sql
-- how many orders total?
SELECT COUNT(*) FROM orders;   -- 4

-- per user: how many orders, how much spent?
SELECT user_id, COUNT(*) AS n, SUM(total) AS spent
FROM orders
GROUP BY user_id;
GROUP BY user_id — kết quả
user_idnspent
12165.0
2180.0
419.5

GROUP BY gom các dòng thành một dòng cho mỗi nhóm, rồi hàm tổng hợp tóm tắt từng nhóm. Cyd (id 3) không có đơn nên không có nhóm.

Cách ứng dụng nói chuyện với CSDL

Code Flask/Python của bạn chỉ là lớp bọc truy vấn

app.py
import sqlite3

conn = sqlite3.connect("shop.db")
row  = conn.execute("SELECT name FROM users WHERE id = ?", (1,))
print(row.fetchone())      # ('Ana',)

sql  = "SELECT name, total FROM orders WHERE user_id =?"
rows = conn.execute(sql, (1,)).fetchall()
print(rows)                # [('Ana', 120.0), ('Ana', 45.0)]

connect → execute(truy vấn, giá trị) → fetch. SQL bên trong chính là những gì bạn đã đọc — ứng dụng chỉ là người đưa thư.

Dòng lệnh sqlite3

Cửa sổ hằng ngày vào dữ liệu

Một chương trình terminal nhỏ: mở file, xem, truy vấn, thoát.

ops@vps — zsh
$ sqlite3 shop.db
sqlite>
$ .tables
users orders line_items
$ .schema orders
CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER,
total REAL, status TEXT)
$ SELECT name FROM users LIMIT 5;
Ana
Bo
Cyd
Dee
$ .quit
02
Phần 2

Mô hình dữ liệu — các bảng liên hệ thế nào

Quyết định thiết kế quan trọng nhất của mọi ứng dụng không phải code — mà là có những bảng nào và chúng liên kết thế nào.

Vì sao tách?

Một bảng to vs. các bảng liên quan

Cách bạn chia dữ liệu là quyết định thiết kế quan trọng nhất — kiến trúc, chứ không phải cú pháp.

Một bảng to (khó cưỡng)

Mỗi đơn hàng lặp lại tên & địa chỉ người dùng. Sửa địa chỉ → phải cập nhật 500 dòng. Lặp lại dữ liệu sinh ra lỗi.

Và bảng phình to, cùng những giá trị lặp đi lặp lại.

Các bảng liên quan (đúng cách)

Thông tin người dùng nằm một lần ở users; mỗi đơn chỉ lưu user_id. Sửa một lần — JOIN ghép lại khi cần.

Đây là 'chuẩn hóa' — nguyên tắc lưu mỗi dữ kiện đúng một lần.

Mối quan hệ

Chỉ hai hình dạng: 1:nhiều và nhiều:nhiều

Gần như mọi mối quan hệ bạn mô hình hóa đều thuộc hai dạng này.

1:nhiều (phổ biến nhất)

Một người dùng → nhiều đơn. FK nằm ở phía 'nhiều': orders.user_id. Một người dùng, nhiều dòng trỏ tới.

nhiều:nhiều (khó hơn)

Một sách có nhiều tác giả; một tác giả viết nhiều sách. Bảng nào cũng không thể chứa một FK cho mối quan hệ này.

Bảng trung gian

Bảng thứ ba (book_authors) chỉ chứa hai FK: book_id + author_id. Nhiều:nhiều = hai quan hệ 1:nhiều qua bảng đó.

Mô hình dữ liệu

Cửa hàng của bạn, nhìn tổng quan

Mỗi đơn thuộc về một người dùng; mỗi đơn có nhiều mục; mỗi mục thuộc về một đơn. Đó là toàn bộ câu chuyện của JOIN.

users

idPK
name
emailUNIQUE

orders

idPK
user_idFK → users.id
total
status

line_items

idPK
order_idFK → orders.id
product
qty

Đọc như bản đồ: các cột FK là sợi dây buộc các thẻ với nhau — JOIN là cách lấy dữ liệu xuyên qua những sợi dây đó.

Mô hình dữ liệu

Nhiều:nhiều — bảng trung gian

Một sách có nhiều tác giả, một tác giả viết nhiều sách. Bảng book_authors chứa đúng hai khóa ngoại, và biến 'nhiều:nhiều' thành hai quan hệ 1:nhiều.

books

idPK
title
year

book_authors

book_idFK → books.id
author_idFK → authors.id

authors

idPK
name
country

Mỗi dòng trong book_authors nói: 'tác giả này đồng tác giả sách này'. Không có cột 'list tác giả' nào ở trong books.

Thiết kế

Cách thiết kế mô hình từ đầu

Làm điều này trước khi viết SQL — đây là phần việc của kiến trúc sư.

  1. 1

    Tìm các danh từ

    'Người dùng đặt đơn chứa các mục' → users, orders, line_items. Danh từ thành bảng.

  2. 2

    Một bảng cho mỗi danh từ

    Mỗi bảng = một thứ; các cột = thuộc tính của thứ đó (bạn cần biết gì về nó?).

  3. 3

    Liên kết chúng

    1:nhiều → FK ở phía 'nhiều'. Nhiều:nhiều → thêm bảng trung gian với hai FK.

  4. 4

    Chọn các khóa

    PK cho mọi bảng; FK cho cột liên kết; UNIQUE cho giá trị không được lặp (email).

  5. 5

    Viết ra

    Viết CREATE TABLE kèm sơ đồ. Sơ đồ là thứ bạn cho người khác (và AI) xem để thống nhất mô hình.

Nếu sai bảng và sai mối liên kết, dù viết bao nhiêu SQL cũng không cứu được. Mô hình chính là kiến trúc.
— trái tim của Phần 2
03
Phần 3

JOIN — ghép các bảng lại với nhau

Dữ liệu được tách ra nhiều bảng có chủ đích. JOIN là cách ghép lại khi cần — cùng xem nó diễn ra, từng dòng một.

JOIN · ý tưởng cốt lõi

JOIN thực sự làm gì

Một cơ chế, ba hệ quả.

Khớp qua khóa

SQLite ghép các dòng của hai bảng khi khóa bằng nhau: orders.user_id = users.id.

Ghép các cột

Mỗi dòng kết quả ghép cột của cả hai bảng — tên người dùng cạnh total đơn hàng của họ.

Hỏi xuyên bảng

Bỗng bạn hỏi được 'ai chi nhiều nhất?' — câu hỏi mà một bảng riêng lẻ không trả lời được.

Ví dụ thực hành

INNER JOIN, từng bước

users
idname
1Ana
2Bo
3Cyd
4Dee
orders
iduser_idtotalstatus
101120.00paid
11145.00shipped
12280.00paid
1349.50pending
inner_join.sql
SELECT u.name, o.total, o.status
FROM   users u
JOIN   orders o ON o.user_id = u.id
ORDER BY o.id;
kết quả — Cyd (id 3) vắng mặt
nametotalstatus
Ana120.00paid
Ana45.00shipped
Bo80.00paid
Dee9.50pending

Đọc là: 'mỗi đơn hàng, tìm người dùng và đặt tên cạnh total'. Cyd biến mất vì INNER JOIN chỉ giữ các dòng khớp.

Ví dụ thực hành

INNER vs LEFT JOIN — thấy sự khác nhau

left_join.sql
SELECT u.name, o.total
FROM   users u
LEFT JOIN orders o ON o.user_id = u.id
ORDER BY u.id;

LEFT JOIN = INNER JOIN, nhưng giữ lại mọi dòng của bảng trái dù không có dòng khớp bên phải.

LEFT JOIN — kết quả
nametotal
Ana120.00
Ana45.00
Bo80.00
CydNULL
Dee9.50

Dòng Cyd vẫn hiện với NULL — 'người dùng này không có đơn'. RIGHT và FULL JOIN là hai hướng còn lại; mặc định (INNER) = chỉ dòng khớp.

Ví dụ thực hành

Nối tiếp ba bảng

orders
iduser_id
101
111
122
134
line_items
idorder_idproductqty
110keyboard1
210mouse2
311usb-c hub1
412monitor1
513adapter3
chain_join.sql
SELECT o.id AS order_id, li.product, li.qty
FROM   orders o
JOIN   line_items li ON li.order_id = o.id
ORDER BY o.id, li.id;
kết quả
order_idproductqty
10keyboard1
10mouse2
11usb-c hub1
12monitor1
13adapter3

Mỗi JOIN thêm cột của bảng kế tiếp, liên kết bằng FK của nó. Mẫu 'bảng → con → cháu' này là hình dạng của hầu hết các báo cáo.

Ví dụ thực hành

JOIN + WHERE: 'Anna mua gì?'

join_where.sql
SELECT li.product, li.qty
FROM   users u
JOIN   orders o    ON o.user_id   = u.id
JOIN   line_items li ON li.order_id = o.id
WHERE  u.email = 'ana@ex.com';
kết quả — các mục Anna đã mua
productqty
keyboard1
mouse2
usb-c hub1

Thứ tự tư duy: JOIN trước (ghép bảng), sau đó WHERE (thu hẹp). Dạng 'tìm X thuộc về Y' này là truy vấn phổ biến nhất của mọi ứng dụng.

Ví dụ thực hành · sai

Bẫy thiếu ON: tích Descartes

cartesian.sql
-- Forgot the ON clause:
SELECT u.name, o.total
FROM   users u, orders o;
users × orders — mọi cặp, không có khóa
nametotal
Ana120.00
Ana45.00
Ana80.00
Ana9.50
Bo120.00
Bo45.00
Bo80.00
Bo9.50
… 16 dòng tổng cộng — phần lớn vô nghĩa

Không có khóa để khớp, CSDL ghép mọi dòng với mọi dòng: 4 users × 4 orders = 16 dòng. Nếu kết quả nhiều dòng hơn bình thường — chắc chắn bạn đang thiếu ON.

JOIN · hiệu năng

JOIN nhanh nhờ điều gì

Cơ chế bên trong rất đơn giản — và có thể tối ưu bằng chỉ mục.

Vòng lặp lồng nhau

Với mỗi dòng của bảng trái, SQLite tìm các dòng khớp ở bảng phải — lặp đi lặp lại.

Chỉ mục trên FK

Có chỉ mục trên orders.user_id, mỗi lần tìm tức thì. Không có: quét toàn bộ orders cho từng user.

Quy tắc

JOIN nhanh hay chậm tùy thuộc vào chỉ mục của các cột được nối. Index cả hai phía.

04
Phần 4

Chỉ mục — tìm kiếm nhanh

Không có chỉ mục, SQLite quét bảng từng dòng một. Chỉ mục giúp bỏ qua bước quét — đây là cách cụ thể, kèm bằng chứng.

Nó là gì

Chỉ mục: như trang mục lục của cuốn sách

Tạo bằng một dòng: CREATE INDEX idx_orders_user ON orders(user_id);

So sánh với cuốn sách

Trang mục lục của sách chỉ từ tên → số trang. Không có nó, muốn tìm một tên phải lật từng trang.

Cơ chế

Chỉ mục là bản sao nhỏ, đã sắp xếp của các giá trị một cột kèm con trỏ tới dòng — nhảy thẳng tới kết quả thay vì quét 100.000 dòng.

Ví dụ thực hành

Quét vs chỉ mục: 100.000 đơn hàng

scan_vs_index.txt
-- find all orders of user 1 (100,000 rows total)

-- no index → full scan:
--   read row 1... user_id = 7   ✗
--   read row 2... user_id = 3   ✗
--   ... 100,000 comparisons

-- with index → sorted lookup:
--   index says: user_id 1 → rows 10, 11
--   jump straight to those 2 rows
-- ~100,000 steps  →  ~17 steps

Chỉ mục là danh sách đã sắp xếp, nên SQLite tìm nhị phân (~log2 của 100.000 ≈ 17 bước nhảy) thay vì so sánh từng dòng.

Bằng chứng

EXPLAIN QUERY PLAN: xem chiến lược thay đổi

SQLite sẽ nói thẳng với bạn: nó quét hay dùng chỉ mục.

ops@vps — zsh
$ sqlite3 shop.db
sqlite>
$ EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = 'x@ex.com';
SCAN users
$ CREATE INDEX idx_users_email ON users(email);
$ EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = 'x@ex.com';
SEARCH users USING INDEX idx_users_email (email=?)
Index ở đâu

Index các cột 'nóng'

indexes.sql
-- index columns that appear in WHERE, ORDER BY, and ON:
CREATE INDEX idx_orders_user   ON orders(user_id);
CREATE INDEX idx_users_email   ON users(email);
CREATE INDEX idx_orders_status ON orders(status);

-- don't index:
--  * a column you never query (pure overhead)
--  * every column (each one taxes every write)

Cột FK xứng đáng có chỉ mục dù bạn chỉ dùng chúng trong JOIN — đó là quy tắc Phần 3 trong thực tế.

Sự đánh đổi

Chỉ mục tốn gì

Quy tắc: index cái bạn đọc nhiều. Đừng index tất cả — chỉ mục là sự đánh đổi tốc độ đọc/tốc độ ghi.

Lợi ích

WHERE, ORDER BY và JOIN trên cột đó nhanh hơn hẳn — từ quét tất cả sang nhảy thẳng.

Cái giá

Mỗi INSERT/UPDATE còn phải cập nhật cả chỉ mục → ghi chậm hơn một chút, và file to thêm đôi chút.

05
Phần 5

SQLite trong thế giới thực

Gắn tất cả lại: các phiên thật, code app an toàn, khi nào nó là công cụ đúng — và những lỗi nên tránh.

Ví dụ thực hành

Một phiên thật trên shop.db

Đếm dòng, hỏi một câu xuyên bảng, xem schema — tất cả từ CLI.

ops@vps — zsh
$ sqlite3 shop.db
sqlite>
$ SELECT COUNT(*) FROM orders;
4
$ SELECT u.name, SUM(o.total) AS spent
$ FROM users u JOIN orders o ON o.user_id = u.id
$ GROUP BY u.name ORDER BY spent DESC;
Ana|165.0
Bo|80.0
Dee|9.5
$ .quit
Code app an toàn

Truy vấn tham số hóa: ? là người bạn của bạn

safe.py
email = request.args.get('email')          # user input
sql  = "SELECT * FROM users WHERE email = ?"
rows = conn.execute(sql, (email,)).fetchall()

# ? is a placeholder — SQLite receives the query
# AND the value separately. The value is always
# treated as data, never as SQL.

Chỉ một thói quen — không bao giờ dán input của user vào chuỗi SQL — đã ngăn cả một lớp lỗi bảo mật.

Bảo mật

SQL injection: lỗi kinh điển

Injection là một trong những lỗi bảo mật phổ biến nhất trong thực tế. Cách dùng ? ở trên là cách sửa chuẩn.

Nối chuỗi (nguy hiểm)

q = "SELECT * FROM users WHERE name = '" + user_input + "'"

Nếu user_input = x' OR '1'='1, truy vấn thành 'name = 'x' OR '1'='1'' — đúng với mọi dòng. Text của user vừa trở thành code SQL.

Placeholder (an toàn)

q = "SELECT * FROM users WHERE name = ?" values = (user_input,)

Dù user_input chứa gì — dấu nháy, OR, DROP TABLE — nó chỉ được so sánh như một giá trị. Không có gì bị thực thi.

Phạm vi

Khi nào SQLite đủ — và khi nào không

Nó là mặc định cho tới khi bạn gặp một bức tường cụ thể.

Một máy, một writer

SQLite cho phép một lần ghi tại một thời điểm (reader chạy song song). Đủ cho đa số app nhỏ/vừa.

Nhúng & mobile

Điện thoại, app desktop, Raspberry Pi — SQLite đã có sẵn. Nó được sinh ra đúng cho việc này.

Dữ liệu nhỏ → vừa

Vài chục MB tới vài GB, hàng triệu dòng — vẫn nhanh trên ổ cứng hiện đại. Đừng 'scale trước'.

Khi nào nên rời đi

Nhiều app server ghi cùng lúc, ghi từ nhiều máy → CSDL dạng server (PostgreSQL, MySQL).

Những lỗi nên tránh

Bốn tội lỗi kinh điển của mô hình dữ liệu

Bạn sẽ gặp cả bốn thứ này trong các dự án thật.

Dữ liệu user bị lặp

Lưu tên/email user trong từng dòng đơn. Sửa một địa chỉ = 500 lần cập nhật. Hãy tách bảng.

Cột FK không có chỉ mục

JOIN và WHERE trên user_id chậm dần khi bảng lớn. Index các cột bạn JOIN.

Danh sách trong một cột

colors = 'red,blue,green' không thể truy vấn, đếm hay JOIN. Đó phải là một bảng với các dòng riêng.

Schema trôi dạt

Tạo bảng tùy tiện ở nhiều nơi, không có nguồn sự thật duy nhất. Giữ một file schema để mọi người đọc.

Quy trình của bạn

Vòng lặp shipper với AI trong đội

Bạn sở hữu mô hình; AI viết truy vấn; CLI là công cụ kiểm chứng của bạn.

  1. 1

    Mô hình hóa

    Vẽ bảng và mối liên kết (1:nhiều / m:n) trước khi có SQL. Cho người thật xem để kiểm tra.

  2. 2

    Soạn SQL

    Mô tả mô hình + câu hỏi cho AI; nó viết SELECT/JOIN. Đọc lại nó so với sơ đồ của bạn.

  3. 3

    Kiểm chứng trong CLI

    Mở .db bằng sqlite3, chạy, nhìn các dòng thật. Sai số dòng? Truy vấn hoặc mô hình đang lệch.

  4. 4

    Tăng tốc

    Chạy EXPLAIN QUERY PLAN; thấy SCAN ở chỗ cần nhanh, thêm index cho cột nóng.

06
Phần 6

Từ shop nhỏ của bạn tới một site e-commerce thật

Vẫn các động từ, khóa và JOIN — giờ áp dụng cho một schema hoàn chỉnh, thực tế. Mỗi bảng có một nhiệm vụ; đây là lý do mỗi bảng tồn tại.

Mở rộng

Từ shop nhỏ tới một site e-commerce thật

users + orders là phần bài tập. Một cửa hàng thật phải xoay xở được với tiền bạc, vận chuyển và thời gian.

Những vấn đề mới

Tiền (thanh toán, hoàn tiền), vận chuyển kèm mã theo dõi, tồn kho, đánh giá, mã giảm giá, giỏ hàng bỏ dở — không thứ nào nằm gọn trong users + orders.

Một bảng cho mỗi thứ

Mỗi danh từ một bảng, mỗi dữ kiện lưu đúng một lần — quy tắc Phần 2, áp dụng ở quy mô đầy đủ.

FK diễn tả mối quan hệ

orders.user_id, order_items.order_id, reviews.product_id… khóa là mối liên kết; JOIN là cách truy vấn xuyên qua chúng.

Snapshot cho lịch sử

Giá và địa chỉ được sao chép vào đơn lúc mua, nên việc sửa sau này không bao giờ ghi đè lịch sử.

Mô hình dữ liệu

Toàn cảnh — commerce cốt lõi

users

idPK
emailUNIQUE
name

orders

idPK
user_idFK → users.id
total · status

order_items

idPK
order_idFK → orders.id
product_idFK → products.id
qty · unit_price

products

idPK
name · price
category_idFK → categories.id

categories

idPK
name

addresses

idPK
user_idFK → users.id
street · city

payments

idPK
order_idFK → orders.id
method · amount

Mỗi bảng = một thứ: người, đơn, mục đơn, sản phẩm, danh mục, địa chỉ, tiền. FK là sợi dây nối; mọi báo cáo chỉ là JOIN xuyên qua chúng.

Mô hình dữ liệu

Vượt ra ngoài bước thanh toán

carts

idPK
user_idFK → users.id

cart_items

idPK
cart_idFK → carts.id
product_idFK → products.id
qty · added_at

shipments

idPK
order_idFK → orders.id
carrier · tracking_no

reviews

idPK
product_idFK → products.id
user_idFK → users.id
rating · textUNIQUE pair

coupons

idPK
codeUNIQUE
discount · expires

Giỏ là 'đang nghĩ sẽ mua', tách khỏi 'đã mua'; shipments tách khỏi vòng đời đơn; reviews là mối quan hệ product↔user có dữ liệu đi kèm; coupons phục vụ kế toán khuyến mãi.

Lý do thiết kế

Vì sao mỗi bảng cốt lõi tồn tại

Đọc cột thứ ba như một bài kiểm tra: nếu bảng nào không giải thích được vì sao nó tồn tại, hãy xóa — dữ liệu đó có lẽ thuộc về nơi khác.

BảngCột chínhVì sao cần bảng này
usersid (PK) · email (UNIQUE)Trục trung tâm: một tài khoản → nhiều đơn, địa chỉ, đánh giá. Mọi thứ gắn với người dùng bắt đầu ở đây.
addressesid (PK) · user_id (FK) · street · cityNgười dùng có nhiều địa chỉ; đơn hàng 'chụp' lại địa chỉ đã dùng, nên chuyển nhà không ghi đè các đơn cũ.
productsid (PK) · name · price · category_id (FK)Mặt hàng trong danh mục, độc lập với đơn. price là giá HIỆN TẠI — giá bạn trả nằm ở order_items.
categoriesid (PK) · nameNhóm 1:nhiều để điều hướng. Mỗi sản phẩm một danh mục chính — cố ý đơn giản, không làm m:n.
ordersid (PK) · user_id (FK) · total · statusMột dòng cho mỗi lần thanh toán: đã đặt gì, total + status tại thời điểm đặt — cột sống của doanh nghiệp.
order_itemsorder_id (FK) · product_id (FK) · qty · unit_priceBảng trung gian m:n CÓ thuộc tính: qty + unit_price chụp tại lúc mua — giá đổi về sau, lịch sử thì không.
Lý do thiết kế

Vì sao mỗi bảng bổ sung tồn tại

Cùng bài kiểm tra như trên: mỗi bảng xứng đáng tồn tại vì giữ những dữ kiện mà không bảng nào khác nên giữ.

BảngCột chínhVì sao cần bảng này
paymentsid (PK) · order_id (FK) · method · amountLưu chuyển tiền được kiểm toán riêng khỏi đơn hàng: hoàn tiền, chia thanh toán là 1:nhiều, đơn hàng vẫn sạch.
shipmentsid (PK) · order_id (FK) · carrier · tracking_noSự kiện vận chuyển tách khỏi vòng đời đơn — cho phép chia lô: một đơn, hai nhà vận chuyển, hai mã theo dõi.
carts / cart_itemscart_id (FK) · product_id (FK) · qty'Đang nghĩ sẽ mua' tách khỏi 'đã mua'. Thanh toán copy các dòng giỏ sang orders + order_items.
reviewsproduct_id (FK) · user_id (FK) · rating · textMối quan hệ m:n product↔user mang rating/text — chính mối quan hệ cũng có thuộc tính riêng. UNIQUE(product_id, user_id): một đánh giá cho mỗi cặp.
couponscode (UNIQUE) · discount · expiresLịch sử dùng mã giảm giá theo từng đơn để kế toán: mã nào, giảm bao nhiêu, và hết hiệu lực khi nào.
Mẫu thiết kế

Ba mẫu bạn sẽ dùng lại — và những gì tiếp theo

Nhận ra các hình dạng này trong mọi schema, dù của bạn hay của người khác.

Snapshot vs tham chiếu

order_items.unit_price và orders.address_id lưu bản sao tại thời điểm sự kiện, nên sửa sau này không bao giờ ghi đè lịch sử.

Bảng trung gian mang thuộc tính

order_items qty/unit_price, reviews rating, cart_items added_at — chính mối quan hệ cũng có dữ liệu.

1:nhiều thắng m:n khi có thể

products.category_id: mỗi sản phẩm một danh mục. Chỉ m:n thật (orders↔products) mới cần bảng trung gian.

Tiếp theo: tính năng tăng trưởng

Danh sách yêu thích, điểm thân thiết — danh từ mới, bảng mới, đúng các quy tắc như cũ.

Tiếp theo: tính năng vận hành

Đa tiền tệ và thuế, nhật ký kiểm toán — lưu chuyển tiền và trách nhiệm giải trình cũng có bảng riêng.

Bài học

Thiết kế mô hình trước: danh từ → bảng → liên kết → khóa. Mỗi bảng có một nhiệm vụ; kiểm chứng từng bảng bằng CLI sqlite3.

Truy vấn mô hình thật

Hai báo cáo trên schema e-commerce

ecommerce_queries.sql
SELECT p.name, SUM(oi.qty * oi.unit_price) AS revenue
FROM   order_items oi
JOIN   products p ON p.id = oi.product_id
JOIN   orders o   ON o.id = oi.order_id
WHERE  o.status IN ('paid','shipped')
GROUP BY p.name
ORDER BY revenue DESC
LIMIT 5;

SELECT o.id, o.total, o.status
FROM   orders o
JOIN   users u ON u.id = o.user_id
WHERE  u.email = 'ana@ex.com'
ORDER BY o.id DESC;
top 5 sản phẩm theo doanh thu
namerevenue
monitor1299.00
keyboard452.00
usb-c hub310.00
mouse186.00
adapter94.00
lịch sử đơn — ana@ex.com
idtotalstatus
139.50pending
1145.00shipped
10120.00paid

Đúng các kỹ thuật đã học: JOIN chuỗi nhiều bảng, WHERE lọc, GROUP BY tổng hợp, ORDER BY + LIMIT top-N — giờ chỉ khác ở quy mô của mô hình.

Mô hình dữ liệu — có những bảng nào, chúng liên hệ thế nào — là quyết định của kiến trúc sư. SQL chỉ là cách ứng dụng nói chuyện với nó.
— toàn bộ khóa học trong một câu

Một file. Vài bảng. Khóa để liên kết chúng.

Thiết kế mô hình trước — rồi để AI viết truy vấn, kiểm chứng trong CLI.