จะดึงบันทึกแบบสุ่มโดยใช้ ORM ของ Django ได้อย่างไร


176

ฉันมีแบบจำลองที่แสดงภาพวาดที่ฉันนำเสนอบนเว็บไซต์ของฉัน ในหน้าเว็บหลักฉันต้องการแสดงบางส่วน: ใหม่ที่สุดหน้าหนึ่งที่ไม่ได้เข้าชมบ่อยครั้งหน้ายอดนิยมและหน้าจอสุ่ม

ฉันใช้ Django 1.0.2

ในขณะที่ 3 คนแรกของพวกเขานั้นง่ายต่อการดึงโดยใช้โมเดล django แบบสุดท้าย (แบบสุ่ม) ทำให้ฉันมีปัญหา ฉันสามารถ ofc รหัสในมุมมองของฉันเพื่อสิ่งนี้:

number_of_records = models.Painting.objects.count()
random_index = int(random.random()*number_of_records)+1
random_paint = models.Painting.get(pk = random_index)

มันไม่เหมือนสิ่งที่ฉันต้องการในมุมมองของฉัน - นี่เป็นส่วนหนึ่งของสิ่งที่เป็นนามธรรมและควรจะอยู่ในรูปแบบ นอกจากนี้ที่นี่ฉันต้องดูแลบันทึกที่ถูกลบ (จำนวนระเบียนทั้งหมดจะไม่ครอบคลุมค่าคีย์ที่เป็นไปได้ทั้งหมด) และอาจมีสิ่งอื่น ๆ อีกมากมาย

ตัวเลือกอื่น ๆ ที่ฉันสามารถทำได้โดยเฉพาะอย่างยิ่งในสิ่งที่เป็นนามธรรมรุ่น?


วิธีที่คุณแสดงสิ่งต่าง ๆ และสิ่งที่คุณแสดงเป็นส่วนหนึ่งของระดับ "มุมมอง" หรือตรรกะทางธุรกิจที่ควรอยู่ในระดับ "ตัวควบคุม" ของ MVC ในความคิดของฉัน
Gabriele D'Antona

ใน Django คอนโทรลเลอร์คือมุมมอง docs.djangoproject.com/th/dev/faq/general/?hl=th

คำตอบ:


169

ใช้order_by('?')จะฆ่าเซิร์ฟเวอร์ db ในวันที่สองในการผลิต วิธีที่ดีกว่าเป็นสิ่งที่ชอบสิ่งที่อธิบายไว้ในการเดินทางแถวสุ่มจากฐานข้อมูลเชิงสัมพันธ์

from django.db.models.aggregates import Count
from random import randint

class PaintingManager(models.Manager):
    def random(self):
        count = self.aggregate(count=Count('id'))['count']
        random_index = randint(0, count - 1)
        return self.all()[random_index]

45
ประโยชน์ของการได้model.objects.aggregate(count=Count('id'))['count']มากกว่าคืออะไรmodel.objects.all().count()
Ryan Saxe

11
ในขณะที่ดีกว่าคำตอบที่ได้รับการยอมรับโปรดทราบว่าวิธีนี้ทำให้แบบสอบถาม SQL สองแบบสอบถาม หากการนับมีการเปลี่ยนแปลงในระหว่างอาจเป็นไปได้ที่จะได้รับข้อผิดพลาดนอกขอบเขต
Nelo Mitranim

2
นี่เป็นทางออกที่ผิด มันจะไม่ทำงานหากรหัสของคุณไม่ได้เริ่มต้นที่ 0 และเมื่อรหัสไม่ต่อเนื่องกัน สมมติว่าระเบียนแรกเริ่มต้นจาก 500 และบันทึกสุดท้ายคือ 599 (สมมติว่าเป็นเรื่องต่อเนื่อง) จากนั้นจำนวนจะเป็น 54950 แน่นอนไม่มีรายการ [54950] เนื่องจากความยาวของ queryst ของคุณคือ 100 มันจะส่งดัชนีออกจากข้อยกเว้นที่ถูกผูกไว้ ฉันไม่ทราบว่าทำไมคนจำนวนมากจึงสนับสนุนเรื่องนี้และนี่ถูกทำเครื่องหมายว่าเป็นคำตอบที่ยอมรับได้
sajid

