الأربعاء، 09 سبتمبر 2026 القاهرة 33.8°C

LINQ OrderBy وThenBy في C#: الترتيب المتعدد وأخطاء شائعة

عندما تحتاج إلى ترتيب بيانات في C# باستخدام LINQ، يكون OrderBy وThenBy هما الأساس لبناء ترتيب متعدد المستويات بصورة واضحة. الفكرة بسيطة: OrderBy يحدد معيار الترتيب الأول، ثم يضيف ThenBy معيارًا ثانيًا عندما تتساوى العناصر في المعيار الأول.

المشكلة أن بعض الأخطاء تبدو صحيحة عند قراءة الكود، مثل استخدام OrderBy مرتين متتاليتين، لكنها تغيّر معنى الترتيب بالكامل. هذا الدليل يوضح الأنماط الصحيحة، الترتيب التصاعدي والتنازلي، التعامل مع النصوص وnull، والفرق العملي بين LINQ على الذاكرة وLINQ الذي يترجم إلى استعلام قاعدة بيانات.

مثال سريع: الترتيب حسب القسم ثم الاسم

لنفترض أن لدينا قائمة موظفين:

var employees = new[]
{
    new Employee("Mona", "Engineering", 31),
    new Employee("Ahmed", "Engineering", 28),
    new Employee("Salma", "Sales", 29),
    new Employee("Omar", "Sales", 26)
};

إذا أردنا الترتيب حسب القسم أولًا ثم الاسم داخل كل قسم:

var result = employees
    .OrderBy(x => x.Department)
    .ThenBy(x => x.Name);

هنا Department هو المفتاح الأساسي، وName هو المفتاح الثانوي.

ماذا يفعل OrderBy؟

OrderBy يبدأ ترتيبًا جديدًا باستخدام المفتاح الذي تحدده:

var byAge = employees.OrderBy(x => x.Age);

النتيجة تكون من الأصغر إلى الأكبر. للنصوص، يستخدم LINQ المقارن الافتراضي للنوع ما لم تمرر comparer مختلفًا.

ماذا يفعل ThenBy؟

ThenBy لا يبدأ ترتيبًا جديدًا؛ بل يضيف مستوى إضافيًا إلى الترتيب الموجود. لذلك لا يُستخدم منطقيًا إلا بعد OrderBy أو OrderByDescending، أو بعد مستوى ترتيب سابق.

var result = employees
    .OrderBy(x => x.Department)
    .ThenBy(x => x.Age);

سيتم تجميع الترتيب أولًا حسب القسم، وعندما يكون موظفان في القسم نفسه يُستخدم العمر لتحديد ترتيبهما.

الخطأ الشائع: استخدام OrderBy مرتين

هذا الكود لا يعني «رتب بالقسم ثم الاسم»:

var result = employees
    .OrderBy(x => x.Department)
    .OrderBy(x => x.Name);

الـOrderBy الثاني ينشئ ترتيبًا أساسيًا جديدًا. إذا كنت تريد الحفاظ على الترتيب الأول وإضافة معيار ثانٍ، استخدم:

var result = employees
    .OrderBy(x => x.Department)
    .ThenBy(x => x.Name);

قاعدة عملية سهلة: معيار أول = OrderBy، المعايير التالية = ThenBy.

OrderByDescending وThenByDescending

يمكنك المزج بين الاتجاهات بحرية. مثلًا: القسم أبجديًا، ثم الأكبر سنًا أولًا:

var result = employees
    .OrderBy(x => x.Department)
    .ThenByDescending(x => x.Age);

أو ترتيب الأقسام تنازليًا، ثم الاسم تصاعديًا:

var result = employees
    .OrderByDescending(x => x.Department)
    .ThenBy(x => x.Name);

أكثر من مستويين للترتيب

يمكنك إضافة أي عدد منطقي من مستويات الترتيب:

var result = employees
    .OrderBy(x => x.Department)
    .ThenByDescending(x => x.Age)
    .ThenBy(x => x.Name);

