Jak przechowywać listę w kolumnie tabeli bazy danych

117

Tak więc, zgodnie z odpowiedzią Mehrdada na pokrewne pytanie , rozumiem, że „właściwa” kolumna tabeli bazy danych nie przechowuje listy. Należy raczej utworzyć inną tabelę, która skutecznie przechowuje elementy tej listy, a następnie połączyć się z nią bezpośrednio lub za pośrednictwem tabeli skrzyżowań. Jednak typ listy, który chcę utworzyć, będzie składał się z unikalnych elementów (w przeciwieństwie do owocu pytania, do którego prowadzi linkprzykład). Co więcej, pozycje na mojej liście są jawnie posortowane - co oznacza, że ​​gdybym przechowywał elementy w innej tabeli, musiałbym je sortować za każdym razem, gdybym miał do nich dostęp. Wreszcie lista jest w zasadzie atomowa, ponieważ za każdym razem, gdy chcę uzyskać dostęp do listy, będę chciał uzyskać dostęp do całej listy, a nie tylko jej części - więc wydaje się głupie, aby wysłać zapytanie do bazy danych, aby zebrać razem fragmenty Lista.

Rozwiązanie AKX (link powyżej) polega na serializacji listy i przechowywaniu jej w kolumnie binarnej. Ale to również wydaje się niewygodne, ponieważ oznacza, że ​​muszę się martwić serializacją i deserializacją.

Czy jest jakieś lepsze rozwiązanie? Jeśli nie to nie ma lepszego rozwiązania, to dlaczego? Wydaje się, że ten problem od czasu do czasu powinien się pojawiać.

... tylko trochę więcej informacji, abyś wiedział, skąd pochodzę. Gdy tylko zacząłem rozumieć SQL i ogólnie bazy danych, zostałem włączony do LINQ to SQL, więc teraz jestem trochę zepsuty, ponieważ spodziewam się poradzić sobie z moim modelem obiektów programowania bez zastanawiania się, jak obiekty są wyszukiwane lub przechowywane w bazie danych.

Dziękuje wszystkim!

Jan

AKTUALIZACJA: Więc w pierwszej lawinie odpowiedzi, które otrzymuję, widzę „możesz przejść na trasę CSV / XML ... ale NIE!”. Więc teraz szukam wyjaśnienia, dlaczego. Wskaż mi kilka dobrych referencji.

Ponadto, aby dać ci lepsze wyobrażenie o tym, co zamierzam: w mojej bazie danych mam tabelę funkcji, która będzie miała listę par (x, y). (Tabela będzie zawierała również inne informacje, które nie mają znaczenia dla naszej dyskusji). Nigdy nie będę potrzebował widzieć części listy par (x, y). Raczej wezmę je wszystkie i narysuję na ekranie. Pozwolę użytkownikowi przeciągać węzły, aby od czasu do czasu zmieniać wartości lub dodawać więcej wartości do wykresu.

JnBrymn
źródło

Odpowiedzi:

183

Nie, nie ma „lepszego” sposobu na przechowywanie sekwencji elementów w jednej kolumnie. Relacyjne bazy danych są zaprojektowane specjalnie do przechowywania jednej wartości na kombinację wiersza / kolumny. Aby przechowywać więcej niż jedną wartość, należy serializować listę w jedną wartość do przechowywania, a następnie deserializować ją po pobraniu. Nie ma innego sposobu na zrobienie tego, o czym mówisz (ponieważ to, o czym mówisz, jest złym pomysłem, którego generalnie nigdy nie powinno się robić ).

Rozumiem, że uważasz, że tworzenie innej tabeli do przechowywania tej listy jest niemądre, ale właśnie to robią relacyjne bazy danych. Prowadzisz żmudną bitwę i bez powodu naruszasz jedną z najbardziej podstawowych zasad projektowania relacyjnych baz danych. Ponieważ stwierdzasz, że dopiero się uczysz SQL, zdecydowanie radzę unikać tego pomysłu i trzymać się praktyk zalecanych przez bardziej doświadczonych programistów SQL.

Zasada, którą łamiesz, nazywa się pierwszą formą normalną , co jest pierwszym krokiem w normalizacji bazy danych.

Na ryzyko oversimplifying rzeczy, normalizacja bazy danych jest procesem definiowania bazy danych w oparciu o to, co dane jest tak, że można pisać sensowne i spójne zapytań przeciwko nim i będzie w stanie utrzymać go łatwo. Normalizacja ma na celu ograniczenie niespójności logicznych i uszkodzeń danych, a jest to wiele poziomów. Artykuł Wikipedii o normalizacji baz danych jest całkiem niezły.

Zasadniczo pierwsza reguła (lub forma) normalizacji stwierdza, że ​​tabela musi reprezentować relację. To znaczy że:

  • Musisz być w stanie odróżnić jeden wiersz od dowolnego innego wiersza (innymi słowy, twoja tabela musi mieć coś, co może służyć jako klucz podstawowy. Oznacza to również, że żaden wiersz nie powinien być duplikowany.
  • Każda kolejność danych musi być określona przez dane, a nie przez fizyczną kolejność wierszy (SQL opiera się na idei zbioru, co oznacza, że jedyną kolejnością, na której powinieneś polegać, jest ta, którą wyraźnie zdefiniujesz w zapytaniu)
  • Każde przecięcie wiersza / kolumny musi zawierać jedną i tylko jedną wartość

Ostatni punkt jest oczywiście najważniejszy. SQL został zaprojektowany, aby przechowywać zestawy za Ciebie, a nie po to, aby zapewnić Ci „wiadro” do samodzielnego przechowywania zestawu. Tak, jest to możliwe. Nie, świat się nie skończy. Jednak już okaleczyłeś się w zrozumieniu języka SQL i najlepszych praktyk, które się z nim wiążą, natychmiast rozpoczynając korzystanie z ORM. LINQ to SQL jest fantastyczny, podobnie jak kalkulatory graficzne. Z tego samego powodu nie należy ich jednak używać jako substytutu wiedzy o tym, jak faktycznie działają procesy, które wykorzystują.

Twoja lista może być teraz całkowicie „atomowa” i może się to nie zmienić w tym projekcie. Ale przyzwyczaisz się do robienia podobnych rzeczy w innych projektach i w końcu (prawdopodobnie szybko) napotkasz scenariusz, w którym teraz dopasowujesz swoją szybką i łatwą listę w kolumnie podejście tam, gdzie jest to całkowicie niewłaściwe. Tworzenie właściwej tabeli dla tego, co próbujesz przechowywać, nie wymaga wiele dodatkowej pracy, a inni programiści SQL nie będą wyśmiewani, gdy zobaczą projekt Twojej bazy danych. Poza tym, LINQ to SQL będzie zobaczyć swój związek i daje właściwego interfejsu obiektowego do listy automatycznie . Dlaczego miałbyś zrezygnować z wygody oferowanej przez ORM, aby móc wykonywać niestandardowe i nierozsądne hakowanie baz danych?

Adam Robinson
źródło
17
Więc mocno wierzysz, że przechowywanie listy w kolumnie to zły pomysł, ale nie wspominasz, dlaczego. Ponieważ dopiero zaczynam od SQL, trochę „dlaczego” byłoby naprawdę bardzo pomocne. Na przykład, mówisz, że „toczę ciężką bitwę i naruszam jedną z najbardziej podstawowych zasad projektowania relacyjnych baz danych bez powodu”… więc jaka jest zasada? Dlaczego powody, które przytoczyłem, są „niedobre”? (a konkretnie posortowana i atomowa natura moich list)
JnBrymn
6
Zasadniczo sprowadza się to do lat doświadczeń skondensowanych w najlepszych praktykach. Podstawowy podmiot, o którym mowa, jest znany jako pierwsza forma normalna .
Toby
1
Dzięki Adam. Bardzo informujące. Słuszna uwaga z twoim ostatnim pytaniem.
JnBrymn
8
„[…] I nie dasz się wyśmiać innym programistom SQL, gdy zobaczą projekt Twojej bazy danych”. Są bardzo dobre powody, by szanować pierwszą normalną formę (i twoja odpowiedź o nich wspomina), ale presja rówieśników / „tak się tutaj dzieje” nie jest jednym z nich.
Lynn
5
Już codziennie przechowujemy paczki list w kolumnach bazy danych. Nazywają się „char” i „varchar”. Oczywiście w Postgres nazywane są również tekstem. To, co tak naprawdę mówi 1NF, to to, że nigdy nie powinieneś chcieć dzielić informacji w jakimkolwiek polu na mniejsze pola, a jeśli to zrobisz, nie masz nic przeciwko. Więc nie przechowujesz nazwiska, przechowujesz imię i nazwisko, drugie imię i nazwisko (w zależności od lokalizacji) i zszywasz je razem. W przeciwnym razie w ogóle nie przechowywalibyśmy ciągów tekstu. Z drugiej strony chce tylko sznurka. Są na to sposoby.
Haakon Løtveit
15

Możesz po prostu zapomnieć o SQL i wybrać podejście „NoSQL”. RavenDB , MongoDB i CouchDB skok na myśl jako możliwych rozwiązań. Dzięki podejściu NoSQL nie używasz modelu relacyjnego… nie jesteś nawet ograniczony do schematów.

jaltiere
źródło
11

Widziałem, jak wiele osób robi to (może to nie być najlepsze podejście, popraw mnie, jeśli się mylę):

Tabela, której używam w przykładzie, jest podana poniżej (tabela zawiera pseudonimy, które nadałeś swoim konkretnym dziewczynom. Każda dziewczyna ma unikalny identyfikator):

nicknames(id,seq_no,names)

Załóżmy, że chcesz przechowywać wiele pseudonimów pod jednym identyfikatorem. Dlatego włączyliśmy seq_nopole.

Teraz wprowadź te wartości do swojej tabeli:

(1,1,'sweetheart'), (1,2,'pumpkin'), (2,1,'cutie'), (2,2,'cherry pie')

Jeśli chcesz znaleźć wszystkie imiona, które nadałeś swojej dziewczynie id 1, możesz użyć:

select names from nicknames where id = 1;
H. Pauwelyn
źródło
5

Prosta odpowiedź: jeśli i tylko wtedy, gdy masz pewność, że lista zawsze będzie używana jako lista, połącz ją na końcu ze znakiem (takim jak „\ 0”), który nie będzie używany w tekst zawsze i przechowuj to. Następnie, gdy go odzyskasz, możesz podzielić przez „\ 0”. Istnieją oczywiście inne sposoby rozwiązania tego problemu, ale zależą one od konkretnego dostawcy bazy danych.

Na przykład możesz przechowywać JSON w bazie danych Postgres. Jeśli Twoja lista jest tekstowa i chcesz po prostu listę bez dalszych kłopotów, to rozsądny kompromis.

Inni odważyli się na sugestie serializacji, ale nie sądzę, aby serializacja była dobrym pomysłem: część fajnej rzeczy w bazach danych polega na tym, że kilka programów napisanych w różnych językach może ze sobą rozmawiać. A programy serializowane przy użyciu formatu Javy nie zrobiłyby tak dobrze, gdyby program Lisp chciał je załadować.

Jeśli chcesz mieć dobry sposób na zrobienie tego typu rzeczy, zwykle dostępne są typy tablicowe lub podobne. Na przykład Postgres oferuje tablicę jako typ i pozwala przechowywać tablicę tekstu, jeśli tego chcesz , a są podobne sztuczki dla MySql i MS SQL używające JSON, a IBM DB2 oferuje również typ tablicy (w ich własną pomocną dokumentację). Nie byłoby to tak powszechne, gdyby nie było takiej potrzeby.

To, co tracisz, idąc tą drogą, to postrzeganie listy jako zbioru rzeczy po kolei. Przynajmniej nominalnie bazy danych traktują pola jako pojedyncze wartości. Ale jeśli to wszystko, czego chcesz, powinieneś to zrobić. To ocena wartości, którą musisz sam dla siebie dokonać.

Haakon Løtveit
źródło
3

Oprócz tego, co powiedzieli wszyscy inni, sugerowałbym, abyś przeanalizował swoje podejście w dłuższej perspektywie niż teraz. Jest obecnie zdarza się, że przedmioty są unikalne. Jest obecnie zdarza się, że uciekają się elementy, które wymagają nowej listy. To jest niemal konieczne, że lista jest obecnie krótka. Mimo że nie mam szczegółowych informacji o domenie, myślenie, że te wymagania mogą się zmienić, nie jest zbyt trudne. Jeśli serializujesz swoją listę, pieczesz w sposób nieelastyczny, który nie jest konieczny w bardziej znormalizowanym projekcie. Przy okazji, to niekoniecznie oznacza pełną relację Many: Many. Możesz mieć tylko jedną tabelę podrzędną z kluczem obcym do rodzica i kolumną znaków dla elementu.

Jeśli nadal chcesz przejść tę drogę serializacji listy, możesz rozważyć przechowywanie listy w formacie XML. Niektóre bazy danych, takie jak SQL Server, mają nawet typ danych XML. Jedynym powodem, dla którego proponuję XML jest to, że prawie z definicji ta lista musi być krótka. Jeśli lista jest długa, serializacja jej ogólnie jest okropnym podejściem. Jeśli wybierzesz trasę CSV, musisz wziąć pod uwagę wartości zawierające ogranicznik, co oznacza, że ​​musisz używać identyfikatorów w cudzysłowie. Zakładając, że listy są krótkie, prawdopodobnie nie będzie miało większego znaczenia, czy użyjesz CSV, czy XML.

Tomasz
źródło
+1 za przewidywanie przyszłych zmian - zawsze projektuj model danych tak, aby był rozszerzalny.
coolgeek
2

Po prostu zapisałbym go jako CSV, jeśli są to proste wartości, powinno to być wszystko, czego potrzebujesz (XML jest bardzo rozwlekły, a serializacja do / z niego prawdopodobnie byłaby przesada, ale byłaby to również opcja).

Oto dobra odpowiedź, jak wyciągać pliki CSV za pomocą LINQ.

David Neale
źródło
Myślałem o tym. To nadal oznacza, że ​​musiałbym serializować i deserializować ... ale podejrzewam, że to wykonalne. Żałuję, że nie ma jakiegoś akceptowanego sposobu robienia tego, co chcę, ale podejrzewam, że tak nie jest.
JnBrymn
capnproto.org to sposób, aby nie musieć serializować i deserializować, podobnie szybko (w porównaniu do csv lub xml) w przypadku, gdy capnproto nie jest obsługiwane w wybranym przez Ciebie języku msgpack.org/index.html
VoronoiPotato
2

Jeśli potrzebujesz zapytać o listę, zapisz ją w tabeli.

Jeśli zawsze chcesz mieć listę, możesz przechowywać ją jako rozdzielaną listę w kolumnie. Nawet w tym przypadku, jeśli nie masz BARDZO konkretnych powodów, aby tego nie robić, przechowuj je w tabeli przeglądowej.

hometoast
źródło
1

Tylko jedna opcja nie została wymieniona w odpowiedziach. Możesz zdenormalizować swój projekt DB. Potrzebujesz więc dwóch stołów. Jedna tabela zawiera właściwą listę, po jednej pozycji w wierszu, inna tabela zawiera całą listę w jednej kolumnie (np. Rozdzielonej przecinkami).

Oto `` tradycyjny '' projekt DB:

List(ListID, ListName) 
Item(ItemID,ItemName) 
List_Item(ListID, ItemID, SortOrder)

Tutaj jest zdenormalizowana tabela:

Lists(ListID, ListContent)

Pomysł tutaj - utrzymujesz tabelę Lists za pomocą wyzwalaczy lub kodu aplikacji. Za każdym razem, gdy modyfikujesz zawartość List_Item, odpowiednie wiersze w Listach są aktualizowane automatycznie. Jeśli czytasz głównie listy, może to działać całkiem dobrze. Zalety - możesz czytać listy w jednym oświadczeniu. Wady - aktualizacje wymagają więcej czasu i wysiłku.

Alsin
źródło
0

Jeśli naprawdę chciałeś przechowywać go w kolumnie i mieć możliwość wykonywania zapytań, wiele baz danych obsługuje teraz XML. Jeśli nie odpytujesz, możesz zapisać je jako wartości oddzielone przecinkami i przeanalizować je za pomocą funkcji, gdy potrzebujesz ich oddzielonych. Zgadzam się ze wszystkimi, chociaż jeśli chcesz użyć relacyjnej bazy danych, dużą częścią normalizacji jest oddzielenie takich danych. Nie mówię jednak, że wszystkie dane pasują do relacyjnej bazy danych. Zawsze możesz zajrzeć do innych typów baz danych, jeśli wiele danych nie pasuje do modelu.

David Daniel
źródło
0

Myślę, że w niektórych przypadkach możesz stworzyć FAKE "listę" pozycji w bazie danych, na przykład towar ma kilka zdjęć, aby pokazać jego szczegóły, możesz połączyć wszystkie identyfikatory zdjęć podzielone przecinkami i zapisać ciąg w DB, wtedy wystarczy przeanalizować ciąg, kiedy go potrzebujesz. Pracuję teraz nad stroną internetową i planuję to wykorzystać.

Nen
źródło
0

Bardzo niechętnie wybrałem ścieżkę, którą ostatecznie zdecydowałem się obrać z powodu wielu odpowiedzi. Chociaż dodają więcej zrozumienia do tego, czym jest SQL i jego zasady, postanowiłem zostać wyjętym spod prawa. Wahałem się również, czy opublikować swoje odkrycia, ponieważ dla niektórych ważniejsze jest wyładowanie frustracji komuś, kto łamie zasady, niż zrozumienie, że istnieje bardzo niewiele uniwersalnych prawd.

Przetestowałem to obszernie iw moim konkretnym przypadku było to o wiele bardziej wydajne niż używanie typu tablicowego (hojnie oferowanego przez PostgreSQL) lub przeszukiwanie innej tabeli.

Oto moja odpowiedź: z powodzeniem zaimplementowałem listę w jednym polu w PostgreSQL, wykorzystując ustaloną długość każdego elementu listy. Powiedzmy, że każdy element ma kolor jako wartość szesnastkową ARGB, czyli 8 znaków. Możesz więc utworzyć tablicę maksymalnie 10 elementów, mnożąc przez długość każdego elementu:

ALTER product ADD color varchar(80)

W przypadku, gdy długość elementów listy jest różna, zawsze możesz wypełnić dopełnienie \ 0

NB: Oczywiście niekoniecznie jest to najlepsze podejście do liczby szesnastkowej, ponieważ lista liczb całkowitych zajmowałaby mniej miejsca, ale ma to na celu zilustrowanie tej idei tablicy przy użyciu stałej długości przydzielonej do każdego elementu.

Powód, dla którego: 1 / Bardzo wygodne: pobierz element i w podłańcuchu i * n, (i +1) * n. 2 / Brak narzutu zapytań krzyżowych. 3 / Bardziej wydajne i oszczędne po stronie serwera. Lista jest jak mały obiekt blob, który klient będzie musiał podzielić.

Chociaż szanuję ludzi przestrzegających zasad, wiele wyjaśnień jest bardzo teoretycznych i często nie uwzględnia faktu, że w niektórych konkretnych przypadkach, szczególnie gdy dążymy do optymalnych kosztów przy rozwiązaniach o niskim opóźnieniu, niektóre drobne poprawki są mile widziane.

„Nie daj Boże, że narusza jakąś świętą, świętą zasadę języka SQL”: przyjęcie bardziej otwartego i pragmatycznego podejścia przed recytacją reguł jest zawsze właściwą drogą. W przeciwnym razie możesz skończyć jak szczery fanatyk recytujący Trzy Prawa Robotyki, zanim zostaniesz zniszczony przez Skynet

Nie udaję, że to rozwiązanie jest przełomem, ani że jest idealne pod względem czytelności i elastyczności bazy danych, ale z pewnością może dać ci przewagę, jeśli chodzi o opóźnienia.

Antonin GAVREL
źródło
Ale to jest bardzo specyficzny przypadek: stała liczba elementów o stałej długości. Nawet wtedy proste wyszukiwanie, takie jak „wszystkie produkty o kolorze co najmniej x”, jest trudniejsze niż standardowy SQL.
Gert Arnold
Jak już wielokrotnie stwierdzałem, nie używam go do koloru, pole, do którego go używam, nie powinno być indeksowane ani używane jako warunek, a mimo to jest krytyczne
Antonin GAVREL
Wiem, próbuję wskazać, że jest to bardzo specyficzne. Jeśli wkrada się do niego jakikolwiek mały dodatkowy wymóg, szybko staje się bardziej niewygodny niż standardowe rozwiązania. Zdecydowanej większości ludzi, którzy kuszą się przechowywać listy w jednym polu bazy danych, prawdopodobnie lepiej tego nie robić.
Gert Arnold
0

Wiele baz danych SQL pozwala, aby tabela zawierała podtabelę jako składnik. Zwykłą metodą jest dopuszczenie, aby domena jednej z kolumn była tabelą. Jest to dodatek do użycia pewnej konwencji, takiej jak CSV, do kodowania podstruktury w sposób nieznany DBMS.

Kiedy Ed Codd opracowywał model relacyjny w latach 1969-1970, konkretnie zdefiniował normalną formę , która uniemożliwiłaby tego rodzaju zagnieżdżanie tabel. Forma normalna została później nazwana First Normal Form. Następnie pokazał, że dla każdej bazy danych istnieje baza danych w pierwszej normalnej formie, która wyraża te same informacje.

Po co się tym przejmować? Cóż, bazy danych w pierwszej normalnej postaci umożliwiają dostęp do wszystkich danych za pomocą klucza. Jeśli podasz nazwę tabeli, wartość klucza do tej tabeli i nazwę kolumny, baza danych będzie zawierała co najwyżej jedną komórkę zawierającą jeden element danych.

Jeśli pozwolisz, aby komórka zawierała listę, tabelę lub jakąkolwiek inną kolekcję, nie możesz teraz zapewnić dostępu za pomocą klucza do elementów podrzędnych, bez całkowitego przeprojektowania koncepcji klucza.

Kluczowy dostęp do wszystkich danych ma fundamentalne znaczenie dla modelu relacyjnego. Bez tej koncepcji model nie jest relacyjny. Odnośnie do tego, dlaczego model relacyjny jest dobrym pomysłem i jakie mogą być ograniczenia tego dobrego pomysłu, należy spojrzeć na 50 lat zgromadzonych doświadczeń z modelem relacyjnym.

Walter Mitty
źródło
-1

możesz zapisać go jako tekst, który wygląda jak lista i utworzyć funkcję, która może zwrócić dane jako rzeczywistą listę. przykład:

Baza danych:

 _____________________
|  word  | letters    |
|   me   | '[m, e]'   |
|  you   |'[y, o, u]' |  note that the letters column is of type 'TEXT'
|  for   |'[f, o, r]' |
|___in___|_'[i, n]'___|

I funkcja kompilatora list (napisana w Pythonie, ale powinna być łatwa do przetłumaczenia na większość innych języków programowania). TEXT reprezentuje tekst załadowany z tabeli sql. zwraca listę łańcuchów z łańcucha zawierającego listę. jeśli chcesz, aby zwracał ints zamiast łańcuchów, ustaw tryb równy „int”. Podobnie jest z „string”, „bool” czy „float”.

def string_to_list(string, mode):
    items = []
    item = ""
    itemExpected = True
    for char in string[1:]:
        if itemExpected and char not in [']', ',', '[']:
            item += char
        elif char in [',', '[', ']']:
            itemExpected = True
            items.append(item)
            item = ""
    newItems = []
    if mode == "int":
        for i in items:
            newItems.append(int(i))

    elif mode == "float":
        for i in items:
            newItems.append(float(i))

    elif mode == "boolean":
        for i in items:
            if i in ["true", "True"]:
                newItems.append(True)
            elif i in ["false", "False"]:
                newItems.append(False)
            else:
                newItems.append(None)
    elif mode == "string":
        return items
    else:
        raise Exception("the 'mode'/second parameter of string_to_list() must be one of: 'int', 'string', 'bool', or 'float'")
    return newItems

Również tutaj jest funkcja listy do łańcucha na wypadek, gdybyś jej potrzebowała.

def list_to_string(lst):
    string = "["
    for i in lst:
        string += str(i) + ","
    if string[-1] == ',':
        string = string[:-1] + "]"
    else:
        string += "]"
    return string
osoba człowieka
źródło