summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2019-12-18 21:50:24 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2019-12-19 10:19:29 -0500
commit7e9f273835ac68df894568ba4292bfbc74ce187b (patch)
tree96d3f1590ff45b90a791f92c622737ed3e89fcfb
parent255a6ee18b9d68b5150f1793e0a318d8ccd913bf (diff)
downloadsqlalchemy-7e9f273835ac68df894568ba4292bfbc74ce187b.tar.gz
Don't apply aliasing + adaption for simple relationship joins
Identified a performance issue in the system by which a join is constructed based on a mapped relationship. The clause adaption system would be used for the majority of join expressions including in the common case where no adaptation is needed. The conditions under which this adaptation occur have been refined so that average non-aliased joins along a simple relationship without a "secondary" table use about 70% less function calls. Change-Id: Ifbe04214576e5a9fac86ca80c1dc7145c27cd50a
-rw-r--r--doc/build/changelog/unreleased_13/rel_join.rst10
-rw-r--r--lib/sqlalchemy/orm/relationships.py20
-rw-r--r--lib/sqlalchemy/orm/util.py1
-rw-r--r--lib/sqlalchemy/testing/profiling.py7
-rw-r--r--test/aaa_profiling/test_orm.py91
-rw-r--r--test/ext/test_serializer.py1
-rw-r--r--test/profiles.txt16
7 files changed, 140 insertions, 6 deletions
diff --git a/doc/build/changelog/unreleased_13/rel_join.rst b/doc/build/changelog/unreleased_13/rel_join.rst
new file mode 100644
index 000000000..1f0e29ef1
--- /dev/null
+++ b/doc/build/changelog/unreleased_13/rel_join.rst
@@ -0,0 +1,10 @@
+.. change::
+ :tags: performance, orm
+
+ Identified a performance issue in the system by which a join is constructed
+ based on a mapped relationship. The clause adaption system would be used
+ for the majority of join expressions including in the common case where no
+ adaptation is needed. The conditions under which this adaptation occur
+ have been refined so that average non-aliased joins along a simple
+ relationship without a "secondary" table use about 70% less function calls.
+
diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py
index fc3e78ea1..7ee12ca6c 100644
--- a/lib/sqlalchemy/orm/relationships.py
+++ b/lib/sqlalchemy/orm/relationships.py
@@ -1044,6 +1044,7 @@ class RelationshipProperty(StrategizedProperty):
source_selectable=adapt_from,
source_polymorphic=True,
of_type_mapper=of_type_mapper,
+ alias_secondary=True,
)
if sj is not None:
return pj & sj
@@ -2215,12 +2216,18 @@ class RelationshipProperty(StrategizedProperty):
dest_polymorphic=False,
dest_selectable=None,
of_type_mapper=None,
+ alias_secondary=False,
):
+
+ aliased = False
+
+ if alias_secondary and self.secondary is not None:
+ aliased = True
+
if source_selectable is None:
if source_polymorphic and self.parent.with_polymorphic:
source_selectable = self.parent._with_polymorphic_selectable
- aliased = False
if dest_selectable is None:
dest_selectable = self.entity.selectable
if dest_polymorphic and self.mapper.with_polymorphic:
@@ -2229,13 +2236,20 @@ class RelationshipProperty(StrategizedProperty):
if self._is_self_referential and source_selectable is None:
dest_selectable = dest_selectable._anonymous_fromclause()
aliased = True
- else:
+ elif dest_selectable is not self.mapper._with_polymorphic_selectable:
aliased = True
dest_mapper = of_type_mapper or self.mapper
single_crit = dest_mapper._single_table_criterion
- aliased = aliased or (source_selectable is not None)
+ aliased = aliased or (
+ source_selectable is not None
+ and (
+ source_selectable
+ is not self.parent._with_polymorphic_selectable
+ or source_selectable._is_subquery
+ )
+ )
(
primaryjoin,
diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py
index b96387c08..2f78fa535 100644
--- a/lib/sqlalchemy/orm/util.py
+++ b/lib/sqlalchemy/orm/util.py
@@ -1035,6 +1035,7 @@ class _ORMJoin(expression.Join):
source_polymorphic=True,
dest_polymorphic=True,
of_type_mapper=right_info.mapper,
+ alias_secondary=True,
)
if sj is not None:
diff --git a/lib/sqlalchemy/testing/profiling.py b/lib/sqlalchemy/testing/profiling.py
index bfc4997e1..b837d9b5b 100644
--- a/lib/sqlalchemy/testing/profiling.py
+++ b/lib/sqlalchemy/testing/profiling.py
@@ -221,7 +221,7 @@ class ProfileStatsFile(object):
profile_f.close()
-def function_call_count(variance=0.05):
+def function_call_count(variance=0.05, times=1):
"""Assert a target for a test case's function call count.
The main purpose of this assertion is to detect changes in
@@ -234,8 +234,11 @@ def function_call_count(variance=0.05):
def decorate(fn):
def wrap(*args, **kw):
+ timerange = range(times)
with count_functions(variance=variance):
- return fn(*args, **kw)
+ for time in timerange:
+ rv = fn(*args, **kw)
+ return rv
return update_wrapper(wrap, fn)
diff --git a/test/aaa_profiling/test_orm.py b/test/aaa_profiling/test_orm.py
index 209bc02e3..e2df85a33 100644
--- a/test/aaa_profiling/test_orm.py
+++ b/test/aaa_profiling/test_orm.py
@@ -1,12 +1,16 @@
+from sqlalchemy import and_
from sqlalchemy import ForeignKey
from sqlalchemy import inspect
from sqlalchemy import Integer
+from sqlalchemy import join
from sqlalchemy import String
from sqlalchemy import testing
+from sqlalchemy.orm import aliased
from sqlalchemy.orm import Bundle
from sqlalchemy.orm import configure_mappers
from sqlalchemy.orm import defaultload
from sqlalchemy.orm import defer
+from sqlalchemy.orm import join as orm_join
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import Load
from sqlalchemy.orm import mapper
@@ -818,6 +822,93 @@ class JoinedEagerLoadTest(fixtures.MappedTest):
go()
+class JoinConditionTest(fixtures.DeclarativeMappedTest):
+ @classmethod
+ def setup_classes(cls):
+ class A(cls.DeclarativeBasic):
+ __tablename__ = "a"
+
+ id = Column(Integer, primary_key=True)
+ b_id = Column(ForeignKey("b.id"))
+ b = relationship("B")
+
+ class B(cls.DeclarativeBasic):
+ __tablename__ = "b"
+
+ id = Column(Integer, primary_key=True)
+ d_id = Column(ForeignKey("d.id"))
+
+ class C(cls.DeclarativeBasic):
+ __tablename__ = "c"
+
+ id = Column(Integer, primary_key=True)
+ a_id = Column(ForeignKey("a.id"))
+ d_id = Column(ForeignKey("d.id"))
+
+ class D(cls.DeclarativeBasic):
+ __tablename__ = "d"
+
+ id = Column(Integer, primary_key=True)
+
+ j = join(B, D, B.d_id == D.id).join(C, C.d_id == D.id)
+
+ A.d = relationship(
+ "D",
+ secondary=j,
+ primaryjoin=and_(A.b_id == B.id, A.id == C.a_id),
+ secondaryjoin=D.id == B.d_id,
+ uselist=False,
+ viewonly=True,
+ )
+
+ def test_a_to_b_plain(self):
+ A, B = self.classes("A", "B")
+
+ # should not use aliasing or adaption so should be cheap
+ @profiling.function_call_count(times=50)
+ def go():
+ orm_join(A, B, A.b)
+
+ go()
+
+ def test_a_to_b_aliased(self):
+ A, B = self.classes("A", "B")
+
+ a1 = aliased(A)
+
+ # uses aliasing, therefore adaption which is expensive
+ @profiling.function_call_count(times=50)
+ def go():
+ orm_join(a1, B, a1.b)
+
+ go()
+
+ def test_a_to_d(self):
+ A, D = self.classes("A", "D")
+
+ # the join condition between A and D uses a secondary selectable with
+ # overlap so incurs aliasing, which is expensive, there is also a check
+ # that determines that this overlap exists which is not currently
+ # cached
+ @profiling.function_call_count(times=50)
+ def go():
+ orm_join(A, D, A.d)
+
+ go()
+
+ def test_a_to_d_aliased(self):
+ A, D = self.classes("A", "D")
+
+ a1 = aliased(A)
+
+ # aliased, uses adaption therefore expensive
+ @profiling.function_call_count(times=50)
+ def go():
+ orm_join(a1, D, a1.d)
+
+ go()
+
+
class BranchedOptionTest(fixtures.MappedTest):
@classmethod
def define_tables(cls, metadata):
diff --git a/test/ext/test_serializer.py b/test/ext/test_serializer.py
index e5490146c..e6c7b717f 100644
--- a/test/ext/test_serializer.py
+++ b/test/ext/test_serializer.py
@@ -243,7 +243,6 @@ class SerializeTest(AssertsCompiledSQL, fixtures.MappedTest):
j2 = serializer.loads(serializer.dumps(j, -1), users.metadata)
assert j2.left is j.left
assert j2.right is j.right
- assert j2._target_adapter._next
@testing.exclude(
"sqlite", "<=", (3, 5, 9), "id comparison failing on the buildbot"
diff --git a/test/profiles.txt b/test/profiles.txt
index 493f27698..89c01f5d1 100644
--- a/test/profiles.txt
+++ b/test/profiles.txt
@@ -543,6 +543,22 @@ test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.7_postgresql
test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.7_sqlite_pysqlite_dbapiunicode_cextensions 23259
test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.7_sqlite_pysqlite_dbapiunicode_nocextensions 30268
+# TEST: test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_aliased
+
+test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_aliased 3.7_sqlite_pysqlite_dbapiunicode_nocextensions 10313
+
+# TEST: test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_plain
+
+test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_plain 3.7_sqlite_pysqlite_dbapiunicode_nocextensions 3256
+
+# TEST: test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_d
+
+test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_d 3.7_sqlite_pysqlite_dbapiunicode_nocextensions 106798
+
+# TEST: test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_d_aliased
+
+test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_d_aliased 3.7_sqlite_pysqlite_dbapiunicode_nocextensions 104564
+
# TEST: test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query
test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 2.7_mssql_pyodbc_dbapiunicode_nocextensions 439163