Metode untuk mengoptimalkan kueri LINQ di C # .NET

pengantar


Pada artikel ini kami membahas beberapa teknik optimasi LINQ-inquiry .
Berikut adalah beberapa pendekatan optimasi kode yang terkait dengan permintaan LINQ .

Diketahui bahwa LINQ (Language-Integrated Query) adalah bahasa yang sederhana dan nyaman untuk menanyakan sumber data.

Dan LINQ to SQL adalah teknologi akses data dalam DBMS. Ini adalah alat yang ampuh untuk bekerja dengan data, di mana kueri dibangun melalui bahasa deklaratif, yang kemudian akan dikonversi menjadi kueri SQL oleh platform dan dikirim ke server database yang sudah siap untuk dieksekusi. Dalam kasus kami, yang kami maksud dengan DBMS adalah MS SQL Server .

Namun, kueri LINQ tidak dikonversi ke kueri SQL yang ditulis secara optimal yang dapat ditulis oleh DBA berpengalaman dengan semua nuansa mengoptimalkan kueri SQL :

  1. koneksi optimal ( GABUNG ) dan penyaringan hasil ( DI MANA )
  2. banyak nuansa dalam penggunaan senyawa dan kondisi kelompok
  3. banyak variasi dalam mengganti kondisi IN dengan EXISTS dan NOT IN , <> dengan EXISTS
  4. caching hasil antara melalui tabel sementara, CTE, variabel tabel
  5. menggunakan klausa ( OPTION ) dengan instruksi dan petunjuk tabel DENGAN (...)
  6. penggunaan tampilan indeks, sebagai salah satu cara untuk menyingkirkan pembacaan data yang berlebihan dalam sampel

Hambatan kinerja utama dari kueri SQL yang dihasilkan saat menyusun kueri LINQ adalah:

  1. konsolidasi seluruh mekanisme pemilihan data dalam satu permintaan
  2. duplikasi blok kode yang identik, yang pada akhirnya mengarah ke beberapa pembacaan data tambahan
  3. kelompok kondisi multikomponen (logis "dan" dan "atau") - DAN dan ATAU , menggabungkan dalam kondisi yang sulit, mengarah pada fakta bahwa pengoptimal, memiliki indeks nonclustered yang sesuai, oleh bidang yang diperlukan, akhirnya mulai memindai dengan indeks klaster ( INDEX SCAN ) berdasarkan grup kondisi
  4. bersarang mendalam dari subqueries membuatnya sangat bermasalah untuk mem-parsing pernyataan SQL dan mem-parsing rencana kueri dari pengembang dan DBA

Metode optimasi


Sekarang kami beralih langsung ke metode optimasi.

1) Pengindeksan tambahan


Yang terbaik adalah mempertimbangkan filter pada tabel sampel utama, karena sangat sering seluruh permintaan dibangun di sekitar satu atau dua tabel utama (aplikasi-orang-operasi) dan dengan serangkaian kondisi standar (IsClosed, Cancelled, Diaktifkan, Status). Penting bagi sampel yang diidentifikasi untuk membuat indeks yang sesuai.

Solusi ini masuk akal ketika memilih dari bidang-bidang ini secara signifikan membatasi set yang dikembalikan ke kueri.

Misalnya, kami memiliki 500.000 aplikasi. Namun, hanya ada 2.000 entri aktif. Kemudian indeks yang dipilih dengan benar akan menyelamatkan kita dari INDEX SCAN pada tabel besar dan akan memungkinkan kita untuk dengan cepat memilih data melalui indeks yang tidak berkerumun.

Kurangnya indeks juga dapat dideteksi melalui permintaan untuk menguraikan rencana kueri atau mengumpulkan statistik untuk tampilan sistemMS SQL Server :

  1. sys.dm_db_missing_index_groups
  2. sys.dm_db_missing_index_group_stats
  3. sys.dm_db_missing_index_details

Semua data tampilan berisi informasi tentang indeks yang hilang, dengan pengecualian indeks spasial.

Namun, indeks dan caching seringkali merupakan metode untuk menangani efek dari query LINQ dan SQL yang ditulis dengan buruk .

