Jak przechowywać zamówione informacje w relacyjnej bazie danych

20

Próbuję zrozumieć, jak prawidłowo przechowywać zamówione informacje w relacyjnej bazie danych.

Przykład:

Powiedz, że mam listę odtwarzania, na którą składają się utwory. W mojej relacyjnej bazie danych mam tabelę Playlistszawierającą niektóre metadane (nazwa, twórca itp.). Mam też tabelę o nazwie Songs, zawierającą playlist_id, a także informacje dotyczące utworu (imię, wykonawca, czas trwania itp.).

Domyślnie nowy utwór dodawany do listy odtwarzania jest dołączany na końcu. Przy zamawianiu według Song-ID (rosnąco) kolejność będzie kolejnością dodawania. Ale co, jeśli użytkownik powinien móc ponownie zamówić utwory na liście odtwarzania?

Wymyśliłem kilka pomysłów, każdy z ich zaletami i wadami:

  1. Nazwana kolumna order, która jest liczbą całkowitą . Po przeniesieniu utworu kolejność wszystkich utworów między jego starą a nową pozycją jest zmieniana, aby odzwierciedlić zmianę. Wadą tego jest to, że za każdym razem, gdy piosenka jest przenoszona, należy wykonać wiele zapytań, a algorytm przenoszenia nie jest tak trywialny jak w przypadku innych opcji.
  2. Wywołana kolumna order, która jest liczbą dziesiętną ( NUMERIC). Po przeniesieniu utworu zostaje mu przypisana wartość zmiennoprzecinkowa między dwiema sąsiednimi liczbami. Wada: pola dziesiętne zajmują więcej miejsca i może zabraknąć precyzji, chyba że dołoży się starań, aby rozdzielić zakres po każdych kilku zmianach.
  3. Innym sposobem byłoby previousutworzenie nextpola i odniesienia do innych utworów. (lub mają teraz wartość NULL w przypadku pierwszego, lub ostatniego utworu na liście odtwarzania; Zasadniczo tworzysz listę połączoną ). Wada: zapytania typu „znajdź X utwór na liście” nie są już czasem stałym, lecz czasem liniowym.

Która z tych procedur jest najczęściej stosowana w praktyce? Która z tych procedur jest najszybsza w średnich i dużych bazach danych? Czy istnieją inne sposoby na zarchiwizowanie tego?

EDYCJA: Dla uproszczenia, w tym przykładzie utwór należy tylko do jednej listy odtwarzania (relacja wiele do jednego). Oczywiście można również użyć tabeli połączeń, więc lista utworów jest relacją wiele do wielu (i stosuje jedną z powyższych strategii na tym stole).

Qqwy
źródło
1
Możesz użyć opcji pierwszej (zamówienie jako liczba całkowita) ze 100 krokami. Następnie nie musisz zmieniać kolejności, jeśli przenosisz jeden utwór, po prostu weź wartość pomiędzy 100. Od czasu do czasu możesz potrzebować nowej numeracji, aby ponownie uzyskać przerwy między utworami.
knut
4
„Wadą tego jest to, że za każdym razem, gdy piosenka jest przenoszona, trzeba zadać wiele pytań”?! - update songorder set order = order - 1 where order >= 12 & order <= 42; update songorder set order = 42 where id = 123;- to dwie aktualizacje - nie trzydzieści. Trzy, jeśli chcesz nałożyć unikalne ograniczenie na zamówienie.
2
Użyj opcji pierwszej, chyba że wiesz, że potrzebujesz czegoś innego. Jednym z problemów, z którym programiści nie znają baz danych, jest niezrozumienie, że bazy danych są bardzo, bardzo dobre w tego rodzaju sprawach. Nie bój się uruchomić db.
GrandmasterB,
1
Queries like 'find the Xth Song in the list' are no longer constant-timedotyczy to również opcji 2
Doc Brown,
2
@MikeNakis: Wydaje się to drogie, ale cała praca jest wykonywana na serwerze, który jest (zwykle) zoptymalizowany do tego rodzaju pracy. Nie użyłbym tej techniki na stole z milionami rzędów, ale nie odrzuciłbym jej za stół z zaledwie kilkoma tysiącami.
TMN,

