Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Masalah performa dengan kueri tunggal
Saat bekerja dengan database relasional, EF memuat entitas terkait dengan memperkenalkan JOIN ke dalam satu kueri. Meskipun JOIN cukup standar saat menggunakan SQL, MEREKA dapat menciptakan masalah performa yang signifikan jika digunakan secara tidak tepat. Halaman ini menjelaskan masalah performa ini, dan menunjukkan cara alternatif untuk memuat entitas terkait yang bekerja di sekitarnya.
Ledakan kartesius
Ayo kita periksa kueri LINQ berikut dan padanan SQL yang telah diterjemahkan:
var blogs = await ctx.Blogs
.Include(b => b.Posts)
.Include(b => b.Contributors)
.ToListAsync();
SELECT [b].[Id], [b].[Name], [p].[Id], [p].[BlogId], [p].[Title], [c].[Id], [c].[BlogId], [c].[FirstName], [c].[LastName]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Contributors] AS [c] ON [b].[Id] = [c].[BlogId]
ORDER BY [b].[Id], [p].[Id]
Dalam contoh ini, karena Posts dan Contributors keduanya merupakan navigasi koleksi dari Blog - berada pada tingkat yang sama - database relasional menghasilkan produk silang: setiap baris dari Posts digabungkan dengan setiap baris dari Contributors. Ini berarti bahwa jika blog tertentu memiliki 10 posting dan 10 kontributor, database mengembalikan 100 baris untuk blog tunggal tersebut. Fenomena ini - kadang-kadang disebut ledakan kartesius - dapat menyebabkan sejumlah besar data ditransfer ke klien secara tidak sengaja, terutama ketika lebih banyak JOIN saudara ditambahkan ke kueri; ini bisa menjadi masalah kinerja yang signifikan dalam aplikasi database.
Perhatikan bahwa ledakan kartesius tidak terjadi ketika dua JOIN tidak berada pada tingkat yang sama:
var blogs = await ctx.Blogs
.Include(b => b.Posts)
.ThenInclude(p => p.Comments)
.ToListAsync();
SELECT [b].[Id], [b].[Name], [t].[Id], [t].[BlogId], [t].[Title], [t].[Id0], [t].[Content], [t].[PostId]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Comment] AS [c] ON [p].[Id] = [c].[PostId]
ORDER BY [b].[Id], [t].[Id]
Dalam kueri ini, Comments adalah navigasi koleksi dari Post, tidak seperti Contributors pada kueri sebelumnya, yang merupakan navigasi koleksi dari Blog. Dalam hal ini, satu baris dikembalikan untuk setiap komentar yang dimiliki blog (melalui postingannya), dan produk silang tidak terjadi.
Duplikasi data
JOIN dapat menimbulkan jenis masalah performa lain. Mari kita periksa kueri berikut, yang hanya memuat satu navigasi koleksi:
var blogs = await ctx.Blogs
.Include(b => b.Posts)
.ToListAsync();
SELECT [b].[Id], [b].[Name], [b].[HugeColumn], [p].[Id], [p].[BlogId], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
ORDER BY [b].[Id]
Diperiksa pada kolom yang diproyeksikan, setiap baris yang dikembalikan oleh kueri ini berisi properti dari tabel Blogs dan Posts; artinya properti blog diduplikasi untuk setiap postingan yang dimiliki oleh blog. Meskipun ini biasanya normal dan tidak menyebabkan masalah, jika Blogs tabel kebetulan memiliki kolom yang sangat besar (misalnya data biner, atau teks besar), kolom tersebut akan diduplikasi dan dikirim kembali ke klien beberapa kali. Ini dapat secara signifikan meningkatkan lalu lintas jaringan dan berdampak buruk pada performa aplikasi Anda.
Jika Anda tidak benar-benar membutuhkan kolom besar, mudah untuk tidak mengkuerinya:
var blogs = await ctx.Blogs
.Select(b => new
{
b.Id,
b.Name,
b.Posts
})
.ToListAsync();
Dengan menggunakan proyeksi untuk secara eksplisit memilih kolom mana yang Anda inginkan, Anda dapat menghilangkan kolom besar dan meningkatkan performa; perhatikan bahwa ini adalah ide yang baik terlepas dari duplikasi data, jadi pertimbangkan untuk melakukannya bahkan ketika tidak memuat navigasi koleksi. Namun, karena ini memproyeksikan blog ke jenis anonim, blog tidak dilacak oleh EF dan perubahannya tidak dapat disimpan kembali seperti biasa.
Perlu dicatat bahwa tidak seperti Cartesian Explosion, duplikasi data yang disebabkan oleh JOIN biasanya tidak signifikan, karena ukuran data yang diduplikasi sangat kecil; ini biasanya menjadi perhatian hanya jika Anda memiliki kolom besar dalam tabel utama Anda.
Memisahkan kueri
Untuk mengatasi masalah performa yang dijelaskan di atas, EF memungkinkan Anda menentukan bahwa kueri LINQ tertentu harus dibagi menjadi beberapa kueri SQL. Alih-alih JOIN, kueri terpisah menghasilkan kueri SQL tambahan untuk setiap navigasi koleksi yang disertakan:
using (var context = new BloggingContext())
{
var blogs = await context.Blogs
.Include(blog => blog.Posts)
.AsSplitQuery()
.ToListAsync();
}
Ini akan menghasilkan SQL berikut:
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url]
FROM [Blogs] AS [b]
ORDER BY [b].[BlogId]
SELECT [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title], [b].[BlogId]
FROM [Blogs] AS [b]
INNER JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId]
ORDER BY [b].[BlogId]
Peringatan
Saat menggunakan kueri terpisah dengan Lewati/Ambil pada versi EF sebelum 10, perhatikan dengan lebih cermat untuk membuat urutan kueri Anda sepenuhnya unik. Tidak melakukannya dapat mengembalikan data yang tidak akurat. Misalnya, jika hasil diurutkan hanya berdasarkan tanggal, tetapi mungkin ada beberapa hasil dengan tanggal yang sama, maka masing-masing kueri terpisah masing-masing bisa mendapatkan hasil yang berbeda dari database. Pemesanan berdasarkan tanggal dan ID (atau properti unik atau kombinasi properti lainnya) membuat pemesanan sepenuhnya unik dan menghindari masalah ini. Perhatikan bahwa database relasional tidak menerapkan pengurutan apa pun secara default, bahkan pada kunci utama.
Catatan
Entitas terkait satu-satu selalu dimuat melalui JOIN dalam kueri yang sama, karena tidak mempengaruhi performa.
Mengaktifkan kueri terpisah secara global
Anda juga dapat mengonfigurasi kueri terpisah sebagai default untuk konteks aplikasi Anda:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(
@"Server=(localdb)\mssqllocaldb;Database=EFQuerying;Trusted_Connection=True;ConnectRetryCount=0",
o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
}
Saat kueri terpisah dikonfigurasi sebagai default, masih dimungkinkan untuk mengonfigurasi kueri tertentu untuk dijalankan sebagai kueri tunggal:
using (var context = new SplitQueriesBloggingContext())
{
var blogs = await context.Blogs
.Include(blog => blog.Posts)
.AsSingleQuery()
.ToListAsync();
}
EF Core menggunakan mode kueri tunggal secara default tanpa adanya konfigurasi apa pun. Karena dapat menyebabkan masalah performa, EF Core menghasilkan peringatan setiap kali kondisi berikut terpenuhi:
- EF Core mendeteksi bahwa kueri memuat beberapa koleksi.
- Pengguna belum mengonfigurasi mode pemisahan kueri secara global.
- Pengguna belum menggunakan
AsSingleQuery/AsSplitQueryoperator pada kueri.
Untuk menonaktifkan peringatan, konfigurasikan mode pemisahan kueri secara global atau di tingkat kueri ke nilai yang sesuai.
Karakteristik pembagian kueri
Meskipun kueri terpisah menghindari masalah performa yang terkait dengan JOIN dan ledakan kartesius, kueri terpisah ini juga memiliki beberapa kelemahan.
- Meskipun sebagian besar database menjamin konsistensi data untuk kueri tunggal, tidak ada jaminan tersebut untuk beberapa kueri. Jika database diperbarui secara bersamaan saat menjalankan kueri Anda, data yang dihasilkan mungkin tidak konsisten. Anda dapat menguranginya dengan membungkus kueri dalam transaksi yang dapat diserialisasikan atau transaksi snapshot, meskipun melakukannya dapat menciptakan masalah performa tersendiri. Untuk informasi selengkapnya, lihat dokumentasi database Anda.
- Setiap pertanyaan saat ini berarti perputaran tambahan jaringan ke database Anda. Beberapa perjalanan pulang-pergi jaringan dapat menurunkan performa, terutama jika latensi ke database tinggi (misalnya, layanan cloud).
- Meskipun beberapa database memungkinkan penggunaan hasil beberapa kueri secara bersamaan (SQL Server dengan MARS, Sqlite), sebagian besar hanya memungkinkan satu kueri aktif pada titik tertentu. Jadi semua hasil dari kueri sebelumnya harus di-buffer dalam memori aplikasi Anda sebelum menjalankan kueri nanti, yang mengarah pada peningkatan persyaratan memori.
- Saat menyertakan navigasi referensi serta navigasi koleksi, masing-masing kueri terpisah akan menyertakan penggabungan dengan navigasi referensi. Ini dapat menurunkan performa, terutama jika ada banyak navigasi referensi. Silakan upvote #29182 jika ini adalah sesuatu yang ingin Anda lihat diperbaiki.
Sayangnya, tidak ada satu strategi untuk memuat entitas terkait yang sesuai dengan semua skenario. Pertimbangkan dengan cermat kelebihan dan kekurangan kueri tunggal dan terpisah untuk memilih yang sesuai dengan kebutuhan Anda.