“ N + 1 เลือกปัญหา” ใน ORM คืออะไร (การทำแผนที่วัตถุสัมพันธ์)


1596

"N + 1 selects problem" โดยทั่วไปแล้วระบุว่าเป็นปัญหาในการอภิปราย Object-Relational mapping (ORM) และฉันเข้าใจว่ามันมีบางอย่างที่เกี่ยวข้องกับการทำแบบสอบถามฐานข้อมูลมากมายสำหรับสิ่งที่ดูเหมือนง่ายในวัตถุ โลก.

ใครบ้างมีคำอธิบายรายละเอียดเพิ่มเติมของปัญหาหรือไม่


2
นี่คือลิงค์ที่ยอดเยี่ยมพร้อมคำอธิบายที่ดีเกี่ยวกับการทำความเข้าใจกับปัญหา+1 นอกจากนี้ยังครอบคลุมถึงการแก้ปัญหาเพื่อแก้ไขปัญหานี้: architects.dzone.com/articles/how-identify-and-resilve-n1
aces

มีโพสต์ที่เป็นประโยชน์พูดคุยเกี่ยวกับปัญหานี้และการแก้ไขที่เป็นไปได้ ปัญหาแอปพลิเคชันทั่วไปและวิธีการแก้ไข: ปัญหา Select N + 1 , Bullet (Silver) สำหรับปัญหา N + 1 , การโหลด Lazy - การโหลดที่กระตือรือร้น
cateyes

สำหรับทุกคนที่กำลังมองหาวิธีแก้ปัญหานี้ฉันพบโพสต์อธิบาย stackoverflow.com/questions/32453989/…
damndemon

2
เมื่อพิจารณาคำตอบนี่ไม่ควรถูกเรียกว่าเป็นปัญหา 1 + N ใช่หรือไม่ ดูเหมือนว่านี่จะเป็นคำศัพท์ฉันไม่ได้เจาะจงถาม OP
user1418717

คำตอบ:


1015

สมมติว่าคุณมีชุดของCarวัตถุ (แถวฐานข้อมูล) และแต่ละชุดCarมีชุดของWheelวัตถุ (เช่นแถว) กล่าวอีกนัยหนึ่งCarWheelเป็นความสัมพันธ์แบบหนึ่งต่อหลายคน

ทีนี้สมมติว่าคุณต้องวนซ้ำรถยนต์ทุกคันและสำหรับแต่ละล้อพิมพ์รายชื่อล้อ การนำไปใช้งานไร้เดียงสา O / R จะทำดังต่อไปนี้

SELECT * FROM Cars;

และสำหรับแต่ละCar:

SELECT * FROM Wheel WHERE CarId = ?

กล่าวอีกนัยหนึ่งคุณมีหนึ่งตัวเลือกสำหรับรถยนต์จากนั้นเลือก N เพิ่มเติมโดยที่ N คือจำนวนรถยนต์ทั้งหมด

อีกวิธีหนึ่งสามารถรับล้อทั้งหมดและทำการค้นหาในหน่วยความจำ:

SELECT * FROM Wheel

สิ่งนี้จะช่วยลดจำนวนการไปกลับไปยังฐานข้อมูลจาก N + 1 ถึง 2 เครื่องมือ ORM ส่วนใหญ่จะให้วิธีการหลายวิธีในการป้องกันการเลือก N + 1

การอ้างอิง: Java Persistence with Hibernate , บทที่ 13


140
หากต้องการชี้แจงเกี่ยวกับ "สิ่งนี้ไม่ดี" - คุณสามารถรับล้อทั้งหมดด้วย 1 select ( SELECT * from Wheel;) แทน N + 1 ด้วย N ขนาดใหญ่ประสิทธิภาพในการทำงานจึงมีความสำคัญมาก
tucuxi

211
@tucuxi ฉันประหลาดใจที่คุณมี upvotes มากมายที่ทำผิด ฐานข้อมูลดีมากเกี่ยวกับดัชนีการทำแบบสอบถามสำหรับ CarID ที่เฉพาะเจาะจงจะกลับมาเร็วมาก แต่ถ้าคุณได้ล้อทั้งหมดครั้งเดียวคุณจะต้องค้นหา CarID ในแอปพลิเคชันของคุณซึ่งไม่ได้จัดทำดัชนีนี่จะช้ากว่า หากคุณไม่มีปัญหาเวลาแฝงที่สำคัญในการเข้าถึงฐานข้อมูลของคุณการที่ n + 1 นั้นเร็วกว่าจริงและใช่ฉันก็ทำการเปรียบเทียบด้วยโค้ดจริงจำนวนมาก
Ariel

74
@ariel วิธีที่ 'ถูกต้อง' คือการรับล้อทั้งหมดตามลำดับโดย CarId (1 เลือก) และหากต้องการรายละเอียดมากกว่า CarId ให้ทำแบบสอบถามที่สองสำหรับรถยนต์ทุกคัน (รวม 2 แบบสอบถาม) การพิมพ์สิ่งที่ออกมานั้นเหมาะสมที่สุดและไม่จำเป็นต้องมีดัชนีหรือที่เก็บข้อมูลรอง (คุณสามารถทำซ้ำได้มากกว่าผลลัพธ์ไม่จำเป็นต้องดาวน์โหลดทั้งหมด) คุณเปรียบเทียบสิ่งผิด หากคุณยังมั่นใจในการวัดประสิทธิภาพของคุณคุณจะโพสต์ความคิดเห็นเพิ่มเติม (หรือคำตอบแบบเต็ม) เพื่ออธิบายการทดสอบและผลลัพธ์ของคุณหรือไม่
tucuxi

92
"ไฮเบอร์เนต (ฉันไม่คุ้นเคยกับกรอบ ORM อื่น ๆ ) ช่วยให้คุณจัดการได้หลายวิธี" และวิธีการเหล่านี้คืออะไร?
Tima

