SERIES INDEX NÂNG CAO - BÀI 1: PHÂN TÍCH NHỮNG SAI LẦM PHỔ BIẾN KHI SỬ DỤNG INDEX TRONG MYSQL

0 0 0

Người đăng: Tờ Mờ Sáng học Lập trình

Theo Viblo Asia

Nếu anh em thấy hay thì ủng hộ tôi 1 follow + 1 upvote + 1 bookmark + 1 comment cho bài viết này tại Mayfest 2025 nhé. Còn nếu bài viết chưa hữu ích thì tôi cũng hi vọng anh em để lại những góp ý thẳng thắn để tôi có thể học hỏi và cải thiện kiến thức của mình. Cảm ơn anh em nhiều!

Mục lục

  • 1. Index trong database là gì?

  • 2. Một ví dụ trực quan

  • 3. Tại sao Index cực kỳ quan trọng đối với hiệu năng truy vấn?

  • 4. Sai lầm phổ biến khi sử dụng Index


1. Index trong database là gì?

Trong database, Index là một cấu trúc dữ liệu được sử dụng để tăng tốc độ truy xuất dòng dữ liệu trong bảng theo một hoặc một số cột cụ thể (là cột đã được đánh index).

Hầu hết các loại index trong MySQL (ví dụ như PRIMARY KEY, UNIQUE, INDEX, FULLTEXT) sử dụng cấu trúc dữ liệu B-Tree để lưu trữ dữ liệu. Ngoài ra còn có SPATIAL sử dụng cấu trúc dữ liệu R-Tree, MEMORY sử dụng hash index. Tôi sẽ phân tích chi tiết các loại này ở các bài viết tiếp theo.

Theo tài liệu chính thức của MySQL:

"Indexes are used to find rows with specific column values quickly. Without an index, MySQL must begin with the first row and then read through the entire table to find the relevant rows." — MySQL Indexes Documentation

Tức là về bản chất, khi không có index, MySQL phải duyệt toàn bộ bảng (Full Table Scan) để tìm dòng dữ liệu phù hợp - một thao tác sẽ gây ra hiệu năng tệ khi dữ liệu lớn.


2. Một ví dụ trực quan

  • Để cho các bạn mới học dễ hình dung, tôi sẽ sử dụng hình ảnh về phần Mục lục mà mọi người thường nhìn thấy trong các cuốn sách. Thử tưởng tượng khi bạn đang đọc một cuốn sách về database dày 1000 trang. Và bạn đang muốn tìm nhanh 1 chương trong cuốn sách, đó là chương "Tối ưu truy vấn SQL"
  • Nếu cuốn sách mà không có phần Mục lục, bạn buộc phải lật từng trang - hành động này giống như bạn đang duyệt toàn bộ dữ liệu trong bảng (Full Table Scan).
  • Nếu có Mục lục, bạn sẽ nhanh chóng tra ngay được chương cần tìm và nhảy đến đúng trang bắt đầu chương đó - hành động này tương tự như khi ta sử dụng Index Lookup.
Thành phần/Hành động Trong sách Trong MySQL
Chương Dữ liệu thực tế trong sách Hàng trong bảng (rows)
Lật từng trang Tìm kiếm tuần tự Full Table Scan
Mục lục Danh sách các chương và số trang tương ứng Index
Nhảy đến số trang ghi ở Mục lục Tìm kiếm theo index Index Lookup

3. Tại sao Index cực kỳ quan trọng đối với hiệu năng truy vấn?

Một truy vấn không index có thể chậm gấp hàng trăm đến hàng nghìn lần so với truy vấn có index

LƯU Ý:

Ở đây tôi dùng từ "có thể" chứ không phải "chắn chắn" hay "luôn luôn". Vì nó còn phụ thuộc vào nhiều yếu tố như độ lớn dữ liệu, chiến lược đánh index, ..., chứ không phải cứ đánh index bừa là auto nhanh đâu nhé.

Ví dụ nếu bảng của bạn trong DB có dữ liệu ít, thì việc đánh index cũng không mang lại sự khác biệt gì lớn lao cả.

