Menu

Partial index trong SQLite: CREATE INDEX ... WHERE

Cách partial index hoạt động trong SQLite: chỉ đánh chỉ mục những hàng bạn thật sự truy vấn, và các mẫu (xóa mềm, duy nhất một phần, tập con hay truy cập) giúp nó phát huy hiệu quả.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Partial index chỉ bao phủ một số hàng

Một chỉ mục (index) thông thường có một mục cho mỗi hàng trong bảng. Partial index chỉ có mục cho những hàng khớp với mệnh đề WHERE mà bạn đưa vào lúc tạo. Chỉ mục nhỏ hơn, ít trang phải duyệt hơn, và ít việc hơn cho mỗi lần insert hay update không chạm tới phần được đánh chỉ mục.

Cú pháp là một câu CREATE INDEX bình thường, gắn thêm WHERE:

idx_orders_pending chỉ chứa mục cho những hàng có status = 'pending'. Các đơn đã giao, đã hủy và đã hoàn tiền hoàn toàn không có trong đó. Nếu 95% bảng orders là dữ liệu lịch sử và bạn chủ yếu truy vấn các đơn đang mở, chỉ mục sẽ nhỏ hơn 20 lần mà tốc độ truy vấn vẫn vậy.

Khi nào trình lập kế hoạch thật sự dùng nó

Partial index chỉ dùng được khi SQLite có thể chứng minh truy vấn của bạn giới hạn trong đúng những hàng mà chỉ mục bao phủ. Cách rõ ràng nhất là lặp lại mệnh đề WHERE của chỉ mục trong truy vấn:

Kế hoạch nên có dòng USING INDEX idx_orders_pending. Bỏ status = 'pending' khỏi truy vấn và trình lập kế hoạch quay về quét toàn bảng, vì nó không có cách nào biết truy vấn nằm trong tập con đã được đánh chỉ mục.

Quy tắc chung: WHERE của truy vấn phải kéo theo WHERE của chỉ mục. So sánh bằng trên cùng cột và cùng giá trị là trường hợp an toàn, dễ thấy. Với bất đẳng thức và OR thì phức tạp hơn; hãy kiểm tra bằng EXPLAIN QUERY PLAN.

Đáng công không: ba lợi ích

Ba lý do cụ thể khiến partial index đáng dùng:

  1. Nhỏ hơn trên đĩa. Chỉ những hàng khớp được lưu. Với khối lượng công việc kiểu "1% bảng hay được truy cập", chỉ mục chỉ khoảng 1% so với chỉ mục đầy đủ.
  2. Ghi rẻ hơn. Insert và update chỉ chạm tới chỉ mục khi hàng khớp bộ lọc. Một lệnh insert với status = 'shipped' vào bảng trên hoàn toàn không động tới idx_orders_pending.
  3. Tốc độ tra cứu như cũ. Tra cứu B-tree có độ phức tạp logarit theo kích thước chỉ mục. Chỉ mục nhỏ hơn thì tra cứu nhanh hơn một chút, nhưng lợi ích lớn hơn nằm ở xung quanh: ít cache miss hơn, ít I/O hơn.

Nếu một cột bị lệch nhiều, tức hầu hết các hàng có cùng một giá trị và bạn chỉ quan tâm đến các giá trị hiếm còn lại, đó là trường hợp kinh điển cho partial index.

Partial unique index (tính năng đắt giá nhất)

Ràng buộc UNIQUE thông thường áp dụng cho mọi hàng. Đó là vấn đề ngay khi bạn bắt đầu dùng xóa mềm (soft delete):

-- Fails: there are two rows with email = 'a@x.com', even though one is deleted.
CREATE UNIQUE INDEX idx_users_email ON users(email);

Partial unique index cho phép bạn đảm bảo tính duy nhất chỉ trên những hàng quan trọng:

Ba hàng, cùng email, không vi phạm ràng buộc nào, vì chỉ hàng có deleted_at IS NULL tham gia kiểm tra tính duy nhất. Thử chèn hàng còn sống thứ hai với cùng email và SQLite sẽ báo UNIQUE constraint failed.

Mẫu này xuất hiện ở khắp nơi: mỗi khách hàng một gói đang hoạt động, mỗi người dùng một địa chỉ chính, mỗi đơn hàng một hóa đơn đang mở. Partial unique index diễn đạt trực tiếp điều đó.

Đánh chỉ mục quanh NULL

NULL tương tác với chỉ mục khá kỳ lạ. Một mục tiêu thường gặp là "bỏ qua hẳn các NULL", chẳng hạn bạn có cột external_id thưa, hầu hết các hàng là NULL nhưng những hàng có giá trị thì phải duy nhất:

Hai NULL cùng tồn tại bình thường; các hàng EXT-001 và EXT-002 được đảm bảo duy nhất. Chỉ mục cũng nhỏ hơn vì hàng NULL không được lưu, nên tra cứu theo external_id vẫn nhanh khi bảng lớn lên.

Bộ lọc được tham chiếu những gì

Mệnh đề WHERE của partial index khá hạn chế. Nó có thể tham chiếu:

  • Các cột của bảng đang được đánh chỉ mục.
  • Hằng số literal.
  • Một số ít hàm dựng sẵn có tính tất định.

Nó không thể tham chiếu:

  • Bảng khác.
  • Truy vấn con.
  • Các hàm không tất định như random() hay CURRENT_TIMESTAMP.
  • Tham số hoặc biến.

Điều này hợp lý: SQLite phải tính bộ lọc mỗi lần insert và update một hàng, và kết quả phải ổn định. Vì vậy câu này chạy được:

Nhưng WHERE created_at > date('now') thì không, vì date('now') thay đổi theo thời gian, nên tập hàng được đánh chỉ mục sẽ tự dịch chuyển mà SQLite không hay biết.

Quy trình kiểm tra nhanh

Khi thêm một partial index, hãy đi qua ba bước kiểm tra:

Truy vấn 1 nên dùng idx_jobs_runnable. Truy vấn 2 và 3 nên quay về quét bảng (hoặc dùng chỉ mục khác, nếu bạn có). Nếu trình lập kế hoạch chọn partial index cho một truy vấn bạn không ngờ tới, hãy đọc lại bộ lọc: có thể nó rộng hơn bạn nghĩ.

Khi nào không nên dùng

Partial index là công cụ sắc bén. Những lý do nên bỏ qua:

  • Bộ lọc khớp với phần lớn bảng. Nếu "đang hoạt động" chiếm 90% số hàng, partial index chỉ là chỉ mục thông thường thêm vài bước. Cứ đánh chỉ mục cột đó là được.
  • Truy vấn của bạn không chứa đúng nguyên văn bộ lọc. Nếu code dùng một ORM sinh ra WHERE status IN (?, ?, ?) hoặc tính bộ lọc động, trình lập kế hoạch thường không nhận ra sự khớp. Hãy kiểm tra bằng EXPLAIN QUERY PLAN, đừng đoán.
  • Tập con hay truy cập thay đổi theo thời gian. Partial index trên "đơn hàng trong 30 ngày qua" nghe hấp dẫn nhưng không viết được, vì bộ lọc phải tất định. Bạn sẽ phải dựng lại chỉ mục, hoặc chọn schema khác (một bảng recent_orders riêng hoặc một cột boolean archived bạn bật mỗi đêm).

Khi bộ lọc ổn định và khớp một phần nhỏ của một bảng lớn, partial index là một trong những cách tinh chỉnh hiệu quả nhất bạn có thể làm trong SQLite.

Tiếp theo: đọc kế hoạch truy vấn

Phần lớn trang này dựa vào EXPLAIN QUERY PLAN để xác nhận chỉ mục thật sự được dùng. Công cụ đó xứng đáng có một trang riêng: cách đọc kết quả của nó, ý nghĩa các từ khóa, và cách phân biệt một lần tra cứu chỉ mục suôn sẻ với một lần quét toàn bảng lén lút. Đó là trang tiếp theo.

Câu hỏi thường gặp

Partial index trong SQLite là gì?

Partial index chỉ đánh chỉ mục những hàng khớp với mệnh đề WHERE được chỉ định lúc tạo. Bạn viết CREATE INDEX name ON table(col) WHERE condition và SQLite chỉ lưu mục cho những hàng có điều kiện đúng. Chỉ mục nhỏ hơn, ghi nhanh hơn, và tốc độ tra cứu vẫn như cũ với các truy vấn khớp bộ lọc.

Khi nào nên dùng partial index thay cho chỉ mục đầy đủ?

Khi bạn liên tục truy vấn một phần nhỏ của một bảng lớn, như đơn hàng đang chờ, người dùng đang hoạt động hay job chưa xử lý. Chỉ đánh chỉ mục phần đó giúp chỉ mục rất nhỏ, và thao tác ghi vào các hàng khác bỏ qua nó hoàn toàn. Nếu truy vấn của bạn không có cùng điều kiện WHERE như chỉ mục, trình lập kế hoạch không dùng được nó.

Partial index có thể đảm bảo tính duy nhất không?

Có. CREATE UNIQUE INDEX ... WHERE ... chỉ đảm bảo tính duy nhất trên những hàng khớp bộ lọc. Cách dùng kinh điển là 'mỗi người dùng chỉ có một bản ghi đang hoạt động': các hàng đã xóa mềm bị loại ra, nên bạn có thể có nhiều mục đã xóa cùng khóa nhưng chỉ một mục còn sống.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu