Bagikan melalui


Kueri yang Efisien

Mengkueri secara efisien adalah subjek yang luas, yang mencakup subjek seluas indeks, strategi pemuatan entitas terkait, dan banyak lainnya. Bagian ini merinci beberapa tema umum untuk membuat kueri Anda lebih cepat, dan jebakan yang biasanya ditemui pengguna.

Gunakan indeks dengan benar

Faktor penentu utama dalam apakah kueri berjalan cepat atau tidak adalah apakah kueri akan menggunakan indeks dengan benar jika sesuai: database biasanya digunakan untuk menyimpan data dalam jumlah besar, dan kueri yang melintasi seluruh tabel biasanya merupakan sumber masalah performa serius. Masalah pengindeksan tidak mudah ditemukan, karena tidak segera jelas apakah kueri tertentu akan menggunakan indeks atau tidak. Contohnya:

// Matches on start, so uses an index (on SQL Server)
var posts1 = context.Posts.Where(p => p.Title.StartsWith("A")).ToList();
// Matches on end, so does not use the index
var posts2 = context.Posts.Where(p => p.Title.EndsWith("A")).ToList();

Cara yang baik untuk menemukan masalah pengindeksan adalah dengan terlebih dahulu menentukan kueri yang lambat, lalu memeriksa rencana kuerinya melalui alat favorit database Anda; lihat halaman diagnosis performa untuk informasi selengkapnya tentang cara melakukannya. Rencana kueri menampilkan apakah kueri melintasi seluruh tabel, atau menggunakan indeks.

Sebagai aturan umum, tidak ada pengetahuan EF khusus untuk menggunakan indeks atau mendiagnosis masalah performa yang terkait dengannya; pengetahuan database umum yang terkait dengan indeks sama relevannya dengan aplikasi EF tentang aplikasi yang tidak menggunakan EF. Berikut ini mencantumkan beberapa panduan umum yang perlu diingat saat menggunakan indeks:

  • Meskipun indeks mempercepat kueri, indeks juga memperlambat pembaruan karena perlu diperbarui. Hindari menentukan indeks yang tidak diperlukan, dan pertimbangkan untuk menggunakan filter indeks untuk membatasi indeks ke subset baris, sehingga mengurangi overhead ini.
  • Indeks komposit dapat mempercepat kueri yang memfilter beberapa kolom, tetapi mereka juga dapat mempercepat kueri yang tidak memfilter semua kolom indeks - tergantung pada pengurutan. Misalnya, indeks pada kolom A dan B mempercepat pemfilteran kueri oleh A dan B serta kueri yang hanya difilter oleh A, tetapi tidak mempercepat kueri hanya memfilter melalui B.
  • Jika kueri difilter menurut ekspresi di atas kolom (misalnya price / 2), indeks sederhana tidak dapat digunakan. Namun, Anda dapat menentukan kolom persisten yang disimpan untuk ekspresi Anda, dan membuat indeks di atasnya. Beberapa database juga mendukung indeks ekspresi, yang dapat langsung digunakan untuk mempercepat pemfilteran kueri menurut ekspresi apa pun.
  • Database yang berbeda memungkinkan indeks dikonfigurasi dengan berbagai cara, dan dalam banyak kasus penyedia EF Core mengeksposnya melalui API Fluent. Misalnya, penyedia SQL Server memungkinkan Anda untuk mengonfigurasi apakah indeks diklusterkan, atau mengatur faktor pengisiannya. Lihat dokumentasi penyedia Anda untuk informasi selengkapnya.

Hanya properti proyek yang Anda butuhkan

EF Core memudahkan untuk mengkueri instans entitas, lalu menggunakan instans tersebut dalam kode. Namun, mengkueri instans entitas dapat sering menarik kembali lebih banyak data daripada yang diperlukan dari database Anda. Pertimbangkan hal berikut:

foreach (var blog in context.Blogs)
{
    Console.WriteLine("Blog: " + blog.Url);
}

Meskipun kode ini hanya benar-benar membutuhkan setiap properti Blog Url , seluruh entitas Blog diambil, dan kolom yang tidak diperlukan ditransfer dari database:

