在数据库中存储可重新排序的列表


54

我正在开发一个愿望清单系统,该系统中用户可以将商品添加到他们的各种愿望清单中,并且我打算允许用户稍后重新订购这些商品。我不确定如何将其存储在数据库中同时保持快速且不会陷入混乱的最佳方法(此应用程序将由相当大的用户群使用,所以我不希望它崩溃清理东西)。

我最初尝试了一个position列,但是当您移动其他每个项目的位置值时,似乎效率很低。

我见过人们使用自引用来引用上一个(或下一个)值,但是同样,您似乎必须更新列表中的许多其他项。

我见过的另一种解决方案是使用十进制数字,并且仅将项目粘贴在它们之间的间隙中,这似乎是迄今为止最好的解决方案,但我敢肯定必须有更好的方法。

我要说的是,一个典型的列表最多可以包含20个左右的项目,并且我可能会将其限制为50个。重新排序将使用拖放操作,并且可能会分批进行以防止出现竞争情况,例如ajax请求。如果有关系,我正在使用postgres(在heroku上)。

有人有什么想法吗?

为任何帮助加油!


您能否做一些基准测试,然后告诉我们IO或数据库是否会成为瓶颈?
rwong 2013年

有关stackoverflow的相关问题。
Jordão酒店

使用自引用时,将项目从列表中的一个位置移动到另一位置时,您只需更新2个项目。参见en.wikipedia.org/wiki/Linked_list
Pieter B

嗯,不知道为什么链接列表在答案中几乎没有引起注意。
克里斯蒂安·韦斯特贝克

Answers:


32

首先,不要试图对十进制数字做任何聪明的事情,因为它们会讨厌你。 REAL并且DOUBLE PRECISION是不准确的,可能不能正确代表什么,你把他们。 NUMERIC是准确的,但是正确的移动顺序会使您失去准确性,并且实现会严重中断。

将动作限制在单个起伏范围内,使整个操作非常容易。对于顺序编号的项目列表,您可以通过递减项目的位置并增加先前递减的项目的位置编号来向上移动项目。(换句话说,项目5将变成4项目,而原来的项目则4变成项目5,实际上是交换,如Morons在他的回答中所述。)将其向下移动是相反的。通过唯一标识列表和位置的任何内容对表建立索引,您可以UPDATE在事务中使用两个s进行操作,这将非常快速地运行。除非您的用户以超人的速度重新排列他们的列表,否则不会造成很大的负担。

拖动和拖放移动(例如,将项目6项之间坐910)是有点麻烦,并根据各自的新位置是否高于或低于旧的以不同的方式进行。在上面的示例中,您必须通过增加大于的所有位置来打开一个孔9,将item 6的位置更新为新10位置,然后再减小大于所有的位置6以填充空出的位置。使用我之前描述的相同索引,这将很快。通过最小化事务处理涉及的行数,您实际上可以比我描述的要快一些,但这是您不需要微优化的过程,直到您证明存在瓶颈。

无论哪种方式,尝试使用自制的,过于精明的解决方案超越数据库通常都不会成功。精通它们的人已经非常非常快地精心编写了有价值的数据库来执行这些操作。


这正是我在数百万年前的项目投标准备系统中处理它的方式。即使在Access中,更新速度也很快。
HLGEM 2013年

感谢您的爆炸,Bllfl!我确实尝试过使用后一种选择,但是我发现,如果我从列表中间删除项目,则会在职位上留下空白(这是一个很幼稚的实现)。有没有一种简单的方法来避免出现这样的差距,还是在我每次重新订购某些东西时(如果我必须真正地对其进行管理)都必须手动进行?
汤姆·布鲁尼

2
@TomBrunoli:在确定要说之前,我必须考虑一下实现,但是您可以通过触发器自动完成大部分或全部重新编号。例如,如果您删除项目7,触发器将在删除后减少同一列表中编号大于7的所有行。插入将执行相同的操作(插入项目7将使所有第7行或更高行递增)。更新的触发(例如,将第3项在9到10之间移动)会稍微复杂一些,但肯定在可行范围之内。
Blrfl 2013年

我以前从未真正接触过触发器,但这似乎是一种很好的方法。
汤姆·布鲁尼

1
@TomBrunoli:在我看来,使用触发器执行此操作可能会导致级联。具有事务中所有更改的存储过程可能是更好的方法。
Blrfl 2013年

15

来自这里的相同答案https://stackoverflow.com/a/49956113/10608


解决方案:制作index一个字符串(因为字符串本质上具有无限的“任意精度”)。或者,如果您使用整数,请增加index100而不是1。

