ความแตกต่างระหว่าง select_related และ prefetch_related ใน Django ORM คืออะไร


291

ใน Django doc

select_related() "ตาม" ความสัมพันธ์กับคีย์ต่างประเทศเลือกข้อมูลวัตถุที่เกี่ยวข้องเพิ่มเติมเมื่อเรียกใช้งานแบบสอบถาม

prefetch_related() ทำการค้นหาแยกต่างหากสำหรับแต่ละความสัมพันธ์และทำการ "เข้าร่วม" ใน Python

การทำการเข้าร่วมใน python หมายความว่าอย่างไร บางคนสามารถอธิบายด้วยตัวอย่างได้หรือไม่?

ความเข้าใจของฉันคือว่าสำหรับความสัมพันธ์ต่างประเทศที่สำคัญการใช้select_related; และสำหรับความสัมพันธ์ M2M, prefetch_relatedการใช้งาน ถูกต้องหรือไม่


2
ทำการเข้าร่วมในหลามหมายความว่าการเข้าร่วมจะไม่เกิดขึ้นในฐานข้อมูล ด้วย select_related การเข้าร่วมของคุณจะเกิดขึ้นในฐานข้อมูลและคุณได้รับแบบสอบถามฐานข้อมูลเดียวเท่านั้น ด้วย prefetch_related คุณจะดำเนินการสองแบบสอบถามแล้วผลลัพธ์จะถูก 'เข้าร่วม' โดย ORM เพื่อให้คุณยังคงสามารถพิมพ์ object.related_set
Mark Galloway

3
ในฐานะเชิงอรรถ Timmy O'Mahony ยังสามารถอธิบายความแตกต่างของพวกเขาโดยใช้ฐานข้อมูล Hit: link
Mærcos

คำตอบ:


424

ความเข้าใจของคุณส่วนใหญ่ถูกต้อง คุณสามารถใช้select_relatedเมื่อวัตถุที่คุณกำลังจะได้รับเลือกเป็นวัตถุเดียวดังนั้นหรือOneToOneField ForeignKeyคุณสามารถใช้prefetch_relatedเมื่อคุณกำลังจะได้รับ "ชุด" สิ่งดังนั้นManyToManyFields ในขณะที่คุณระบุไว้หรือย้อนกลับForeignKeys เพียงชี้แจงสิ่งที่ฉันหมายถึงโดย "ย้อนกลับForeignKeys" นี่เป็นตัวอย่าง:

class ModelA(models.Model):
    pass

class ModelB(models.Model):
    a = ForeignKey(ModelA)

ModelB.objects.select_related('a').all() # Forward ForeignKey relationship
ModelA.objects.prefetch_related('modelb_set').all() # Reverse ForeignKey relationship

ความแตกต่างคือselect_relatedการเข้าร่วม SQL และได้รับผลลัพธ์กลับเป็นส่วนหนึ่งของตารางจากเซิร์ฟเวอร์ SQL prefetch_relatedในทางกลับกันดำเนินการค้นหาอื่นและดังนั้นจึงลดคอลัมน์ซ้ำซ้อนในวัตถุเดิม ( ModelAในตัวอย่างข้างต้น) คุณสามารถใช้prefetch_relatedเพื่อสิ่งใดก็ได้ที่คุณสามารถselect_relatedใช้ได้

ข้อเสียคือprefetch_relatedต้องสร้างและส่งรายการ ID เพื่อเลือกกลับไปที่เซิร์ฟเวอร์ซึ่งอาจใช้เวลาสักครู่ ฉันไม่แน่ใจว่ามีวิธีที่ดีในการทำธุรกรรม แต่ความเข้าใจของฉันคือว่า Django มักจะส่งรายการและบอกว่า SELECT ... WHERE pk IN (... , ... , ... ) เป็นพื้น ในกรณีนี้หากข้อมูลที่ดึงข้อมูลล่วงหน้ากระจัดกระจาย (สมมติว่าวัตถุของรัฐสหรัฐอเมริกาที่เชื่อมโยงกับที่อยู่ของผู้คน) สิ่งนี้อาจดีมาก แต่ถ้าใกล้กว่าแบบหนึ่งต่อหนึ่งสิ่งนี้อาจทำให้เสียการสื่อสารจำนวนมาก หากมีข้อสงสัยลองดูและดูว่าแบบไหนดีกว่ากัน

