重新排序有序列表
Posted
技术标签:
【中文标题】重新排序有序列表【英文标题】:Reordering an ordered list 【发布时间】:2010-07-31 18:53:48 【问题描述】:我有一个包含数据的以下 SQL 表
ProductList
id order productname
79 1 name1
42 2 name2
67 3 somename
88 4 othername
99 5 XYZ
66 6 ABC
显示顺序非常不稳定,会经常变化,用户会添加或删除项目并重新排序。
我应该如何在不更新多条记录的情况下处理这种情况。示例:如果用户在 1 和 2 订单之间输入新产品,我不想更新 2 下的所有记录的顺序,如果有人将订单 3 切换到 4,我不想更新 3 下的每条记录。
【问题讨论】:
没有计划任务来“重新排序”订单,它必须是实时的。 就像 Will A 说的 - 从它们之间距离很远的值开始。当你必须在两者之间插入一些东西时,将它插入到有问题的值之间的一半。也许每月一次,将所有记录更改回 100 步左右。无需重新排序。 我对计划任务的建议是在安静期间运行计划的“维护”任务 - 将订单值设置回“更清洁”的值。 是什么阻止您使用多个更新以及围绕它们的事务?你甚至不在READ COMMITTED 隔离中?还有,什么是frequently在会经常变化?? 某些数据库(如 SQL Server)允许您即时计算行数。这样就够了吗?能够在查询时计算行号(使用 ROW_NUMBER() 函数)?如果没有,您可以通过运行这样的查询并更新“订单”列来定期更新该表,比如每小时更新一次 - 这是否足够好(每隔一小时左右更新一次表)? 【参考方案1】:使用由旧 BASIC 编码器出名的“可怕的老把戏(?) - 将您的订单设置为 100、200、300、400 等,然后您可以在需要时选择“中间”的订单。这可能会变得混乱 - 如果您预计会进行大量重新排序,那么我建议您有一个计划任务来不时地为整个表格“重新排序”订单值。
【讨论】:
计划任务有问题 - 旧订单将保留到作业执行为止。 @OMG Ponies - 这怎么会是个问题?我的看法是订单值仅用于显示目的 - 如果它在任何地方用作外键的主键端,那么所有赌注都将关闭。 @Ponies:我认为计划任务不是改变实际排序,而是将数字重新设置为以 100 分隔,这样您就不会遇到所有中间数字已满。【参考方案2】:你有两个选择:
迭代行,使用带有函数/等的 UPDATE 语句生成更新的订单值 Scorched Earth:您删除现有记录,并插入相同的记录以保存更正的订单值没有 SQL 功能可以让这更容易,也没有真正简单的选项。
【讨论】:
@Mike Sherov:我喜欢那个游戏 =) +1。尽管它更新了用户订单中的所有行,但它可以实现最干净的检索,并且还可以防止并发问题。巧妙地使用聚集索引也几乎总是意味着所有更新的行都在同一个页面上,因此多行更新的性能损失是最小的。【参考方案3】:同一问题的第二个答案:
使用 DOUBLE 字段进行排序并拆分要插入的两个值之间的差异。在任何标准业务应用程序中,我非常非常怀疑您是否会接近无法解决排序顺序差异的插入数量。
【讨论】:
其实你会很快的。如果在 8 * 8 = 64 中使用 8 字节数,则在同一位置重新排序。 @BorisYankov 它比 64 差,因为 typical double 使用 52 位作为尾数,所以在我们用完位之前,只有 52 次在同一个地方重新排序。我想你可以取新中间数的日志基数 2 并达到一个阈值,比如 -52 你可以重新排序整个表【参考方案4】:您可以使用字符串来代替无限多的数字。如果要在“1”和“2”之间插入,请将其设为“15”。
【讨论】:
你没有。您可以无限附加字符。例如,在“1”和“15”之间有“125”。 这看起来不错。一个缺点是插入次数过多,排序列中的字符串会变得很大。【参考方案5】:您可以改为使用链表进行排序(使用一些特殊的方式来识别头部)。
插入是一个 INSERT 和一个 UPDATE。删除是一个 DELETE 和一个 UPDATE。一个动作是三个更新。 (当然,使用事务来确保链表不会中断。)
【讨论】:
插入、删除和更新对于链表来说都是相对有效的(尽管 OP 甚至不想为交换进行两次更新)。链表的问题在于检索——对于 N 个项目,您需要 N 个 SELECT。 @Larry Lustig,是的,但是 OP 可能正在检索列表中的所有项目,并且 O(N) 可能对客户端代码没有任何伤害。 我明白了,在一次 SELECT 中从数据库中检索所有记录后,您正在内存中构建链表。这比我认为您推荐的要有效得多——通过在检索每一行时检查 Next 指针来一次检索一行。 分页呢?用“LIMIT”查询任何东西都是不可能的。【参考方案6】:添加一个名为 OrderDateTime 的 DATETIME 列。使用此列(按降序排列)来解决排序中的“关系”,并且仅在发生排序操作时更新它。
例如,在您的示例中,假设所有行都具有昨天的 OrderDateTime 值。
现在,要在 1 和 2 之间插入一个项目,您需要将其 Order 值设置为 2,将 OrderDateTime 值设置为现在。当您 SELECT * FROM ProductList ORDER BY Order ASC, OrderDateTime DESC 时,新的第 2 项将排在现有项之前。
同样,要交换项目 4 和 5,您需要更新项目 5,使其订单为 4,OrderDateTime 为现在。它将成为更新的 4 项并更早出现。
您需要注意,如果您尝试在已经具有相同 Order 值的其他两个项目之间插入一个项目,您会拆分 OrderDateTime 值的差异。
【讨论】:
很聪明,虽然它感觉像个黑客。 很有趣,但我不想成为支持它的人。 我为你最初的聪明解决方案喝彩,但你最后的警告实际上是该方法的一个致命缺陷。首先,DATETIME 不是唯一的。你需要一个单调的计数器。有了这个,假设你有(val, order, counter)
和元组(a, 1, 2)
和(b, 1, 3)
。您不能在a
和b
之间插入c
,因为没有办法将计数器差异拆分为2 和3(不更新a,这可能会导致级联更新)。这种方法只是增加了另一个维度,从而在某种程度上将问题“推迟”到另一列。【参考方案7】:
在我的例子中,我允许用户重新排列他们游览中的步骤。我有一个他们可以更改的名为 STEP 的列,我创建了一个名为 STEP2 的列。 STEP2 列会自动更新为与 step 相同的值。但是当用户更改 STEP 时,我按建议的更改和原始顺序 (STEP2) DESC 进行排序。用户将 STEP 更新为新值后,我保存到数据库中。然后我运行一个 proc 来重新排序这些步骤。
过程是:
DECLARE @t StackTable
INSERT INTO @t(Id)
SELECT Id
FROM TourStops
WHERE TourIdRef = @tourId
ORDER BY [Step], [Step2] DESC
UPDATE TourStops
SET [Step] = t.Position, [Step2] = t.Position
FROM TourStops s join @t t on s.Id = t.Id
StackTable 是一个包含 2 列的 UDT:Id 和 Position。位置是一个 IDENTITY 列。每次创建时,它都从 1 开始,每添加一行就增加 1。
【讨论】:
我原来的例子不起作用。我尝试将 TIMESTAMP 字段与子查询一起使用,但无法获得一致的结果。我不知道具体为什么。也许是因为更改值更改了时间戳,因此重新排序了结果......?但是我不得不将结果移动到不同的表中,因此不会因为时间戳更改而更改顺序。【参考方案8】:根据 Larry Lustig 关于使用替身的建议,我想出了这个。在一个有 10K 行的表上运行,它运行得非常快。欢迎提出提高效率的建议!
--- Note:
--- [Order] column is a value type of double
--- when inserting into MyTable, set [Order] to
--- MAX([Order])+10
CREATE PROCEDURE dbo.MoveUpLevel
(
@MyTableID int
)
AS
BEGIN
SET NOCOUNT ON
DECLARE @LevelMe float
DECLARE @LevelMin float
DECLARE @LevelMax float
SELECT @LevelMe = [Order]
FROM dbo.MyTable WITH (NOLOCK)
WHERE MyTableID = @MyTableID
SELECT @LevelMin = MIN([Order])
,@LevelMax = MAX([Order])
FROM dbo.MyTable WITH (NOLOCK)
--- Check if already at the top
IF @LevelMe = @LevelMin
RETURN(0)
DECLARE @LevelAboveMe float
SELECT TOP 1 @LevelAboveMe = [Order]
FROM dbo.MyTable WITH (NOLOCK)
WHERE [Order] < @LevelMe
ORDER BY [Order] DESC
DECLARE @LevelAboveMe2 float = 0
IF NOT @LevelAboveMe = @LevelMin
SELECT TOP 1 @LevelAboveMe2 = [Order]
FROM dbo.MyTable WITH (NOLOCK)
WHERE [Order] < @LevelAboveMe
ORDER BY [Order] DESC
-- calculate new level
SET @LevelMe = @LevelAboveMe2 + ((@LevelAboveMe - @LevelAboveMe2)/2)
-- store to DB
UPDATE dbo.MyTable
SET [Order] = @LevelMe
WHERE MyTableID = @MyTableID
RETURN(0)
END
GO
CREATE PROCEDURE dbo.MoveDownLevel
(
@MyTableID int
)
AS
BEGIN
SET NOCOUNT ON
DECLARE @LevelMe float
DECLARE @LevelMin float
DECLARE @LevelMax float
SELECT @LevelMe = [Order]
FROM dbo.MyTable WITH (NOLOCK)
WHERE MyTableID = @MyTableID
SELECT @LevelMin = MIN([Order])
,@LevelMax = MAX([Order])
FROM dbo.MyTable WITH (NOLOCK)
--- Check if already at the bottom
IF @LevelMe = @LevelMax
RETURN(0)
DECLARE @LevelBelowMe float
SELECT TOP 1 @LevelBelowMe = [Order]
FROM dbo.MyTable WITH (NOLOCK)
WHERE [Order] > @LevelMe
ORDER BY [Order] ASC
DECLARE @LevelBelowMe2 float = @LevelMax + 10
IF NOT @LevelBelowMe = @LevelMax
SELECT TOP 1 @LevelBelowMe2 = [Order]
FROM dbo.MyTable WITH (NOLOCK)
WHERE [Order] > @LevelBelowMe
ORDER BY [Order] ASC
-- calculate new level
SET @LevelMe = @LevelBelowMe + ((@LevelBelowMe2 - @LevelBelowMe)/2)
-- store to DB
UPDATE dbo.MyTable
SET [Order] = @LevelMe
WHERE MyTableID = @MyTableID
RETURN(0)
END
GO
【讨论】:
以上是关于重新排序有序列表的主要内容,如果未能解决你的问题,请参考以下文章