يصبح المعنى هنا: رتب حسب القسم، ثم العمر تنازليًا داخل القسم، ثم الاسم عند تساوي العمر.

الترتيب المستقر ولماذا يهم

في LINQ to Objects، عمليات الترتيب من هذا النوع تحافظ على الترتيب النسبي للعناصر التي تتساوى مفاتيحها. هذا يجعل إضافة مستويات ThenBy ذات معنى متوقعًا، لأن كل مستوى يحسم التعادل في المستوى السابق.

لكن لا تعتمد على «الترتيب الأصلي» كقاعدة أعمال خفية. إذا كنت تحتاج نتيجة حتمية دائمًا، أضف مفتاحًا أخيرًا فريدًا مثل Id:

var result = employees
    .OrderBy(x => x.Department)
    .ThenBy(x => x.Name)
    .ThenBy(x => x.Id);

ترتيب النصوص وحساسية حالة الأحرف

عند العمل مع IEnumerable<T> يمكنك تمرير comparer مناسب بدل الاعتماد على المقارنة الافتراضية. مثال لترتيب الأسماء بدون حساسية لحالة الأحرف وفق الثقافة الحالية:

var result = employees
    .OrderBy(x => x.Name, StringComparer.CurrentCultureIgnoreCase);

اختيار comparer مهم إذا كان سلوك اللغة والثقافة جزءًا من المتطلبات. لا تفترض أن ترتيب النصوص المناسب لبيانات تقنية هو نفسه المناسب لأسماء مستخدمين بلغات مختلفة.

ماذا عن null؟

إذا كان المفتاح يمكن أن يكون null، فحدد السلوك المطلوب بدل تركه ضمنيًا. مثال لو أردت وضع القيم الفارغة في النهاية ثم ترتيب القيم الموجودة:

var result = items
    .OrderBy(x => x.Name is null)
    .ThenBy(x => x.Name);

المعيار الأول يعطي false للعناصر التي لديها اسم وtrue للـnull، فتظهر القيم الموجودة أولًا.

Deferred Execution: متى يحدث الترتيب فعليًا؟

مع LINQ to Objects، بناء التعبير لا يعني عادة تنفيذ الترتيب فورًا. الاستعلام يُنفذ عند enumerating النتيجة، مثل استخدام foreach أو ToList().

var query = employees
    .OrderBy(x => x.Department)
    .ThenBy(x => x.Name);

var list = query.ToList();

إذا تغير المصدر قبل التنفيذ، قد تتغير النتيجة. وإذا كنت ستقرأ النتيجة عدة مرات وتريد snapshot ثابتة، materialize مرة واحدة عند النقطة المناسبة.

IEnumerable أم IQueryable؟

هذه نقطة مهمة جدًا عند استخدام Entity Framework أو مزود LINQ آخر.

مع IEnumerable<T>، الترتيب يتم بواسطة كود .NET على البيانات الموجودة في الذاكرة. أما مع IQueryable<T>، فالتعبير قد يترجمه provider إلى لغة أخرى مثل SQL.

var query = db.Employees
    .OrderBy(x => x.Department)
    .ThenBy(x => x.Name);

var page = await query
    .Skip(20)
    .Take(20)
    .ToListAsync();

في هذه الحالة، راجع SQL الناتج عند وجود مشكلة أداء. ليس كل comparer أو method يعمل بالطريقة نفسها مع كل provider، لأن المزود يجب أن يستطيع ترجمة التعبير.

رتب قبل Skip وTake عند عمل Pagination

الـpagination تحتاج ترتيبًا واضحًا ومستقرًا. لا تعتمد على الترتيب الطبيعي للصفوف في قاعدة البيانات.

var page = await db.Employees
    .OrderBy(x => x.CreatedAt)
    .ThenBy(x => x.Id)
    .Skip((pageNumber - 1) * pageSize)
    .Take(pageSize)
    .ToListAsync();

