Bagaimana cara menarik catatan acak menggunakan ORANG Django?


176

Saya memiliki model yang mewakili lukisan yang saya tampilkan di situs saya. Di halaman web utama saya ingin menunjukkan beberapa di antaranya: yang terbaru, yang tidak dikunjungi untuk sebagian besar waktu, yang paling populer, dan yang acak.

Saya menggunakan Django 1.0.2.

Sementara 3 pertama dari mereka mudah ditarik menggunakan model Django, yang terakhir (acak) menyebabkan saya beberapa masalah. Saya dapat ofc kode itu dalam pandangan saya, untuk sesuatu seperti ini:

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

Itu tidak terlihat seperti sesuatu yang saya ingin miliki dalam pandangan saya tho - ini sepenuhnya merupakan bagian dari abstraksi basis data dan harus ada dalam model. Juga, di sini saya perlu mengurus catatan yang dihapus (maka jumlah semua catatan tidak akan mencakup semua nilai kunci yang mungkin) dan mungkin banyak hal lainnya.

Ada pilihan lain bagaimana saya bisa melakukannya, lebih disukai entah bagaimana di dalam abstraksi model?


Bagaimana Anda menampilkan hal-hal dan hal-hal mana yang Anda tampilkan adalah bagian dari tingkat "Lihat" atau logika bisnis yang seharusnya masuk dalam tingkat "Pengendali" MVC, menurut pendapat saya.
Gabriele D'Antona

Di Django controller adalah view. docs.djangoproject.com/en/dev/faq/general/…

Jawaban:


169

Menggunakan order_by('?')akan mematikan server db pada hari kedua dalam produksi. Cara yang lebih baik adalah sesuatu seperti yang dijelaskan dalam Mendapatkan baris acak dari database relasional .

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
Apa manfaat dari model.objects.aggregate(count=Count('id'))['count']kelebihanmodel.objects.all().count()
Ryan Saxe

11
Meskipun jauh lebih baik daripada jawaban yang diterima, perhatikan bahwa pendekatan ini membuat dua kueri SQL. Jika jumlah berubah di antaranya, dimungkinkan untuk mendapatkan kesalahan di luar batas.
Nelo Mitranim

2
Ini solusi yang salah. Ini tidak akan berfungsi jika id Anda tidak mulai dari 0. Dan juga ketika id tidak bersebelahan. Katakanlah, catatan pertama dimulai dari 500 dan yang terakhir adalah 599 (dengan asumsi kedekatan). Maka hitungannya akan menjadi 54950. Tentunya daftar [54950] tidak ada karena panjang queryst Anda adalah 100. Itu akan membuang indeks dari pengecualian terikat. Saya tidak tahu mengapa begitu banyak orang yang memilih ini dan ini ditandai sebagai jawaban yang diterima.
sajid

1
@sajid: Mengapa, tepatnya, Anda bertanya kepada saya? Sangat mudah untuk melihat jumlah total kontribusi saya untuk pertanyaan ini: mengedit tautan untuk menunjuk ke arsip setelah membusuk. Saya bahkan belum memberikan suara pada salah satu jawaban. Tapi saya merasa lucu bahwa jawaban ini dan jawaban yang Anda klaim jauh lebih baik digunakan .all()[randint(0, count - 1)]. Mungkin Anda harus fokus pada mengidentifikasi bagian mana dari jawaban yang salah atau lemah, daripada mendefinisikan ulang "off-by-one-error" untuk kami dan meneriaki pemilih yang bodoh. (Mungkin karena itu tidak digunakan .objects?)
Nathan Tuggy

3
@NathanTuggy. Ok saya salah. Maaf
sajid

260

Cukup gunakan:

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

Itu didokumentasikan dalam API QuerySet .


71
Harap dicatat bahwa pendekatan ini bisa sangat lambat, seperti yang didokumentasikan :)
Nicolas Dumazet

6
"Mungkin mahal dan lambat, tergantung pada backend database yang Anda gunakan." - Adakah pengalaman tentang backend DB yang berbeda? (sqlite / mysql / postgres)?
kender

4
Saya belum mengujinya, jadi ini spekulasi murni: mengapa harus lebih lambat daripada mengambil semua item dan melakukan pengacakan dengan Python?
muhuk

8
Saya membaca bahwa ini lambat dalam mysql, karena mysql memiliki urutan acak yang sangat tidak efisien.
Brandon Henry

33
Kenapa tidak adil random.choice(Model.objects.all())?
Jamey

25

Solusi dengan order_by ('?') [: N] sangat lambat bahkan untuk tabel berukuran sedang jika Anda menggunakan MySQL (tidak tahu tentang database lain).

order_by('?')[:N]akan diterjemahkan ke SELECT ... FROM ... WHERE ... ORDER BY RAND() LIMIT Npermintaan.

Ini berarti bahwa untuk setiap baris dalam tabel fungsi RAND () akan dieksekusi, maka seluruh tabel akan diurutkan sesuai dengan nilai fungsi ini dan kemudian catatan N pertama akan dikembalikan. Jika meja Anda kecil, ini bagus. Tetapi dalam kebanyakan kasus ini adalah permintaan yang sangat lambat.

Saya menulis fungsi sederhana yang berfungsi walaupun id memiliki lubang (beberapa baris tempat dihapus):

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]

Ini lebih cepat daripada order_by ('?') Di hampir semua kasus.


30
Juga, sayangnya, ini jauh dari acak. Jika Anda memiliki catatan dengan id 1 dan yang lainnya dengan id 100, maka itu akan mengembalikan yang kedua 99% dari waktu.
DS.

16

Inilah solusi sederhana:

from random import randint

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

10

Anda dapat membuat manajer pada model Anda untuk melakukan hal semacam ini. Untuk pertama memahami apa yang seorang manajer, yang Painting.objectsmetode adalah manajer yang berisi all(), filter(), get(), dll Membuat manajer Anda sendiri memungkinkan Anda untuk pra-filter hasil dan memiliki semua metode ini sama, serta metode kustom Anda sendiri, bekerja pada hasil .

EDIT : Saya memodifikasi kode saya untuk mencerminkan order_by['?']metode ini. Perhatikan bahwa manajer mengembalikan model acak dalam jumlah tidak terbatas. Karena itu, saya memasukkan sedikit kode penggunaan untuk menunjukkan cara mendapatkan hanya satu model.

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.

Pemakaian

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

Terakhir, Anda dapat memiliki banyak manajer pada model Anda, jadi silakan membuat LeastViewsManager()atau MostPopularManager().


3
Menggunakan get () hanya akan berfungsi jika pks Anda berurutan, yaitu Anda tidak pernah menghapus item apa pun. Kalau tidak, Anda cenderung mencoba dan mendapatkan pk yang tidak ada. Menggunakan .all () [random_index] tidak mengalami masalah ini dan tidak efisien.
Daniel Roseman

Saya mengerti bahwa itulah contoh saya hanya mereplikasi kode pertanyaan dengan manajer. Masih tergantung OP untuk memeriksa batas-batasnya.
Soviut

1
daripada menggunakan .get (id = random_index) bukankah lebih baik menggunakan .filter (id__gte = random_index) [0: 1]? Pertama, ini membantu menyelesaikan masalah dengan pks yang tidak berurutan. Kedua, get_query_set harus mengembalikan ... sebuah QuerySet. Dan dalam contoh Anda, tidak.
Nicolas Dumazet

2
Saya tidak akan membuat manajer baru hanya untuk menampung satu metode. Saya akan menambahkan "get_random" ke manajer default sehingga Anda tidak harus melewati semua () [0] simpanan setiap kali Anda membutuhkan gambar acak. Selanjutnya, jika penulis adalah ForeignKey ke model Pengguna, Anda bisa mengatakan user.painting_set.get_random ().
Antti Rasinen

Saya biasanya membuat manajer baru ketika saya ingin aksi selimut, seperti mendapatkan daftar catatan acak. Saya akan membuat metode pada manajer default jika saya melakukan tugas yang lebih spesifik dengan catatan yang sudah saya miliki.
Soviut

6

Jawaban lainnya berpotensi lambat (menggunakan order_by('?')) atau menggunakan lebih dari satu permintaan SQL. Berikut adalah contoh solusi tanpa pemesanan dan hanya satu permintaan (dengan asumsi Postgres):

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

Perlu diketahui bahwa ini akan meningkatkan kesalahan indeks jika tabel kosong. Tulis sendiri fungsi pembantu model-agnostik untuk memeriksanya.


Sebuah bukti konsep yang bagus, tapi ini juga dua pertanyaan di dalam database, yang Anda simpan adalah satu perjalanan pulang pergi ke database. Anda harus menjalankan ini berkali-kali untuk membuat penulisan dan mempertahankan kueri mentah yang sepadan. Dan jika Anda ingin menjaga dari tabel kosong, Anda bisa menjalankan count()terlebih dahulu dan membuang kueri mentah.
Endre Both

2

Hanya ide sederhana bagaimana saya melakukannya:

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

1

Hanya untuk mencatat kasus khusus (yang cukup umum), jika ada kolom kenaikan otomatis terindeks dalam tabel tanpa penghapusan, cara optimal untuk melakukan pemilihan acak adalah kueri seperti:

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

yang mengasumsikan kolom bernama id untuk tabel. Dalam Django Anda dapat melakukan ini dengan:

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

di mana Anda harus mengganti appname dengan nama aplikasi Anda.

Secara umum, dengan kolom id, order_by ('?') Dapat dilakukan lebih cepat dengan:

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

1

Ini sangat direkomendasikan. Memperoleh baris acak dari basis data relasional

Karena menggunakan django orm untuk melakukan hal seperti itu, akan membuat server db Anda marah terutama jika Anda memiliki tabel data besar: |

Dan solusinya adalah menyediakan Model Manager dan menulis permintaan SQL dengan tangan;)

Perbarui :

Solusi lain yang bekerja pada backend basis data apa pun bahkan yang non-rel tanpa menulis kebiasaan ModelManager. Mendapatkan objek acak dari Queryset di Django


1

Anda mungkin ingin menggunakan pendekatan yang sama dengan yang Anda gunakan untuk sampel iterator apa pun, terutama jika Anda berencana untuk mengambil sampel beberapa item untuk membuat set sampel . @MatijnPieters dan @DzinX menaruh banyak pemikiran dalam hal ini:

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

Solusi Matijn dan DxinX adalah untuk set data yang tidak menyediakan akses acak. Untuk set data yang melakukan (dan SQL tidak dengan OFFSET), ini tidak perlu tidak efisien.
Endre Both

@EndreBoth memang. Saya hanya suka pengkodean "efisiensi" menggunakan pendekatan yang sama terlepas dari sumber data. Terkadang efisiensi pengambilan sampel data tidak secara signifikan mempengaruhi kinerja pipa yang dibatasi oleh proses lain (apa pun yang Anda lakukan dengan data, seperti pelatihan ML).
hobs

1

Salah satu pendekatan yang jauh lebih mudah untuk ini melibatkan hanya menyaring ke catatan menarik dan menggunakan random.sampleuntuk memilih sebanyak yang Anda inginkan:

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

Perhatikan bahwa Anda harus memiliki beberapa kode untuk memverifikasi yang my_querysettidak kosong; random.samplekembali ValueError: sample larger than populationjika argumen pertama mengandung terlalu sedikit elemen.


2
Apakah ini akan menyebabkan seluruh kueri diset untuk diambil?
perrohunter

@perrohunter Bahkan tidak akan berfungsi Queryset(setidaknya dengan Python 3.7 dan Django 2.1); Anda harus mengonversikannya ke daftar terlebih dahulu, yang jelas mengambil seluruh queryset.
Endre Both

@ Endreoth - ini ditulis pada tahun 2016, ketika tidak ada yang ada.
eykanal

Itu sebabnya saya menambahkan info versi. Tetapi jika itu berhasil pada tahun 2016, ia melakukannya dengan menarik seluruh queryset ke daftar, kan?
Endre Both

@ EndBoth Benar.
eykanal

1

Hai Saya perlu memilih catatan acak dari queryset yang panjangnya saya juga perlu melaporkan (yaitu halaman web menghasilkan item yang dijelaskan dan mengatakan catatan tersisa)

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

memakan waktu setengah (0,7 vs vs 1,7) sebagai:

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

Saya kira itu menghindari menarik seluruh permintaan sebelum memilih entri acak dan membuat sistem saya cukup responsif untuk halaman yang diakses berulang kali untuk tugas yang berulang di mana pengguna ingin melihat item_count menghitung mundur.


0

Metode untuk menambah kunci primer tanpa penghapusan

Jika Anda memiliki tabel di mana kunci utama adalah integer berurutan tanpa celah, maka metode berikut ini akan berfungsi:

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

Metode ini jauh lebih efisien daripada metode lain di sini yang mengulangi semua baris tabel. Meskipun membutuhkan dua permintaan basis data, keduanya sepele. Selain itu, ini sederhana dan tidak memerlukan mendefinisikan kelas tambahan Namun, penerapannya terbatas pada tabel dengan kunci primer peningkatan-otomatis di mana baris tidak pernah dihapus, sehingga tidak ada celah dalam urutan id.

Dalam kasus di mana baris telah dihapus sehingga ada celah, metode ini masih bisa berfungsi jika dicoba lagi sampai kunci utama yang ada dipilih secara acak.

Referensi


0

Saya mendapat solusi yang sangat sederhana, buat pengelola khusus:

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

dan kemudian tambahkan model:

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

Sekarang, Anda bisa menggunakannya:

Example.objects.random()

dari pilihan impor acak
Adam Starrh

3
Tolong, jangan gunakan metode ini, jika Anda ingin kecepatan. Solusi ini SANGAT lambat. Saya sudah memeriksa. Lebih lambat dari order_by('?').first()lebih dari 60 kali.
LagRange

@ Alex78191 tidak, "?" buruk juga, tetapi metode saya EXTRA lambat. Saya menggunakan solusi jawaban teratas.
LagRange
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.