Optimasi query: execution plan, SARGable, parameter sniffing, pagination
Ringkasan satu halaman. Dari Lesson 0023. Skrip lab: dotnet/lab/0023-optimasi-query.sql. Index: Reference: Index. N+1 dan AsNoTracking: Reference: EF Core.
Alur "query ini lambat" [1]
- Ambil SQL: log EF Core (
LogTo, level Information, ada durasi) /ToQueryString(). Cek N+1, tracking, kolom berlebih (0006). - Jalankan dengan actual plan +
SET STATISTICS IO ON. - Baca plan: operator mahal, scan vs seek, Key Lookup berulang, estimated vs actual rows, warning (mis.
CONVERT_IMPLICIT). - Perbaiki: SARGable, index (composite/covering), parameter sniffing, keyset pagination.
- Ukur lagi: logical reads + plan.
Estimated vs actual plan [2] [3]
| Estimated | Actual | |
|---|---|---|
| Query dijalankan | tidak | ya |
| Isi | plan kompilasi saja | plan + runtime (actual rows, warning, waktu) |
| T-SQL | SET SHOWPLAN_XML ON (sendirian di batch) | SET STATISTICS XML ON |
Estimated rows jauh dari actual rows → plan dipilih untuk jumlah baris yang salah. Penyebab: statistik, local variable (pakai rata-rata density), parameter sniffing. Scan tidak selalu buruk bila sebagian besar baris diambil.
SARGable [4]
| Non-SARGable → scan | SARGable → seek |
|---|---|
YEAR(CreatedAt) = 2025 AND MONTH(CreatedAt) = 6 | CreatedAt >= '2025-06-01' AND CreatedAt < '2025-07-01' |
LEFT(Name, 12) = N'Product 0001' | Name LIKE N'Product 0001%' |
Price * 1.1 > 1000 | Price > 1000 / 1.1 |
LIKE N'%abc', ISNULL(col, …), CONVERT(…, col) | tulis ulang, atau computed column + index |
- Aturan: jangan bungkus kolom dengan fungsi/perhitungan; pindahkan ke sisi konstanta. Berlaku juga untuk JOIN, HAVING, GROUP BY, ORDER BY.
- Tipe parameter harus cocok dengan kolom (
nvarcharvsvarchar) supaya tidak adaCONVERT_IMPLICITpada kolom. [5] - Tanggal: half-open
>= awal AND < berikutnya, bukanBETWEEN … '…-30'. EF Core:p.CreatedAt.Year == 2025→DATEPARTpada kolom.
Parameter sniffing [3] [6]
Plan dikompilasi untuk nilai parameter pertama, disimpan, dipakai ulang. Masalah bila data tidak merata (lab: Category 1 = 90.000, Category 6 = 20). Kena: stored procedure, sp_executesql (EF Core, Dapper), prepared query.
| Cara | Harga |
|---|---|
OPTION (RECOMPILE) [7] | CPU kompilasi tiap eksekusi |
OPTION (OPTIMIZE FOR (@p = nilai)) | harus tahu nilai yang khas |
OPTIMIZE FOR UNKNOWN / local variable | plan rata-rata, buruk untuk nilai ekstrem |
| Covering index (akar: hilangkan Key Lookup) | biaya tulis + storage |
DBCC FREEPROCCACHE (plan_handle) | darurat saja; tanpa parameter membuang semua plan |
SQL Server 2022 PSP optimization (compat 160, predikat equality): beberapa plan aktif per query. Untuk demo: ALTER DATABASE SCOPED CONFIGURATION SET PARAMETER_SENSITIVE_PLAN_OPTIMIZATION = OFF lalu = ON lagi; kosongkan cache database: ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE.
Pagination [8] [9]
-- OFFSET: memproses semua baris yang dilewati; random access OK
SELECT Id, Name FROM dbo.Product ORDER BY Id OFFSET 90000 ROWS FETCH NEXT 20 ROWS ONLY;
-- Keyset: seek langsung; hanya maju/mundur, tidak bisa lompat halaman
SELECT TOP (20) Id, Name FROM dbo.Product WHERE Id > @lastId ORDER BY Id;
-- Keyset dua kunci (urutan unik: Id sebagai pemecah seri, index (Price) / (Price, Id))
SELECT TOP (20) Id, Name, Price FROM dbo.Product
WHERE Price > @lastPrice OR (Price = @lastPrice AND Id > @lastId)
ORDER BY Price, Id;
- Urutan harus unik; database tidak punya urutan default, bahkan untuk primary key.
- OFFSET bisa melewatkan/menggandakan baris saat data berubah; keyset tidak.
- EF Core:
Skip/Take= OFFSET;Where(p => p.Id > lastId).Take(n)= keyset.
SQL dari EF Core [10] [11]
optionsBuilder.UseSqlServer(cs)
.LogTo(Console.WriteLine, LogLevel.Information) // SQL + durasi; jangan dibiarkan di production
.EnableSensitiveDataLogging(); // nilai parameter: development saja
var sql = db.Products.Where(p => p.CategoryId == id)
.TagWith("Products by category") // komentar di SQL
.ToQueryString(); // SQL tanpa eksekusi (EF Core 5+, untuk debugging)
Padanan Node: LogTo ≈ Prisma log: ['query']; ToQueryString ≈ knex .toSQL().
Jawaban 60–90 detik
Ambil SQL dari log EF Core atau ToQueryString, cek N+1 dan tracking. Jalankan dengan actual plan dan STATISTICS IO.
Cari scan pada tabel besar, Key Lookup ribuan kali, estimated vs actual yang meleset. Perbaiki: kolom tanpa fungsi (SARGable), index composite/covering,
parameter sniffing (RECOMPILE, OPTIMIZE FOR, atau index yang menghilangkan lookup), keyset pagination untuk halaman jauh. Ukur lagi.
Sumber: [1] EF Core: Performance Diagnosis · [2] Display the estimated execution plan · [3] Query processing architecture guide · [4] Troubleshoot high-CPU-usage issues · [5] Dapper README · [6] PSP optimization · [7] Query hints · [8] EF Core: Pagination · [9] ORDER BY (OFFSET/FETCH) · [10] EF Core: Simple logging · [11] ToQueryString