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


54

ユーザーがさまざまなウィッシュリストにアイテムを追加できるウィッシュリストシステムに取り組んでおり、ユーザーが後でアイテムを再注文できるようにする予定です。これをデータベースに保存して高速で混乱を起こさない最善の方法については本当にわかりませんものをきれいにするため)。

最初にpositionコラムを試しましたが、アイテムを移動するときに他のすべてのアイテムの位置の値を変更する必要があるのは非常に効率が悪いようです。

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

私が見た別の解決策は、小数を使用し、それらの間の隙間にアイテムを貼り付けるだけです。これはこれまでの最良の解決策のように思えますが、より良い方法が必要だと確信しています。

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

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

助けてください!


少しベンチマークを行い、IOまたはデータベースがボトルネックになるかどうか教えてください。
rwong

stackoverflowに関する関連質問。
ジョルダン

自己参照を使用すると、リスト内のある場所から別の場所にアイテムを移動するときに、2つのアイテムを更新するだけで済みます。参照してくださいen.wikipedia.org/wiki/Linked_list
ピーター・B

うーん、リンクリストが回答でほとんど注目されていない理由はわかりません。
クリスティアン・ウェスター

回答:


32

最初に、10進数で巧妙なことをしようとしないでください。 REALかつDOUBLE PRECISION不正確であり、適切にあなたがそれらに入れて何を表していないことがあります。 NUMERIC正確ですが、正しい一連の動きは精度を使い果たし、実装はひどく壊れます。

移動を単一のアップとダウンに制限すると、操作全体が非常に簡単になります。順番に番号が付けられたアイテムのリストでは、アイテムの位置をデクリメントし、前のデクリメントが何であれ位置番号をインクリメントすることにより、アイテムを上に移動できます。(言い換えれば、アイテム5はになり、アイテムが4何に4なるかは5、事実上、モロンが彼の答えで説明したようにスワップになります。)アイテムを下に移動するのは逆です。リストと位置を一意に識別するものでテーブルにインデックスを付けると、UPDATE非常に高速に実行されるトランザクション内で2つのs を使用してそれを行うことができます。ユーザーが超人的な速度でリストを再配置しない限り、これは大きな負荷にはなりません。

ドラッグアンドドロップの移動(たとえば、6アイテム9との間に位置するようにアイテムを移動する10)は少し複雑で、新しい位置が古い位置の上か下かによって異なる方法で実行する必要があります。上記の例では9、よりも大きいすべての位置を増分して穴を開け、アイテム6の位置を新しい位置に更新してから、空いているスポットを埋めるために10すべてより大きい位置を減分する必要があります6。前に説明したのと同じインデックス付けで、これは迅速になります。トランザクションが触れる行の数を最小限に抑えることで、実際にこれを説明よりも少し速くすることができますが、それはボトルネックがあることを証明できるまでは必要ない微最適化です。

いずれにせよ、自家製の、あまりにも賢い半分のソリューションでデータベースをしのぐことは、通常成功につながりません。非常に優れた人々によって、これらの操作を非常に迅速に行うために、その価値のあるデータベースが注意深く作成されています。


これは、何億年も前にあったプロジェクト入札準備システムで私がまさにそれを処理した方法です。Accessでも、この更新はかなり高速でした。
HLGEM

Blrfl、説明をありがとう!後者のオプションを実行しようとしましたが、リストの中央からアイテムを削除すると、位置にギャップが残ることがわかりました(かなり単純な実装でした)。このようなギャップを作成するのを避ける簡単な方法はありますか、または何かを再注文するたびに手動で行う必要がありますか(実際に管理する必要がある場合)?
トムブルーノリ

2
@TomBrunoli:確かに言う前に実装について少し考える必要がありますが、トリガーを使用して自動的に番号の付け直しのほとんどまたはすべてを実行できる可能性があります。たとえば、アイテム7を削除すると、トリガーは、削除が行われた後、7より大きい番号の同じリスト内のすべての行をデクリメントします。挿入でも同じことが行われます(項目7を挿入すると、すべての行7以上が増分されます)。更新のトリガー(たとえば、アイテム3を9から10に移動する)はやや複雑になりますが、確かに実行可能な領域内にあります。
Blrfl

実際にトリガーを実際に調べたことはありませんでしたが、それは良い方法のように思えます。
トムブルーノリ

1
@TomBrunoli:トリガーを使用してこれを行うと、カスケードが発生することがあります。トランザクション内のすべての変更を含むストアドプロシージャは、これに適したルートです。
Blrfl

15

ここから同じ答え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つのアイテムの間にスペースがなくなることです。それでも、他のアイテムを再ランク付けする必要ありません。文字列にさらに文字を追加して、フォーカスを絞り込みます。


1
私は単一のレコードを更新するだけでこれを行う方法を考え出そうとしましたが、この答えは頭の中で考えていた解決策を非常によく説明しています。
NSjonas

13

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

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

リストに新しいアイテムを挿入するには、1回の挿入と1行の変更が必要です。

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

アイテムを削除するには、1行の削除と1行の変更が必要です。

