SQLAlchemy 2.0 Dokumentation
Häufig gestellte Fragen
- Installation
- Verbindungen / Engines
- MetaData / Schema
- SQL-Ausdrücke
- ORM-Konfiguration
- Performance¶
- Sessions / Abfragen
- Probleme bei der Integration von Drittanbietern
Projektversionen
- Vorherige: ORM-Konfiguration
- Nächste: Sessions / Queries
- Nach oben: Startseite
- Auf dieser Seite
Performance¶
Warum ist meine Anwendung nach einem Upgrade auf 1.4 und/oder 2.x langsam?¶
SQLAlchemy ab Version 1.4 enthält eine SQL-Kompilierungs-Caching-Funktion, die es Core- und ORM-SQL-Konstrukten ermöglicht, ihre stringifizierte Form zusammen mit anderen strukturellen Informationen, die zur Abfrage von Ergebnissen aus der Anweisung verwendet werden, zu cachen. Dies ermöglicht es, den relativ teuren String-Kompilierungsprozess zu überspringen, wenn ein weiteres strukturell äquivalentes Konstrukt das nächste Mal verwendet wird. Dieses System stützt sich auf Funktionalitäten, die für alle SQL-Konstrukte implementiert sind, einschließlich Objekten wie Column, select() und TypeEngine-Objekten, um einen Cache-Schlüssel zu erzeugen, der ihren Zustand vollständig repräsentiert, in dem Maße, wie er den SQL-Kompilierungsprozess beeinflusst.
Das Caching-System ermöglicht es SQLAlchemy 1.4 und höher, performanter als SQLAlchemy 1.3 zu sein, was die Zeit betrifft, die für die wiederholte Umwandlung von SQL-Konstrukten in Strings aufgewendet wird. Dies funktioniert jedoch nur, wenn das Caching für das verwendete Dialekt und die SQL-Konstrukte aktiviert ist; wenn nicht, ist die String-Kompilierung normalerweise ähnlich wie bei SQLAlchemy 1.3, mit einer leichten Geschwindigkeitsabnahme in einigen Fällen.
Es gibt jedoch einen Fall, in dem die Leistung des ORM erheblich schlechter sein kann als die von 1.3 oder früheren Versionen, wenn das neue Caching-System von SQLAlchemy deaktiviert wurde (aus den unten genannten Gründen). Dies liegt an der fehlenden Zwischenspeicherung innerhalb von ORM Lazy Loadern und Object Refresh Queries, die in den Releases 1.3 und früher das jetzt veraltete BakedQuery-System verwendeten. Wenn eine Anwendung nach dem Wechsel zu 1.4 eine signifikante (30 % oder mehr) Leistungsverschlechterung (gemessen an der Zeit für den Abschluss von Operationen) feststellt, ist dies die wahrscheinliche Ursache des Problems. Schritte zur Abhilfe finden Sie unten.
Siehe auch
SQL-Kompilierungs-Caching – Überblick über das Caching-System
Objekt erzeugt keinen Cache-Schlüssel, Leistungsauswirkungen – Zusätzliche Informationen zu den Warnungen, die für Elemente generiert werden, die kein Caching aktivieren.
Schritt eins – SQL-Logging aktivieren und prüfen, ob Caching funktioniert¶
Hier verwenden wir die unter Engine-Logging beschriebene Technik und suchen nach Anweisungen mit dem Indikator [no key] oder sogar [dialect does not support caching]. Die Indikatoren, die wir für SQL-Anweisungen sehen würden, die erfolgreich am Caching-System teilnehmen, wären [generated in Xs] bei erstmaliger Ausführung von Anweisungen und dann [cached since Xs ago] für die überwiegende Mehrheit der nachfolgenden Anweisungen. Wenn [no key] insbesondere für SELECT-Anweisungen verbreitet ist oder wenn das Caching aufgrund von [dialect does not support caching] vollständig deaktiviert ist, kann dies die Ursache für erhebliche Leistungseinbußen sein.
Schritt zwei – Identifizieren, welche Konstrukte das Aktivieren von Caching verhindern¶
Vorausgesetzt, Anweisungen werden nicht gecacht, sollten früh in den Logs der Anwendung (nur SQLAlchemy 1.4.28 und höher) Warnungen ausgegeben werden, die auf Dialekte, TypeEngine-Objekte und SQL-Konstrukte hinweisen, die nicht am Caching teilnehmen.
Für benutzerspezifische Datentypen, wie z. B. solche, die von TypeDecorator und UserDefinedType abstammen, sehen die Warnungen so aus:
sqlalchemy.ext.SAWarning: MyType will not produce a cache key because the
``cache_ok`` attribute is not set to True. This can have significant
performance implications including some performance degradations in
comparison to prior SQLAlchemy versions. Set this attribute to True if this
type object's state is safe to use in a cache key, or False to disable this
warning.Für benutzerdefinierte und Drittanbieter-SQL-Elemente, wie z. B. solche, die mit den unter Benutzerdefinierte SQL-Konstrukte und Kompilierungserweiterung beschriebenen Techniken erstellt wurden, sehen diese Warnungen so aus:
sqlalchemy.exc.SAWarning: Class MyClass will not make use of SQL
compilation caching as it does not set the 'inherit_cache' attribute to
``True``. This can have significant performance implications including some
performance degradations in comparison to prior SQLAlchemy versions. Set
this attribute to True if this object can make use of the cache key
generated by the superclass. Alternatively, this attribute may be set to
False which will disable this warning.Für benutzerdefinierte und Drittanbieter-Dialekte, die die Dialect-Klassenhierarchie verwenden, sehen die Warnungen so aus:
sqlalchemy.exc.SAWarning: Dialect database:driver will not make use of SQL
compilation caching as it does not set the 'supports_statement_cache'
attribute to ``True``. This can have significant performance implications
including some performance degradations in comparison to prior SQLAlchemy
versions. Dialect maintainers should seek to set this attribute to True
after appropriate development and testing for SQLAlchemy 1.4 caching
support. Alternatively, this attribute may be set to False which will
disable this warning.Schritt drei – Caching für die gegebenen Objekte aktivieren und/oder Alternativen suchen¶
Schritte zur Abmilderung des fehlenden Cachings sind:
Überprüfen und setzen Sie
ExternalType.cache_okaufTruefür alle benutzerdefinierten Typen, die vonTypeDecorator,UserDefinedTypeabstammen, sowie von Unterklassen dieser, wie z.B.PickleType. Setzen Sie dies **nur**, wenn der benutzerdefinierte Typ keine zusätzlichen Zustandsattribute enthält, die die SQL-Darstellung beeinflussen.class MyCustomType(TypeDecorator): cache_ok = True impl = String
Wenn die verwendeten Typen aus einer Drittanbieterbibliothek stammen, konsultieren Sie die Wartungsarbeiten dieser Bibliothek, damit sie angepasst und veröffentlicht werden kann.
Siehe auch
ExternalType.cache_ok– Hintergrundinformationen zu den Anforderungen für die Aktivierung des Cachings für benutzerdefinierte Datentypen.Stellen Sie sicher, dass Drittanbieter-Dialekte
Dialect.supports_statement_cacheaufTruesetzen. Dies deutet darauf hin, dass die Wartungsarbeiten eines Drittanbieter-Dialekts sichergestellt haben, dass ihr Dialekt mit SQLAlchemy 1.4 oder höher funktioniert und dass ihr Dialekt keine Kompilierungsfunktionen enthält, die das Caching beeinträchtigen könnten. Da es einige gängige Kompilierungsmuster gibt, die das Caching tatsächlich beeinträchtigen können, ist es wichtig, dass die Dialekt-Wartungsarbeiten dies sorgfältig prüfen und testen und dabei auf ältere Muster eingehen, die mit dem Caching nicht funktionieren.Siehe auch
Caching für Drittanbieter-Dialekte – Hintergrundinformationen und Beispiele für Drittanbieter-Dialekte zur Teilnahme am SQL-Statement-Caching.
Benutzerdefinierte SQL-Klassen, einschließlich aller DQL/DML-Konstrukte, die Sie mit der Erweiterung für benutzerdefinierte SQL-Konstrukte und Kompilierung erstellen könnten, sowie Ad-hoc-Unterklassen von Objekten wie
ColumnoderTable. Das AttributHasCacheKey.inherit_cachekann für triviale Unterklassen aufTruegesetzt werden, die keine unterklassenspezifischen Zustandsinformationen enthalten, die die SQL-Kompilierung beeinflussen.Siehe auch
Unterstützung für das Caching von benutzerdefinierten Konstrukten aktivieren – Richtlinien für die Anwendung des Attributs
HasCacheKey.inherit_cache.
Siehe auch
SQL-Kompilierungs-Caching – Überblick über das Caching-System
Objekt erzeugt keinen Cache-Schlüssel, Leistungsauswirkungen – Hintergrundinformationen zu Warnungen, die ausgegeben werden, wenn das Caching für bestimmte Konstrukte und/oder Dialekte nicht aktiviert ist.
Wie kann ich eine SQLAlchemy-gestützte Anwendung profilieren?¶
Die Suche nach Leistungsproblemen umfasst in der Regel zwei Strategien. Eine ist das Query-Profiling und die andere das Code-Profiling.
Query-Profiling¶
Manchmal kann einfaches SQL-Logging (aktiviert über das Logging-Modul von Python oder über das Argument echo=True in create_engine()) einen Hinweis darauf geben, wie lange Dinge dauern. Wenn Sie beispielsweise etwas direkt nach einer SQL-Operation protokollieren, sehen Sie in Ihrem Log etwas wie das Folgende:
17:37:48,325 INFO [sqlalchemy.engine.base.Engine.0x...048c] SELECT ...
17:37:48,326 INFO [sqlalchemy.engine.base.Engine.0x...048c] {<params>}
17:37:48,660 DEBUG [myapp.somemessage]Wenn Sie myapp.somemessage direkt nach der Operation protokollieren, wissen Sie, dass der SQL-Teil 334 ms gedauert hat.
Das Protokollieren von SQL zeigt auch, ob Dutzende/Hunderte von Abfragen ausgegeben werden, die besser in viel weniger Abfragen organisiert werden könnten. Bei der Verwendung des SQLAlchemy ORM bietet die Funktion "eager loading" die Möglichkeit, dies teilweise (contains_eager()) oder vollständig (joinedload(), subqueryload()) zu automatisieren, aber ohne das ORM "eager loading" bedeutet typischerweise die Verwendung von Joins, damit Ergebnisse aus mehreren Tabellen in einem Ergebnis-Set geladen werden können, anstatt die Anzahl der Abfragen zu vervielfachen, wenn die Tiefe zunimmt (d.h. r + r*r2 + r*r2*r3 …)
Für längerfristiges Profiling von Abfragen oder zur Implementierung eines "Slow Query"-Monitors auf Anwendungsebene können Ereignisse verwendet werden, um Cursor-Ausführungen abzufangen, wie im folgenden Beispiel:
from sqlalchemy import event
from sqlalchemy.engine import Engine
import time
import logging
logging.basicConfig()
logger = logging.getLogger("myapp.sqltime")
logger.setLevel(logging.DEBUG)
@event.listens_for(Engine, "before_cursor_execute")
def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
conn.info.setdefault("query_start_time", []).append(time.time())
logger.debug("Start Query: %s", statement)
@event.listens_for(Engine, "after_cursor_execute")
def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
total = time.time() - conn.info["query_start_time"].pop(-1)
logger.debug("Query Complete!")
logger.debug("Total Time: %f", total)Oben verwenden wir die Ereignisse ConnectionEvents.before_cursor_execute() und ConnectionEvents.after_cursor_execute(), um einen Abfangpunkt um die Ausführung einer Anweisung herum einzurichten. Wir hängen einen Timer mit dem info-Wörterbuch an die Verbindung an; wir verwenden hier einen Stack für den gelegentlichen Fall, dass die Cursor-Ausführungsereignisse verschachtelt sein können.
Code-Profiling¶
Wenn das Logging zeigt, dass einzelne Abfragen zu lange dauern, benötigen Sie eine Aufschlüsselung, wie viel Zeit für die Datenbankverarbeitung der Abfrage, die Übertragung von Ergebnissen über das Netzwerk, die Handhabung durch die DBAPI und schließlich den Empfang durch das Ergebnis-Set von SQLAlchemy und/oder die ORM-Schicht aufgewendet wurde. Jede dieser Stufen kann je nach Spezifität ihre eigenen Engpässe aufweisen.
Dazu müssen Sie das Python Profiling Module verwenden. Nachfolgend finden Sie ein einfaches Rezept, das das Profiling in einen Kontextmanager integriert:
import cProfile
import io
import pstats
import contextlib
@contextlib.contextmanager
def profiled():
pr = cProfile.Profile()
pr.enable()
yield
pr.disable()
s = io.StringIO()
ps = pstats.Stats(pr, stream=s).sort_stats("cumulative")
ps.print_stats()
# uncomment this to see who's calling what
# ps.print_callers()
print(s.getvalue())Um einen Codeabschnitt zu profilieren:
with profiled():
session.scalars(select(FooClass).where(FooClass.somevalue == 8)).all()Die Ausgabe des Profilings kann dazu dienen, eine Vorstellung davon zu bekommen, wo die Zeit verbracht wird. Ein Abschnitt der Profiling-Ausgabe sieht so aus:
13726 function calls (13042 primitive calls) in 0.014 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
222/21 0.001 0.000 0.011 0.001 lib/sqlalchemy/orm/loading.py:26(instances)
220/20 0.002 0.000 0.010 0.001 lib/sqlalchemy/orm/loading.py:327(_instance)
220/20 0.000 0.000 0.010 0.000 lib/sqlalchemy/orm/loading.py:284(populate_state)
20 0.000 0.000 0.010 0.000 lib/sqlalchemy/orm/strategies.py:987(load_collection_from_subq)
20 0.000 0.000 0.009 0.000 lib/sqlalchemy/orm/strategies.py:935(get)
1 0.000 0.000 0.009 0.009 lib/sqlalchemy/orm/strategies.py:940(_load)
21 0.000 0.000 0.008 0.000 lib/sqlalchemy/orm/strategies.py:942(<genexpr>)
2 0.000 0.000 0.004 0.002 lib/sqlalchemy/orm/query.py:2400(__iter__)
2 0.000 0.000 0.002 0.001 lib/sqlalchemy/orm/query.py:2414(_execute_and_instances)
2 0.000 0.000 0.002 0.001 lib/sqlalchemy/engine/base.py:659(execute)
2 0.000 0.000 0.002 0.001 lib/sqlalchemy/sql/elements.py:321(_execute_on_connection)
2 0.000 0.000 0.002 0.001 lib/sqlalchemy/engine/base.py:788(_execute_clauseelement)
...Oben sehen wir, dass die SQLAlchemy-Funktion instances() 222 Mal (rekursiv und 21 Mal von außen) aufgerufen wurde und insgesamt 0,011 Sekunden für alle Aufrufe zusammen benötigte.
Langsame Ausführung¶
Die Einzelheiten dieser Aufrufe können uns zeigen, wo die Zeit verbracht wird. Wenn Sie zum Beispiel sehen, dass Zeit in cursor.execute(), z.B. gegen die DBAPI, verbracht wird:
2 0.102 0.102 0.204 0.102 {method 'execute' of 'sqlite3.Cursor' objects}Dies würde darauf hindeuten, dass die Datenbank lange braucht, um mit der Rückgabe von Ergebnissen zu beginnen, und dass Ihre Abfrage optimiert werden sollte, entweder durch Hinzufügen von Indizes oder durch Umstrukturierung der Abfrage und/oder des zugrundeliegenden Schemas. Für diese Aufgabe ist eine Analyse des Abfrageplans erforderlich, wobei ein System wie EXPLAIN, SHOW PLAN usw. verwendet wird, wie es vom Datenbank-Backend bereitgestellt wird.
Langsame Ergebnisabholung – Core¶
Wenn Sie andererseits Tausende von Aufrufen im Zusammenhang mit dem Abrufen von Zeilen sehen oder sehr lange Aufrufe von fetchall(), kann dies bedeuten, dass Ihre Abfrage mehr Zeilen als erwartet zurückgibt oder dass das Abrufen von Zeilen selbst langsam ist. Das ORM selbst verwendet typischerweise fetchall() zum Abrufen von Zeilen (oder fetchmany(), wenn die Option Query.yield_per() verwendet wird).
Eine übermäßig große Anzahl von Zeilen würde sich durch einen sehr langsamen Aufruf von fetchall() auf DBAPI-Ebene bemerkbar machen.
2 0.300 0.600 0.300 0.600 {method 'fetchall' of 'sqlite3.Cursor' objects}Eine unerwartet große Anzahl von Zeilen, auch wenn das Endergebnis nicht viele Zeilen zu enthalten scheint, kann das Ergebnis eines kartesischen Produkts sein – wenn mehrere Zeilensätze kombiniert werden, ohne die Tabellen entsprechend zu verknüpfen. Es ist oft einfach, dieses Verhalten mit SQLAlchemy Core oder ORM-Abfragen zu erzeugen, wenn die falschen Column-Objekte in einer komplexen Abfrage verwendet werden, die zusätzliche, unerwartete FROM-Klauseln einbezieht.
Andererseits, ein schneller Aufruf von fetchall() auf DBAPI-Ebene, aber dann Langsamkeit, wenn SQLAlchemy's CursorResult aufgefordert wird, ein fetchall() auszuführen, kann auf Langsamkeit bei der Verarbeitung von Datentypen hindeuten, wie z.B. Unicode-Konvertierungen und ähnliches.
# the DBAPI cursor is fast...
2 0.020 0.040 0.020 0.040 {method 'fetchall' of 'sqlite3.Cursor' objects}
...
# but SQLAlchemy's result proxy is slow, this is type-level processing
2 0.100 0.200 0.100 0.200 lib/sqlalchemy/engine/result.py:778(fetchall)In einigen Fällen führt ein Backend möglicherweise eine Typen-Level-Verarbeitung durch, die nicht erforderlich ist. Genauer gesagt, Aufrufe innerhalb der Typ-API, die langsam sind, sind bessere Indikatoren – unten sehen Sie, wie es aussieht, wenn wir einen solchen Typ verwenden:
from sqlalchemy import TypeDecorator
import time
class Foo(TypeDecorator):
impl = String
def process_result_value(self, value, thing):
# intentionally add slowness for illustration purposes
time.sleep(0.001)
return valueDie Profiling-Ausgabe dieser absichtlich langsamen Operation kann wie folgt aussehen:
200 0.001 0.000 0.237 0.001 lib/sqlalchemy/sql/type_api.py:911(process)
200 0.001 0.000 0.236 0.001 test.py:28(process_result_value)
200 0.235 0.001 0.235 0.001 {time.sleep}Das heißt, wir sehen viele teure Aufrufe innerhalb des type_api-Systems, und das eigentliche zeitaufwendige ist der Aufruf time.sleep().
Überprüfen Sie unbedingt die Dialekt-Dokumentation auf Hinweise zu bekannten Vorschlägen zur Leistungsoptimierung auf dieser Ebene, insbesondere für Datenbanken wie Oracle. Es können Systeme im Zusammenhang mit der Sicherstellung der numerischen Genauigkeit oder der String-Verarbeitung vorhanden sein, die möglicherweise nicht in allen Fällen erforderlich sind.
Es kann auch noch auf niedrigerer Ebene Punkte geben, an denen die Leistung beim Abrufen von Zeilen leidet; zum Beispiel, wenn die aufgewendete Zeit auf einen Aufruf wie socket.receive() konzentriert zu sein scheint, könnte dies darauf hindeuten, dass alles schnell ist, außer der tatsächlichen Netzwerkverbindung, und zu viel Zeit mit der Datenübertragung über das Netzwerk verbracht wird.
Langsame Ergebnisabholung – ORM¶
Um Langsamkeit beim Abrufen von Zeilen durch das ORM (was der häufigste Bereich der Leistungsbedenken ist) zu erkennen, zeigen Aufrufe wie populate_state() und _instance() die individuelle Populatierung von ORM-Objekten an:
# the ORM calls _instance for each ORM-loaded row it sees, and
# populate_state for each ORM-loaded row that results in the population
# of an object's attributes
220/20 0.001 0.000 0.010 0.000 lib/sqlalchemy/orm/loading.py:327(_instance)
220/20 0.000 0.000 0.009 0.000 lib/sqlalchemy/orm/loading.py:284(populate_state)Die Langsamkeit des ORM beim Umwandeln von Zeilen in ORM-gemappte Objekte ist ein Produkt der Komplexität dieser Operation in Kombination mit dem Overhead von cPython. Gängige Strategien zur Abmilderung hierbei sind:
Einzelne Spalten statt ganzer Entitäten abrufen, d.h.
select(User.id, User.name)
anstatt
select(User)
Verwenden Sie
Bundle-Objekte, um spaltenbasierte Ergebnisse zu organisieren.u_b = Bundle("user", User.id, User.name) a_b = Bundle("address", Address.id, Address.email) for user, address in session.execute(select(u_b, a_b).join(User.addresses)): ...
Verwenden Sie Ergebnis-Caching – siehe Dogpile Caching für ein ausführliches Beispiel.
Erwägen Sie einen schnelleren Interpreter wie den von PyPy.
Die Ausgabe eines Profilings kann etwas einschüchternd sein, aber nach etwas Übung ist sie sehr leicht zu lesen.
Siehe auch
Performance – eine Reihe von Demonstrationen zur Leistung mit integrierten Profiling-Funktionen.
Ich füge 400.000 Zeilen mit dem ORM ein und es ist sehr langsam!¶
Die Art der ORM-Einfügungen hat sich geändert, da die meisten enthaltenen Treiber ab SQLAlchemy 2.0 RETURNING mit insertmanyvalues-Unterstützung verwenden. Siehe den Abschnitt Optimierte ORM-Bulk-Einfügung jetzt für alle Backends außer MySQL implementiert für Details.
Insgesamt sollten die integrierten Treiber von SQLAlchemy außer dem von MySQL jetzt eine sehr schnelle ORM-Bulk-Einfügungsleistung bieten.
Drittanbieter-Treiber können sich ebenfalls mit geringfügigen Codeänderungen an der neuen Bulk-Infrastruktur beteiligen, vorausgesetzt, ihre Backends unterstützen die erforderlichen Syntaxen. SQLAlchemy-Entwickler ermutigen Benutzer von Drittanbieter-Dialekten, Probleme bei diesen Treibern zu melden, damit sie SQLAlchemy-Entwickler um Hilfe bitten können.
Die Designs von flambé! dem Drachen und Der Alchemist wurden von Rotem Yaari erstellt und großzügig gespendet.
Erstellt mit Sphinx 7.2.6. Dokumentation zuletzt generiert: Di 11 Mär 2025 14:40:17 EDT