58
@Ariel ลองใช้การวัดประสิทธิภาพด้วยฐานข้อมูลและเซิร์ฟเวอร์แอปพลิเคชันบนเครื่องอื่น จากประสบการณ์ของฉันการเดินทางไปยังฐานข้อมูลมีค่าใช้จ่ายสูงกว่าการสืบค้น ใช่แล้วคำค้นหานั้นเร็วมาก แต่มันเป็นทริปไปกลับที่ก่อให้เกิดความหายนะ ฉันได้แปลง "WHERE Id = const " เป็น "WHERE Id IN ( const , const , ... )" และคำสั่งที่ได้รับขนาดเพิ่มขึ้นจากมัน
ฮันส์

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

ที่ทำให้คุณได้รับชุดผลลัพธ์ที่แถวลูกใน table2 ทำให้เกิดการซ้ำซ้อนโดยส่งกลับผลลัพธ์ table1 สำหรับแต่ละแถวลูกใน table2 ตัวแม็พ O / R ควรแยกอินสแตนซ์ table1 ตามฟิลด์คีย์ที่ไม่ซ้ำกันจากนั้นใช้คอลัมน์ table2 ทั้งหมดเพื่อเติมอินสแตนซ์ชายด์

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 ด้วย ID = 1, Address = "22 Valley St" จากนั้นเติมอาร์เรย์ Inhabitants ด้วยอินสแตนซ์ People สำหรับ 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 คือความซับซ้อนของคิวรีจะลดลงและคุณสามารถใช้การโหลดแบบขี้เกียจซึ่งชุดผลลัพธ์ลูกจะถูกโหลดตามการร้องขอแรกเท่านั้น


4
ข้อดีอีกอย่างของ n + 1 ก็คือมันเร็วกว่าเพราะฐานข้อมูลสามารถส่งคืนผลลัพธ์โดยตรงจากดัชนี ทำการเข้าร่วมแล้วการเรียงลำดับจำเป็นต้องใช้ตารางชั่วคราวซึ่งช้ากว่า เหตุผลเดียวที่จะหลีกเลี่ยง n + 1 คือถ้าคุณมีเวลาในการตอบสนองกับฐานข้อมูลมาก
Ariel

17
การเข้าร่วมและการเรียงลำดับนั้นค่อนข้างเร็ว (เพราะคุณจะเข้าร่วมในเขตข้อมูลที่จัดทำดัชนีและเรียงลำดับ) 'n + 1' ของคุณใหญ่แค่ไหน คุณเชื่ออย่างจริงจังว่าปัญหา n + 1 ใช้กับการเชื่อมต่อฐานข้อมูลความล่าช้าสูงเท่านั้นหรือไม่
tucuxi

9
@ariel - คำแนะนำของคุณว่า N + 1 คือ "เร็วที่สุด" ผิดแม้ว่ามาตรฐานของคุณอาจถูกต้อง เป็นไปได้อย่างไร? ดูen.wikipedia.org/wiki/Anecdotal_evidenceและความคิดเห็นของฉันในคำตอบอื่น ๆ สำหรับคำถามนี้
whitneyland

7
@Ariel - ฉันคิดว่าฉันเข้าใจดี :) ฉันแค่พยายามชี้ให้เห็นว่าผลลัพธ์ของคุณใช้กับเงื่อนไขหนึ่งชุดเท่านั้น ฉันสามารถสร้างตัวอย่างเคาน์เตอร์ที่แสดงตรงกันข้ามได้อย่างง่ายดาย มันสมเหตุสมผลไหม
whitneyland

13
เมื่อต้องการย้ำปัญหา SELECT N + 1 คือที่แกนกลาง: ฉันมี 600 เร็กคอร์ดที่จะเรียกคืน มันเร็วกว่าที่จะได้ 600 ทั้งหมดในหนึ่งแบบสอบถามหรือ 1 ครั้งใน 600 แบบสอบถาม เว้นแต่ว่าคุณอยู่บน MyISAM และ / หรือคุณมี schema ที่ทำดัชนีปกติ / ไม่ดี (ในกรณีที่ ORM ไม่ใช่ปัญหา), db ที่ปรับค่าอย่างถูกต้องจะส่งคืน 600 แถวใน 2 ms ในขณะที่คืนค่าแถวแต่ละแถวใน ประมาณ 1 มิลลิวินาที ดังนั้นเรามักจะเห็น N + 1 ใช้เวลาหลายร้อยมิลลิวินาทีซึ่งการเข้าร่วมใช้เวลาเพียงไม่กี่คู่
สุนัข

64

ผู้จัดจำหน่ายที่มีความสัมพันธ์แบบหนึ่งต่อหลายกับผลิตภัณฑ์ ผู้จัดหาหนึ่งรายมีผลิตภัณฑ์มากมาย

***** 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      |
+-----+-----------+--------------------+-------+------------+

ปัจจัย:

  • โหมด Lazy สำหรับ Supplier ตั้งค่าเป็น "true" (ค่าเริ่มต้น)

  • โหมดการดึงข้อมูลที่ใช้สำหรับการสืบค้นบนผลิตภัณฑ์คือเลือก

  • โหมดการดึงข้อมูล (ค่าเริ่มต้น): เข้าถึงข้อมูลผู้จัดหา

  • การแคชไม่ได้มีบทบาทเป็นครั้งแรก

  • มีการเข้าถึงซัพพลายเออร์

โหมดดึงข้อมูลคือเลือกดึงข้อมูล (ค่าเริ่มต้น)

// 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 + 1!


3
มันควรจะเป็น 1 เลือกสำหรับผู้ผลิตแล้วเลือก N สำหรับผลิตภัณฑ์หรือไม่
bencampbell_14

@bencampbell_ ใช่ตอนแรกฉันรู้สึกเหมือนกัน แต่ด้วยตัวอย่างของเขามันเป็นผลิตภัณฑ์หนึ่งสำหรับซัพพลายเออร์จำนวนมาก
Mohd Faizan Khan

38

