diff --git a/django/db/models/query.py b/django/db/models/query.py index 2aa1f6464e..6ab41dc21c 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -335,12 +335,11 @@ class QuerySet(object): kwargs[arg.default_alias] = arg query = self.query.clone() - force_subq = query.low_mark != 0 or query.high_mark is not None for (alias, aggregate_expr) in kwargs.items(): - query.add_annotation(aggregate_expr, self.model, alias, is_summary=True) + query.add_annotation(aggregate_expr, alias, is_summary=True) if not query.annotations[alias].contains_aggregate: raise TypeError("%s is not an aggregate expression" % alias) - return query.get_aggregation(using=self.db, force_subq=force_subq) + return query.get_aggregation(self.db, kwargs.keys()) def count(self): """ @@ -824,7 +823,7 @@ class QuerySet(object): if alias in names: raise ValueError("The annotation '%s' conflicts with a field on " "the model." % alias) - obj.query.add_annotation(annotation, self.model, alias, is_summary=False) + obj.query.add_annotation(annotation, alias, is_summary=False) # expressions need to be added to the query before we know if they contain aggregates added_aggregates = [] for alias, annotation in obj.query.annotations.items(): diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index b800c3fc3e..dbbc9aecc8 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -1097,6 +1097,11 @@ class SQLAggregateCompiler(SQLCompiler): Creates the SQL for this query. Returns the SQL string and list of parameters. """ + # Empty SQL for the inner query is a marker that the inner query + # isn't going to produce any results. This can happen when doing + # LIMIT 0 queries (generated by qs[:0]) for example. + if not self.query.subquery: + raise EmptyResultSet sql, params = [], [] for annotation in self.query.annotation_select.values(): agg_sql, agg_params = self.compile(annotation) diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index a5d067a37b..81b3870339 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -313,32 +313,35 @@ class Query(object): clone.change_aliases(change_map) return clone - def get_aggregation(self, using, force_subq=False): + def get_aggregation(self, using, added_aggregate_names): """ Returns the dictionary with the values of the existing aggregations. """ if not self.annotation_select: return {} - - # annotations must be forced into subquery - has_annotation = any( + has_limit = self.low_mark != 0 or self.high_mark is not None + has_existing_annotations = any( annotation for alias, annotation - in self.annotation_select.items() - if not annotation.contains_aggregate) - - # If there is a group by clause, aggregating does not add useful - # information but retrieves only the first row. Aggregate - # over the subquery instead. - if self.group_by is not None or force_subq or has_annotation: - + in self.annotations.items() + if alias not in added_aggregate_names + ) + # Decide if we need to use a subquery. + # + # Existing annotations would cause incorrect results as get_aggregation() + # must produce just one result and thus must not use GROUP BY. But we + # aren't smart enough to remove the existing annotations from the + # query, so those would force us to use GROUP BY. + # + # If the query has limit or distinct, then those operations must be + # done in a subquery so that we are aggregating on the limit and/or + # distinct results instead of applying the distinct and limit after the + # aggregation. + if (self.group_by or has_limit or has_existing_annotations or self.distinct): from django.db.models.sql.subqueries import AggregateQuery outer_query = AggregateQuery(self.model) inner_query = self.clone() - if not force_subq: - # In forced subq case the ordering and limits will likely - # affect the results. + if not has_limit and not self.distinct_fields: inner_query.clear_ordering(True) - inner_query.clear_limits() inner_query.select_for_update = False inner_query.select_related = False inner_query.related_select_cols = [] @@ -398,34 +401,10 @@ class Query(object): Performs a COUNT() query using the current filter constraints. """ obj = self.clone() - if len(self.select) > 1 or self.annotation_select or (self.distinct and self.distinct_fields): - # If a select clause exists, then the query has already started to - # specify the columns that are to be returned. - # In this case, we need to use a subquery to evaluate the count. - from django.db.models.sql.subqueries import AggregateQuery - subquery = obj - subquery.clear_ordering(True) - subquery.clear_limits() - - obj = AggregateQuery(obj.model) - try: - obj.add_subquery(subquery, using=using) - except EmptyResultSet: - # add_subquery evaluates the query, if it's an EmptyResultSet - # then there are can be no results, and therefore there the - # count is obviously 0 - return 0 - - obj.add_count_column() - number = obj.get_aggregation(using=using)[None] - - # Apply offset and limit constraints manually, since using LIMIT/OFFSET - # in SQL (in variants that provide them) doesn't change the COUNT - # output. - number = max(0, number - self.low_mark) - if self.high_mark is not None: - number = min(number, self.high_mark - self.low_mark) - + obj.add_annotation(Count('*'), alias='__count', is_summary=True) + number = obj.get_aggregation(using, ['__count'])['__count'] + if number is None: + number = 0 return number def has_filters(self): @@ -986,9 +965,9 @@ class Query(object): warnings.warn( "add_aggregate() is deprecated. Use add_annotation() instead.", RemovedInDjango20Warning, stacklevel=2) - self.add_annotation(aggregate, model, alias, is_summary) + self.add_annotation(aggregate, alias, is_summary) - def add_annotation(self, annotation, model, alias, is_summary): + def add_annotation(self, annotation, alias, is_summary): """ Adds a single annotation expression to the Query """ @@ -1746,52 +1725,6 @@ class Query(object): for col in annotation.get_group_by_cols(): self.group_by.append(col) - def add_count_column(self): - """ - Converts the query to do count(...) or count(distinct(pk)) in order to - get its size. - """ - summarize = False - if not self.distinct: - if not self.select: - count = Count('*') - summarize = True - else: - assert len(self.select) == 1, \ - "Cannot add count col with multiple cols in 'select': %r" % self.select - col = self.select[0].col - if isinstance(col, (tuple, list)): - count = Count(col[1]) - else: - count = Count(col) - - else: - opts = self.get_meta() - if not self.select: - lookup = self.join((None, opts.db_table, None)), opts.pk.column - count = Count(lookup[1], distinct=True) - summarize = True - else: - # Because of SQL portability issues, multi-column, distinct - # counts need a sub-query -- see get_count() for details. - assert len(self.select) == 1, \ - "Cannot add count col with multiple cols in 'select'." - col = self.select[0].col - if isinstance(col, (tuple, list)): - count = Count(col[1], distinct=True) - else: - count = Count(col, distinct=True) - # Distinct handling is done in Count(), so don't do it at this - # level. - self.distinct = False - - # Set only aggregate to be the count column. - # Clear out the select cache to reflect the new unmasked annotations. - count = count.resolve_expression(self, summarize=summarize) - self._annotations = {None: count} - self.set_annotation_mask(None) - self.group_by = None - def add_select_related(self, fields): """ Sets up the select_related data structure so that we only select