Probably I should not post into a ten-year old thread, 
but it was this thread which helped me in my problem, so I want to share
my solution to the problem in case anyone else finds this.

Am Samstag, 7. März 2009 06:00:55 UTC+1 schrieb Malcolm Tredinnick:
>
> One day I'll stop posting in this thread. Really.
>
> On Sat, 2009-03-07 at 12:03 +1100, Malcolm Tredinnick wrote:
>
> > However, find things that are simultaneously in all those categories can
> > be done without all the annotation nonsense I posted. Simply
> > 
> >         
> Publication.objects.filter(article=1).filter(article=2).filter(article=3)
>
 
I tried that solution, and it works, but as you said is slow. One thing to 
note is
that it includes tables with at least those three articles, so another 
query is needed
to exclude the remaining ones (if an exact matching is what we are looking 
for).

>         
> > It's a short loop to build up such a set dynamically. The difference
> > between that (3 filter calls) and you Q-object version is described in
> > the documentation:
> > 
> http://docs.djangoproject.com/en/dev/topics/db/queries/#spanning-multi-valued-relationships
>
> I forgot to mention that this approach is probably only practical for a
> small number of article instances. Each filter() adds a new table join,
> so at some point the database will complain. Either literally, about too
> many joins, or by taking ages to attempt to optimise and execute the
> query.
>

Exactly, in my tests I found a large difference even with only about 20 
joins
and 300 objects in the database. Definitely nothing for performance.
 

> For a large number of article instances, the annotation-based solution I
> gave earlier is more appropriate.
>

It does not filter exactly however: If one of the articles is contained, and
the num_cats is coincidentially exactly 3, it will include the article.
But I wrote, based on your annotation suggestion, a working solution
for exact matching of a many-to-many field:

def many_to_many_exact(manager, fieldname, foreign_keys, foreign_manager):
   from django.db.models import Count
   result = manager
   filter_dict = {fieldname + '__in': foreign_keys}
   result = result.filter(**filter_dict)
   count_name = 'count_' + fieldname
   annotate_dict = {count_name: Count(fieldname)}
   filter_dict = {count_name: len(foreign_keys)}
   result = result.annotate(**annotate_dict).filter(**filter_dict)
   exclude_dict = {fieldname + '__in': 
foreign_manager.exclude(pk__in=foreign_keys)}
   return result.exclude(**exclude_dict)

 
As for the example, this would be the same as the following code:

Book.objects
   .filter(categories__in=[1, 2, 3])
   .annotate(num_cats=Count('categories'))
   .filter(num_cats=3)
   .exclude(categories__in=Categories.objects.exclude(pk__in=[1, 2, 3]))


All of the above code, including the categories for exclude, are executed 
in one query.
Probably the categories of a book are way less than the number
of existing categories, thus the database system will be smart and check
them against an index in the exclusion part.
The resulting query has the following structure:

SELECT table.*, COUNT(many_to_many_table.related_id) AS "count_related" 
FROM table INNER JOIN many_to_many_table 
   ON (table.id = many_to_many_table.table_id) 
WHERE (many_to_many_table.related_id IN ( ...(e.g. 1,2,3) ) 
   AND NOT (table.id IN 
      (SELECT V1.table_id FROM many_to_many_table V1 
       WHERE V1.related_id IN 
         (SELECT U0.id FROM related U0 
          WHERE NOT (U0.id IN ( ...(same as above) ))))))
GROUP BY table.id
HAVING COUNT(many_to_many_table.related_id) = ...(e.g. 3)


Of course there are two nestings, but I am afraid 
it is not to be solved more efficiently without custom sql.

Best regards,
Tassilo

>

-- 
You received this message because you are subscribed to the Google Groups 
"Django users" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
Visit this group at https://groups.google.com/group/django-users.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-users/398bcfa2-9d26-4064-8694-55a65410d0c4%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to