ما هي "مشكلة تحديد N + 1" في ORM (رسم الخرائط العلائقية للكائنات)؟

StackOverflow https://stackoverflow.com/questions/97197

  •  01-07-2019
  •  | 
  •  

سؤال

يُشار عمومًا إلى "مشكلة تحديد N+1" على أنها مشكلة في مناقشات تعيين الكائنات العلائقية (ORM)، وأنا أفهم أن لها علاقة بالحاجة إلى إجراء الكثير من استعلامات قاعدة البيانات لشيء يبدو بسيطًا في الكائن عالم.

هل أحد عنده شرح أكثر تفصيلاً للمشكلة؟

هل كانت مفيدة؟

المحلول

لنفترض أن لديك مجموعة من Car الكائنات (صفوف قاعدة البيانات)، ولكل منها Car لديه مجموعة من Wheel الكائنات (أيضًا الصفوف).بعبارة أخرى، Car -> Wheel هي علاقة 1 إلى كثير.

الآن، لنفترض أنك بحاجة إلى تكرار جميع السيارات، ولكل منها، طباعة قائمة بالعجلات.سيقوم تطبيق O/R الساذج بما يلي:

SELECT * FROM Cars;

وثم لكل Car:

SELECT * FROM Wheel WHERE CarId = ?

بمعنى آخر، لديك تحديد واحد للسيارات، ثم تحديد N إضافي، حيث N هو إجمالي عدد السيارات.

وبدلاً من ذلك، يمكن للمرء الحصول على كافة العجلات وإجراء عمليات البحث في الذاكرة:

SELECT * FROM Wheel

يؤدي هذا إلى تقليل عدد الرحلات ذهابًا وإيابًا إلى قاعدة البيانات من N+1 إلى 2.تمنحك معظم أدوات ORM عدة طرق لمنع تحديد N+1.

مرجع: استمرار جافا مع السبات, ، الفصل 13.

نصائح أخرى

SELECT 
table1.*
, table2.*
INNER JOIN table2 ON table2.SomeFkId = table1.SomeId

يؤدي ذلك إلى حصولك على مجموعة نتائج حيث تتسبب الصفوف الفرعية في الجدول 2 في التكرار عن طريق إرجاع نتائج الجدول 1 لكل صف فرعي في الجدول 2.يجب أن يقوم مصممو خرائط O/R بالتمييز بين مثيلات الجدول 1 بناءً على حقل مفتاح فريد، ثم استخدام جميع أعمدة الجدول 2 لملء المثيلات الفرعية.

SELECT table1.*

SELECT table2.* WHERE SomeFkId = #

N+1 هو المكان الذي يقوم فيه الاستعلام الأول بملء الكائن الأساسي بينما يقوم الاستعلام الثاني بملء كافة الكائنات التابعة لكل كائن أساسي فريد يتم إرجاعه.

يعتبر:

class House
{
    int Id { get; set; }
    string Address { get; set; }
    Person[] Inhabitants { get; set; }
}

class Person
{
    string Name { get; set; }
    int HouseId { get; set; }
}

والجداول ذات هيكل مماثل.قد يُرجع استعلام واحد عن العنوان "22 Valley St":

Id Address      Name HouseId
1  22 Valley St Dave 1
1  22 Valley St John 1
1  22 Valley St Mike 1

يجب أن يقوم O/RM بملء مثيل Home بالمعرف = 1، والعنوان = "22 Valley St" ثم تعبئة مصفوفة السكان بمثيلات الأشخاص لـ Dave وJohn وMike باستعلام واحد فقط.

سيؤدي استعلام N+1 لنفس العنوان المستخدم أعلاه إلى:

Id Address
1  22 Valley St

مع استعلام منفصل مثل

SELECT * FROM Person WHERE HouseId = 1

ويؤدي إلى مجموعة بيانات منفصلة مثل

Name    HouseId
Dave    1
John    1
Mike    1

والنتيجة النهائية هي نفسها المذكورة أعلاه مع استعلام واحد.

تتمثل مزايا الاختيار الفردي في أنك تحصل على جميع البيانات مقدمًا وهو ما قد تريده في النهاية.تتمثل مزايا N+1 في تقليل تعقيد الاستعلام ويمكنك استخدام التحميل البطيء حيث يتم تحميل مجموعات النتائج الفرعية فقط عند الطلب الأول.

المورد الذي لديه علاقة رأس بأطراف مع المنتج.مورد واحد لديه (يزود) العديد من المنتجات.

***** Table: Supplier *****
+-----+-------------------+
| ID  |       NAME        |
+-----+-------------------+
|  1  |  Supplier Name 1  |
|  2  |  Supplier Name 2  |
|  3  |  Supplier Name 3  |
|  4  |  Supplier Name 4  |
+-----+-------------------+