性能问题是这样的:两个已排序项目之间没有“介于”值。

item      index
-----------------
gizmo     1
              <<------ Oh no! no room between 1 and 2.
                       This requires incrementing _every_ item after it
gadget    2
gear      3
toolkit   4
box       5

而是这样做(以下更好的解决方案):

item      index
-----------------
gizmo     100
              <<------ Sweet :). I can re-order 99 (!) items here
                       without having to change anything else
gadget    200
gear      300
toolkit   400
box       500

更好的是:这是Jira解决此问题的方法。它们的“等级”(您所谓的索引)是一个字符串值,可在排名项目之间留出大量的喘息空间。

这是我使用的jira数据库的真实示例

   id    | jira_rank
---------+------------
 AP-2405 | 0|hzztxk:
 ES-213  | 0|hzztxs:
 AP-2660 | 0|hzztzc:
 AP-2688 | 0|hzztzk:
 AP-2643 | 0|hzztzs:
 AP-2208 | 0|hzztzw:
 AP-2700 | 0|hzztzy:
 AP-2702 | 0|hzztzz:
 AP-2411 | 0|hzztzz:i
 AP-2440 | 0|hzztzz:r

注意这个例子hzztzz:i。串级的好处是,你用完了两个项目之间的房间,你仍然不必重新排序别的。您只需开始在字符串中附加更多字符以缩小焦点。


1
我试图通过仅更新一条记录来提出某种方法,而这个答案很好地说明了我正在思考的解决方案。
NSjonas

13

我见过人们使用自引用来引用上一个(或下一个)值,但是同样,您似乎必须更新列表中的许多其他项。

为什么?假设您采用具有列(listID,itemID,nextItemID)的链表列表方法。

将新项目插入列表需要花费一插入和一修改行。

重新定位项目需要进行三行修改(要移动的项目,之前的项目以及在新位置之前的项目)。

删除一项需要一删除和一修改行。

无论列表中包含10项还是10,000项,这些成本都保持不变。在所有三种情况下,如果目标行是第一个列表项,则修改较少。如果您更经常操作最后一个列表项,则存储prevItemID而不是下一个可能会有所帮助。


10

“但似乎效率很低”

测量了吗?还是只是一个猜测?没有任何证据就不要做这样的假设。

“每个列表20至50个项目”

老实说,这并不是“很多东西”,对我来说听起来很少。

我建议您坚持使用“位置列”方法(如果这对您来说是最简单的实现)。对于如此小的列表,在遇到实际的性能问题之前,不要开始不必要的优化


6

这实际上是规模和用例的问题。

您希望列表中有多少个项目?如果数以百万计,我认为锣的十进制路线很明显。

如果为6,则整数重新编号是显而易见的选择。■还有一个问题是列表或列表如何重新排列。如果您使用向上和向下箭头(一次向上或向下移动一个插槽),则i将使用整数,然后在移动时与上一个(或下一个)交换。

如果用户可以进行250次更改然后一次提交,那么您提交的频率是多少,而不是我说再次重新编号的整数...

tl; dr:需要更多信息。


编辑:“愿望清单”听起来像很多小清单(假设,这可能是错误的。)。所以我说带重新编号的整数。(每个列表包含其自己的位置)


我将在更多上下文中更新问题
Tom Brunoli 2013年

小数点不起作用,因为精度受到限制,每个插入的项都可能需要1位
njzk2,18年

3

如果目标是最大程度地减少每个重新排序操作的数据库操作数:

假如说

  • 所有购物商品都可以用32位整数枚举。
  • 用户的愿望清单有最大大小限制。(我看到一些受欢迎的网站限制使用20至40个项目)

将用户排序的愿望列表存储为一列整数(整数数组)的压缩序列。每次对心愿单进行重新排序时,整个数组(单行;单列)都会更新-这将通过一次SQL更新来执行。

https://www.postgresql.org/docs/current/static/arrays.html


如果目标不同,则坚持“位置栏”方法。


关于“速度”,请确保对存储过程方法进行基准测试。虽然为一个愿望清单随机发布20多个单独的更新可能很慢,但使用存储过程可能有一种快速的方法。


3