Odpowiedzi:

29

Bazy danych są zoptymalizowane pod kątem niektórych rzeczy. Jednym z nich jest szybka aktualizacja wielu wierszy. Staje się to szczególnie prawdziwe, gdy baza danych wykonuje swoją pracę.

Rozważać:

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

I chcesz przejść Beat Itdo końca, będziesz mieć dwa zapytania:

update table 
  set order = order - 1
  where order >= 2 and order <= 5;

update table
  set order = 5
  where song = 'Beat It'

I to wszystko. To skaluje się bardzo dobrze przy bardzo dużych liczbach. Spróbuj umieścić kilka tysięcy utworów na hipotetycznej liście odtwarzania w bazie danych i sprawdź, ile czasu zajmuje przeniesienie utworu z jednego miejsca do drugiego. Ponieważ mają one bardzo znormalizowane formy:

update table 
  set order = order - 1
  where order >= ? and order <= ?;

update table
  set order = ?
  where song = ?

Masz dwa przygotowane oświadczenia, które możesz bardzo efektywnie wykorzystać.

Daje to pewne znaczące zalety - kolejność tabeli jest czymś, o czym możesz racjonalnie myśleć. Trzecia piosenka ma orderzawsze 3. Jedynym sposobem na zagwarantowanie tego jest użycie kolejnych liczb całkowitych jako kolejności. Użycie pseudo-połączonych list lub liczb dziesiętnych lub liczb całkowitych z przerwami nie pozwoli Ci zagwarantować tej właściwości; w takich przypadkach jedynym sposobem na zdobycie n-tej piosenki jest posortowanie całej tabeli i uzyskanie n-tej płyty.

I tak naprawdę jest to o wiele łatwiejsze niż myślisz. Łatwo jest ustalić, co chcesz zrobić, wygenerować dwie instrukcje aktualizacji, a inni ludzie mogą przejrzeć te dwie instrukcje aktualizacji i zorientować się, co się dzieje.

wedant
źródło
2
Zaczynam lubić to podejście.
Mike Nakis,
2
@MikeNakis działa dobrze. Istnieje również drzewo binarne oparte na podobnym pomyśle - zmodyfikowanym drzewie przedpremierowym . Zajmuje ci to trochę więcej czasu, ale pozwala ci wykonywać bardzo ładne zapytania dotyczące danych hierarchicznych. Nigdy nie miałem z tym problemów z wydajnością, nawet na dużych drzewach. Umiejętność wnioskowania o kodzie jest czymś, na co kładę duży nacisk, dopóki nie zostanie pokazane, że prosty kod nie ma wymaganej wydajności (i tak było tylko w ekstremalnych sytuacjach).
Czy będą jakieś problemy z używaniem, orderponieważ order byjest to słowo kluczowe?
kojow7
@ kojow7, jeśli twoje pola mają nazwy sprzeczne ze słowami kluczowymi, powinieneś zawinąć je w znaczniki „'”.
Andri,
Takie podejście ma sens, ale jaki jest najlepszy sposób, aby uzyskać orderwartość dodając nowy utwór do listy odtwarzania. Powiedzmy, że to dziewiąta piosenka. Czy jest lepszy sposób na wstawienie 9 orderniż zrobienie tego COUNTprzed dodaniem płyty?
delashum
3

Po pierwsze, z opisu tego, co zrobiłeś, nie wynika jasno, ale potrzebujesz PlaylistSongstabeli zawierającej PlaylistIdai SongIdopisującej, które utwory należą do poszczególnych list odtwarzania.

To w tej tabeli musisz dodać informacje o zamówieniu.

Moim ulubionym mechanizmem są liczby rzeczywiste. Ostatnio go wdrożyłem i działało to jak urok. Gdy chcesz przenieść utwór do określonej pozycji, obliczasz jego nową Orderingwartość jako średnią Orderingwartości z poprzedniego utworu i następnego utworu. Jeśli użyjesz 64-bitowej liczby rzeczywistej, zabraknie ci precyzji w tym samym czasie, w którym piekło zamarznie, ale jeśli naprawdę piszesz swoje oprogramowanie dla potomnych, rozważ ponowne przypisanie ładnych zaokrąglonych liczb całkowitych Orderingdo wszystkich utworów w każdym lista odtwarzania co jakiś czas.

