Зберігання списку, який можна повторно замовити в базі даних


54

Я працюю над системою списку бажань, де користувачі можуть додавати елементи до своїх різних списків бажань, і я планую дозволити користувачам згодом повторно замовляти товари. Я не дуже впевнений у найкращому способі зберігати це в базі даних, залишаючись при цьому швидким і не перетворюючись на безлад (цей додаток буде використовуватися досить великою базою користувачів, тому я не хочу, щоб він знижувався прибирати речі).

Спочатку я спробував positionстовпчик, але здається, що це було б досить неефективно, коли б ви змінювали значення позиції кожного іншого елемента, коли ви переміщуєте їх.

Я бачив людей, які використовують самовідсилання для посилання на попереднє (або наступне) значення, але знову ж таки, здається, вам доведеться оновити цілу масу інших елементів у списку.

Ще одне рішення, яке я бачив, - це використання десяткових чисел та просто наклеювання предметів у проміжки між ними, що здається найкращим рішенням поки що, але я впевнений, що має бути кращий спосіб.

Я б сказав, що типовий список міститиме приблизно 20 або більше предметів, і я, ймовірно, обмежуватимуть його до 50. Повторне замовлення буде використовувати перетягування і, ймовірно, буде виконуватися партіями, щоб запобігти умовам перегонів і подібним прохання ajax. Я використовую postgres (на heroku), якщо це має значення.

Хтось має ідеї?

Будь ласка, будь-яка допомога!


Чи можете ви трохи зробити тестування та сказати, чи буде IO або Database вузьким місцем?
rwong

Пов'язане запитання про stackoverflow .
Йордао

За допомогою самостійної посилання, коли ви переміщуєте елемент з одного місця в списку до іншого, вам потрібно буде лише оновити 2 елементи. Дивіться en.wikipedia.org/wiki/Linked_list
Пітер Б

Гм, не впевнений, чому пов'язані списки навряд чи отримують уваги у відповідях.
Крістіан Вестербек

Відповіді:


32

По-перше, не намагайтеся робити щось розумне з десятковими числами, тому що вони будуть вас ошукати. REALі DOUBLE PRECISIONвони неточні і можуть не належним чином представляти те, що ви вкладаєте в них. NUMERICточна, але правильна послідовність рухів позбавить вас точності, і ваша реалізація погано зламається.

Обмеження рухів поодинці підйомів і падінь робить всю операцію дуже простою. Для списку елементів, що пронумеровані послідовно, ви можете перемістити елемент вгору, зменшуючи його положення та збільшуючи номер позиції, що б придумав попередній декремент. (Іншими словами, пункт 5стане 4і те , що елемент 4стає 5, фактично своп , як дебіли описав у своїй відповіді.) Рухаючись вниз буде навпаки. Індексуйте свою таблицю за будь-яким унікальним ідентифікатором списку та позиції, і ви можете зробити це за допомогою двох UPDATEs всередині транзакції, яка буде запускатися дуже швидко. Якщо ваші користувачі не переставляють свої списки з надлюдськими швидкостями, це не спричинить великого навантаження.

