DjangoのORMを使用してランダムなレコードをプルする方法は?


176

私のサイトに掲載している絵画を表すモデルがあります。メインのWebページで、いくつか紹介します。最新の、最も頻繁にアクセスされなかったもの、最も人気のあるもの、ランダムなものです。

私はDjango 1.0.2を使用しています。

最初の3つはdjangoモデルを使用して簡単にプルできますが、最後の1つ(ランダム)は私に問題を引き起こします。私の見解では、次のようにコードを記述することができます。

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の「コントローラー」レベルに入る「ビュー」レベルまたはビジネスロジックの一部です。
ガブリエレダントーナ

Djangoでは、コントローラーがビューです。docs.djangoproject.com/en/dev/faq/general/...

回答:


169

を使用order_by('?')すると、実稼働の2日目に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 2014

11
受け入れられた回答よりもはるかに優れていますが、このアプローチは2つのSQLクエリを作成することに注意してください。カウントがその間に変化すると、範囲外エラーが発生する可能性があります。
Nelo Mitranim

2
これは間違ったソリューションです。IDが0から始まっていない場合は機能しません。IDが連続していない場合も同様です。たとえば、最初のレコードは500から始まり、最後のレコードは599です(隣接性があると想定)。その場合、カウントは54950になります。querystの長さが100であるため、確かにlist [54950]は存在しません。インデックスが範囲外の例外をスローします。なぜこれほど多くの人々がこれに賛成したのか、そしてこれが受け入れられた回答としてマークされた理由はわかりません。
サジド

1
@sajid:なぜ、正確に、あなたは私に尋ねているのですか?この質問に対する私の貢献の合計を見るのは非常に簡単です。腐敗したアーカイブを指すようにリンクを編集します。私も答えに投票していません。しかし、私はこの答えとあなたがはるかに優れているとあなたが主張する答えの両方.all()[randint(0, count - 1)]が実際に使用されているのは面白いと思います。たぶん、答えのどの部分が間違っているか弱いかを特定することに焦点を当てる必要があるかもしれません。私たちのために「1つずつのエラー」を再定義し、愚かな有権者に怒鳴るのではありません。(たぶんそれは使っていないの.objectsでしょうか?)
Nathan Tuggy

3
@NathanTuggy。わかりました。申し訳ありません
サジド

260

単に使用してください:

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

QuerySet APIに記載されています


71
文書化されているように、このアプローチは非常に遅くなる可能性があることに注意してください:)
Nicolas Dumazet 2009年

6
「使用しているデータベースのバックエンドによっては、高価で時間がかかる場合があります。」-異なるDBバックエンドでの経験はありますか?(sqlite / mysql / postgres)?
ケンダー

4
私はそれをテストしていないので、これは純粋な推測です:すべてのアイテムを取得してPythonでランダム化を実行するよりも遅いのはなぜですか?
muhuk 09年

8
mysqlはランダムな順序付けが信じられないほど効率的でないため、mysqlでは遅いと読みました。
ブランドンヘンリー

33
なぜrandom.choice(Model.objects.all())ですか?
Jamey

25

MySQLを使用している場合(他のデータベースについては不明)、order_by( '?')[:N]を使用したソリューションは、中規模のテーブルでも非常に遅くなります。

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%の確率で2番目のレコードが返されます。
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()の使用は、pkが連続している場合、つまりアイテムを削除しない場合にのみ機能します。それ以外の場合は、存在しないpkを取得しようとする可能性があります。.all()[random_index]を使用しても、この問題は発生せず、効率も低下しません。
ダニエルローズマン

私の例がマネージャーの質問のコードを単純に複製しているのはそのためです。彼の境界チェックを実行するのはOP次第です。
2009年

1
.get(id = random_index)を使用する代わりに、.filter(id__gte = random_index)[0:1]を使用する方が良いでしょうか?まず、連続していないパックで問題を解決するのに役立ちます。次に、get_query_setはクエリセットを返します。そして、あなたの例では、そうではありません。
Nicolas Dumazet 09年

2
1つのメソッドを格納するためだけに新しいマネージャーを作成することはしません。デフォルトのマネージャーに「get_random」を追加して、ランダムイメージが必要になるたびにall()[0]フープを通過する必要がないようにします。さらに、作成者がUserモデルのForeignKeyである場合、user.painting_set.get_random()と言うことができます。
Antti Rasinen、2009年

ランダムなレコードのリストを取得するなど、包括的なアクションが必要な場合は、通常、新しいマネージャーを作成します。既に持っているレコードを使用してより具体的なタスクを実行している場合は、デフォルトのマネージャーでメソッドを作成します。
ソビエト連邦2009年

6

他の答えは、潜在的に遅い(を使用order_by('?'))か、複数のSQLクエリを使用しています。以下は、順序付けがなく、クエリが1つだけのサンプルソリューションです(Postgresを想定)。

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

テーブルが空の場合、インデックスエラーが発生することに注意してください。モデルにとらわれないヘルパー関数を記述して、それを確認します。


概念実証には優れていますが、これはデータベース内のクエリも2つなので、保存するのはデータベースへの1回の往復だけです。価値のある生のクエリを記述および維持するには、これを何度も実行する必要があります。また、空のテーブルから保護する場合はcount()、事前にを実行して、生のクエリを省略してもかまいません。
Endre Both

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という名前の列を想定しています。ジャンゴではこれを行うことができます:

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

ここで、appnameをアプリケーション名に置き換える必要があります。

一般に、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サーバーは特に怒ります。

そして解決策は、Model Managerを提供し、SQLクエリを手動で作成することです;)

更新

カスタムを作成せずに、rel以外のデータベースバックエンドでも機能する別のソリューションModelManagerDjangoのクエリセットからランダムオブジェクトを取得する


1

特に、複数のアイテムをサンプリングしてサンプルセットを作成する場合は、イテレータのサンプリングに使用するのと同じアプローチを使用できます。@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)データセットの場合、これは不必要に非効率的です。
Endre Both

確かに@EndreBoth。データソースに関係なく同じアプローチを使用するコーディングの「効率」が好きです。データサンプリングの効率は、他のプロセスによって制限されたパイプラインのパフォーマンスに大きな影響を与えない場合があります(MLトレーニングのように、実際にデータを使って行っていることは何でも)。
ホブ、

1

これへのより簡単なアプローチの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 2017

@perrohunter Queryset(少なくともPython 3.7およびDjango 2.1では)さえ機能しません。まずリストに変換する必要があります。これにより、明らかにクエリセット全体が取得されます。
Endre Both

@EndreBoth-これは、どちらも存在しなかった2016年に書かれました。
eykanal

そのため、バージョン情報を追加しました。しかし、2016年に機能した場合は、クエリセット全体をリストに入れて機能しました。
Endre Both

@EndreBoth正解。
eykanal

1

こんにちは私はまた、報告する必要がある長さのクエリセットからランダムなレコードを選択する必要がありました(つまり、Webページで生成された説明アイテムと残りのレコード)

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

次の半分の時間(0.7秒vs 1.7秒)かかりました:

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

ランダムなエントリを選択する前にクエリ全体をプルダウンするのを避け、ユーザーがitem_countのカウントダウンを確認したい繰り返しのタスクで繰り返しアクセスされるページに対してシステムを十分に応答可能にすると思います。


0

削除なしで主キーを自動インクリメントする方法

主キーがギャップのない連続した整数であるテーブルがある場合、次の方法が機能するはずです。

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

この方法は、テーブルのすべての行を反復処理する他の方法よりもはるかに効率的です。2つのデータベースクエリが必要ですが、どちらも簡単です。さらに、それは単純であり、追加のクラスを定義する必要はありません。ただし、その適用は、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.