Hoặc kể cả khi bạn có 1 bảng dữ liệu lớn, nhưng câu truy vấn của bạn BẮT BUỘC phải truy cập hầu hết các rows mới tính ra được kết quả đúng, thì thêm index cũng không khiến truy vấn trở nên nhanh hơn đâu nhé.

Ví dụ minh họa:

Giả sử bạn có bảng users chứa 1 triệu bản ghi, câu truy vấn dưới đây dùng để lấy ra những user có tên là 'Tờ Mờ Sáng học Lập trình':

SELECT * FROM users WHERE name = 'Tờ Mờ Sáng học Lập trình';
  • Nếu cột name không được đánh index, MySQL phải duyệt toàn bộ 1 triệu dòng (Full Table Scan) .
  • Nếu cột name có được đánh index, MySQL tìm được kết quả chỉ sau log₂(N) bước. Tức là với N = 1 triệu dòng, thì sẽ tìm được kết quả sau khoảng log₂(1000000) = 20 bước .

Tối ưu hóa index đúng cách có thể giảm thời gian truy vấn từ vài giây (s) xuống còn vài mili giây (ms)


4. Sai lầm phổ biến khi sử dụng Index

Sai lầm 1: Đánh index quá nhiều

  • Mỗi index tạo thêm chi phí ghi dữ liệu.
  • INSERT/UPDATE sẽ chậm hơn vì tốn thêm thời gian để cập nhật index (nhưng không phải mọi trường hợp)

LƯU Ý:

Tại sao ở gạch đầu dòng thứ 2 tôi lại mở ngoặc là "không phải mọi trường hợp"?

Bởi vì nếu anh em thử đánh index cho cột dữ liệu ở trong mệnh đề WHERE của câu lệnh UPDATE... WHERE ..., thì index sẽ có thể tăng tốc độ thực hiện câu lệnh này nhiều lần.

Vì khi đó index giúp tăng tốc phần lọc dữ liệu trong mệnh đề WHERE, trong khi thời gian để cập nhật index ấy lại chẳng đáng bao nhiêu.

Sai lầm 2: Không đánh index cho các cột dùng trong WHERE, JOIN, ORDER BY

Đây là các nơi cần đánh index nhất.

❌ NHƯNG, một lần nữa tôi phải nhắc với anh em rằng:

Index không phải là chìa khóa vạn năng, nếu cứ áp dụng máy móc, thấy WHERE, JOIN, ORDER BY cột nào là đánh index cột đó thì cẩn thận có khi còn "vỡ mồm" đó!!!

Ví dụ minh họa:

Giả sử bạn có bảng:

CREATE TABLE users ( id INT PRIMARY KEY, name VARCHAR(100), gender CHAR(1), -- chỉ có 2 giá trị là: 'M' (đại diện cho Male, tức là Nam) và 'F' (đại diện cho Female, tức là Nữ) created_at DATETIME
);

Và bạn tạo index cho cột gender như sau:

CREATE INDEX idx_gender ON users(gender);

Truy vấn sau đây tưởng chừng như index sẽ giúp nhanh hơn:

SELECT * FROM users WHERE gender = 'M';

❌ NHƯNG:

  • Nếu bảng có 1 triệu bản ghi, và 500.000 bản ghi có gender = 'M', thì thực tế index không giúp tăng tốc đáng kể, vì mỗi lần query vẫn trả về quá nhiều rows.
  • Trong khi đó, index idx_gendertốn dung lượng lưu trữ và chi phí cập nhật mỗi lần có thao tác INSERT/UPDATE.

