Wydajność UUID w MySQL?

86

Rozważamy użycie wartości UUID jako kluczy podstawowych dla naszej bazy danych MySQL. Wstawiane dane są generowane z dziesiątek, setek, a nawet tysięcy zdalnych komputerów i są wstawiane z szybkością 100-40 000 wstawień na sekundę, a my nigdy nie wykonamy żadnych aktualizacji.

Sama baza danych zazwyczaj osiąga około 50 milionów rekordów, zanim zaczniemy wybierać dane, więc nie jest to ogromna baza danych, ale też nie mała. Planujemy również działać na InnoDB, chociaż jesteśmy otwarci na zmianę tego, jeśli istnieje lepszy silnik do tego, co robimy.

Byliśmy gotowi do użycia identyfikatora UUID Java Type 4, ale podczas testów zauważyliśmy dziwne zachowanie. Po pierwsze, przechowujemy jako varchar (36) i teraz zdaję sobie sprawę, że lepiej byłoby użyć binarnego (16) - chociaż nie jestem pewien, o ile lepiej.

Większe pytanie brzmi: jak bardzo te losowe dane psują indeks, skoro mamy 50 milionów rekordów? Czy byłoby lepiej, gdybyśmy na przykład użyli identyfikatora UUID typu 1, w którym skrajne lewe bity miały znacznik czasu? A może powinniśmy całkowicie porzucić UUID i rozważyć klucze podstawowe auto_increment?

Szukam ogólnych przemyśleń / wskazówek na temat wydajności różnych typów UUID, gdy są one przechowywane jako indeks / klucz podstawowy w MySQL. Dzięki!

Patrick Lightbody
źródło
2
brakuje jednego ważnego szczegółu: czy klucze podstawowe mają być generowane przez serwer logowania, czy przez same komputery klienckie?
1
@hop są generowane przez 10-1000 klientów, którzy wstawiają dane
Patrick Lightbody
Gdzie w swoim scenariuszu potrzebujesz uniwersalnej wyjątkowości? Radzę trzymać się auto_increment i używać oddzielnego pola do opisania zdalnego komputera, który wysyła dane. Nie ma potrzeby odkrywania na nowo koła tutaj.
Theodore Zographos,

Odpowiedzi:

36

UUID to uniwersalnie unikalny identyfikator. To uniwersalna część, którą powinieneś rozważyć tutaj.

Czy naprawdę potrzebujesz, aby identyfikatory były uniwersalne i niepowtarzalne? Jeśli tak, to identyfikatory UUID mogą być jedynym wyborem.

Chciałbym zdecydowanie sugerują, że jeśli zrobić użytku UUID, można przechowywać je jako liczby, a nie jako ciąg znaków. Jeśli masz ponad 50 milionów rekordów, oszczędność miejsca na dysku poprawi Twoją wydajność (chociaż nie mogę powiedzieć, o ile).

Jeśli twoje identyfikatory nie muszą być unikalne uniwersalnie, to nie sądzę, że możesz zrobić o wiele lepiej niż po prostu używając auto_increment, co gwarantuje, że identyfikatory będą unikalne w tabeli (ponieważ wartość będzie rosła za każdym razem)

Dancrumb
źródło
2
Ciekawy punkt; spowodowałoby to zrównoleglenie generowania kluczy. Uważam, że zwiększyłoby to wydajność generowania kluczy. Jednak wybierasz wydajność INSERT zamiast wydajności SELECT, jeśli używasz VARCHAR do przechowywania identyfikatora UUID. Zdecydowanie powinieneś wybrać VARBINARY do przechowywania, aby zapewnić wydajność SELECT. Dodatkowy krok może wpłynąć na wydajność INSERT, ale opłaci ci się poprawa wydajności SELECT.
Dancrumb,
12
Skończyło się na tym, że przeprowadziliśmy testy porównawcze na rzeczywistych danych i identyfikatorach GUID bez kluczy, które były dość szybkie, identyfikatory GUID z kluczami były okropne (nawet gdy były przechowywane jako BINARY), a int w / AUTO_COMPLETE był najszybszy. Myślę, że w naszym przypadku rzeczywiście brakowało nam lasu z drzew, ponieważ generowanie sekwencji wydawało się nieistotne w porównaniu z kosztem przechowywania większej ilości danych + posiadanie naprawdę kiepskiego BTREE z powodu losowości GUID
Patrick Lightbody
1
przechowywać jako liczbę oznacza przechowywanie w formacie binarnym? ale format binarny jest nieczytelny dla człowieka. To jest powolne, ponieważ duże bajty klucza podstawowego uuid? Jeśli tak, to mógłbym zapisać autoinkrementację w innej kolumnie dla uuid. Wtedy wydajność nie ucierpi. Czy mam rację?
Chamnap,
4
Ściśle mówiąc, UUID jest uniwersalny , co oznacza, że ​​nigdy nie pojawi się nigdzie indziej na świecie. Potrzebujesz tego tylko wtedy, gdy udostępniasz swoje dane publicznie. Jeśli chodzi o przechowywanie UUID jako liczby, nie mam na myśli binaryformatu. Mam na myśli liczbę 128-bitową, a nie ciąg 288-bitowy. Na przykład słowo „cześć” w kodzie ASCII to 68 65 6C 6C 6Fliczba 448 378 203 247. Przechowywanie ciągu „68656C6C6F” wymaga 10 bajtów. Numer 448.378.203.247 wymaga tylko 5. W sumie, chyba że naprawdę potrzebujesz pierwszego U w UUID, nie możesz zrobić dużo lepiej niżauto_increment
Dancrumb
1
@Chamnap: Zaproponuj zadanie pytania o przepełnienie stosu: o)
Dancrumb,
78

