如何设计可重新排序的表?

时间:2009-10-17 05:09:58

标签: database sorting sequence

我需要对数据库做出设计决定。要求是一个数据库表具有 AUTO_INCREMENT PRIMARY KEY 字段,名为 id 。默认情况下,每行显示给用户(在Web中)按 id 排序。例如,如果表中有4条记录。用户界面将按 0,1,2,3 的顺序显示行。

现在,要求用户可以拖放&在UI中删除行以更改序列。比如说,用户拖动rom 3并将其降低为0.因此,显示顺序变为 3,0,1,2 。该序列应该持久存储在数据库中。

我想知道如何设计数据库表以使其具有持久性和可扩展性。我的第一个想法是每行都有一个“ 序列 ”字段,表示显示顺序。默认情况下,该值应与 id 相同。从数据库中选择要显示的数据时,行将按序列而不是 id

进行升序排序。

如果更改了序列,则会将其更新为新值。结果是它可能涉及其他行的很多变化。以上为例,最初表格如下:

|id   | sequence |
|0    | 0        |
|1    | 1        |
|2    | 2        |
|3    | 3        |

现在,在将id为3的拖动行放到第一位之后。其序列更新为0.同时,还应更新ID为0,1,2的行。

|id   | sequence |
|0    | 1        |
|1    | 2        |
|2    | 3        |
|3    | 0        |

我担心这种方法会使重新序列成本耗费大量资源而且不具备可扩展性。因此,我认为序列可以通过将 id 与K(例如10)相乘来实现。这为插入的序列值留下了空白。但是,如果将K + 1行移动到此间隙,则差距仍然会消耗。

|id   | sequence |
|0    | 0        |
|1    | 10       |
|2    | 20       |
|3    | 30       |

这似乎是数据库设计的常见问题。有人有更好的想法来实现这个目标吗?

8 个答案:

答案 0 :(得分:10)

对我来说,显而易见的答案是使用你提到的最后一个解决方案,但是使用小数(浮点数)。

首先,请说:{0.1, 0.2, 0.3, 0.4, 0.5}。如果您将最后一项移至0.20.3之间,则会变为0.25。如果将其移至顶部,则变为0.05。每次你只需要两侧的两个数字的中点。换句话说,前一个/下一个项目的平均值。

另一个类似的解决方案是使用字符,然后按字母顺序按字符串排序。从{1, 2, 3, 4, 5}开始,如果您将2移动到2和3之间,则使用25.如果您对列表进行字符串排序,则保持正确的顺序:{1, 2, 25, 3, 4}

我能用这些方法想到的唯一问题是,最终,你将达到浮点精度的极限,即试图找到0.00781250.0078124之间的数字。解决这个问题的几种方法:

  • 每隔一段时间运行一个脚本,遍历每个项目并将其重新排序到{0.1, 0.2, 0.3, ...}
  • 使用时,请勿使用两位小数。在0.20.25之间,您可以使用0.23代替计算出的0.225
  • 在本地重新排序,而非全局重排。如果您有{0.2, 0.3, 0.6}并希望在0.2后插入,则可以将第二个设置为0.4并在0.3插入新项目。

答案 1 :(得分:3)

ID和Sequence / SortOrder是分开的,完全不应相互依赖。

用于上移/下移功能: 您可以交换Sequence / SortOrder值

对于拖放功能:

1)为所选记录建立新的Sequence / OrderNumber。

2)获取所选记录的当前序列,然后使用新号码更新所选记录。

3) a)如果新序列号低于当前序列号,则增加序列号>>新序列号的记录的所有序列号(排除所选序列号)

b)如果新序列号高于当前序列号,则减少新选择序列号以下的所有序列号,并将其减去当前序列号之上。

希望这是有道理的,尽管它是正确的方式(下面是实际的实现)。

我已经在单个SQL语句中实现了这一点,该语句具有少量逻辑,不适用于纯粹主义者,但它的效果很好。

以下是一个示例(OP:您希望将GUID ID更改为INT):

CREATE PROCEDURE [proc_UpdateCountryRowOrder]
    @ID UNIQUEIDENTIFIER,
    @NewPosition INT
AS

SET NOCOUNT ON

DECLARE @CurrentPosition INT
DECLARE @MaximumPosition INT

IF (@NewPosition < 1) SET @NewPosition = 1