Seperti yang ditunjukkan oleh praktik kehidupan yang keras untuk bisnis, seringkali penting untuk menerapkan fitur bisnis pada tanggal tertentu. Dan oleh karena itu, seringkali pertanyaan yang banyak ditelusuri dengan caching.

Ini sebagian dibenarkan, karena pengguna tidak selalu membutuhkan data terbaru dan tingkat respons antarmuka pengguna yang dapat diterima terjadi.

Pendekatan ini memungkinkan Anda untuk menyelesaikan kebutuhan bisnis, tetapi pada akhirnya mengurangi efisiensi sistem informasi, hanya menunda solusi dari masalah.

Perlu juga diingat bahwa dalam proses pencarian yang diperlukan untuk menambahkan indeks baru, proposal optimasi MS SQL mungkin salah, termasuk dalam kondisi berikut:

  1. jika indeks dengan set bidang yang sama sudah ada
  2. jika bidang dalam tabel tidak dapat diindeks karena pembatasan pengindeksan (lebih lanjut tentang ini dijelaskan di sini ).

2) Menggabungkan atribut menjadi satu atribut baru


Terkadang beberapa bidang dari tabel yang sama di mana sekelompok kondisi terjadi dapat digantikan oleh pengenalan satu bidang baru.

Ini terutama berlaku untuk bidang negara, yang menurut jenisnya biasanya bitwise atau integer.

Contoh:

IsClosed = 0 AND Cancelled = 0 AND Diaktifkan = 0 diganti oleh Status = 1 .

Di sini Anda memasukkan Status atribut bilangan bulat, yang disediakan dengan mengisi status ini di tabel. Langkah selanjutnya adalah mengindeks atribut baru ini.

Ini adalah solusi mendasar untuk masalah kinerja, karena kami meminta data tanpa perhitungan yang tidak perlu.

3) Materialisasi pengiriman


Sayangnya, kueri LINQ tidak bisa langsung menggunakan tabel temporer, CTE, dan variabel tabel.

Namun, ada cara lain untuk mengoptimalkan kasus ini - ini adalah tampilan yang diindeks.

Sekelompok kondisi (dari contoh di atas) IsClosed = 0 AND Cancelled = 0 AND Enabled = 0 (atau serangkaian kondisi serupa lainnya) menjadi pilihan yang baik untuk menggunakannya dalam tampilan yang diindeks, menyimpan sebagian kecil data dari kumpulan besar.

Tetapi ada sejumlah batasan saat mewujudkan tampilan:

  1. menggunakan subqueries, klausa EXISTS harus diganti menggunakan JOIN
  2. Tidak dapat menggunakan klausa UNION , UNION ALL , EXCEPTION , INTERSECT
  3. Anda tidak dapat menggunakan petunjuk meja dan klausa OPTION
  4. tidak ada kemampuan untuk bekerja dengan siklus
  5. tidak mungkin menampilkan data dalam satu tampilan dari tabel yang berbeda

Penting untuk diingat bahwa manfaat nyata menggunakan tampilan indeks sebenarnya hanya dapat diperoleh dengan mengindeksnya.

Tetapi ketika menggunakan tampilan, indeks ini mungkin tidak digunakan, dan untuk menggunakannya secara eksplisit, Anda harus menentukan WITH (NOEXPAND) .

Karena tidak mungkin untuk menentukan petunjuk tabel dalam kueri LINQ , jadi Anda harus membuat representasi lain - "pembungkus" dari formulir berikut:

CREATE VIEW _ AS SELECT * FROM MAT_VIEW WITH (NOEXPAND);

4) Menggunakan fungsi tabel


Seringkali dalam kueri LINQ, blok subquery besar atau blok yang menggunakan representasi dengan struktur kompleks membentuk kueri akhir dengan struktur eksekusi yang sangat kompleks dan tidak optimal.

