Fixed #20950 -- Instantiate OrderedDict() only when needed

The use of OrderedDict (even an empty one) was surprisingly slow. By
initializing OrderedDict only when needed it is possible to save
non-trivial amount of computing time (Model.save() is around 30% faster
for example).

This commit targetted sql.Query only, there are likely other places
which could use similar optimizations.
This commit is contained in:
Anssi Kääriäinen 2013-08-21 14:25:19 +03:00 committed by Anssi Kääriäinen
parent 886bb9d878
commit ff723d894d
2 changed files with 47 additions and 22 deletions

View File

@ -391,7 +391,7 @@ class SQLCompiler(object):
if not distinct or elt in select_aliases: if not distinct or elt in select_aliases:
result.append('%s %s' % (elt, order)) result.append('%s %s' % (elt, order))
group_by.append((elt, [])) group_by.append((elt, []))
elif get_order_dir(field)[0] not in self.query.extra: elif not self.query._extra or get_order_dir(field)[0] not in self.query._extra:
# 'col' is of the form 'field' or 'field1__field2' or # 'col' is of the form 'field' or 'field1__field2' or
# '-field1__field2__field', etc. # '-field1__field2__field', etc.
for table, cols, order in self.find_ordering_name(field, for table, cols, order in self.find_ordering_name(field,
@ -987,7 +987,7 @@ class SQLUpdateCompiler(SQLCompiler):
# We need to use a sub-select in the where clause to filter on things # We need to use a sub-select in the where clause to filter on things
# from other tables. # from other tables.
query = self.query.clone(klass=Query) query = self.query.clone(klass=Query)
query.extra = {} query._extra = {}
query.select = [] query.select = []
query.add_fields([query.get_meta().pk.name]) query.add_fields([query.get_meta().pk.name])
# Recheck the count - it is possible that fiddling with the select # Recheck the count - it is possible that fiddling with the select

View File

@ -143,7 +143,10 @@ class Query(object):
self.select_related = False self.select_related = False
# SQL aggregate-related attributes # SQL aggregate-related attributes
self.aggregates = OrderedDict() # Maps alias -> SQL aggregate function # The _aggregates will be an OrderedDict when used. Due to the cost
# of creating OrderedDict this attribute is created lazily (in
# self.aggregates property).
self._aggregates = None # Maps alias -> SQL aggregate function
self.aggregate_select_mask = None self.aggregate_select_mask = None
self._aggregate_select_cache = None self._aggregate_select_cache = None
@ -153,7 +156,9 @@ class Query(object):
# These are for extensions. The contents are more or less appended # These are for extensions. The contents are more or less appended
# verbatim to the appropriate clause. # verbatim to the appropriate clause.
self.extra = OrderedDict() # Maps col_alias -> (col_sql, params). # The _extra attribute is an OrderedDict, lazily created similarly to
# .aggregates
self._extra = None # Maps col_alias -> (col_sql, params).
self.extra_select_mask = None self.extra_select_mask = None
self._extra_select_cache = None self._extra_select_cache = None
@ -165,6 +170,18 @@ class Query(object):
# load. # load.
self.deferred_loading = (set(), True) self.deferred_loading = (set(), True)
@property
def extra(self):
if self._extra is None:
self._extra = OrderedDict()
return self._extra
@property
def aggregates(self):
if self._aggregates is None:
self._aggregates = OrderedDict()
return self._aggregates
def __str__(self): def __str__(self):
""" """
Returns the query as a string of SQL with the parameter values Returns the query as a string of SQL with the parameter values
@ -245,7 +262,7 @@ class Query(object):
obj.select_for_update_nowait = self.select_for_update_nowait obj.select_for_update_nowait = self.select_for_update_nowait
obj.select_related = self.select_related obj.select_related = self.select_related
obj.related_select_cols = [] obj.related_select_cols = []
obj.aggregates = self.aggregates.copy() obj._aggregates = self._aggregates.copy() if self._aggregates is not None else None
if self.aggregate_select_mask is None: if self.aggregate_select_mask is None:
obj.aggregate_select_mask = None obj.aggregate_select_mask = None
else: else:
@ -257,7 +274,7 @@ class Query(object):
# used. # used.
obj._aggregate_select_cache = None obj._aggregate_select_cache = None
obj.max_depth = self.max_depth obj.max_depth = self.max_depth
obj.extra = self.extra.copy() obj._extra = self._extra.copy() if self._extra is not None else None
if self.extra_select_mask is None: if self.extra_select_mask is None:
obj.extra_select_mask = None obj.extra_select_mask = None
else: else:
@ -344,7 +361,7 @@ class Query(object):
# and move them to the outer AggregateQuery. # and move them to the outer AggregateQuery.
for alias, aggregate in self.aggregate_select.items(): for alias, aggregate in self.aggregate_select.items():
if aggregate.is_summary: if aggregate.is_summary:
query.aggregate_select[alias] = aggregate.relabeled_clone(relabels) query.aggregates[alias] = aggregate.relabeled_clone(relabels)
del obj.aggregate_select[alias] del obj.aggregate_select[alias]
try: try:
@ -358,7 +375,7 @@ class Query(object):
query = self query = self
self.select = [] self.select = []
self.default_cols = False self.default_cols = False
self.extra = {} self._extra = {}
self.remove_inherited_models() self.remove_inherited_models()
query.clear_ordering(True) query.clear_ordering(True)
@ -527,7 +544,7 @@ class Query(object):
# It would be nice to be able to handle this, but the queries don't # It would be nice to be able to handle this, but the queries don't
# really make sense (or return consistent value sets). Not worth # really make sense (or return consistent value sets). Not worth
# the extra complexity when you can write a real query instead. # the extra complexity when you can write a real query instead.
if self.extra and rhs.extra: if self._extra and rhs._extra:
raise ValueError("When merging querysets using 'or', you " raise ValueError("When merging querysets using 'or', you "
"cannot have extra(select=...) on both sides.") "cannot have extra(select=...) on both sides.")
self.extra.update(rhs.extra) self.extra.update(rhs.extra)
@ -756,8 +773,9 @@ class Query(object):
self.group_by = [relabel_column(col) for col in self.group_by] self.group_by = [relabel_column(col) for col in self.group_by]
self.select = [SelectInfo(relabel_column(s.col), s.field) self.select = [SelectInfo(relabel_column(s.col), s.field)
for s in self.select] for s in self.select]
self.aggregates = OrderedDict( if self._aggregates:
(key, relabel_column(col)) for key, col in self.aggregates.items()) self._aggregates = OrderedDict(
(key, relabel_column(col)) for key, col in self._aggregates.items())
# 2. Rename the alias in the internal table/alias datastructures. # 2. Rename the alias in the internal table/alias datastructures.
for ident, aliases in self.join_map.items(): for ident, aliases in self.join_map.items():
@ -967,7 +985,7 @@ class Query(object):
""" """
opts = model._meta opts = model._meta
field_list = aggregate.lookup.split(LOOKUP_SEP) field_list = aggregate.lookup.split(LOOKUP_SEP)
if len(field_list) == 1 and aggregate.lookup in self.aggregates: if len(field_list) == 1 and self._aggregates and aggregate.lookup in self.aggregates:
# Aggregate is over an annotation # Aggregate is over an annotation
field_name = field_list[0] field_name = field_list[0]
col = field_name col = field_name
@ -1049,7 +1067,7 @@ class Query(object):
lookup_parts = lookup.split(LOOKUP_SEP) lookup_parts = lookup.split(LOOKUP_SEP)
num_parts = len(lookup_parts) num_parts = len(lookup_parts)
if (len(lookup_parts) > 1 and lookup_parts[-1] in self.query_terms if (len(lookup_parts) > 1 and lookup_parts[-1] in self.query_terms
and lookup not in self.aggregates): and (not self._aggregates or lookup not in self._aggregates)):
# Traverse the lookup query to distinguish related fields from # Traverse the lookup query to distinguish related fields from
# lookup types. # lookup types.
lookup_model = self.model lookup_model = self.model
@ -1108,10 +1126,11 @@ class Query(object):
value, lookup_type = self.prepare_lookup_value(value, lookup_type, can_reuse) value, lookup_type = self.prepare_lookup_value(value, lookup_type, can_reuse)
clause = self.where_class() clause = self.where_class()
for alias, aggregate in self.aggregates.items(): if self._aggregates:
if alias in (parts[0], LOOKUP_SEP.join(parts)): for alias, aggregate in self.aggregates.items():
clause.add((aggregate, lookup_type, value), AND) if alias in (parts[0], LOOKUP_SEP.join(parts)):
return clause clause.add((aggregate, lookup_type, value), AND)
return clause
opts = self.get_meta() opts = self.get_meta()
alias = self.get_initial_alias() alias = self.get_initial_alias()
@ -1170,6 +1189,8 @@ class Query(object):
Returns whether or not all elements of this q_object need to be put Returns whether or not all elements of this q_object need to be put
together in the HAVING clause. together in the HAVING clause.
""" """
if not self._aggregates:
return False
if not isinstance(obj, Node): if not isinstance(obj, Node):
return (refs_aggregate(obj[0].split(LOOKUP_SEP), self.aggregates) return (refs_aggregate(obj[0].split(LOOKUP_SEP), self.aggregates)
or (hasattr(obj[1], 'contains_aggregate') or (hasattr(obj[1], 'contains_aggregate')
@ -1632,7 +1653,7 @@ class Query(object):
# Set only aggregate to be the count column. # Set only aggregate to be the count column.
# Clear out the select cache to reflect the new unmasked aggregates. # Clear out the select cache to reflect the new unmasked aggregates.
self.aggregates = {None: count} self._aggregates = {None: count}
self.set_aggregate_mask(None) self.set_aggregate_mask(None)
self.group_by = None self.group_by = None
@ -1781,7 +1802,8 @@ class Query(object):
self.extra_select_mask = set(names) self.extra_select_mask = set(names)
self._extra_select_cache = None self._extra_select_cache = None
def _aggregate_select(self): @property
def aggregate_select(self):
"""The OrderedDict of aggregate columns that are not masked, and should """The OrderedDict of aggregate columns that are not masked, and should
be used in the SELECT clause. be used in the SELECT clause.
@ -1789,6 +1811,8 @@ class Query(object):
""" """
if self._aggregate_select_cache is not None: if self._aggregate_select_cache is not None:
return self._aggregate_select_cache return self._aggregate_select_cache
elif not self._aggregates:
return {}
elif self.aggregate_select_mask is not None: elif self.aggregate_select_mask is not None:
self._aggregate_select_cache = OrderedDict( self._aggregate_select_cache = OrderedDict(
(k, v) for k, v in self.aggregates.items() (k, v) for k, v in self.aggregates.items()
@ -1797,11 +1821,13 @@ class Query(object):
return self._aggregate_select_cache return self._aggregate_select_cache
else: else:
return self.aggregates return self.aggregates
aggregate_select = property(_aggregate_select)
def _extra_select(self): @property
def extra_select(self):
if self._extra_select_cache is not None: if self._extra_select_cache is not None:
return self._extra_select_cache return self._extra_select_cache
if not self._extra:
return {}
elif self.extra_select_mask is not None: elif self.extra_select_mask is not None:
self._extra_select_cache = OrderedDict( self._extra_select_cache = OrderedDict(
(k, v) for k, v in self.extra.items() (k, v) for k, v in self.extra.items()
@ -1810,7 +1836,6 @@ class Query(object):
return self._extra_select_cache return self._extra_select_cache
else: else:
return self.extra return self.extra
extra_select = property(_extra_select)
def trim_start(self, names_with_path): def trim_start(self, names_with_path):
""" """