1
@sajid: ทำไมคุณถามฉัน มันค่อนข้างง่ายที่จะเห็นผลรวมทั้งหมดของการมีส่วนร่วมของฉันสำหรับคำถามนี้: แก้ไขลิงก์เพื่อชี้ไปยังที่เก็บถาวรหลังจากที่มันหายไป ฉันไม่ได้โหวตแม้แต่คำตอบใด ๆ แต่ผมพบว่ามันสนุกว่าคำตอบนี้และคนที่คุณเรียกร้องให้มีการใช้งานที่ดีมากทั้งสอง.all()[randint(0, count - 1)]มีผลบังคับใช้ บางทีคุณควรมุ่งเน้นไปที่การระบุว่าส่วนใดของคำตอบนั้นผิดหรืออ่อนแอแทนที่จะนิยาม "ข้อผิดพลาดแบบ" ต่อหนึ่งข้อ "สำหรับเราและตะโกนใส่ผู้มีสิทธิเลือกตั้งที่โง่เขลา (อาจเป็นเพราะมันไม่ได้ใช้งาน.objectsเหรอ)
Nathan Tuggy

3
@NathanTuggy ตกลงฉันไม่ดี ขออภัย
sajid

260

เพียงใช้:

MyModel.objects.order_by('?').first()

มันถูกบันทึกไว้ในชุดข้อความ API


71
โปรดทราบว่าวิธีการนี้อาจจะช้ามากเป็นเอกสาร :)
นิโคลัส Dumazet

6
"อาจแพงและช้าขึ้นอยู่กับแบ็กเอนด์ฐานข้อมูลที่คุณใช้" - ประสบการณ์ใดในแบ็กเอนด์ DB ที่แตกต่าง? (SQLite / MySQL / postgres)?
kender

4
ฉันยังไม่ได้ทดสอบดังนั้นนี่เป็นการคาดเดาที่แท้จริง: ทำไมมันช้ากว่าการดึงไอเท็มทั้งหมดและทำการสุ่มใน Python
muhuk

8
ฉันอ่านว่ามันช้าใน mysql เนื่องจาก mysql มีการสุ่มสั่งที่ไม่มีประสิทธิภาพอย่างไม่น่าเชื่อ
Brandon Henry

33
ทำไมไม่เป็นเช่นนั้นrandom.choice(Model.objects.all())?
Jamey

25

โซลูชันที่มี order_by ('?') [: N] นั้นช้ามากแม้กับตารางขนาดกลางถ้าคุณใช้ MySQL (ไม่รู้ฐานข้อมูลอื่น)

order_by('?')[:N]จะถูกแปลเป็นSELECT ... FROM ... WHERE ... ORDER BY RAND() LIMIT Nข้อความค้นหา

หมายความว่าสำหรับทุกแถวในตารางฟังก์ชัน RAND () จะถูกดำเนินการจากนั้นตารางทั้งหมดจะถูกจัดเรียงตามค่าของฟังก์ชั่นนี้จากนั้นเรคคอร์ด N แรกจะถูกส่งกลับ หากโต๊ะของคุณมีขนาดเล็กก็ถือว่าใช้ได้ แต่ในกรณีส่วนใหญ่นี่เป็นคำถามที่ช้ามาก

ฉันเขียนฟังก์ชันง่าย ๆ ที่ใช้งานได้แม้ว่า id จะมีรู (บางแถวที่ถูกลบ):

def get_random_item(model, max_id=None):
    if max_id is None:
        max_id = model.objects.aggregate(Max('id')).values()[0]
    min_id = math.ceil(max_id*random.random())
    return model.objects.filter(id__gte=min_id)[0]

มันเร็วกว่า order_by ('?') ในเกือบทุกกรณี


30
นอกจากนี้น่าเศร้าที่มันอยู่ไกลจากการสุ่ม หากคุณมีบันทึกที่มี id 1 และอีกรายการที่มี id 100 จะมีการส่งคืนระเบียนที่สอง 99% ของเวลา
DS

16

นี่เป็นวิธีง่ายๆ:

from random import randint

count = Model.objects.count()
random_object = Model.objects.all()[randint(0, count - 1)] #single random object

10

คุณสามารถสร้างผู้จัดการในแบบจำลองของคุณเพื่อทำสิ่งนี้ เข้าใจสิ่งแรกผู้จัดการคือPainting.objectsวิธีการเป็นผู้จัดการที่มีall(), filter(), get()ฯลฯ การสร้างผู้จัดการของคุณเองช่วยให้คุณสามารถกรองก่อนผลและมีวิธีการเหล่านี้เหมือนกันเช่นเดียวกับวิธีของคุณเองในการทำงานเกี่ยวกับผลการ .