Manfaat utama menggunakan fungsi tabel dalam kueri LINQ :

  1. Kemampuan, seperti dalam kasus tampilan, untuk menggunakan dan menentukan sebagai objek, tetapi Anda dapat melewati serangkaian parameter input:
    DARI FUNGSI (@ param1, @ param2 ...)
    pada akhirnya, Anda dapat mencapai pengambilan sampel data yang fleksibel
  2. Saat menggunakan fungsi tabel, tidak ada batasan yang kuat seperti dalam kasus tampilan indeks yang dijelaskan di atas:

    1. :
      LINQ .
      .
      ,
    2. , , :

      • ( )
      • UNION EXISTS

  3. OPTION , OPTION(MAXDOP N), . :

    • OPTION (RECOMPILE)
    • , OPTION (FORCE ORDER)

    OPTION .
  4. :
    ( ), .
    , , WHERE (a, b, c).

    a = 0 and b = 0.

    , c .

    a = 0 and b = 0 , .

    Di sini fungsi tabel mungkin merupakan opsi yang lebih baik.

    Juga, fungsi tabel lebih dapat diprediksi dan konstan dalam waktu eksekusi.

Contohnya


Mari kita pertimbangkan contoh implementasi menggunakan contoh dari database Pertanyaan.

Ada kueri SELECT yang menggabungkan beberapa tabel dan menggunakan satu tampilan (OperativeQuestions), yang memverifikasi melalui email afiliasi (melalui EXISTS ) dari "Permintaan Aktif" ([Pertanyaan Operatif]):

Permintaan No. 1
(@p__linq__0 nvarchar(4000))SELECT
1 AS [C1],
[Extent1].[Id] AS [Id],
[Join2].[Object_Id] AS [Object_Id],
[Join2].[ObjectType_Id] AS [ObjectType_Id],
[Join2].[Name] AS [Name],
[Join2].[ExternalId] AS [ExternalId]
FROM [dbo].[Questions] AS [Extent1]
INNER JOIN (SELECT [Extent2].[Object_Id] AS [Object_Id],
[Extent2].[Question_Id] AS [Question_Id], [Extent3].[ExternalId] AS [ExternalId],
[Extent3].[ObjectType_Id] AS [ObjectType_Id], [Extent4].[Name] AS [Name]
FROM [dbo].[ObjectQuestions] AS [Extent2]
INNER JOIN [dbo].[Objects] AS [Extent3] ON [Extent2].[Object_Id] = [Extent3].[Id]
LEFT OUTER JOIN [dbo].[ObjectTypes] AS [Extent4] 
ON [Extent3].[ObjectType_Id] = [Extent4].[Id] ) AS [Join2] 
ON [Extent1].[Id] = [Join2].[Question_Id]
WHERE ([Extent1].[AnswerId] IS NULL) AND (0 = [Extent1].[Exp]) AND ( EXISTS (SELECT
1 AS [C1]
FROM [dbo].[OperativeQuestions] AS [Extent5]
WHERE (([Extent5].[Email] = @p__linq__0) OR (([Extent5].[Email] IS NULL) 
AND (@p__linq__0 IS NULL))) AND ([Extent5].[Id] = [Extent1].[Id])
));


Pandangan memiliki struktur yang agak rumit: memiliki subquery joins dan penggunaan sorting DISTINCT , yang dalam kasus umum adalah operasi yang agak intensif sumber daya.

Pilihan sekitar sepuluh ribu catatan dari OperativeQuestions.

Masalah utama dari kueri ini adalah bahwa untuk catatan dari kueri eksternal, subquery internal dilakukan pada tampilan [OperativeQuestions], yang seharusnya membatasi sampel output (melalui EXISTS ) hingga ratusan catatan untuk [Email] = @ p__linq__0 .

Dan mungkin tampak bahwa subquery harus sekali menghitung catatan dengan [Email] = @ p__linq__0, dan kemudian beberapa dari ratusan catatan ini harus bergabung dengan Pertanyaan ID c, dan kueri akan cepat.

Bahkan, semua tabel terhubung secara seri: Id Pertanyaan dan Id dari OperativeQuestions diperiksa kepatuhannya, dan Email difilter.

Bahkan, permintaan berfungsi dengan semua puluhan ribu catatan OperativeQuestions, dan Anda hanya perlu data yang menarik di Email.

Teks tampilan OperativeQuestions:

Permintaan No. 2
 
