So schreiben Sie effizient eine DISTINCT-Abfrage in Django mit einer Tabelle mit Fremdschlüsseln
-
21-12-2019 - |
Frage
Ich möchte im Front-End-Dropdown verschiedene Benutzerstädte anzeigen.Dazu erstelle ich eine Datenbankabfrage, die eindeutige Ergebnisse abruft city_name
vom Tisch City
aber nur die Städte, in denen Benutzer anwesend sind.
Etwas wie unten funktioniert für eine kleine Größe von User
Tisch, dauert aber sehr lange, wenn User
Tabelle in der Größe 10 Millionen.Die Zahl der einzelnen Städte dieser Nutzer beträgt jedoch immer noch etwa 100.
class City(models.Model):
city_code = models.IntegerField(unique=True)
city_name = models.CharField(max_length=256)
class User(models.Model):
city = models.ForeignKey('City', to_field='city_code')
Jetzt versuche ich, nach bestimmten Städtenamen zu suchen:
City.objects.filter().values_list('city__city_name').distinct()
was auf PostgreSQL Folgendes bedeutet:
SELECT DISTINCT "city"."city_name"
FROM "user"
LEFT OUTER JOIN "city"
ON ("user"."city_id" = "city"."city_code");
Zeit:9760,302 ms
Das zeigte deutlich, dass PostgreSQL den Index für „user“.‘city_id‘ nicht nutzte.Ich habe auch von einer Workaround-Lösung gelesen Hier Dazu gehörte das Schreiben einer benutzerdefinierten SQL-Abfrage, die irgendwie den Index nutzt.
Ich habe versucht, mit der obigen Abfrage eine eindeutige „Benutzer-ID“ zu finden, und das ging tatsächlich ziemlich schnell.
WITH
RECURSIVE t(n) AS
(SELECT min(city_id)
FROM user
UNION
SELECT
(SELECT city_id
FROM user
WHERE city_id > n order by city_id limit 1)
FROM t
WHERE n is not null)
SELECT n
FROM t;
Zeit:79,056 ms
Aber jetzt fällt es mir schwer, dies in meinen Django-Code zu integrieren.Ich denke immer noch, dass es eine Art Hack ist, dem Code dafür eine benutzerdefinierte Abfrage hinzuzufügen.Eine größere Sorge für mich ist jedoch, dass der Spaltenname völlig dynamisch sein kann und ich diese Spaltennamen nicht fest codieren kann (z. B.city_id usw.) im Code.
#original_fields could be a list from input, like ['area_code__district_code__name']
dataset_klass.objects.filter().values_list(*original_fields).distinct()
Bei Verwendung der benutzerdefinierten Abfrage müsste zumindest der Feldname mit „__“ als Trennzeichen aufgeteilt und der erste Teil verarbeitet werden.Aber für mich sieht es nach einem schlechten Hack aus.
Wie kann ich das verbessern?
PS.Der City
User
Das Beispiel wird nur gezeigt, um das Szenario zu erläutern.Die Syntax ist möglicherweise nicht korrekt.
Lösung
Endlich habe ich diese Workaround-Lösung gefunden.
from django.db import connection, transaction
original_field = 'city__city_name'
dataset_name = 'user'
dataset_klass = eval(camelize(dataset_name))
split_arr = original_field.split("__",1)
"""If a foreign key relation is present
"""
if len(split_arr) > 1:
parent_field = dataset_klass._meta.get_field_by_name(split_arr[0])[0]
cursor = connection.cursor()
"""This query will run fast only if parent_field is indexed (city_id)
"""
cursor.execute('WITH RECURSIVE t(n) AS ( select min({0}) from {1} '
'union select (select {0} from {1} where {0} > n'
' order by {0} limit 1) from t where n is not null) '
'select n from t;'.format(parent_field.get_attname_column()[1], dataset_name))
"""Create a list of all distinct city_id's"""
distinct_values = [single[0] for single in cursor.fetchall()]
"""create a dict of foreign key field to the above list"""
"""to get the actual city_name's using _meta information"""
filter_dict = {parent_field.rel.field_name+'__in':distinct_values}
values = parent_field.rel.to.objects.filter(**filter_dict).values_list(split_arr[1])
else:
values = dataset_klass.objects.filter().values_list(original_field).distinct()
Was den Index nutzt city_id
In user
Tabelle, läuft ziemlich schnell.