***** Table: Product *****
+-----+-----------+--------------------+-------+------------+
| ID  |   NAME    |     DESCRIPTION    | PRICE | SUPPLIERID |
+-----+-----------+--------------------+-------+------------+
|1    | Product 1 | Name for Product 1 |  2.0  |     1      |
|2    | Product 2 | Name for Product 2 | 22.0  |     1      |
|3    | Product 3 | Name for Product 3 | 30.0  |     2      |
|4    | Product 4 | Name for Product 4 |  7.0  |     3      |
+-----+-----------+--------------------+-------+------------+

عوامل:

  • تم ضبط الوضع البطيء للمورد على "صحيح" (افتراضي)

  • وضع الجلب المستخدم للاستعلام عن المنتج هو تحديد

  • وضع الجلب (الافتراضي):يتم الوصول إلى معلومات المورد

  • التخزين المؤقت لا يلعب دورا للمرة الأولى

  • يتم الوصول إلى المورد

وضع الجلب هو تحديد الجلب (افتراضي)

// It takes Select fetch mode as a default
Query query = session.createQuery( "from Product p");
List list = query.list();
// Supplier is being accessed
displayProductsListWithSupplierName(results);

select ... various field names ... from PRODUCT
select ... various field names ... from SUPPLIER where SUPPLIER.id=?
select ... various field names ... from SUPPLIER where SUPPLIER.id=?
select ... various field names ... from SUPPLIER where SUPPLIER.id=?

نتيجة:

  • 1 تحديد بيان للمنتج
  • N حدد كشوفات للمورد

هذه مشكلة تحديد N+1!

لا أستطيع التعليق مباشرة على الإجابات الأخرى، لأنني لا أتمتع بالسمعة الكافية.ولكن تجدر الإشارة إلى أن المشكلة تنشأ أساسًا فقط لأنه، تاريخيًا، كانت الكثير من قواعد بيانات قواعد البيانات سيئة للغاية عندما يتعلق الأمر بمعالجة الصلات (يعد MySQL مثالًا جديرًا بالملاحظة بشكل خاص).لذلك كان n+1، في كثير من الأحيان، أسرع بشكل ملحوظ من الصلة.ثم هناك طرق لتحسين n+1 ولكن دون الحاجة إلى صلة، وهو ما تتعلق به المشكلة الأصلية.

ومع ذلك، أصبحت MySQL الآن أفضل بكثير مما كانت عليه من قبل عندما يتعلق الأمر بالصلات.عندما تعلمت MySQL لأول مرة، استخدمت الصلات كثيرًا.ثم اكتشفت مدى بطئهم، وتحولت إلى n+1 في الكود بدلاً من ذلك.لكنني عدت مؤخرًا إلى عمليات الانضمام، لأن MySQL أصبحت الآن أفضل بكثير في التعامل معها مما كانت عليه عندما بدأت استخدامه لأول مرة.

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

تمت مناقشة هذا هنا بواسطة أحد فريق تطوير MySQL:

http://jorgenloland.blogspot.co.uk/2013/02/dbt-3-q3-6-x-performance-in-mysql-5610.html

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

لقد ابتعدنا عن ORM في Django بسبب هذه المشكلة.في الأساس، إذا حاولت وفعلت

for p in person:
    print p.car.colour

سيسعد ORM بإرجاع جميع الأشخاص (عادةً كمثيلات لكائن شخص)، ولكنه سيحتاج بعد ذلك إلى الاستعلام عن جدول السيارة لكل شخص.

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

الخطوة 1:اختيار واسع

  select * from people_car_colour; # this is a view or sql function

سيعود هذا بشيء من هذا القبيل

  p.id | p.name | p.telno | car.id | car.type | car.colour
  -----+--------+---------+--------+----------+-----------
  2    | jones  | 2145    | 77     | ford     | red
  2    | jones  | 2145    | 1012   | toyota   | blue
  16   | ashby  | 124     | 99     | bmw      | yellow

الخطوة 2:تجسيد

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

الخطوه 3:يجعل

for p in people:
    print p.car.colour # no more car queries

يرى صفحة الويب هذه لتنفيذ طي المعجبين لبيثون.

لنفترض أن لديك شركة وموظف.لدى الشركة العديد من الموظفين (أي.لدى الموظف حقل COMPANY_ID).

في بعض تكوينات O/R، عندما يكون لديك كائن شركة معين وتذهب للوصول إلى كائنات الموظف الخاصة به، ستقوم أداة O/R بإجراء تحديد واحد لكل موظف، بينما إذا كنت تقوم بالأشياء في SQL مباشرة، فيمكنك ذلك select * from employees where company_id = XX.وبالتالي N (عدد الموظفين) بالإضافة إلى 1 (الشركة)

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

