后端大佬请进!帮我看看这个排序方法可以行得通吗?

2022-03-18 16:41:54 +08:00
 userKamtao

兄弟萌!我这个排序方法可以行得通吗?

最近接到一个需求,技术栈 go+mysql 要给做个列表排序,一开始觉得很简单,直接加个排序号字段就 ORDER BY 好了 但是做一半我才发现,如果 100W 条数据 如果最后一条记录要排到第一位 排序号岂不是要整体记录都更新一次。

于是想了这个方案。 如图

5560 次点击
所在节点    程序员
58 条回复
aphorism
2022-03-18 22:06:57 +08:00
如果应用场景就是会有大量不可预测位置的插入,而且需要保持序列,解决方案就是在排序字段上不要使用 Numeric order 而使用 lexical order ,但这样也需要该排序字段允许足够的长度来容纳大量的数据插入。使用浮点型作为排序字段是并不可靠的。
documentzhangx66
2022-03-19 00:01:04 +08:00
对于静态数组:
22 、23 、24 、25 、26 、-1 、-1

value 大概率是保存在连续的内存空间。此时插入一个 21 并保持排序,那么这 5 个 value 在内存里的位置,就需要整体后移。

但有一种东西,叫链表,他们在内存里,大概率是这种样子:

HeadNodeID:3 。

NodeID:1 ,Value:25 ,NextNodeID: 5
NodeID:2 ,Value:24 ,NextNodeID: 1
NodeID:3 ,Value:22 ,NextNodeID: 4
NodeID:4 ,Value:23 ,NextNodeID: 2
NodeID:5 ,Value:26 ,NextNodeID: -1

看着比较复杂,如果按 Value 排序,上图就变为:

HeadNodeID:3 。

NodeID:3 ,Value:22 ,NextNodeID: 4
NodeID:4 ,Value:23 ,NextNodeID: 2
NodeID:2 ,Value:24 ,NextNodeID: 1
NodeID:1 ,Value:25 ,NextNodeID: 5
NodeID:5 ,Value:26 ,NextNodeID: -1


此时,插入 21 ,并且保持排序,就只需要:

增加一个 Node:
NodeID:6 ,Value:21 ,NextNodeID: 3

然后把 HeadNodeID ,从 3 改为 6 就行。

数据库内的表的数据结构,大部分是基于 链表与静态数组 的高级复合型数据结构,并不是单纯的静态数组,所以你不需要过分担心这个问题,只需要设计好表结构与索引,然后正常使用就好,数据库不会傻到你加个值,重排序就把所有数据往后移。真实情况是,就像前面的例子,数据库只是加了个链表节点,然后修改了一下其他少数几个链表的指针,以及头结点,以及相关索引等。
jones2000
2022-03-19 00:36:37 +08:00
1. 目前的数据量使用 mysql 是否可以满足需求。
2. 目前程序瓶颈在哪里,是否可以通过升级数据库配置来解决。

能用硬件解决的问题, 就花钱解决, 买硬件比花钱找人优化软件靠谱。 没必要不要自己写排序。
yolee599
2022-03-19 01:02:07 +08:00
骚操作:用数据库实现双向链表
yolee599
2022-03-19 01:03:14 +08:00
如果需要快速定位再加一个跳表作为索引
GeruzoniAnsasu
2022-03-19 02:48:25 +08:00
@xylophone21
@zmxnv123
@documentzhangx66

链表存的话读取会非常慢,不能范围预加载,OFFSET 更是灾难,因为现在没有记录告诉你 第二页第一行对应 ID 是几的结果行,读一页必须找到页头开始的行然后沿着它的 next 遍历到页尾,这件事还不能用 SQL 解决,得在代码里循环,数据库负担就很重了


不过,可以在这个基础上往外加上「桶」来映射排好序的样子可以解决。桶始终保持有序,如果你愿意也可以保持它的 capacity 平均,这样分页 offset 的时候估算会快(准)一些。然后读出的时候只需要检索桶 ID 符合的记录就可以了,与平时基本无异。