好吧,我最近遇到了这个棘手的问题,这篇问答文章中的所有答案都给了我很多启发。以我的看法,每种解决方案都有其优缺点。

  • 如果该position字段必须是连续的且没有间隔,则基本上需要重新排序整个列表。这是O(N)操作。优点是客户端不需要任何特殊逻辑即可获取订单。

  • 如果我们想避免O(N)操作但仍然保持精确的顺序,则方法之一是使用“自引用来引用上一个(或下一个)值”。这是一个教科书链接列表方案。通过设计,它不会产生“列表中的很多其他项”。但是,这需要客户端(Web服务或移动应用程序)实施链接列表遍历逻辑以导出顺序。

  • 一些变化不使用参考,即链表。他们选择将整个订单表示为一个独立的Blob,例如JSON-array-in-a-string [5,2,1,3,...];然后将此类订单存储在单独的位置。此方法还具有要求客户端代码维护该分离的顺序blob的副作用。

  • 在许多情况下,我们实际上并不需要存储确切的顺序,我们只需要在每个记录之间保持相对排名即可。因此,我们可以允许顺序记录之间的间隙。变化包括:(1)使用带空格的整数,例如100、200、300 ...,但是您很快就会用尽间隙,然后需要恢复过程;(2)使用带有自然间隔的十进制,但是您需要确定是否可以承受最终的精度限制;(3)使用此答案中所述的基于字符串的等级,但要小心棘手的实现陷阱

  • 真正的答案可以是“取决于”。重新审查您的业务需求。例如,如果它是一个愿望清单系统,我个人会很乐意使用仅由几个等级组成的系统:“必须”,“必须”,“以后”,然后展示没有特别要求的项目每个等级内的顺序。如果是交付系统,则可以很好地将交付时间用作带有自然差距的粗略排名(并且可以防止自然冲突,因为不会同时发生交付)。你的旅费可能会改变。


2

对位置列使用浮点数。

然后,您可以重新排序列表,仅更改“已移动”行中的位置列。

基本上,如果您的用户希望将“红色”放置在“蓝色”之后但在“黄色”之前

那你只需要计算一下

red.position = ((yellow.position - blue.position) / 2) + blue.position

经过数百万个重新定位后,您可能会得到很小的浮点数,以至于它们之间没有“中间”,但这与看到独角兽的可能性差不多。

您可以使用初始间隔为1000的整数字段来实现此功能。因此,您的初始排序为1000->蓝色,2000->黄色,3000->红色。在将“红色”移动到蓝色之后,您将获得1000->蓝色,1500->红色,2000->黄色。

问题在于,初始差距看似很大,只有1000步,而少了10步,就会使您陷入类似1000-> blue,1001-puce,1004-> biege ......的境地……在“蓝色”之后插入任何内容,而无需重新编号整个列表。使用浮点数,两个位置之间总是会有一个“中途”点。


4
基于浮点数的datbase中的索引和排序要比int 昂贵。整数也是一种很好的序数类型...不需要作为位发送就可以在客户端上进行排序(打印时呈现相同但位数不同的两个数字之间的差)。

但是,任何使用整数的方案都意味着您需要在每次更改顺序时更新列表中的所有/大多数行。使用浮点数,您仅更新已移动的行。同样,“浮点数比整数更昂贵”在很大程度上取决于实现和所使用的硬件。当然,与更新行及其关联索引所需的cpu相比,所涉及的额外cpu无关紧要。
James Anderson

5
对于反对者而言,此解决方案正是Trello(trello.com)所做的。打开您的chrome调试器,并在重新排序之前/之后比较json输出(拖放卡),您会得到- "pos": 1310719, + "pos": 638975.5。公平地说,大多数人不会在其中包含400万个条目的trello列表中使用,但是Trello的列表大小和用例对于用户可分类的内容来说很常见。用户可排序的任何东西几乎都与高性能无关,因此int vs float排序速度是没有意义的,尤其是考虑到数据库主要受IO性能限制。
zelk

1
@PieterB至于“为什么不使用64位整数”,我会说,这对于开发人员来说主要是人体工程学。平均浮点数的<1.0深度和> 1.0的深度差不多。因此,您可以将“ position”列的默认值设置为1.0,然后插入0.5、0.25、0.75,就像加倍一样容易。使用整数时,您的默认值必须为2 ^ 30左右,这使得调试时考虑起来有些棘手。4073741824是否大于496359787?开始计数数字。
zelk

1
此外,如果遇到数字间空间不足的情况,也就不难修复了。移动其中之一。但是重要的是,这以尽力而为的方式工作,可以处理不同方(例如trello)进行的许多同时编辑。您可以将两个数相除,甚至可以撒一些随机噪声,瞧,即使其他人在同一时间做同样的事情,仍然有一个全局指令,并且不需要在事务内部插入来获取那里。
zelk
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.