2013-03-01 8 views
7

最新のスプリントタイムで注文されたランナーのリストを表示したいと考えます。Django:最新の子モデルフィールドに基づいてクエリセットを注文する

class Runner(models.Model): 
    name = models.CharField(max_length=255) 

class Sprint(models.Model): 
    runner = models.ForeignKey(Runner) 
    time = models.PositiveIntegerField() 
    created = models.DateTimeField(auto_now_add=True) 

これは私がSQLにどうなるのかの簡単なスケッチです:

SELECT runner.id, runner.name, sprint.time 
FROM runner 
LEFT JOIN sprint ON (sprint.runner_id = runner.id) 
WHERE 
    sprint.id = (
    SELECT sprint_inner.id 
    FROM sprint as sprint_inner 
    WHERE sprint_inner.runner_id = runner.id 
    ORDER BY sprint_inner.created DESC 
    LIMIT 1 
) 
    OR sprint.id = NULL 
ORDER BY sprint.time ASC 

Django QuerySet documentation状態:

注文する複数値フィールドを指定することが許されています結果は (たとえば、ManyToManyFieldフィールド)。通常、これは実行するには 分かりやすいものではなく、実際は高度な使用方法です。 しかし、クエリセットのフィルタリングまたは利用可能なデータ が、 のそれぞれのデータの注文データを1つだけ選択することを意味する場合、注文は正確に となります。注意して多値フィールドでの注文を使用し、 の結果が期待どおりであることを確認してください。

私はここにいくつかのフィルタを適用する必要があると思いますが、私はそれがこの例では明らかにされていませんので、Djangoは...

つのノートを見込んで正確に何かわからない:ランナーテーブルには、いくつかを持っています百回のエントリーでは、スプリントには数百もあり、後にはおそらく数千ものエントリーがあります。データは改ページされたビューで表示されるので、Pythonでのソートはオプションではありません。

唯一の他の可能性は、自分自身でSQLを書くことですが、私はこれを避けたいと思います。私は一つだけのクエリでORMを経由してこれを行う方法はないと思う

答えて

2

、あなたはランナーのリストを取得し、彼らの最新のスプリントIDを追加するためにannotateを使用することができます - そして、これらのスプリントをフィルタリングし、注文します。

>>> from django.db.models import Max 

# all runners now have a `last_race` attribute, 
# which is the `id` of the last sprint they ran 
>>> runners = Runner.objects.annotate(last_race=Max("sprint__id")) 

# a list of each runner's last sprint ordered by the the sprint's time, 
# we use `select_related` to limit lookup queries later on 
>>> results = Sprint.objects.filter(id__in=[runner.last_race for runner in runners]) 
...       .order_by("time") 
...       .select_related("runner") 

# grab the first result 
>>> first_result = results[0] 

# you can access the runner's details via `.runner`, e.g. `first_result.runner.name` 
>>> isinstance(first_result.runner, Runner) 
True 

# this should only ever execute 2 queries, no matter what you do with the results 
>>> from django.db import connection 
>>> len(connection.queries) 
2 

これはかなり高速で、データベースのインデックスとキャッシュを利用します。

数千もの記録はそれほど多くはありませんが、これはこれらの種類の数字ではうまくいくはずです。問題が発生し始めたら、弾丸を噛んで生のSQLを使うことをお勧めします。

+0

これは比較的高いメモリ使用量を引き起こしませんか?私が見ることができる限り、少なくともすべてのランナーをメモリに引き込み、かなり大きなスプリントIDのリストを作成します。これをDBに数百人のランナーがいるすべてのページビューで実行すると、私は少し*不快な気分になります。これがキャッシングが始まる場所です。 – Strayer

+1

これを10,000人のランナーでテストした結果、10MB(実際には3MB)未満のRAMが使用されました。それ以上のものを必要としていると思われる場合は、実際には未処理のSQLを使用する必要があります。いつものように、これに対する最善のアプローチは、最初にプロファイルすることです - 推測ではありません。早期の最適化とそのすべて... – Matt

+0

そして、数百のレコードは実際にはあまり多くはありません...パフォーマンスの最適化について心配することはありません。通常数十万件のレコードが考えられますが、それでも通常は問題はあまりありません(インデックスを1つか2つにして解決します)。 – Matt

0
def view_name(request): 
    spr = Sprint.objects.values('runner', flat=True).order_by(-created).distinct() 
    runners = [] 
    for s in spr: 
     latest_sprint = Sprint.objects.filter(runner=s.runner).order_by(-created)[:1] 
     for latest in latest_sprint: 
      runners.append({'runner': s.runner, 'time': latest.time}) 

    return render(request, 'page.html', { 
      'runners': runners, 
    }) 


{% for runner in runners %} 
    {{runner.runner}} - {{runner.time}} 
{% endfor %} 
+0

問題は、最新のスプリントを取得するのではなく、ランナークエリセットを最新のスプリント 'time'フィールドで注文することです。 – Strayer

+0

これはうまくいきます。問題は、これがランナーの注文をアプリケーションに移動させることで、少なくとも大きなメモリ使用量と比較的高いCPU使用率が発生することです。テーブルサイズに関する最新の質問を参照してください。このアプローチのもう1つの問題は、スプリントを一切持たないランナーを表示しないことです。これはPythonコード内でも解決できますが、インデックスとキャッシュを利用できるため、データベースにとって完璧な仕事です。これは小さなデータベースでも機能しますが、私がこのようにすればSysAdminが私を殺すでしょう;) – Strayer

+0

hmmm ...これは難しいです。そして私たちも同じです。私は雇用主の期待のために私の仕事について言えばコーディングに注意しています。 :) – catherine

関連する問題