Bagikan melalui


Kueri Tunggal vs. Terpisah

Masalah performa dengan kueri tunggal

Saat bekerja melawan database relasional, EF memuat entitas terkait dengan memperkenalkan WAN 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

Mari kita periksa kueri LINQ berikut dan SQL yang diterjemahkan setara:

var blogs = ctx.Blogs
    .Include(b => b.Posts)
    .Include(b => b.Contributors)
    .ToList();
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 keduanya Posts dan Contributors merupakan navigasi Blog koleksi - mereka berada pada tingkat yang sama - database relasional mengembalikan 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 secara tidak sengaja ditransfer ke klien, terutama karena lebih banyak WAN saudara ditambahkan ke kueri; ini bisa menjadi masalah performa utama dalam aplikasi database.

Perhatikan bahwa ledakan kartesius tidak terjadi ketika dua JOIN tidak berada pada tingkat yang sama:

var blogs = ctx.Blogs
    .Include(b => b.Posts)
    .ThenInclude(p => p.Comments)
    .ToList();
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 Postkoleksi dari , 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

WAN dapat membuat jenis masalah performa lain. Mari kita periksa kueri berikut, yang hanya memuat satu navigasi koleksi:

var blogs = ctx.Blogs
    .Include(b => b.Posts)
    .ToList();
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]

Memeriksa kolom yang diproyeksikan, setiap baris yang dikembalikan oleh kueri ini berisi properti dari Blogs tabel dan Posts ; ini berarti bahwa properti blog diduplikasi untuk setiap posting yang dimiliki 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 = ctx.Blogs
    .Select(b => new
    {
        b.Id,
        b.Name,
        b.Posts
    })
    .ToList();

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 ledakan kartesius, duplikasi data yang disebabkan oleh WAN biasanya tidak signifikan, karena ukuran data duplikat dapat diabaikan; ini biasanya adalah sesuatu yang perlu dikhawatirkan hanya jika Anda memiliki kolom besar di 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 = context.Blogs
        .Include(blog => blog.Posts)
        .AsSplitQuery()
        .ToList();
}

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 versi EF sebelum 10, beri perhatian khusus untuk membuat urutan kueri Anda sepenuhnya unik; tidak melakukannya dapat menyebabkan data yang salah dikembalikan. 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 ke satu selalu dimuat melalui WAN dalam kueri yang sama, karena tidak memiliki dampak 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 = context.Blogs
        .Include(blog => blog.Posts)
        .AsSingleQuery()
        .ToList();
}

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/AsSplitQuery operator pada kueri.

Untuk menonaktifkan peringatan, konfigurasikan mode pemisahan kueri secara global atau di tingkat kueri ke nilai yang sesuai.

Karakteristik kueri terpisah

Meskipun kueri terpisah menghindari masalah performa yang terkait dengan JOIN dan ledakan kartesius, kueri 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 rekam jepret, meskipun melakukannya dapat menciptakan masalah performa sendiri. Untuk informasi selengkapnya, lihat dokumentasi database Anda.
  • Setiap kueri saat ini menyiratkan roundtrip jaringan tambahan ke database Anda. Beberapa roundtrips jaringan dapat menurunkan performa, terutama di mana 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 gabungan ke navigasi referensi. Ini dapat menurunkan performa, terutama jika ada banyak navigasi referensi. Silakan upvote #29182 jika ini adalah sesuatu yang ingin Anda lihat tetap.

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.