Django: Сценарий, выполняющий множество запросов, работает значительно медленнее при выполнении из представления администратора, чем при выполнении из оболочки
У меня есть сценарий, который циклически просматривает строки внешнего csv-файла (около 12 000 строк) и выполняет один запрос Model.objects.get() для получения каждого элемента из базы данных (конечный продукт будет намного сложнее, но сейчас он урезан до минимальной функциональности, чтобы попытаться разобраться в этом).
На данный момент путь к локальному csv-файлу жестко закодирован в скрипте. Когда я запускаю скрипт через shell, используя py manage.py runscript update_products_from_csv
, он выполняется примерно за 6 секунд.
Конечная цель - иметь возможность загружать csv через администратора, а затем запускать скрипт оттуда. Я уже смог этого добиться, но время выполнения при таком способе занимает более 160 секунд. Вид для этого в админке выглядит так...
from .scripts import update_products_from_csv
class CsvUploadForm(forms.Form):
csv_file = forms.FileField(label='Upload CSV')
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
# list_display, list_filter, fieldsets, etc
def changelist_view(self, request, extra_context=None):
extra_context = extra_context or {}
extra_context['csv_upload_form'] = CsvUploadForm()
return super().changelist_view(request, extra_context=extra_context)
def get_urls(self):
urls = super().get_urls()
new_urls = [path('upload-csv/', self.upload_csv),]
return new_urls + urls
def upload_csv(self, request):
if request.method == 'POST':
# csv_file = request.FILES['csv_file'].file
# result_string = update_products_from_csv.run(csv_file)
# I commented out the above two lines and added the below line to rule out
# the possibility that the csv upload itself was the problem. Whether I execute
# the script using the uploaded file or let it use the hardcoded local path,
# the results are the same. It works, but takes more than 20 times longer
# than executing the same script from the shell.
result_string = update_products_from_csv.run()
print(result_string)
messages.success(request, result_string)
return HttpResponseRedirect(reverse('admin:products_product_changelist'))
На данный момент фактические выполняемые части скрипта примерно так же просты...
import csv
from time import time
from apps.products.models import Product
CSV_PATH = 'path/to/local/csv_file.csv'
def run():
csv_data = get_csv_data()
update_data = build_update_data(csv_data)
update_handler(update_data)
return 'Finished'
def get_csv_data():
with open(CSV_PATH, 'r') as f:
return [d for d in csv.DictReader(f)]
def build_update_data(csv_data):
update_data = []
# Code that loops through csv data, applies some custom logic, and builds a list of
# dicts with the data cleaned and formatted as needed
return update_data
def update_handler(update_data):
query_times = []
for upd in update_data:
iter_start = time()
product_obj = Product.objects.get(external_id=upd['external_id'])
# external_id is not the primary key but is an indexed field in the Product model
query_times.append(time() - iter_start)
# Code to export query_times to an external file for analysis
update_handler()
имеет кучу другого кода, проверяющего значения полей, чтобы увидеть, нужно ли что-то изменить, и создающего объекты, когда совпадения не существует, но сейчас это все закомментировано. Как вы можете видеть, я также проверяю время выполнения каждого запроса и записываю эти значения в журнал. (Я весь день отбрасывал вызовы time()
в разных местах и определил, что запрос - единственная часть, которая заметно отличается)
Когда я запускаю его из оболочки, среднее время запроса составляет 0,0005 секунды, а общее время всех запросов составляет примерно 6,8 секунды каждый раз.
Когда я запускаю его через вид администратора, а затем проверяю запросы в Django Debug Toolbar, он показывает 12000+ запросов, как и ожидалось, и показывает общее время запроса всего около 3900 мс. Но когда я смотрю на журнал времени запросов, собранных вызовами time()
, среднее время запроса составляет 0.013 секунды (в 26 раз дольше, чем при запуске через оболочку), а общее время всех запросов всегда составляет 156-157 секунд.
Запросы в Django Debug Toolbar, когда я запускаю его через админку, все выглядят как SELECT ••• FROM "products_product" WHERE "products_product"."external_id" = 10 LIMIT 21
, и согласно панели инструментов они в основном все 0-1мс. Я не уверен, как я могу проверить, как выглядят запросы при запуске из оболочки, но я не могу представить, что они будут отличаться? Я не смог найти ничего в документации django-extensions runscript о том, что он оптимизирует запросы или что-то в этом роде.
Еще одна интересная сторона заключается в том, что при запуске из админки, с момента, когда я вижу в терминале печать result_string
, проходит еще 1-3 минуты до появления сообщения об успехе в окне браузера.
Я не знаю, что еще проверить. Очевидно, я упускаю что-то фундаментальное, но я не знаю что.
Кто-то на Reddit предположил, что запуск скрипта из оболочки может автоматически запускать новый поток, в котором логика может работать, не отягощенная другими процессами сервера Django, и, похоже, это ответ. Если я запускаю скрипт в новом потоке из окна администратора, он выполняется так же быстро, как и при запуске из оболочки.