web-dev-qa-db-ja.com

並べ替え可能なリストをデータベースに保存する

ユーザーがさまざまなウィッシュリストにアイテムを追加できるウィッシュリストシステムに取り組んでいます。ユーザーが後でアイテムを再注文できるようにする予定です。私はこれをデータベースに保存して高速で混乱に陥らないようにする最善の方法について本当に確信がありません(このアプリはかなり大規模なユーザーベースで使用されるため、ダウンさせたくありません)ものを片付けるために)。

私は最初にposition列を試しましたが、他のアイテムの位置の値を変更する場合、それらを変更しなければならないのは非常に非効率的です。

以前の(または次の)値を参照するために自己参照を使用している人を見てきましたが、繰り返しになりますが、リスト内の他の多くの項目を更新する必要があるようです。

私が見た別の解決策は、10進数を使用し、それらの間にアイテムを貼り付けることです。これは、これまでのところ最良の解決策のようですが、もっと良い方法があるはずだと確信しています。

典型的なリストには最大約20アイテムが含まれると思います。おそらく50に制限します。並べ替えはドラッグアンドドロップを使用し、おそらく競合状態などを防ぐためにバッチで行われます。 ajaxリクエスト。必要に応じて、(herokuで)postgresを使用しています。

誰かアイデアはありますか?

助けてくれて乾杯!

63
Tom Brunoli

まず、10進数を巧妙に操作しようとしないでください。 REALおよびDOUBLE PRECISIONは不正確であり、入力した内容を適切に表していない可能性があります。 NUMERICは正確ですが、正しい順序の移動では精度が不足し、実装がうまく機能しなくなります。

動きをシングルアップとダウンに制限することで、操作全体が非常に簡単になります。連続番号の付いたアイテムのリストの場合、アイテムを上に移動するには、その位置をデクリメントし、前のデクリメントで生じた位置番号をインクリメントします。 (言い換えれば、アイテム54になり、アイテム45になります。これは、Moronsが彼の回答で説明したように交換されます。)下に移動すると、反対。リストと位置を一意に識別するものでテーブルにインデックスを付けます。非常に高速に実行されるトランザクション内で2つのUPDATEsを使用してそれを行うことができます。ユーザーがリストを超人的な速度で再配置しない限り、これによって負荷が大きくなることはありません。

ドラッグアンドドロップ移動(たとえば、アイテム6を移動して、アイテム910の間に移動する)は少しトリッキーであり、新しい位置が上にあるかどうかによって異なる方法で実行する必要がありますまたは古いものの下。上記の例では、9より大きいすべての位置をインクリメントし、アイテム6の位置を新しい10に更新してから、すべての位置をデクリメントして、穴を開ける必要があります。 6は、空いた場所を埋めます。前に説明したのと同じ索引付けを使用すると、これは迅速になります。トランザクションが関係する行の数を最小限に抑えることで、これを実際に私が説明したよりも少し速くすることができます。

どちらの方法でも、自作の、あまりにも賢い半熟のソリューションでデータベースをしのぐことは、通常、成功につながりません。その価値があるデータベースは、これらの操作を非常に非常に得意な人が非常に迅速に実行できるように慎重に作成されています。

37
Blrfl

ここから同じ答え https://stackoverflow.com/a/49956113/10608


解決策:indexを文字列にします(文字列は本質的に無限の「任意の精度」を持っているためです)。または、intを使用する場合は、indexを1ではなく100増やします。

このソリューションによって解決される問題(パフォーマンス/複雑さ):2つのソートされたアイテム間に「中間」の値はありません。

item      index
-----------------
gizmo     1
              <<------ Oh no! no room between 1 and 2.
                       This requires incrementing _every_ item after it
gadget    2
gear      3
toolkit   4
box       5

代わりに、次のようなことを行います(以下のより良い解決策を使用)。

item      index
-----------------
gizmo     100
              <<------ Sweet :). I can re-order 99 (!) items here
                       without having to change anything else
gadget    200
gear      300
toolkit   400
box       500

さらに良い点:Jiraがこの問題を解決する方法を次に示します。それらの「ランク」(あなたがインデックスと呼ぶもの)は、ランク付けされたアイテム間の大量の呼吸の余地を可能にする文字列値です。

これは私が使用するjiraデータベースの実際の例です

   id    | jira_rank
---------+------------
 AP-2405 | 0|hzztxk:
 ES-213  | 0|hzztxs:
 AP-2660 | 0|hzztzc:
 AP-2688 | 0|hzztzk:
 AP-2643 | 0|hzztzs:
 AP-2208 | 0|hzztzw:
 AP-2700 | 0|hzztzy:
 AP-2702 | 0|hzztzz:
 AP-2411 | 0|hzztzz:i
 AP-2440 | 0|hzztzz:r

この例に注意してくださいhzztzz:i。文字列ランクの利点は、2つのアイテム間のスペースが足りなくなることです。still他のアイテムを再度ランク付けする必要はありません。フォーカスを絞り込むために、文字列にさらに文字を追加し始めるだけです。

29
Alexander Bird

「しかしそれはかなり非効率的だと思われる」

あなたは測定でしたか?それとも単なる推測ですか?証拠なしにそのような仮定をしないでください。

「リストごとに20から50アイテム」

正直なところ、それは「たくさんのアイテム」ではありません。

「ポジションカラム」アプローチを使用することをお勧めします(それが最も簡単な実装である場合)。このような小さなリストサイズの場合、実際のパフォーマンスの問題が発生する前に、不必要な最適化を開始しないでください。

16
Doc Brown

以前の(または次の)値を参照するために自己参照を使用している人を見てきましたが、繰り返しになりますが、リスト内の他の多くの項目を更新する必要があるようです。

どうして?たとえば、列(listID、itemID、nextItemID)を使用したリンクリストテーブルのアプローチを取るとします。

リストに新しいアイテムを挿入すると、1つの挿入と1つの変更された行がかかります。

アイテムの再配置には3行の変更が必要です(移動するアイテム、その前のアイテム、新しい場所の前のアイテム)。

アイテムを削除すると、1行が削除され、1行が変更されます。

これらの費用は、リストに10個のアイテムがある場合でも、10,000個のアイテムがある場合でも同じです。 3つすべてのケースで、ターゲット行が最初のリストアイテムである場合、変更は1つ少なくなります。 lastリストアイテムをより頻繁に操作する場合は、次のアイテムではなくprevItemIDを保存すると便利です。

14
sqweek

これは実際にはスケールの問題であり、ユースケースです。

リストにはいくつのアイテムが必要ですか?数百万の場合、10進法のルートを使用するのは明らかな方法だと思います。

6の場合、整数の再番号付けは当然の選択です。 ■また、質問はhowリストまたは再構成です。上矢印と下矢印を使用している場合(一度に1スロットずつ上または下に移動)、iは整数を使用し、移動時に前(または次)と入れ替えます。

また、どのくらいの頻度でコミットしますか?ユーザーが250の変更を行うことができれば、すぐにコミットできます。

tl; dr:詳細情報が必要です


編集:「ウィッシュリスト」は多くの小さなリストのように聞こえます(仮定、これは誤りである可能性があります)。 (各リストには独自の位置が含まれています)

6
Morons

OK最近私はこのトリッキーな問題に直面しています。このQ&Aポストのすべての回答が多くのインスピレーションを与えてくれました。私の見たところ、各ソリューションには長所と短所があります。

  • positionフィールドが隙間なく連続している必要がある場合は、基本的にリスト全体を並べ替える必要があります。これはO(N)操作です。利点は、クライアント側が注文を取得するために特別なロジックを必要としないことです。

  • O(N)演算を避けたいが、正確なシーケンスを維持する場合、アプローチの1つは「自己参照を使用して前の(または次の)値を参照する」ことです。これは教科書リンクリストのシナリオです。設計上、「リスト内の他のアイテムの多く」は発生しません。ただし、これにはクライアント側(Webサービスまたはモバイルアプリ)がリンクを実装する必要があります順序を導出するための走査論理をリストします。

  • 一部のバリエーションは、参照、つまりリンクリストを使用しません。彼らは注文全体をJSON-array-in-a-string [5,2,1,3,...]などの自己完結型のblobとして表すことを選択します。その後、そのような注文は別の場所に保管されます。このアプローチには、クライアント側のコードがその分離された順序BLOBを維持する必要があるという副作用もあります。

  • 多くの場合、実際に正確な順序を保存する必要はありません。各レコード間の相対的なランクを維持する必要があるだけです。したがって、順次レコード間のギャップを許容できます。バリエーションには次のものが含まれます。(1)100、200、300などのギャップのある整数を使用しますが、ギャップがすぐになくなり、リカバリプロセスが必要になります。 (2) decimalを使用 これには自然なギャップが伴いますが、最終的な精度の制限に耐えられるかどうかを判断する必要があります。 (3) この答え で説明されている文字列ベースのランクを使用しますが、 トリッキーな実装のトラップ に注意してください。

  • 本当の答えは「それは依存する」ことができます。ビジネス要件を再確認します。例えば、欲しいものリストのシステムなら、個人的には「必須」「良い」「多分後で」といった数ランクで整理し、特に明記せずにプレゼントするシステムを使っています。各ランク内で注文します。それが配達システムであるならば、あなたは配達時間を大まかなランクとしてうまく使うことができます。そして、それは自然なギャップ(そして、配達が同時に起こらないので自然な衝突防止)を伴います。あなたのマイレージは異なる場合があります。

5
RayLuo

目的が、並べ替え操作ごとのデータベース操作の数を最小限にすることである場合:

仮定して

  • すべてのショッピングアイテムは、32ビット整数で列挙できます。
  • ユーザーのウィッシュリストには最大サイズの制限があります。 (私はいくつかの人気のあるウェブサイトを制限として20から40のアイテムを使用するのを見ました)

ユーザーの並べ替えられたウィッシュリストを、整数(整数配列)のパックされたシーケンスとして1つの列に格納します。ウィッシュリストが並べ替えられるたびに、配列全体(単一行、単一列)が更新されます。これは、単一のSQL更新で実行されます。

https://www.postgresql.org/docs/current/static/arrays.html


目的が異なる場合は、「ポジションカラム」アプローチを使用してください。


「速度」については、ストアドプロシージャのアプローチのベンチマークを確認してください。 1つのウィッシュリストのシャッフルに対して20+separate更新を発行するのは遅い場合がありますが、ストアドプロシージャを使用して高速な方法がある場合があります。

3
rwong

位置列には浮動小数点数を使用します。

次に、「移動した」行の位置列のみを変更してリストを並べ替えることができます。

基本的に、ユーザーが「赤」を「青」の後で「黄色」の前に配置したい場合

次に、計算する必要があります

red.position = ((yellow.position - blue.position) / 2) + blue.position

数百万回の再配置後、浮動小数点数が非常に小さくなり、「間に」がなくなる場合がありますが、これは、ユニコーンを見つけるのとほぼ同じくらい可能性があります。

たとえば、最初のギャップが1000の整数フィールドを使用してこれを実装できます。したがって、最初のoredringは1000-> blue、2000-> Yellow、3000-> Redになります。青の後に赤を「移動」すると、1000->青、1500->赤、2000->黄色になります。

問題は、最初のギャップが1000のように大きく、わずか10移動すると、1000-> blue、1001-puce、1004-> biege ......のような状況になります。リスト全体の番号を付け直さずに、「blue」の後に何かを挿入します。浮動小数点数を使用すると、2つの位置の間に常に「中間」ポイントが存在します。

3
James Anderson

はい、質問はかなり古く、すでにいくつかの回答があります。それでも、ここで提供されるすべてのソリューションはかなり複雑です。もっと簡単なものはどうですか?

元の質問はウィッシュリストに関するものです。通常、アイテムの数は数十、おそらく数百ですが、数千ではありません。次に、並べ替え順序をシリアル化された配列として単一のテキストフィールドに格納しませんか?挿入、更新、削除は、この方法で1つの追加レコードにのみ影響します。

シリアル化された配列が十分ではない場合は、常にJSONフィールドにすることができますが、データベースで変更されているセルは1つだけです。

2

OPはLinked-Listを使用して並べ替え順序を保存するという概念について簡単に触れましたが、アイテムが頻繁に並べ替えられる場合には多くの利点があります。

以前の(または次の)値を参照するために自己参照を使用している人を見てきましたが、繰り返しになりますが、リスト内の他の多くの項目を更新する必要があるようです。

事は-しないでください!リンクリストを使用する場合、挿入、削除、並べ替えはO(1)操作であり、データベースによる参照整合性により、破損した参照、孤立したレコード、またはループがないことが保証されます。

次に例を示します。

_CREATE TABLE Wishlists (
  WishlistId int           NOT NULL IDENTITY(1,1) PRIMARY KEY,
  [Name]     nvarchar(200) NOT NULL
);

