【问题标题】:Data Structure for storing a sorting field to efficiently allow modifications用于存储排序字段以有效地允许修改的数据结构
【发布时间】:2010-12-11 01:43:14
【问题描述】:

我正在使用 Django 和 PostgreSQL,但如果有更好的方法来使用原始 SQL 或特定于数据库的操作,我并不完全依赖 Django ORM。

我有一个需要顺序排序的模型。查找操作通常会按顺序检索整个列表。对此数据最常见的操作是将一行移动到列表的底部,中间项目的子集冒泡以替换前一个项目,如下所示:

(对 A 的操作,带有子集 B、C、E) A -> B B -> C C -> E D -> D E -> A 注意 D 是如何不动的。

一般来说,项目的子集不会超过大约 50 个项目,但基本列表可能会增长到数万个条目。

最明显的实现方式是使用简单的整数顺序字段。这似乎不是最理想的。它需要使位置排序列不唯一的折衷方案,其中仅在修改操作期间才需要非唯一性。要看到这一点,想象一下使用 A 和子集 B 的最小操作:

oldpos = B.pos
B.pos = A.pos
A.pos = oldpos

即使您已经存储了位置,在第二行您还是违反了唯一性约束。此外,这种方法会导致原子性问题 - 您的读取操作必须在写入之前发生,在此期间您的记录可能会更改。 Django 的默认事务处理文档没有解决这个问题,尽管我知道在 SQL 中使用“REPEATABLE READ”级别的事务锁定应该是可能的。

我正在寻找更适合这种使用模式的替代数据结构。我查看了this question 的想法。

一个建议是杜威十进制样式的解决方案,它使插入操作在现有值之间以数字方式发生,因此在 B 和 C 之间插入 A 导致:

A=1 -> B=2 B=2 -> A=2.5 C=3 -> C=3

这解决了列唯一性问题,但引入了列必须是指定小数位数的浮点数的问题。要么我高估了,存储的数据比我需要的多,要么系统受到我施加的任意十进制长度的限制。此外,我不希望使用甚至超过数据库 - 一些键将比其他键更频繁地移动,从而使该解决方案更快地达到极限。我可以通过定期重新编号数据库来解决这个问题,但似乎一个好的数据结构应该避免需要这个。

我考虑过的另一种结构是链表(和变体)。这具有使修改简单的优点,但我不确定它相对于 SQL 的属性 - 在 SQL 查询中排序这样的列表似乎很痛苦,并且提取列表的非顺序子集很糟糕检索属性。

除此之外,还有B-Tree、各种二叉树等等。你对这个数据结构有什么建议? SQL 中是否有此解决方案的标准数据结构?使用顺序整数的最初想法真的会有缩放问题,还是我看到没有问题?

【问题讨论】:

  • 由于答案数量少,在这里悬赏......
  • 嗨,保罗 - 我看到你接受了我的回答 - 谢谢:D。您决定采用哪种建议的解决方案,为什么?

标签: python database django sorting data-structures


【解决方案1】:

首选解决方案:

linked list 是实现此目的的常用方法。按顺序返回项目的查询是trivial in Oracle,但我不确定您将如何在 PostreSQL 中执行此操作。

另一种选择是使用 ltree module for postgresql. 实现此功能

不太优雅(且写入繁重)的解决方案: 开始交易。行级锁范围内的“选择更新”。将目标记录移动到位置 0,将目标未来的后续记录更新为 +1,其中它们的位置高于目标原始位置(反之亦然),然后将目标更新到新位置 - 一次额外的写入不需要唯一的约束。提交:D

如果您可以等待 Postgresql 8.5(Alpha 可用),那么简单(但仍然需要大量写入)解决方案:)

将其包装在事务中,在范围内选择更新,并使用延迟约束(postgresql 8.5 has support for deferred unique constraints,如 Oracle)。

【讨论】:

  • postgres 中的 ltree 模块是一个有趣的建议。我去看看。
  • 另外有趣的是 ltree 支持开箱即用的 b-tree 索引。
  • 锁定整个表是非常不可取的,因为系统旨在支持许多同时更新。
  • 更新解决方案以移除全表锁并实施行级锁。如果您可以等待 8.5,还添加了潜在的解决方案。给我你的赏金该死的;)
  • 随着赏金的开放,答案不断改进!延迟约束的想法很好。
【解决方案2】:

临时表和事务应保持原子性和排序顺序的唯一约束。重申问题,你想从:

A  10   to  B  10
B  25       C  25
C  26       E  26
E  34       A  34

每行之间可以有任意数量的项目。因此,首先您阅读记录并创建一个列表[['A',10],['B',25],['C',26],['E',34]]。通过一些 Pythonic 魔法,您可以移动标识符并将它们插入到临时表中:

create temporary table reorder (
    id varchar(20), -- whatever
    sort_order number,
    primary key (id));

现在更新:

update table XYZ
set sort_order = (select sort_order from reorder where xyz.id = reorder.id)
where id in (select id from reorder)

我只是假设 pgsql 可以处理该查询。如果可以,它将是原子的。

(可选)将表 REORDER 创建为永久表,并且事务将确保对同一记录重新排序两次的尝试将被序列化。


编辑:存在一些交易问题。你可能需要实现我的两个想法。如果两个进程都想更新项目 B(例如),则可能会出现问题。因此,假设所有订单值都是偶数:

  1. 开始交易
  2. 将所有正在使用的订单递增 1。这会将行级写锁放在要更新的所有行上。
  3. 选择您刚刚更新的数据,如果有任何sort_order 字段,甚至某些其他进程已经添加了符合您条件的记录。您可以中止事务并重新启动,也可以只删除记录并仅使用在第 2 步中更新的记录完成操作。“正确”的做法取决于您需要此代码完成什么。
  4. 使用正确的偶数排序顺序填写您的临时重新排序表。
  5. 如上更新主表。
  6. 删除临时表。
  7. 提交事务