当需要分页和 offset 的时候,先提取桶表的 capacity 列来估计,比如一个桶容量是 100 ,我要 OFFSET 到 1000 ,那就先把前 10 个桶的 capacity 全读出来看够不够,当桶容量总体比较平均的时候这个估计是很近似的,要检索的量也很少。然后还是 where bucket_id in []就行

然后桶还可以嵌套,更小粒度的桶可以被大桶索引,而且对更下层的桶来说都无需改动原有的查询流程,迁移负担很小








—— 没错 恭喜我们又重新发明了 B+树
documentzhangx66
2022-03-19 02:59:07 +08:00
@GeruzoniAnsasu 我写的最后一段,文字太长,没排好版,导致你没注意看清楚...
GeruzoniAnsasu
2022-03-19 03:19:50 +08:00
@documentzhangx66 你再仔细想想,「重排序」的时候你想喂给数据库的是什么?
documentzhangx66
2022-03-19 04:56:04 +08:00
@GeruzoniAnsasu

我 27 楼评论的意思是,你在 26 楼评论我之前,需要认真看看我在 22 楼的评论的最后一段文字,因为那段文字里,有这样的一句话:“基于 链表与静态数组 的高级复合型数据结构”。这句话其实涵盖了几乎所有的数据结构,包括你在 26 楼写的结构。

另外重排序,并没有你在 28 楼里写的喂给数据库的这种说法。现代数据库的结构,也并不是你在 26 楼说的这么简单,其存储结构、内存结构、专用的排序区域与策略等等,比教科书中纯粹的 B+树要复杂很多。教科书为了教学,只会教最简单的东西。
yzbythesea
2022-03-19 05:03:00 +08:00
@documentzhangx66

mysql 是 BTree ,链表或者数组的插入都是 O(n)
GeruzoniAnsasu
2022-03-19 05:12:51 +08:00
@documentzhangx66
我是想说无论数据库怎样实现它的索引,在这个业务里并不能用得上。
SQL 并没有移动行然后自动给你更新所有自增列的值这种内建操作。

OP 描述的困境是,用于排序的索引值是稠密无缝的(如果 int 的话)。如果索引是随机访问地址,那么改变顺序就要批量更新大量地址,如果索引是相对地址(即链表),那没有任何现存机制帮助我们随机提取一批相邻索引的行。


数据库自己的数据结构 works perfect so what?
how does it help?
documentzhangx66
2022-03-19 05:19:31 +08:00
@yzbythesea

1.我写的是:基于 链表与静态数组 的高级复合型数据结构,不是 单纯的 链表或静态数组。这种高级复合型数据结构,几乎可以组成所有的数据结构,包括各种树、森林、网、环等等。你说的 BTree 已经包含在我说的里面。BTree 这种说法不严谨,准确来说是 B-Tree 或 B+Tree 。

2. Mysql 主流引擎,使用的结构有好几种,从 B-Tree 、B+Tree 、R-Tree 、Hash 结构,都有;而且 B-Tree 与 B+Tree 还存在争议。
documentzhangx66
2022-03-19 05:29:19 +08:00
@GeruzoniAnsasu

1.我在 22 楼写的那玩意,是在告诉楼主,静态数组 与 链表,在操作上的一些差异。

我并不是说,SQL 或 数据库引擎,就是这么运行的。

所以我在 22 楼结尾加了那段很长的文字,是在告诉楼主,现代数据库,比较复杂,让他别担心这个问题。


2.如果楼主关心的不是这事,而是关心数据库主流 B+Tree 结构,是怎么插入一个中间数值的,那么楼主可以去搜 B+Tree 的算法动画,看看分步演示。


3.数据库自己的数据结构,但并不一定 works perfect 。因为数据库并不知道用户的需求。所以近代数据库才有各种细分,比如关系型、NOSQL 、图数据库,等等。而且各种数据库懂优化的 DBA 贼贵。
GeruzoniAnsasu
2022-03-19 05:38:49 +08:00
我们往表里放了一个链表,现在插入和移动都变得很快了,这很好——