CREATE TABLE WishlistItems (
  ItemId     int           NOT NULL IDENTITY(1,1),
  WishlistId int           NOT NULL,
  Text       nvarchar(200) NOT NULL,
  SortAfter  int               NULL,

  CONSTRAINT PK_WishlistItem PRIMARY KEY ( ItemId, WishlistId ),
  CONSTRAINT FK_Wishlist_WishlistItem FOREIGN KEY ( WishlistId ) REFERENCES Wishlists ( WishlistId ),
  CONSTRAINT FK_Sorting FOREIGN KEY ( SortAfter, WishlistId ) REFERENCES WishlistItems ( ItemId, WishlistId )
);

CREATE UNIQUE INDEX UX_Sorting ON WishlistItems ( SortAfter, WishlistId );

 -----

SET IDENTITY_INSERT Wishlists ON;

INSERT INTO Wishlists ( WishlistId, [Name] ) VALUES
  ( 1, 'Wishlist 1' ),
  ( 2, 'Wishlist 2' );

SET IDENTITY_INSERT Wishlists OFF;

SET IDENTITY_INSERT WishlistItems ON;

INSERT INTO WishlistItems ( ItemId, WishlistId, [Text], SortAfter ) VALUES
( 1, 1, 'One', NULL ),
( 2, 1, 'Two', 1 ),
( 3, 1, 'Three', 2 ),
( 4, 1, 'Four', 3 ),
( 5, 1, 'Five', 4 ),
( 6, 1, 'Six', 5 ),
( 7, 1, 'Seven', 6 ),
( 8, 1, 'Eight', 7 );

SET IDENTITY_INSERT WishlistItems OFF;
_

次の点に注意してください。

  • _FK_Sorting_で複合主キーと外部キーを使用して、誤ってアイテムが誤った親アイテムを参照するのを防ぎます。
  • _UNIQUE INDEX UX_Sorting_は2つの役割を果たします:
    • 単一のNULL値を許可するため、各リストに含めることができる「ヘッド」アイテムは1つだけです。
    • (重複するSortAfter値を防止することにより)2つ以上の項目が同じソート場所にあると主張することを防ぎます。

このアプローチの主な利点:

  • intまたはrealベースの並べ替え順序のように、頻繁に並べ替えた後に項目間のスペースが不足するように、再調整やメンテナンスは必要ありません。
  • 再注文するアイテム(およびその兄弟)のみを更新する必要があります。

ただし、このアプローチには欠点があります。

  • 単純な_ORDER BY_。を実行できないため、このリストを再帰CTEを使用してSQLでのみソートできます。
    • 回避策として、CTEを使用するラッパーVIEWまたはTVFを作成して、インクリメントするソート順を含む派生を追加できますが、これは大規模な操作で使用するとコストがかかります。
  • リストを表示するには、リスト全体をプログラムにロードする必要があります。SortAfter列はプログラムにロードされていないアイテムを参照するため、行のサブセットを操作することはできません。
    • ただし、複合主キーにより、リストのすべての項目を簡単にロードできます(つまり、_SELECT * FROM WishlistItems WHERE WishlistId = @wishlistToLoad_を実行するだけです)。
  • _UX_Sorting_が有効になっているときに操作を実行するには、DBMSによる遅延制約のサポートが必要です。
    • i.e。このアプローチの理想的な実装は、遅延可能な制約とインデックスのサポートが追加されるまで、SQL Serverでは機能しません。
    • 回避策は、一意のインデックスをフィルターインデックスにして、列で複数のNULL値を許可することです。これは、残念ながらリストcouldに複数のHEAD項目があることを意味します。
      • この回避策は、3番目の列Stateを追加することです。これは、リストアイテムが「アクティブ」かどうかを宣言する単純なフラグであり、一意のインデックスは非アクティブアイテムを無視します。
    • これは、1990年代にサポートするためにSQL Serverで使用されていたものであり、そのサポートが不可解に削除されました。

回避策1:簡単な_ORDER BY_を実行する機能が必要です。

以下は、SortOrder列を追加する再帰CTEを使用するVIEWです。

