如何在关系数据库中存储订购的信息


20

我正在尝试了解如何在关系数据库中正确存储有序信息。

一个例子:

假设我有一个由歌曲组成的播放列表。在我的关系数据库中,我有一个的表Playlists,其中包含一些元数据(名称,创建者等)。我还有一个名为的表Songs,其中包含playlist_id和特定于歌曲的信息(名称,艺术家,时长等)。

默认情况下,将新歌曲添加到播放列表时,它将添加到末尾。按Song-ID(升序)订购时,该顺序将为添加顺序。但是,如果用户应该能够对播放列表中的歌曲重新排序,该怎么办?

我提出了一些想法,每个想法都有其优点和缺点:

  1. 称为的列order,它是整数。移动歌曲时,所有歌曲在其旧位置和新位置之间的顺序都会更改,以反映更改。这样做的缺点是,每次移动歌曲时都需要进行很多查询,并且移动算法不像其他选项那样琐碎。
  2. 称为的列order,它是一个十进制NUMERIC)。移动歌曲时,会为其分配两个相邻数字之间的浮点值。缺点:十进制字段会占用更多空间,并且可能会精度不够,除非在每次更改后都注意重新分配范围。
  3. 另一种方法是使用previousnext字段引用其他歌曲。(或者,如果现在是播放列表中的第一首和最后一首歌曲,则为NULL;基本上,您将创建一个链表)。缺点:诸如“在列表中找到第X首歌曲”之类的查询不再是固定时间,而是线性时间。

在实践中最常使用以下哪个程序?在大中型数据库上,以下哪个过程最快?还有其他方法可以存档吗?

编辑:为简单起见,在此示例中,一首歌曲仅属于一个播放列表(多对一关系)。当然,也可以使用Junction Table,因此song⟷playlist是一个多对多关系(并在该表上应用上述策略之一)。


1
您可以使用100步的选项一(以整数订购)。这样一来,如果您移动一首歌曲,则无需重新排序,只需在100之间取一个值。有时,您可能需要重新编号以重新获得歌曲之间的间隔。
knut

4
“这样做的缺点是每次移动歌曲时都需要进行很多查询”?- update songorder set order = order - 1 where order >= 12 & order <= 42; update songorder set order = 42 where id = 123;这是两个更新-而不是三十个。如果要在订单上施加唯一约束,则三个。

2
使用选项一,除非您知道事实上您还需要其他东西。刚接触数据库的程序员遇到的一个问题是,不了解数据库在这种事情上非常非常好。不要害怕使您的数据库工作。
GrandmasterB

1
Queries like 'find the Xth Song in the list' are no longer constant-time也是选项2.真
布朗博士

2
@MikeNakis:似乎很昂贵,但是所有工作都是在服务器上完成的,(通常)针对这种工作进行了优化。我不会在具有数百万行的表上使用此技术,但不会为只有几千行的表打折。
TMN

Answers:


29

数据库针对某些事物进行了优化。快速更新许多行是其中之一。当您让数据库完成其工作时,情况尤其如此。

考虑:

order song
1     Happy Birthday
2     Beat It
3     Never Gonna Give You Up
4     Safety Dance
5     Imperial March

而您想移到Beat It最后,您将有两个查询:

update table 
  set order = order - 1
  where order >= 2 and order <= 5;

update table
  set order = 5
  where song = 'Beat It'

就是这样。这可以很好地按比例放大。尝试将几千首歌曲放入数据库的假设播放列表中,查看将歌曲从一个位置移动到另一位置需要多长时间。由于它们具有非常标准化的形式:

update table 
  set order = order - 1
  where order >= ? and order <= ?;

update table
  set order = ?
  where song = ?

您有两个准备好的语句,可以非常有效地重用。

这提供了一些显着的优势-您可以推断出表格的顺序。第三首歌有一个order总是 3。保证这一点的唯一方法是使用连续整数作为顺序。使用伪链接列表,十进制数字或带空格的整数将不能保证此属性。在这些情况下,获取第n首歌曲的唯一方法是对整个表进行排序并获取第n条记录。