但我怎么知道第四个元素应该是 ID 为几的行? 看起来 ID=4 被 ID=5 指着,然后 ID=4 自己指着 ID=3…… 那,id=3/4/5 这三行到底是第几个元素?没有映射,他们的 ID 与 next/prev 都不直接反应呈现顺序。当只有一个表的时候这做不到。

所以需要第二个映射表。

第四个元素,按桶大小为 3 估计(我给的例子),应该在第二桶,那么我们
查 SUM(桶容量) WHERE ID <2 ,得到 3 ,
再查 capacity WHERE id=2 ,为 3 ,说明确实在第二个桶里。那么
直接查第一个表 WHERE bucket_id = 2 ,提取出 3 行,看 2 号桶的头,是 id=2 。遍历内存里的三个元素找到 id=2 然后沿着 next 遍历(4-3)-1=0 次,得到 id=2 的元素。
总计 3 次查询完成了随机检索,一次桶表扫描,一次桶表索引,一次数据表索引,内存遍历忽略不计
GeruzoniAnsasu
2022-03-19 05:45:29 +08:00
@documentzhangx66
你甚至不知道 OP 遇到了个什么问题……

他之前的做法可以 SELECT * ORDER BY pos ,pos 是个浮点,他可以在任意两个 pos 之间找一个数作为新 pos 。但这也许会有问题,所以他想问有没有不用浮点数的方案。

那如果不用浮点的话 没法指定 pos=1 和 pos=2 之间的新数了,想把 pos=100w 移到 1 和 2 中间让它变成 2 ,其它的顺次后移,怎么办? id=100w 的那行,pos 改成什么? pos=2 的那行 pos 改成什么?其它的呢?

好,如果不用这种 pos=整数的表示方法,我们用一个链表 ——



回到我的回复开头
documentzhangx66
2022-03-19 06:01:40 +08:00
@GeruzoniAnsasu

你没理解我的意思。

我让他直接操作就好,不用管这个问题。
GeruzoniAnsasu
2022-03-19 06:04:40 +08:00
@documentzhangx66 我在#28 是让你想清楚「直接操作」怎么操作,操作什么?是指真的去更新 100w 行吗?
yzbythesea
2022-03-19 06:07:16 +08:00
@documentzhangx66

链表,数组和树是完全不同的数据结构,怎么能互相“基于”?如果不懂的话,可以先去看下。
documentzhangx66
2022-03-19 06:13:42 +08:00
@GeruzoniAnsasu

1.直接操作具体是指怎么操作,需要楼主先讲清楚他的细节。包括设计细节与代码细节。

2.但无论怎么操作,只要他是正经的设计,不是乱来的设计,那么这个问题,原则上就是我在 22 楼给他科普的那些东西,不需要更新所有 100w 行。

3.我一直觉得,楼主是不是在设计上,犯了强制给表加一个 int 自增列的陋习,才带来这个额外的麻烦。1 楼也是看不懂楼主的操作,才提问。楼主在 4 楼,确认了 3 楼的回答正确;但 3 楼发言正确的前提,是建立在赞成楼主的设计。我觉得楼主的设计不一定正确,所以和 1 楼产生了同样的疑问。
documentzhangx66
2022-03-19 06:23:13 +08:00
@yzbythesea

你有这方面的误解,并不一定是你的问题。

国内的教课书与各种中文资料,比较古老与死板,它可能认为 链表、单链表、双链表,甚至动态数组、树、环,等等,都是不同的东西。

但如果你能够多翻翻国外的现代书籍,多翻翻英文版维基百科,多动手写写这些东西,自己多实现几遍,你会发现现代的理念有些不一样:

静态数组 与 链表,是组成高级数据结构的基石。

比如你觉得树和链表,不一样,但你想想,简单的树,是不是由链表衍生而来的?简单的环是不是链表首位相连?一些高级的树,是不是 链表上加了一些链表与静态数组?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/841306

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX