【问题标题】:Django: Script that executes many queries runs massively slower when executed from Admin view than when executed from shellDjango:执行许多查询的脚本在从管理视图执行时比从 shell 执行时运行速度要慢得多
【发布时间】:2021-12-30 22:32:12
【问题描述】:

我有一个脚本,它遍历外部 csv 文件的行(大约 12,000 行)并执行单个 Model.objects.get() 查询以从数据库中检索每个项目(最终产品会复杂得多,但现在它被剥离到尽可能简单的功能来尝试解决这个问题)。

目前,本地 csv 文件的路径已硬编码到脚本中。当我使用 py manage.py runscript update_products_from_csv 通过 shell 运行脚本时,它会在大约 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() 电话,并确定查询是唯一明显不同的部分。)

当我从 shell 运行它时,平均查询时间为 0.0005 秒,所有查询时间的总和约为 6.8 秒。

当我通过管理视图运行它,然后检查 Django 调试工具栏中的查询时,它按预期显示了 12,000 多个查询,并且显示的总查询时间仅为大约 3900 毫秒。但是当我查看time() 调用收集的查询时间日志时,平均查询时间为 0.013 秒(比我通过 shell 运行它时长 26 倍),所有查询时间的总和总是在156-157 秒。

当我通过管理员运行 Django 调试工具栏中的查询时,它们看起来都像 SELECT ••• FROM "products_product" WHERE "products_product"."external_id" = 10 LIMIT 21,根据工具栏,它们大多都是 0-1ms。我不确定从 shell 运行查询时如何检查查询的外观,但我无法想象它们会有所不同?我在 django-extensions runscript 文档中找不到任何关于它进行查询优化或类似操作的内容。

另一个有趣的方面是,从管理员运行它时,从我在终端中看到 result_string 打印开始,在浏览器窗口中出现成功消息之前又过了 1-3 分钟。

我不知道还要检查什么。我显然遗漏了一些基本的东西,但我不知道是什么。

【问题讨论】:

  • 为什么不在for upd in update_data 循环之前根据需要的external_ids 获取所有Products?
  • 对不起,我不认为我理解这个问题。您的意思是在执行循环之前将所有Product 对象放入查询集中,就像Product.objects.all() 一样,然后从查询集中获取每个对象?我尝试了相同的结果。还是您的意思是首先获取具有匹配external_id 值的Product 对象的查询集update_dataupdate_data 和 products 表之间或多或少 100% 重叠,因此它不会真正缩小搜索区域。

标签: django django-admin django-orm


【解决方案1】:

Reddit 上有人建议从 shell 运行脚本可能会自动启动一个新线程,在该线程中逻辑可以不受其他 Django 服务器进程的阻碍而运行,这似乎就是答案。如果我在管理视图的新线程中运行脚本,它的运行速度与我从 shell 运行时一样快。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-24
    • 2017-01-07
    • 1970-01-01
    • 1970-01-01
    • 2014-09-12
    • 1970-01-01
    相关资源
    最近更新 更多