A volte solo la semplice registrazione SQL (abilitata tramite il modulo di registrazione di Python o tramite l'argomento echo=True
su create_engine()
) può darti un'idea di quanto tempo ci vuole. Per esempio, se si accede qualcosa di giusto dopo un'operazione di SQL, si vedrebbe qualcosa di simile nel registro:
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]
se ci si collega myapp.somemessage
subito dopo l'operazione, si sa ci sono voluti 334ms per completare la parte di SQL di cose.
Registrazione SQL illustrerà anche se decine/centinaia di query vengono emesse che potrebbero essere meglio organizzate in molte meno query tramite join. Quando si utilizza l'ORM di SQLAlchemy, la funzione "caricamento ansioso" viene fornita parzialmente (contains_eager()
) o completamente (eagerload()
, eagerload_all()
) automatizza questa attività, ma senza l'ORM significa solo utilizzare i join in modo che i risultati su più tabelle possano essere caricati in un set di risultati invece di moltiplicare i numeri di query a più profondità aggiunta (es. r + r*r2 + r*r2*r3
...)
Se la registrazione rivela che le singole query impiegano troppo tempo, è necessario disporre di una suddivisione di quanto tempo è stato speso all'interno del database che elabora la query, invia i risultati sulla rete, viene gestito dal DBAPI e infine viene ricevuto dal set di risultati di SQLAlchemy e/o dal livello ORM. Ognuna di queste fasi può presentare i propri colli di bottiglia individuali, a seconda delle specifiche.
Per questo è necessario utilizzare la profilatura, ad esempio cProfile o hotshot.Qui è un decoratore che uso:
import cProfile as profiler
import gc, pstats, time
def profile(fn):
def wrapper(*args, **kw):
elapsed, stat_loader, result = _profile("foo.txt", fn, *args, **kw)
stats = stat_loader()
stats.sort_stats('cumulative')
stats.print_stats()
# uncomment this to see who's calling what
# stats.print_callers()
return result
return wrapper
def _profile(filename, fn, *args, **kw):
load_stats = lambda: pstats.Stats(filename)
gc.collect()
began = time.time()
profiler.runctx('result = fn(*args, **kw)', globals(), locals(),
filename=filename)
ended = time.time()
return ended - began, load_stats, locals()['result']
al profilo una sezione di codice, posizionarlo in una funzione con il decoratore:
@profile
def go():
return Session.query(FooClass).filter(FooClass.somevalue==8).all()
myfoos = go()
L'uscita di profilatura può essere usato per dare un'idea dove il tempo è stato speso. Se ad esempio si vede tutto il tempo trascorso all'interno di cursor.execute()
, si tratta della chiamata DBAPI di basso livello al database e significa che la query deve essere ottimizzata, aggiungendo indici o ristrutturando la query e/o lo schema sottostante. Per questo compito, consiglierei di usare pgadmin insieme alla sua utility grafica EXPLAIN per vedere che tipo di lavoro sta facendo la query.
Se vedete molte migliaia di chiamate relative al recupero di righe, potrebbe significare che la vostra query restituisce più righe del previsto: un prodotto cartesiano a seguito di un join incompleto può causare questo problema. Un altro problema è il tempo trascorso nella gestione dei tipi: un tipo SQLAlchemy come Unicode
eseguirà la codifica/decodifica delle stringhe sui parametri di collegamento e sulle colonne dei risultati, che potrebbero non essere necessarie in tutti i casi.
L'output di un profilo può essere un po 'scoraggiante, ma dopo un po' di pratica sono molto facili da leggere. C'era una volta qualcuno sulla mailing list che sosteneva la lentezza, e dopo averlo postare i risultati del profilo, sono stato in grado di dimostrare che i problemi di velocità erano dovuti alla latenza della rete - il tempo trascorso all'interno di cursor.execute() e di tutto il Python i metodi erano molto veloci, mentre la maggior parte del tempo era speso per socket.receive().
Se ti senti ambizioso, c'è anche un esempio più impegnativo di SQLAlchemy profiling all'interno dei test delle unità SQLAlchemy, se fai un salto intorno a http://www.sqlalchemy.org/trac/browser/sqlalchemy/trunk/test/aaa_profiling. Lì, abbiamo test che usano decoratori che asseriscono un numero massimo di chiamate di metodo usate per operazioni particolari, in modo che se qualcosa di inefficiente viene controllato, i test lo rivelano (è importante notare che in Python le chiamate di funzione hanno il più alto sovraccarico di qualsiasi operazione, e il conteggio delle chiamate è più spesso o quasi proporzionale al tempo trascorso). Di nota sono i test "zoomark" che utilizzano un fantastico schema di "cattura SQL" che elimina il sovraccarico del DBAPI dall'equazione - sebbene questa tecnica non sia realmente necessaria per la profilazione di varietà da giardino.
Se stai usando Flask-SQLAlchemy, aggiungere 'SQLALCHEMY_ECHO = true' alla configurazione della tua applicazione, invece. – henrycjc