Ở đây tôi sẽ liệt kê cho anh em một số trường hợp ngoại lệ mà chúng ta nên cân nhắc KHÔNG ĐÁNH INDEX cho cột dùng trong WHERE, JOIN, ORDER BY:

  • TH1: Cột có tính phân bố thấp (low cardinality), ví dụ như những cột: Giới tính (Nam/Nữ), cột lưu Trạng thái (0/1).

  • TH2: Cột thường xuyên thay đổi giá trị => chi phí cập nhật index cao. Ví dụ: cột last_login được cập nhật mỗi lần user login. Nếu đánh index cho cột này thì mỗi lần update sẽ phải maintain index => làm chậm tốc độ ghi. Nếu queries theo last_login không thật sự thường xuyên hoặc không selective thì bỏ index sẽ giúp tốc độ ghi tốt hơn.

  • TH3: Dữ liệu trong bảng quá nhỏ ví dụ chỉ loanh quanh 200 - 300 bản ghi, hoặc truy vấn hiếm khi dùng đến => chi phí tạo và duy trì index không đáng.

  • TH4: Mệnh đề WHERE sử dụng LIKE vớileading wildcard. Ví dụ WHERE name LIKE '%Sang' => MySQL không thể tận dụng index nếu pattern bắt đầu bằng % => Full Table Scan vẫn xảy ra.

  • TH5: Column với giá trị NULL quá nhiều. Ví dụ: cột bị NULL với > 90% bản ghi => các câu truy vấn kiểu WHERE cot IS NOT NULL mặc dù có dùng index nhưng thực tế nó vẫn phải scan hầu hết bảng, index ít có giúp ích gì ở đây.

Tôi cũng thường sử dụng câu lệnh EXPLAIN để kiểm tra kế hoạch thực thi trước khi đưa ra quyết định. Tôi sẽ có một bài viết khác để phân tích rõ hơn về câu lệnh này.

Sai lầm 3: Sử dụng hàm (function) trong mệnh đề WHERE trên cột đã đánh index, làm mất tác dụng của index

Ví dụ ta có bảng users, được đánh index ở cột created_at:

CREATE INDEX idx_created_at ON users(created_at);

❌ Nhưng khi ta chạy câu lệnh truy vấn để lọc ra những user được tạo vào năm 2024:

SELECT * FROM users WHERE YEAR(created_at) = 2025;

thì khi chạy lệnh EXPLAIN ta sẽ thấy câu lệnh trên sẽ vô hiệu hóa index (quát sát cột type = ALL và cột key = NULL), vì MySQL không thể sử dụng index khi cột bị bao bởi một hàm. Khi ấy MySQL sẽ buộc phải thực hiện full table scan.

image.png

✅ Thay vào đó, chúng ta nên viết lại câu lệnh theo cách sau:

SELECT * FROM users WHERE created_at BETWEEN '2025-01-01' AND '2025-12-31';

Lúc này index mới phát huy hiệu quả, chạy lại câu lệnh EXPLAIN để kiểm chứng:

image.png

Sai lầm 4: Hiểu sai/Không hiểu về cách hoạt động của Composite Index

Composite index là một index bao gồm nhiều hơn một cột. Nó được sử dụng để tăng tốc độ truy vấn khi truy vấn đó có điều kiện lọc, sắp xếp hoặc nhóm theo nhiều cột cùng lúc.

Thế nhưng tôi đã gặp nhiều bạn hiểu sai hoặc không hiểu về cách hoạt động của composite index, các bạn ấy tạo một composite index (cột1, cột2), nhưng đến khi viết câu lệnh truy vấn thì lại chỉ WHERE theo cột2, thì composite index khi ấy thực tế là không được sử dụng.

Ví dụ:

Tôi có bảng users được đánh composite index:

CREATE INDEX idx_name ON users (last_name, first_name);
  • ✅ TH1: Truy vấn đúng cách (sử dụng tốt composite index)

    SELECT * FROM users WHERE last_name = 'Tran' AND first_name = 'Sang';
    

    hoặc

    SELECT * FROM users WHERE first_name = 'Sang' AND last_name = 'Tran';
    

    Khi chạy lệnh EXPLAIN ta sẽ thấy truy vấn sử dụng cả hai cột theo đúng thứ tự trong composite index idx_name (quan sát cột type = ref và cột key = idx_name):

    image.png

  • ❌ TH2: Truy vấn sử dụng sai thứ tự cột trong composite index

    SELECT * FROM users WHERE first_name = 'Sang';
    

    Kết quả khi chạy lệnh EXPLAIN ta sẽ thấy MySQL thực hiện quét toàn bộ bảng - full table scan (quát sát cột type = ALL và cột key = NULL):

    image.png

    Lý do là vì composite index là (last_name, first_name) => index này sẽ không dùng được nếu không có last_name trong mệnh đề WHERE

  • ⚠️ TH3: Truy vấn chỉ dùng cột đầu tiên trong composite index

    SELECT * FROM users WHERE last_name = 'Tran';
    

    Kết quả là composite index vẫn được dùng, mặc dù không tối ưu bằng việc dùng đầy đủ cả hai cột (quan sát cột type = ref và cột key = idx_name)

    image.png

Vậy tóm lại, khi composite index để thứ tự là (last_name, first_name) thì:

Truy vấn lọc theo Index có được dùng hay không?
last_name ✅ Có
last_name, first_name ✅ Tốt nhất
first_name, last_name ✅ Giống last_name, first_name
first_name ❌ Không

Đây là nguyên tắc leftmost prefix trong MySQL:

"MySQL cannot use the index to perform lookups if the columns do not form a leftmost prefix of the index." — 10.3.6 Multiple-Column Indexes

Trong bài tiếp theo, tôi sẽ cùng anh em đào sâu vào cấu trúc dữ liệu dùng để lưu trữ Index trong MySQL

Hẹn gặp lại 👋


🙋🏻‍♂️ Một số kênh mạng xã hội khác mà tôi dùng để chia sẻ và trao đổi với anh em kiến thức về ngành CNTT và lập trình:

Bình luận

Bài viết tương tự

- vừa được xem lúc

Mô hình quan hệ - thực thể (Entity – Relationship Model)

Mô hình quan hệ thực thể (Entity Relationship model - E-R) được CHEN giới thiệu vào năm 1976 là một mô hình được sử dụng rộng rãi trong các bản thiết kế cơ sở dữ liệu ở mức khái niệm, được xây dựng dựa trên việc nhận thức thế giới thực thông qua tập các đối tượng được gọi là các thực thể và các mối

0 0 138

- vừa được xem lúc

[Embulk #1] Công cụ giúp giảm nỗi đau chuyển đổi dữ liệu

Embulk là gì. Embulk là một công cụ open source có chức năng cơ bản là load các record từ database này và import sang database khác.

0 0 66

- vừa được xem lúc

Window Functions trong MySQL, Nâng cao và cực kì hữu dụng (Phần II).

Chào mọi người, lại là mình đây, ở phần trước mình đã giới thiệu với mọi người về Window Functions Phần I. Nếu chưa rõ nó là gì thì mọi người nên đọc lại trước nha, để nắm được định nghĩa và các key words, tránh mắt chữ O mồm chứ A vì phần này mình chủ yếu sẽ thực hành với các Window Functions.

0 0 115

- vừa được xem lúc

Window Functions trong MySQL, Nâng cao và cực kì hữu dụng (Phần I).

Chào mọi người, mình mới tìm hiểu đc topic Window Functions cá nhân mình cảm thấy khá là hay và mình đánh giá nó là phần nâng cao. Vì ít người biết nên Window Functions thấy rất ít khi sử dụng, thay vì đó là những câu subquery dài dằng dặc như tin nhắn nhắn cho crush, và người khác đọc hiểu được câu

0 0 986

- vừa được xem lúc

Disable và Enable trigger trong Oracle

Origin post: https://www.tranthanhdeveloper.com/2020/12/disable-va-enable-trigger-trong-oracle.html.

0 0 51

- vừa được xem lúc

Lưu trữ dữ liệu với Data Store

. Data Store là một trong những componet của bộ thư viện Android JetPack, nó là một sự lựa chọn hoàn hảo để thay thế cho SharedPreferences để lưu trữ dữ liệu đơn giản dưới dạng key-value. Chúng ta cùng làm một so sánh nhỏ để thấy sự tối ưu của Data Store với SharedPreferences nhé.

0 0 77