عرض نتائج البحث ومشكلات الأداء

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

صورة

خيار الترحيل # 1


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


افترض أن التطبيق يستخدم قاعدة بيانات علائقية. في هذه الحالة ، لعرض المعلومات في هذا النموذج ، ستحتاج إلى تنفيذ استفساري SQL:

  • احصل على صفوف للصفحة الحالية.
  • احسب إجمالي عدد الأسطر التي تطابق معايير البحث - وهذا ضروري لعرض الصفحات.

ضع في اعتبارك الاستعلام الأول باستخدام قاعدة بيانات MS SQL التجريبية AdventureWorks لخادم 2016 كمثال . لهذا الغرض ، سوف نستخدم الجدول Sales.SalesOrderHeader:

SELECT * FROM Sales.SalesOrderHeader
ORDER BY OrderDate DESC
OFFSET 0 ROWS
FETCH NEXT 50 ROWS ONLY

يعرض الاستعلام أعلاه أول 50 طلبًا من قائمة مرتبة ترتيبًا تنازليًا حسب تاريخ الإضافة ، وبعبارة أخرى ، آخر 50 طلبًا.

يعمل بسرعة على أساس تجريبي ، لكن دعونا نلقي نظرة على خطة التنفيذ وإحصاءات الإدخال / الإخراج:


Table 'SalesOrderHeader'. Scan count 1, logical reads 698, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

يمكنك الحصول على إحصائيات الإدخال / الإخراج لكل طلب عن طريق تشغيل الأمر SET STATISTICS IO ON في وقت تشغيل الاستعلام.

كما ترى من خطة التنفيذ ، فإن الأكثر كثافة في استخدام الموارد هو فرز جميع الصفوف في الجدول المصدر حسب تاريخ إضافتها. والمشكلة هي أنه كلما زاد عدد الصفوف في الجدول ، كلما كان التصنيف "أصعب". من الناحية العملية ، يجب تجنب مثل هذه الحالات ، لذا أضف المؤشر في تاريخ الإضافة وانظر ما إذا كان استهلاك الموارد قد تغير:


Table 'SalesOrderHeader'. Scan count 1, logical reads 165, physical reads 0, read-ahead reads 5, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

من الواضح ، أنها أصبحت أفضل بكثير. ولكن هل تم حل جميع المشاكل؟ دعنا نغير طلب البحث للأوامر حيث تتجاوز التكلفة الإجمالية للسلع 100 دولار:

SELECT * FROM Sales.SalesOrderHeader
WHERE SubTotal > 100
ORDER BY OrderDate DESC
OFFSET 0 ROWS
FETCH NEXT 50 ROWS ONLY


Table 'SalesOrderHeader'. Scan count 1, logical reads 1081, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

لدينا موقف مضحك: خطة الاستعلام أسوأ قليلاً من سابقتها ، لكن العدد الفعلي للقراءات المنطقية هو ضعف ضعف مسح الجدول بالكامل. هناك طريقة للخروج - إذا قمنا بعمل السعر المركب من المؤشر الحالي وقمنا بإضافة السعر الإجمالي للسلع كحقل ثان ، ثم مرة أخرى نحصل على 165 قراءة منطقية:

CREATE INDEX IX_SalesOrderHeader_OrderDate_SubTotal on Sales.SalesOrderHeader(OrderDate, SubTotal);

يمكن أن تستمر هذه السلسلة من الأمثلة لفترة طويلة ، لكن الفكرتين الرئيسيتين اللتين أريد التعبير عنهما هنا هما:

  • يمكن أن تؤثر إضافة أي معيار جديد أو ترتيب فرز إلى استعلام البحث بشكل كبير على سرعة تنفيذه.
  • ولكن إذا احتجنا إلى طرح جزء من البيانات فقط ، وليس كل النتائج التي تناسب شروط البحث ، فهناك العديد من الطرق لتحسين هذا الاستعلام.