SELECT [b].[BlogId], [b].[CreationDate], [b].[Name], [b].[Rating], [b].[Url]
FROM [Blogs] AS [b]

Ini dapat dioptimalkan dengan menggunakan Select untuk memberi tahu EF kolom mana yang akan diproyeksikan:

foreach (var blogName in context.Blogs.Select(b => b.Url))
{
    Console.WriteLine("Blog: " + blogName);
}

SQL yang dihasilkan hanya menarik kembali kolom yang diperlukan:

SELECT [b].[Url]
FROM [Blogs] AS [b]

Jika Anda perlu memproyeksikan lebih dari satu kolom, proyeksi ke jenis anonim C# dengan properti yang Anda inginkan.

Perhatikan bahwa teknik ini sangat berguna untuk kueri baca-saja, tetapi semuanya menjadi lebih rumit jika Anda perlu memperbarui blog yang diambil, karena pelacakan perubahan EF hanya berfungsi dengan instans entitas. Dimungkinkan untuk melakukan pembaruan tanpa memuat seluruh entitas dengan melampirkan instans Blog yang dimodifikasi dan memberi tahu EF properti mana yang telah berubah, tetapi itu adalah teknik yang lebih canggih yang mungkin tidak sepadan.

Membatasi ukuran resultset

Secara default, kueri mengembalikan semua baris yang cocok dengan filternya:

var blogsAll = context.Posts
    .Where(p => p.Title.StartsWith("A"))
    .ToList();

Karena jumlah baris yang dikembalikan tergantung pada data aktual dalam database Anda, tidak mungkin untuk mengetahui berapa banyak data yang akan dimuat dari database, berapa banyak memori yang akan diambil oleh hasil, dan berapa banyak beban tambahan yang akan dihasilkan saat memproses hasil ini (misalnya dengan mengirimkannya ke browser pengguna melalui jaringan). Sangat penting, database pengujian sering berisi sedikit data, sehingga semuanya berfungsi dengan baik saat pengujian, tetapi masalah performa tiba-tiba muncul ketika kueri mulai berjalan pada data dunia nyata dan banyak baris dikembalikan.

Akibatnya, biasanya perlu diberikan pemikiran untuk membatasi jumlah hasil:

var blogs25 = context.Posts
    .Where(p => p.Title.StartsWith("A"))
    .Take(25)
    .ToList();

Minimal, UI Anda dapat menampilkan pesan yang menunjukkan bahwa lebih banyak baris mungkin ada dalam database (dan memungkinkan pengambilannya dengan cara lain). Solusi lengkap akan menerapkan penomoran halaman, di mana UI Anda hanya menampilkan sejumlah baris pada satu waktu, dan memungkinkan pengguna untuk maju ke halaman berikutnya sesuai kebutuhan; lihat bagian berikutnya untuk detail selengkapnya tentang cara menerapkannya secara efisien.

Penomoran halaman yang efisien

Penomoran halaman mengacu pada pengambilan hasil di halaman, bukan sekaligus; ini biasanya dilakukan untuk hasil yang besar, di mana antarmuka pengguna ditampilkan yang memungkinkan pengguna untuk menavigasi ke halaman hasil berikutnya atau sebelumnya. Cara umum untuk menerapkan penomoran halaman dengan database adalah dengan menggunakan Skip operator dan Take (OFFSET dan LIMIT di SQL); sementara ini adalah implementasi intuitif, ini juga cukup tidak efisien. Untuk penomoran halaman yang memungkinkan pemindahan satu halaman sekaligus (bukan melompat ke halaman arbitrer), pertimbangkan untuk menggunakan halaman keyset sebagai gantinya.

Untuk informasi selengkapnya, lihat halaman dokumentasi tentang penomoran halaman.

Dalam database relasional, semua entitas terkait dimuat dengan memperkenalkan WAN dalam satu kueri.

SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Post] AS [p] ON [b].[BlogId] = [p].[BlogId]
ORDER BY [b].[BlogId], [p].[PostId]