W mojej pracy używamy UUID jako PK. Z doświadczenia mogę ci powiedzieć, że NIE UŻYWAJ ICH jako PK (nawiasem mówiąc, SQL Server).

To jedna z tych rzeczy, które jeśli masz mniej niż 1000 nagrań, to jest ok, ale kiedy masz miliony, to najgorsza rzecz, jaką możesz zrobić. Czemu? Ponieważ UUID nie są sekwencyjne, więc za każdym razem, gdy wstawiany jest nowy rekord, MSSQL musi spojrzeć na odpowiednią stronę, aby wstawić rekord, a następnie wstawić rekord. Naprawdę brzydką konsekwencją tego jest to, że strony kończą się w różnych rozmiarach i kończą na fragmentacji, więc teraz musimy przeprowadzać okresową de-fragmentację.

Gdy użyjesz autoinkrementacji, MSSQL zawsze przejdzie do ostatniej strony, a skończysz z równymi rozmiarami stron (w teorii), więc wydajność wybierania tych rekordów jest znacznie lepsza (również dlatego, że INSERT nie będą blokować tabeli / strony dla Tak długo).

Jednak dużą zaletą używania UUID jako PK jest to, że jeśli mamy klastry DB, nie będzie konfliktów podczas łączenia.

Poleciłbym następujący model: 1. PK INT Identity 2. Dodatkowa kolumna generowana automatycznie jako UUID.

W ten sposób proces łączenia jest możliwy (UUID byłby twoim PRAWDZIWYM kluczem, podczas gdy PK byłby czymś tymczasowym, co zapewnia dobrą wydajność).

UWAGA: Najlepszym rozwiązaniem jest użycie NEWSEQUENTIALID (jak mówiłem w komentarzach), ale w przypadku starszej aplikacji, która ma niewiele czasu na refaktoryzację (i co gorsza, nie kontroluje wszystkich wstawek), nie jest to możliwe. Ale rzeczywiście od 2017 roku powiedziałbym, że najlepszym rozwiązaniem jest tutaj NEWSEQUENTIALID lub Guid.Comb z NHibernate.

Mam nadzieję że to pomoże

Kat Lim Ruiz
źródło
Naprawdę nie wiem, co oznaczają te terminy, ale faktem jest, że indeksy muszą być ponownie indeksowane co miesiąc. Jeśli to, o czym wspomniałeś, eliminuje zadanie ponownego zindeksowania, nie wiem, ale mogę zapytać.
Kat Lim Ruiz,
3
Coś, o czym myślałem, to to, że może to nie działać tak dobrze w relacjach rodzic-dziecko. W tym przypadku myślę, że musisz dodać w tabeli podrzędnej: parent-pk, parent-guid. W przeciwnym razie możesz utracić odniesienia między bazami danych. Nie myślałem o tym zbyt wiele, ani nie robiłem żadnego przykładu, ale może to być potrzebne
Kat Lim Ruiz
4
@KatLimRuiz na serwerze sql możesz użyć NEWSEQUENTIALID () technet.microsoft.com/en-us/library/ms189786.aspx, aby uniknąć problemu z wydajnością
giammin
Rzeczywiście, ale NEWSEQUENTIALID działa tylko jako DEFAULT. Musisz więc zaprojektować cały swój DAL wokół tego, co jest w porządku w przypadku nowych projektów, ale nie jest takie łatwe w przypadku dużego dziedzictwa
Kat Lim Ruiz,
@KatLimRuiz genius. To świetny kompromis
jmgunn87,
26