إليك وصفًا جيدًا للمشكلة - https://web.archive.org/web/20160310145416/http://www.realsolve.co.uk/site/tech/hib-tip-pitfall.php?name=why-lazy

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

في رأيي المقال مكتوب في السبات المأزق:لماذا يجب أن تكون العلاقات كسولة هو بالضبط عكس قضية N+1 الحقيقية.

إذا كنت بحاجة إلى تفسير صحيح يرجى الرجوع السبات - الفصل 19:تحسين الأداء – جلب الاستراتيجيات

حدد جلب (الافتراضي) عرضة للغاية لمشاكل N+1 ، لذلك قد نرغب في تمكين الانضمام إلى الجلب

تحقق من مشاركة Ayende حول هذا الموضوع: مكافحة مشكلة تحديد N + 1 في NHibernate

بشكل أساسي، عند استخدام ORM مثل NHibernate أو EntityFramework، إذا كانت لديك علاقة رأس برأس (رئيسي-تفصيلي)، وتريد سرد كافة التفاصيل لكل سجل رئيسي، فيجب عليك إجراء مكالمات استعلام N + 1 إلى قاعدة البيانات، "N" هو عدد السجلات الرئيسية:استعلام واحد للحصول على كافة السجلات الرئيسية، واستعلامات N، استعلام واحد لكل سجل رئيسي، للحصول على كافة التفاصيل لكل سجل رئيسي.

المزيد من مكالمات استعلام قاعدة البيانات -> المزيد من وقت الاستجابة -> انخفاض أداء التطبيق/قاعدة البيانات.

ومع ذلك، لدى ORM خيارات لتجنب هذه المشكلة، وذلك باستخدام "الصلات" بشكل أساسي.

تحدث مشكلة استعلام N+1 عندما تنسى إحضار اقتران ثم تحتاج إلى الوصول إليه:

List<PostComment> comments = entityManager.createQuery(
    "select pc " +
    "from PostComment pc " +
    "where pc.review = :review", PostComment.class)
.setParameter("review", review)
.getResultList();

LOGGER.info("Loaded {} comments", comments.size());

for(PostComment comment : comments) {
    LOGGER.info("The post title is '{}'", comment.getPost().getTitle());
}

الذي يولد عبارات SQL التالية:

SELECT pc.id AS id1_1_, pc.post_id AS post_id3_1_, pc.review AS review2_1_
FROM   post_comment pc
WHERE  pc.review = 'Excellent!'

INFO - Loaded 3 comments

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 1

INFO - The post title is 'Post nr. 1'

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 2

INFO - The post title is 'Post nr. 2'

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 3

INFO - The post title is 'Post nr. 3'

أولاً، يقوم Hibernate بتنفيذ استعلام JPQL وقائمة PostComment يتم جلب الكيانات.

ثم لكل PostComment, ، ويرتبط post يتم استخدام الخاصية لإنشاء رسالة سجل تحتوي على Post عنوان.

بسبب ال post لم تتم تهيئة الاقتران، ويجب على السبات جلب الملف Post كيان مع استعلام ثانوي ، ولل PostComment الكيانات، سيتم تنفيذ N المزيد من الاستعلامات (وبالتالي مشكلة استعلام N+1).

أولا، تحتاج تسجيل ومراقبة SQL الصحيح حتى تتمكن من اكتشاف هذه المشكلة.

ثانيًا، من الأفضل اكتشاف هذا النوع من المشكلات من خلال اختبارات التكامل.يمكنك استخدام تأكيد JUnit التلقائي للتحقق من صحة العدد المتوقع لعبارات SQL التي تم إنشاؤها.ال مشروع وحدة ديسيبل يوفر بالفعل هذه الوظيفة، وهو مفتوح المصدر.

عندما حددت مشكلة استعلام N+1، تحتاج إلى استخدام JOIN FETCH بحيث يتم جلب الارتباطات الفرعية في استعلام واحد، بدلاً من N.إذا كنت بحاجة إلى جلب العديد من الارتباطات الفرعية، فمن الأفضل جلب مجموعة واحدة في الاستعلام الأولي والمجموعة الثانية مع استعلام SQL ثانوي.

يحتوي الرابط المتوفر على مثال بسيط جدًا لمشكلة n + 1.إذا قمت بتطبيقه على Hibernate فهو يتحدث بشكل أساسي عن نفس الشيء.عندما تقوم بالاستعلام عن كائن، يتم تحميل الكيان ولكن أي اقترانات (ما لم يتم تكوينها بطريقة أخرى) سيتم تحميلها ببطء.ومن ثم، استعلام واحد للكائنات الجذرية واستعلام آخر لتحميل الارتباطات لكل منها.100 كائن تم إرجاعها يعني استعلامًا أوليًا واحدًا ثم 100 استعلام إضافي للحصول على الاقتران لكل منها، n + 1.