Jika blog umum memiliki beberapa posting terkait, baris untuk posting ini akan menduplikasi informasi blog. Duplikasi ini menyebabkan apa yang disebut masalah "ledakan kartesius". Karena lebih banyak hubungan satu-ke-banyak dimuat, jumlah data duplikat dapat tumbuh dan berdampak buruk pada performa aplikasi Anda.

EF memungkinkan menghindari efek ini melalui penggunaan "kueri terpisah", yang memuat entitas terkait melalui kueri terpisah. Untuk informasi selengkapnya, baca dokumentasi tentang kueri terpisah dan tunggal.

Catatan

Implementasi kueri terpisah saat ini menjalankan roundtrip untuk setiap kueri. Kami berencana untuk meningkatkan ini di masa depan, dan menjalankan semua kueri dalam satu perjalanan pulang pergi.

Disarankan untuk membaca halaman khusus tentang entitas terkait sebelum melanjutkan dengan bagian ini.

Ketika berhadapan dengan entitas terkait, kita biasanya tahu terlebih dahulu apa yang perlu kita muat: contoh umumnya adalah memuat sekumpulan Blog tertentu, bersama dengan semua Posting mereka. Dalam skenario ini, selalu lebih baik menggunakan pemuatan yang bersemangat, sehingga EF dapat mengambil semua data yang diperlukan dalam satu perjalanan pulang-pergi. Fitur yang difilter termasuk juga memungkinkan Anda membatasi entitas terkait mana yang ingin Anda muat, sambil menjaga proses pemuatan tetap bersemangat dan oleh karena itu dapat dilakukan dalam satu perjalanan pulang-pergi:

using (var context = new BloggingContext())
{
    var filteredBlogs = context.Blogs
        .Include(
            blog => blog.Posts
                .Where(post => post.BlogId == 1)
                .OrderByDescending(post => post.Title)
                .Take(5))
        .ToList();
}

Dalam skenario lain, kita mungkin tidak tahu entitas terkait mana yang akan kita butuhkan sebelum kita mendapatkan entitas utamanya. Misalnya, ketika memuat beberapa Blog, kita mungkin perlu berkonsultasi dengan beberapa sumber data lain - mungkin layanan web - untuk mengetahui apakah kita tertarik dengan Posting Blog tersebut. Dalam kasus ini, pemuatan eksplisit atau malas dapat digunakan untuk mengambil entitas terkait secara terpisah, dan mengisi navigasi Posting Blog. Perhatikan bahwa karena metode ini tidak bersemangat, metode ini memerlukan perjalanan pulang pergi tambahan ke database, yang merupakan sumber perlambatan; tergantung pada skenario spesifik Anda, mungkin lebih efisien untuk selalu memuat semua Posting, daripada menjalankan roundtrips tambahan dan secara selektif hanya mendapatkan Postingan yang Anda butuhkan.

Waspadalah terhadap pemuatan malas

Pemuatan malas sering kali tampak seperti cara yang sangat berguna untuk menulis logika database, karena EF Core secara otomatis memuat entitas terkait dari database saat diakses oleh kode Anda. Ini menghindari pemuatan entitas terkait yang tidak diperlukan (seperti pemuatan eksplisit), dan tampaknya membebaskan programmer dari harus berurusan dengan entitas terkait sama sekali. Namun, pemuatan malas sangat rentan untuk menghasilkan perjalanan pulang-pergi tambahan yang tidak diperlukan yang dapat memperlambat aplikasi.

Pertimbangkan hal berikut:

foreach (var blog in context.Blogs.ToList())
{
    foreach (var post in blog.Posts)
    {
        Console.WriteLine($"Blog {blog.Url}, Post: {post.Title}");
    }
}

Ini tampaknya potongan kode yang tidak bersalah berulang melalui semua blog dan posting mereka, mencetaknya. Mengaktifkan pengelogan pernyataan EF Core mengungkapkan hal-hal berikut:

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [b].[BlogId], [b].[Rating], [b].[Url]
      FROM [Blogs] AS [b]
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (5ms) [Parameters=[@__p_0='1'], CommandType='Text', CommandTimeout='30']
      SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Title]
      FROM [Post] AS [p]
      WHERE [p].[BlogId] = @__p_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[@__p_0='2'], CommandType='Text', CommandTimeout='30']
      SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Title]
      FROM [Post] AS [p]
      WHERE [p].[BlogId] = @__p_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[@__p_0='3'], CommandType='Text', CommandTimeout='30']
      SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Title]
      FROM [Post] AS [p]
      WHERE [p].[BlogId] = @__p_0

