Jak zaprojektować bazę danych do przechowywania posortowanej listy?

42

Chcę przechowywać posortowaną listę w bazie danych. Chcę wydajnie wykonać następujące operacje.

  1. Wstaw (x) - Wstaw rekord x do tabeli
  2. Usuń (x) - Usuń rekord x z tabeli
  3. Przed (x, n) - zwraca rekordy „n” poprzedzające rekord x na posortowanej liście.
  4. Po (x, n) - zwraca rekordy „n” następujące po rekordzie x z posortowanej listy.
  5. Pierwszy (n) - Zwraca pierwsze rekordy „n” z posortowanej listy.
  6. Last (n) - Zwraca ostatnie „n” rekordy z posortowanej listy.
  7. Porównaj (x, y) - Biorąc pod uwagę dwa rekordy xiy z tabeli, sprawdź, czy x> y.

Prostą metodą, o której mógłbym pomyśleć, jest zapisanie w tabeli i zapytaniu jakiegoś atrybutu „ranga” poprzez sortowanie według tego atrybutu. Ale w tej metodzie wstawianie / modyfikowanie rekordu o randze staje się kosztowną operacją. Czy istnieje lepsza metoda?

W szczególności chcę zaimplementować tabelę za pomocą SimpleDB firmy Amazon. Ale ogólna odpowiedź na relacyjną bazę danych również powinna być pomocna.

Aktualizacja profilu obciążenia:

Ponieważ planuję to dla aplikacji internetowej, zależy to od liczby użytkowników korzystających z aplikacji.

Jeśli jest 100 000 aktywnych użytkowników (super optymizm: P), to mój bardzo przybliżony szacunek na dzień

500k wybiera, 100k wstawia i usuwa, 500k aktualizacji

Spodziewałbym się, że stół wzrośnie do 500 tys.

Chcę zoptymalizować operacje aktualizacji, wstawiania i porównywania. Ranga przedmiotów będzie się ciągle zmieniać i muszę aktualizować tabelę.

ćitti
źródło
Opracuj trochę swojego oczekiwanego profilu obciążenia. Ile wyborów / wkładek / aktualizacji dziennie? Dla jakich operacji najbardziej chcesz zoptymalizować? O ile spodziewacie się, że stół będzie się powiększał w ciągu dnia lub w sumie?
Nick Chammas,
Czy to dla tablicy rankingów graczy? W każdym razie zaktualizowałem moją odpowiedź poniżej, przekazując informacje zwrotne na podstawie twojego prognozowanego profilu obciążenia.
Nick Chammas,
nie, to nie jest tablica rankingów graczy.
chitti,
Jakie podejście wykorzystałeś?
Nick Chammas,
Nie jestem nawet pewien, o co tu pytano ani czego nie trzeba robić z listy rzeczy do zrobienia.
Evan Carroll

Odpowiedzi:

22

Jeśli ranga nie jest całkowicie arbitralna, ale można ją wyprowadzić z innej własności (np. Imię, wynik gracza itp.), Przyjrzyj się odpowiedzi Joela .

Jeśli jest to dowolna właściwość twoich danych, to powinna być przechowywana jako kolumna w twojej tabeli rekordów. Zakładając, że Amazon SimpleDB jest podobny do typowego RDBMS, możesz następnie zindeksować tę kolumnę i szybko zaspokoić wszystkie powyższe zapytania za pomocą odpowiedniej strategii indeksowania. Jest to normalne w przypadku RDBMS.

