Cơ sở dữ liệuSQL Server, phần 13/24
Chỉ mục lọc, cái giá khi ghi và chỉ mục thừa
Chỉ mục lọc và chỉ mục duy nhất, mỗi chỉ mục tốn gì khi ghi, cách tìm chỉ mục thiếu và chỉ mục không ai đọc rồi gỡ an toàn, kèm bẫy tham số hóa của EF Core với chỉ mục lọc.
Mỗi chỉ mục tăng tốc vài câu đọc nhưng bắt mọi đơn mới ghi thêm một lần và thêm bản ghi log. Chỉ mục lọc thu chi phí đó về phần dữ liệu nóng, nhưng chỉ có tác dụng khi câu SQL viết giá trị lọc thành hằng số, điều EF Core lặng lẽ làm hỏng khi giá trị nằm trong biến. Đọc xong, bạn khai báo được chỉ mục lọc trong EF Core, đếm được giá ghi của từng chỉ mục, và gỡ chỉ mục thừa mà không làm chậm câu nào.
Đọc nhanh
IX_DonHang_DangMochỉ giữ đơn mới, khoảng 1% bảng, nên nhỏ và có thống kê riêng sát hơn.- Chỉ mục lọc chỉ được dùng khi giá trị lọc viết thẳng trong câu, nên
TrangThai = @TrangThaikhông khớp. - Mỗi đơn mới ghi vào mọi chỉ mục chứa nó, và mỗi lần ghi có bản ghi log riêng.
- Bộ đếm sử dụng chỉ mục và gợi ý chỉ mục thiếu mất khi khởi động lại, nên disable trước, theo dõi Query Store, rồi mới xóa.
1. Chỉ mục lọc và chỉ mục duy nhất
IX_DonHang_DangMo
Kỹ thuật thường dùng đã tạo IX_DonHang_DangMo (KhachHangId, NgayTao) INCLUDE (TongTien) WHERE TrangThai = 1 trên dbo.DonHang 10 triệu dòng, SQL Server 2019. Đơn mới chiếm 1% bảng, khoảng 100.000 dòng.
Dòng lá gồm 18 byte khóa và row locator, 9 byte TongTien, 3 byte null bitmap và 1 byte header: 31 byte. Một page chứa 8.096 / 33 = 245 dòng, nên chỉ mục cần khoảng 409 page lá, khoảng 3,2 MB. Đó là 1,4% tầng lá của IX_DonHang_KhachHang. Chỉ mục lọc còn có thống kê lọc riêng trên đúng 100.000 dòng đó, nên ước lượng cho đơn đang mở sát hơn thống kê cả bảng.
Trình tối ưu chỉ chọn chỉ mục lọc khi chứng minh được điều kiện của câu nằm gọn trong điều kiện lọc. Kế hoạch được lưu để dùng lại phải đúng với mọi giá trị tham số. Nếu TrangThai là tham số, kế hoạch phải đúng cả khi @TrangThai = 4, nên chỉ mục chỉ chứa TrangThai = 1 bị loại.
-- TrangThai là tham số: IX_DonHang_DangMo không được dùng
EXEC sys.sp_executesql
N'SELECT KhachHangId, NgayTao, TongTien FROM dbo.DonHang
WHERE KhachHangId = @KhachHangId AND TrangThai = @TrangThai;',
N'@KhachHangId int, @TrangThai tinyint',
@KhachHangId = 42, @TrangThai = 1;
-- Giá trị lọc viết thẳng trong câu: khớp điều kiện của chỉ mục
EXEC sys.sp_executesql
N'SELECT KhachHangId, NgayTao, TongTien FROM dbo.DonHang
WHERE KhachHangId = @KhachHangId AND TrangThai = 1;',
N'@KhachHangId int',
@KhachHangId = 42;
XML kế hoạch của câu thứ nhất thường mang cảnh báo UnmatchedIndexes: có chỉ mục lọc lẽ ra dùng được nhưng không khớp vì tham số hóa.
Chỉ mục lọc và tham số
Màn hình "đơn đang mở" phải gửi TrangThai = 1 dưới dạng hằng số trong câu SQL. Nếu buộc phải dùng tham số, OPTION (RECOMPILE) cho trình tối ưu nhìn giá trị thật, đổi lại mỗi lần chạy là một lần biên dịch. Database đặt PARAMETERIZATION FORCED biến cả hằng số thành tham số và có thể làm chỉ mục lọc mất tác dụng.
Giới hạn khác của chỉ mục lọc, theo tài liệu Microsoft:
- Điều kiện lọc chỉ gồm phép so sánh đơn giản trên cột của một bảng. Không có
LIKE, không tham chiếu cột tính toán. - Điều kiện lọc gây đổi kiểu ở vế trái phép so sánh thì lệnh tạo báo lỗi. Đặt
CASThoặcCONVERTở vế phải. - Không đặt điều kiện lọc lên ràng buộc
PRIMARY KEYhayUNIQUE, nhưng đặt được lên chỉ mục có thuộc tínhUNIQUE. - Chỉ mục lọc chứa phần lớn bảng tốn công bảo trì hơn chỉ mục đầy đủ. Lợi chỉ rõ khi tập lọc nhỏ, như 1% ở đây.
Chỉ mục duy nhất
Chỉ mục unique cho trình tối ưu biết một giá trị khóa khớp tối đa một dòng: ước lượng chắc hơn, và seek dừng ngay khi tìm thấy. Với nonclustered unique, row locator chỉ nằm ở tầng lá, nên dòng tầng trên ngắn hơn bản không unique.
SQL Server coi các giá trị NULL là bằng nhau trong chỉ mục unique: chỉ một dòng được mang NULL. Khách chưa có số điện thoại để NULL, nên ràng buộc "một số điện thoại thuộc một khách" cần chỉ mục unique có lọc WHERE SoDienThoai IS NOT NULL. Khóa chính: IDENTITY, SEQUENCE, GUID và NULL đã tạo đúng chỉ mục đó, UX_KhachHang_SoDienThoai. Câu WHERE SoDienThoai = '0901234567' khớp điều kiện lọc vì một giá trị cụ thể không thể là NULL, và seek dừng sau đúng một dòng.
Chỉ mục unique phân vùng phải chứa cột phân vùng trong khóa. Khóa chính (NgayTao, DonHangId) thỏa điều kiện đó, nhưng chỉ bảo đảm cặp hai cột không trùng. Muốn riêng DonHangId duy nhất trên toàn bảng thì phải dùng chỉ mục unique không aligned, và chỉ mục đó chặn SWITCH. Cách còn lại là dựa vào nơi sinh mã, chẳng hạn một sequence.
2. Cái giá của chỉ mục khi ghi
Mỗi chỉ mục là một bản sao có thứ tự riêng của một phần bảng. Mỗi dòng ghi vào bảng phải ghi vào mọi bản sao chứa nó, mỗi lần ghi một bản ghi log riêng.
flowchart LR I["INSERT một đơn mới, TrangThai = 1"] --> P["PK_DonHang: page lá cuối của partition 3"] I --> K["IX_DonHang_KhachHang: giữa cây, cuối dải của khách"] I --> D["IX_DonHang_DangMo: đơn mới có mặt ở đây"] I --> C["NCCI_DonHang: delta store đang mở"]
| Cấu trúc | Dòng mới ghi vào đâu | Ghi chú |
|---|---|---|
PK_DonHang |
Page lá cuối của partition 3, vì NgayTao tăng dần |
Hết chỗ thì cấp page mới ở mép phải |
IX_DonHang_KhachHang |
Cuối dải của khách đó, một trong khoảng 10.683 page lá của partition 3 | Chèn vào giữa cây: page đầy thì tách đôi |
IX_DonHang_DangMo |
Đơn mới có TrangThai = 1 nên có mặt ở đây |
Rời chỉ mục khi TrangThai đổi sang 2 |
NCCI_DonHang |
Delta store đang mở | Nén thành rowgroup cột khi đủ dòng |
Bốn cấu trúc cho một đơn: với 13.100 đơn mỗi ngày là khoảng 52.400 lần chèn dòng chỉ mục. Mỗi lần chèn cần page đích trong buffer pool. Page cuối của PK_DonHang gần như luôn nóng, còn page giữa IX_DonHang_KhachHang của một khách ít mua có thể phải đọc từ đĩa trước khi ghi.
UPDATE chỉ chạm chỉ mục chứa cột bị sửa. Đổi TrangThai từ 1 sang 2 sửa dòng trên PK_DonHang và xóa dòng khỏi IX_DonHang_DangMo. Với NCCI_DonHang, dòng còn ở delta store thì sửa tại chỗ, dòng đã nằm trong rowgroup nén thì bị đánh dấu xóa và bản mới vào delta store. IX_DonHang_KhachHang theo định nghĩa gốc không bị chạm; nếu đã INCLUDE (TrangThai, TongTien) như bài trước, nó cũng phải sửa.
Đếm được giá đó bằng hai DMV. sys.dm_tran_database_transactions cho số bản ghi log của một lần chèn trong giao dịch được hủy ngay sau đó. Chạy lại với TrangThai = 4 thì dòng không vào IX_DonHang_DangMo, và số bản ghi log giảm theo. Lần chèn nào gây tách page giữa cây thì số bản ghi và số byte log tăng hẳn, vì nửa page dòng chuyển sang page mới đều được ghi log (Page, dòng và extent).
Đếm bản ghi log của một lần chèn, rồi hủy
BEGIN TRAN;
INSERT dbo.DonHang (DonHangId, NgayTao, KhachHangId, TrangThai, TongTien)
VALUES (99000001, CAST('2026-10-02T12:30:00' AS datetime2(0)), 77, 1, 250000.00);
SELECT dt.database_transaction_log_record_count, dt.database_transaction_log_bytes_used
FROM sys.dm_tran_database_transactions AS dt
JOIN sys.dm_tran_current_transaction AS ct
ON dt.transaction_id = ct.transaction_id
WHERE dt.database_id = DB_ID();
ROLLBACK;
sys.dm_db_index_operational_stats đếm tách page tích lũy theo chỉ mục. Với chỉ mục B-tree, mỗi lần cấp page ở tầng lá ứng với một lần tách page, kể cả lần "tách" ở mép phải của PK_DonHang vốn chỉ thêm page trống. So tỷ lệ cấp page trên số lần chèn giữa các chỉ mục để thấy chỉ mục nào tách dày. Bộ đếm về 0 khi khởi động lại, và có thể sớm hơn với bảng ít dùng. Hạ fillfactor chỉ nên làm khi số đo này cho thấy tách dày, như chương kỹ thuật đã nói.
Đếm số lần chèn và cấp page ở tầng lá, theo chỉ mục
SELECT
i.name AS index_name,
SUM(os.leaf_insert_count) AS leaf_inserts,
SUM(os.leaf_allocation_count) AS leaf_page_allocations
FROM sys.dm_db_index_operational_stats(DB_ID(), OBJECT_ID(N'dbo.DonHang'), NULL, NULL) AS os
JOIN sys.indexes AS i
ON i.object_id = os.object_id AND i.index_id = os.index_id
GROUP BY i.name
ORDER BY leaf_page_allocations DESC;
3. Tìm chỉ mục thiếu và chỉ mục thừa
Gợi ý chỉ mục thiếu
Khi biên dịch, trình tối ưu ghi lại chỉ mục mà nó cho là tốt nhất cho câu đó nếu chưa có. Ba DMV sys.dm_db_missing_index_details, _groups và _group_stats gom các gợi ý; câu thu gọn dưới xếp chúng theo chi phí nhân tác động nhân số lần dùng.
20 gợi ý chỉ mục thiếu đáng xem nhất của database hiện tại
SELECT TOP (20)
mid.statement AS table_name,
mid.equality_columns, mid.inequality_columns, mid.included_columns,
migs.user_seeks, migs.user_scans, migs.avg_total_user_cost,
migs.avg_user_impact, migs.last_user_seek
FROM sys.dm_db_missing_index_details AS mid
JOIN sys.dm_db_missing_index_groups AS mig
ON mig.index_handle = mid.index_handle
JOIN sys.dm_db_missing_index_group_stats AS migs
ON migs.group_handle = mig.index_group_handle
WHERE mid.database_id = DB_ID()
ORDER BY migs.avg_total_user_cost * migs.avg_user_impact
* (migs.user_seeks + migs.user_scans) DESC;
Một dòng minh họa sau vài ngày chạy màn hình lịch sử đơn:
| equality_columns | inequality_columns | included_columns | user_seeks | avg_user_impact |
|---|---|---|---|---|
[KhachHangId] |
NULL |
[NgayTao], [TrangThai], [TongTien] |
18.420 | 97,8 |
Gợi ý này gần trùng IX_DonHang_KhachHang. Tạo nguyên văn là thêm một chỉ mục thứ hai cùng dẫn đầu bằng KhachHangId, và mỗi đơn mới phải ghi vào cả hai. Cách đúng là mở rộng chỉ mục đang có bằng INCLUDE.
Giới hạn của tính năng này, theo tài liệu Microsoft:
- Chỉ chia cột thành nhóm so sánh bằng và nhóm so sánh khoảng, không cho thứ tự cột khóa.
- Không cân nhắc kích thước khi gợi ý nhiều cột
INCLUDE. Không gợi ý chỉ mục lọc hay chỉ mục unique. - Không gộp với chỉ mục đang có; các câu khác nhau sinh nhiều gợi ý gần giống nhau.
- Dựa trên ước lượng lúc biên dịch, không kiểm lại sau khi chạy. Không ghi cho kế hoạch trivial. Gom tối đa 600 nhóm.
- Mất khi khởi động lại, failover hoặc đưa database offline. Mất riêng cho một bảng khi metadata của bảng đổi: thêm cột, tạo chỉ mục, hoặc
ALTER INDEX.
Chỉ mục không ai đọc
sys.dm_db_index_usage_stats đếm số lần thực thi câu có dùng từng chỉ mục, không đếm số dòng. user_lookups chỉ có ở clustered index: số lần key lookup quay về PK_DonHang. user_updates đếm số câu lệnh ghi đã phải bảo trì chỉ mục: xóa 1.000 dòng trong một câu tăng 1. Chỉ mục có số đọc bằng 0 và user_updates lớn sau một thời gian chạy đủ dài là ứng viên để bỏ.
Số lần đọc và ghi của từng chỉ mục trên DonHang, kèm giờ khởi động instance
SELECT
i.name AS index_name, i.type_desc, i.is_unique,
ISNULL(us.user_seeks, 0) AS user_seeks,
ISNULL(us.user_scans, 0) AS user_scans,
ISNULL(us.user_lookups, 0) AS user_lookups,
ISNULL(us.user_updates, 0) AS user_updates,
us.last_user_seek, us.last_user_scan
FROM sys.indexes AS i
LEFT JOIN sys.dm_db_index_usage_stats AS us
ON us.database_id = DB_ID()
AND us.object_id = i.object_id
AND us.index_id = i.index_id
WHERE i.object_id = OBJECT_ID(N'dbo.DonHang')
ORDER BY
ISNULL(us.user_seeks, 0) + ISNULL(us.user_scans, 0) + ISNULL(us.user_lookups, 0),
ISNULL(us.user_updates, 0) DESC;
SELECT sqlserver_start_time
FROM sys.dm_os_sys_info;
Bộ đếm bắt đầu lại từ rỗng mỗi khi instance khởi động, và dòng của một database bị xóa khi database đó detach hoặc tắt, chẳng hạn do AUTO_CLOSE. Database tạo mới trên LocalDB có AUTO_CLOSE bật sẵn: trên máy thử, bộ đếm của DonHang trống mỗi lần ứng dụng đóng kết nối cuối. So sqlserver_start_time với chu kỳ nghiệp vụ: chỉ mục chỉ phục vụ báo cáo cuối tháng trông như không dùng nếu máy vừa khởi động lại giữa tháng. Chỉ mục unique có thể không bao giờ được đọc mà vẫn đang giữ ràng buộc.
Gỡ một chỉ mục an toàn
Ví dụ với IX_DonHang_DangMo, giả sử màn hình đơn đang mở đã chuyển sang hệ khác.
- Lưu lệnh
CREATE INDEXgốc vào kho mã. - Tìm câu đang dùng chỉ mục trong Query Store (
sys.query_store_plan, lọcquery_planchứa tên chỉ mục) và trong module có hint (sys.sql_modules). - Disable thay vì xóa. Dữ liệu chỉ mục được giải phóng, định nghĩa và thống kê ở lại. Câu nào dùng hint trỏ vào chỉ mục đã disable sẽ báo lỗi ngay, nên phụ thuộc ẩn lộ ra sớm.
- Theo dõi Query Store qua ít nhất một chu kỳ nghiệp vụ đầy đủ: câu nào tăng thời gian chạy hoặc đổi kế hoạch.
- Có câu chậm đi thì bật lại bằng
REBUILD, lệnh dựng lại toàn bộ chỉ mục từ bảng. Không có hồi quy thì xóa hẳn.
Bước 2: tìm câu trong Query Store và module nhắc tới IX_DonHang_DangMo
SELECT DISTINCT qsq.query_id, LEFT(qt.query_sql_text, 200) AS query_text
FROM sys.query_store_plan AS qsp
JOIN sys.query_store_query AS qsq ON qsq.query_id = qsp.query_id
JOIN sys.query_store_query_text AS qt ON qt.query_text_id = qsq.query_text_id
WHERE qsp.query_plan LIKE N'%IX_DonHang_DangMo%';
SELECT OBJECT_SCHEMA_NAME(m.object_id) AS schema_name, OBJECT_NAME(m.object_id) AS module_name
FROM sys.sql_modules AS m
WHERE m.definition LIKE N'%IX_DonHang_DangMo%';
ALTER INDEX IX_DonHang_DangMo ON dbo.DonHang DISABLE;
-- Sau một chu kỳ nghiệp vụ, một trong hai:
ALTER INDEX IX_DonHang_DangMo ON dbo.DonHang REBUILD;
-- DROP INDEX IX_DonHang_DangMo ON dbo.DonHang;
Chỉ disable chỉ mục nonclustered không unique
Disable chỉ mục unique tắt luôn ràng buộc PRIMARY KEY hoặc UNIQUE dựa trên nó, và các khóa ngoại trỏ vào đó. Disable clustered index làm cả bảng không đọc ghi được cho đến khi rebuild hoặc drop. Bật lại một chỉ mục đã disable là dựng lại từ đầu: trên bản Standard, rebuild giữ khóa bảng suốt thời gian chạy.
4. Áp dụng trong .NET
EF Core khai báo chỉ mục lọc bằng HasFilter. Phần dễ hỏng nằm ở câu LINQ: EF Core viết hằng số trong biểu thức thẳng vào SQL, nhưng biến thì thành tham số, và tham số làm chỉ mục lọc bị loại như ở mục 1.
modelBuilder.Entity<DonHang>()
.HasIndex(d => new { d.KhachHangId, d.NgayTao }, "IX_DonHang_DangMo")
.IncludeProperties(d => d.TongTien)
.HasFilter("[TrangThai] = 1");
// Hằng số: EF Core viết CAST(1 AS tinyint) vào câu SQL, khớp chỉ mục lọc
var dangMo = db.DonHang
.Where(d => d.KhachHangId == khach && d.TrangThai == 1);
// Biến: EF Core gửi @trangThai, chỉ mục lọc bị loại
var theoBien = db.DonHang
.Where(d => d.KhachHangId == khach && d.TrangThai == trangThai);
// EF.Constant: ép giá trị của biến thành hằng số trong câu SQL
var epHang = db.DonHang
.Where(d => d.KhachHangId == khach && d.TrangThai == EF.Constant(trangThai));
Chạy trên LocalDB SQL Server 2019 (15.0.4382), .NET 10.0.401, EF Core 10.0.3, bảng thử 1 triệu đơn dựng ở bài đầu của chương. Khách 42 ở đó có 303 đơn mới; mỗi câu lấy NgayTao, TongTien.
CREATE INDEX [IX_DonHang_DangMo] ON [DonHang] ([KhachHangId], [NgayTao]) INCLUDE ([TongTien]) WHERE [TrangThai] = 1;
TrangThai == 1 303 dòng 9 reads WHERE [d].[KhachHangId] = @khach AND [d].[TrangThai] = CAST(1 AS tinyint)
TrangThai == trangThai 303 dòng 4,608 reads WHERE [d].[KhachHangId] = @khach AND [d].[TrangThai] = @trangThai
TrangThai == EF.Constant(trangThai) 303 dòng 9 reads WHERE [d].[KhachHangId] = @khach AND [d].[TrangThai] = CAST(1 AS tinyint)
Câu có hằng số seek trên IX_DonHang_DangMo: chạy đúng câu đó bằng sqlcmd rồi đọc sys.dm_db_index_usage_stats trong cùng phiên, chỉ mục này có một user_seeks. Câu có tham số không dùng được chỉ mục lọc, và trình tối ưu chọn quét PK_DonHang: 4.608 lần đọc cho cùng 303 dòng.
Bẫy hay gặp là một hàm repository chung kiểu LayDon(int khach, byte trangThai): code gọn, nhưng mọi câu đi qua nó mất chỉ mục lọc. EF.Constant sửa được, đổi lại mỗi giá trị khác nhau là một câu SQL khác nhau và một kế hoạch riêng trong cache. Với TrangThai chỉ có 5 giá trị thì chấp nhận được; với cột nhiều giá trị như KhachHangId thì không.
Đầu file có #:property PublishAot=false: file-based app bật PublishAot mặc định, và khi đó EF Core không dựng model lúc chạy.
DonDangMo.cs: chỉ mục lọc và ba cách viết TrangThai, chạy bằng dotnet run DonDangMo.cs
#:package Microsoft.EntityFrameworkCore.SqlServer@10.0.3
#:property PublishAot=false
// Chỉ mục lọc khai báo bằng HasFilter, và vì sao giá trị lọc phải là hằng số trong câu SQL.
// Chạy: dotnet run DonDangMo.cs (cần database Kumeo_chimuc đã dựng ở bài 1)
using System.Data.Common;
using System.Text.RegularExpressions;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
const string cs = @"Server=(localdb)\MSSQLLocalDB;Database=Kumeo_chimuc;Integrated Security=true;TrustServerCertificate=true";
await using var db = new BanHangDb(cs);
foreach (var line in db.Database.GenerateCreateScript().Split('\n'))
if (line.Contains("IX_DonHang_DangMo")) Console.WriteLine(line.Trim());
Console.WriteLine();
var conn = (SqlConnection)db.Database.GetDbConnection();
long reads = 0;
conn.InfoMessage += (_, e) =>
{
foreach (Match m in Regex.Matches(e.Message, @"logical reads (\d+)"))
reads += long.Parse(m.Groups[1].Value);
};
await db.Database.OpenConnectionAsync();
await db.Database.ExecuteSqlRawAsync("""
IF INDEXPROPERTY(OBJECT_ID(N'dbo.DonHang'), N'IX_DonHang_DangMo', 'IndexID') IS NULL
CREATE INDEX IX_DonHang_DangMo ON dbo.DonHang (KhachHangId, NgayTao)
INCLUDE (TongTien) WHERE TrangThai = 1;
SET STATISTICS IO ON;
""");
int khach = 42;
byte trangThai = 1;
var cachViet = new (string Ten, Func<IQueryable<DonHang>> Q)[]
{
("TrangThai == 1", () => db.DonHang.Where(d => d.KhachHangId == khach && d.TrangThai == 1)),
("TrangThai == trangThai", () => db.DonHang.Where(d => d.KhachHangId == khach && d.TrangThai == trangThai)),
("TrangThai == EF.Constant(trangThai)", () => db.DonHang.Where(d => d.KhachHangId == khach && d.TrangThai == EF.Constant(trangThai))),
};
foreach (var (ten, q) in cachViet)
{
var query = q().Select(d => new { d.NgayTao, d.TongTien });
var sql = query.ToQueryString();
var where = sql[(sql.IndexOf("WHERE") + 6)..].Trim();
reads = 0;
var dong = (await query.ToListAsync()).Count;
Console.WriteLine($"{ten,-37} {dong,4} dòng {reads,6:N0} reads WHERE {where}");
}
class DonHang
{
public long DonHangId { get; set; }
public DateTime NgayTao { get; set; }
public int KhachHangId { get; set; }
public byte TrangThai { get; set; }
public decimal TongTien { get; set; }
}
class BanHangDb(string cs) : DbContext
{
public DbSet<DonHang> DonHang => Set<DonHang>();
protected override void OnConfiguring(DbContextOptionsBuilder o) =>
o.UseSqlServer(cs).AddInterceptors(new DocHetThongBao());
protected override void OnModelCreating(ModelBuilder b)
{
var e = b.Entity<DonHang>();
e.ToTable("DonHang");
e.HasKey(d => new { d.NgayTao, d.DonHangId });
e.Property(d => d.NgayTao).HasColumnType("datetime2(0)");
e.Property(d => d.TongTien).HasColumnType("decimal(18, 2)");
e.HasIndex(d => new { d.KhachHangId, d.NgayTao }, "IX_DonHang_KhachHang");
e.HasIndex(d => new { d.KhachHangId, d.NgayTao }, "IX_DonHang_DangMo")
.IncludeProperties(d => d.TongTien)
.HasFilter("[TrangThai] = 1");
}
}
// SqlClient chỉ phát InfoMessage của STATISTICS IO khi reader được đọc tới hết.
class DocHetThongBao : DbCommandInterceptor
{
public override InterceptionResult DataReaderClosing(DbCommand c, DataReaderClosingEventData e, InterceptionResult r)
{
while (e.DataReader.NextResult()) { }
return r;
}
public override async ValueTask<InterceptionResult> DataReaderClosingAsync(DbCommand c, DataReaderClosingEventData e, InterceptionResult r)
{
while (await e.DataReader.NextResultAsync()) { }
return r;
}
}
Những chỗ hay hiểu sai
- "EF Core luôn tham số hóa, nên viết
== 1hay== trangThainhư nhau." Hằng số trong biểu thức LINQ đi thẳng vào câu SQL; biến thành tham số. Chỉ dạng đầu khớp chỉ mục lọc. - "Gợi ý chỉ mục thiếu cho sẵn câu
CREATE INDEXđúng." Gợi ý không có thứ tự cột, không biết chỉ mục đang có, và mất khi khởi động lại. - "
user_seeks = 0nghĩa là chỉ mục chưa bao giờ được dùng." Nghĩa là chưa được dùng kể từ lần khởi động hoặc lần database mở gần nhất. - "Disable chỉ mục nonclustered thì dữ liệu vẫn còn, bật lại tức thì." Disable giải phóng dữ liệu chỉ mục. Bật lại là dựng lại toàn bộ.
Kết luận
Mỗi chỉ mục là một bản sao phải ghi theo mọi đơn. Chỉ mục lọc làm bản sao đó nhỏ, nhưng chỉ khi câu SQL nói đúng giá trị lọc; còn chỉ mục thừa thì gỡ bằng số đo, không bằng cảm giác.
Trong dự án .NET của bạn:
- Khai báo chỉ mục lọc bằng
HasIndex(...).HasFilter("[TrangThai] = 1"). Trong LINQ, viết giá trị lọc là hằng số hoặc bọcEF.Constant(...), rồi kiểm bằngToQueryString(). - Viết test tích hợp cho màn hình đơn đang mở với ngưỡng
logical reads, để bắt lần ai đó đổi hằng số thành biến. - Trước khi thêm chỉ mục, đếm số chỉ mục trên
DonHang: mỗi chỉ mục thêm một lần ghi và bản ghi log cho mỗi đơn. Không đưaTrangThaivào khóa hayINCLUDEnếu không có câu đáng giá. - Mỗi tháng xem
sys.dm_db_index_usage_statscùngsqlserver_start_time; gộp gợi ý chỉ mục thiếu vào chỉ mục đang có. - Bật Query Store trước mọi lần thêm, đổi hoặc xóa chỉ mục; gỡ bằng
DISABLEtrước khiDROP.
Đọc tiếp
- Chỉ mục phủ, thứ tự cột khóa và điều kiện seek được: bài trước,
INCLUDEvà cách viết điều kiện. - Thống kê, histogram và ngưỡng tự cập nhật: bài tiếp theo, nguồn của số dòng ước lượng.
- Kỹ thuật thường dùng: định nghĩa
IX_DonHang_DangMo, fillfactor, Query Store, columnstore. - Page, dòng và extent: page, allocation unit và tách page.
- Khóa và leo thang khóa: khóa và chặn trên chính các chỉ mục này.
Nguồn
- Create filtered indexes
- Forced parameterization with filtered indexes
- Index architecture and design guide
- sys.dm_db_index_operational_stats
- Tune nonclustered indexes with missing index suggestions
- sys.dm_db_index_usage_stats
- Disable indexes and constraints
- EF Core — Indexes: index filter
- EF.Constant