Jako dodatkowy bonus, oto kod, który napisałem, który to implementuje. Oczywiście nie możesz go używać w obecnym stanie, a teraz byłoby dla mnie zbyt dużo pracy, aby go odkażać dla ciebie, więc zamieszczam go tylko po to, aby czerpać z niego pomysły.

Klasą jest ParameterTemplate(cokolwiek, nie pytaj!) Metoda pobiera listę szablonów parametrów, do których ten szablon należy od swojego rodzica ActivityTemplate. (Cokolwiek, nie pytaj!) Kod zawiera pewną ochronę przed brakiem precyzji. Dzielnik służy do testowania: test jednostkowy wykorzystuje duży dzielnik, aby szybko zabrakło precyzji, a tym samym wywołać precyzyjny kod zabezpieczający. Druga metoda jest publiczna i „wyłącznie do użytku wewnętrznego; nie wywołuje”, aby kod testowy mógł ją wywołać. (Nie może to być pakiet prywatny), ponieważ mój kod testowy nie znajduje się w tym samym pakiecie co kod, który testuje.) Pole kontrolujące kolejność jest wywoływane Ordering, dostępne za pośrednictwem getOrdering()i setOrdering(). Nie widzisz żadnego SQL, ponieważ używam mapowania obiektowo-relacyjnego przez Hibernację.

/**
 * 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;
}
Mike Nakis
źródło
Użyłbym porządkowania liczb całkowitych i gdybym poczuł, że zmiana kolejności była zbyt droga, po prostu zmniejszyłbym liczbę zmian kolejności, każąc każdemu skokowi o X, gdzie X to kwota, którą muszę zmniejszyć, zmieniając kolejność, powiedzmy 20, co powinno być dobrze na początek.
Warren P,
1
@WarrenP tak, wiem, można to również zrobić w ten sposób, dlatego właśnie nazwałem to „moim ulubionym” podejściem zamiast „najlepszym” lub „jedynym”.
Mike Nakis,
0

To, co zadziałało, dla małej listy rzędu 100 przedmiotów, to podejście hybrydowe:

  1. Kolumna Sortuj dziesiętny Sortuj, ale z wystarczającą dokładnością, aby zapisać różnicę 0,5 (tj. Dziesiętny (8,2) lub coś w tym rodzaju).
  2. Podczas sortowania chwyć PK z wiersza powyżej i poniżej, do którego właśnie przeniesiono bieżący wiersz, jeśli istnieją. (Na przykład nie będziesz mieć rzędu powyżej, jeśli przeniesiesz element na pierwszą pozycję)
  3. Opublikuj PK bieżącego, poprzedniego i następnego wiersza na serwerze, aby wykonać sortowanie.
  4. Jeśli masz poprzedni wiersz, ustaw pozycję bieżącego wiersza na poprzedni + 0,5. Jeśli masz tylko następny, ustaw pozycję bieżącego wiersza na następny - 0,5.
  5. Następnie mam przechowywany proc, który aktualizuje wszystkie pozycje za pomocą funkcji SQL_No Row_Number, sortując według nowej kolejności sortowania. Spowoduje to przekształcenie porządku z 1,1,5,2,3,4,6 do 1,2,3,4,5,6, ponieważ funkcja row_number daje liczby całkowite.

W rezultacie otrzymujesz kolejność całkowitą bez przerw, zapisaną w kolumnie dziesiętnej. Wydaje mi się, że jest dość czysty. Ale może nie skalować się bardzo dobrze, gdy masz setki tysięcy wierszy, które musisz zaktualizować naraz. Ale jeśli tak, to dlaczego przede wszystkim używasz sortowania zdefiniowanego przez użytkownika? (Uwaga: jeśli masz dużą tabelę z milionami użytkowników, ale każdy użytkownik ma tylko kilkaset pozycji do posortowania, możesz dobrze zastosować powyższe podejście, ponieważ i tak użyjesz klauzuli where, aby ograniczyć zmiany tylko do jednego użytkownika )

Jan
źródło