これらのコストは、リストに10アイテムまたは10,000アイテムがあるかどうかに関係なく同じままです。3つのケースすべてで、ターゲット行が最初のリストアイテムである場合、変更は1つ少なくなります。最後のリストアイテムをより頻繁に操作する場合は、次にではなくprevItemIDを保存する方が有益な場合があります。


10

「しかし、それは非常に効率が悪いようです」

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

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

正直なところ、それは「たくさんのアイテム」ではなく、私にはほんの少ししか聞こえません。

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


6

これは、実際には規模とユースケースの問題です。

リストにはいくつのアイテムが期待されますか?数百万の場合、私は10進ルートをゴングが明らかだと思います。

6の場合、整数の再番号付けは明らかな選択です。■質問は、リストをどのように並べ替えるかです。上矢印と下矢印を使用している場合(一度に1スロットずつ上下に移動する場合)、iは整数を使用し、移動中に前(または次)と交換します。

また、どのくらいの頻度でコミットしますか?ユーザーが250の変更を行ってから一度にコミットできる場合、再度番号を付けて整数を言うよりも...

tl; dr:さらに情報が必要です。


編集:「ウィッシュリスト」は多くの小さなリストのように聞こえます(仮定、これは間違っているかもしれません)。(各リストには独自の位置が含まれています)


質問をもう少しコンテキストで更新します
トムブルーノリ

精度が制限されているため、小数は機能せず、挿入された各アイテムは潜在的に1ビットを
消費し

3

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

仮定して

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

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

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


目的が異なる場合は、「列の配置」アプローチを使用してください。


「速度」に関しては、ストアドプロシージャアプローチのベンチマークを必ず行ってください。1つのウィッシュリストシャッフルに対して20以上の個別の更新を発行するのは遅いかもしれませんが、ストアドプロシージャを使用する高速な方法があるかもしれません。


3

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

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

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

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

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

  • 本当の答えは「依存する」です。ビジネス要件を再検討してください。たとえば、それがウィッシュリストシステムである場合、個人的には「必須」、「良い」、「多分後」などの少数のランクで編成されたシステムを使用し、特定のアイテムを提示しません各ランク内の順序。配信システムの場合、配信時間を自然なギャップを伴う大まかなランクとして使用できます(配信が同時に発生しないため、自然な競合防止)。あなたのマイレージは異なる場合があります。


2

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

その後、「移動」行の位置列のみを変更してリストの順序を変更できます。

基本的に、ユーザーが「赤」を「青」の後、「黄」の前に配置する場合

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

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

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

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

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


4
floatに基づくデータベースでのインデックス付けと並べ替えは、int よりも高価です。Intsも素晴らしい順序型です...クライアントでソートできるようにビットとして送信する必要はありません(印刷時に同じように表示されるが、ビット値が異なる2つの数値の差)。

ただし、intを使用するスキームでは、順序が変わるたびにリスト内のすべて/ほとんどの行を更新する必要があります。フロートを使用すると、移動した行のみを更新します。また、「intよりも浮動小数点数が高い」ことは、使用される実装とハードウェアに大きく依存します。確かに、関係する余分なCPUは、行とそれに関連付けられたインデックスを更新するために必要なCPUと比較して重要ではありません。
ジェームズアンダーソン

5
反対者にとって、このソリューションはまさにTrello(trello.com)が行うことです。Chromeデバッガーを開き、リオーダーの前後にjson出力を比較し(カードをドラッグ/ドロップ)、-を取得します"pos": 1310719, + "pos": 638975.5。公平を期すために、ほとんどの人は400万のエントリを含むtrelloリストを作成しませんが、Trelloのリストサイズとユースケースは、ユーザーがソート可能なコンテンツではかなり一般的です。また、ユーザーがソート可能なものは、高いパフォーマンスとはほとんど関係がありません。特に、データベースがIOパフォーマンスによってほとんど制約されていることを考慮すると、intとfloatのソート速度は重要ではありません。
ゼルク

1
@PieterB「64ビット整数を使用しない理由」については、開発者にとってはほとんど人間工学です。平均フロートの1.0を超えるのとほぼ同じビット深度<1.0であるため、 'position'列をデフォルトで1.0に設定し、0.5、0.25、0.75を2倍にするのと同じくらい簡単に挿入できます。整数の場合、デフォルトは2 ^ 30程度である必要があり、デバッグするときを考えるのが少し難しくなります。4073741824は496359787よりも大きいですか?数字のカウントを開始します。
ゼルク

1
さらに、数字間のスペースが足りなくなった場合は、修正するのはそれほど難しくありません。それらの1つを移動します。しかし、重要なことは、これがベストエフォート方式で機能することであり、異なるパーティ(例えば、トレロ)による多くの同時編集を処理します。2つの数値を分割できます。たぶん、少しランダムなノイズをまき散らすこともできます。他の誰かが同じことを同時にやったとしても、グローバルな注文があり、取得するためにトランザクション内にINSERTする必要はありませんそこ。
ゼルク
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.