Переміщення за допомогою перетягування (наприклад, переміщення предмета, 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на 100 замість 1.

Проблема продуктивності полягає в наступному: між двома відсортованими елементами немає значень "між".

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, з якою я працюю

   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. Перевага рядкового рядка полягає в тому, що у вас не вистачає місця між двома елементами, вам все одно не потрібно переставляти нічого іншого. Ви просто починаєте додавати до рядка більше символів, щоб звузити фокус.


1
Я намагався придумати певний спосіб зробити це лише оновленням одного запису, і ця відповідь пояснює рішення, яке я дуже добре придумував.
NSjonas

13

Я бачив людей, які використовують самовідсилання для посилання на попереднє (або наступне) значення, але знову ж таки, здається, вам доведеться оновити цілу масу інших елементів у списку.

Чому? Скажімо, ви використовуєте підхід таблиці з пов'язаним списком зі стовпцями (listID, itemID, nextItemID).

Вставлення нового елемента в список коштує як одну вставку, так і одну змінену рядок.

Перестановка елемента коштує трьох змін рядків (елемент переміщується, елемент перед ним та елемент перед його новим розташуванням).

Видалення елемента коштує одного видалення та одного зміненого рядка.

Ці витрати залишаються однаковими незалежно від того, чи є в списку 10 предметів чи 10 000 позицій. У всіх трьох випадках є одна менша модифікація, якщо цільовий рядок є першим елементом списку. Якщо ви частіше працюєте над останнім елементом списку, можливо, вигідніше зберігати prevItemID, а не наступний.


10

"але здається, що це було б досить неефективно"

Ви це міряли ? Або це лише здогадка? Не робіть таких припущень без жодних доказів.

"Від 20 до 50 позицій у списку"

Чесно кажучи, для мене це не дуже багато предметів.

Я пропоную вам дотримуватися підходу "стовпець позиції" (якщо це для вас найпростіша реалізація). Для таких невеликих розмірів списку не починайте зайву оптимізацію, перш ніж у вас виникнуть реальні проблеми з продуктивністю


6

Це справді питання масштабу та використання випадку ..

Скільки предметів ви очікуєте в списку? Якщо мільйони, я вважаю, що гонг десятковий маршрут очевидний.

Якщо 6, то цілі числа перенумерування - це очевидний вибір. s Також питання полягає в тому, як списки чи перестановка. Якщо ви використовуєте стрілки вгору та вниз (рухаючись вгору або вниз по одному слоту за один раз), я б використовував цілі числа, а потім мінявся на попередній (або наступний) рух.

Крім того, як часто ви берете на себе зобов’язання, якщо користувач може вносити 250 змін, то здійснюють відразу, ніж я кажу цілі числа з перенумеруванням знову ...

tl; dr: потрібна додаткова інформація.


Редагувати: "Список побажань" звучить як безліч невеликих списків (припущення, це може бути помилковим). Тому я кажу Integer з перенумеруванням. (Кожен список містить свою позицію)


Я оновлю питання ще трохи контексту
Том Брунолі,

десяткові знаки не працюють, оскільки точність обмежена, і кожен вставлений елемент потенційно займає 1 біт
njzk2

3

Якщо метою є мінімізація кількості операцій з базою даних за операцію упорядкування:

Якщо припустити, що

  • Усі товари для покупок можна перерахувати за допомогою 32-бітних цілих чисел.
  • Для списку бажань користувача існує обмеження максимального розміру. (Я бачив, що деякі популярні веб-сайти використовують 20 - 40 позицій як обмеження)

Збережіть відсортований список бажань у вигляді упакованої послідовності цілих чисел (цілих масивів) в одному стовпчику. Кожен раз, коли список побажань упорядковується, весь масив (один рядок; один стовпець) оновлюється - що має виконуватися одним оновленням SQL.

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


Якщо мета інша, дотримуйтесь підходу "стовпчик позиції".


Щодо "швидкості", переконайтеся, що орієнтуєте підхід, що зберігається. Хоча надсилання 20+ окремих оновлень для одного списку бажань може бути повільним, можливо, існує швидкий спосіб використання збереженої процедури.


3

Гаразд Я останнім часом стикаюся з цією складною проблемою, і всі відповіді в цьому запитанні з питань відповідей дали багато натхнення. Як я бачу, у кожного рішення є свої плюси і мінуси.

  • Якщо positionполе має бути послідовним без пропусків, вам, в основному, потрібно буде повторно замовити весь список. Це операція O (N). Перевага полягає в тому, що для отримання замовлення стороні клієнта не буде потрібна спеціальна логіка.

  • Якщо ми хочемо уникнути операції O (N), але ВІДПОВІДЬ підтримувати точну послідовність, одним із підходів є використання "самонавідки для позначення попереднього (або наступного) значення". Це сценарій списку, пов'язаний з підручником. За задумом він НЕ буде містити "в списку багато інших елементів". Однак для клієнта це вимагає від клієнта (веб-сервісу чи, можливо, мобільного додатку) реалізувати логіку траверсу зв'язаного списку.

  • Деякі варіанти не використовують посилання, тобто пов'язаний список. Вони вирішують представляти весь порядок як автономна крапка, наприклад, JSON-масив-в-рядку [5,2,1,3,...]; такий порядок потім буде зберігатися в окремому місці. Цей підхід також має побічний ефект, вимагаючи від клієнтського коду для підтримки цього розділеного блоку замовлення.

  • У багатьох випадках нам не потрібно зберігати точний порядок, нам просто потрібно підтримувати відносну позицію серед кожного запису. Тому ми можемо дозволити прогалини між послідовними записами. Варіації включають в себе: (1) використання цілого числа з пробілами, такими як 100, 200, 300 ..., але ви швидко закінчитеся з пробілами, а потім знадобиться процес відновлення; (2) використання десяткових значень із природними прогалинами, але вам потрібно вирішити, чи можете ви жити з можливим обмеженням точності; (3) використовуючи рядовий ранг, як описано у цій відповіді, але будьте обережні хитрі пастки реалізації .

  • Справжня відповідь може бути "це залежить". Перегляньте вимоги вашого бізнесу. Наприклад, якщо це система списку побажань, особисто я б із задоволенням використовував систему, організовану лише декількома рядами, як "must-have", "good-to-have", "можливо, пізніше", а потім подавати предмети без конкретних порядку всередині кожного рангу. Якщо це система доставки, то ви можете дуже добре використовувати час доставки як грубу оцінку, яка має природний розрив (і природне запобігання конфліктам, оскільки одночасна доставка не відбудеться). Ваш пробіг може відрізнятися.


2

Використовуйте номер з плаваючою комою для стовпчика позиції.

Потім можна упорядкувати список, змінивши лише стовпець позиції у рядку "переміщений".

В основному, якщо ваш користувач хоче розмістити "червоний" після "синього", але перед "жовтим"

Тоді вам просто потрібно прорахувати

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

Після декількох мільйонів повторних позицій у вас можуть бути настільки малі цифри з плаваючою комою, що немає "між" - але це приблизно так само, як і бачення єдинорога.

Ви можете реалізувати це за допомогою цілого поля з початковим проміжком, скажімо, 1000. Отже, ваш початковий oredring буде 1000-> синій, 2000-> жовтий, 3000-> червоний. Після "переміщення" Червоного на синє у вас буде 1000-> синій, 1500-> Червоний, 2000-> Жовтий.

Проблема полягає в тому, що при начебто великому початковому розриві в 1000 всього 10 рухів ви потрапите в ситуацію, як 1000-> синій, 1001-пуче, 1004-> биж ...... де ви більше не зможете щоб вставити що-небудь після "синього", не перенумеруючи весь список. Використовуючи номери з плаваючою комою, завжди буде точка "на півдорозі" між двома позиціями.


4
Індексація та сортування у даній базі на основі поплавців дорожче, ніж ints. Інти також є хорошим порядковим типом ... не потрібно надсилати як біти, щоб мати можливість сортувати на клієнті (різниця між двома числами, які роблять однаковими при друкуванні, але мають різні бітові значення).

Але будь-яка схема з використанням ints означає, що вам потрібно оновлювати всі / більшість рядків у списку щоразу, коли порядок змінюється. Використовуючи поплавці, ви оновлюєте лише рядок, який перемістився. Крім того, "плаває дорожче, ніж ints" дуже залежить від реалізації та використовуваного обладнання. Безумовно, додаткова задіяна процесора є незначною порівняно з процесором, необхідним для оновлення рядка та пов'язаних з ним індексів.
Джеймс Андерсон

5
Для найсайєрів це рішення саме те, що робить Trello ( trello.com ). Відкрийте хромований налагоджувач і відрізняйте вихід json від до / після переупорядкування (перетягніть / впустіть карту) і отримаєте - "pos": 1310719, + "pos": 638975.5. Якщо чесно, більшість людей не роблять списки трелло з 4 мільйонами записів у них, але розмір та використання списку Trello досить поширені для вмісту, який може сортувати користувач. І все, що може сортуватися користувачем, не має нічого спільного з високою продуктивністю, швидкість сортування int vs float є суперечливою для цього, особливо враховуючи, що бази даних здебільшого обмежені продуктивністю IO.
zelk

1
@PieterB Що стосується "чому не використовувати 64-бітове ціле", я б сказав, що це, головним чином, ергономіка для розробника. Приблизно стільки ж бітової глибини <1,0, скільки є> 1,0 для вашого середнього поплавця, тому ви можете за замовчуванням стовпець "позиція" до 1,0 та вставити 0,5, 0,25, 0,75 так само легко, як і подвоєння. З цілими числами за замовчуванням повинно бути 2 ^ 30 або близько того, трохи складніше думати, коли ви налагоджуєте. На 4073741824 більше 496359787? Почніть рахувати цифри.
zelk

1
Крім того, якщо ви коли-небудь потрапляєте на випадок, коли у вас не вистачає місця між номерами ... це не так важко виправити. Перемістіть один із них. Але важливо, що це працює найкращим чином, який обробляє багато паралельних редагувань різними сторонами (наприклад, trello). Ви можете розділити два числа, можливо, навіть розпорошити трохи випадкового шуму і вуаля, навіть якщо хтось інший робив те ж саме в той же час, все ще існує глобальний порядок, і вам не потрібно було ВСТАВИТИ всередині транзакції, щоб отримати там.
zelk
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.