ユーザーがさまざまなウィッシュリストにアイテムを追加できるウィッシュリストシステムに取り組んでいます。ユーザーが後でアイテムを再注文できるようにする予定です。私はこれをデータベースに保存して高速で混乱に陥らないようにする最善の方法について本当に確信がありません(このアプリはかなり大規模なユーザーベースで使用されるため、ダウンさせたくありません)ものを片付けるために)。
私は最初にposition
列を試しましたが、他のアイテムの位置の値を変更する場合、それらを変更しなければならないのは非常に非効率的です。
以前の(または次の)値を参照するために自己参照を使用している人を見てきましたが、繰り返しになりますが、リスト内の他の多くの項目を更新する必要があるようです。
私が見た別の解決策は、10進数を使用し、それらの間にアイテムを貼り付けることです。これは、これまでのところ最良の解決策のようですが、もっと良い方法があるはずだと確信しています。
典型的なリストには最大約20アイテムが含まれると思います。おそらく50に制限します。並べ替えはドラッグアンドドロップを使用し、おそらく競合状態などを防ぐためにバッチで行われます。 ajaxリクエスト。必要に応じて、(herokuで)postgresを使用しています。
誰かアイデアはありますか?
助けてくれて乾杯!
まず、10進数を巧妙に操作しようとしないでください。 REAL
およびDOUBLE PRECISION
は不正確であり、入力した内容を適切に表していない可能性があります。 NUMERIC
は正確ですが、正しい順序の移動では精度が不足し、実装がうまく機能しなくなります。
動きをシングルアップとダウンに制限することで、操作全体が非常に簡単になります。連続番号の付いたアイテムのリストの場合、アイテムを上に移動するには、その位置をデクリメントし、前のデクリメントで生じた位置番号をインクリメントします。 (言い換えれば、アイテム5
は4
になり、アイテム4
は5
になります。これは、Moronsが彼の回答で説明したように交換されます。)下に移動すると、反対。リストと位置を一意に識別するものでテーブルにインデックスを付けます。非常に高速に実行されるトランザクション内で2つのUPDATE
sを使用してそれを行うことができます。ユーザーがリストを超人的な速度で再配置しない限り、これによって負荷が大きくなることはありません。
ドラッグアンドドロップ移動(たとえば、アイテム6
を移動して、アイテム9
と10
の間に移動する)は少しトリッキーであり、新しい位置が上にあるかどうかによって異なる方法で実行する必要がありますまたは古いものの下。上記の例では、9
より大きいすべての位置をインクリメントし、アイテム6
の位置を新しい10
に更新してから、すべての位置をデクリメントして、穴を開ける必要があります。 6
は、空いた場所を埋めます。前に説明したのと同じ索引付けを使用すると、これは迅速になります。トランザクションが関係する行の数を最小限に抑えることで、これを実際に私が説明したよりも少し速くすることができます。
どちらの方法でも、自作の、あまりにも賢い半熟のソリューションでデータベースをしのぐことは、通常、成功につながりません。その価値があるデータベースは、これらの操作を非常に非常に得意な人が非常に迅速に実行できるように慎重に作成されています。
ここから同じ答え 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他のアイテムを再度ランク付けする必要はありません。フォーカスを絞り込むために、文字列にさらに文字を追加し始めるだけです。
「しかしそれはかなり非効率的だと思われる」
あなたは測定でしたか?それとも単なる推測ですか?証拠なしにそのような仮定をしないでください。
「リストごとに20から50アイテム」
正直なところ、それは「たくさんのアイテム」ではありません。
「ポジションカラム」アプローチを使用することをお勧めします(それが最も簡単な実装である場合)。このような小さなリストサイズの場合、実際のパフォーマンスの問題が発生する前に、不必要な最適化を開始しないでください。
以前の(または次の)値を参照するために自己参照を使用している人を見てきましたが、繰り返しになりますが、リスト内の他の多くの項目を更新する必要があるようです。
どうして?たとえば、列(listID、itemID、nextItemID)を使用したリンクリストテーブルのアプローチを取るとします。
リストに新しいアイテムを挿入すると、1つの挿入と1つの変更された行がかかります。
アイテムの再配置には3行の変更が必要です(移動するアイテム、その前のアイテム、新しい場所の前のアイテム)。
アイテムを削除すると、1行が削除され、1行が変更されます。
これらの費用は、リストに10個のアイテムがある場合でも、10,000個のアイテムがある場合でも同じです。 3つすべてのケースで、ターゲット行が最初のリストアイテムである場合、変更は1つ少なくなります。 lastリストアイテムをより頻繁に操作する場合は、次のアイテムではなくprevItemIDを保存すると便利です。
これは実際にはスケールの問題であり、ユースケースです。
リストにはいくつのアイテムが必要ですか?数百万の場合、10進法のルートを使用するのは明らかな方法だと思います。
6の場合、整数の再番号付けは当然の選択です。 ■また、質問はhowリストまたは再構成です。上矢印と下矢印を使用している場合(一度に1スロットずつ上または下に移動)、iは整数を使用し、移動時に前(または次)と入れ替えます。
また、どのくらいの頻度でコミットしますか?ユーザーが250の変更を行うことができれば、すぐにコミットできます。
tl; dr:詳細情報が必要です
編集:「ウィッシュリスト」は多くの小さなリストのように聞こえます(仮定、これは誤りである可能性があります)。 (各リストには独自の位置が含まれています)
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) この答え で説明されている文字列ベースのランクを使用しますが、 トリッキーな実装のトラップ に注意してください。
本当の答えは「それは依存する」ことができます。ビジネス要件を再確認します。例えば、欲しいものリストのシステムなら、個人的には「必須」「良い」「多分後で」といった数ランクで整理し、特に明記せずにプレゼントするシステムを使っています。各ランク内で注文します。それが配達システムであるならば、あなたは配達時間を大まかなランクとしてうまく使うことができます。そして、それは自然なギャップ(そして、配達が同時に起こらないので自然な衝突防止)を伴います。あなたのマイレージは異なる場合があります。
目的が、並べ替え操作ごとのデータベース操作の数を最小限にすることである場合:
仮定して
ユーザーの並べ替えられたウィッシュリストを、整数(整数配列)のパックされたシーケンスとして1つの列に格納します。ウィッシュリストが並べ替えられるたびに、配列全体(単一行、単一列)が更新されます。これは、単一のSQL更新で実行されます。
https://www.postgresql.org/docs/current/static/arrays.html
目的が異なる場合は、「ポジションカラム」アプローチを使用してください。
「速度」については、ストアドプロシージャのアプローチのベンチマークを確認してください。 1つのウィッシュリストのシャッフルに対して20+separate更新を発行するのは遅い場合がありますが、ストアドプロシージャを使用して高速な方法がある場合があります。
位置列には浮動小数点数を使用します。
次に、「移動した」行の位置列のみを変更してリストを並べ替えることができます。
基本的に、ユーザーが「赤」を「青」の後で「黄色」の前に配置したい場合
次に、計算する必要があります
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つの位置の間に常に「中間」ポイントが存在します。
はい、質問はかなり古く、すでにいくつかの回答があります。それでも、ここで提供されるすべてのソリューションはかなり複雑です。もっと簡単なものはどうですか?
元の質問はウィッシュリストに関するものです。通常、アイテムの数は数十、おそらく数百ですが、数千ではありません。次に、並べ替え順序をシリアル化された配列として単一のテキストフィールドに格納しませんか?挿入、更新、削除は、この方法で1つの追加レコードにのみ影響します。
シリアル化された配列が十分ではない場合は、常にJSONフィールドにすることができますが、データベースで変更されているセルは1つだけです。
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でのみソートできます。VIEW
またはTVFを作成して、インクリメントするソート順を含む派生を追加できますが、これは大規模な操作で使用するとコストがかかります。SortAfter
列はプログラムにロードされていないアイテムを参照するため、行のサブセットを操作することはできません。SELECT * FROM WishlistItems WHERE WishlistId = @wishlistToLoad
_を実行するだけです)。UX_Sorting
_が有効になっているときに操作を実行するには、DBMSによる遅延制約のサポートが必要です。NULL
値を許可することです。これは、残念ながらリストcouldに複数のHEAD項目があることを意味します。State
を追加することです。これは、リストアイテムが「アクティブ」かどうかを宣言する単純なフラグであり、一意のインデックスは非アクティブアイテムを無視します。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
_
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;
_
ItemId
を決定し、_@tailItemId
_に格納するか、SELECT MAX( SortOrder ) FROM OrderableWishlistItems WHERE WishlistId = @listId
を使用します。INSERT INTO WishlistItems ( WishlistId, [Text], SortAfter ) VALUES ( @listId, @text, @tailItemId )
。_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;
_
アイテムがリストの末尾にある場合(つまり、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;
_