Biorąc pod uwagę, że oczekujesz wysokiej aktywności wstawiania i aktualizacji, ale także stosunkowo wysokiej aktywności odczytu, zalecam wykonanie następujących czynności:

  • Zgromadź tabelę na poziomie, szczególnie jeśli zdecydowana większość twoich zapytań jest przeciwna randze. Jeśli nie, lub jeśli wybranie klucza klastrowania nie jest dostępne w SimpleDB, po prostu utwórz indeks z rangą jako wiodącą kolumną. Spełniałoby to zapytania 3-6.
  • Indeks najpierw rekordu, a następnie rangi (lub, w świecie SQL Server, po prostu zapisz i INCLUDE-ing rangę, lub po prostu zapisz, jeśli masz klastrowane rangi) spełniłby zapytanie 7.
  • Operacje 1 i 2 można zoptymalizować, odpowiednio rozdzielając dane (tj. Ustawiając FILLFACTORw SQL Server). Jest to szczególnie ważne, jeśli skupisz się na rankingu.
  • Wstawiając lub aktualizując rangi, zachowaj jak największą lukę między numerami rang, aby zminimalizować możliwość zmiany rangi istniejącego rekordu, aby uwzględnić wstawienie lub aktualizację rang. Na przykład, jeśli uszeregujesz swoje rekordy w krokach co 1000, pozostawisz wystarczająco dużo miejsca na około połowę tylu zmian i wkładek z minimalną szansą, będziesz musiał zmienić pozycję rekordu, który nie jest bezpośrednio zaangażowany w te zmiany.
  • Każdej nocy zmieniaj rangę wszystkich rekordów, aby wyzerować odstępy między nimi.
  • Możesz dostroić częstotliwość masowych ponownych rankingów, a także rozmiar odstępu w rankingu, aby uwzględnić oczekiwaną liczbę wstawek lub aktualizacji w stosunku do liczby istniejących rekordów. Więc jeśli masz 100 000 rekordów i spodziewasz się, że twoje wstawki i aktualizacje będą stanowić 10% tego, zostaw wystarczająco dużo miejsca na 10 000 nowych rang i zmieniaj rangę co noc.
  • Zmiana rankingu rekordów 500 000 jest kosztowną operacją, ale dla takich baz danych taka operacja powinna być odpowiednia raz dziennie lub w tygodniu poza godzinami pracy. To przesunięcie masy poza godzinami pracy, aby utrzymać luki w rankingach, pozwala zaoszczędzić Ci konieczności zmiany rankingu wielu rekordów dla każdej aktualizacji lub wstawienia rankingu w godzinach normalnych i szczytowych.

Jeśli spodziewasz się, że 100K + odczytów na stole o wielkości 100K + nie polecam podejścia z listą połączoną. Nie będzie dobrze skalować do tych rozmiarów.

Nick Chammas
źródło
Rangi można modyfikować. Oczekuję, że szeregi będą się ciągle zmieniać, a nowe rekordy będą ciągle wstawiane. Martwię się o przypadek, w którym wstawię nowy element z rangą, więc należy zmienić szeregi wszystkich rekordów poniżej nowego rekordu w kolejności sortowania. Czy to nie jest kosztowna operacja, gdy mam tysiące rekordów w bazie danych?
chitti,
@chitti - Ach, to problem. Możesz rozdzielić swoje rankingi (np. 0, 1000, 2000, 3000, ...) i okresowo zmieniać kolejność wszystkich rekordów w miarę zapełniania się luk w rankingu. Nie będzie to jednak skalowane, jeśli spodziewasz się więcej niż kilkudziesięciu tysięcy rekordów.
Nick Chammas,
1
@chitti - Właściwie to trochę zabawne. Właśnie z tym problemami radzą sobie mechanizmy baz danych podczas indeksowania danych, ponieważ zamawiają je i zmieniają w miarę dodawania lub zmieniania danych. Jeśli spojrzysz w górę FILLFACTOR, zobaczysz, że zasadniczo chodzi o stworzenie dodatkowej przestrzeni na rekordy w indeksie, tak jak opisywane luki rang tworzą przestrzeń dla zmian i wstawiania rang.
Nick Chammas,
2
Dziękuję za zaktualizowaną odpowiedź. „Pozycja” jest arbitralną własnością moich danych. Jestem prawie przekonany, że potrzebuję niestandardowej kolumny indeksu. Sprawdź ten link SO z podobnym pytaniem. Najlepsza odpowiedź zawiera zalecenia dotyczące sposobu obsługi takiej kolumny rankingu.
chitti,
@chitti - Akceptowana odpowiedź na to SO pytanie jest świetna. Sugeruje to samo podejście, które tu szczegółowo opisałem, z dodatkową sugestią używania liczb dziesiętnych zamiast liczb całkowitych, aby znacznie zwiększyć elastyczność w przypisywaniu i zmienianiu rang. Świetne znalezisko.
Nick Chammas,
13

Ogólnie używam opisanej przez ciebie metody „rangi”. Zamiast kłopotać się aktualizowaniem wierszy, gdy trzeba było zmienić kolejność elementów, często mogłem uciec od usunięcia wszystkich rekordów z listy i ponownego wstawienia nowych elementów w odpowiedniej kolejności. Ta metoda jest wyraźnie zoptymalizowana do wyszukiwania.

