طرق تحسين استعلامات LINQ في C # .NET

المقدمة


في هذه المقالة ناقشنا بعض تقنيات التحسين LINQ .
فيما يلي بعض مناهج تحسين التعليمات البرمجية المتعلقة باستعلامات LINQ .

من المعروف أن LINQ (الاستعلام المتكامل مع اللغة) هي لغة بسيطة ومريحة للاستعلام عن مصدر البيانات.

و LINQ إلى SQL هي تقنية الوصول إلى البيانات في نظام إدارة قواعد البيانات. هذه أداة قوية للعمل مع البيانات ، حيث يتم إنشاء الاستعلامات من خلال لغة تعريفية ، والتي سيتم تحويلها بعد ذلك إلى استعلامات SQL بواسطة النظام الأساسي وإرسالها إلى خادم قاعدة البيانات للتنفيذ. في حالتنا ، نعني بـ MSMS خادم MS SQL .

ومع ذلك ، لا يتم تحويل استعلامات LINQ إلى استعلامات SQL المكتوبة بشكل مثالي والتي يمكن أن يكتبها DBA ذو خبرة مع جميع الفروق الدقيقة في تحسين استعلامات SQL :

  1. الاتصالات المثلى ( JOIN ) وتصفية النتائج ( أين )
  2. العديد من الفروق الدقيقة في استخدام المركبات وظروف المجموعة
  3. العديد من الاختلافات في استبدال شروط IN بـ EXISTS و NOT IN ، <> بـ EXISTS
  4. التخزين المؤقت للنتائج من خلال الجداول المؤقتة ، CTE ، متغيرات الجدول
  5. باستخدام بند ( OPTION ) مع تعليمات وتلميحات الجدول مع (...)
  6. استخدام طرق العرض المفهرسة ، كوسيلة للتخلص من القراءات الزائدة للبيانات في العينات

اختناقات الأداء الرئيسية لاستعلامات SQL الناتجة عند تجميع استعلامات LINQ هي:

  1. دمج آلية اختيار البيانات بالكامل في طلب واحد
  2. تكرار كتل متطابقة من التعليمات البرمجية ، مما يؤدي في النهاية إلى قراءات إضافية متعددة للبيانات
  3. مجموعات من الشروط المتعددة المكونات (منطقي "و" و "أو") - يؤدي استخدام AND و OR ، معًا في ظروف صعبة ، إلى حقيقة أن المُحسِّن ، الذي يحتوي على فهارس غير مجمعة مناسبة ، حسب الحقول الضرورية ، يبدأ في النهاية في الفحص بواسطة فهرس الكتلة ( INDEX SCAN ) حسب مجموعة الشرط
  4. التداخل العميق في الاستعلامات الفرعية يجعل من الصعب للغاية تحليل عبارات SQL وتحليل خطة الاستعلام من المطورين و DBAs

طرق التحسين


الآن ننتقل مباشرة إلى طرق التحسين.

1) فهرسة إضافية


من الأفضل النظر في الفلاتر في جداول أخذ العينات الرئيسية ، حيث غالبًا ما يتم بناء الاستعلام بالكامل حول جدول واحد أو جدولين رئيسيين (التطبيقات - الأشخاص - العمليات) ومع مجموعة قياسية من الشروط (IsClosed ، ملغاة ، ممكّنة ، الحالة). من المهم للعينات المحددة إنشاء الفهارس المقابلة.

يكون هذا الحل منطقيًا عند الاختيار من هذه الحقول بشكل كبير يحد المجموعة التي تم إرجاعها إلى الاستعلام.

على سبيل المثال ، لدينا 500000 تطبيق. ومع ذلك ، لا يوجد سوى 2000 إدخال نشط. ثم سيوفرنا الفهرس المختار بشكل صحيح من INDEX SCAN على جدول كبير وسيسمح لنا بتحديد البيانات بسرعة من خلال فهرس غير مجمع.

يمكن أيضًا الكشف عن عدم وجود الفهارس من خلال المطالبات بتحليل خطط الاستعلام أو جمع الإحصائيات لطرق عرض النظامخادم MS SQL :

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

تحتوي جميع بيانات العرض على معلومات حول الفهارس المفقودة ، باستثناء الفهارس المكانية.

ومع ذلك، والفهارس والتخزين المؤقت وغالبا ما تكون أساليب التعامل مع آثار سيئة مكتوبة الاستفسارات LINQ و الاستفسارات SQL .

كما تظهر الممارسة القاسية للحياة في العمل ، غالبًا ما يكون من المهم تنفيذ ميزات العمل بحلول تاريخ معين. وبالتالي ، غالبًا ما يتم وضع الاستعلامات الثقيلة في الخلفية باستخدام التخزين المؤقت.

هذا مبرر جزئيًا ، حيث لا يحتاج المستخدم دائمًا إلى أحدث البيانات ويحدث مستوى مقبول من استجابة واجهة المستخدم.