http://pramatr.com/2009/02/05/sql-n-1-selects-explained/

مليونير لديه سيارات N.تريد الحصول على جميع العجلات (4).

استعلام واحد (1) يقوم بتحميل جميع السيارات، ولكن لكل سيارة (N) يتم تقديم استعلام منفصل لتحميل العجلات.

التكاليف:

افترض أن الفهارس تتناسب مع ذاكرة الوصول العشوائي.

تحليل وتخطيط الاستعلام 1 + N + البحث في الفهرس و1 + N + (N * 4) الوصول إلى اللوحة لتحميل الحمولة.

افترض أن الفهارس لا تتناسب مع ذاكرة الوصول العشوائي.

تكاليف إضافية في أسوأ الحالات 1 + وصول لوحة N لمؤشر التحميل.

ملخص

عنق الزجاجة هو الوصول إلى اللوحة (ca.70 مرة في الثانية الواحدة وصول عشوائي على HDD) سيقوم Join Sealion بالوصول أيضًا إلى اللوحة 1 + n + (n * 4) لحمولة الحمولة.لذا، إذا كانت الفهارس مناسبة لذاكرة الوصول العشوائي - فلا مشكلة، فهي سريعة بدرجة كافية لأن عمليات ذاكرة الوصول العشوائي فقط هي المعنية.

يعد إصدار استعلام واحد يعرض 100 نتيجة أسرع بكثير من إصدار 100 استعلام يعرض كل منها نتيجة واحدة.

تعتبر مشكلة تحديد N+1 مؤلمة، ومن المنطقي اكتشاف مثل هذه الحالات في اختبارات الوحدة.لقد قمت بتطوير مكتبة صغيرة للتحقق من عدد الاستعلامات التي يتم تنفيذها بواسطة طريقة اختبار معينة أو مجرد كتلة عشوائية من التعليمات البرمجية - JDBC شم

ما عليك سوى إضافة قاعدة JUnit خاصة إلى فصل الاختبار الخاص بك ووضع تعليق توضيحي مع العدد المتوقع من الاستعلامات على طرق الاختبار الخاصة بك:

@Rule
public final QueryCounter queryCounter = new QueryCounter();

@Expectation(atMost = 3)
@Test
public void testInvokingDatabase() {
    // your JDBC or JPA code
}

المشكلة كما ذكر الآخرون بشكل أكثر أناقة هي أنه إما أن يكون لديك منتج ديكارتي لأعمدة OneToMany أو أنك تقوم بتحديد N+1.إما مجموعة النتائج العملاقة المحتملة أو الدردشة مع قاعدة البيانات، على التوالي.

أنا مندهش من عدم ذكر هذا ولكن هذه هي الطريقة التي تعاملت بها مع هذه المشكلة ... أقوم بإنشاء جدول معرفات شبه مؤقت. أنا أيضا أفعل هذا عندما يكون لديك IN () تقييد الشرط.

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

قم أولاً بإدراج معرفات الكائنات الأصلية كدفعة في جدول المعرفات.معرف الدفعة هذا هو شيء ننشئه في تطبيقنا ونتمسك به.

INSERT INTO temp_ids 
    (product_id, batch_id)
    (SELECT p.product_id, ? 
    FROM product p ORDER BY p.product_id
    LIMIT ? OFFSET ?);

الآن لكل OneToMany العمود الذي تقوم به فقط SELECT على جدول معرفات INNER JOINفي جدول الطفل مع WHERE batch_id= (أو العكس).أنت فقط تريد التأكد من الترتيب حسب عمود المعرف لأنه سيجعل دمج أعمدة النتائج أسهل (وإلا فستحتاج إلى HashMap/Table لمجموعة النتائج بأكملها والتي قد لا تكون بهذا السوء).

ثم تقوم فقط بتنظيف جدول المعرفات بشكل دوري.

يعمل هذا أيضًا بشكل جيد بشكل خاص إذا قام المستخدم بتحديد 100 عنصر مميز أو نحو ذلك لنوع من المعالجة المجمعة.ضع 100 معرف مميز في الجدول المؤقت.

الآن عدد الاستعلامات التي تجريها هو حسب عدد أعمدة OneToMany.

خذ مثال Matt Solnit، تخيل أنك تحدد الارتباط بين السيارة والعجلات كـ LAZY وتحتاج إلى بعض حقول العجلات.هذا يعني أنه بعد التحديد الأول، سيقوم السبات بإجراء "Select * from Wheels Where car_id = :id" لكل سيارة.

هذا يجعل الاختيار الأول والمزيد اختيارًا واحدًا لكل سيارة N، ولهذا السبب يطلق عليها مشكلة n+1.

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

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

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top