有效地在数据库中存储项目位置(用于订购)

时间:2012-06-19 04:25:12

标签: sql database theory

情景:

有一个用户拥有的电影数据库,电影显示在名为“my-movies”的页面上,电影可以按用户所需的顺序显示。例如位置#1中的“搏击俱乐部”,位置#3中的“驱动器”,依此类推。

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

movieid,userid,position
1 | 1 | 1
2 | 1 | 2
3 | 1 | 3

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

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

我的总结问题:将项目位置存储在对提取和更新友好的列表中的最有效方法是什么?

4 个答案:

答案 0 :(得分:12)

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

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

要移动电影的其中一个排名,我们需要更新位置和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位置列值,因为它实际上不会影响确定的电影排名。

答案 1 :(得分:7)

我一直在努力应对这种情况,并且已经意识到 BY FAR 最好的解决方案是按照你想要的顺序列出电影列表/数组,例如;

userId,moviesOrder

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

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

'感觉......效率低下'?

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

OTOH,请考虑更改订单;

存储在db中的位置与阵列拼接相比,最多需要100行更改。链接列表的想法很有意思,但是如果单个元素失败,它会破坏所有内容,并且看起来也会慢得多。其他想法,比如留下空白,使用浮点数虽然很乱,但在某些时候容易出现失败,除非你是GC。

似乎应该有更好的方法在SQL中执行它,但确实没有。

答案 2 :(得分: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次写入。

答案 3 :(得分:0)

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. 在位置> = 2且位置<4的间隔中搜索项目,并为其位置添加+1
  2. 将项目D的位置设置为2。

这将产生以下信息: A-> 1,B-> 3,C-> 4,D-> 2,F-> 5

如果我们想将B移到D,则需要做相反的操作,而是应用-1。

  1. 在位置> 2和位置<= 4从其位置减去-1的间隔中搜索项目
  2. 将项目位置设置为4

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

在创建和项时,其位置等于每个项+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;