SELECT @CurrentPosition = [Countries].[Order]
FROM [Countries]
WHERE [Countries].[ID] = @ID

SELECT @MaximumPosition = MAX([Countries].[Order])
FROM [Countries]

IF (@NewPosition > @MaximumPosition) SET @NewPosition = @MaximumPosition

IF (@NewPosition <> @CurrentPosition)
BEGIN
    IF (@NewPosition < @CurrentPosition)
    BEGIN
        BEGIN TRAN

        UPDATE [Countries]
        SET [Countries].[Order] = [Countries].[Order] + 1
        WHERE [Countries].[Order] >= @NewPosition
        AND [Countries].[Order] < @CurrentPosition

        UPDATE [Countries]
        SET [Countries].[Order] = @NewPosition
        WHERE ID = @ID

        COMMIT TRAN
    END
    ELSE
    BEGIN
        BEGIN TRAN

        UPDATE [Countries]
        SET [Countries].[Order] = [Countries].[Order] - 1
        WHERE [Countries].[Order] <= @NewPosition
        AND [Countries].[Order] > @CurrentPosition

        UPDATE [Countries]
        SET [Countries].[Order] = @NewPosition
        WHERE ID = @ID

        COMMIT TRAN
    END
END
GO

答案 2 :(得分:2)

链接列表怎么样? : - )

CREATE TABLE item(
    id INT PRIMARY KEY,
    prev INT,
    next INT
);

WITH RECURSIVE sequence AS (
    SELECT item.id, item.prev, item.next FROM item
    WHERE item.prev IS NULL
  UNION
    SELECT item.id, item.prev, item.next FROM sequence
    INNER JOIN item ON sequence.next = item.id
)
SELECT * FROM sequence;

实际上,我手边没有PostgreSQL来测试它是否真的有用(并且MySQL不支持SQL-99的WITH RECURSIVE),我也不认真推荐它。

答案 3 :(得分:1)

我在这里回答了类似的问题:Visually order large data sets

如果您移动了很多项目,那么您必须循环移动每个项目并检查溢出。但总而言之,基本逻辑是有一个带有间隙的分类栏,可以定期重新初始化。

答案 4 :(得分:0)

我通过按用户选择的顺序将ids(db键)的CSV字符串返回给服务器来完成此操作。我在我的数据库上有一个函数,它将csv字符串转换为一个包含2个字段的表 - id和一个序列(实际上是一个带有标识的int)。此临时表中的序列字段值反映CSV字符串中项目的顺序。然后,我使用新的序列字段值更新数据表,与id匹配。

编辑:赏金点看起来很好吃,我想我会提供一些关于我答案的细节。这是代码:

declare @id_array varchar(1000)
set @id_array = '47,32,176,12,482'

declare @id_list_table table ([id] int, [sequence] int)

insert @id_list_table ([id], [sequence])
  select [id], [sequence]
  from get_id_table_from_list (@id_array)

update date_table
  set [sequence] = id_list.[sequence]
  from date_table
    inner join @id_list_table as id_list
      on (id_list.[id] = date_table.[id])

我将@id_array设置为测试变量 - 通常您的UI会按修改后的顺序获取id值,并将其作为参数传递给存储过程。 get_id_table_from_list函数将csv字符串解析为具有两个“int”列的表:[id]和[sequence]。 [sequence]列是一个标识。处理我的测试数据的函数的结果如下所示:

    id   seq
    47   1  
    32   2  
    176  3  
    12   4  
    482  5  

你需要一个能解析csv的函数(如果你有兴趣我可以发帖,我已经看到其他人在这里和那里发布)。请注意,我的代码假设您正在使用sql server - 序列事物依赖于身份字段,而更新查询使用T-SQL扩展('from'子句) - 如果您使用其他数据库,则需要做一些简单的改变。

答案 5 :(得分:0)

此修复程序比您想象的要容易。一个临时表和一个更新查询以及您的完成。


CREATE TABLE #TempData
(
NewSequence bigint identity(1,1),
[Id] BigInt
)

INSERT INTO #TempData ([Id])
SELECT [Id] 
FROM TableNameGoesHere
ORDER BY Sequence

UPDATE TableNameGoesHere
SET Sequence = t2.NewSequence
FROM TableNameGoesHere t1
INNER JOIN #TempData t2
ON t1.[Id] = t2.[Id]

DROP TABLE #TempData

答案 6 :(得分:0)