Alternatywnym podejściem byłoby modelowanie rekordów jako połączonej listy przy użyciu kolumny tabeli klucza zwrotnego „poprzednik” w tabeli:

ID   setID   item       predecessor
---  ------  ------     ------------
1    1       Apple      null
2    1       Orange     1
3    2       Cucumber   null
4    1       Pear       2
5    1       Grape      4
6    2       Carrot     3

Możesz łatwo pobrać listę oraz dodawać i usuwać elementy przy niewielkim obciążeniu, ale uporządkowanie rekordów we właściwej kolejności będzie trudne. Być może istnieje sprytny sposób na zrobienie tego w jednym zapytaniu, prawdopodobnie z dużą ilością aliasowanych połączeń tabel.

Tego drugiego podejścia często używam, gdy modeluję relację typu drzewo (kategorie, foldery, zestawy i podzbiory). Generalnie miałem jakąś funkcję rekurencyjną, aby zrekonstruować pełne drzewo w mojej aplikacji.

bpanulla
źródło
2
Połączony model listy jest czysty. Aby pobrać taką hierarchię w SQL Server, należy użyć rekurencyjnej CTE .
Nick Chammas,
Jednak zbudowanie tej hierarchii byłoby dość kosztowne dla wysokiego stołu. Zaletą jest to, że zmiany rang / wstawki / itp. Można łatwo wprowadzić. W zależności od oczekiwanego profilu obciążenia chitti może to być najlepsze podejście.
Nick Chammas,
Opcja listy połączonej wygląda na najlepszy pomysł dla wszystkich operacji oprócz Porównaj. Masz pomysł, jak zaimplementować Porównaj bez konieczności śledzenia ścieżki między dwoma porównywanymi elementami?
chitti,
Jeśli masz identyfikatory elementów, myślę, że porównanie () byłoby proste, chyba że źle zrozumiałem, co masz na myśli przez porównanie (). Kiedy powiedziałeś: „znajdź, jeśli x> y” miałeś na myśli „znajdź, czy x poprzedza y”? Nie widzę w tym łatwości bez niestandardowego indeksu lub procedury składowanej, która przechodziłaby listę (lub tej ciekawej funkcji CTE wspomnianej przez @Nick).
bpanulla,
5
Ten typ rozwiązania jest również zbliżony do modelu danych grafowych ( en.wikipedia.org/wiki/Graph_theory ). System pamięci zoptymalizowany do przechowywania węzłów graficznych i krawędzi może być lepszym rozwiązaniem niż RDBMS. Sklepy z potrójnymi i poczwórnymi bazami danych i wykresami, takie jak Neo4J, są w tym całkiem dobre.
bpanulla
6

Myślę, że należy przechowywać właściwość lub właściwości używane do obliczenia rangi, a następnie zbudować nad nimi indeks. Zamiast próbować zmusić bazę danych do fizycznego przechowywania danych w uporządkowanej kolejności lub za pomocą ręcznie zarządzanej połączonej listy, dlaczego nie pozwolić silnikowi bazy danych robić to, do czego został przeznaczony?

