Як зберігати впорядковану інформацію у реляційній базі даних


20

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

Приклад:

Скажіть, у мене є Плейлист, що складається з Пісень. Всередині моєї реляційної бази даних у мене є таблиця Playlists, що містить деякі метадані (ім'я, творець тощо). У мене також є таблиця з назвою Songs, що містить playlist_idінформацію про конкретні пісні (ім'я, виконавця, тривалість тощо).

За замовчуванням, коли нова композиція додається до списку відтворення, вона додається до кінця. При замовленні на Song-ID (висхідний), це буде порядок додавання. Але що робити, якщо користувач повинен мати можливість замовити пісні в списку відтворення?

Я придумав пару ідей, кожна зі своїми перевагами та недоліками:

  1. Стовпець order, який називається , є цілим числом . Коли пісня переміщена, порядок всіх пісень між її старим та новим положенням змінюється, щоб відобразити зміну. Недолік цього полягає в тому, що потрібно робити багато запитів щоразу при переміщенні пісні, а алгоритм переміщення не такий тривіальний, як в інших варіантах.
  2. Стовпець order, що називається , є десятковою ( NUMERIC). Коли пісня переміщена, їй присвоюється значення плаваючої точки між двома сусідніми номерами. Недолік: Десяткові поля займають більше місця, і можливо, не вистачить точності, якщо не буде обережно перерозподілити діапазон після кожні кілька змін.
  3. Іншим способом було б мати поле previousі nextполе, яке посилається на інші Пісні. (або є NULL у випадку першої, відповідно останньої пісні в списку відтворення прямо зараз; в основному ви створюєте пов'язаний список ). Недолік: Запити, такі як "знайти X-цю пісню у списку", більше не є постійним часом, а натомість лінійного часу.

Яка з цих процедур найчастіше використовується на практиці? Яка з цих процедур найшвидша на середніх та великих базах даних? Чи є інші способи досягти цього?

EDIT: Для простоти, у прикладі Пісня належить лише одному списку відтворення (відносини «багато в одному»). Звичайно, можна також скористатись таблицею з’єднання, так що список пісень - це співвідношення «багато-багато» (і застосувати одну із наведених вище стратегій на цій таблиці).


1
Ви можете використовувати варіант один (замовити як Integer) зі 100 кроками. Тоді вам не потрібно повторно замовляти, якщо ви переміщуєте одну пісню, просто прийміть значення між 100. Час від часу вам може знадобитися нове перенумерування, щоб знову отримати прогалини між піснями.
кнут

4
"Недолік цього полягає в тому, що потрібно робити багато запитів щоразу, коли пісня переміщується" ?! - update songorder set order = order - 1 where order >= 12 & order <= 42; update songorder set order = 42 where id = 123;- це два оновлення - не тридцять. Три, якщо ви хочете поставити на замовлення унікальне обмеження.

2
Використовуйте варіант один, якщо ви не знаєте, для чого вам потрібно щось інше. Однією з проблемних програмістів, що зустрічаються з новими базами даних, є нерозуміння того, що такі бази даних дуже-дуже хороші. Не бійтеся ставити свій db на роботу.
GrandmasterB

1
Queries like 'find the Xth Song in the list' are no longer constant-timeтакож стосується варіанту 2.
Док Браун

2
@MikeNakis: Це здається дорогим, але вся робота проводиться на сервері, який (як правило) оптимізований для такого роду робіт. Я б не використовував цю техніку на столі з мільйонами рядків, але я не став би її на стіл лише з кількома тисячами.
TMN

Відповіді:


29

Бази даних оптимізовані для певних речей. Швидке оновлення багатьох рядків - один із них. Це стає особливо актуальним, коли ви дозволяєте базі даних робити свою роботу.

Поміркуйте:

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

І ви хочете перейти Beat Itдо кінця, у вас було б два запити:

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

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

Це забезпечує деякі істотні переваги - порядок таблиці - це те, на що ви можете міркувати. Третя пісня завжди має order3, завжди. Єдиний спосіб гарантувати це - використання послідовних цілих чисел як порядку. Використання псевдопов'язаних списків або десяткових чисел або цілих чисел із пробілами не дасть вам гарантії цього властивості; у цих випадках єдиний спосіб отримати n-ту пісню - сортувати всю таблицю та отримати n-й запис.

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


2
Мені починає подобатися такий підхід.
Майк Накіс

2
@MikeNakis це працює добре. Існує також бінарне дерево, яке базується на подібній ідеї - модифіковане дерево попереднього замовлення . Потрібно трохи більше, щоб обернутися головою, але це дозволяє робити дуже приємні запити щодо ієрархічних даних. У мене ніколи не було проблем з роботою, навіть у великих деревах. Можливість міркувати про код - це те, на що я робив великий акцент, поки не буде показано, що простому коду не вистачає необхідної продуктивності (і це було лише в екстремальних ситуаціях).

Чи будуть якісь проблеми з використанням, orderоскільки order byце ключове слово?
kojow7

@ kojow7, якщо у ваших полях є імена, що суперечать ключовим словам, слід загорнути їх у позначки "` ".
Андрій

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

3

Перш за все, з вашого опису не зрозуміло, що ви зробили, але вам потрібна PlaylistSongsтаблиця, яка містить a PlaylistIdі a SongId, що описує, які пісні належать до яких списків відтворення.

Саме в цю таблицю потрібно додати інформацію про замовлення.

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

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

Клас є ParameterTemplate(як би там не було, не питайте!) Метод отримує список шаблонів параметрів, до яких цей шаблон належить від його батьківського ActivityTemplate. (Що б там не було, не питай!) Код містить захист від неточності. Для тестування використовується дільник: тест блоку використовує великий дільник, щоб швидко втратити точність і, таким чином, запустити код захисту точності. Другий метод є загальнодоступним і "лише для внутрішнього використання; не викликати", щоб код тестування міг викликати його. (Це не могло бути приватним пакетом, тому що мій код тестування не в тому ж пакеті, що і код, який він тестує.) Поле, яке контролює замовлення, викликається Ordering, отримується доступ через getOrdering()і setOrdering(). Ви не бачите жодного 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;
}