ทุกอย่างที่กล่าวถึงข้างต้นนั้นเกี่ยวกับการสื่อสารกับฐานข้อมูล อย่างไรก็ตามในด้าน Python prefetch_relatedมีประโยชน์พิเศษที่มีการใช้วัตถุเดี่ยวเพื่อแทนแต่ละวัตถุในฐานข้อมูล ด้วยselect_relatedวัตถุที่ซ้ำกันจะถูกสร้างขึ้นใน Python สำหรับแต่ละวัตถุ "ผู้ปกครอง" เนื่องจากออบเจ็กต์ใน Python มีโอเวอร์เฮดของหน่วยความจำที่เหมาะสมจึงสามารถนำมาพิจารณาได้เช่นกัน


3
อะไรจะเร็วกว่ากัน?
เงิน elad

24
select_relatedเป็นหนึ่งข้อความค้นหาในขณะที่prefetch_relatedสองดังนั้นก่อนหน้านี้จะเร็วกว่า แต่select_relatedจะไม่ช่วยคุณในการManyToManyField's
bhinesley

31
@eladsilver ขออภัยสำหรับการตอบกลับช้า มันขึ้นอยู่กับ select_relatedใช้ JOIN ใน SQL ในขณะที่prefetch_relatedรันเคียวรีในรูปแบบแรกรวบรวม ID ทั้งหมดที่จำเป็นในการดึงข้อมูลล่วงหน้าแล้วเรียกใช้แบบสอบถามด้วยส่วนคำสั่ง IN ใน WHERE พร้อมด้วย ID ทั้งหมดที่ต้องการ หากคุณพูดว่ามี 3-5 รุ่นที่ใช้คีย์ต่างประเทศตัวเดียวกันselect_relatedเกือบจะดีกว่าแน่นอน หากคุณมีรุ่น 100 หรือ 1,000 โดยใช้คีย์ต่างประเทศเดียวกันprefetch_relatedอาจจะดีกว่า ในระหว่างนั้นคุณจะต้องทดสอบและดูว่าเกิดอะไรขึ้น
CrazyCasta

1
ฉันจะโต้แย้งความคิดเห็นของคุณเกี่ยวกับ prefetch ที่เกี่ยวข้อง "โดยทั่วไปไม่ค่อยสมเหตุสมผล" นั่นเป็นความจริงสำหรับฟิลด์ FK ที่ทำเครื่องหมายว่าไม่ซ้ำกัน แต่ทุกที่ที่หลายแถวมีค่า FK เดียวกัน (ผู้เขียนผู้ใช้หมวดหมู่เมือง ฯลฯ ฯลฯ ) การดึงข้อมูลล่วงหน้าจะลดแบนด์วิดท์ระหว่าง Django และ DB แต่ไม่ซ้ำแถว นอกจากนี้ยังใช้หน่วยความจำน้อยกว่าในฐานข้อมูลโดยทั่วไป อย่างใดอย่างหนึ่งเหล่านี้มักจะสำคัญกว่าค่าใช้จ่ายของแบบสอบถามเพิ่มเติมเดียว รับนี้เป็นคำตอบที่ดีที่สุดสำหรับคำถามที่นิยมพอสมควรฉันคิดว่าควรสังเกตในคำตอบ
Gordon Wrigley

1
@GordonWrigley ใช่มันเป็นเวลานานแล้วตั้งแต่ที่ฉันเขียนดังนั้นฉันจึงกลับไปและชี้แจงเล็กน้อย ฉันไม่แน่ใจว่าฉันเห็นด้วยกับบิต "ใช้หน่วยความจำน้อยลงใน DB" แต่ใช่กับทุกสิ่ง และสามารถใช้หน่วยความจำน้อยลงในฝั่ง Python
CrazyCasta