_CREATE VIEW OrderableWishlistItems AS 

    WITH c ( ItemId, WishlistId, [Text], SortAfter, SortOrder )
    AS
    (
        SELECT
              ItemId, WishlistId, [Text], SortAfter, 1 AS SortOrder
        FROM
              WishlistItems
        WHERE
              SortAfter IS NULL

        UNION ALL

        SELECT
              i.ItemId, i.WishlistId, i.[Text], i.SortAfter, c.SortOrder + 1
        FROM
              WishlistItems AS i
              INNER JOIN c ON
                  i.WishlistId = c.WishlistId
                  AND
                  i.SortAfter = c.ItemId
    )
    SELECT
        ItemId, WishlistId, [Text], SortAfter, SortOrder
    FROM
        c;
_

このビューは、_ORDER BY_を使用して値をソートする必要がある他のクエリで使用できます。

_Query:

    SELECT * FROM OrderableWishlistItems

Results:

    ItemId  WishlistId  Text        SortAfter   SortOrder
    1       1           One         (null)      1
    2       1           Two             1       2
    3       1           Three           2       3
    4       1           Four            3       4
    5       1           Five            4       5
    6       1           Six             5       6
    7       1           Seven           6       7
    8       1           Eight           7       8
_

回避策2:操作の実行時の_UNIQUE INDEX_違反制約の防止:

State列をWishlistItemsテーブルに追加します。この列はHIDDENとしてマークされているため、たとえばEntity FrameworkなどのほとんどのORMツールには、モデルの生成時に含まれません。

_CREATE TABLE WishlistItems (
  ItemId     int           NOT NULL IDENTITY(1,1),
  WishlistId int           NOT NULL,
  Text       nvarchar(200) NOT NULL,
  SortAfter  int               NULL,
  [State]    bit           NOT NULL HIDDEN,

  CONSTRAINT PK_WishlistItem PRIMARY KEY ( ItemId, WishlistId ),
  CONSTRAINT FK_Wishlist_WishlistItem FOREIGN KEY ( WishlistId ) REFERENCES Wishlists ( WishlistId ),
  CONSTRAINT FK_Sorting FOREIGN KEY ( SortAfter, WishlistId ) REFERENCES WishlistItems ( ItemId, WishlistId )
);

CREATE UNIQUE INDEX UX_Sorting ON WishlistItems ( SortAfter, WishlistId ) WHERE [State] = 1;
_

オペレーション:

リストの末尾に新しいアイテムを追加します。

  1. 最初にリストをロードして、リストの現在の最後の項目のItemIdを決定し、_@tailItemId_に格納するか、SELECT MAX( SortOrder ) FROM OrderableWishlistItems WHERE WishlistId = @listIdを使用します。
  2. INSERT INTO WishlistItems ( WishlistId, [Text], SortAfter ) VALUES ( @listId, @text, @tailItemId )

アイテム4をアイテム7の下に再注文

_BEGIN TRANSACTION

    DECLARE @itemIdToMove int = 4
    DECLARE @itemIdToMoveAfter int = 7

    DECLARE @prev int = ( SELECT SortAfter FROM WishlistItems WHERE ItemId = @itemIdToMove )

    UPDATE WishlistItems SET [State] = 0 WHERE ItemId IN ( @itemIdToMove , @itemIdToMoveAfter )

    UPDATE WishlistItems SET [SortAfter] = @itemIdToMove WHERE ItemId = @itemIdToMoveAfter 

    UPDATE WishlistItems SET [SortAfter] = @prev WHERE SortAfter = @itemIdToMove 

    UPDATE WishlistItems SET [State] = 1 WHERE ItemId IN ( @itemIdToMove, @itemIdToMoveAfter )

COMMIT;
_

リストの中央からアイテム4を削除します。

アイテムがリストの末尾にある場合(つまり、NOT EXISTS ( SELECT 1 FROM WishlistItems WHERE SortAfter = @itemId ))、単一のDELETEを実行できます。

アイテムの後にソートされたアイテムがある場合は、_State = 1;_を設定する代わりに、DELETEを後で変更することを除いて、アイテムの並べ替えと同じ手順を実行します。

_BEGIN TRANSACTION

    DECLARE @itemIdToRemove int = 4

    DECLARE @prev int = ( SELECT SortAfter FROM WishlistItems WHERE ItemId = @itemIdToRemove )

    UPDATE WishlistItems SET [State] = 0 WHERE ItemId = @itemIdToRemove

    UPDATE WishlistItems SET [SortAfter] = @prev WHERE SortAfter = @itemIdToRemove

    DELETE FROM WishlistItems WHERE ItemId = @itemIdToRemove

COMMIT;
_
1
Dai