Я використовував би ціле замовлення, і якби я вважав, що переупорядкування є надто дорогим, я б просто зменшив кількість повторних послідовностей, кожен скачок на X, де X - кількість, яку мені потрібно зменшити, щоб переупорядкувати на, скажімо, 20, що повинен бути прекрасним як закваска.
Warren P

1
@WarrenP так, я знаю, це також можна зробити так, тому я просто назвав цей "мій улюблений" підхід замість "найкращого" або "єдиного" підходу.
Майк Накіс

0

Що для мене спрацювало, для невеликого списку на 100 позицій було застосувати гібридний підхід:

  1. Десятковий стовпець SortOrder, але з достатньою точністю, щоб зберігати 0,5 різниці (тобто десяткової (8,2) або чогось іншого).
  2. Під час сортування візьміть ПК у рядку вгорі та внизу, де щойно переміщено поточний рядок, якщо вони існують. (Ви не будете мати рядок вгорі, якщо перемістите елемент, наприклад, на перше місце)
  3. Опублікуйте ПК поточного, попереднього та наступного рядка на сервері для виконання сортування.
  4. Якщо у вас є попередній рядок, встановіть позицію поточного рядка на попередню + 0,5. Якщо у вас є лише наступний, встановіть позицію поточного рядка на наступну - 0,5.
  5. Далі, у мене є Stored proc, який оновлює всі позиції за допомогою функції SQL Server Row_Number, упорядковуючи новий порядок сортування. Це перетворить впорядкування з 1,1,5,2,3,4,6 до 1,2,3,4,5,6, оскільки функція number_number дає вам цілі порядки.

Отже, ви отримуєте ціле замовлення без пропусків, збережене в десятковій колонці. Це досить чисто, я відчуваю. Але це може не дуже масштабуватись, як тільки у вас є сотні тисяч рядків, які потрібно оновити, і все відразу. Але якщо ви це робите, чому ви в першу чергу використовуєте визначений користувачем сорт? (Примітка: якщо у вас є велика таблиця з мільйонами користувачів, але кожен користувач має лише кілька сотень елементів для сортування, ви можете використовувати вищезазначений підхід на відмінно, оскільки ви будете використовувати пункт де так чи інакше, щоб обмежити зміни лише для одного користувача )

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.