我知道这是一个3岁的问题,但这些评论帮助我解决了类似的问题,我希望提供我的代码,以防其他任何人寻找类似的东西。

基于@DisgruntledGoat提供的代码示例(顺便说一句,谢谢!)我创建了以下代码,专门为Entity Framework 4.1代码编写。它基本上需要三个参数 - 存储库对象,实体对象的id和布尔值,以指示实体的DisplayOrder是否应在显示中向上或向下移动。实体必须从Entity继承,这意味着它必须具有Id值并实现IOrderedEntity,这意味着它必须具有DisplayOrder float属性。

MoveDisplayOrder方法查找相邻的两个条目(小于或大于当前显示顺序,具体取决于移动方向),然后平均它们的值(因此需要浮点而不是整数值)。然后,它将更新该实体的存储库。然后,为了清理目的,如果生成的新显示顺序中的小数位数超过5,则使用EF更新数据库中的所有值,并将它们重新设置为值1,2,3等。 / p>

此代码对我来说非常有用,如果需要清理或重构,欢迎提出反馈。

  public abstract class Entity : IIdentifiableEntity
  {
    public int Id { get; set; }
  }

  public interface IOrderedEntity
  {
    float DisplayOrder { get; set; }
  }

  public interface IRepository<TEntity>
  {
    TEntity FindById(int id);
    bool InsertOrUpdate(TEntity entity);
    // More repository methods here...
  }

public static class RepositoryExtenstions
{
  public static void MoveDisplayOrder<T>(IRepository<T> repository, int id, bool moveUp) where T : Entity, IOrderedEntity
  {
    var currentStatus = repository.FindById(id);
    IQueryable<IOrderedEntity> adjacentStatuses;
    if (moveUp)
      adjacentStatuses = repository.All().OrderByDescending(ms => ms.DisplayOrder).Where(ms => ms.DisplayOrder < currentStatus.DisplayOrder);
    else
      adjacentStatuses = repository.All().OrderBy(ms => ms.DisplayOrder).Where(ms => ms.DisplayOrder > currentStatus.DisplayOrder);

    var adjacentTwoDisplayOrders = adjacentStatuses.Select(ms => ms.DisplayOrder).Take(2).ToList();
    float averageOfPreviousTwoDisplayOrders;
    switch (adjacentTwoDisplayOrders.Count)
    {
      case 0:
        // It's already at the top or bottom, so don't move it
        averageOfPreviousTwoDisplayOrders = currentStatus.DisplayOrder;
        break;
      case 1:
        // It's one away, so just add or subtract 0.5 to the adjacent value
        if (moveUp)
          averageOfPreviousTwoDisplayOrders = adjacentTwoDisplayOrders[0] - 0.5F;
        else
          averageOfPreviousTwoDisplayOrders = adjacentTwoDisplayOrders[0] + 0.5F;
        break;
      default: // 2
        // Otherwise, just average the adjacent two values
        averageOfPreviousTwoDisplayOrders = adjacentTwoDisplayOrders.Average();
        break;
    }

    currentStatus.DisplayOrder = averageOfPreviousTwoDisplayOrders;
    repository.InsertOrUpdate(currentStatus);
    var floatPrecision = currentStatus.DisplayOrder.ToString().Substring(currentStatus.DisplayOrder.ToString().IndexOf('.') + 1).Length;
    if(floatPrecision > 5)
      ReorganizeDisplayOrder(repository);
  }

  public static void ReorganizeDisplayOrder<T>(IRepository<T> repository) where T : Entity, IOrderedEntity
  {
    var entities = repository.All().OrderBy(ms => ms.DisplayOrder).ToList();
    float counter = 1F;
    foreach (var entity in entities)
    {
      entity.DisplayOrder = counter;
      repository.InsertOrUpdate(entity);
      counter++;
    }
  }
}

答案 7 :(得分:-1)

- 编辑:对于读这篇文章的其他人来说,我已经被一些奇怪的个人怨恨所淹没,而不是关于这个主题的一般不准确,在阅读时给予考虑,并投票如何你想要! :)

- 旧:

执行此操作的典型方法是使用“序列”编号(我将其称为“SortOrder”)。

这真的很容易。

“可扩展”不会进入它;因为你已经有了一个你正在处理的所有节点的列表(拖放,就像你说的那样),所以你真正在做的只是交换这些数字。

微不足道的。