Należy wziąć pod uwagę fakt, że Autoinkrementy są generowane pojedynczo i nie można ich rozwiązać za pomocą rozwiązania równoległego. Walka o używanie UUID ostatecznie sprowadza się do tego, co chcesz osiągnąć, w porównaniu z tym, co potencjalnie poświęcasz.

Krótko o wydajności :

Identyfikator UUID, taki jak powyższy, ma 36 znaków, w tym myślniki. Jeśli zapiszesz ten VARCHAR (36), dramatycznie zmniejszysz wydajność porównania. To jest twój klucz podstawowy, nie chcesz, aby był powolny.

Na poziomie bitowym UUID ma 128 bitów, co oznacza, że ​​zmieści się w 16 bajtach, pamiętaj, że nie jest to bardzo czytelne dla człowieka, ale utrzyma niski poziom pamięci i jest tylko 4 razy większy niż 32-bitowy int, czyli 2 razy większe niż 64-bitowe int. Użyję VARBINARY (16) Teoretycznie może to działać bez dużego narzutu.

Polecam przeczytanie następujących dwóch postów:

Sądzę, że między tymi dwoma odpowiadają na twoje pytanie.

Kyle Rosendo
źródło
2
Właściwie przeczytałem oba te artykuły przed wysłaniem tego pytania i nadal nie mam tutaj dobrej odpowiedzi. Na przykład nie mów o UUIDS typu 1 i 4 :(
Patrick Lightbody
W porządku, zaktualizowałem odrobinę moją odpowiedź. Nie sądzę jednak, aby zapewniało to zbyt wiele dodatkowych informacji.
Kyle Rosendo
@Patrick: zadałeś zbyt wiele różnych tematów w swoim pytaniu.
1
9 lat później, ale należy również zauważyć, że w przeciwieństwie do identyfikatorów całkowitych, aplikacje mogą bezpiecznie generować UUID, całkowicie usuwając generowanie z bazy danych. Manipulowanie identyfikatorami UUID w celu optymalizacji wydajności (oparte na sygnaturach czasowych, ale zmodyfikowane, aby można je było naiwnie sortować) jest znacznie łatwiejsze w prawie każdym języku innym niż SQL. Na szczęście prawie wszystkie dzisiejsze bazy danych (w tym MySQL) obsługują klucze główne UUID znacznie lepiej niż kiedyś.
Miles Elam,
5

Staram się unikać UUID po prostu dlatego, że przechowywanie go i używanie go jako klucza podstawowego jest trudne, ale są też zalety. Głównym jest to, że są WYJĄTKOWE.

Zwykle rozwiązuję problem i unikam UUID, używając podwójnych pól kluczy.

COLLECTOR = UNIKALNY PRZYPISANY DO MASZYNY

ID = REKORD ZBIERANY PRZEZ KOLEKCJĘ (pole auto_inc)

To daje mi dwie rzeczy. Szybkość pól automatycznego włączania i niepowtarzalność danych przechowywanych w centralnej lokalizacji po ich zebraniu i zgrupowaniu. Wiem też, przeglądając dane, w których zostały zebrane, co często jest dość istotne dla moich potrzeb.

Widziałem wiele przypadków, gdy miałem do czynienia z innymi zestawami danych dla klientów, w których zdecydowali się użyć UUID, ale nadal mam pole, w którym zebrano dane, co naprawdę jest stratą czasu. Po prostu użycie dwóch (lub więcej w razie potrzeby) pól jako klucza naprawdę pomaga.

Właśnie widziałem zbyt wiele hitów wydajnościowych przy użyciu UUID. Czują się jak oszust ...

Glenn J. Schworak
źródło
3

Zamiast centralnie generować unikalne klucze dla każdego wstawienia, co powiesz na przydzielanie bloków kluczy do poszczególnych serwerów? Kiedy skończą im się klucze, mogą poprosić o nowy blok. Następnie rozwiązujesz problem narzutu, podłączając każdą wkładkę.

Keyserver utrzymuje następny dostępny identyfikator

  • Serwer 1 żąda blokady identyfikatora.
  • Serwer kluczy zwraca (1,1000)
    Serwer 1 może wstawić 1000 rekordów, dopóki nie zażąda nowego bloku
  • Serwer 2 żąda bloku indeksu.
  • Zwroty serwera kluczy (1001,2000)
  • itp...

Mógłbyś wymyślić bardziej wyrafinowaną wersję, w której serwer mógłby zażądać liczby potrzebnych kluczy lub zwrócić nieużywane bloki do serwera kluczy, który wtedy oczywiście musiałby utrzymywać mapę używanych / nieużywanych bloków.

Bouke Versteegh
źródło
Ciekawa sugestia teoretyczna. W praktyce byłoby to trudne do opanowania. Bardziej praktycznym rozwiązaniem byłaby prawdopodobnie odpowiedź Schworaka.
Simon East
2

Przypisałbym każdemu serwerowi numeryczny identyfikator w sposób transakcyjny. Następnie każdy wstawiony rekord będzie automatycznie zwiększał swój własny licznik. Połączenie ServerID i RecordID będzie unikalne. Pole ServerID może być indeksowane, a przyszłe wybieranie wydajności na podstawie ServerID (w razie potrzeby) może być znacznie lepsze.

Nikolai
źródło
2

Krótka odpowiedź jest taka, że ​​wiele baz danych ma problemy z wydajnością (w szczególności z dużymi wolumenami INSERT) z powodu konfliktu między ich metodą indeksowania a celową entropią UUID w bitach wyższego rzędu. Istnieje kilka typowych hacków:

  • wybierz inny typ indeksu (np. nieklastrowany w MSSQL), któremu to nie przeszkadza
  • zmontuj dane, aby przenieść entropię do bitów niższego rzędu (np. zmiana kolejności bajtów UUID V1 w MySQL)
  • uczyń identyfikator UUID kluczem pomocniczym z automatycznym zwiększaniem int klucz podstawowy

... ale to wszystko są hacki - i to prawdopodobnie kruche.

Najlepszą odpowiedzią, ale niestety najwolniejszą, jest zażądanie od dostawcy ulepszenia produktu, aby mógł on traktować UUID jako klucze podstawowe, tak jak każdy inny typ. Nie powinni zmuszać Cię do rzucania własnego, na wpół upieczonego hacka, aby nadrobić swoje niepowodzenie w rozwiązaniu tego, co stało się powszechnym przypadkiem użycia i będzie się nadal rozwijać.

StephenS
źródło
1

A co z ręcznie wykonanym UID? Nadaj każdemu z tysięcy serwerów identyfikator i uczyń klucz podstawowy kluczem combo autoincrement, MachineID ???

MindStalker
źródło
Myślałem o tym i być może będę musiał przeprowadzić testy porównawcze. Nawet tymczasowa lokalna sekwencja na każdej z 1000 maszyn w połączeniu ze znacznikiem czasu może wystarczyć. Np .: machine_id + temp_seq + timestamp
Patrick Lightbody
Czy jest możliwe, aby temp_sequence resetował każdy znacznik czasu? Nie jestem pewny.
MindStalker
1

Ponieważ klucz podstawowy jest generowany zdecentralizowany, i tak nie masz możliwości korzystania z auto_increment.

Jeśli nie musisz ukrywać tożsamości komputerów zdalnych, użyj identyfikatorów UUID typu 1 zamiast identyfikatorów UUID. Są łatwiejsze do wygenerowania i przynajmniej nie szkodzą wydajności bazy danych.

To samo dotyczy varchar (tak naprawdę char) kontra binarne: może to tylko pomóc. Czy to naprawdę ważne, o ile poprawiono wydajność?


źródło
0

Zdaję sobie sprawę, że to pytanie jest dość stare, ale trafiłem na to w swoich badaniach. Od tego czasu wydarzyło się wiele rzeczy (dyski SSD są wszechobecne, InnoDB ma aktualizacje itp.).

W swoich badaniach znalazłem ten dość interesujący post dotyczący wydajności:

twierdząc, że ze względu na losowość GUID / UUID drzewa indeksu mogą być raczej niezrównoważone. w bazie wiedzy MariaDB znalazłem inny post sugerujący rozwiązanie. Ale od tego czasu zajmuje się tym nowy UUID_TO_BIN . Ta funkcja jest dostępna tylko w MySQL (testowana wersja 8.0.18), a nie w MariaDB (wersja 10.4.10)

TL; DR: Przechowuj UUID jako przekonwertowane / zoptymalizowane wartości BINARY (16).

theking2
źródło