แก้ไข : ฉันแก้ไขรหัสเพื่อให้สอดคล้องกับorder_by['?']วิธีการ โปรดทราบว่าผู้จัดการส่งคืนโมเดลแบบสุ่มไม่ จำกัด จำนวน ด้วยเหตุนี้ฉันจึงรวมรหัสการใช้งานเล็กน้อยเพื่อแสดงวิธีรับรุ่นเดียว

from django.db import models

class RandomManager(models.Manager):
    def get_query_set(self):
        return super(RandomManager, self).get_query_set().order_by('?')

class Painting(models.Model):
    title = models.CharField(max_length=100)
    author = models.CharField(max_length=50)

    objects = models.Manager() # The default manager.
    randoms = RandomManager() # The random-specific manager.

การใช้

random_painting = Painting.randoms.all()[0]

สุดท้ายคุณสามารถมีผู้จัดการหลายคนในแบบจำลองของคุณได้ดังนั้นอย่าลังเลที่จะสร้างLeastViewsManager()หรือMostPopularManager()


3
การใช้ get () จะใช้ได้ก็ต่อเมื่อ pks ของคุณนั้นต่อเนื่องกันนั่นคือคุณจะไม่ลบรายการใด ๆ มิฉะนั้นคุณน่าจะลองรับ pk ที่ไม่มีอยู่ การใช้. all () [random_index] ไม่ประสบปัญหานี้และไม่มีประสิทธิภาพน้อยลง
Daniel Roseman

ฉันเข้าใจว่าเหตุใดตัวอย่างของฉันจึงลอกแบบรหัสของคำถามกับผู้จัดการ มันจะยังคงขึ้นอยู่กับ OP เพื่อตรวจสอบขอบเขตของเขา
Soviut

1
แทนที่จะใช้. get (id = random_index) จะดีกว่าไหมถ้าใช้. filter (id__gte = random_index) [0: 1]? ก่อนจะช่วยแก้ปัญหาด้วย pks ไม่ต่อเนื่อง ประการที่สอง get_query_set ควรกลับ ... QuerySet และในตัวอย่างของคุณมันไม่ได้
Nicolas Dumazet

2
ฉันจะไม่สร้างผู้จัดการคนใหม่เพื่อสร้างวิธีการหนึ่ง ฉันจะเพิ่ม "get_random" ให้กับตัวจัดการเริ่มต้นเพื่อที่คุณจะได้ไม่ต้องผ่านทุกตัว () [0] ห่วงทุกครั้งที่คุณต้องการภาพแบบสุ่ม นอกจากนี้หากผู้เขียนเป็นชาวต่างชาติที่สำคัญกับรูปแบบผู้ใช้คุณสามารถพูดว่า user.painting_set.get_random ()
Antti Rasinen

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

6

คำตอบอื่น ๆ อาจช้า (ใช้order_by('?')) หรือใช้แบบสอบถาม SQL มากกว่าหนึ่ง นี่คือตัวอย่างโซลูชันที่ไม่มีการสั่งซื้อและมีเพียงหนึ่งแบบสอบถาม (สมมติว่า Postgres):

Model.objects.raw('''
    select * from {0} limit 1
    offset floor(random() * (select count(*) from {0}))
'''.format(Model._meta.db_table))[0]

โปรดทราบว่าสิ่งนี้จะเพิ่มข้อผิดพลาดของดัชนีหากตารางว่างเปล่า เขียนฟังก์ชั่นตัวช่วยแบบไม่เชื่อเรื่องพระเจ้าเพื่อตรวจสอบสิ่งนั้น


หลักฐานของแนวคิดที่ดี แต่นี่เป็นแบบสอบถามสองรายการภายในฐานข้อมูลสิ่งที่คุณบันทึกคือหนึ่งไปกลับไปยังฐานข้อมูล คุณจะต้องดำเนินการนี้หลาย ๆ ครั้งเพื่อทำการเขียนและรักษาแบบสอบถามแบบดิบที่คุ้มค่า และถ้าคุณต้องการป้องกันตารางที่ว่างเปล่าคุณก็อาจเรียกใช้ a count()ล่วงหน้าและแจกจ่ายด้วยการสืบค้นแบบดิบ
ทั้ง