ฉันไม่สามารถแสดงความคิดเห็นโดยตรงกับคำตอบอื่น ๆ เพราะฉันไม่มีชื่อเสียงเพียงพอ แต่มันก็น่าสังเกตว่าปัญหาที่เกิดขึ้นเป็นหลักเพราะในอดีตจำนวนมากของ DBMS ได้ค่อนข้างยากจนเมื่อมันมาถึงการจัดการร่วม (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 กับพวกเขาแล้วลองอีกครั้งในรุ่นล่าสุด คุณอาจจะประหลาดใจ


7
การเรียกใช้ MySQL รุ่นแรก ๆ ว่า DBMS เชิงสัมพันธ์นั้นค่อนข้างยืดเยื้อ ... ถ้าผู้คนที่ประสบปัญหาเหล่านั้นใช้ฐานข้อมูลจริงพวกเขาก็คงจะไม่เจอปัญหาแบบนั้น ;-)
Craig

2
ที่น่าสนใจปัญหาเหล่านี้จำนวนมากได้รับการแก้ไขใน MySQL ด้วยการแนะนำและการเพิ่มประสิทธิภาพของเครื่องมือ INNODB ที่ตามมา แต่คุณยังคงพบเจอกับคนที่พยายามโปรโมต MYISAM เพราะพวกเขาคิดว่ามันเร็วกว่า
Craig

5
FYI หนึ่งในสามJOINอัลกอริทึมทั่วไปที่ใช้ใน RDBMS นั้นเรียกว่าซ้อนวนซ้ำ มันคือการเลือก N + 1 ภายใต้ประทุน ความแตกต่างเพียงอย่างเดียวคือฐานข้อมูลสร้างทางเลือกที่ชาญฉลาดในการใช้งานโดยยึดตามสถิติและดัชนีแทนที่จะใช้รหัสลูกค้าบังคับให้มันลงเส้นทางนั้นอย่างเป็นหมวดหมู่
Brandon

2
@Brandon ใช่! เช่นเดียวกับคำแนะนำ JOIN และคำแนะนำ INDEX การบังคับให้ใช้เส้นทางการดำเนินการบางอย่างในทุกกรณีจะไม่ค่อยเอาชนะฐานข้อมูล ฐานข้อมูลมักจะดีมากในการเลือกวิธีการที่ดีที่สุดในการรับข้อมูล บางทีในช่วงแรก ๆ ของ dbs คุณจำเป็นต้อง 'วลี' คำถามของคุณด้วยวิธีที่แปลกประหลาดเพื่อเกลี้ยกล่อม db พร้อม แต่หลังจากหลายทศวรรษของวิศวกรรมระดับโลกตอนนี้คุณสามารถได้รับประสิทธิภาพที่ดีที่สุดโดยถามคำถามเชิงสัมพันธ์กับฐานข้อมูลของคุณและปล่อยให้มัน จัดเรียงวิธีดึงและรวบรวมข้อมูลนั้นให้คุณ
สุนัข

3
ไม่เพียง แต่เป็นฐานข้อมูลที่ใช้ดัชนีและสถิติการดำเนินการทั้งหมดยังเป็น I / O ภายในเครื่องซึ่งส่วนใหญ่มักจะทำงานกับแคชที่มีประสิทธิภาพสูงมากกว่าดิสก์ โปรแกรมเมอร์ฐานข้อมูลอุทิศความสนใจอย่างยิ่งยวดในการปรับสิ่งเหล่านี้ให้เหมาะสม
Craig

27

เราย้ายออกจาก ORM ใน Django เนื่องจากปัญหานี้ โดยทั่วไปถ้าคุณลองทำ

for p in person:
    print p.car.colour

ORM จะส่งคืนทุกคนอย่างมีความสุข (โดยทั่วไปจะเป็นอินสแตนซ์ของวัตถุบุคคล) แต่จากนั้นจะต้องค้นหาตารางรถยนต์สำหรับแต่ละบุคคล

วิธีที่ง่ายและมีประสิทธิภาพมากสำหรับสิ่งนี้คือสิ่งที่ฉันเรียกว่า " fanfolding " ซึ่งหลีกเลี่ยงแนวคิดไร้สาระที่ผลลัพธ์การสืบค้นจากฐานข้อมูลเชิงสัมพันธ์ควรแมปกลับไปยังตารางดั้งเดิมที่ประกอบด้วยการสืบค้น

ขั้นตอนที่ 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: ทำให้เป็นจริง

ดูดผลลัพธ์เป็นผู้สร้างวัตถุทั่วไปด้วยอาร์กิวเมนต์ที่จะแยกหลังรายการที่สาม ซึ่งหมายความว่าวัตถุ "jones" จะไม่ถูกสร้างขึ้นมากกว่าหนึ่งครั้ง

ขั้นตอนที่ 3: แสดงผล

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

ดูหน้าเว็บนี้สำหรับการนำfanfolding ไปใช้กับ python


10
ฉันดีใจที่ฉันสะดุดโพสต์ของคุณเพราะฉันคิดว่าฉันบ้าไปแล้ว เมื่อฉันค้นพบเกี่ยวกับปัญหา N + 1 ความคิดทันทีของฉันก็ดีทำไมคุณไม่สร้างมุมมองที่มีข้อมูลทั้งหมดที่คุณต้องการและดึงออกมาจากมุมมองนั้น คุณได้ตรวจสอบตำแหน่งของฉันแล้ว ขอบคุณครับ
นักพัฒนา

14
เราย้ายออกจาก ORM ใน Django เนื่องจากปัญหานี้ ฮะ? Django มีselect_relatedซึ่งมีไว้เพื่อแก้ไขปัญหานี้ - อันที่จริงเอกสารของมันเริ่มต้นด้วยตัวอย่างที่คล้ายกับp.car.colourตัวอย่างของคุณ
Adrian17

8
นี่เป็นคำตอบเก่า ๆ ที่เรามีselect_related()และprefetch_related()ใน Django ตอนนี้
Mariusz Jamro

