【问题标题】:Storing item positions (for ordering) in a database efficiently有效地将项目位置(用于订购)存储在数据库中
【发布时间】:2012-06-21 02:25:32
【问题描述】:

场景:

有一个用户拥有的电影数据库,电影显示在一个名为“我的电影”的页面上,电影可以按照用户想要的顺序显示。例如位置#1 的“搏击俱乐部”,位置#3 的“Drive”等等。

显而易见的解决方案是为每个项目存储一个位置,例如:

电影ID、用户ID、位置
1 | 1 | 1
2 | 1 | 2
3 | 1 | 3

那么在输出数据的时候是按位置排序的。此方法适用于输出,但是在更新时存在问题:项目的位置所有其他位置都需要更新,因为位置是相对的。如果电影 #3 现在位于位置 2,则现在需要将电影 #3 更新到位置 #2。如果数据库包含 10,000 部电影,并且电影从位置 #1 移动到位置 #9999,那么几乎有 10,000 行需要更新!

我唯一的解决方案是单独存储定位,而不是为每个项目位置设置一个单独的字段,它只是在运行时获取并与每个项目(json、xml 等)相关联的位置的一个大数据转储,但是感觉...效率低下,因为不能让数据库进行排序。

我总结的问题:在便于获取和更新的列表中存储项目位置的最有效方法是什么?

【问题讨论】:

    标签: sql database theory


    【解决方案1】:

    如果您结合使用用户将电影放置在给定位置的位置和时间戳,而不是尝试保持实际位置,那么您可以实现一种相当简单的方法来选择和更新数据。例如;一组基本数据:

    create table usermovies (userid int, movieid int, position int, positionsetdatetime datetime)
    
    insert into usermovies (userid, movieid, position, positionsetdatetime)
    values (123, 99, 1, getutcdate())
    insert into usermovies (userid, movieid, position, positionsetdatetime)
    values (123, 98, 2, getutcdate())
    insert into usermovies (userid, movieid, position, positionsetdatetime)
    values (123, 97, 3, getutcdate())
    insert into usermovies (userid, movieid, position, positionsetdatetime)
    values (123, 96, 4, getutcdate())
    insert into usermovies (userid, movieid, position, positionsetdatetime)
    values (123, 95, 5, getutcdate())
    insert into usermovies (userid, movieid, position, positionsetdatetime)
    values (123, 94, 6, getutcdate())
    
    insert into usermovies (userid, movieid, position, positionsetdatetime)
    values (987, 99, 1, getutcdate())
    insert into usermovies (userid, movieid, position, positionsetdatetime)
    values (987, 98, 2, getutcdate())
    insert into usermovies (userid, movieid, position, positionsetdatetime)
    values (987, 97, 3, getutcdate())
    insert into usermovies (userid, movieid, position, positionsetdatetime)
    values (987, 96, 4, getutcdate())
    insert into usermovies (userid, movieid, position, positionsetdatetime)
    values (987, 95, 5, getutcdate())
    insert into usermovies (userid, movieid, position, positionsetdatetime)
    values (987, 94, 6, getutcdate())
    

    如果您使用这样的查询来查询用户的电影:

    ;with usermovieswithrank as (
      select userid
      , movieid 
      , dense_rank() over (partition by userid order by position asc, positionsetdatetime desc) as movierank
      from usermovies
    )
    select * from usermovieswithrank where userid=123 order by userid, movierank asc
    

    然后你会得到预期的结果:

    USERID  MOVIEID     MOVIERANK
    123     99          1
    123     98          2
    123     97          3
    123     96          4
    123     95          5
    123     94          6
    

    要移动某部电影的排名,我们需要更新 position 和 positionsetdatetime 列。例如,如果用户 ID 123 将电影 95 从排名 5 移动到排名 2,那么我们这样做:

    update usermovies set position=2, positionsetdatetime=getutcdate() 
    where userid=123 and movieid=95 
    

    这会导致(在更新后使用上面的 SELECT 查询):

    USERID  MOVIEID     MOVIERANK
    123     99          1
    123     95          2
    123     98          3
    123     97          4
    123     96          5
    123     94          6
    

    那么如果用户 ID 123 将电影 96 移动到排名 1:

    update usermovies set position=1, positionsetdatetime=getutcdate()
    where userid=123 and movieid=96 
    

    我们得到:

    USERID  MOVIEID     MOVIERANK
    123     96          1
    123     99          2
    123     95          3
    123     98          4
    123     97          5
    123     94          6
    

    当然,您最终会在 usermovies 表中得到重复的位置列值,但是使用此方法您将永远不会显示该列,您只需将其与 positionsetdatetime 一起使用来确定每个用户的排序排名和排名你确定的是真实的位置。

    如果您希望位置列在不参考 positionsetdatetime 的情况下正确反映电影排名,您可以使用上面选择查询中的 movierank 更新 usermovies 位置列值,因为它实际上不会影响确定的电影排名。

    【讨论】:

    • 刚刚注意到这个问题已经存在一年了 - 哎呀!没关系,也许我的建议对某人有帮助:-)
    • 如果用户在列表中向下 拍摄电影,这将不起作用。例如,如果他们将电影 98 从位置 4 移动到位置 6,则会有两部电影的位置为 6,但电影 98 将首先显示(在位置 5),因为它的位置集日期时间较新。
    • @bergie3000 你是对的——我很抱歉我错过了!我怀疑它可以通过在向下移动时将所需位置加 1 来轻松解决;所以在你的例子中,我认为将电影 98 从位置 4 设置到位置 7(即所需的位置 6,加 1)会做到吗?
    • 不,忘记那个建议;它需要更多的关注。我会看看我能想出什么!
    • @Elliveny 差不多 4 年后,你有什么想法吗?无论如何都是很好的答案:)
    【解决方案2】:

    我一直在努力解决如何最好地处理这种情况,并意识到 BY FAR 最好的解决方案是按照您想要的顺序排列电影列表/数组,例如;

    用户 ID,电影订单

    1 : [4,3,9,1...]

    显然你会序列化你的数组。

    '感觉……效率低下'?

    假设用户有一个包含 100 部电影的列表。按位置搜索将是一个数据库查询,一个字符串到数组的转换,然后是 moviesOrder[index]。可能比直接查找数据库慢,但仍然非常非常快。

    OTOH,考虑一下你是否改变订单;

    与数组拼接相比,存储在数据库中的位置最多需要 100 行更改。链表的想法很有趣,但并不能像展示的那样工作,如果单个元素失败,会破坏一切,而且看起来也慢得多。其他一些想法,比如留出空隙,使用浮点数虽然很乱,但在某些时候很容易失败,除非你 GC。

    似乎应该有更好的方法在 SQL 中执行此操作,但实际上没有。

    【讨论】:

    • 我喜欢这个,因为如果你仔细想想,一个孩子的订单是属于父母的。在真空中,一行具有“5”的“顺序”属性意味着什么?您必须查看所有其他行才能表示任何含义。
    • 我想我自己也更喜欢这个......如果电影也被删除,我们需要确保我们更新它
    • 唯一的问题是一个带有 order by 的查询......我们必须在查询后对数据进行排序......(除非有一些花哨的 sql 可以拆分字符串并完成所有这些)
    【解决方案3】:

    存储订单链表样式。不是保存绝对位置,而是保存前一项的ID。这样,任何更改只需要您更新两行。

    movieid | userid  | previousid
       1    |    1    | 
       2    |    1    |    1
       3    |    1    |    4
       4    |    1    |    2
    

    为了让电影有序...

    SELECT movieid WHERE userid = 1 ORDER BY previousid
    
    -> 1, 2, 4, 3
    

    要(比如说)将#4 向上移动一个空格:

    DECLARE @previousid int, @currentid int
    SET @previousid = SELECT previousid FROM movies WHERE movieid = @currentid
    
    -- current movie's previous becomes its preceding's preceding
    UPDATE movies SET previousid = 
        (SELECT previousid FROM movies WHERE movieid = @previousid)
    WHERE movieid = @currentid
    
    -- the preceding movie's previous becomes the current one's previous
    UPDATE movies SET previousid = @currentid WHERE movieid = @previousid
    

    这仍然是 1 次读取 + 2 次写入,但超过了 10,000 次写入。

    【讨论】:

    • 以及列出电影的 SQL 查询是什么?
    • @bjan 选择应该相当简单...更新有点棘手,但我认为这可行。
    • 根据我的测试,它导致重复的previousid !!
    • 按顺序获取电影的查询不太有效。考虑:(id,prev):(1,2),(2,3),(3,_)。该查询将返回 3、1、2。它应该是 3、2、1。给定您的架构,在纯 SQL 中执行此查询似乎没有一种很好的(非递归、一次扫描)方法。如果你改为 SELECT movieid, previousid WHERE userid = 1,那么用任何其他编程语言对它们进行排序都是微不足道的。
    • @McGarnagle UPDATE 看起来很简单,但没有简单的方法来选择
    【解决方案4】:
    ID   NAME  POSITION
    7     A       1
    9     B       2
    13    C       3
    15    D       4
    21    F       5
    

    给定当前场景,如果我们想将项目 D 移动到位置 2,我们可以搜索 2(我们要移动项目的位置)和 4(项目的当前位置)之间的间隔,然后将查询写入 ADD + 1 到此区间内每个元素的位置,因此在这种情况下,我们可以执行以下步骤:

    1. 在 position >= 2 AND position
    2. 将项目 D 位置设置为 2。

    这将产生知识: A->1, B->3, C-> 4, D->2, F->5

    如果我们想将 B 移动到 D,那么我们需要做相反的事情并应用 -1。

    1. 在 position > 2 AND position
    2. 将项目位置设置为 4

    当从表中删除一个项目时,我们需要更新其位置大于被删除元素位置的每个项目。

    并且在创建 Item 时,它的位置等于每个 item 的 COUNT +1。

    免责声明:如果您的金额非常大,那么此解决方案可能不是您想要的,但在大多数情况下都可以。通常,用户不会将项目从位置 10000 移动到位置 2,但如果用户删除项目 1,则查询会将 -1 减去剩余的 9999 个项目。如果这是您的情况,那么使用链表的解决方案可能是最适合您的解决方案,但是订购会更具挑战性,因为您需要逐项查看列表中的下一个。

    示例查询

    -- MOVE DOWN
    UPDATE movie SET position = position-1  WHERE position <= 18 AND position > 13 AND id > 0;
    UPDATE movie SET position = 18 WHERE id = 130;
    
    -- MOVE UP
    UPDATE movie SET position = position+1  WHERE position < 18 AND position >= 13 AND id > 0;
    UPDATE movie SET position = 13 WHERE id = 130;
    

    【讨论】:

      【解决方案5】:

      这里真的很有趣的解决方案。另一种可能性是用一些空间存储位置,比如 10 或 100 的倍数。

      ID   NAME  POSITION
      7     A       100
      9     B       200
      13    C       300
      15    D       400
      21    F       500
      

      这个 100 的倍数可以为每个新添加完成。 然后将行 C 移动到位置 1,将是当前值 -1 或当前值之后的 +1。甚至 -50,以便将来可以实现。

      ID   NAME  POSITION
      7     A       100
      9     B       200
      13    C       50
      15    D       400
      21    F       500
      

      这可以继续,如果移动太多而无法进行,则再次对所有行进行重新排序。

      【讨论】:

      • 我看到 Atlassian 在 Jira 中使用的另一个类似答案是,它们使用字母而不是数字进行字典排序......并且通过在特定位置添加前缀或附加字符很容易改变顺序。如果我们谷歌,会有更多信息。
      • 请注意,更有效的方法是从 0 开始并使用 2 的幂增量,如 128 或 1024。这样您就可以在不重新编号的情况下最大化更新计数,因为您总是在现有值。这对于所有用户订购场景应该足够了,因为给定 int 达到 ~2G,在订单值溢出之前按 1024 订购会给你 ~2M 项目。这远远大于用户可管理的数量(可能是数千?)。如果将某些东西移到顶部,也可以使用负值。
      猜你喜欢
      • 2011-03-24
      • 2018-10-13
      • 1970-01-01
      • 2010-09-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-10-03
      相关资源
      最近更新 更多