إضافة Id كمفتاح ثانٍ تساعد عندما تتساوى قيمة CreatedAt لعدة سجلات، وتقلل احتمالات ظهور ترتيب غير حتمي بين الصفحات.

ملاحظات الأداء

  • لا ترتب البيانات عدة مرات إذا كنت تستطيع بناء ترتيب واحد واضح باستخدام ThenBy.
  • لا تستدعِ ToList() مبكرًا على IQueryable ثم ترتب آلاف السجلات في الذاكرة دون سبب.
  • في قواعد البيانات، الأعمدة المستخدمة كثيرًا في الفلترة والترتيب قد تحتاج تصميم index مناسب حسب الاستعلامات الحقيقية.
  • افحص execution plan وSQL الفعلي عند وجود استعلام بطيء بدل استنتاج السبب من كود LINQ فقط.
  • إذا كنت تحتاج أعلى N عناصر فقط، تأكد أن التصفية والترتيب وTake تتم في المصدر قدر الإمكان.

ترتيب ديناميكي حسب اختيار المستخدم

لو كان المستخدم يختار عمود الترتيب، اجعل الاختيارات صريحة بدل تمرير أسماء خصائص عشوائية:

IQueryable<Employee> query = db.Employees;

query = sortBy switch
{
    "name" => descending
        ? query.OrderByDescending(x => x.Name)
        : query.OrderBy(x => x.Name),

    "age" => descending
        ? query.OrderByDescending(x => x.Age)
        : query.OrderBy(x => x.Age),

    _ => query.OrderBy(x => x.Id)
};

هذا أوضح وأسهل في المراجعة والاختبار من بناء expressions نصية غير منضبطة، ويعطيك default ordering معروفًا.

أخطاء متكررة مع LINQ Sorting

  • استخدام OrderBy ثانية بدل ThenBy.
  • تنفيذ ToList() قبل الفلترة والترتيب عند العمل مع قاعدة البيانات.
  • Pagination بدون ترتيب حتمي.
  • الاعتماد على مقارنة نصية افتراضية بينما المتطلبات تحتاج ثقافة أو حساسية أحرف مختلفة.
  • استخدام method داخل IQueryable لا يستطيع provider ترجمتها.
  • ترتيب result ضخمة في الذاكرة بينما قاعدة البيانات تستطيع تنفيذ العمل بكفاءة أكبر.

قائمة مراجعة قبل اعتماد الاستعلام

  1. ما هو المفتاح الأساسي للترتيب؟ استخدم له OrderBy.
  2. هل توجد حالات تعادل؟ أضف ThenBy مناسبًا.
  3. هل كل مستوى تصاعدي أم تنازلي؟ اجعل الاتجاه صريحًا.
  4. هل النتيجة تحتاج ترتيبًا حتميًا؟ أضف مفتاحًا فريدًا أخيرًا.
  5. هل الاستعلام IEnumerable أم IQueryable؟
  6. هل يوجد Skip/Take؟ تأكد من الترتيب قبلهما.
  7. إذا كان الاستعلام بطيئًا، هل راجعت SQL والخطة والفهارس؟

إذا كنت تبني أساسك في C# والمنصة نفسها، ابدأ من دليل تعلم .NET في 2026. ولتحسين طريقة تحليل المسألة قبل كتابة الاستعلام، راجع التفكير البرمجي وحل المشكلات.

الخلاصة

استخدم OrderBy لبدء الترتيب، ثم ThenBy لإضافة المعايير التالية. اجعل اتجاه كل مستوى واضحًا، أضف مفتاحًا حتميًا عند pagination، وانتبه للفرق بين التنفيذ في الذاكرة والترجمة عبر IQueryable. بهذه القواعد البسيطة تتجنب معظم أخطاء LINQ Sorting وتكتب استعلامات أسهل للفهم والصيانة.

كلمات البحث
LINQ C# .NET OrderBy ThenBy
تعليقات
جارٍ التحميل...