Ответ 1
Иногда просто обычное ведение журнала SQL (включено через модуль протоколирования python или с помощью аргумента echo=True
на create_engine()
) может дать вам представление о том, как долго это происходит. Например, если вы зарегистрируете что-то сразу после операции SQL, вы увидите что-то подобное в своем журнале:
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]
если вы зарегистрировали myapp.somemessage
сразу после операции, вы знаете, что для завершения SQL-части вещей потребовалось 334 мс.
Журнал регистрации SQL также будет иллюстрировать, будут ли выпущены десятки/сотни запросов, которые могут быть лучше организованы в гораздо меньшее число запросов через объединения. При использовании ORM SQLAlchemy функция "надежной загрузки" предоставляется частично (contains_eager()
) или полностью (eagerload()
, eagerload_all()
) автоматизирует это действие, но без ORM это просто означает использование объединений, чтобы результаты несколько таблиц могут быть загружены в один результирующий набор вместо умножения числа запросов при добавлении большего количества глубины (т.е. r + r*r2 + r*r2*r3
...)
Если ведение журнала показывает, что отдельные запросы занимают слишком много времени, вам потребуется разбивка того, сколько времени было потрачено в базе данных, обрабатывающей запрос, отправляя результаты по сети, обрабатывая DBAPI и, наконец, получая SQLAlchemy и/или ORM-уровень. Каждый из этих этапов может представить свои собственные узкие места в зависимости от специфики.
Для этого вам нужно использовать профилирование, например cProfile или hotshot. Вот декоратор, который я использую:
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 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']
Чтобы профилировать раздел кода, поместите его в функцию с декоратором:
@profile
def go():
return Session.query(FooClass).filter(FooClass.somevalue==8).all()
myfoos = go()
Вывод профилирования может быть использован для того, чтобы дать представление о времени, в которое расходуется время. Если, например, вы видите, что все время тратится на cursor.execute()
, то вызов DBAPI низкого уровня в базу данных, и это означает, что ваш запрос должен быть оптимизирован либо путем добавления индексов, либо для реструктуризации запроса и/или базовой схемы. Для этой задачи я бы рекомендовал использовать pgadmin вместе со своей графической утилитой EXPLAIN, чтобы увидеть, какую работу выполняет запрос.
Если вы видите много тысяч вызовов, связанных с извлечением строк, это может означать, что ваш запрос возвращает больше строк, чем ожидалось - декартовой продукт из-за неполного соединения может вызвать эту проблему. Еще одна проблема - время, затрачиваемое на обработку типов. Тип SQLAlchemy, такой как Unicode
, будет выполнять строковое кодирование/декодирование по параметрам связывания и столбцам результатов, что может не понадобиться во всех случаях.
Вывод профиля может быть немного сложным, но после некоторой практики их очень легко читать. Когда-то кто-то в списке рассылки утверждал, что он медленный, и после того, как он опубликовал результаты профиля, я смог продемонстрировать, что проблемы с скоростью были связаны с задержкой в сети - временем, проведенным в курсоре .execute(), а также всем Python методы были очень быстрыми, тогда как большая часть времени была потрачена на socket.receive().
Если вы чувствуете амбициозность, есть еще более привлекательный пример профилирования SQLAlchemy в модульных тестах SQLAlchemy, если вы ткнете http://www.sqlalchemy.org/trac/browser/sqlalchemy/trunk/test/aaa_profiling. Там у нас есть тесты с использованием декораторов, которые утверждают, что максимальное количество вызовов методов используется для определенных операций, поэтому, если что-то неэффективное будет проверено, тесты покажут его (важно отметить, что в Python вызовы функций имеют самый высокий накладные расходы на любую операцию, а количество вызовов чаще всего не пропорционально времени). Следует отметить те тесты "zoomark", которые используют причудливую схему "захвата SQL", которая устраняет накладные расходы DBAPI из уравнения - хотя эта техника не нужна для профилирования садовых сортов.