【发布时间】: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_data?update_data和 products 表之间或多或少 100% 重叠,因此它不会真正缩小搜索区域。
标签: django django-admin django-orm