web-dev-qa-db-ja.com

注文した情報をリレーショナルデータベースに保存する方法

注文した情報をリレーショナルデータベースに適切に保存する方法を理解しようとしています。

例:

曲で構成されるプレイリストがあるとします。リレーショナルデータベース内に、いくつかのメタデータ(名前、作成者など)を含むPlaylistsのテーブルがあります。 Songsというテーブルもあり、playlist_id、および曲固有の情報(名前、アーティスト、期間など)。

デフォルトでは、新しい曲がプレイリストに追加されると、最後に追加されます。 Song-ID(昇順)でご注文の場合は、追加注文となります。しかし、ユーザーがプレイリストの曲を並べ替えることができる必要がある場合はどうでしょうか。

私はいくつかのアイデアを思いつきました、それぞれに長所と短所があります:

  1. integerであるorderという列。曲を移動すると、その変更を反映するために、古い位置と新しい位置の間のすべての曲の順序が変更されます。これの欠点は、曲が移動されるたびに多くのクエリを実行する必要があり、移動アルゴリズムが他のオプションほど簡単ではないことです。
  2. orderという列。これはdecimalNUMERIC)です。曲を移動すると、隣接する2つの番号の間の浮動小数点値が割り当てられます。欠点:10進数フィールドはより多くのスペースを必要とし、数回の変更ごとに範囲を再分散するように注意を払わない限り、精度が不足する可能性があります。
  3. 別の方法は、他の曲を参照するpreviousnextフィールドを持つことです。 (または、現在プレイリストの最初、最後の曲の場合はNULLです。基本的には linked-list を作成します)。欠点:「リストのX番目の曲を見つける」のようなクエリは、一定時間ではなく、線形時間です。

これらの手順のどれが実際に最も頻繁に使用されますか?これらの手順のどれが中規模から大規模のデータベースで最も高速ですか?これをアーカイブする他の方法はありますか?

EDIT:簡単にするために、この例では、ソングは1つのプレイリストにのみ属します(多対1の関係)。もちろん、Junction Tableを使用することもできます。そのため、song⟷playlistは多対多の関係になります(そして上記の戦略の1つをそのテーブルに適用します)。

24
Qqwy

データベースは特定の目的のために最適化されています。多くの行をすばやく更新することもその1つです。これは、データベースに機能させる場合に特に当てはまります。

考慮してください:

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

そして、あなたは移動したいBeat It最後に、2つのクエリを作成します。

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 = ?

非常に効率的に再利用できる2つの準備済みステートメントがあります。

これにはいくつかの重要な利点があります-テーブルの順序はあなたが考えられるものです。 3番目の曲のorderは常に3です。これを保証する唯一の方法は、連続した整数を順序として使用することです。疑似リンクリスト、10進数、またはギャップのある整数を使用しても、このプロパティは保証されません。これらの場合、n番目の曲を取得する唯一の方法は、テーブル全体をソートしてn番目のレコードを取得することです。

そして、本当に、これはあなたが思っているよりもずっと簡単です。 2つの更新ステートメントを生成し、他の人が2つの更新ステートメントを見て、何が行われているのかを理解するために、何をしたいかを理解するのは簡単です。

32
user40980

まず第一に、あなたが何をしたかについてのあなたの説明から明らかではありません、しかしあなたはPlaylistSongsPlaylistIdを含むSongIdテーブルが必要で、どの曲が属するかを説明しますどのプレイリストに。

この表では、注文情報を追加する必要があります。

私の好きなメカニズムは実数です。最近実装しましたが、とても魅力的でした。曲を特定の位置に移動する場合、その新しいOrdering値を、前の曲と次の曲のOrdering値の平均として計算します。 64ビット実数を使用すると、地獄がフリーズするのとほぼ同時に精度が不足しますが、本当に後世のためにソフトウェアを作成している場合は、Nice丸め整数Orderingの再割り当てを検討してください時々、各プレイリストのすべての曲に値を設定します。

追加のボーナスとして、これは私がこれを実装するコードを書いたものです。もちろん、そのままでは使えませんし、今のところ消毒するのも大変なので、アイデアを出すためだけに投稿しています。

クラスはParameterTemplateです(何も質問しないでください!)メソッドは、このテンプレートが属するパラメーターテンプレートのリストを親ActivityTemplateから取得します。 (何であれ、質問しないでください!)コードには、精度が不足しないように保護されています。除数はテストに使用されます。単体テストは大きな除数を使用して、精度がすぐに不足し、精度保護コードがトリガーされるようにします。 2番目のメソッドはパブリックであり、「内部でのみ使用します。呼び出さないでください」なので、テストコードで呼び出すことができます。 (私のテストコードは、テストするコードと同じパッケージにないため、パッケージプライベートにすることはできません。)順序を制御するフィールドはOrderingと呼ばれ、getOrdering()を介してアクセスされます。 setOrdering()。 Hibernateを介してオブジェクトリレーショナルマッピングを使用しているため、SQLは表示されません。

/**
 * 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;
}
3
Mike Nakis

私にとってうまくいったのは、100アイテム程度の小さなリストの場合、ハイブリッドアプローチを取ることでした。

  1. Decimal SortOrder列ですが、0.5の差を格納するのに十分な精度しかありません(つまり、Decimal(8,2)など)。
  2. 並べ替えの際、現在の行が移動された場所の上下にある行のPKを取得します(存在する場合)。 (たとえば、アイテムを最初の位置に移動した場合、上の行はなくなります)
  3. 現在、前、次の行のPKをサーバーにポストして、ソートを実行します。
  4. 前の行がある場合は、現在の行の位置を前+ 0.5に設定します。次のみがある場合は、現在の行の位置を次の0.5に設定します。
  5. 次に、SQL ServerのRow_Number関数を使用してすべての位置を更新し、新しい並べ替え順序で並べ替えるストアドプロシージャがあります。これは、row_number関数が整数の序数を提供するため、順序を1,1.5,2,3,4,6から1,2,3,4,5,6に変換します。

つまり、10進数の列に格納された、ギャップのない整数の順序になります。かなり綺麗だと思います。ただし、一度に更新する必要のある数十万の行があると、非常にうまくスケールアップしない場合があります。しかし、もしそうなら、なぜ最初にユーザー定義の並べ替えを使用しているのですか? (注:何百万ものユーザーがいる大きなテーブルがあり、各ユーザーが並べ替えるアイテムが数百しかない場合は、とにかくwhere句を使用して変更を1人のユーザーだけに制限するので、上記のアプローチを使用できます)

0
John