以「课程章节排序」为例,说明一种无需大规模重写序号的列表排序方案:通过「前驱 ord + 后继 ord」在数轴上插空,用**分数索引(Fractional Indexing)**的思想实现 O(1) 插入与拖拽排序,并给出创建/更新时的业务规则、边界处理以及可复用的业务场景。
前端效果:

若列表顺序用连续整数 1、2、3、4… 表示:
当列表很长、或排序操作频繁时,这种「连续整数序号」方案会带来大量写操作和锁竞争。
不追求序号连续,而是把每一项对应到实数轴上的一个点,顺序即数轴上的先后关系:
ord = (ordA + ordB) / 2。ord = preOrd + 1000。这样:
这就是 Fractional Indexing(分数索引) 的简化实现:用稀疏的、可再分的数值表示顺序,通过「取中间值」在任意两点之间插入,避免全局重排。
ORDER BY ord ASC。ord = (preOrd + nextOrd) / 2(整数除法)。ord = preOrd + ORD_STEP(如 1000)。ord = max(ord) + ORD_STEP;无任何项时 ord = ORD_STEP。ord = 0。步长 ORD_STEP 抽成配置常量(如 1000),既保证新项与已有项不重叠,又为将来在两点之间多次插入留出空间(多次取半后可能需要在某次触发「重算序号」或扩大数值域,这是后续优化点)。
创建时不直接传 ord,而是通过前驱/后继的 ord 表达「插在哪里」,由后端统一计算新 ord。
| 入参 | 含义 | 计算方式 |
|---|---|---|
preOrd + nextOrd 都有 |
插在两者之间 | ord = (preOrd + nextOrd) / 2 |
仅preOrd |
插在前一条后面 | ord = preOrd + ORD_STEP |
| 都不传 | 追加到末尾 | ord = max(当前课程下 ord) + ORD_STEP,空列表时为 ORD_STEP |
业务要点:
courseId、title、可选的 preOrd/nextOrd 等,不暴露「直接写 ord」,避免前端传错导致顺序错乱。更新时既支持「按位置语义」的排序,也支持直接改 ord(如后台批处理)。
| 入参 | 含义 | 计算方式 |
|---|---|---|
preOrd + nextOrd 都有 |
移到两者之间 | ord = (preOrd + nextOrd) / 2 |
仅preOrd |
移到前一条后面 | ord = preOrd + ORD_STEP |
仅nextOrd(preOrd 为空) |
移到第一位(置顶) | ord = 0 |
仅ord |
直接指定序号 | 使用传入的ord |
| 都不传 | 不改顺序 | 不更新 ord 字段 |
与创建的差异:
ord = 0。这样前端把列表最后一条拖到最前时,只需传 nextOrd = 当前第一条的 ord,无需 preOrd。ord,便于管理端或脚本批量调整顺序。max(ord) 无值,则 ord = 0 + ORD_STEP,即第一条为 1000。ord = 0。若原来就有 ord=0 的项,列表里会出现两个 0,顺序按 id 或更新时间等次要字段区分;也可在「仅 nextOrd」时改为 min(ord)-1 或做一次轻量重排,视产品需求而定。(preOrd + nextOrd) / 2 为整数除法。若 preOrd=100, nextOrd=102,得到 101;若 100 与 101 之间再插,得到 100,与 100 重复。此时需要:ord 冲突或间距过小时触发局部或全局重算(例如按当前顺序重新赋 1000、2000、3000…),避免长期取半导致精度用尽。ORD_STEP(如 1000)抽成配置常量,便于:同一套「前驱 ord + 后继 ord + 步长」的思想,可用于所有用户可拖拽排序、且希望避免大规模更新的列表场景,例如:
| 场景 | 说明 |
|---|---|
| 课程章节/目录 | 本文实现:按 preOrd/nextOrd 插入与移动,置顶用 nextOrd 且 ord=0。 |
| 导航/菜单排序 | 菜单项、分类的展示顺序,支持拖拽调整。 |
| 看板列顺序 | 看板(Kanban)各列的顺序,或同一列内卡片的顺序。 |
| 待办/任务列表 | 任务顺序、子任务顺序。 |
| 相册/媒体顺序 | 相册内照片、播放列表内条目。 |
| 表单题目顺序 | 问卷、表单的题目顺序。 |
| 商品/规格顺序 | 商品列表、SKU 或规格的展示顺序。 |
复用要点:
ord(或 sort_order)字段,同一「父」维度内排序(如 courseId、menuId、boardId)。