Joel Brown
źródło
2
Co jeśli „właściwości używane do obliczania rangi” są arbitralne? Np .: Zestaw pozycji koszyka na zakupy, których kolejność zmienia się na podstawie dowolnych działań użytkownika.
chitti
Kiedy mówisz, że ranga jest dowolna, co masz na myśli? Musi istnieć algorytm używany do obliczania rangi. Na przykład: „na podstawie wpisów w koszyku” - w jaki sposób? W bazie danych musi znajdować się coś, co jest sterownikiem do obliczania rangi. Może to być kombinacja kilku rzeczy, ale te rzeczy muszą jakoś być przechowywane w tabeli klienta lub w tabelach związanych z klientem. Jeśli jest w danych, możesz utworzyć funkcję, która je oblicza. Jeśli możesz to obliczyć, możesz go zapisać i zindeksować.
Joel Brown,
Powiedzmy, że musimy utrzymać kolejność produktów w koszyku, a zamówienie może zostać „arbitralnie” zmienione przez użytkownika za pomocą interfejsu internetowego. Jak przechowujesz taką listę elementów w bazie danych i jak utrzymasz porządek sortowania?
chitti,
Jeśli dobrze cię rozumiem, „arbitralnie zmieniając” kolejność elementów w koszyku masz na myśli, że użytkownik może przeciągać elementy w górę i w dół listy i upuszczać je tam, gdzie chcą. Wydaje mi się, że to trochę wymyślone. Dlaczego użytkownicy mieliby to robić? Gdyby mogli to zrobić, czy zrobiliby to dużo? Czy używanie prostej sekwencji elementów w koszyku tak naprawdę wiąże się z poważnymi problemami z wydajnością? Wydaje mi się, że kolejny numer od jednego do liczby przedmiotów w koszyku + FK do zamówienia dałby potrzebny indeks. Po prostu zaktualizuj przedmioty, gdy ktoś zostanie przeciągnięty.
Joel Brown
3
Koszyk jest tylko przykładem, który podałem, aby pokazać, że istnieją przypadki, w których „pozycja” może być dowolna. Być może nie był to świetny przykład. Lepszym przykładem może być kolejka DVD Netflix. Dla samej argumentacji wyobraź sobie kolejkę z serwisem Netflix zawierającą 100 tys. Elementów, które użytkownik może dowolnie zmienić kolejność i robi to co minutę. Jak zaprojektowałbyś bazę danych do przechowywania uporządkowanej listy filmów w tej hipotetycznej aplikacji?
chitti,
1

Są to ograniczenia nieobsługiwane przez RDBMS, takie jak simpleDB. Wymaganych funkcji nie można zaimplementować po stronie DB w simpleDB, należy je zaimplementować od strony programowania / aplikacji.

W przypadku RDBMS SQL serverwymagane funkcje są podstawowe dla indeksu klastrowego.

  • Wstaw (x) - Wstaw rekord x do tabeli> Prosta wstawka.
  • Usuń (x) - Usuń rekord x z tabeli> Proste usuwanie.
  • Przed (x, n) - zwraca rekordy „n” poprzedzające rekord x na posortowanej liście. > Wybierz najlepsze n wyników, gdzie x mniej niż wartość i uporządkuj według klauzuli.

  • Po (x, n) - zwraca rekordy „n” następujące po rekordzie x z posortowanej listy. > Wybierz najlepsze n wyników, gdzie x jest większe od wartości i uporządkuj według klauzuli.

  • Pierwszy (n) - Zwraca pierwsze rekordy „n” z posortowanej listy. > Wybierz najlepsze n wyników.

  • Last (n) - Zwraca ostatnie „n” rekordy z posortowanej listy. > Wybierz najlepsze n wyników po zamówieniu według opisu.

  • Porównaj (x, y) - Biorąc pod uwagę dwa rekordy xiy z tabeli, sprawdź, czy x> y. > Instrukcja IFSQL.
StanleyJohns
źródło
SimpleDB zapewnia automatyczne indeksowanie, sortowanie i podstawowy język zapytań . Mój problem pozostanie, nawet jeśli wybiorę RDBMS. Problem polega na tym, że ranking danych w mojej bazie danych zmienia się arbitralnie i nie można ich uchwycić jako pojedynczej właściwości (chyba że użyję niestandardowej kolumny rankingu), którą można zindeksować.
chitti
0

Oto, czego użyłem, aby zmienić ranking mojej tabeli Postgres po każdej wstawce:

CREATE OR REPLACE FUNCTION re_rank_list() RETURNS trigger AS $re_rank_list$
DECLARE
    temprow record;
    row_idx integer := 1;    
BEGIN
    FOR temprow IN
    SELECT * FROM your_schema.your_list WHERE list_id = NEW.list_id ORDER BY rank ASC
    LOOP
        UPDATE your_schema.your_list SET rank = row_idx * 100 WHERE id = temprow.id;
        row_idx := row_idx + 1;
    END LOOP;
    RETURN NEW;
END;
$re_rank_list$ LANGUAGE plpgsql;


CREATE TRIGGER re_rank_list AFTER UPDATE ON your_schema.your_list_value
    FOR EACH ROW 
    WHEN (pg_trigger_depth() = 0)
    EXECUTE PROCEDURE re_rank_list();

W moim przypadku użycia wydajność nie jest problemem, ale pewność, że nigdy się nie złamie lub nie zadziała, jest ważna.

znak
źródło