... and so on

Apa yang terjadi di sini? Mengapa semua kueri ini dikirim untuk perulangan sederhana di atas? Dengan pemuatan malas, Posting Blog hanya (malas) yang dimuat ketika properti Postingannya diakses; akibatnya, setiap iterasi di foreach bagian dalam memicu kueri database tambahan, dalam perjalanan pulang-perginya sendiri. Akibatnya, setelah kueri awal memuat semua blog, kita kemudian memiliki kueri lain per blog, memuat semua postingannya; ini kadang-kadang disebut masalah N+1 , dan dapat menyebabkan masalah performa yang sangat signifikan.

Dengan asumsi kita akan membutuhkan semua posting blog, masuk akal untuk menggunakan pemuatan bersemangat di sini sebagai gantinya. Kita dapat menggunakan operator Sertakan untuk melakukan pemuatan, tetapi karena kita hanya memerlukan URL Blog (dan kita hanya boleh memuat apa yang diperlukan). Jadi kita akan menggunakan proyeksi sebagai gantinya:

foreach (var blog in context.Blogs.Select(b => new { b.Url, b.Posts }).ToList())
{
    foreach (var post in blog.Posts)
    {
        Console.WriteLine($"Blog {blog.Url}, Post: {post.Title}");
    }
}

Ini akan membuat EF Core mengambil semua Blog - bersama dengan Posting mereka - dalam satu kueri. Dalam beberapa kasus, mungkin juga berguna untuk menghindari efek ledakan kartesius dengan menggunakan kueri terpisah.

Peringatan

Karena pemuatan malas membuatnya sangat mudah untuk secara tidak sengaja memicu masalah N+1, disarankan untuk menghindarinya. Pemuatan yang bersemangat atau eksplisit membuatnya sangat jelas dalam kode sumber ketika roundtrip database terjadi.

Buffering dan streaming

Buffering mengacu pada pemuatan semua hasil kueri Anda ke dalam memori, sedangkan streaming berarti bahwa EF memberi aplikasi satu hasil setiap kali, tidak pernah berisi seluruh hasil dalam memori. Pada prinsipnya, persyaratan memori kueri streaming diperbaiki - mereka sama apakah kueri mengembalikan 1 baris atau 1000; kueri buffering, di sisi lain, membutuhkan lebih banyak memori semakin banyak baris yang dikembalikan. Untuk kueri yang menghasilkan hasil besar, ini bisa menjadi faktor performa penting.

Apakah buffer atau aliran kueri bergantung pada caranya dievaluasi:

// ToList and ToArray cause the entire resultset to be buffered:
var blogsList = context.Posts.Where(p => p.Title.StartsWith("A")).ToList();
var blogsArray = context.Posts.Where(p => p.Title.StartsWith("A")).ToArray();

// Foreach streams, processing one row at a time:
foreach (var blog in context.Posts.Where(p => p.Title.StartsWith("A")))
{
    // ...
}

// AsEnumerable also streams, allowing you to execute LINQ operators on the client-side:
var doubleFilteredBlogs = context.Posts
    .Where(p => p.Title.StartsWith("A")) // Translated to SQL and executed in the database
    .AsEnumerable()
    .Where(p => SomeDotNetMethod(p)); // Executed at the client on all database results

Jika kueri Anda hanya mengembalikan beberapa hasil, maka Anda mungkin tidak perlu khawatir tentang hal ini. Namun, jika kueri Anda mungkin mengembalikan sejumlah besar baris, ada baiknya memberikan pemikiran untuk streaming alih-alih buffering.

Catatan

Hindari menggunakan ToList atau ToArray jika Anda berniat menggunakan operator LINQ lain pada hasilnya - ini tidak perlu buffer semua hasil ke dalam memori. Gunakan AsEnumerable sebagai gantinya.