2

เป็นความคิดง่ายๆที่ฉันจะทำ:

def _get_random_service(self, professional):
    services = Service.objects.filter(professional=professional)
    i = randint(0, services.count()-1)
    return services[i]

1

เพียงเพื่อให้ทราบถึงกรณีพิเศษ (ค่อนข้างธรรมดา) หากมีคอลัมน์เพิ่มอัตโนมัติในตารางที่ไม่มีการลบวิธีที่เหมาะสมที่สุดในการเลือกแบบสุ่มคือแบบสอบถามเช่น:

SELECT * FROM table WHERE id = RAND() LIMIT 1

ที่ถือว่าคอลัมน์ชื่อ id เป็นตาราง ใน django คุณสามารถทำได้โดย:

Painting.objects.raw('SELECT * FROM appname_painting WHERE id = RAND() LIMIT 1')

คุณต้องแทนที่ชื่อแอปด้วยชื่อแอปพลิเคชันของคุณ

โดยทั่วไปด้วยคอลัมน์ id order_by ('?') สามารถทำได้เร็วกว่ามากด้วย:

Paiting.objects.raw(
        'SELECT * FROM auth_user WHERE id>=RAND() * (SELECT MAX(id) FROM auth_user) LIMIT %d' 
    % needed_count)

1

นี่คือขอแนะนำอย่างมากการรับแถวสุ่มจากฐานข้อมูลเชิงสัมพันธ์

เพราะการใช้ django orm ทำสิ่งเช่นนั้นจะทำให้เซิร์ฟเวอร์ db ของคุณโกรธเป็นพิเศษหากคุณมีตารางข้อมูลขนาดใหญ่: |

และการแก้ปัญหาคือให้เป็นผู้จัดการรุ่นและเขียนแบบสอบถาม SQL ด้วยมือ;)

อัปเดต :

โซลูชั่นที่ทำงานบนฐานข้อมูลใด ๆ อีกส่วนหลังแม้แต่คนที่ไม่ใช่ rel ModelManagerโดยไม่ต้องเขียนกำหนดเอง รับวัตถุสุ่มจาก Queryset ใน Django


1

คุณอาจต้องการที่จะใช้วิธีการเดียวกันที่คุณต้องการใช้ในการลิ้มลอง iterator ใด ๆ โดยเฉพาะอย่างยิ่งถ้าคุณวางแผนที่จะหลายรายการตัวอย่างในการสร้างชุดตัวอย่าง @MatijnPieters และ @DzinX นำความคิดมาสู่สิ่งนี้:

def random_sampling(qs, N=1):
    """Sample any iterable (like a Django QuerySet) to retrieve N random elements

    Arguments:
      qs (iterable): Any iterable (like a Django QuerySet)
      N (int): Number of samples to retrieve at random from the iterable

    References:
      @DZinX:  https://stackoverflow.com/a/12583436/623735
      @MartinPieters: https://stackoverflow.com/a/12581484/623735
    """
    samples = []
    iterator = iter(qs)
    # Get the first `N` elements and put them in your results list to preallocate memory
    try:
        for _ in xrange(N):
            samples.append(iterator.next())
    except StopIteration:
        raise ValueError("N, the number of reuested samples, is larger than the length of the iterable.")
    random.shuffle(samples)  # Randomize your list of N objects
    # Now replace each element by a truly random sample
    for i, v in enumerate(qs, N):
        r = random.randint(0, i)
        if r < N:
            samples[r] = v  # at a decreasing rate, replace random items
    return samples

โซลูชันของ Matijn และ DxinX สำหรับชุดข้อมูลที่ไม่มีการเข้าถึงแบบสุ่ม สำหรับชุดข้อมูลที่ทำ (และ SQL ทำด้วยOFFSET) สิ่งนี้ไม่มีประสิทธิภาพโดยไม่จำเป็น
ทั้ง

@ EndreBoth แน่นอน ฉันชอบการเข้ารหัส "ประสิทธิภาพ" ของการใช้วิธีการเดียวกันโดยไม่คำนึงถึงแหล่งข้อมูล บางครั้งประสิทธิภาพของการสุ่มตัวอย่างข้อมูลจะไม่ส่งผลกระทบต่อประสิทธิภาพของไปป์ไลน์ที่ จำกัด โดยกระบวนการอื่น ๆ (สิ่งที่คุณกำลังทำกับข้อมูลเช่นการฝึกอบรม ML)
เตา