26

ทั้งสองวิธีบรรลุวัตถุประสงค์เดียวกันเพื่อละเว้นการสืบค้น db ที่ไม่จำเป็น แต่พวกเขาใช้วิธีการต่าง ๆ เพื่อประสิทธิภาพ

เหตุผลเดียวที่จะใช้วิธีการอย่างใดอย่างหนึ่งเหล่านี้คือเมื่อแบบสอบถามขนาดใหญ่เดียวจะดีกว่าแบบสอบถามขนาดเล็กจำนวนมาก Django ใช้คิวรีขนาดใหญ่เพื่อสร้างแบบจำลองในหน่วยความจำแบบ preemptively แทนที่จะทำตามคำสั่งที่ต้องการกับฐานข้อมูล

select_relatedทำการรวมกับการค้นหาแต่ละรายการ แต่ขยายการเลือกเพื่อรวมคอลัมน์ของตารางที่เข้าร่วมทั้งหมด อย่างไรก็ตามวิธีนี้มีข้อแม้

ตัวเชื่อมมีความเป็นไปได้ที่จะคูณจำนวนแถวในแบบสอบถาม เมื่อคุณทำการรวมคีย์ต่างประเทศหรือเขตข้อมูลแบบหนึ่งต่อหนึ่งจำนวนแถวจะไม่เพิ่มขึ้น อย่างไรก็ตามการเข้าร่วมหลายต่อหลายคนไม่มีการรับประกันนี้ ดังนั้น Django จำกัดselect_relatedการประชาสัมพันธ์ที่จะไม่ส่งผลให้เกิดการเข้าร่วมจำนวนมาก

"เข้าร่วมในหลาม"สำหรับprefetch_relatedเป็นเล็ก ๆ น้อย ๆ ที่น่ากลัวแล้วมันควรจะเป็น มันสร้างแบบสอบถามแยกต่างหากสำหรับแต่ละตารางที่จะเข้าร่วม มันกรองแต่ละตารางเหล่านี้ด้วยคำสั่ง WHERE IN เช่น:

SELECT "credential"."id",
       "credential"."uuid",
       "credential"."identity_id"
FROM   "credential"
WHERE  "credential"."identity_id" IN
    (84706, 48746, 871441, 84713, 76492, 84621, 51472);

แทนที่จะทำการเข้าร่วมเพียงครั้งเดียวที่อาจมีแถวมากเกินไปแต่ละตารางจะแบ่งออกเป็นแบบสอบถามแยกต่างหาก


1

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

สมมติว่าคุณมีโมเดล Django 3 แบบที่เกี่ยวข้องกัน

class M1(models.Model):
    name = models.CharField(max_length=10)

class M2(models.Model):
    name = models.CharField(max_length=10)
    select_relation = models.ForeignKey(M1, on_delete=models.CASCADE)
    prefetch_relation = models.ManyToManyField(to='M3')

class M3(models.Model):
    name = models.CharField(max_length=10)

ที่นี่คุณสามารถเคียวรีM2โมเดลและM1อ็อบเจ็กต์ที่เกี่ยวข้องโดยใช้select_relationฟิลด์และM3อ็อบเจ็กต์ที่ใช้prefetch_relationฟิลด์

อย่างไรก็ตามตามที่เราได้กล่าวถึงM1ความสัมพันธ์ของจากM2a ForeignKeyมันจะส่งกลับเพียง1ระเบียนสำหรับM2วัตถุใด ๆ สิ่งเดียวกันใช้สำหรับOneToOneFieldเช่นกัน

แต่M3ความสัมพันธ์จากM2นั้นเป็นสิ่งManyToManyFieldที่อาจส่งคืนจำนวนM1วัตถุใด ๆ