Buffering internal oleh EF

Dalam situasi tertentu, EF akan menyangga hasil secara internal, terlepas dari bagaimana Anda mengevaluasi kueri Anda. Dua kasus di mana ini terjadi adalah:

  • Ketika strategi eksekusi coba lagi ada. Ini dilakukan untuk memastikan hasil yang sama dikembalikan jika kueri dicoba kembali nanti.
  • Saat kueri terpisah digunakan, hasil dari semua tetapi kueri terakhir di-buffer - kecuali MARS (Beberapa Set Hasil Aktif) diaktifkan di SQL Server. Ini karena biasanya tidak mungkin memiliki beberapa hasil kueri yang aktif secara bersamaan.

Perhatikan bahwa buffering internal ini terjadi selain buffering apa pun yang Anda sebabkan melalui operator LINQ. Misalnya, jika Anda menggunakan ToList pada kueri dan strategi eksekusi coba lagi ada, hasilnya dimuat ke dalam memori dua kali: sekali secara internal oleh EF, dan sekali oleh ToList.

Pelacakan, tanpa pelacakan, dan resolusi identitas

Disarankan untuk membaca halaman khusus tentang pelacakan dan tanpa pelacakan sebelum melanjutkan dengan bagian ini.

EF melacak instans entitas secara default, sehingga perubahan pada instans tersebut terdeteksi dan bertahan saat SaveChanges dipanggil. Efek lain dari kueri pelacakan adalah EF mendeteksi apakah instans telah dimuat untuk data Anda, dan akan secara otomatis mengembalikan instans terlacak tersebut daripada mengembalikan yang baru; ini disebut resolusi identitas. Dari perspektif performa, pelacakan perubahan berarti sebagai berikut:

  • EF secara internal mempertahankan kamus instans terlacak. Saat data baru dimuat, EF memeriksa kamus untuk melihat apakah instans sudah dilacak untuk kunci entitas tersebut (resolusi identitas). Pemeliharaan kamus dan pencarian membutuhkan waktu saat memuat hasil kueri.
  • Sebelum menyerahkan instans yang dimuat ke aplikasi, rekam jepret EF instans tersebut dan menyimpan rekam jepret secara internal. Ketika SaveChanges dipanggil, instans aplikasi dibandingkan dengan rekam jepret untuk menemukan perubahan yang akan dipertahankan. Rekam jepret membutuhkan lebih banyak memori, dan proses rekam jepret itu sendiri membutuhkan waktu; terkadang dimungkinkan untuk menentukan perilaku rekam jepret yang berbeda, mungkin lebih efisien melalui pembanding nilai, atau menggunakan proksi pelacakan perubahan untuk melewati proses rekam jepret sama sekali (meskipun itu datang dengan serangkaian kekurangannya sendiri).

Dalam skenario baca-saja di mana perubahan tidak disimpan kembali ke database, overhead di atas dapat dihindari dengan menggunakan kueri tanpa pelacakan. Namun, karena kueri tanpa pelacakan tidak melakukan resolusi identitas, baris database yang dirujuk oleh beberapa baris lain yang dimuat akan terwujud sebagai instans yang berbeda.

Untuk mengilustrasikan, asumsikan kita memuat sejumlah besar Posting dari database, serta Blog yang dirujuk oleh setiap Postingan. Jika 100 Postingan terjadi untuk mereferensikan Blog yang sama, kueri pelacakan mendeteksi ini melalui resolusi identitas, dan semua instans Post akan merujuk instans Blog yang diduplikasi yang sama. Kueri tanpa pelacakan, sebaliknya, menduplikasi Blog yang sama 100 kali - dan kode aplikasi harus ditulis dengan sesuai.

Berikut adalah hasil untuk tolok ukur membandingkan perilaku pelacakan vs. tanpa pelacakan untuk kueri yang memuat 10 Blog dengan 20 Posting masing-masing. Kode sumber tersedia di sini, jangan ragu untuk menggunakannya sebagai dasar untuk pengukuran Anda sendiri.