实际上,这比您想象的要容易得多。很容易弄清楚您想做什么,生成两个更新语句,然后让其他人查看这两个更新语句并了解正在执行的操作。


2
我开始喜欢这种方法。
Mike Nakis 2015年

2
@MikeNakis,效果很好。还有一个基于类似思想的二叉树 - 修改后的预购树。花费很多时间,但可以使您对分层数据进行一些非常好的查询。即使在大树上,我也从未遇到过性能问题。我一直强调能够推理代码,直到证明简单代码缺乏所需的性能(并且仅在极端情况下才如此)。

会不会有什么问题,使用order,因为order by是一个关键的词?
kojow7

@ kojow7,如果您的字段名称与关键字冲突,则应将其包装在对勾标记“`”中。
安德里

这种方法很有意义,但是order在向播放列表中添加新歌曲时获取价值的最佳方法是什么。说这是第9首歌曲,有没有orderCOUNT添加唱片之前先插入9更好的方法了?
delashum

3

首先,根据您所做的描述尚不清楚,但是您需要一个PlaylistSongs包含a PlaylistId和a 的表SongId描述哪些歌曲属于哪些播放列表。

您必须在此表中添加订购信息。

我最喜欢的机制是实数。我最近实现了它,并且像个魅力一样起作用。当您要将一首歌曲移至特定位置时,可以将其新Ordering值计算Ordering为上一首和下一首歌曲的平均值。如果您使用64位实数,则将在大约死地狱的同一时间用完精度,但如果您实际上是为后代编写软件,则可以考虑Ordering为每首歌曲中的所有歌曲重新分配漂亮的舍入整数值每隔一段时间播放一次播放列表。

另外,这是我编写的实现此目的的代码。当然,您不能按原样使用它,对我来说,现在为您清理它将是一件繁重的工作,因此,我仅将其发布给您以从中获取想法。

该类是ParameterTemplate(无论什么都不要问!)。该方法从其父级获取此模板所属的参数模板的列表ActivityTemplate。(无论如何,不​​要问!)代码中包含一些防止精度用完的保护措施。除数用于测试:单元测试使用大除数,以快速耗尽精度,从而触发精度保护代码。第二种方法是公共的,并且“仅供内部使用;请勿调用”,以便测试代码可以调用它。(它不能是私有程序包,因为我的测试代码与其测试的代码不在同一个程序包中。)控制顺序的字段称为Ordering,通过getOrdering()和访问setOrdering()。您没有看到任何SQL,因为我正在通过Hibernate使用对象关系映射。

/**
 * Moves this {@link ParameterTemplate} to the given index in the list of {@link ParameterTemplate}s of the parent {@link ActivityTemplate}.
 *
 * The index must be greater than or equal to zero, and less than or equal to the number of entries in the list.  Specifying an index of zero will move this item to the top of
 * the list. Specifying an index which is equal to the number of entries will move this item to the end of the list.  Any other index will move this item to the position
 * specified, also moving other items in the list as necessary. The given index cannot be equal to the current index of the item, nor can it be equal to the current index plus
 * one.  If the given index is below the current index of the item, then the item will be moved so that its new index will be equal to the given index.  If the given index is
 * above the current index, then the new index of the item will be the given index minus one.
 *
 * NOTE: this method flushes the persistor and refreshes the parent node so as to guarantee that the changes will be immediately visible in the list of {@link
 * ParameterTemplate}s of the parent {@link ActivityTemplate}.
 *
 * @param toIndex the desired new index of this {@link ParameterTemplate} in the list of {@link ParameterTemplate}s of the parent {@link ActivityTemplate}.
 */
public void moveAt( int toIndex )
{
    moveAt( toIndex, 2.0 );
}

/**
 * For internal use only; do not invoke.
 */
public boolean moveAt( int toIndex, double divisor )
{
    MutableList<ParameterTemplate<?>> parameterTemplates = getLogicDomain().getMutableCollections().newArrayList();
    parameterTemplates.addAll( getParentActivityTemplate().getParameterTemplates() );
    assert parameterTemplates.getLength() >= 1; //guaranteed since at the very least, this parameter template must be in the list.
    int fromIndex = parameterTemplates.indexOf( this );
    assert 0 <= toIndex;
    assert toIndex <= parameterTemplates.getLength();
    assert 0 <= fromIndex;
    assert fromIndex < parameterTemplates.getLength();
    assert fromIndex != toIndex;
    assert fromIndex != toIndex - 1;

    double order;
    if( toIndex == 0 )
    {
        order = parameterTemplates.fetchFirstElement().getOrdering() - 1.0;
    }
    else if( toIndex == parameterTemplates.getLength() )
    {
        order = parameterTemplates.fetchLastElement().getOrdering() + 1.0;
    }
    else
    {
        double prevOrder = parameterTemplates.get( toIndex - 1 ).getOrdering();
        parameterTemplates.moveAt( fromIndex, toIndex );
        double nextOrder = parameterTemplates.get( toIndex + (toIndex > fromIndex ? 0 : 1) ).getOrdering();
        assert prevOrder <= nextOrder;
        order = (prevOrder + nextOrder) / divisor;
        if( order <= prevOrder || order >= nextOrder ) //if the accuracy of the double has been exceeded
        {
            parameterTemplates.clear();
            parameterTemplates.addAll( getParentActivityTemplate().getParameterTemplates() );
            for( int i = 0; i < parameterTemplates.getLength(); i++ )
                parameterTemplates.get( i ).setOrdering( i * 1.0 );
            rocs3dDomain.getPersistor().flush();
            rocs3dDomain.getPersistor().refresh( getParentActivityTemplate() );
            moveAt( toIndex );
            return true;
        }
    }
    setOrdering( order );
    rocs3dDomain.getPersistor().flush();
    rocs3dDomain.getPersistor().refresh( getParentActivityTemplate() );
    assert getParentActivityTemplate().getParameterTemplates().indexOf( this ) == (toIndex > fromIndex ? toIndex - 1 : toIndex);
    return false;
}

我将使用整数排序,如果我觉得重新排序太昂贵,我只需减少一次重新排序的次数,方法是每次跳一次X,其中X是我需要减少重新排序的数量,例如20。作为入门者应该没问题。
沃伦·P

1
@WarrenP是的,我知道,也可以通过这种方式完成,这就是为什么我将这种方法称为“我最喜欢的”方法,而不是“最好的”或“一个”方法。
Mike Nakis 2015年

0

对我有用的,对于100个项目中的一个小清单,是采取一种混合方法:

  1. 十进制SortOrder列,但仅具有足够的精度来存储0.5的差异(即Decimal(8,2)或类似的东西)。
  2. 排序时,抓住当前行刚移到的上下两行的PK(如果存在)。(例如,如果您将项目移到第一个位置,则不会在上面一行)
  3. 将当前行,上一行和下一行的PK发布到服务器以执行排序。
  4. 如果有上一行,请将当前行的位置设置为prev + 0.5。如果只有下一个,请将当前行的位置设置为下一个-0.5。
  5. 接下来,我有一个存储过程,该过程使用SQL Server Row_Number函数更新所有位置,并按新的排序顺序进行排序。这将把顺序从1,1.5,2,3,4,6转换为1,2,3,4,5,6,因为row_number函数为您提供了整数序数。

因此,您最终得到一个无间隙的整数顺序,并存储在小数列中。我觉得这很干净。但是,一旦您有成千上万的行需要一次更新,它可能无法很好地扩展。但是,如果这样做,为什么首先要使用用户定义的排序?(请注意:如果您有一个包含数百万个用户的大型表,但每个用户只有几百个项目要排序,则可以使用上述方法很好,因为无论如何您都将使用where子句将更改限制为一个用户)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.