Quản lý Indexes và Tối ưu hóa Truy Vấn
54 min read
Mục lục
- Giới Thiệu: Sự Cần Thiết Của Tốc Độ và Vai Trò Của Indexes & Tối Ưu Hóa
- SQL Indexes
- Phần 2: Làm Việc Với Indexes
- Phần 3: Tối Ưu Hóa Truy Vấn
- Tối Ưu Hóa Truy Vấn là gì?
- Mục 3.2: Các Kỹ Thuật Viết Truy Vấn Thông Minh
- Tiểu mục 3.2.1: Tận Dụng Indexes Của Bạn (Tham chiếu Mục 2.2) (Điểm truy vấn 7a)
- Tiểu mục 3.2.2: Hãy Cụ Thể: SELECT Columns vs. SELECT * (Điểm truy vấn 7b)
- Tiểu mục 3.2.3: Lọc Thông Minh: Mệnh đề WHERE và Tính SARGable (Điểm truy vấn 7c)
- Tiểu mục 3.2.4: Chiến Lược JOIN Hiệu Quả (Điểm truy vấn 7d)
- Tiểu mục 3.2.5: Vượt Lên Trên Subquery: Sử Dụng JOINs và CTEs (Điểm truy vấn 7e)
- Mục 3.3: Số Liệu Thống Kê (Statistics): Tai Mắt Của Bộ Tối Ưu Hóa
- Kết Luận: Những Điểm Chính và Bước Tiếp Theo
Giới Thiệu: Sự Cần Thiết Của Tốc Độ và Vai Trò Của Indexes & Tối Ưu Hóa
Trong thế giới công nghệ thông tin hiện đại, tốc độ là yếu tố then chốt. Các ứng dụng từ trang web thương mại điện tử đến hệ thống quản lý nội bộ đều phụ thuộc vào khả năng truy xuất dữ liệu nhanh chóng từ cơ sở dữ liệu (database/CSDL). Người dùng mong đợi phản hồi tức thì, và sự chậm trễ dù chỉ vài giây cũng có thể dẫn đến trải nghiệm tồi tệ hoặc giảm hiệu quả công việc. CSDL thường chứa một lượng thông tin khổng lồ, từ vài nghìn đến hàng tỷ bản ghi. Việc tìm kiếm một mẩu thông tin cụ thể trong biển dữ liệu này đặt ra một thách thức lớn về hiệu năng.
Khi một truy vấn SQL được gửi đến CSDL để lấy dữ liệu, phương pháp mặc định và đơn giản nhất mà hệ thống có thể sử dụng là "quét toàn bộ bảng" (full table scan). Hãy tưởng tượng bạn phải tìm một cuốn sách cụ thể trong một thư viện khổng lồ mà không có bất kỳ hệ thống danh mục nào; bạn sẽ phải xem xét từng cuốn sách trên từng kệ. Tương tự, quét toàn bộ bảng có nghĩa là CSDL phải đọc tuần tự từng hàng (row) trong bảng để kiểm tra xem hàng đó có khớp với điều kiện truy vấn hay không. Mặc dù cách này đảm bảo tìm thấy dữ liệu (nếu có), nó cực kỳ không hiệu quả, đặc biệt đối với các bảng lớn chứa hàng triệu hoặc hàng tỷ hàng (). Thời gian chờ đợi có thể kéo dài, làm chậm ứng dụng và tiêu tốn tài nguyên hệ thống (CPU, bộ nhớ, I/O đĩa).
May mắn thay, các hệ quản trị cơ sở dữ liệu (DBMS) cung cấp các công cụ mạnh mẽ để giải quyết vấn đề này: Indexes (Chỉ mục) và Query Optimization (Tối ưu hóa truy vấn). Indexes giống như danh mục trong thư viện, cung cấp một đường dẫn nhanh chóng đến dữ liệu cần tìm. Tối ưu hóa truy vấn là quá trình mà DBMS (và cả người viết truy vấn) cố gắng tìm ra cách thực thi truy vấn hiệu quả nhất. Vấn đề cốt lõi không chỉ là tìm thấy dữ liệu, mà là tìm thấy nó một cách hiệu quả trong các tập dữ liệu có thể rất lớn. Quét toàn bộ bảng là phương pháp brute-force mặc định, trong khi indexes cung cấp một cách tiếp cận thông minh hơn, có mục tiêu hơn. CSDL được thiết kế để lưu trữ dữ liệu, nhưng tiện ích của chúng phụ thuộc vào việc truy xuất dữ liệu đó một cách hiệu quả. Khi khối lượng dữ liệu tăng lên, sự kém hiệu quả của việc quét tuyến tính trở thành một nút thắt cổ chai lớn. Indexes cung cấp một cách truy cập dữ liệu phi tuyến tính, thay đổi cơ bản độ phức tạp của việc tìm kiếm, thường từ O(N) cho việc quét thành O(log N) hoặc thậm chí O(1) cho các tra cứu có chỉ mục. Sự khác biệt này là nền tảng của hiệu năng CSDL khi truy xuất.
Báo cáo này nhằm mục đích giải thích các khái niệm về SQL Indexes và Query Optimization một cách chi tiết, rõ ràng và dễ hiểu cho những người mới bắt đầu làm quen với công nghệ thông tin hoặc SQL, giúp xây dựng nền tảng vững chắc về tối ưu hóa hiệu năng cơ sở dữ liệu.
SQL Indexes
Index là gì?
Cách đơn giản nhất để hiểu về SQL Index là liên tưởng đến mục lục ở cuối một cuốn sách. Khi bạn muốn tìm thông tin về một chủ đề cụ thể trong sách, thay vì lật từng trang từ đầu đến cuối, bạn sẽ tra cứu mục lục. Mục lục liệt kê các từ khóa hoặc chủ đề theo thứ tự bảng chữ cái và chỉ cho bạn số trang chính xác nơi thông tin đó xuất hiện. Điều này giúp bạn tìm kiếm nhanh hơn rất nhiều.
Một SQL Index hoạt động theo nguyên tắc tương tự:
Nó là một cấu trúc dữ liệu riêng biệt được liên kết với một bảng (table) trong cơ sở dữ liệu.
Index này chứa các giá trị từ một hoặc nhiều cột của bảng đó (đóng vai trò như "từ khóa" hoặc "khóa" - keys ) và các "con trỏ" (pointers) - giống như số trang - trỏ đến vị trí thực tế của các hàng dữ liệu tương ứng trong bảng ().
Mục đích chính của index là tăng tốc độ truy xuất dữ liệu:
Khi bạn thực hiện một truy vấn (ví dụ:
SELECT * FROM Users WHERE UserID = 123
), thay vì phải duyệt qua toàn bộ bảngUsers
, CSDL có thể sử dụng index trên cộtUserID
(nếu có). Nó nhanh chóng tìm thấy giá trị123
trong index, lấy con trỏ tương ứng và đi thẳng đến hàng dữ liệu cần thiết trong bảng.Quá trình này giúp loại bỏ việc phải quét toàn bộ bảng, đặc biệt hiệu quả với các bảng lớn. Về cơ bản, index giảm đáng kể số lượng trang dữ liệu (data pages) mà DBMS cần phải truy cập hoặc quét qua.
Cần lưu ý rằng indexes là các không bắt buộc. Bạn có thể có một CSDL hoạt động mà không cần bất kỳ index nào (ngoại trừ các index thường được tạo tự động cho khóa chính).
Indexes không phải là một phần của tiêu chuẩn SQL chính thức mà là các kỹ thuật triển khai được sử dụng bởi các DBMS để tăng tốc độ hoạt động.
Việc tạo ra indexes thực chất là tạo ra sự dư thừa dữ liệu vì mục đích tốc độ. Chúng lưu trữ bản sao của dữ liệu (các cột được đánh index) trong một cấu trúc riêng biệt, được tối ưu hóa để tránh quá trình tìm kiếm chậm chạp trên dữ liệu bảng chính. Dữ liệu bảng gốc có thể được tổ chức để tối ưu hóa việc lưu trữ hoặc dựa trên khóa chính (clustered index, sẽ được thảo luận sau), nhưng không nhất thiết phải tối ưu cho việc tìm kiếm trên mọi cột.
Một index hoạt động như một bảng tra cứu chuyên biệt, được sắp xếp trước cho các cột cụ thể, chứa các con trỏ tới các hàng dữ liệu đầy đủ. Sự tách biệt này cho phép sử dụng các cấu trúc tìm kiếm tối ưu (như B-trees) mà không cần quyết định thứ tự lưu trữ vật lý của toàn bộ bảng (đối với non-clustered indexes). Cái giá phải trả cho sự dư thừa này là không gian lưu trữ và chi phí bảo trì khi dữ liệu thay đổi, đây chính là sự đánh đổi cơ bản sẽ được thảo luận chi tiết hơn.
Cách Indexes Tăng Tốc (Cấu Trúc B-Tree)
Vậy làm thế nào mà index thực sự giúp tìm kiếm nhanh hơn? Câu trả lời nằm ở cấu trúc dữ liệu cơ bản được sử dụng phổ biến nhất cho indexes: B-tree (Balanced Tree - Cây cân bằng).
Một biến thể rất phổ biến được sử dụng trong nhiều cơ sở dữ liệu là B+ tree.
Hãy hình dung B-tree như một cây bị lộn ngược hoặc quá trình sắp xếp và phân loại thẻ bài. Cấu trúc này bao gồm các thành phần:
Nút Gốc (Root Node): Đây là điểm bắt đầu của mọi cuộc tìm kiếm trong index ().
Nút Nhánh/Nút Trung Gian (Branch/Internal Nodes): Các nút này nằm giữa nút gốc và các nút lá. Chúng chứa các khoảng giá trị khóa (key) đã được sắp xếp và các con trỏ chỉ xuống các nút ở cấp thấp hơn trong cây. Khi tìm kiếm, DBMS so sánh giá trị cần tìm với các khoảng giá trị trong nút hiện tại để quyết định sẽ đi theo nhánh nào tiếp theo.
Nút Lá (Leaf Nodes): Đây là tầng dưới cùng của cây B-tree. Trong nhiều triển khai (như B+ trees), các nút lá chứa các giá trị khóa thực tế của index, được sắp xếp theo thứ tự, cùng với các con trỏ trỏ đến các hàng dữ liệu tương ứng trong bảng chính. Các nút lá này thường được liên kết với nhau theo một danh sách liên kết đôi (doubly-linked list), cho phép duyệt qua các giá trị trong một khoảng một cách hiệu quả (range scan).
B-tree có hai đặc tính quan trọng giúp nó hoạt động hiệu quả:
Cân Bằng (Balanced): Tất cả các nút lá đều nằm ở cùng một độ sâu (cùng một khoảng cách từ nút gốc). Điều này đảm bảo rằng thời gian tìm kiếm bất kỳ giá trị nào trong index là nhất quán và có thể dự đoán được.
Được Sắp Xếp (Sorted): Các giá trị khóa trong mỗi nút (cả nút nhánh và nút lá) luôn được duy trì theo thứ tự sắp xếp ().
Cơ chế tìm kiếm trong B-tree hoạt động như sau: DBMS bắt đầu từ nút gốc, so sánh giá trị cần tìm với các khóa trong nút đó. Dựa trên kết quả so sánh, nó đi theo con trỏ thích hợp xuống nút ở cấp tiếp theo. Quá trình này lặp lại, đi từ cấp này sang cấp khác, cho đến khi đến được nút lá chứa giá trị cần tìm (hoặc xác định giá trị đó không tồn tại trong index). Quá trình điều hướng này giống như việc tìm một từ trong từ điển bằng cách liên tục thu hẹp phạm vi tìm kiếm.
Do cấu trúc phân cấp và cân bằng này, việc tìm kiếm trong B-tree cực kỳ hiệu quả. Thay vì phải xem xét N hàng (như trong full table scan), B-tree chỉ yêu cầu xem xét một số lượng nút rất nhỏ, tỷ lệ thuận với logarit của số lượng hàng (độ phức tạp thời gian O(log N)). Điều này có nghĩa là ngay cả khi bảng tăng kích thước lên gấp nhiều lần, thời gian tìm kiếm thông qua index chỉ tăng lên rất ít. Cấu trúc B-tree chính là "động cơ" tạo nên hiệu năng của index. Tính chất cân bằng và sắp xếp của nó cho phép cơ sở dữ liệu thực hiện tìm kiếm "chia để trị" hiệu quả cao, tương tự như tra từ điển hoặc tìm số trong danh sách đã sắp xếp bằng cách liên tục chia đôi không gian tìm kiếm. Tính chất "cân bằng" rất quan trọng vì nó đảm bảo hiệu suất logarit này là nhất quán, ngăn chặn các trường hợp xấu nhất khi cây bị lệch (trở nên giống như danh sách liên kết, làm giảm hiệu suất trở lại O(N)). Các nút chứa nhiều khóa giúp giảm chiều cao của cây so với cây nhị phân, giảm thiểu số lượng thao tác I/O đĩa cần thiết (vì mỗi lần duyệt qua một nút có thể liên quan đến việc đọc một trang đĩa).
Các Loại Index Phổ Biến
Không phải tất cả các index đều giống nhau. Các hệ quản trị cơ sở dữ liệu cung cấp nhiều loại index khác nhau, mỗi loại được thiết kế để phù hợp với các tình huống và kiểu dữ liệu cụ thể. Việc hiểu rõ các loại index phổ biến sẽ giúp lựa chọn chiến lược indexing phù hợp.
Dưới đây là một số loại index thường gặp nhất, cùng với các trường hợp sử dụng đơn giản:
Index Đơn Cột (Single-Column Index): Đây là loại index cơ bản nhất, được tạo trên chỉ một cột duy nhất của bảng.
- Trường hợp sử dụng: Tăng tốc các truy vấn thường xuyên lọc (sử dụng
WHERE
) hoặc sắp xếp (sử dụngORDER BY
) dựa trên giá trị của cột đó. Ví dụ:SELECT * FROM Products WHERE CategoryID = 5;
- một index trênCategoryID
sẽ hữu ích.
- Trường hợp sử dụng: Tăng tốc các truy vấn thường xuyên lọc (sử dụng
Index Ghép/Phức Hợp (Composite/Multi-Column Index): Loại index này được tạo trên hai hoặc nhiều cột của bảng.
- Trường hợp sử dụng: Tăng tốc các truy vấn lọc hoặc sắp xếp dựa trên sự kết hợp của các cột đó, đặc biệt là khi điều kiện lọc khớp với thứ tự các cột trong index. Ví dụ:
SELECT * FROM Employees WHERE LastName = 'Nguyen' AND FirstName = 'An';
- một index trên(LastName, FirstName)
sẽ rất hiệu quả. Thứ tự các cột trong định nghĩa index ghép là rất quan trọng; index trên(LastName, FirstName)
sẽ không hiệu quả bằng cho truy vấn chỉ lọc theoFirstName
.
- Trường hợp sử dụng: Tăng tốc các truy vấn lọc hoặc sắp xếp dựa trên sự kết hợp của các cột đó, đặc biệt là khi điều kiện lọc khớp với thứ tự các cột trong index. Ví dụ:
Index Duy Nhất (Unique Index): Index này không chỉ tăng tốc truy vấn mà còn đảm bảo rằng tất cả các giá trị trong cột (hoặc tổ hợp các cột) được đánh index phải là duy nhất; không có hai hàng nào có cùng giá trị.
- Trường hợp sử dụng: Thường được sử dụng cho các cột khóa chính (primary key) hoặc các cột khác yêu cầu tính duy nhất, chẳng hạn như địa chỉ email hoặc mã số nhân viên. Nó vừa đảm bảo tính toàn vẹn dữ liệu, vừa tăng tốc độ tra cứu dựa trên các giá trị duy nhất đó.
Index Phân Cụm (Clustered Index): Đây là một loại index đặc biệt. Thay vì tạo một cấu trúc riêng biệt trỏ đến dữ liệu, clustered index xác định thứ tự lưu trữ vật lý của chính các hàng dữ liệu trong bảng, dựa trên các giá trị khóa của index.
Analogy: Giống như một cuốn từ điển, nơi các từ được sắp xếp theo thứ tự bảng chữ cái và nội dung của chúng đi kèm ngay sau đó (), hoặc một danh bạ điện thoại được sắp xếp theo tên ().
Đặc điểm: Mỗi bảng chỉ có thể có một clustered index vì dữ liệu chỉ có thể được sắp xếp vật lý theo một thứ tự duy nhất. Trong nhiều hệ thống (như SQL Server), khóa chính thường mặc định được tạo là clustered index.
Trường hợp sử dụng: Rất hiệu quả cho các truy vấn tìm kiếm một khoảng giá trị trên cột được clustered (ví dụ:
WHERE OrderDate BETWEEN '2023-01-01' AND '2023-01-31'
) vì các hàng liên quan nằm gần nhau về mặt vật lý. Các bảng không có clustered index đôi khi được gọi là "heap".
Index Không Phân Cụm (Non-Clustered Index): Đây là loại index "tiêu chuẩn" và phổ biến nhất. Cấu trúc index (thường là B-tree) được lưu trữ tách biệt khỏi các hàng dữ liệu thực tế. Index chứa các giá trị khóa và các con trỏ trỏ đến vị trí của hàng dữ liệu tương ứng (có thể nằm trong heap hoặc trong clustered index) ().
Analogy: Giống như mục lục ở cuối sách giáo khoa, liệt kê các thuật ngữ và số trang nơi chúng xuất hiện. Dữ liệu trong sách (bảng) có thể không theo thứ tự của mục lục.
Đặc điểm: Một bảng có thể có nhiều non-clustered indexes.
Trường hợp sử dụng: Dùng để tăng tốc truy vấn trên các cột không phải là khóa chính, hoặc để cung cấp các thứ tự sắp xếp thay thế cho dữ liệu mà không ảnh hưởng đến cách lưu trữ vật lý của bảng.
Các Loại Index Khác (Đề cập ngắn gọn): Ngoài các loại trên, còn có các index chuyên dụng khác tùy thuộc vào hệ quản trị cơ sở dữ liệu:
Full-Text Index: Tối ưu hóa cho việc tìm kiếm văn bản trong các cột chứa nhiều chữ (ví dụ: nội dung bài viết, email).
Hash Index: Sử dụng hàm băm để tạo index, rất nhanh cho các truy vấn tìm kiếm chính xác (phép toán
=
), nhưng thường không hỗ trợ tìm kiếm theo khoảng.GiST, GIN, SP-GiST, BRIN (PostgreSQL): Các loại index chuyên biệt trong PostgreSQL dùng cho các kiểu dữ liệu phức tạp như dữ liệu hình học, mảng, JSONB, hoặc cho các bảng rất lớn.
Việc lựa chọn loại index phù hợp là rất quan trọng vì nó quyết định cách index được cấu trúc và lưu trữ, từ đó ảnh hưởng đến đặc tính hiệu năng và sự phù hợp của nó đối với các mẫu truy vấn và kiểu dữ liệu khác nhau:
Một clustered index sắp xếp dữ liệu vật lý, làm cho các truy vấn phạm vi trên khóa clustered rất nhanh (dữ liệu liền kề) nhưng chỉ cho phép một index loại này trên mỗi bảng.
Non-clustered indexes mang lại sự linh hoạt (cho phép nhiều index) nhưng có thể yêu cầu một bước tra cứu bổ sung để lấy dữ liệu hàng đầy đủ.
Unique indexes () thêm một ràng buộc về tính toàn vẹn dữ liệu.
Composite indexes giải quyết việc lọc nhiều cột nhưng nhạy cảm với thứ tự cột.
Các index chuyên dụng như Full-Text hoặc Hash sử dụng các cấu trúc hoàn toàn khác nhau được tối ưu hóa cho các tác vụ cụ thể (tìm kiếm văn bản, khớp chính xác) nơi B-trees có thể kém hiệu quả hơn.
Chọn đúng loại index là một phần quan trọng của việc tối ưu hóa.
Phần 2: Làm Việc Với Indexes
Tạo và Xóa Indexes
Việc thêm hoặc bớt index khỏi cơ sở dữ liệu là những tác vụ quản trị cơ bản. Cú pháp SQL để thực hiện việc này tương đối chuẩn, mặc dù có thể có những khác biệt nhỏ giữa các hệ quản trị cơ sở dữ liệu khác nhau như MySQL, PostgreSQL, SQL Server, Oracle, v.v.
Cú pháp tạo Index:
Lệnh phổ biến nhất để tạo một index (thường là non-clustered B-tree theo mặc định) là CREATE INDEX
:
CREATE INDEX index_name
ON table_name (column1, column2,...);
Tạo một Unique Index:
CREATE UNIQUE INDEX index_name
ON table_name (column_name);
Tạo một Clustered Index (cú pháp có thể thay đổi, đây là ví dụ cho SQL Server):
CREATE CLUSTERED INDEX index_name
ON table_name (column_name);
Sử dụng ALTER TABLE
để thêm index (trong một số hệ thống):
ALTER TABLE table_name
ADD INDEX index_name (column_name);
Cú pháp xóa Index:
Để loại bỏ một index không còn cần thiết, sử dụng lệnh DROP INDEX
:
DROP INDEX index_name ON table_name;
Một số hệ thống có thể có cú pháp hơi khác, ví dụ:
DROP INDEX table_name.index_name;
hoặcALTER TABLE table_name DROP INDEX index_name;
Ví dụ đơn giản:
Giả sử có bảng Customers
với cột Email
. Để tạo index trên cột Email
:
-- Tạo index trên cột Email của bảng Customers
CREATE INDEX idx_customers_email ON Customers (Email);
Sau khi tạo, truy vấn lọc theo Email
(ví dụ: WHERE Email = '
example@domain.com
'
) sẽ nhanh hơn đáng kể.
Để xóa index này:
-- Xóa index idx_customers_email khỏi bảng Customers
DROP INDEX idx_customers_email ON Customers;
Việc tạo và xóa index là các công cụ cơ bản. Phần quan trọng không chỉ là biết cú pháp, mà là hiểu khi nào và ở đâu nên tạo index, điều này sẽ được thảo luận tiếp theo.
Chọn Cột Nào Để Đặt Index
Việc quyết định cột nào cần được đánh index là một trong những khía cạnh quan trọng nhất của việc quản lý index hiệu quả. Mục tiêu là xác định các cột mà việc tạo index trên đó sẽ mang lại lợi ích về hiệu năng lớn nhất cho các truy vấn thường xuyên được thực thi. Việc đánh index không nên thực hiện một cách ngẫu nhiên.
Dưới đây là những ứng viên hàng đầu cho việc đánh index:
Cột trong Mệnh đề
WHERE
: Đây là những ứng viên rõ ràng nhất. Nếu bạn thường xuyên lọc dữ liệu dựa trên một cột cụ thể, việc tạo index trên cột đó sẽ giúp DBMS nhanh chóng xác định các hàng phù hợp mà không cần quét toàn bộ bảng (). Ví dụ: trong truy vấnSELECT * FROM Orders WHERE Status = 'Pending';
, cộtStatus
là một ứng viên tốt cho index.Cột trong Mệnh đề
JOIN
: Các cột được sử dụng để kết nối các bảng với nhau (thường là khóa ngoại - foreign keys) là cực kỳ quan trọng để đánh index. Index trên các cột này giúp tăng tốc đáng kể hoạt động JOIN bằng cách cho phép DBMS nhanh chóng tìm thấy các hàng khớp giữa các bảng (). Ví dụ: trongSELECT... FROM Orders o JOIN Customers c ON o.CustomerID = c.CustomerID;
, cảOrders.CustomerID
vàCustomers.CustomerID
đều nên được xem xét để đánh index.Cột trong Mệnh đề
ORDER BY
: Nếu bạn thường xuyên yêu cầu kết quả được sắp xếp theo một cột cụ thể, index trên cột đó có thể giúp ích. Index (đặc biệt là B-tree) vốn đã lưu trữ các giá trị khóa theo thứ tự sắp xếp. DBMS có thể tận dụng thứ tự này để trả về kết quả đã được sắp xếp mà không cần thực hiện một bước sắp xếp riêng biệt, tốn kém (). Ví dụ:SELECT * FROM Articles ORDER BY PublishDate DESC;
- index trênPublishDate
có thể hữu ích.Khóa Chính (Primary Keys): Hầu hết các hệ quản trị cơ sở dữ liệu tự động tạo index (thường là unique clustered index) trên cột khóa chính (). Điều này là cần thiết vì khóa chính được sử dụng để xác định duy nhất các hàng và thường xuyên được dùng để tra cứu.
Khóa Ngoại (Foreign Keys): Như đã đề cập trong phần
JOIN
, khóa ngoại là những ứng viên rất mạnh mẽ cho việc đánh index để cải thiện hiệu năng kết nối bảng ().
Đặc điểm của Cột:
Ngoài việc xem xét các mệnh đề SQL, đặc điểm của chính cột đó cũng quan trọng:
Tính Chọn Lọc (Cardinality): Các cột có độ chọn lọc cao (nghĩa là có nhiều giá trị duy nhất so với tổng số hàng, ví dụ: cột ID, email) thường là ứng viên tốt hơn cho index so với các cột có độ chọn lọc thấp (ít giá trị duy nhất, ví dụ: cột giới tính, trạng thái chỉ có vài giá trị).
Một index trên cột chỉ có hai giá trị (True/False) thường không giúp ích nhiều vì mỗi giá trị sẽ trỏ đến khoảng một nửa số hàng trong bảng.
- Kiểu Dữ Liệu: Một số kiểu dữ liệu có thể phù hợp hơn cho việc đánh index như các cột số nguyên (integer) thường là ứng viên tốt. Các cột văn bản dài (BLOB, TEXT) có thể cần các loại index đặc biệt như Full-Text hoặc chỉ nên index một phần đầu của chuỗi (prefix index).
Việc đánh index không phải là ngẫu nhiên; đó là một chiến lược có mục tiêu dựa trên cách dữ liệu được truy cập. Bạn đánh index các cột mà cơ sở dữ liệu sử dụng để tìm, kết nối, hoặc sắp xếp dữ liệu trong các truy vấn quan trọng nhất của bạn. Indexes hoạt động bằng cách cung cấp đường dẫn tra cứu nhanh dựa trên các giá trị cột được đánh index. Do đó, chúng hiệu quả nhất khi truy vấn cung cấp một giá trị hoặc phạm vi cụ thể cho một cột được đánh index (WHERE
), cần kết nối các hàng dựa trên các giá trị khớp trong các cột được đánh index (JOIN
), hoặc cần dữ liệu được trình bày theo thứ tự do index cung cấp (ORDER BY
). Đánh index các cột không được sử dụng theo những cách này mang lại ít hoặc không có lợi ích về tốc độ truy vấn nhưng vẫn phải chịu chi phí (lưu trữ, chi phí ghi). Độ chọn lọc cao (high cardinality) làm cho một index hiệu quả hơn (mỗi giá trị trỏ đến ít hàng hơn), tăng khả năng hữu dụng của nó.
Phân tích các truy vấn
SELECT
phổ biến và quan trọng nhất mà ứng dụng thực hiện sau đó xác định các cột thường xuyên xuất hiện trong các mệnh đềWHERE
,JOIN
, vàORDER BY
. Đây chính là những nơi mà index có khả năng mang lại lợi ích lớn nhất.
Ưu và Nhược Điểm Của Index
Mặc dù indexes là công cụ mạnh mẽ để tăng tốc độ truy vấn, việc sử dụng chúng không phải là không có chi phí. Có một sự đánh đổi quan trọng cần phải hiểu rõ khi quyết định tạo và quản lý indexes.
Ưu điểm:
- Cải thiện Hiệu năng Truy vấn (Đọc): Đây là lợi ích chính và rõ ràng nhất. Indexes giúp các truy vấn
SELECT
, đặc biệt là những truy vấn có điều kiện lọc (WHERE
), kết nối (JOIN
), hoặc sắp xếp (ORDER BY
) trên các cột được đánh index, chạy nhanh hơn đáng kể.
Nhược điểm:
Chi phí cho các Thao tác Sửa đổi Dữ liệu (Hiệu năng Ghi): Đây là nhược điểm lớn nhất. Mỗi khi dữ liệu trong bảng bị thay đổi thông qua các lệnh
INSERT
,UPDATE
, hoặcDELETE
, không chỉ dữ liệu trong bảng chính cần được cập nhật, mà tất cả các indexes liên quan đến các cột bị ảnh hưởng cũng phải được cập nhật.Ví dụ, khi chèn một hàng mới, DBMS phải thêm các mục tương ứng vào từng non-clustered index trên bảng đó. Khi cập nhật giá trị của một cột được đánh index, DBMS phải xóa mục index cũ và chèn mục index mới. Quá trình cập nhật index này tốn thời gian và tài nguyên hệ thống, làm chậm các thao tác ghi dữ liệu. Càng nhiều index trên một bảng, chi phí cho mỗi thao tác ghi càng cao.
Tốn Không Gian Lưu Trữ: Indexes không phải là miễn phí về mặt dung lượng. Chúng là các cấu trúc dữ liệu riêng biệt và chiếm dụng không gian trên đĩa cứng hoặc SSD (). Kích thước của một index phụ thuộc vào số lượng hàng trong bảng, số lượng và kiểu dữ liệu của các cột trong index (). Nhiều index trên các bảng lớn có thể làm tăng đáng kể tổng dung lượng lưu trữ của cơ sở dữ liệu, ảnh hưởng đến chi phí lưu trữ và thời gian sao lưu/phục hồi ().
Do đó, việc quản lý index đòi hỏi một sự cân bằng tinh tế. Cần phải cân nhắc giữa lợi ích về tốc độ đọc (SELECT) và chi phí về tốc độ ghi (INSERT, UPDATE, DELETE) cùng với chi phí lưu trữ.
Nguy cơ của Việc Đánh Index Quá Nhiều (Over-Indexing):
Một sai lầm phổ biến là cố gắng đánh index cho mọi cột có thể ("indexing overkill" ). Việc có quá nhiều index trên một bảng có thể làm chậm đáng kể các thao tác ghi dữ liệu, tiêu tốn quá nhiều không gian lưu trữ và thậm chí có thể làm chậm cả truy vấn đọc vì DBMS phải mất thêm thời gian để quyết định nên sử dụng index nào (hoặc có thể chọn nhầm index). Nguyên tắc chung là chỉ tạo index khi thực sự cần thiết và khi lợi ích về hiệu năng đọc vượt trội so với chi phí ghi và lưu trữ.
Việc đánh index không phải là một giải pháp miễn phí. Mỗi index được thêm vào cải thiện một số kịch bản đọc nhất định nhưng lại áp đặt một "thuế" lên mọi hoạt động ghi và tiêu tốn dung lượng lưu trữ. Khi một hàng được chèn, cập nhật hoặc xóa, cơ sở dữ liệu phải sửa đổi không chỉ dữ liệu bảng cơ sở mà còn tất cả các index bao gồm các cột hoặc hàng bị ảnh hưởng (). Điều này liên quan đến việc tìm đúng vị trí trong cấu trúc của mỗi index (ví dụ: B-tree) và có khả năng sắp xếp lại các nút (tách/hợp nhất ). Càng có nhiều index, càng có nhiều công việc cần phải thực hiện cho mỗi lần ghi, làm tăng độ trễ và tiêu thụ tài nguyên (). Tương tự, index là các cấu trúc vật lý yêu cầu lưu trữ (), đôi khi làm tăng đáng kể kích thước cơ sở dữ liệu (). Do đó, quyết định thêm một index đòi hỏi phải xem xét cẩn thận tỷ lệ đọc/ghi của bảng và tần suất/tầm quan trọng của các truy vấn mà index đó sẽ hỗ trợ (). Chiến lược đánh index tối ưu sẽ giảm thiểu "thuế" này trong khi tối đa hóa lợi ích cho các truy vấn đọc quan trọng nhất.
Bảo Trì Index (Housekeeping)
Giống như một ngôi nhà cần được dọn dẹp định kỳ để giữ ngăn nắp, indexes cũng cần được "bảo trì" để duy trì hiệu quả hoạt động theo thời gian. Do các thao tác INSERT
, UPDATE
, và DELETE
liên tục diễn ra trên bảng, cấu trúc của các index liên quan có thể trở nên lộn xộn hoặc "phân mảnh" (fragmented).
Giải thích về Phân Mảnh (Fragmentation):
Phân mảnh index xảy ra theo hai dạng chính:
Phân Mảnh Logic (Logical Fragmentation): Điều này xảy ra khi thứ tự logic của các trang (pages) trong index (dựa trên giá trị khóa) không khớp với thứ tự vật lý của chúng trong tệp dữ liệu (). Ví dụ, trang index số 1 chỉ đến trang số 2, nhưng do các hoạt động sửa đổi, nó lại chỉ đến trang số 5, rồi trang 5 lại chỉ đến trang số 2. Điều này làm cho việc quét tuần tự qua các trang index (ví dụ, khi thực hiện range scan) trở nên kém hiệu quả hơn vì DBMS phải "nhảy" qua lại giữa các trang không liền kề.
Phân Mảnh Bên Ngoài (External Fragmentation - liên quan đến Phân mảnh Vật lý): Điều này xảy ra khi các trang dữ liệu của index bị phân tán rải rác trên đĩa lưu trữ, thay vì nằm liền kề nhau. Điều này buộc đầu đọc/ghi của đĩa phải di chuyển nhiều hơn để đọc các trang cần thiết, làm tăng thời gian I/O.
Ngoài ra, một vấn đề liên quan là Mật độ Trang (Page Density) / Độ Đầy Trang (Page Fullness). Khi các trang index được tạo hoặc khi các hàng bị xóa, các trang có thể không được lấp đầy hoàn toàn. Ví dụ, khi một trang đầy bị tách ra để chứa hàng mới, hai trang mới có thể chỉ đầy khoảng 50%. Mật độ trang thấp có nghĩa là cần nhiều trang hơn (và do đó cần nhiều thao tác I/O hơn) để lưu trữ cùng một lượng dữ liệu, đồng thời lãng phí không gian lưu trữ. Phân mảnh thường đi kèm với mật độ trang thấp.
Tác Động của Phân Mảnh:
Phân mảnh index là làm suy giảm hiệu năng truy vấn. DBMS phải thực hiện nhiều thao tác I/O hơn để đọc cùng một lượng dữ liệu do các trang bị phân tán hoặc không đầy đủ. Điều này làm tăng thời gian phản hồi của truy vấn và tiêu tốn nhiều tài nguyên hệ thống hơn.
Hoạt Động Bảo Trì Index:
Có hai hoạt động chính để khắc phục tình trạng phân mảnh và duy trì index:
Tổ Chức Lại (Reorganize): Hoạt động này sắp xếp lại các trang ở cấp lá (leaf level) của index để thứ tự vật lý khớp với thứ tự logic. Nó cũng nén chặt các trang để loại bỏ không gian trống, dựa trên cài đặt fill factor (tỷ lệ lấp đầy trang).
Đặc điểm: Ít tốn tài nguyên hơn Rebuild, thường được thực hiện trực tuyến (online), nghĩa là bảng vẫn có thể được truy cập trong khi reorganize diễn ra. Chỉ tác động đến cấp lá. Không cập nhật số liệu thống kê (statistics).
Khi nào sử dụng: Phù hợp cho các index có mức độ phân mảnh thấp đến trung bình (ví dụ: thường là từ 5-10% đến dưới 30%).
Xây Dựng Lại (Rebuild): Hoạt động này thực hiện một công việc triệt để hơn: nó xóa hoàn toàn index cũ và tạo ra một index mới, sạch sẽ và tối ưu.
Đặc điểm: Loại bỏ hoàn toàn mọi phân mảnh, nén chặt các trang theo fill factor. Tốn nhiều tài nguyên hơn (CPU, I/O, log space). Có thể thực hiện online (thường yêu cầu phiên bản Enterprise) hoặc offline (khóa bảng trong quá trình rebuild). Rebuild index cũng cập nhật số liệu thống kê liên quan đến index đó với độ chính xác cao (tương đương FULL SCAN).
Khi nào sử dụng: Phù hợp cho các index bị phân mảnh nặng (ví dụ: trên 30%).
Sự Cần Thiết Của Bảo Trì:
Việc bảo trì index định kỳ là rất quan trọng để đảm bảo hiệu năng cơ sở dữ liệu tối ưu theo thời gian, đặc biệt là đối với các cơ sở dữ liệu có tần suất sửa đổi dữ liệu cao. Nên lập lịch thực hiện các tác vụ bảo trì này trong thời gian hệ thống ít tải (off-peak hours). Cần lưu ý rằng các index rất nhỏ (nằm trong các mixed extents) có thể không giảm được phân mảnh đáng kể sau khi reorganize hoặc rebuild.
Indexes bị suy giảm hiệu quả theo thời gian do thay đổi dữ liệu, trở nên kém hiệu quả hơn. Bảo trì index (reorganize hoặc rebuild) giống như việc "dọn dẹp" cấu trúc index để khôi phục hiệu suất của nó, giải quyết vấn đề phân mảnh và không gian lãng phí. Các thay đổi dữ liệu gây ra việc tách trang (khi một trang đầy và cần chỗ cho hàng mới) và việc xóa để lại không gian trống (). Tách trang có thể dẫn đến thứ tự logic không khớp với thứ tự vật lý () và cả việc tách và xóa đều có thể dẫn đến các trang không đầy (mật độ thấp ). Các index bị phân mảnh và các trang có mật độ thấp yêu cầu đọc nhiều trang hơn từ đĩa/bộ nhớ để đáp ứng một truy vấn (), làm tăng chi phí I/O và giảm hiệu suất. Reorganize () sắp xếp lại vật lý các trang lá và nén chúng, trong khi Rebuild () tạo ra một index mới, có cấu trúc tối ưu. Sự lựa chọn phụ thuộc vào mức độ nghiêm trọng của phân mảnh và khả năng chịu đựng việc tiêu thụ tài nguyên/khóa trong cửa sổ bảo trì ().
Bảng So Sánh: Rebuild vs. Reorganize Index
Tính Năng | Rebuild (Xây Dựng Lại) | Reorganize (Tổ Chức Lại) |
Loại bỏ Phân mảnh | Loại bỏ hoàn toàn mọi phân mảnh | Chỉ loại bỏ phân mảnh ở cấp lá (leaf level) |
Mức độ Tài nguyên | Cao (CPU, I/O, Log Space) | Thấp |
Khóa/Online | Offline (mặc định), Online (thường cần Enterprise Ed.) | Luôn Online |
Cập nhật Statistics | Có (tương đương Full Scan) | Không |
Thu hồi Không gian | Cao (Nén trang theo fill factor) | Trung bình (Chỉ nén trang lá) |
Cấp độ Tác động | Toàn bộ cấu trúc index | Chỉ cấp lá (Leaf level) |
Khi nào dùng (Gợi ý) | Phân mảnh nặng (>30%) | Phân mảnh thấp/trung bình (ví dụ: 5-30% |
Phần 3: Tối Ưu Hóa Truy Vấn
Việc có các index phù hợp là một phần quan trọng, nhưng đó chỉ là một nửa của câu chuyện về hiệu năng. Cách bạn viết truy vấn và cách DBMS diễn giải và thực thi chúng cũng đóng vai trò cực kỳ quan trọng. Đây là lúc khái niệm "Tối ưu hóa truy vấn" (Query Optimization) xuất hiện.
Tối Ưu Hóa Truy Vấn là gì?
Định nghĩa: Tối ưu hóa truy vấn là quá trình mà hệ quản trị cơ sở dữ liệu (DBMS) tự động phân tích một câu lệnh SQL và xác định cách hiệu quả nhất để thực thi nó. Ngôn ngữ SQL mang tính "khai báo" (declarative), nghĩa là bạn chỉ định kết quả bạn muốn nhận được, chứ không phải các bước cụ thể để lấy kết quả đó. Nhiệm vụ của bộ tối ưu hóa truy vấn (Query Optimizer) là dịch yêu cầu logic của bạn thành một kế hoạch thực thi (execution plan) vật lý tối ưu.
Mục tiêu chính:
Tăng tốc độ thực thi: Giảm thời gian phản hồi của truy vấn, làm cho ứng dụng chạy nhanh hơn.
Giảm tiêu thụ tài nguyên: Sử dụng ít tài nguyên hệ thống hơn (CPU, bộ nhớ, I/O đĩa) trong quá trình thực thi truy vấn.
Vai trò của Bộ Tối Ưu Hóa (Query Optimizer):
Bên trong mỗi DBMS có một thành phần thông minh gọi là Query Optimizer. Khi nhận được một truy vấn SQL, Optimizer sẽ:
Phân tích cú pháp và ngữ nghĩa của truy vấn.
Xem xét các cấu trúc có sẵn như indexes.
Tham khảo các số liệu thống kê (statistics) về dữ liệu để ước tính chi phí.
Tạo ra nhiều kế hoạch thực thi khả thi (ví dụ: sử dụng index nào, thứ tự join bảng ra sao).
Ước tính chi phí (thời gian, tài nguyên) cho mỗi kế hoạch.
Chọn ra kế hoạch được ước tính là có chi phí thấp nhất (thường là nhanh nhất hoặc ít tốn tài nguyên nhất) để thực thi.
Tại sao Tối ưu hóa lại Quan trọng:
Các truy vấn hiệu quả dẫn đến ứng dụng nhanh hơn, trải nghiệm người dùng tốt hơn và có thể giảm chi phí vận hành cơ sở hạ tầng (do sử dụng ít tài nguyên hơn) (). Ngược lại, các truy vấn được tối ưu hóa kém có thể làm chậm toàn bộ hệ thống.
Tối ưu hóa truy vấn phần lớn là một quy trình tự động do cơ sở dữ liệu thực hiện, nhưng việc hiểu các mục tiêu và cách thức hoạt động của nó cho phép các nhà phát triển và quản trị viên cơ sở dữ liệu (DBA) viết các truy vấn và thiết kế lược đồ (bao gồm cả index) giúp bộ tối ưu hóa đưa ra các lựa chọn tốt hơn. Một truy vấn SQL duy nhất thường có thể được thực thi theo nhiều cách khác nhau (ví dụ: thứ tự nối khác nhau, sử dụng các index hoặc quét khác nhau). Công việc của bộ tối ưu hóa truy vấn () là khám phá những khả năng này và chọn kế hoạch mà nó dự đoán sẽ hiệu quả nhất dựa trên các ước tính chi phí (). Những ước tính này phụ thuộc rất nhiều vào siêu dữ liệu như index () và số liệu thống kê (). Do đó, việc cung cấp các index tốt và duy trì số liệu thống kê chính xác () là những cách chính mà con người có thể ảnh hưởng đến bộ tối ưu hóa để tạo ra các kế hoạch tốt hơn và đạt được các mục tiêu về tốc độ () và hiệu quả ().
Mục 3.2: Các Kỹ Thuật Viết Truy Vấn Thông Minh
Mặc dù bộ tối ưu hóa làm rất nhiều việc nặng nhọc, cách bạn cấu trúc và viết các câu lệnh SQL của mình có thể tạo ra sự khác biệt lớn về hiệu năng. Dưới đây là một số kỹ thuật quan trọng mà người mới bắt đầu nên biết.
Tiểu mục 3.2.1: Tận Dụng Indexes Của Bạn (Tham chiếu Mục 2.2) (Điểm truy vấn 7a)
Như đã thảo luận, indexes được tạo ra để tăng tốc các truy vấn lọc, kết nối hoặc sắp xếp dựa trên các cột cụ thể (). Tuy nhiên, việc tạo index chỉ là bước đầu tiên. Bạn cần viết truy vấn sao cho bộ tối ưu hóa có thể sử dụng các index đó một cách hiệu quả.
Điều này thường có nghĩa là đảm bảo các điều kiện trong mệnh đề WHERE
hoặc JOIN
của bạn tham chiếu đến các cột đã được đánh index.
Một khái niệm quan trọng liên quan là Covering Index (Index Bao Phủ). Đây là một index chứa tất cả các cột mà một truy vấn cụ thể cần đến, bao gồm cả các cột trong danh sách SELECT
và các cột trong mệnh đề WHERE
, JOIN
, ORDER BY
. Khi một index bao phủ hoàn toàn một truy vấn, DBMS có thể trả lời truy vấn đó chỉ bằng cách đọc dữ liệu từ index, mà không cần phải truy cập vào bảng chính (). Đây là một trong những tình huống tối ưu hóa hiệu quả nhất.
Ví dụ: Nếu bạn có index idx_user_lastname_firstname
trên (LastName, FirstName)
của bảng Users
, và bạn chạy truy vấn:
SQL
SELECT FirstName
FROM Users
WHERE LastName = 'Tran';
Index này bao phủ truy vấn vì cả cột FirstName
(trong SELECT
) và cột LastName
(trong WHERE
) đều có trong index. DBMS có thể tìm 'Tran' trong index và lấy giá trị FirstName
tương ứng trực tiếp từ đó.
Việc tạo một index chỉ là một nửa trận chiến; viết các truy vấn cho phép bộ tối ưu hóa sử dụng index đó một cách hiệu quả là nửa còn lại. Một index tồn tại, nhưng bộ tối ưu hóa () sẽ chỉ sử dụng nó nếu các vị từ (điều kiện) của truy vấn khớp với cấu trúc index theo cách cho phép tra cứu hiệu quả (xem SARGability tiếp theo). Một covering index () đặc biệt hiệu quả vì nó loại bỏ nhu cầu thực hiện bước thứ hai thường cần thiết với các non-clustered indexes: tra cứu hàng đầy đủ trong bảng cơ sở sau khi tìm thấy mục nhập trong index. Tất cả dữ liệu cần thiết đều nằm ngay trong các trang index, giảm thiểu I/O.
Tiểu mục 3.2.2: Hãy Cụ Thể: SELECT Columns
vs. SELECT *
(Điểm truy vấn 7b)
Một thói quen phổ biến, đặc biệt là với người mới bắt đầu, là sử dụng SELECT *
để lấy tất cả các cột từ một bảng. Mặc dù tiện lợi khi gõ, thói quen này thường gây hại cho hiệu năng và khả năng bảo trì ().
Tác động đến Hiệu năng:
Ngăn chặn Index Bao Phủ: Như đã đề cập ở trên,
SELECT *
yêu cầu tất cả các cột. Trừ khi bạn có một index chứa mọi cột trong bảng (điều này hiếm khi là ý tưởng tốt), việc sử dụngSELECT *
gần như chắc chắn sẽ ngăn cản DBMS sử dụng covering index (). Điều này có thể buộc DBMS phải thực hiện các thao tác quét kém hiệu quả hơn (như Clustered Index Scan hoặc Table Scan) thay vì Index Seek hiệu quả ().Truyền Dữ Liệu Không Cần Thiết:
SELECT *
lấy về tất cả các cột, ngay cả khi ứng dụng của bạn chỉ cần sử dụng một vài cột trong số đó. Việc này lãng phí băng thông mạng để truyền dữ liệu thừa, tốn bộ nhớ trên cả máy chủ cơ sở dữ liệu và máy khách để xử lý dữ liệu đó, và có thể làm tăng I/O đĩa nếu các cột không cần thiết không nằm trong bộ đệm ().
Các Vấn đề Khác:
Khả năng đọc:
SELECT *
làm cho mã nguồn SQL khó đọc hơn vì không rõ ràng những cột nào đang thực sự được sử dụng.Tính Mong Manh: Mã nguồn sử dụng
SELECT *
rất dễ bị lỗi khi lược đồ (schema) của bảng thay đổi. Nếu một cột mới được thêm vào, hoặc thứ tự cột thay đổi, ứng dụng có thể gặp lỗi nếu nó không được chuẩn bị để xử lý sự thay đổi đó ().
Khuyến nghị:
Luôn luôn chỉ định rõ ràng tên các cột bạn thực sự cần trong danh sách SELECT
(). Ngay cả khi bạn hiện tại cần tất cả các cột, việc liệt kê chúng một cách tường minh vẫn là thực hành tốt hơn vì nó làm cho mã rõ ràng hơn, dễ bảo trì hơn và ít bị ảnh hưởng bởi thay đổi lược đồ trong tương lai ().
Cần lưu ý rằng, nếu bạn thực sự cần tất cả các cột, sự khác biệt về hiệu năng thời gian truy xuất dữ liệu thô giữa SELECT *
và SELECT col1, col2,..., colN
có thể là không đáng kể, vì cùng một lượng dữ liệu cần được lấy từ bảng (). Tuy nhiên, tác động tiêu cực đến khả năng sử dụng covering index và khả năng bảo trì trong tương lai vẫn là những lý do mạnh mẽ để tránh SELECT *
.
Việc chỉ chọn các cột cần thiết là một nguyên tắc tối ưu hóa cơ bản giúp giảm sử dụng tài nguyên và tăng cơ hội để bộ tối ưu hóa sử dụng các chiến lược index hiệu quả hơn (như covering indexes). Yêu cầu ít cột hơn có nghĩa là cần đọc ít dữ liệu hơn từ đĩa/bộ nhớ, truyền qua mạng và xử lý bởi ứng dụng khách (). Quan trọng nhất, nó làm tăng khả năng một non-clustered index hiện có có thể bao phủ truy vấn (). Nếu truy vấn SELECT colA, colB FROM table WHERE colC = 5
được đưa ra, và có một index trên (colC, colA, colB)
, cơ sở dữ liệu có khả năng đáp ứng toàn bộ truy vấn chỉ bằng cách đọc index, tránh truy cập vào dữ liệu bảng chính. SELECT *
làm cho việc tối ưu hóa này không thể thực hiện được trừ khi index chứa mọi cột.
Tiểu mục 3.2.3: Lọc Thông Minh: Mệnh đề WHERE
và Tính SARGable (Điểm truy vấn 7c)
Mệnh đề WHERE
là công cụ chính để lọc dữ liệu, nhưng cách bạn viết các điều kiện lọc có thể ảnh hưởng lớn đến việc liệu index có được sử dụng hiệu quả hay không.
Lọc Sớm: Áp dụng các điều kiện lọc càng sớm càng tốt trong mệnh đề
WHERE
để giảm số lượng hàng cần được xử lý bởi các giai đoạn sau của truy vấn (như JOIN hoặc sắp xếp) ().Vị Từ SARGable (SARGable Predicates): Đây là một khái niệm quan trọng. SARGable là viết tắt của "Search ARGument ABLE", có nghĩa là vị từ (điều kiện) trong mệnh đề
WHERE
được viết theo cách cho phép công cụ cơ sở dữ liệu sử dụng index để tìm kiếm dữ liệu một cách hiệu quả ().Vị Từ Không SARGable (Non-SARGable Predicates): Ngược lại, một số cách viết điều kiện sẽ ngăn cản DBMS sử dụng index một cách tối ưu. Trường hợp phổ biến nhất là áp dụng một hàm (function) hoặc thực hiện một phép tính trực tiếp lên cột đã được đánh index trong mệnh đề
WHERE
(). Khi bạn làm điều này, DBMS thường không thể sử dụng index để "nhảy" thẳng đến các giá trị cần tìm, mà thay vào đó phải tính toán giá trị của hàm/biểu thức cho từng hàng rồi mới so sánh, dẫn đến việc quét index hoặc quét toàn bộ bảng.Ví dụ:
Không SARGable:
WHERE YEAR(OrderDate) = 2023
() - HàmYEAR()
áp dụng lên cộtOrderDate
.WHERE Price * 1.1 > 100
- Phép tính nhân áp dụng lên cộtPrice
.WHERE SUBSTRING(LastName, 1, 1) = 'N'
- HàmSUBSTRING()
áp dụng lên cộtLastName
.WHERE StatusID + 1 = 5
- Phép tính cộng áp dụng lên cộtStatusID
.WHERE Notes LIKE '%important%'
() - MẫuLIKE
bắt đầu bằng ký tự đại diện (%
).
SARGable (Tương đương):
WHERE OrderDate >= '2023-01-01' AND OrderDate < '2024-01-01'
() - So sánh trực tiếp cộtOrderDate
với các giá trị cố định.WHERE Price > 100 / 1.1
() - Phép tính được thực hiện trên giá trị hằng số, không phải trên cột.WHERE LastName LIKE 'N%'
() - MẫuLIKE
bắt đầu bằng một tiền tố cụ thể.WHERE StatusID = 4
- Phép tính được thực hiện trên giá trị hằng số.
Tác động: Các vị từ không SARGable thường buộc DBMS phải thực hiện Index Scan (quét toàn bộ index) hoặc Table Scan (quét toàn bộ bảng) thay vì Index Seek (tìm kiếm hiệu quả trong index) (), làm giảm đáng kể hiệu năng.
Cách bạn viết các điều kiện lọc của mình có ý nghĩa rất lớn đối với việc sử dụng index. Tránh thao tác trực tiếp trên cột được đánh index trong mệnh đề WHERE
; thay vào đó, hãy thao tác trên giá trị đang được so sánh. Indexes (như B-trees) lưu trữ và sắp xếp các giá trị cột thô (). Khi một vị từ là SARGable (), cơ sở dữ liệu có thể sử dụng trực tiếp cấu trúc index để tìm các giá trị thô khớp (ví dụ: tìm nơi OrderDate
nằm trong phạm vi '2023-01-01'
đến '2024-01-01'
). Khi một hàm được áp dụng cho cột (YEAR(OrderDate) = 2023
), cơ sở dữ liệu không có index trên kết quả của hàm. Nó sẽ phải tính toán YEAR(OrderDate)
cho mọi hàng và sau đó so sánh nó với 2023, thực sự bỏ qua index trên OrderDate
để tra cứu trực tiếp (). Viết lại truy vấn để so sánh giá trị cột thô với một phạm vi được tính toán (OrderDate >= '2023-01-01' AND OrderDate < '2024-01-01'
) cho phép sử dụng index một cách hiệu quả.
Bảng So Sánh: Ví dụ về SARGable vs. Non-SARGable
Vị Từ Không SARGable | Lý Do | Vị Từ SARGable Thay Thế |
YEAR(OrderDate) = 2023 | Hàm áp dụng lên cột được đánh index | OrderDate >= '2023-01-01' AND OrderDate < '2024-01-01' |
Price * 1.1 > 100 | Phép tính trên cột được đánh index | Price > 100 / 1.1 |
LastName LIKE '%Smith' | Ký tự đại diện đứng đầu trong LIKE | LastName LIKE 'Smith%' |
ISNULL(Col, fallback) = value | Hàm áp dụng lên cột được đánh index | (Col = value) OR (Col IS NULL AND value IS NULL) (hoặc dùng INTERSECT ) |
Col + 10 = 100 | Phép tính trên cột được đánh index | Col = 90 |
Col <> value | So sánh phủ định (thường kém hiệu quả) | (Có thể vẫn quét, tùy dữ liệu/DBMS) |
Col NOT IN (val1, val2) | So sánh phủ định (thường kém hiệu quả) | (Có thể vẫn quét, tùy dữ liệu/DBMS) |
Tiểu mục 3.2.4: Chiến Lược JOIN
Hiệu Quả (Điểm truy vấn 7d)
Kết nối dữ liệu từ nhiều bảng bằng JOIN
là một hoạt động rất phổ biến trong SQL. Tuy nhiên, các thao tác JOIN có thể trở nên rất tốn kém nếu không được thực hiện một cách hiệu quả ().
Đánh Index Cột JOIN: Điều quan trọng nhất để tăng tốc JOIN là đảm bảo rằng các cột được sử dụng trong mệnh đề
ON
(thường là cột khóa chính ở một bảng và khóa ngoại ở bảng kia) được đánh index (). Index giúp DBMS nhanh chóng tìm thấy các hàng khớp giữa các bảng.Chọn Loại JOIN Phù Hợp: Sử dụng loại JOIN (ví dụ:
INNER JOIN
,LEFT JOIN
,RIGHT JOIN
) phù hợp với yêu cầu logic của bạn.INNER JOIN
thường hiệu quả hơn vì nó chỉ trả về các hàng có sự khớp nối ở cả hai bảng, loại bỏ các hàng không khớp sớm (). Chỉ sử dụngLEFT
hoặcRIGHT JOIN
khi bạn thực sự cần giữ lại tất cả các hàng từ một bảng, ngay cả khi không có hàng khớp ở bảng kia.Thứ Tự JOIN: Mặc dù bộ tối ưu hóa truy vấn thường cố gắng tự động xác định thứ tự JOIN tốt nhất, nguyên tắc chung là nó thường cố gắng bắt đầu với các bảng nhỏ hơn (sau khi áp dụng bộ lọc
WHERE
) để giảm khối lượng dữ liệu cần xử lý trong các bước JOIN tiếp theo (). Trong các truy vấn phức tạp, đôi khi việc sắp xếp lại thứ tự các bảng trong mệnh đềFROM
vàJOIN
có thể ảnh hưởng đến kế hoạch thực thi.Lọc Dữ Liệu Trước Khi JOIN: Nếu có thể, hãy áp dụng các điều kiện lọc trong mệnh đề
WHERE
cho từng bảng riêng lẻ trước khi thực hiện JOIN. Điều này giúp giảm số lượng hàng mà thao tác JOIN phải xử lý. Ví dụ, thay vìSELECT... FROM A JOIN B ON
A.id
=
B.id
WHERE A.type = 'X' AND
B.date
> '2023-01-01'
, việc lọcA.type
vàB.date
sớm sẽ hiệu quả hơn là JOIN toàn bộ A và B rồi mới lọc.
Việc JOIN hiệu quả phụ thuộc rất nhiều vào việc có các index phù hợp trên các cột nối và lọc dữ liệu hiệu quả trước hoặc trong quá trình JOIN. Việc nối liên quan đến việc so sánh các giá trị từ các cột trong các bảng khác nhau để tìm các kết quả khớp. Nếu không có index trên các cột nối (), cơ sở dữ liệu có thể phải dùng đến các thuật toán kém hiệu quả như nested loop join (lặp qua mọi hàng của bảng này cho mỗi hàng của bảng kia) hoặc hash join (yêu cầu xây dựng bảng băm trong bộ nhớ). Indexes cho phép cơ sở dữ liệu tra cứu nhanh các hàng khớp trong bảng được nối (), tăng tốc đáng kể quá trình này. Chọn đúng loại JOIN () đảm bảo chỉ các hàng cần thiết được xử lý. Lọc sớm () làm giảm kích thước của các tập dữ liệu được nối, giảm thiểu hơn nữa công việc cần thiết.
Tiểu mục 3.2.5: Vượt Lên Trên Subquery: Sử Dụng JOIN
s và CTEs (Điểm truy vấn 7e)
Subquery (truy vấn con) là một truy vấn được lồng bên trong một truy vấn SQL khác. Mặc dù hữu ích trong nhiều trường hợp, một số loại subquery nhất định đôi khi có thể kém hiệu quả hơn các phương pháp thay thế (). Đặc biệt là các subquery tương quan (correlated subqueries - subquery tham chiếu đến cột của truy vấn bên ngoài) hoặc subquery trong danh sách SELECT
có thể bị thực thi lặp đi lặp lại cho mỗi hàng của truy vấn bên ngoài.
Thay Thế Bằng
JOIN
: Nhiều tình huống sử dụng subquery để lọc hoặc lấy dữ liệu liên quan có thể được viết lại hiệu quả hơn bằng cách sử dụng các thao tácJOIN
. Các bộ tối ưu hóa thường được tinh chỉnh rất tốt để xử lý các JOIN ().Giới thiệu về CTEs (Common Table Expressions):
Định nghĩa: CTE là một tập kết quả tạm thời, được đặt tên, mà bạn có thể định nghĩa ở đầu một câu lệnh SQL (sử dụng từ khóa
WITH
) và sau đó tham chiếu đến nó như một bảng thông thường trong phần còn lại của truy vấn ().Cải thiện Khả năng đọc: Ưu điểm lớn nhất của CTE là chúng giúp chia nhỏ các truy vấn phức tạp thành các bước logic, được đặt tên rõ ràng. Điều này làm cho mã SQL trở nên có tổ chức, dễ đọc và dễ bảo trì hơn nhiều so với việc lồng nhiều subquery vào nhau ().
Khả năng Tái sử dụng: Một CTE có thể được tham chiếu đến nhiều lần trong cùng một câu lệnh SQL (). Nếu bạn cần sử dụng cùng một tập kết quả trung gian ở nhiều nơi, CTE giúp tránh lặp lại mã.
Hiệu năng (so với Subquery): Trong nhiều hệ quản trị cơ sở dữ liệu hiện đại (như PostgreSQL và thường là SQL Server ), CTE có thể mang lại lợi ích về hiệu năng so với các subquery phức tạp tương đương. Lý do là:
Materialization (Trong một số hệ thống như PostgreSQL): Nếu một CTE được tham chiếu nhiều lần, DBMS có thể chỉ thực thi định nghĩa CTE một lần và lưu kết quả tạm thời (materialize) vào bộ nhớ. Các tham chiếu sau đó sẽ sử dụng kết quả đã lưu này, tránh việc tính toán lại (). Subquery có thể bị đánh giá lại mỗi lần nó xuất hiện.
Tối ưu hóa tốt hơn: Cấu trúc rõ ràng của CTE đôi khi có thể giúp bộ tối ưu hóa hiểu rõ hơn luồng xử lý và tạo ra kế hoạch thực thi tốt hơn.
Lưu ý: Lợi ích về hiệu năng không phải lúc nào cũng được đảm bảo. Đối với các subquery đơn giản, sự khác biệt có thể không đáng kể. Trong một số trường hợp rất phức tạp hoặc khi kết quả trung gian rất lớn và được sử dụng nhiều lần, việc sử dụng bảng tạm (temporary tables) có thể còn hiệu quả hơn CTE (). CTE cũng hỗ trợ truy vấn đệ quy (recursive queries), điều mà subquery không làm được ().
- Hạn chế của CTE: CTE không thể được sử dụng trực tiếp trong một số ngữ cảnh mà subquery có thể, ví dụ như trong mệnh đề
WHERE
vớiIN
hoặcEXISTS
().
Ví dụ:
Sử dụng Subquery: SQL
SELECT e.Name, e.Salary FROM Employees e WHERE e.DepartmentID IN (SELECT DepartmentID FROM Departments WHERE Location = 'Hanoi');
Sử dụng CTE: SQL
WITH HanoiDepartments AS ( SELECT DepartmentID FROM Departments WHERE Location = 'Hanoi' ) SELECT e.Name, e.Salary FROM Employees e JOIN HanoiDepartments hd ON e.DepartmentID = hd.DepartmentID;
(Lưu ý: Ví dụ này cũng có thể viết lại bằng JOIN trực tiếp mà không cần subquery hay CTE, nhưng nó minh họa cú pháp).
Mặc dù subquery có vị trí của chúng, JOINs và CTEs thường cung cấp các cách dễ đọc hơn và có khả năng hiệu quả hơn để cấu trúc các truy vấn phức tạp, đặc biệt là khi chia nhỏ logic thành các bước hoặc tái sử dụng kết quả trung gian. JOINs là các hoạt động quan hệ cơ bản mà các bộ tối ưu hóa được điều chỉnh rất nhiều (). Thay thế một subquery bằng JOIN thường cho phép bộ tối ưu hóa xem xét các thuật toán nối và đường dẫn truy cập hiệu quả hơn. CTEs () cải thiện cấu trúc mã, giúp con người và có khả năng cả bộ tối ưu hóa dễ dàng hiểu logic của truy vấn hơn. Khía cạnh materialization trong các hệ thống như PostgreSQL () có nghĩa là nếu một phép tính phức tạp trong CTE cần thiết nhiều lần, nó chỉ được tính toán một lần, tránh công việc dư thừa, không giống như một subquery có thể được đánh giá lại tùy thuộc vào loại và vị trí của nó. Tuy nhiên, việc so sánh hiệu suất không phải lúc nào cũng rõ ràng và có thể phụ thuộc vào DBMS cụ thể và độ phức tạp của truy vấn (). Một số người cho rằng bảng tạm cung cấp hiệu suất và khả năng gỡ lỗi tốt hơn cho các quy trình rất phức tạp, nhiều bước ().
Bảng So Sánh: Subquery vs. CTE
Tính Năng | Subquery (Truy vấn con) | CTE (Common Table Expression) |
Vị trí Định nghĩa | Nội tuyến (SELECT , FROM , WHERE ) | Ở đầu truy vấn (WITH clause) |
Đặt Tên | Tùy chọn (Trừ PostgreSQL yêu cầu bí danh) | Bắt buộc |
Khả năng đọc (Truy vấn đơn giản) | Thường chấp nhận được | Có thể hơi dài dòng hơn |
Khả năng đọc (Truy vấn phức tạp) | Có thể khó theo dõi (lồng nhau) | Thường cao hơn nhiều (module, các bước có tên) |
Tái sử dụng trong Truy vấn | Thường dùng một lần tại nơi định nghĩa | Có thể tham chiếu nhiều lần |
Hỗ trợ Đệ quy | Không | Có |
Sử dụng trong WHERE IN/EXISTS | Có | Không |
Hiệu năng Tiềm năng | Có thể kém hiệu quả (đặc biệt là correlated) | Thường tốt hơn cho phức tạp/đa dụng (đặc biệt PostgreSQL do materialization ), nhưng tùy DBMS/truy vấn |
Mục 3.3: Số Liệu Thống Kê (Statistics): Tai Mắt Của Bộ Tối Ưu Hóa
Một yếu tố cực kỳ quan trọng nhưng thường bị bỏ qua trong tối ưu hóa truy vấn là số liệu thống kê (database statistics). Đây là những thông tin mà bộ tối ưu hóa truy vấn sử dụng để đưa ra các quyết định thông minh về cách thực thi một truy vấn.
Statistics là gì?
Statistics là các đối tượng siêu dữ liệu (metadata) được lưu trữ bởi DBMS, mô tả sự phân bố của các giá trị dữ liệu bên trong các cột và indexes của bảng (). Chúng không chứa dữ liệu thực tế của bảng, mà là bản tóm tắt về dữ liệu đó.
Tại sao Statistics lại Cần thiết?
Bộ tối ưu hóa cần phải ước tính xem có bao nhiêu hàng sẽ được trả về bởi các phần khác nhau của một truy vấn (quá trình này gọi là ước tính số lượng - cardinality estimation) để có thể chọn ra kế hoạch thực thi có chi phí thấp nhất (). Ví dụ, nó cần biết liệu điều kiện WHERE City = 'Hanoi'
sẽ trả về 10 hàng hay 1 triệu hàng. Thông tin này ảnh hưởng lớn đến quyết định như có nên sử dụng index hay không, sử dụng loại JOIN nào, thứ tự JOIN các bảng ra sao, v.v. (). Statistics cung cấp dữ liệu thô cho các ước tính này.
Các Thành phần Chính của Statistics:
Histogram: Đây là thành phần quan trọng nhất. Histogram chia phạm vi giá trị của cột (thường là cột đầu tiên trong đối tượng statistics) thành nhiều "bậc" hoặc "khoảng" (thường lên đến 200 bậc). Đối với mỗi bậc, nó lưu trữ thông tin như giá trị biên trên của bậc, số lượng hàng có giá trị bằng đúng biên trên, số lượng hàng có giá trị nằm trong khoảng của bậc (không bao gồm biên trên), và số lượng giá trị duy nhất trong khoảng đó (). Bộ tối ưu hóa sử dụng histogram để ước tính số lượng hàng khớp với các điều kiện trong
WHERE
(ví dụ:=
,>
,<
,BETWEEN
).Density Vector (Vector Mật độ): Cung cấp thông tin về mức độ duy nhất (hoặc trùng lặp) của các giá trị trong một cột hoặc tổ hợp các cột. Mật độ được tính bằng
1 / (số lượng giá trị duy nhất)
(). Mật độ thấp có nghĩa là tính chọn lọc cao (ít hàng trùng lặp). Bộ tối ưu hóa sử dụng density để tinh chỉnh các ước tính, đặc biệt là cho các điều kiện trên nhiều cột hoặc khi ước tính kết quả của JOIN ().Thông tin khác: Statistics cũng có thể chứa các thông tin khác như số lượng giá trị NULL, độ dài trung bình của khóa, v.v. ().
Cách Bộ Tối Ưu Hóa Sử Dụng Statistics:
Khi gặp một điều kiện lọc như WHERE ProductID = 123
, bộ tối ưu hóa sẽ tra cứu statistics (cụ thể là histogram) của cột ProductID
. Dựa trên thông tin trong histogram, nó sẽ ước tính có bao nhiêu hàng trong bảng có ProductID
là 123 (). Ước tính này (và các ước tính tương tự cho các phần khác của truy vấn) sẽ được sử dụng để tính toán chi phí của các kế hoạch thực thi khác nhau và chọn ra kế hoạch tốt nhất.
Giữ Statistics Luôn Cập Nhật:
Vấn đề: Khi dữ liệu trong bảng thay đổi (thêm, sửa, xóa), statistics hiện có sẽ trở nên lỗi thời (stale) vì chúng không còn phản ánh chính xác sự phân bố dữ liệu hiện tại ().
Hậu quả: Statistics lỗi thời dẫn đến các ước tính số lượng sai lệch, khiến bộ tối ưu hóa có thể chọn một kế hoạch thực thi rất kém hiệu quả, gây ra tình trạng truy vấn chạy chậm đột ngột (). Đây là một trong những nguyên nhân phổ biến gây ra các vấn đề về hiệu năng.
Cập nhật Tự động: Hầu hết các DBMS đều có cơ chế tự động cập nhật statistics khi có một lượng thay đổi dữ liệu đáng kể xảy ra (ví dụ: tùy chọn
AUTO_UPDATE_STATISTICS
trong SQL Server) ().Cập nhật Thủ công: Tuy nhiên, đôi khi việc cập nhật tự động không đủ nhanh hoặc không xảy ra đúng lúc (ví dụ: sau khi tải một lượng lớn dữ liệu). Trong những trường hợp đó, việc cập nhật statistics thủ công bằng các lệnh như
UPDATE STATISTICS
(SQL Server ) hoặcANALYZE
(PostgreSQL, MySQL ) là cần thiết (). Việc cập nhật có thể dựa trên việc quét toàn bộ bảng (FULL SCAN) để có độ chính xác cao nhất, hoặc chỉ quét một tỷ lệ mẫu (sampling) để nhanh hơn nhưng có thể kém chính xác hơn ().
Mối Quan Hệ Với Indexes:
Khi bạn tạo một index, DBMS thường tự động tạo statistics cho các cột trong index đó (). DBMS cũng có thể tự động tạo statistics cho các cột đơn lẻ không có index nếu chúng được sử dụng trong các điều kiện lọc và tùy chọn tạo statistics tự động được bật (AUTO_CREATE_STATISTICS
trong SQL Server ).
Số liệu thống kê chính xác là nền tảng để bộ tối ưu hóa truy vấn đưa ra các quyết định thông minh. Bộ tối ưu hóa gần như hoàn toàn dựa vào số liệu thống kê () để dự đoán chi phí của các tùy chọn kế hoạch thực thi khác nhau. Nếu số liệu thống kê nói rằng một điều kiện lọc có tính chọn lọc cao (trả về ít hàng) trong khi thực tế nó trả về nhiều hàng (vì dữ liệu đã thay đổi và số liệu thống kê cũ), bộ tối ưu hóa có thể chọn một index seek theo sau là nhiều lần tra cứu khóa tốn kém, trong khi quét bảng có thể đã rẻ hơn. Ngược lại, nếu số liệu thống kê gợi ý tính chọn lọc thấp (nhiều hàng) trong khi thực tế nó cao, nó có thể chọn quét trong khi seek sẽ nhanh hơn nhiều (). Việc giữ cho số liệu thống kê tương đối cập nhật () đảm bảo bộ tối ưu hóa có cái nhìn thực tế về phân phối dữ liệu, cho phép nó tạo ra các kế hoạch hiệu quả hơn. Số liệu thống kê lỗi thời là một nguyên nhân phổ biến gây suy giảm hiệu suất truy vấn đột ngột, nghiêm trọng.
Kết Luận: Những Điểm Chính và Bước Tiếp Theo
Hiểu về SQL Indexes và Query Optimization là một bước quan trọng để xây dựng các ứng dụng cơ sở dữ liệu hiệu quả và phản hồi nhanh. Báo cáo này đã cung cấp một cái nhìn tổng quan, dễ tiếp cận cho người mới bắt đầu về các khái niệm cốt lõi:
Indexes là chìa khóa tốc độ: Chúng hoạt động giống như mục lục sách, sử dụng các cấu trúc như B-tree để cho phép DBMS nhanh chóng định vị dữ liệu mà không cần quét toàn bộ bảng, đặc biệt hữu ích cho các cột trong
WHERE
,JOIN
, vàORDER BY
.Sự đánh đổi của Index: Lợi ích về tốc độ đọc phải được cân bằng với chi phí về tốc độ ghi (do cần cập nhật index) và không gian lưu trữ. Việc đánh index quá nhiều thường gây hại.
Bảo trì Index là cần thiết: Phân mảnh xảy ra theo thời gian do sửa đổi dữ liệu, làm giảm hiệu quả của index. Các hoạt động
REORGANIZE
(cho phân mảnh nhẹ) vàREBUILD
(cho phân mảnh nặng) giúp khôi phục hiệu năng.Tối ưu hóa truy vấn là quá trình tìm đường đi hiệu quả nhất: DBMS có bộ tối ưu hóa để chọn kế hoạch thực thi tốt nhất, nhưng cách viết truy vấn của bạn có ảnh hưởng lớn.
Kỹ thuật viết truy vấn thông minh:
Luôn
SELECT
các cột cụ thể thay vìSELECT *
.Viết các điều kiện
WHERE
SARGable (tránh dùng hàm/tính toán trên cột được đánh index).Đảm bảo các cột
JOIN
được đánh index.Cân nhắc sử dụng
JOIN
hoặc CTE thay cho các subquery phức tạp để cải thiện khả năng đọc và có thể cả hiệu năng.
Statistics là "bộ não" của Optimizer: Số liệu thống kê (đặc biệt là histogram và density) cung cấp thông tin về phân bố dữ liệu, giúp bộ tối ưu hóa ước tính số lượng hàng và chọn kế hoạch tốt nhất. Giữ statistics cập nhật là rất quan trọng.
Đây chỉ là những kiến thức nền tảng. Thế giới tối ưu hóa cơ sở dữ liệu còn rất nhiều khía cạnh sâu sắc hơn, bao gồm các loại index chuyên biệt, phân tích kế hoạch thực thi chi tiết bằng các công cụ như EXPLAIN
hoặc Execution Plan
(), các kỹ thuật tối ưu hóa nâng cao, và các tính năng đặc thù của từng hệ quản trị cơ sở dữ liệu.
Cách tốt nhất để củng cố kiến thức này là thực hành. Hãy thử nghiệm tạo indexes trên cơ sở dữ liệu thử nghiệm của bạn, quan sát sự thay đổi về thời gian thực thi truy vấn (nếu có thể đo lường), và tập viết lại các truy vấn theo các kỹ thuật đã học.