CREATE VIEW [dbo].[OperativeQuestions]
AS
SELECT DISTINCT Q.Id, USR.email AS Email
FROM            [dbo].Questions AS Q INNER JOIN
                         [dbo].ProcessUserAccesses AS BPU ON BPU.ProcessId = CQ.Process_Id 
OUTER APPLY
                     (SELECT   1 AS HasNoObjects
                      WHERE   NOT EXISTS
                                    (SELECT   1
                                     FROM     [dbo].ObjectUserAccesses AS BOU
                                     WHERE   BOU.ProcessUserAccessId = BPU.[Id] AND BOU.[To] IS NULL)
) AS BO INNER JOIN
                         [dbo].Users AS USR ON USR.Id = BPU.UserId
WHERE        CQ.[Exp] = 0 AND CQ.AnswerId IS NULL AND BPU.[To] IS NULL 
AND (BO.HasNoObjects = 1 OR
              EXISTS (SELECT   1
                           FROM   [dbo].ObjectUserAccesses AS BOU INNER JOIN
                                      [dbo].ObjectQuestions AS QBO 
                                                  ON QBO.[Object_Id] =BOU.ObjectId
                               WHERE  BOU.ProcessUserAccessId = BPU.Id 
                               AND BOU.[To] IS NULL AND QBO.Question_Id = CQ.Id));


Representasi pemetaan asli dalam DbContext (EF Core 2)
public class QuestionsDbContext : DbContext
{
    //...
    public DbQuery<OperativeQuestion> OperativeQuestions { get; set; }
    //...
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Query<OperativeQuestion>().ToView("OperativeQuestions");
    }
}


Query LINQ asli
var businessObjectsData = await context
    .OperativeQuestions
    .Where(x => x.Email == Email)
    .Include(x => x.Question)
    .Select(x => x.Question)
    .SelectMany(x => x.ObjectQuestions,
                (x, bo) => new
                {
                    Id = x.Id,
                    ObjectId = bo.Object.Id,
                    ObjectTypeId = bo.Object.ObjectType.Id,
                    ObjectTypeName = bo.Object.ObjectType.Name,
                    ObjectExternalId = bo.Object.ExternalId
                })
    .ToListAsync();


Dalam kasus khusus ini, solusi untuk masalah ini dipertimbangkan tanpa perubahan infrastruktur, tanpa memperkenalkan tabel terpisah dengan hasil yang siap pakai ("Permintaan Aktif"), di mana suatu mekanisme akan diperlukan untuk mengisi data dan tetap memperbaruinya.

Meskipun ini adalah solusi yang baik, ada opsi lain untuk mengoptimalkan tugas ini.

Tujuan utamanya adalah untuk me-cache entri oleh [Email] = @ p__linq__0 dari tampilan OperativeQuestions.

Kami memasukkan fungsi tabel [dbo]. [OperativeQuestionsUserMail] dalam database.

Mengirim Email sebagai parameter input, kami mendapatkan kembali tabel nilai:

Permintaan No. 3

CREATE FUNCTION [dbo].[OperativeQuestionsUserMail]
(
    @Email  nvarchar(4000)
)
RETURNS
@tbl TABLE
(
    [Id]           uniqueidentifier,
    [Email]      nvarchar(4000)
)
AS
BEGIN
        INSERT INTO @tbl ([Id], [Email])
        SELECT Id, @Email
        FROM [OperativeQuestions]  AS [x] WHERE [x].[Email] = @Email;
     
    RETURN;
END


Ini mengembalikan tabel nilai dengan struktur data yang telah ditentukan.

Agar kueri untuk OperativeQuestionsUserMail menjadi optimal, untuk memiliki rencana kueri yang optimal, diperlukan struktur yang ketat, dan bukan KEMBALI MEJA SEBAGAI KEMBALI ...

Dalam hal ini, Permintaan 1 yang diinginkan dikonversi menjadi Permintaan 4:

Permintaan No. 4
(@p__linq__0 nvarchar(4000))SELECT
1 AS [C1],
[Extent1].[Id] AS [Id],
[Join2].[Object_Id] AS [Object_Id],
[Join2].[ObjectType_Id] AS [ObjectType_Id],
[Join2].[Name] AS [Name],
[Join2].[ExternalId] AS [ExternalId]
FROM (
    SELECT Id, Email FROM [dbo].[OperativeQuestionsUserMail] (@p__linq__0)
) AS [Extent0]
INNER JOIN [dbo].[Questions] AS [Extent1] ON([Extent0].Id=[Extent1].Id)
INNER JOIN (SELECT [Extent2].[Object_Id] AS [Object_Id], [Extent2].[Question_Id] AS [Question_Id], [Extent3].[ExternalId] AS [ExternalId], [Extent3].[ObjectType_Id] AS [ObjectType_Id], [Extent4].[Name] AS [Name]
FROM [dbo].[ObjectQuestions] AS [Extent2]
INNER JOIN [dbo].[Objects] AS [Extent3] ON [Extent2].[Object_Id] = [Extent3].[Id]
LEFT OUTER JOIN [dbo].[ObjectTypes] AS [Extent4] 
ON [Extent3].[ObjectType_Id] = [Extent4].[Id] ) AS [Join2] 
ON [Extent1].[Id] = [Join2].[Question_Id]
WHERE ([Extent1].[AnswerId] IS NULL) AND (0 = [Extent1].[Exp]);


Memetakan tampilan dan fungsi dalam DbContext (EF Core 2)
public class QuestionsDbContext : DbContext
{
    //...
    public DbQuery<OperativeQuestion> OperativeQuestions { get; set; }
    //...
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Query<OperativeQuestion>().ToView("OperativeQuestions");
    }
}
 
public static class FromSqlQueries
{
    public static IQueryable<OperativeQuestion> GetByUserEmail(this DbQuery<OperativeQuestion> source, string Email)
        => source.FromSql($"SELECT Id, Email FROM [dbo].[OperativeQuestionsUserMail] ({Email})");
}


Kueri LINQ terakhir
var businessObjectsData = await context
    .OperativeQuestions
    .GetByUserEmail(Email)
    .Include(x => x.Question)
    .Select(x => x.Question)
    .SelectMany(x => x.ObjectQuestions,
                (x, bo) => new
                {
                    Id = x.Id,
                    ObjectId = bo.Object.Id,
                    ObjectTypeId = bo.Object.ObjectType.Id,
                    ObjectTypeName = bo.Object.ObjectType.Name,
                    ObjectExternalId = bo.Object.ExternalId
                })
    .ToListAsync();


Urutan waktu eksekusi menurun dari 200-800 ms, menjadi 2-20 ms., Dll., Yaitu sepuluh kali lebih cepat.

Jika kita mengambil lebih banyak rata-rata, maka alih-alih 350 ms kita mendapat 8 ms.

Dari plus yang jelas, kami juga mendapatkan:

  1. pengurangan umum dalam beban bacaan,
  2. Mengurangi kemungkinan memblokir secara signifikan
  3. pengurangan waktu pemblokiran rata-rata ke nilai yang dapat diterima

Kesimpulan


Optimalisasi dan fine-tuning panggilan ke database MS SQL melalui LINQ adalah masalah yang bisa diselesaikan.

Dalam pekerjaan ini, perawatan dan konsistensi sangat penting.

Di awal proses:

  1. perlu memeriksa data yang digunakan kueri (nilai, tipe data yang dipilih)
  2. mengindeks data ini dengan benar
  3. periksa kebenaran kondisi penghubung antara tabel

Pada iterasi berikutnya, optimasi mengungkapkan:

  1. dasar permintaan dan filter permintaan utama
  2. mengulangi blok permintaan serupa dan kondisi berpotongan
  3. di SSMS atau GUI lain untuk SQL Server , kueri SQL itu sendiri dioptimalkan (alokasi penyimpanan data perantara, membangun kueri yang dihasilkan menggunakan toko ini (mungkin ada beberapa))
  4. pada tahap terakhir, dengan mengambil query SQL yang dihasilkan sebagai dasar , struktur kueri LINQ dibangun kembali

Akibatnya, kueri LINQ yang dihasilkan harus identik dalam struktur dengan kueri SQL optimal yang diidentifikasi dari paragraf 3.

Ucapan Terima Kasih


Terima kasih banyak kepada kolega jobgemws dan alex_ozrdari Fortis untuk membantu dengan artikel ini.

All Articles