يسمح لك هذا النهج بحل احتياجات العمل ، ولكنه يقلل في النهاية من كفاءة نظام المعلومات ، ويؤخر ببساطة حل المشكلات.

من الجدير بالذكر أيضًا أنه في عملية البحث اللازمة لإضافة فهارس جديدة ، قد تكون مقترحات تحسين MS SQL غير صحيحة ، بما في ذلك في الظروف التالية:

  1. إذا كانت الفهارس بمجموعة مماثلة من الحقول موجودة بالفعل
  2. إذا كانت الحقول في الجدول لا يمكن فهرستها بسبب قيود الفهرسة (المزيد عن هذا موضح هنا ).

2) دمج السمات في سمة جديدة


في بعض الأحيان يمكن استبدال بعض الحقول من نفس الجدول التي تحدث بها مجموعة من الشروط بإدخال حقل جديد واحد.

وينطبق هذا بشكل خاص على حقول الحالة ، والتي تكون حسب النوع إما أحادية أو صحيحة.

مثال:

IsClosed = 0 AND ملغى = 0 AND ممكّن = 0 يتم استبداله بالحالة = 1 .

هنا تقوم بإدخال حالة سمة العدد الصحيح ، والتي يتم توفيرها عن طريق ملء هذه الحالات في الجدول. الخطوة التالية هي فهرسة هذه السمة الجديدة.

هذا هو الحل الأساسي لمشكلة الأداء ، لأننا نطلب البيانات دون حسابات غير ضرورية.

3) تجسيد التقديم


لسوء الحظ ، لا يمكن لاستعلامات LINQ استخدام الجداول المؤقتة و CTEs ومتغيرات الجدول مباشرة.

ومع ذلك ، هناك طريقة أخرى لتحسين هذه الحالة - وهي طرق العرض المفهرسة.

مجموعة من الشروط (من المثال أعلاه) IsClosed = 0 AND Canceled = 0 AND Enabled = 0 (أو مجموعة من الشروط المماثلة الأخرى) تصبح خيارًا جيدًا لاستخدامها في طريقة عرض مفهرسة ، وتخزين شريحة صغيرة من البيانات من مجموعة كبيرة.

ولكن هناك عدد من القيود عند تجسيد وجهة نظر:

  1. باستخدام الاستعلامات الفرعية ، يجب استبدال عبارات EXISTS باستخدام JOIN
  2. لا يمكن استخدام شروط UNION و UNION ALL و EXCEPTION و INTERSECT
  3. لا يمكنك استخدام تلميحات الجدول وعبارات OPTION
  4. عدم القدرة على العمل مع الدورات
  5. من المستحيل عرض البيانات في طريقة عرض واحدة من جداول مختلفة

من المهم أن تتذكر أن الفوائد الحقيقية لاستخدام طريقة عرض مفهرسة لا يمكن الحصول عليها إلا بفهرستها.

ولكن عند استدعاء طريقة عرض ، قد لا يتم استخدام هذه الفهارس ، ولاستخدامها بشكل صريح ، يجب عليك تحديد WITH (NOEXPAND) .

نظرًا لأنه من المستحيل تحديد تلميحات الجدول في استعلامات LINQ ، لذلك يجب عليك إنشاء تمثيل آخر - "غلاف" من النموذج التالي:

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

4) استخدام وظائف الجدول


غالبًا في استعلامات LINQ ، تشكل كتل الاستعلامات الفرعية الكبيرة أو الكتل التي تستخدم التمثيلات ذات البنية المعقدة الاستعلام النهائي ببنية تنفيذ معقدة للغاية وليست مثالية.

الفوائد الرئيسية لاستخدام وظائف الجدول في استعلامات LINQ :

  1. القدرة ، كما هو الحال في طرق العرض ، على الاستخدام
    والتحديد ككائن ، ولكن يمكنك تمرير مجموعة من معلمات الإدخال: FROM FUNCTION (@ param1، @ param2 ...)
    في النهاية ، يمكنك الحصول على عينات بيانات مرنة
  2. عند استخدام وظيفة جدول ، لا توجد قيود قوية كما هو الحال في طرق العرض المفهرسة الموضحة أعلاه:

    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 , .

    هنا قد تكون وظيفة الجدول خيارًا أفضل.

    كما أن وظيفة الجدول أكثر قابلية للتنبؤ بها وثابتة في وقت التنفيذ.

أمثلة


دعنا نفكر في تنفيذ مثال باستخدام مثال قاعدة بيانات الأسئلة.

يوجد استعلام SELECT الذي يدمج عدة جداول ويستخدم طريقة عرض واحدة (OperativeQuestions) ، والتي تتحقق عن طريق البريد الإلكتروني من الانتماء (عبر EXISTS ) لـ "الاستعلامات النشطة" ([OperativeQuestions]):

طلب رقم 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])
));


يحتوي العرض على بنية معقدة نوعًا ما: فهو يحتوي على روابط استعلام فرعي واستخدام الفرز DISTINCT ، والذي يعد في الحالة العامة عملية كثيفة الموارد إلى حد ما.

مجموعة مختارة من حوالي عشرة آلاف سجل من OperativeQuestions.

المشكلة الرئيسية لهذا الاستعلام هي أنه بالنسبة للسجلات من استعلام خارجي ، يتم تنفيذ استعلام فرعي داخلي في طريقة عرض [OperativeQuestions] ، والتي يجب أن تحدد عينة الإخراج (عبر EXISTS ) إلى مئات السجلات لـ [Email] = @ p__linq__0 .

وقد يبدو أن الاستعلام الفرعي يجب أن يحسب السجلات بمجرد [Email] = @ p__linq__0 ، وبعد ذلك يجب توصيل هذين المئات من السجلات بواسطة أسئلة المعرّف c ، وسيكون الاستعلام سريعًا.

في الواقع ، جميع الجداول متصلة في سلسلة: يتم التحقق من أسئلة الأسئلة والمعرف من OperativeQuestions للامتثال ، ويتم تصفية البريد الإلكتروني.

في الواقع ، يعمل الطلب مع جميع عشرات الآلاف من سجلات OperativeQuestions ، وتحتاج فقط إلى البيانات ذات الأهمية على البريد الإلكتروني.

عرض OperativeQuestions النص:

طلب رقم 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));


تمثيل الخرائط الأصلي في 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");
    }
}


استعلام LINQ الأصلي
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();


في هذه الحالة بالذات ، يتم النظر في حل لهذه المشكلة بدون تغييرات في البنية التحتية ، دون تقديم جدول منفصل مع نتائج جاهزة ("الاستعلامات النشطة") ، والتي ستكون هناك حاجة إلى آلية لملء بياناتها وتحديثها باستمرار.

على الرغم من أن هذا حل جيد ، هناك خيار آخر لتحسين هذه المهمة.

الهدف الرئيسي هو تخزين الإدخالات من خلال [Email] = @ p__linq__0 من عرض OperativeQuestions.

نقوم بإدخال دالة الجدول [dbo]. [OperativeQuestionsUserMail] في قاعدة البيانات.

إرسال البريد الإلكتروني كمعلمة إدخال ، نعود إلى جدول القيم:

طلب رقم 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


يؤدي ذلك إلى إرجاع جدول القيم ببنية بيانات محددة مسبقًا.

من أجل أن تكون الاستعلامات إلى OperativeQuestionsUserMail هي الأمثل ، ولها خطط استعلام مثالية ، يلزم وجود بنية صارمة ، وليس إعادة جدول TABLE AS RETURN ...

في هذه الحالة ، يتم تحويل الطلب 1 المطلوب إلى الطلب 4:

طلب رقم 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]);


تعيين طرق العرض والوظائف في 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})");
}


استعلام LINQ النهائي
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();


انخفض ترتيب وقت التنفيذ من 200-800 مللي ثانية ، إلى 2-20 مللي ثانية ، إلخ ، أي أسرع بعشر مرات.

إذا أخذنا متوسطًا أكثر ، فبدلاً من 350 مللي ثانية حصلنا على 8 مللي ثانية.

من الإيجابيات الواضحة ، نحصل أيضًا على:

  1. انخفاض عام في حمل القراءة ،
  2. انخفاض كبير في احتمالية الحجب
  3. تخفيض متوسط ​​وقت الحجب إلى قيم مقبولة

استنتاج


يعد تحسين وضبط المكالمات لقاعدة بيانات MS SQL من خلال LINQ مشكلة يمكن حلها.

في هذا العمل ، الرعاية والاتساق مهمان جدًا.

في بداية العملية:

  1. من الضروري التحقق من البيانات التي يعمل بها الاستعلام (القيم وأنواع البيانات المحددة)
  2. فهرسة هذه البيانات بشكل صحيح
  3. تحقق من صحة شروط الربط بين الجداول

في التكرار التالي ، يكشف التحسين:

  1. يتم تعريف أساس الطلب ومرشح الطلب الرئيسي
  2. تكرار كتل الاستعلام المتشابهة والظروف المتقاطعة
  3. في SSMS أو واجهة المستخدم الرسومية الأخرى لـ SQL Server ، يتم تحسين استعلام SQL نفسه (تخصيص مخزن بيانات وسيط ، وبناء الاستعلام الناتج باستخدام هذا المخزن (قد يكون هناك العديد))
  4. في المرحلة الأخيرة ، مع الأخذ في الاعتبار استعلام SQL الناتج كأساس ، يتم إعادة بناء بنية استعلام LINQ

ونتيجة لذلك ، يجب أن يصبح استعلام LINQ الناتج متطابقًا في البنية مع استعلام SQL الأمثل المحدد من الفقرة 3.

شكر وتقدير


جزيل الشكر للزملاء jobgemws و alex_ozrمن Fortis للمساعدة في هذا المقال.

All Articles