Metode NumBlogs NumPostsPerBlog Rata-rata Kesalahan StdDev Median Rasio RasioSD Gen 0 Gen 1 Gen 2 Dialokasikan
AsTracking 10 20 1,414.7 kami 27.20 kami 45.44 kami 1.405,5 kami 1 0.00 60.5469 13.6719 - 380,11 KB
AsNoTracking 10 20 993.3 kami 24.04 kami 65.40 kami 966.2 kami 0,71 0,05 37.1094 6.8359 - 232,89 KB

Akhirnya, dimungkinkan untuk melakukan pembaruan tanpa overhead pelacakan perubahan, dengan menggunakan kueri tanpa pelacakan dan kemudian melampirkan instans yang dikembalikan ke konteks, menentukan perubahan mana yang akan dilakukan. Ini mentransfer beban pelacakan perubahan dari EF kepada pengguna, dan hanya boleh dicoba jika overhead pelacakan perubahan telah terbukti tidak dapat diterima melalui pembuatan profil atau tolok ukur.

Menggunakan kueri SQL

Dalam beberapa kasus, SQL yang lebih dioptimalkan ada untuk kueri Anda, yang tidak dihasilkan EF. Ini dapat terjadi ketika konstruksi SQL adalah ekstensi khusus untuk database Anda yang tidak didukung, atau hanya karena EF belum menerjemahkannya. Dalam kasus ini, menulis SQL dengan tangan dapat memberikan peningkatan performa yang substansial, dan EF mendukung beberapa cara untuk melakukan ini.

  • Gunakan kueri SQL langsung dalam kueri Anda, misalnya melalui FromSqlRaw. EF bahkan memungkinkan Anda menyusun SQL dengan kueri LINQ reguler, memungkinkan Anda untuk mengekspresikan hanya sebagian kueri di SQL. Ini adalah teknik yang baik ketika SQL hanya perlu digunakan dalam satu kueri di basis kode Anda.
  • Tentukan fungsi yang ditentukan pengguna (UDF), lalu panggil dari kueri Anda. Perhatikan bahwa EF memungkinkan UDF untuk mengembalikan resultset lengkap - ini dikenal sebagai fungsi bernilai tabel (TVF) - dan juga memungkinkan pemetaan DbSet ke fungsi, membuatnya terlihat seperti tabel lain.
  • Tentukan tampilan database dan kueri darinya dalam kueri Anda. Perhatikan bahwa tidak seperti fungsi, tampilan tidak dapat menerima parameter.

Catatan

SQL mentah umumnya harus digunakan sebagai upaya terakhir, setelah memastikan bahwa EF tidak dapat menghasilkan SQL yang Anda inginkan, dan ketika performa cukup penting bagi kueri yang diberikan untuk membenarkannya. Menggunakan SQL mentah membawa kerugian pemeliharaan yang cukup besar.

Pemrograman asinkron

Sebagai aturan umum, agar aplikasi Anda dapat diskalakan, penting untuk selalu menggunakan API asinkron daripada yang sinkron (misalnya SaveChangesAsync daripada SaveChanges). API sinkron memblokir utas selama durasi I/O database, meningkatkan kebutuhan utas dan jumlah sakelar konteks utas yang harus terjadi.

Untuk informasi selengkapnya, lihat halaman tentang pemrograman asinkron.

Peringatan

Hindari mencampur kode sinkron dan asinkron dalam aplikasi yang sama - sangat mudah untuk secara tidak sengaja memicu masalah kelaparan kumpulan utas yang halus.

Peringatan

Implementasi asinkron Microsoft.Data.SqlClient sayangnya memiliki beberapa masalah yang diketahui (misalnya #593, #601, dan lainnya). Jika Anda melihat masalah performa yang tidak terduga, coba gunakan eksekusi perintah sinkronisasi, terutama saat berhadapan dengan teks besar atau nilai biner.

Sumber Daya Tambahan:

  • Lihat halaman topik performa tingkat lanjut untuk topik tambahan yang terkait dengan kueri yang efisien.
  • Lihat bagian performa halaman dokumentasi perbandingan null untuk beberapa praktik terbaik saat membandingkan nilai nullable.