الآن دعنا ننتقل إلى الاستعلام الثاني ، المذكور في البداية - إلى الحساب الذي يحسب عدد السجلات التي تستوفي معايير البحث. خذ نفس المثال - العثور على الطلبات التي تكلف أكثر من 100 دولار:

SELECT COUNT(1) FROM Sales.SalesOrderHeader
WHERE SubTotal > 100

في وجود المؤشر المركب المشار إليه أعلاه ، نحصل على:


Table 'SalesOrderHeader'. Scan count 1, logical reads 698, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

حقيقة أن الاستعلام يمر عبر الفهرس بأكمله ليس مفاجئًا ، حيث إن حقل الإجمالي الفرعي ليس في الموضع الأول ، لذلك لا يمكن للاستعلام استخدامه. يتم حل المشكلة عن طريق إضافة فهرس آخر إلى حقل المجموع الفرعي ، ونتيجة لذلك يعطي بالفعل 48 قراءة منطقية فقط.

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

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

خيار الترحيل رقم 2


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


في الواقع ، فقط حقيقة أنه لا توجد طريقة للذهاب إلى أرقام صفحات محددة قد تغيرت ، والآن لا يحتاج هذا الجدول إلى معرفة عدد منهم يمكن عرضها. ولكن السؤال الذي يطرح نفسه - كيف يعرف الجدول ما إذا كانت هناك بيانات للصفحة التالية (من أجل عرض الارتباط "التالي" بشكل صحيح)؟

الجواب بسيط للغاية: يمكنك طرح سجل واحد من قاعدة البيانات أكثر مما تحتاج إلى عرضه ، وسيظهر وجود هذا السجل "الإضافي" ما إذا كان هناك الجزء التالي. وبالتالي ، للحصول على صفحة واحدة من البيانات ، ستحتاج إلى إكمال استعلام واحد فقط ، مما يحسن الأداء بشكل كبير ويجعل دعم هذه الوظيفة أسهل. في ممارستي ، كانت هناك حالة عندما أدى رفض حساب العدد الإجمالي للسجلات إلى تسريع إخراج النتائج بمقدار 4-5 مرات.

بالنسبة لهذا النهج ، هناك العديد من الخيارات لواجهة المستخدم: الأمرين "back" و "forward" ، كما في المثال أعلاه ، الزر "تحميل المزيد" ، الذي يضيف ببساطة جزءًا جديدًا إلى النتائج المعروضة ، "التمرير اللامتناهي" ، والذي يعمل على مبدأ "تحميل المزيد" "، لكن الإشارة للحصول على الجزء التالي هي أن يقوم المستخدم بالتمرير عبر جميع النتائج المعروضة حتى النهاية. مهما كان الحل البصري ، يبقى مبدأ أخذ عينات البيانات كما هو.

الفروق الدقيقة في تنفيذ الترحيل


في جميع الأمثلة على الاستعلامات أعلاه ، يتم استخدام نهج "الإزاحة + الرقم" ، عندما يشير الاستعلام نفسه إلى ترتيب خطوط النتيجة وعدد الصفوف التي يجب إرجاعها. أولاً ، ضع في اعتبارك أفضل طريقة لتنظيم نقل المعلمات في هذه الحالة. في الواقع ، قابلت عدة طرق:

  • الرقم التسلسلي للصفحة المطلوبة (pageIndex) ، حجم الصفحة (pageSize).
  • الرقم التسلسلي للسجل الأول المراد إرجاعه (startIndex) ، الحد الأقصى لعدد السجلات نتيجة (عدد).
  • الرقم التسلسلي للسجل الأول المراد إرجاعه (startIndex) ، الرقم التسلسلي لآخر سجل يتم إرجاعه (endIndex).

