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, и, похоже, это ответ. Если я запускаю скрипт в новом потоке из окна администратора, он выполняется так же быстро, как и при запуске из оболочки.

Вернуться на верх