mirror of
https://github.com/django/django.git
synced 2025-10-31 09:41:08 +00:00
Fixed #18854 -- Join promotion in disjunction cases
The added promotion logic is based on promoting any joins used in only some of the childs of an OR clause unless the join existed before the OR clause addition.
This commit is contained in:
@@ -772,17 +772,37 @@ class Query(object):
|
||||
unref_amount = cur_refcount - to_counts.get(alias, 0)
|
||||
self.unref_alias(alias, unref_amount)
|
||||
|
||||
def promote_unused_aliases(self, initial_refcounts, used_aliases):
|
||||
def promote_disjunction(self, aliases_before, alias_usage_counts,
|
||||
num_childs):
|
||||
"""
|
||||
Given a "before" copy of the alias_refcounts dictionary (as
|
||||
'initial_refcounts') and a collection of aliases that may have been
|
||||
changed or created, works out which aliases have been created since
|
||||
then and which ones haven't been used and promotes all of those
|
||||
aliases, plus any children of theirs in the alias tree, to outer joins.
|
||||
This method is to be used for promoting joins in ORed filters.
|
||||
|
||||
The principle for promotion is: any alias which is used (it is in
|
||||
alias_usage_counts), is not used by every child of the ORed filter,
|
||||
and isn't pre-existing needs to be promoted to LOUTER join.
|
||||
|
||||
Some examples (assume all joins used are nullable):
|
||||
- existing filter: a__f1=foo
|
||||
- add filter: b__f1=foo|b__f2=foo
|
||||
In this case we should not promote either of the joins (using INNER
|
||||
doesn't remove results). We correctly avoid join promotion, because
|
||||
a is not used in this branch, and b is used two times.
|
||||
|
||||
- add filter a__f1=foo|b__f2=foo
|
||||
In this case we should promote both a and b, otherwise they will
|
||||
remove results. We will also correctly do that as both aliases are
|
||||
used, and in addition both are used only once while there are two
|
||||
filters.
|
||||
|
||||
- existing: a__f1=bar
|
||||
- add filter: a__f2=foo|b__f2=foo
|
||||
We will not promote a as it is previously used. If the join results
|
||||
in null, the existing filter can't succeed.
|
||||
|
||||
The above (and some more) are tested in queries.DisjunctionPromotionTests
|
||||
"""
|
||||
for alias in self.tables:
|
||||
if alias in used_aliases and (alias not in initial_refcounts or
|
||||
self.alias_refcount[alias] == initial_refcounts[alias]):
|
||||
for alias, use_count in alias_usage_counts.items():
|
||||
if use_count < num_childs and alias not in aliases_before:
|
||||
self.promote_joins([alias])
|
||||
|
||||
def change_aliases(self, change_map):
|
||||
@@ -1150,16 +1170,12 @@ class Query(object):
|
||||
can_reuse)
|
||||
return
|
||||
|
||||
table_promote = False
|
||||
join_promote = False
|
||||
|
||||
if (lookup_type == 'isnull' and value is True and not negate and
|
||||
len(join_list) > 1):
|
||||
# If the comparison is against NULL, we may need to use some left
|
||||
# outer joins when creating the join chain. This is only done when
|
||||
# needed, as it's less efficient at the database level.
|
||||
self.promote_joins(join_list)
|
||||
join_promote = True
|
||||
|
||||
# Process the join list to see if we can remove any inner joins from
|
||||
# the far end (fewer tables in a query is better). Note that join
|
||||
@@ -1167,39 +1183,6 @@ class Query(object):
|
||||
# information available when reusing joins.
|
||||
col, alias, join_list = self.trim_joins(target, join_list, path)
|
||||
|
||||
if connector == OR:
|
||||
# Some joins may need to be promoted when adding a new filter to a
|
||||
# disjunction. We walk the list of new joins and where it diverges
|
||||
# from any previous joins (ref count is 1 in the table list), we
|
||||
# make the new additions (and any existing ones not used in the new
|
||||
# join list) an outer join.
|
||||
join_it = iter(join_list)
|
||||
table_it = iter(self.tables)
|
||||
next(join_it), next(table_it)
|
||||
unconditional = False
|
||||
for join in join_it:
|
||||
table = next(table_it)
|
||||
# Once we hit an outer join, all subsequent joins must
|
||||
# also be promoted, regardless of whether they have been
|
||||
# promoted as a result of this pass through the tables.
|
||||
unconditional = (unconditional or
|
||||
self.alias_map[join].join_type == self.LOUTER)
|
||||
if join == table and self.alias_refcount[join] > 1:
|
||||
# We have more than one reference to this join table.
|
||||
# This means that we are dealing with two different query
|
||||
# subtrees, so we don't need to do any join promotion.
|
||||
continue
|
||||
join_promote = join_promote or self.promote_joins([join], unconditional)
|
||||
if table != join:
|
||||
table_promote = self.promote_joins([table])
|
||||
# We only get here if we have found a table that exists
|
||||
# in the join list, but isn't on the original tables list.
|
||||
# This means we've reached the point where we only have
|
||||
# new tables, so we can break out of this promotion loop.
|
||||
break
|
||||
self.promote_joins(join_it, join_promote)
|
||||
self.promote_joins(table_it, table_promote or join_promote)
|
||||
|
||||
if having_clause or force_having:
|
||||
if (alias, col) not in self.group_by:
|
||||
self.group_by.append((alias, col))
|
||||
@@ -1256,33 +1239,36 @@ class Query(object):
|
||||
subtree = True
|
||||
else:
|
||||
subtree = False
|
||||
connector = AND
|
||||
connector = q_object.connector
|
||||
if connector == OR:
|
||||
alias_usage_counts = dict()
|
||||
aliases_before = set(self.tables)
|
||||
if q_object.connector == OR and not force_having:
|
||||
force_having = self.need_force_having(q_object)
|
||||
for child in q_object.children:
|
||||
if connector == OR:
|
||||
refcounts_before = self.alias_refcount.copy()
|
||||
if force_having:
|
||||
self.having.start_subtree(connector)
|
||||
else:
|
||||
self.where.start_subtree(connector)
|
||||
if connector == OR:
|
||||
refcounts_before = self.alias_refcount.copy()
|
||||
if isinstance(child, Node):
|
||||
self.add_q(child, used_aliases, force_having=force_having)
|
||||
else:
|
||||
self.add_filter(child, connector, q_object.negated,
|
||||
can_reuse=used_aliases, force_having=force_having)
|
||||
if connector == OR:
|
||||
used = alias_diff(refcounts_before, self.alias_refcount)
|
||||
for alias in used:
|
||||
alias_usage_counts[alias] = alias_usage_counts.get(alias, 0) + 1
|
||||
if force_having:
|
||||
self.having.end_subtree()
|
||||
else:
|
||||
self.where.end_subtree()
|
||||
|
||||
if connector == OR:
|
||||
# Aliases that were newly added or not used at all need to
|
||||
# be promoted to outer joins if they are nullable relations.
|
||||
# (they shouldn't turn the whole conditional into the empty
|
||||
# set just because they don't match anything).
|
||||
self.promote_unused_aliases(refcounts_before, used_aliases)
|
||||
connector = q_object.connector
|
||||
if connector == OR:
|
||||
self.promote_disjunction(aliases_before, alias_usage_counts,
|
||||
len(q_object.children))
|
||||
if q_object.negated:
|
||||
self.where.negate()
|
||||
if subtree:
|
||||
@@ -2005,3 +1991,11 @@ def is_reverse_o2o(field):
|
||||
expected to be some sort of relation field or related object.
|
||||
"""
|
||||
return not hasattr(field, 'rel') and field.field.unique
|
||||
|
||||
def alias_diff(refcounts_before, refcounts_after):
|
||||
"""
|
||||
Given the before and after copies of refcounts works out which aliases
|
||||
have been added to the after copy.
|
||||
"""
|
||||
return set(t for t in refcounts_after
|
||||
if refcounts_after[t] > refcounts_before.get(t, 0))
|
||||
|
||||
Reference in New Issue
Block a user