للوهلة الأولى ، قد يبدو أن هذا أمر أساسي للغاية بحيث لا يوجد فرق. لكن الأمر ليس كذلك - الخيار الأكثر ملاءمة وعالمية هو الخيار الثاني (startIndex ، count). هناك عدة أسباب لذلك:

  • +1 , , pageIndex pageSize . , 50 . , , . «+1» , , 1 51, — 51 101 .. 51 pageIndex, 52 102 .. , — «» , .
  • الخيار الثالث لا معنى له على الإطلاق ، نظرًا لتنفيذ الاستعلامات في معظم قواعد البيانات ، ستظل بحاجة إلى نقل الكمية ، وليس فهرس السجل الأخير. دع الطرح من startIndex من endIndex وعملية حسابية أولية ، ولكنه غير ضروري هنا.

الآن يجب أن نصف أوجه القصور في تنفيذ الترحيل من خلال "تعويض + الكمية":

  • سيكون الحصول على كل صفحة تالية أكثر تكلفة وأبطأ من الصفحة السابقة ، لأن قاعدة البيانات ستظل بحاجة إلى مراجعة جميع السجلات "من البداية" وفقًا لمعايير البحث والفرز ، ثم التوقف عند الجزء المطلوب.
  • لا يمكن لجميع DBMSs دعم هذا النهج.

هناك بدائل ، لكنها أيضًا غير كاملة. يُطلق على أول هذه الطرق "ترحيل مجموعة المفاتيح" أو "طريقة البحث" وتتكون مما يلي: بعد استلام جزء ، يمكنك تذكر قيم الحقول في السجل الأخير على الصفحة ، ثم استخدامها للحصول على الجزء التالي. على سبيل المثال ، قمنا بتنفيذ الطلب التالي:

SELECT * FROM Sales.SalesOrderHeader
ORDER BY OrderDate DESC
OFFSET 0 ROWS
FETCH NEXT 50 ROWS ONLY

وفي السجل الأخير حصلنا على قيمة تاريخ الطلب '2014-06-29'. بعد ذلك ، للحصول على الصفحة التالية ، يمكنك محاولة القيام بذلك:

SELECT * FROM Sales.SalesOrderHeader
WHERE OrderDate < '2014-06-29'
ORDER BY OrderDate DESC
OFFSET 0 ROWS
FETCH NEXT 50 ROWS ONLY

تكمن المشكلة في أن OrderDate هو حقل غير فريد ، ومن المرجح أن يتخطى الشرط المذكور أعلاه الكثير من الخطوط المطلوبة. لجعل هذا الطلب لا لبس فيه ، تحتاج إلى إضافة حقل فريد للشرط (افترض أن 75074 هي القيمة الأخيرة للمفتاح الأساسي من الجزء الأول):

SELECT * FROM Sales.SalesOrderHeader
WHERE (OrderDate = '2014-06-29' AND SalesOrderID < 75074)
   OR (OrderDate < '2014-06-29')
ORDER BY OrderDate DESC, SalesOrderID DESC
OFFSET 0 ROWS
FETCH NEXT 50 ROWS ONLY

سيعمل هذا الخيار بشكل صحيح ، ولكن في الحالة العامة سيكون من الصعب تحسينه ، لأن الحالة تحتوي على عامل التشغيل OR. إذا زادت قيمة المفتاح الأساسي مع نمو OrderDate ، فيمكن تبسيط الحالة عن طريق ترك الفلتر فقط بواسطة SalesOrderID. ولكن إذا لم يكن هناك ارتباط صارم بين قيم المفتاح الأساسي والحقل الذي يتم فرز النتيجة من خلاله - في معظم DBMS ، لا يمكن تجنب هذا OR. الاستثناء المعروف بالنسبة لي هو PostgreSQL ، حيث يتم دعم المقارنة tuple بالكامل ، ويمكن كتابة الشرط أعلاه كـ "WHERE (OrderDate، SalesOrderID) <('2014-06-29'، 75074). إذا كان لديك مفتاح مركب مع هذين الحقلين ، فيجب أن يكون الطلب المماثل سهلًا إلى حد ما.