1
เย็น. แต่select_related()เพื่อนและดูเหมือนจะไม่ดำเนินการใด ๆ LEFT OUTER JOINศักดิ์ประโยชน์ที่เห็นได้ชัดของการเข้าร่วมเช่น ปัญหาไม่ใช่ปัญหาของส่วนต่อประสาน แต่มีปัญหาเกี่ยวกับความคิดแปลก ๆ ที่วัตถุและข้อมูลเชิงสัมพันธ์สามารถแม็ปได้ .... ในมุมมองของฉัน
rorycl

26

เนื่องจากนี่เป็นคำถามที่พบบ่อยมากฉันจึงเขียน บทความนี้ขึ้นอยู่กับคำตอบนี้

ปัญหาแบบสอบถาม N + 1 คืออะไร

ปัญหาแบบสอบถาม N + 1 เกิดขึ้นเมื่อกรอบการเข้าถึงข้อมูลดำเนินการคำสั่ง N SQL เพิ่มเติมเพื่อดึงข้อมูลเดียวกับที่สามารถเรียกคืนได้เมื่อดำเนินการสืบค้น SQL หลัก

ยิ่งค่าของ N มากขึ้นเท่าไรก็ยิ่งมีการดำเนินการสืบค้นมากขึ้นเท่านั้น และแตกต่างจากบันทึกการสืบค้นที่ช้าที่สามารถช่วยคุณค้นหาการสืบค้นที่ทำงานช้าปัญหา N + 1 จะไม่เกิดขึ้นเพราะแบบสอบถามเพิ่มเติมแต่ละรายการจะทำงานเร็วพอที่จะไม่ทริกเกอร์บันทึกการสืบค้นที่ช้า

ปัญหากำลังดำเนินการกับแบบสอบถามจำนวนมากซึ่งโดยรวมแล้วใช้เวลาเพียงพอในการตอบสนองช้าลง

ลองพิจารณาว่าเรามีตารางฐานข้อมูลการโพสต์และ post_comments ดังต่อไปนี้ซึ่งก่อให้เกิดความสัมพันธ์แบบหนึ่งต่อหลายกลุ่ม :

ตาราง <code> โพสต์ </code> และ <code> post_comments </code> ตาราง

เราจะสร้าง 4 postแถวต่อไปนี้:

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 1', 1)

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 2', 2)

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 3', 3)

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 4', 4)

และเราจะสร้างpost_commentระเบียนย่อย4 รายการ:

INSERT INTO post_comment (post_id, review, id)
VALUES (1, 'Excellent book to understand Java Persistence', 1)

INSERT INTO post_comment (post_id, review, id)
VALUES (2, 'Must-read for Java developers', 2)

INSERT INTO post_comment (post_id, review, id)
VALUES (3, 'Five Stars', 3)

INSERT INTO post_comment (post_id, review, id)
VALUES (4, 'A great reference book', 4)

ปัญหาแบบสอบถาม N + 1 กับ SQL ธรรมดา

หากคุณเลือกการpost_commentsใช้แบบสอบถาม SQL นี้:

List<Tuple> comments = entityManager.createNativeQuery("""
    SELECT
        pc.id AS id,
        pc.review AS review,
        pc.post_id AS postId
    FROM post_comment pc
    """, Tuple.class)
.getResultList();

และในภายหลังคุณตัดสินใจดึงข้อมูลที่เกี่ยวข้องpost titleสำหรับแต่ละรายการpost_comment:

for (Tuple comment : comments) {
    String review = (String) comment.get("review");
    Long postId = ((Number) comment.get("postId")).longValue();

    String postTitle = (String) entityManager.createNativeQuery("""
        SELECT
            p.title
        FROM post p
        WHERE p.id = :postId
        """)
    .setParameter("postId", postId)
    .getSingleResult();

    LOGGER.info(
        "The Post '{}' got this review '{}'",
        postTitle,
        review
    );
}

คุณจะทริกเกอร์ปัญหาการสอบถาม N + 1 เพราะแทนที่จะเป็นการสืบค้น SQL หนึ่งครั้งคุณได้ดำเนินการ 5 (1 + 4):

SELECT
    pc.id AS id,
    pc.review AS review,
    pc.post_id AS postId
FROM post_comment pc

SELECT p.title FROM post p WHERE p.id = 1
-- The Post 'High-Performance Java Persistence - Part 1' got this review
-- 'Excellent book to understand Java Persistence'

SELECT p.title FROM post p WHERE p.id = 2
-- The Post 'High-Performance Java Persistence - Part 2' got this review
-- 'Must-read for Java developers'

SELECT p.title FROM post p WHERE p.id = 3
-- The Post 'High-Performance Java Persistence - Part 3' got this review
-- 'Five Stars'

SELECT p.title FROM post p WHERE p.id = 4
-- The Post 'High-Performance Java Persistence - Part 4' got this review
-- 'A great reference book'

การแก้ไขปัญหาคิวรี N + 1 นั้นง่ายมาก สิ่งที่คุณต้องทำคือดึงข้อมูลทั้งหมดที่คุณต้องการในการสืบค้น SQL ดั้งเดิมออกมาดังนี้:

List<Tuple> comments = entityManager.createNativeQuery("""
    SELECT
        pc.id AS id,
        pc.review AS review,
        p.title AS postTitle
    FROM post_comment pc
    JOIN post p ON pc.post_id = p.id
    """, Tuple.class)
.getResultList();

for (Tuple comment : comments) {
    String review = (String) comment.get("review");
    String postTitle = (String) comment.get("postTitle");

    LOGGER.info(
        "The Post '{}' got this review '{}'",
        postTitle,
        review
    );
}

ในครั้งนี้มีการดำเนินการค้นหา SQL เพียงแบบสอบถามเดียวเพื่อดึงข้อมูลทั้งหมดที่เราสนใจใช้ต่อไป

ปัญหาการสืบค้น N + 1 กับ JPA และ Hibernate

เมื่อใช้ JPA และ Hibernate มีหลายวิธีที่คุณสามารถทริกเกอร์ปัญหาการสอบถาม N + 1 ได้ดังนั้นจึงเป็นสิ่งสำคัญที่จะต้องทราบว่าคุณสามารถหลีกเลี่ยงสถานการณ์เหล่านี้ได้อย่างไร

