Comment écrire efficacement une requête DISTINCT dans Django avec une table ayant des clés étrangères
-
21-12-2019 - |
Question
Je souhaite afficher des villes distinctes d'utilisateurs dans la liste déroulante frontale.Pour cela, je fais une requête db qui récupère des éléments distincts city_name
du tableau City
mais uniquement les villes où les utilisateurs sont présents.
Quelque chose comme ci-dessous fonctionne pour une petite taille de User
table, mais cela prend beaucoup de temps si User
table en taille 10 millions.Cependant, les villes distinctes de ces utilisateurs sont toujours d'environ 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')
Maintenant, j'essaie de rechercher des noms de villes distincts comme :
City.objects.filter().values_list('city__city_name').distinct()
ce qui se traduit par ceci sur PostgreSQL :
SELECT DISTINCT "city"."city_name"
FROM "user"
LEFT OUTER JOIN "city"
ON ("user"."city_id" = "city"."city_code");
Temps:9760,302 ms
Cela montrait clairement que PostgreSQL n'utilisait pas d'index sur 'user'.'city_id'.J'ai également lu des informations sur une solution de contournement ici ce qui impliquait l'écriture d'une requête SQL personnalisée qui utilise d'une manière ou d'une autre l'index.
J'ai essayé de trouver un « utilisateur ». « city_id » distinct en utilisant la requête ci-dessus, et cela s'est avéré assez rapide.
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;
Temps:79,056 ms
Mais maintenant, j'ai du mal à intégrer cela dans mon code Django.Je pense toujours que c'est une sorte de hack ajoutant une requête personnalisée dans le code pour cela.Mais un plus gros souci pour moi est que le nom de colonne peut être totalement dynamique, et je ne peux pas coder en dur ces noms de colonnes (par exemple.city_id, etc.) dans le code.
#original_fields could be a list from input, like ['area_code__district_code__name']
dataset_klass.objects.filter().values_list(*original_fields).distinct()
L’utilisation de la requête personnalisée nécessiterait au moins de diviser le nom du champ avec « __ » comme délimiteur et de traiter la première partie.Mais cela me semble être un mauvais hack.
Comment puis-je améliorer cela ?
PS.Le City
User
L'exemple est juste montré pour expliquer le scénario.La syntaxe n'est peut-être pas correcte.
La solution
J'ai finalement trouvé cette solution de contournement.
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()
Qui utilise l'index sur city_id
dans user
table, court assez vite.