พิจารณากรณีที่คุณมี 2 M2วัตถุm21, m22ที่มีเดียวกัน5ที่เกี่ยวข้องวัตถุที่มีรหัสM3 1,2,3,4,5เมื่อคุณดึงข้อมูลM3วัตถุที่เกี่ยวข้องสำหรับวัตถุเหล่านั้นM2ถ้าคุณใช้ตัวเลือกที่เกี่ยวข้องนี่จะเป็นวิธีการทำงาน

ขั้นตอน:

  1. ค้นหาm21วัตถุ
  2. แบบสอบถามทั้งหมดM3วัตถุที่เกี่ยวข้องกับm21วัตถุที่มี ID 1,2,3,4,5ที่
  3. ทำซ้ำสิ่งเดียวกันสำหรับm22วัตถุและM2วัตถุอื่นทั้งหมด

เนื่องจากเรามี1,2,3,4,5ID เดียวกันทั้งm21m22อบเจ็กต์ถ้าเราใช้ตัวเลือก select_related มันจะทำการสืบค้น DB สองครั้งสำหรับ ID เดียวกันซึ่งถูกดึงออกมาแล้ว

แต่ถ้าคุณใช้ prefetch_related เมื่อคุณพยายามรับM2วัตถุมันจะจดบันทึก ID ทั้งหมดที่วัตถุของคุณส่งคืน (หมายเหตุ: เฉพาะ ID เท่านั้น) ในขณะที่ทำการสืบค้นM2ตารางและในขั้นตอนสุดท้าย Django จะทำการสืบค้นที่M3ตาราง ด้วยชุดของ ID ทั้งหมดที่M2วัตถุของคุณส่งคืน และเข้าร่วมกับM2วัตถุโดยใช้ Python แทนฐานข้อมูล

วิธีนี้คุณจะทำการค้นหาM3วัตถุทั้งหมดเพียงครั้งเดียวซึ่งจะปรับปรุงประสิทธิภาพ


0

ในฐานะที่เป็นเอกสาร Django พูดว่า:

prefetch_related ()

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

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

select_related ทำงานได้โดยการสร้างการเข้าร่วม SQL และรวมถึงฟิลด์ของวัตถุที่เกี่ยวข้องในคำสั่ง SELECT ด้วยเหตุผลนี้ select_related จะได้รับวัตถุที่เกี่ยวข้องในการสืบค้นฐานข้อมูลเดียวกัน อย่างไรก็ตามเพื่อหลีกเลี่ยงชุดผลลัพธ์ที่มีขนาดใหญ่กว่าซึ่งเป็นผลมาจากการเข้าร่วมในความสัมพันธ์แบบ 'จำนวนมาก' select_related จะถูก จำกัด เพียงความสัมพันธ์ที่มีค่าเดียว - คีย์ต่างประเทศและตัวต่อตัว

prefetch_related จะทำการค้นหาแยกต่างหากสำหรับแต่ละความสัมพันธ์และทำการ 'เข้าร่วม' ใน Python สิ่งนี้ช่วยให้สามารถดึงวัตถุหลายต่อหลายคนและหลายต่อหลายคนซึ่งไม่สามารถทำได้โดยใช้ select_related นอกเหนือจากคีย์ต่างประเทศและความสัมพันธ์แบบหนึ่งต่อหนึ่งที่สนับสนุนโดย select_related นอกจากนี้ยังสนับสนุนการดึงข้อมูลล่วงหน้าของ GenericRelation และ GenericForeignKey อย่างไรก็ตามจะต้องถูก จำกัด ให้เป็นชุดผลลัพธ์ที่เป็นเนื้อเดียวกัน ตัวอย่างเช่นการดึงวัตถุล่วงหน้าที่อ้างอิงโดย GenericForeignKey จะได้รับการสนับสนุนก็ต่อเมื่อเคียวรีถูก จำกัด เพียงหนึ่ง ContentType

ข้อมูลเพิ่มเติมเกี่ยวกับเรื่องนี้: https://docs.djangoproject.com/en/2.2/ref/models/querysets/#prefetch-related

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.