第 2 步确保如果两个列表重叠,则只有第一个列表可以访问该行 在交易完成之前有问题:

update XYZ set sort_order = sort_order + 1
where -- whatever your select criteria are

select * from XYZ
where -- same select criteria
order by sort_order

或者,您可以在表格中添加一个控制字段以获得相同的效果,然后您就不需要使用sort_order 字段。使用sort_order 字段的好处是通过BIT 字段或LOCK_BY_USERID 字段进行索引,当字段通常为空时往往性能较差,因为99% 的时间索引是无意义的。 SQL 引擎不喜欢大部分时间都为空的索引。

【讨论】:

    【解决方案3】:

    您可以通过将 order 列作为一个始终为偶数的整数来解决重新编号问题。移动数据时,将 order 字段更改为新的排序值 + 1,然后快速更新以将所有奇数 order 字段转换为偶数:

    update table set sort_order = bitand(sort_order, '0xFFFFFFFE')
    where sort_order <> bitand(sort_order, '0xFFFFFFFE')
    

    因此您可以保持 sort_order 的唯一性作为约束

    编辑:好的,再次查看问题,我已经开始了一个新的答案。

    【讨论】:

    • 这是一个非常可行的解决方案。关于这个两遍偶数/奇数过程的性能与仅允许字段不唯一并在事务期间锁定行的性能有什么关系?
    • 变量太多:DBMS、索引类型、表中的行数、修改行的百分比、同一事务中的其他更新等。您需要使用良好的示例数据对其进行分析。最重要的一步是拥有一个可以在不进行表扫描的情况下进行更新的 DBMS。当您将函数应用于索引列时,某些 DBMS 很难使用索引。
    • 首先,此解决方案没有考虑将项目从其旧位置移动所造成的间隙。其次,任何使用简单排序列的解决方案都将导致在重新排序时多次写入。使用这种两遍机制,您将始终拥有至少等于您范围内记录数的写入次数,以及修改这些记录的索引,这肯定会影响数据库性能最后,您仍然将需要锁定表以使操作原子化 - 与您的原始解决方案相比没有任何好处。
    • 如果您主要关心的是您的独特约束,您总是可以在重新定位目标之前创建空间。我已经更新了我的答案来解释这个解决方案。
    • 马特,不需要任何间隙。在示例中,选择了 4 个条目,它们的排序顺序向上旋转一个位置,顶部条目成为最后一个条目。诀窍是同时更新 4 个排序顺序字段。有关执行此操作的方法,请参阅我对临时表的第二个答案。
    【解决方案4】:

    为什么不做一些长度的简单字符字段,比如最初最多 16 个(或 255 个)。

    首先从 aaa 到 zzz 标记事物(应该是 17576 个条目)。 (您还可以添加 0-9,以及大写字母和符号以进行优化。)

    随着项目的添加,它们可以到达最后,直到您允许额外的“结束时间”(zzza、zzzaa、zzzaaa、zzzaab、zzzaac、zzzaad 等)

    这应该是相当简单的编程,它与杜威十进制系统非常相似。

    是的,您偶尔需要重新平衡它,但这应该是一个简单的操作。最简单的方法是两遍,第一遍是将新的排序标签设置为'0'(或任何早于第一个字符的字符),然后是适当长度的新标签,步骤2是删除' 0 从前面。

    显然,您可以对浮点数做同样的事情,并定期重新平衡它,这只是一个变体。一个优点是大多数数据库将允许您为字符字段设置一个非常大的最大大小,大到足以使您非常、非常、非常不可能用完数字来进行排序,并且也不太可能您将不得不修改架构,而不会浪费大量空间。

    【讨论】:

      【解决方案5】:

      在我看来,您真正的问题是需要在事务期间锁定表。我没有立即看到在单个操作中解决此问题的好方法,因此需要锁定。

      所以问题是您是否可以以“Django 方式”而不是直接使用 SQL 来执行此操作。搜索“django lock table”发现了一些有趣的链接,包括this snippet,还有许多其他实现类似行为的链接。

      可以在stack overflow post 中找到一个直接的 SQL 链表样式的解决方案,对我来说它看起来合乎逻辑且简洁,但同样是两个操作。

      我很想知道结果如何以及您的最终解决方案是什么,请务必让我们更新!

      【讨论】:

      • 该帖子上接受的答案或多或少是我最初提出的。我真的不认为这是链表概念的实现。我同意锁定表是我的问题的关键部分,但我仍然对更好的数据结构也很感兴趣,因为不知道平面编号是否可以很好地扩展。
      • 适当的锁定级别是“可重复读取”,这可以防止检索到的数据在事务期间被修改,而不锁定表的其余部分。
      • "过早的优化是万恶之源!" ;) 听起来您已经想到了一个上限,为什么不使用 50,000 个条目测试固定数字方法,看看它是如何扩展的?这将有助于您做出决定,因为我确信实现数据结构会带来成本/收益权衡。
      • PostgreSQL 文档说它只提供 2 级实际隔离,因此可序列化似乎是唯一的选择:postgresql.org/docs/current/static/transaction-iso.html
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-26
      • 1970-01-01
      • 1970-01-01
      • 2016-11-25
      • 1970-01-01
      相关资源
      最近更新 更多