สำหรับตัวอย่างถัดไปให้พิจารณาว่าเรากำลังทำแผนที่postและpost_commentsตารางไปยังเอนทิตีต่อไปนี้:

<code> โพสต์ </code> และ <code> เอนทิตี PostComment </code>

การแม็พ JPA มีลักษณะเช่นนี้:

@Entity(name = "Post")
@Table(name = "post")
public class Post {

    @Id
    private Long id;

    private String title;

    //Getters and setters omitted for brevity
}

@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {

    @Id
    private Long id;

    @ManyToOne
    private Post post;

    private String review;

    //Getters and setters omitted for brevity
}

FetchType.EAGER

การใช้FetchType.EAGERความสัมพันธ์ JPA ของคุณโดยปริยายหรืออย่างชัดเจนเป็นความคิดที่ไม่ดีเพราะคุณจะดึงข้อมูลที่คุณต้องการได้มากขึ้น ยิ่งไปกว่านั้นFetchType.EAGERกลยุทธ์ยังมีแนวโน้มที่จะเกิดปัญหาการสอบถาม N + 1

น่าเสียดายที่การเชื่อมโยง@ManyToOneและการ@OneToOneเชื่อมโยงใช้FetchType.EAGERตามค่าเริ่มต้นดังนั้นหากการจับคู่ของคุณมีลักษณะดังนี้:

@ManyToOne
private Post post;

คุณกำลังใช้FetchType.EAGERกลยุทธ์และทุกครั้งที่คุณลืมที่จะใช้JOIN FETCHเมื่อโหลดPostCommentเอนทิตีบางอย่างด้วยการสืบค้น JPQL หรือ Criteria API:

List<PostComment> comments = entityManager
.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

คุณกำลังจะเรียกปัญหาการสืบค้น N + 1:

SELECT 
    pc.id AS id1_1_, 
    pc.post_id AS post_id3_1_, 
    pc.review AS review2_1_ 
FROM 
    post_comment pc

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4

ขอให้สังเกตงบเลือกเพิ่มเติมที่จะดำเนินการเพราะpostสมาคมจะต้องมีความจริงก่อนที่จะกลับมาListของPostCommentหน่วยงาน

ซึ่งแตกต่างจากแผนการดึงข้อมูลเริ่มต้นซึ่งคุณใช้เมื่อเรียกใช้findเมธอดEnrityManagerแบบสอบถาม JPQL หรือ Criteria API จะกำหนดแผนอย่างชัดเจนที่ไฮเบอร์เนตไม่สามารถเปลี่ยนแปลงได้โดยการฉีด JOIN FETCH โดยอัตโนมัติ ดังนั้นคุณต้องทำด้วยตนเอง

หากคุณไม่ต้องการการpostเชื่อมโยงเลยแสดงว่าคุณโชคไม่ดีเมื่อใช้FetchType.EAGERเพราะไม่มีวิธีหลีกเลี่ยงที่จะดึงมันออกมา นั่นเป็นเหตุผลว่าทำไมจึงควรใช้เป็นFetchType.LAZYค่าเริ่มต้น

แต่ถ้าคุณต้องการใช้การpostเชื่อมโยงคุณสามารถใช้JOIN FETCHเพื่อสอบถามปัญหา N + 1 ได้:

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    join fetch pc.post p
    """, PostComment.class)
.getResultList();

for(PostComment comment : comments) {
    LOGGER.info(
        "The Post '{}' got this review '{}'", 
        comment.getPost().getTitle(), 
        comment.getReview()
    );
}

เวลานี้ไฮเบอร์เนตจะดำเนินการคำสั่ง SQL เดี่ยว:

SELECT 
    pc.id as id1_1_0_, 
    pc.post_id as post_id3_1_0_, 
    pc.review as review2_1_0_, 
    p.id as id1_0_1_, 
    p.title as title2_0_1_ 
FROM 
    post_comment pc 
INNER JOIN 
    post p ON pc.post_id = p.id

-- The Post 'High-Performance Java Persistence - Part 1' got this review 
-- 'Excellent book to understand Java Persistence'

-- The Post 'High-Performance Java Persistence - Part 2' got this review 
-- 'Must-read for Java developers'

-- The Post 'High-Performance Java Persistence - Part 3' got this review 
-- 'Five Stars'

-- The Post 'High-Performance Java Persistence - Part 4' got this review 
-- 'A great reference book'

สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับสาเหตุที่คุณควรหลีกเลี่ยงFetchType.EAGERกลยุทธ์ดึงข้อมูลลองดูบทความนี้ด้วย

FetchType.LAZY

แม้ว่าคุณจะเปลี่ยนเป็นการใช้FetchType.LAZYอย่างชัดเจนสำหรับการเชื่อมโยงทั้งหมดคุณยังสามารถชนกับปัญหา N + 1 ได้

เวลานี้การpostเชื่อมโยงถูกแมปดังนี้:

@ManyToOne(fetch = FetchType.LAZY)
private Post post;

ตอนนี้เมื่อคุณดึงPostCommentเอนทิตี้:

List<PostComment> comments = entityManager
.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

ไฮเบอร์เนตจะดำเนินการคำสั่ง SQL เดียว:

SELECT 
    pc.id AS id1_1_, 
    pc.post_id AS post_id3_1_, 
    pc.review AS review2_1_ 
FROM 
    post_comment pc

แต่ถ้าหลังจากนั้นคุณจะอ้างอิงความสัมพันธ์แบบขี้เกียจpost:

for(PostComment comment : comments) {
    LOGGER.info(
        "The Post '{}' got this review '{}'", 
        comment.getPost().getTitle(), 
        comment.getReview()
    );
}

คุณจะได้รับข้อความค้นหา N + 1:

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1
-- The Post 'High-Performance Java Persistence - Part 1' got this review 
-- 'Excellent book to understand Java Persistence'

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2
-- The Post 'High-Performance Java Persistence - Part 2' got this review 
-- 'Must-read for Java developers'

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3
-- The Post 'High-Performance Java Persistence - Part 3' got this review 
-- 'Five Stars'

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4
-- The Post 'High-Performance Java Persistence - Part 4' got this review 
-- 'A great reference book'

เนื่องจากการpostเชื่อมโยงถูกดึงออกมาอย่างเกียจคร้านคำสั่ง SQL รองจะถูกเรียกใช้เมื่อเข้าถึงการเชื่อมโยงที่ขี้เกียจเพื่อสร้างข้อความบันทึก

อีกครั้งการแก้ไขประกอบด้วยการเพิ่มส่วนJOIN FETCHคำสั่งในการสืบค้น JPQL:

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    join fetch pc.post p
    """, PostComment.class)
.getResultList();

for(PostComment comment : comments) {
    LOGGER.info(
        "The Post '{}' got this review '{}'", 
        comment.getPost().getTitle(), 
        comment.getReview()
    );
}

และเช่นเดียวกับในFetchType.EAGERตัวอย่างแบบสอบถาม JPQL นี้จะสร้างคำสั่ง SQL เดียว

แม้ว่าคุณจะใช้FetchType.LAZYและไม่อ้างอิงการเชื่อมโยงลูกของ@OneToOneความสัมพันธ์ JPA สองทิศทางคุณยังสามารถทริกเกอร์ปัญหาการสอบถาม N + 1

สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับวิธีการที่คุณสามารถเอาชนะปัญหา N + 1 แบบสอบถามที่สร้างขึ้นโดย@OneToOneสมาคมตรวจสอบบทความนี้

วิธีตรวจหาปัญหาการสืบค้น N + 1 โดยอัตโนมัติ

หากคุณต้องการตรวจสอบปัญหาการสืบค้น N + 1 ในเลเยอร์การเข้าถึงข้อมูลของคุณโดยอัตโนมัติบทความนี้จะอธิบายวิธีการทำเช่นนั้นโดยใช้db-utilโครงการโอเพ่นซอร์ส

ก่อนอื่นคุณต้องเพิ่มการพึ่งพา Maven ต่อไปนี้:

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>db-util</artifactId>
    <version>${db-util.version}</version>
</dependency>

หลังจากนั้นคุณเพียงแค่ต้องใช้SQLStatementCountValidatorยูทิลิตี้เพื่อยืนยันคำสั่ง SQL พื้นฐานที่สร้างขึ้น:

SQLStatementCountValidator.reset();

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

SQLStatementCountValidator.assertSelectCount(1);

ในกรณีที่คุณใช้FetchType.EAGERและเรียกใช้กรณีทดสอบข้างต้นคุณจะได้รับกรณีทดสอบล้มเหลว:

SELECT 
    pc.id as id1_1_, 
    pc.post_id as post_id3_1_, 
    pc.review as review2_1_ 
FROM 
    post_comment pc

SELECT p.id as id1_0_0_, p.title as title2_0_0_ FROM post p WHERE p.id = 1

SELECT p.id as id1_0_0_, p.title as title2_0_0_ FROM post p WHERE p.id = 2


-- SQLStatementCountMismatchException: Expected 1 statement(s) but recorded 3 instead!

สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับdb-utilโครงการเปิดแหล่งที่มาตรวจสอบบทความนี้


แต่ตอนนี้คุณมีปัญหากับการแบ่งหน้า หากคุณมี 10 คันรถแต่ละคันมี 4 ล้อและคุณต้องการให้เลขหน้ากับ 5 คันต่อหน้า SELECT cars, wheels FROM cars JOIN wheels LIMIT 0, 5ดังนั้นคุณโดยทั่วไปคุณมี แต่สิ่งที่คุณจะได้รับคือ 2 คันที่มี 5 ล้อ (รถคันแรกที่มีทั้ง 4 ล้อและรถคันที่สองที่มีเพียง 1 ล้อ) เพราะ LIMIT จะ จำกัด ผลการค้นหาทั้งหมดไม่เพียง แต่ข้อราก
CappY

2
ฉันมีบทความสำหรับเรื่องนั้นด้วย
Vlad Mihalcea

ขอบคุณสำหรับบทความ ฉันจะอ่านมัน โดยการเลื่อนอย่างรวดเร็ว - ฉันเห็นวิธีแก้ปัญหานั้นคือฟังก์ชั่น Window แต่พวกเขาค่อนข้างใหม่ใน MariaDB - ดังนั้นปัญหายังคงอยู่ในเวอร์ชันที่เก่ากว่า :)
CappY

@VladMihalcea ฉันชี้ให้เห็นจากบทความของคุณหรือจากโพสต์ทุกครั้งที่คุณอ้างถึงกรณี ManyToOne ในขณะที่อธิบายปัญหา N + 1 แต่จริงๆแล้วคนส่วนใหญ่สนใจในกรณี OneToMany ที่เกี่ยวข้องกับปัญหา N + 1 คุณช่วยอธิบายและอธิบายกรณี OneToMany ได้ไหม
JJ Beam

18

สมมติว่าคุณมี บริษัท และพนักงาน บริษัท มีพนักงานจำนวนมาก (เช่น EMPLOYEE มีฟิลด์ COMPANY_ID)

ในบางการกำหนดค่า O / R เมื่อคุณมีวัตถุ บริษัท แมปและไปในการเข้าถึงวัตถุพนักงานของเครื่องมือ O / R จะดำเนินการอย่างหนึ่งเลือกสำหรับพนักงานทุก wheras ถ้าคุณเป็นเพียงแค่ทำในสิ่งที่ตรง SQL select * from employees where company_id = XXคุณสามารถทำได้ ดังนั้น N (จำนวนพนักงาน) บวก 1 (บริษัท )

นี่คือวิธีที่ EJB Entity Beans เวอร์ชันเริ่มต้นทำงาน ฉันเชื่อว่าสิ่งต่าง ๆ เช่น Hibernate ทำไปแล้ว แต่ฉันก็ไม่แน่ใจเหมือนกัน เครื่องมือส่วนใหญ่มักจะมีข้อมูลเกี่ยวกับกลยุทธ์ในการทำแผนที่


18

นี่คือคำอธิบายที่ดีของปัญหา

หลังจากที่คุณเข้าใจปัญหาที่สามารถหลีกเลี่ยงได้โดยการเข้าร่วมการดึงข้อมูลในแบบสอบถามของคุณ สิ่งนี้บังคับให้ดึงข้อมูลของวัตถุที่โหลดแบบสันหลังยาวดังนั้นข้อมูลจะถูกดึงในหนึ่งแบบสอบถามแทนการสอบถาม n + 1 หวังว่านี่จะช่วยได้


17

ตรวจสอบ Ayende โพสต์ในหัวข้อ: การต่อสู้กับการเลือก N + 1 ปัญหาใน NHibernate

โดยทั่วไปเมื่อใช้ ORM เช่น NHibernate หรือ EntityFramework หากคุณมีความสัมพันธ์แบบหนึ่งต่อหลายคน (รายละเอียดหลัก) และต้องการแสดงรายละเอียดทั้งหมดต่อเรคคอร์ดหลักคุณต้องทำการสอบถาม N + 1 ไปที่ ฐานข้อมูล "N" คือจำนวนเรคคอร์ดหลัก: 1 เคียวรีเพื่อรับเรคคอร์ดหลักทั้งหมดและเคียวรี N หนึ่งรายการต่อเรคคอร์ดหลักเพื่อรับรายละเอียดทั้งหมดต่อเรคคอร์ดหลัก

มีการเรียกคิวรีฐานข้อมูลเพิ่มเติม→เวลาแฝงมากขึ้น→ลดประสิทธิภาพของแอปพลิเคชั่น / ฐานข้อมูล

อย่างไรก็ตาม ORMs มีตัวเลือกเพื่อหลีกเลี่ยงปัญหานี้ส่วนใหญ่ใช้ JOIN


3
ตัวเชื่อมไม่ใช่วิธีแก้ปัญหาที่ดี (บ่อยครั้ง) เพราะอาจทำให้เกิดผลิตภัณฑ์แบบคาร์ทีเซียนซึ่งหมายความว่าจำนวนแถวผลลัพธ์คือจำนวนผลลัพธ์ตารางรากคูณด้วยจำนวนผลลัพธ์ในตารางลูกแต่ละตาราง โดยเฉพาะอย่างยิ่งไม่ดีในหลายระดับ herarchy การเลือก 20 "บล็อก" กับ 100 "โพสต์" ในแต่ละและ 10 "ความคิดเห็น" ในแต่ละโพสต์จะส่งผลให้แถวผล 20,000,000 NHibernate มีวิธีแก้ปัญหาเช่น "ชุดขนาด" (เลือกลูกที่มีในส่วนคำสั่งในรหัสผู้ปกครอง) หรือ "เลือกย่อย"
Erik Hart

14

มันเร็วกว่ามากในการออก 1 คิวรีซึ่งให้ผลลัพธ์ 100 ผลลัพธ์มากกว่าการออก 100 คิวรีซึ่งแต่ละผลลัพธ์ส่งคืน 1 ผลลัพธ์


13

ในความคิดของฉันบทความที่เขียนในPitfalls ไฮเบอร์เนต: ทำไมความสัมพันธ์ควรจะขี้เกียจตรงข้ามกับปัญหา N + 1 ที่แท้จริงคือ

หากคุณต้องการคำอธิบายที่ถูกต้องโปรดอ้างอิงไฮเบอร์เนต - บทที่ 19: การปรับปรุงประสิทธิภาพ - การดึงกลยุทธ์

เลือกการดึงข้อมูล (ค่าเริ่มต้น) มีความเสี่ยงสูงที่ N + 1 จะเลือกปัญหาดังนั้นเราอาจต้องการเปิดใช้งานการเข้าร่วมการดึงข้อมูล


2
ฉันอ่านหน้าไฮเบอร์เนต มันไม่ได้บอกว่าสิ่งที่เป็นปัญหา N + 1 เลือกจริงคือ แต่มันบอกว่าคุณสามารถใช้การรวมเพื่อแก้ไขได้
เอียนบอยด์

3
ขนาดแบทช์เป็นสิ่งจำเป็นสำหรับการดึงข้อมูลที่เลือกเพื่อเลือกวัตถุลูกสำหรับผู้ปกครองหลายคนในหนึ่งคำสั่งเลือก Subselect อาจเป็นอีกทางเลือกหนึ่ง การเข้าร่วมอาจแย่ได้ถ้าคุณมีหลายลำดับชั้นและสร้างผลิตภัณฑ์คาร์ทีเซียน
Erik Hart

10

ลิงก์ที่ให้มามีตัวอย่างง่ายๆของปัญหา n + 1 ถ้าคุณใช้กับไฮเบอร์เนตมันเป็นเรื่องที่พูดกันโดยทั่วไป เมื่อคุณสอบถามวัตถุจะมีการโหลดเอนทิตี แต่การเชื่อมโยงใด ๆ (ยกเว้นการกำหนดค่าไว้เป็นอย่างอื่น) จะถูกโหลดอย่างขี้เกียจ ดังนั้นหนึ่งแบบสอบถามสำหรับวัตถุรากและแบบสอบถามอื่นเพื่อโหลดการเชื่อมโยงสำหรับแต่ละเหล่านี้ วัตถุที่ส่งคืน 100 รายการหมายถึงแบบสอบถามเริ่มต้นหนึ่งรายการและจากนั้น 100 แบบสอบถามเพิ่มเติมเพื่อให้ได้การเชื่อมโยงสำหรับแต่ละรายการ n + 1

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


9

เศรษฐีหนึ่งคนไม่มีรถยนต์ คุณต้องการล้อทั้งหมด (4) ชิ้น

แบบสอบถามหนึ่ง (1) โหลดรถยนต์ทั้งหมด แต่สำหรับรถ (N) แต่ละคันจะมีการส่งแบบสอบถามแยกต่างหากสำหรับล้อโหลด

ค่าใช้จ่าย:

สมมติว่าดัชนีพอดีกับ ram

1 + N เคียวรีวิเคราะห์และวางแผน + ค้นหาดัชนีและ 1 + N + (N * 4) เข้าถึงเพลทสำหรับการโหลดเพย์โหลด

สมมติว่าดัชนีไม่พอดีกับหน่วยความจำ

ค่าใช้จ่ายเพิ่มเติมในกรณีที่เลวร้ายที่สุด 1 + N plate เข้าถึงดัชนีการโหลด

สรุป

คอขวดคือการเข้าถึงแผ่น (แคลิฟอร์เนีย 70 ครั้งต่อวินาทีเข้าถึงสุ่มบน hdd) การเลือกการเข้าร่วมที่กระตือรือร้นก็จะเข้าถึงแผ่น 1 + N + (N * 4) ครั้งสำหรับการบรรจุ ดังนั้นถ้าดัชนีพอดีกับหน่วยความจำ - ไม่มีปัญหามันเร็วพอเพราะมีเพียงหน่วยปฏิบัติการของแรมเท่านั้นที่เกี่ยวข้อง


9

ปัญหาการเลือก N + 1 เป็นความเจ็บปวดและเหมาะสมในการตรวจสอบกรณีดังกล่าวในการทดสอบหน่วย ฉันได้พัฒนาห้องสมุดขนาดเล็กสำหรับตรวจสอบจำนวนแบบสอบถามที่ดำเนินการโดยวิธีทดสอบที่กำหนดหรือเพียงแค่บล็อกรหัสโดยพลการ - JDBC Sniffer

เพียงเพิ่มกฎ JUnit พิเศษให้กับคลาสทดสอบของคุณและใส่หมายเหตุประกอบไว้ด้วยจำนวนคำค้นหาที่คาดหวังในวิธีทดสอบของคุณ:

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

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

5

ปัญหาตามที่คนอื่น ๆ ระบุไว้อย่างหรูหรามากขึ้นคือคุณมีผลิตภัณฑ์คาร์ทีเซียนของคอลัมน์ OneToMany หรือคุณเลือก N + 1 อาจเป็นชุดผลลัพธ์ขนาดยักษ์หรือช่างพูดกับฐานข้อมูลตามลำดับ

ฉันประหลาดใจนี้ไม่ได้กล่าวถึง แต่วิธีการที่ฉันได้รับแก้ไขปัญหานี้ ... ฉันจะทำให้ตารางรหัสกึ่งชั่วคราว ฉันยังทำเช่นนี้เมื่อคุณมีIN ()ข้อ จำกัด ข้อ

วิธีนี้ใช้ไม่ได้กับทุกกรณี (อาจไม่ได้เป็นเสียงส่วนใหญ่) แต่ก็ใช้งานได้ดีโดยเฉพาะถ้าคุณมีวัตถุเด็กมากมายเช่นสินค้าคาร์ทีเซียนจะหลุดจากมือ (เช่นOneToManyคอลัมน์จำนวนมากจำนวนผลลัพธ์จะเป็น การคูณคอลัมน์) และแบทช์เช่นเดียวกับงานมากกว่า

ก่อนอื่นให้คุณแทรกรหัสวัตถุหลักเป็นชุดลงในตารางรหัส batch_id นี้เป็นสิ่งที่เราสร้างในแอปของเราและยึดมั่น

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=(หรือกลับกัน) คุณเพียงแค่ต้องการให้แน่ใจว่าคุณสั่งซื้อจากคอลัมน์ id เพราะจะทำให้การผสานคอลัมน์ผลลัพธ์ง่ายขึ้น (ไม่เช่นนั้นคุณจะต้องมี HashMap / Table สำหรับชุดผลลัพธ์ทั้งหมดซึ่งอาจไม่เลว)

จากนั้นคุณเพียงแค่ทำความสะอาดตารางรหัส

วิธีนี้ใช้งานได้ดีโดยเฉพาะถ้าผู้ใช้เลือก 100 รายการที่แตกต่างกันสำหรับการประมวลผลจำนวนมาก ใส่รหัสที่แตกต่างกัน 100 รหัสในตารางชั่วคราว

ตอนนี้จำนวนของแบบสอบถามที่คุณทำคือจำนวนคอลัมน์ OneToMany


1

ใช้ตัวอย่างของ Matt Solnit ลองจินตนาการว่าคุณกำหนดความสัมพันธ์ระหว่าง Car กับ Wheels เป็น LAZY และคุณต้องการเขตข้อมูล Wheels บางส่วน ซึ่งหมายความว่าหลังจากการเลือกครั้งแรกไฮเบอร์เนตจะทำ "เลือก * จากล้อที่ car_id =: id" สำหรับรถยนต์แต่ละคัน

นี่เป็นตัวเลือกแรกและอีก 1 รายการที่เลือกโดยรถ N แต่ละคันนั่นคือสาเหตุที่เรียกว่าปัญหา +1

เพื่อหลีกเลี่ยงปัญหานี้ให้ทำการดึงความสัมพันธ์ออกมาอย่างกระตือรือร้นเพื่อให้จำศีลโหลดข้อมูลด้วยการเข้าร่วม

แต่ความสนใจถ้าหลาย ๆ ครั้งที่คุณไม่ได้เข้าใช้งานล้อที่เกี่ยวข้องจะเป็นการดีกว่าที่จะทำให้ LAZY หรือเปลี่ยนประเภทการดึงข้อมูลด้วย Criteria


1
การรวมเข้าด้วยกันไม่ใช่วิธีแก้ปัญหาที่ดีโดยเฉพาะเมื่อโหลดลำดับชั้นมากกว่า 2 ระดับ ทำเครื่องหมายที่ "subselect" หรือ "batch-size" แทน สุดท้ายจะโหลดเด็กโดยรหัสผู้ปกครองในข้อ "ใน" เช่น "เลือก ... จากล้อที่ car_id ใน (1,3,4,6,7,8,11,13)"
Erik Hart
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.