1

วิธีที่ง่ายกว่ามากในการทำเช่นนี้คือการกรองลงไปยังชุดระเบียนที่สนใจและใช้random.sampleในการเลือกให้มากที่สุด

from myapp.models import MyModel
import random

my_queryset = MyModel.objects.filter(criteria=True)  # Returns a QuerySet
my_object = random.sample(my_queryset, 1)  # get a single random element from my_queryset
my_objects = random.sample(my_queryset, 5)  # get five random elements from my_queryset

โปรดทราบว่าคุณควรมีรหัสบางส่วนเพื่อยืนยันว่าmy_querysetไม่ว่างเปล่า random.sampleผลตอบแทนValueError: sample larger than populationถ้าอาร์กิวเมนต์แรกมีองค์ประกอบน้อยเกินไป


2
สิ่งนี้จะทำให้ชุดสืบค้นทั้งหมดถูกสืบค้นหรือไม่?
perrohunter

@perrohunter มันจะไม่ทำงานด้วยQueryset(อย่างน้อยกับ Python 3.7 และ Django 2.1); คุณต้องแปลงเป็นรายการก่อนซึ่งจะเรียกชุดแบบสอบถามทั้งหมดอย่างชัดเจน
ทั้ง

@EndreBoth - ถูกเขียนในปี 2559 เมื่อไม่มีสิ่งเหล่านี้
eykanal

นั่นเป็นเหตุผลที่ฉันเพิ่มข้อมูลรุ่น แต่ถ้ามันใช้งานได้ในปี 2559 มันทำได้โดยดึงคิวรีทั้งหมดในรายการใช่มั้ย
Endre ทั้ง

@EndreBoth ถูกต้อง
eykanal

1

สวัสดีฉันต้องการที่จะเลือกบันทึกแบบสุ่มจากชุดแบบสอบถามที่มีความยาวฉันก็ต้องรายงาน (เช่นหน้าเว็บที่ผลิตรายการที่อธิบายไว้และกล่าวว่าบันทึกทางซ้าย)

q = Entity.objects.filter(attribute_value='this or that')
item_count = q.count()
random_item = q[random.randomint(1,item_count+1)]

ใช้เวลาครึ่งนาน (0.7 วินาทีกับ 1.7 วินาที) เมื่อ:

item_count = q.count()
random_item = random.choice(q)

ฉันเดาว่าจะหลีกเลี่ยงการดึงข้อความค้นหาทั้งหมดก่อนที่จะเลือกรายการแบบสุ่มและทำให้ระบบของฉันตอบสนองได้เพียงพอสำหรับหน้าเว็บที่มีการเข้าถึงซ้ำ ๆ สำหรับงานซ้ำ ๆ ที่ผู้ใช้ต้องการเห็นรายการนับจำนวน


0

วิธีการสำหรับคีย์หลักที่เพิ่มขึ้นอัตโนมัติโดยไม่มีการลบ

หากคุณมีตารางที่คีย์หลักเป็นจำนวนเต็มตามลำดับโดยไม่มีช่องว่างวิธีการดังต่อไปนี้ควรใช้งานได้:

import random
max_id = MyModel.objects.last().id
random_id = random.randint(0, max_id)
random_obj = MyModel.objects.get(pk=random_id)

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

ในกรณีที่แถวถูกลบเช่นช่องว่างวิธีนี้จะยังคงทำงานถ้ามันถูกลองใหม่จนกว่าจะมีการเลือกคีย์หลักที่มีอยู่แบบสุ่ม

อ้างอิง


0

ฉันมีวิธีแก้ปัญหาที่ง่ายมากทำให้ผู้จัดการที่กำหนดเอง:

class RandomManager(models.Manager):
    def random(self):
        return random.choice(self.all())

แล้วเพิ่มในรูปแบบ:

class Example(models.Model):
    name = models.CharField(max_length=128)
    objects = RandomManager()

ตอนนี้คุณสามารถใช้มัน:

Example.objects.random()

จากตัวเลือกการนำเข้าแบบสุ่ม
Adam Starrh

3
กรุณาอย่าใช้วิธีนี้ถ้าคุณต้องการความเร็ว วิธีนี้ช้ามาก ฉันตรวจสอบแล้ว มันช้ากว่าorder_by('?').first()กว่า 60 เท่า
LagRange

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