用户表有Mobile字段,设置了unique=True情况下,可以利用唯一性冲突来实现'不存在就插入,存在就跳过'。但那种方式性能更佳呢?
因为mysql有INSERT IGNORE INTO 专门应对这种场景,用原生SQL效率肯定最高。而Django ORM项目中使用原生SQL有点麻烦。
Django ORM可选的方式有三个,那种开销最小呢:
get_or_create -- 缺点:如果数据存在则加载,开销大
try: except IntegrityError --缺点:处理冲突也需要额外开销
if not .exists --需要先select,也是有额外开销
chatgpt一开始推荐get_or_create+only, gemini一开始推荐try: except IntegrityError,经过一轮质疑之后两位AI都倾向于objects.bulk_create([new], ignore_conflicts=True)。 为了验证,拿了1500条数据进行验证,发现.bulk_create([new], ignore_conflicts=True)在批量处理时最高效。但逐条处理并不占优势,但有点意外的是if not .exists 效率并不比try: except IntegrityError低,两者用时非常接近,多轮测试各有胜负。
结论:推荐if not .exists,既直观,效率又不低。
from django.core.management.base import BaseCommand
from django.db import IntegrityError
from your_app.models import AccDetail # 替换为你的 app 名称和模型路径
import time
class Command(BaseCommand):
help = "在现有表上,用自定义手机号列表测试四种“已存在”场景下的插入/忽略性能"
def handle(self, *args, **options):
# —— 在这里填入你的、已确保存在于表中的手机号列表 —— #
PHONES = [
'13800000000',
'13900000000',
'13600000000',
'13100000000'
]
results = {}
# 方案一:exists() + save()
start = time.time()
for phone in PHONES:
if not AccDetail.objects.filter(Mobile=phone).exists():
AccDetail(Mobile=phone).save()
results['exists_then_save'] = time.time() - start
# 方案二:create() + 捕获 IntegrityError
start = time.time()
for phone in PHONES:
try:
AccDetail.objects.create(Mobile=phone)
except IntegrityError:
pass
results['create_with_exception'] = time.time() - start
# 方案三:bulk_create(ignore_conflicts=True)
start = time.time()
objs = [AccDetail(Mobile=phone) for phone in PHONES]
AccDetail.objects.bulk_create(objs, ignore_conflicts=True)
results['bulk_create_ignore'] = time.time() - start
# 方案四:bulk_create(ignore_conflicts=True)
start = time.time()
for phone in PHONES:
AccDetail.objects.bulk_create([AccDetail(Mobile=phone)], ignore_conflicts=True)
results['bulk_create_ignore_one_by_one'] = time.time() - start
# 方案五:only('id','Mobile').get_or_create()
start = time.time()
for phone in PHONES:
AccDetail.objects.only('id', 'Mobile').get_or_create(Mobile=phone)
results['get_or_create_only'] = time.time() - start
# 方案六:无only.get_or_create()
start = time.time()
for phone in PHONES:
AccDetail.objects.get_or_create(Mobile=phone)
results['get_or_create_no_only'] = time.time() - start
# 输出对比结果
self.stdout.write("\n方案 耗时 (秒)")
self.stdout.write("-" * 60)
for name, elapsed in results.items():
self.stdout.write(f"{name:30s}{elapsed:8.4f}")
结果:
方案 耗时 (秒)
------------------------------------------------------------
exists_then_save 0.4612
create_with_exception 0.4909
bulk_create_ignore 0.2935
bulk_create_ignore_one_by_one 0.8326
get_or_create_only 1.3603
get_or_create_no_only 24.4371
方案 耗时 (秒)
------------------------------------------------------------
exists_then_save 0.4629
create_with_exception 0.4745
bulk_create_ignore 0.2848
bulk_create_ignore_one_by_one 0.7976
get_or_create_only 1.0892
get_or_create_no_only 22.4974
exists_then_save 和 get_or_create_only 都有select操作,为什么效率差异这么明显呢?
# exists() 最终语句
SELECT (1) AS "__exists" FROM `iqdii_openacc` WHERE `Mobile` = '13812340000' LIMIT 1;
# only(...).get_or_create() 最终语句
SELECT `id`,`Mobile` FROM `iqdii_openacc` WHERE `Mobile` = '13812340000' LIMIT 1;
因为get_or_create()要返回模型实例,拿到sql返回的数据后 Django 会把这一行数据实例化成一个 AccDetail
对象:分配内存、解析两个字段值、调用 __init__
、关联 _state
等元数据。这些 Python 对象创建和属性赋值的成本,往往比一次轻量的 exists()
查询要高。
exists()只返回一个布尔值,无模型实例化
备注:Django对于插入一条新记录,.create() 和 .save() 方法是等效的,create只是对.save进行了封装,本质都是.save()
脚本需要放在 Django 目录的这个文件夹下 (需要新建management和commands,且都要有__init__.py,__init__.py内容可以为空)
your_project/
└── yourapp/ # 这里的 yourapp 要在 INSTALLED_APPS 里
├── __init__.py
├── models.py
├── views.py
├── management/ # 必须叫 management
│ ├── __init__.py
│ └── commands/ # 必须叫 commands
│ ├── __init__.py
│ └── bench_conflict_existing.py # 你的脚本
└── ..
执行命令:
python manage.py bench_conflict_existing
用这种方法就不需要借助页面,不需要设置urls.py 了,
也不需要django.setup()。
import django
django.setup()
class Command
是 Django 约定俗成的管理命令(management command)入口
当你执行 manage.py bench_conflict_existing 时,Django 会先加载项目的 settings、初始化 ORM、缓存、日志系统等;然后才实例化你的 Command 并调用 handle()。
如果你写成普通脚本,就要自己写 django.setup()、手动处理 sys.path,代码可用性和可维护性都打折。