Django kanban看板大批量任务排序的高效实现方案咨询
Django看板拖拽排序的高效实现方案
全量更新同列所有记录order的做法在单列表量过千后会产生明显的性能瓶颈,以下是经过生产验证的替代方案,绝大多数场景下单次拖拽仅需更新1到数条记录,完全不需要全表操作。
方案1:间隙排序(优先推荐,改动成本最低)
不要用连续整数作为order字段的取值,初始化时给同列任务的order预留足够大的间隔,比如初始值按10000为步长递增:第一个任务order=10000,第二个=20000,以此类推。
当拖拽任务到两个任务中间时,新的order直接取前后两个相邻任务order的平均值即可:
- 拖到列最顶部:新order = 当前列最小order - 10000
- 拖到列最底部:新order = 当前列最大order + 10000
- 拖到A任务和B任务中间:新order = (A.order + B.order) / 2
这种方案99%以上的拖拽操作只需要更新被拖拽的1条任务记录,完全不需要改动其他任何数据。
注意事项
- order字段不要用FloatField,容易出现浮点数精度问题,推荐用
DecimalField(max_digits=30, decimal_places=15),足够支撑单间隙连续十几次插入都不会出现精度不足的问题 - 只有当两个相邻任务的order间隔小到无法再取中间值时,才需要触发一次同列全量order重排,把所有任务的order重新按10000步长打平,这种操作几个月都未必触发一次,完全可以扔到异步任务里后台执行,用户无感知
方案2:局部区间更新(适合必须用连续整数排序的场景)
如果因为业务限制必须用连续整数作为order值,也不需要全量更新同列所有记录,只需要更新拖拽位置覆盖的小区间内的记录即可:
- 同列内拖拽:比如把原位置第5位的任务拖到第2位,只需要把原位置2、3、4的任务order+1,再把被拖拽任务的order设为2即可,区间外的记录完全不用动
- 跨列拖拽:只需要更新目标列中插入位置之后的所有任务order+1,原列中被拖拽任务位置之后的所有任务order-1即可,不需要碰两列里其他位置的记录
可以直接用Django的F表达式在数据库层面完成批量更新,不需要把数据捞到内存,执行效率极高,示例代码:
from django.db.models import F from .models import Task def move_task(task, target_column, before_task=None, after_task=None): """ task: 被拖拽的任务实例 target_column: 拖拽目标列 before_task: 插入位置的后一个任务,插到列顶部时为None after_task: 插入位置的前一个任务,插到列底部时为None """ # 计算新order值和需要批量更新的区间 if after_task is None: # 插顶部 min_order = Task.objects.filter(column=target_column).order_by('order').values_list('order', flat=True).first() new_order = min_order - 1 if min_order is not None else 1 elif before_task is None: # 插底部 max_order = Task.objects.filter(column=target_column).order_by('-order').values_list('order', flat=True).first() new_order = max_order + 1 if max_order is not None else 1 else: new_order = before_task.order if task.column == target_column: # 同列移动 if task.order < new_order: # 向下拖,区间内的任务order-1 Task.objects.filter( column=target_column, order__gt=task.order, order__lte=new_order ).update(order=F('order') - 1) else: # 向上拖,区间内的任务order+1 Task.objects.filter( column=target_column, order__gte=new_order, order__lt=task.order ).update(order=F('order') + 1) else: # 跨列移动,目标列插入位置之后的任务order+1 Task.objects.filter( column=target_column, order__gte=new_order ).update(order=F('order') + 1) # 原列被拖任务之后的任务order-1 Task.objects.filter( column=task.column, order__gt=task.order ).update(order=F('order') - 1) # 更新被拖拽任务本身 task.column = target_column task.order = new_order task.save(update_fields=['column', 'order'])
这种方案单次拖拽最多也就更新几十条记录,哪怕单列表量过万也不会有性能问题。
基础优化项
- 给
column和order字段加联合索引,避免排序、过滤时全表扫描,模型配置参考:class Task(models.Model): # 其他字段... column = models.ForeignKey(Column, on_delete=models.CASCADE) order = models.DecimalField(max_digits=30, decimal_places=15) class Meta: indexes = [ models.Index(fields=['column', 'order']), ] - 所有批量更新操作直接用ORM的
update()方法配合F表达式,不要循环save单条记录,减少数据库交互次数 - 如果确实需要触发全列重排,不要放在同步接口逻辑里,扔到异步任务队列后台执行即可
内容的提问来源于stack exchange,提问作者Jon
相关产品推荐
相关产品推荐