يمكن العثور على الطريقة البديلة الثانية ، على سبيل المثال ، في واجهة برمجة التطبيقات المرنة ElasticSearch أوCosmos DB - عندما يقوم الاستعلام بالإضافة إلى البيانات بإرجاع معرف خاص يمكنك من خلاله الحصول على الدفعة التالية من البيانات. إذا كان هذا المعرف له عمر غير محدود (كما هو الحال في Comsos DB) ، فهذه طريقة رائعة لتنفيذ الترحيل مع الانتقال المتسلسل بين الصفحات (الخيار رقم 2 المذكور أعلاه). عيوبه المحتملة: لا يتم دعم جميع DBMSs ؛ قد يكون للمعرف المستلم للجزء التالي عمر محدود ، وهو غير مناسب عمومًا لتنفيذ تفاعل المستخدم (مثل واجهة برمجة التطبيقات ومرونة البحث المرنة).

تصفية معقدة


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


ما هي فكرة البحث الوجهي؟ في ذلك ، لكل عنصر عامل تصفية ، يظهر عدد السجلات المطابقة لهذا المعيار مع مراعاة عوامل التصفية المحددة في جميع الفئات الأخرى .

على سبيل المثال ، إذا حددنا فئة الدراجات واللون الأسود في هذا المثال ، فسيعرض الجدول الدراجات السوداء فقط ، ولكن في نفس الوقت:

  • لكل معيار من معايير مجموعة "الفئات" ، سيتم عرض عدد المنتجات من هذه الفئة باللون الأسود.
  • لكل معيار من مجموعة الألوان ، سيتم عرض عدد الدراجات من هذا اللون.

هنا مثال على إخراج النتيجة لمثل هذه الشروط:


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

تخيل الآن كيف يمكن تنفيذ ذلك على أساس علائقي. ستتطلب كل مجموعة من المعايير ، مثل الفئة واللون ، طلبًا منفصلاً:

SELECT pc.ProductCategoryID, pc.Name, COUNT(1) FROM Production.Product p
  INNER JOIN Production.ProductSubcategory ps ON p.ProductSubcategoryID = ps.ProductSubcategoryID
  INNER JOIN Production.ProductCategory pc ON ps.ProductCategoryID = pc.ProductCategoryID
WHERE p.Color = 'Black'
GROUP BY pc.ProductCategoryID, pc.Name
ORDER BY COUNT(1) DESC


SELECT Color, COUNT(1) FROM Production.Product p
  INNER JOIN Production.ProductSubcategory ps ON p.ProductSubcategoryID = ps.ProductSubcategoryID
WHERE ps.ProductCategoryID = 1 --Bikes
GROUP BY Color
ORDER BY COUNT(1) DESC


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

عادة ، بعد هذه البيانات ، يقدمون لي بعض الحلول ، وهي:

  • اجمع بين جميع الكميات في استعلام واحد. من الناحية الفنية ، هذا ممكن باستخدام الكلمة الأساسية UNION ، إلا أن هذا لن يساعد كثيرًا في الأداء - على أي حال ، سيتعين على قاعدة البيانات تنفيذ كل جزء من الأجزاء من البداية.
  • . , . , . , 10 «», 5 . «» , -. 9- , , . 50 , , 250. , . , 5-10 . , , - , , ( ).

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

  • «» . , , , , — «1425 , ?» , «». «». , , . -. , , .
  • search engine , Solr, ElasticSearch, Sphinx . «» . , , — . , search engine , : , , ; search engine . — . « ». «» , , . , - transactional outbox .


  1. — , . «» «» — , :
    • — .
    • , , , . - — .
  2. , — #2.
  3. faceted search, :
    • .
    • search engine Solr, ElasticSearch, Sphinx . , , .
  4. faceted search . , , .
  5. SQL , , , ( «» ). , — «». , .

All Articles