Partycjonowanie tabeli - nauka

0

Witam, od jakiegoś czasu zagłębiam się w administrowanie MSSQL i miałbym prośbę o sprawdzenie, czy dobrze rozumiem aspekt partycjonowania tabel. Gdy uczyłem się partycjonować korzystałem z różnych książek, tutoriali, czy informacji w internecie, gdzie zazwyczaj partycjonowanie odbywało się za pomocą lat, czy miesięcy. W Tabeli, którą chce partycjonować nie mam takiego odniesienia. Tabela składa się z trzech kolumn tj. id (liczby od 1 i dalej), relation_id (liczby od 1779698 do 682498137) i position (zamienne znaki liter i liczb, np. R3535, które mogą sie powtarzac). Uznałem, że najlepsza do partycjonowania byłaby kolumna relation_id. Poniżej mój kod, którym to zrobiłem:

Stworzyłem pliki i grupy plików względem kolumny relation_id

-- Dodanie nowych grup plików
ALTER DATABASE mip1 
ADD FILEGROUP relation_id_100kk; 

ALTER DATABASE mip1 
ADD FILEGROUP relation_id_200kk; 

ALTER DATABASE mip1 
ADD FILEGROUP relation_id_300kk; 

ALTER DATABASE mip1 
ADD FILEGROUP relation_id_400kk; 

ALTER DATABASE mip1 
ADD FILEGROUP relation_id_500kk; 

ALTER DATABASE mip1 
ADD FILEGROUP relation_id_600kk; 

ALTER DATABASE mip1 
ADD FILEGROUP relation_id_700kk;


-- Dodanie nowych plików do nowych grup plików
ALTER DATABASE mip1 
ADD FILE  
( 
    NAME = 'relation_id_100kk',  
    FILENAME = 'P:\relation_id_100kk.ndf',  
    SIZE = 50GB,  
    MAXSIZE = 70GB,  
    FILEGROWTH = 1024MB  
) 
TO FILEGROUP relation_id_100kk; 

ALTER DATABASE mip1 
ADD FILE  
( 
    NAME = 'relation_id_200kk',  
    FILENAME = 'P:\relation_id_200kk.ndf',  
    SIZE = 50GB,  
    MAXSIZE = 70GB,  
    FILEGROWTH = 1024MB  
) 
TO FILEGROUP relation_id_200kk; 

ALTER DATABASE mip1 
ADD FILE  
( 
    NAME = 'relation_id_300kk',  
    FILENAME = 'P:\relation_id_300kk.ndf',  
    SIZE = 50GB,  
    MAXSIZE = 70GB,  
    FILEGROWTH = 1024MB  
) 
TO FILEGROUP relation_id_300kk; 

ALTER DATABASE mip1 
ADD FILE  
( 
    NAME = 'relation_id_400kk',  
    FILENAME = 'P:\relation_id_400kk.ndf',  
    SIZE = 50GB,  
    MAXSIZE = 70GB,  
    FILEGROWTH = 1024MB  
) 
TO FILEGROUP relation_id_400kk; 

ALTER DATABASE mip1 
ADD FILE  
( 
    NAME = 'relation_id_500kk',  
    FILENAME = 'P:\relation_id_500kk.ndf',  
    SIZE = 50GB,  
    MAXSIZE = 70GB,  
    FILEGROWTH = 1024MB  
) 
TO FILEGROUP relation_id_500kk; 

ALTER DATABASE mip1 
ADD FILE  
( 
    NAME = 'relation_id_600kk',  
    FILENAME = 'P:\relation_id_600kk.ndf',  
    SIZE = 50GB,  
    MAXSIZE = 70GB,  
    FILEGROWTH = 1024MB  
) 
TO FILEGROUP relation_id_600kk; 

ALTER DATABASE mip1 
ADD FILE  
( 
    NAME = 'relation_id_700kk',  
    FILENAME = 'P:\relation_id_700kk.ndf',  
    SIZE = 50GB,  
    MAXSIZE = 70GB,  
    FILEGROWTH = 1024MB  
) 
TO FILEGROUP relation_id_700kk;

Stworzyłem funkcje partycji:

CREATE PARTITION FUNCTION PartitionFunction_Relation_Id (bigint)
AS RANGE LEFT FOR VALUES (
    100000000, 
    200000000, 
    300000000, 
    400000000, 
    500000000, 
    600000000
);

Schemat partycjonowania


CREATE PARTITION SCHEME PartitionScheme_Relation_Id
AS PARTITION PartitionFunction_Relation_Id
TO (
    relation_id_100kk, 
    relation_id_200kk, 
    relation_id_300kk, 
    relation_id_400kk, 
    relation_id_500kk, 
    relation_id_600kk, 
    relation_id_700kk
);

Stworzyłem nową tabelę taką jak tą, którą chce partycjonowac:


CREATE TABLE [mipadm].[u_tt_batch_relation_mounting_positions_partitioned](
    [id] [bigint] IDENTITY(1,1) NOT NULL,
	[tt_relation_id] [bigint] NOT NULL,
	[position] [nvarchar](20) NOT NULL,
 CONSTRAINT [PK_u_tt_batch_relation_mounting_positions_partitioned] PRIMARY KEY CLUSTERED 
(
	[id] ASC, [tt_relation_id]
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF)
) ON PartitionScheme_Relation_Id(tt_relation_id);

Przeniesienie danych ze starej tabeli:

INSERT INTO [mipadm].[u_tt_batch_relation_mounting_positions_partitioned] ([tt_relation_id], [position])
SELECT [tt_relation_id], [position]
FROM [mipadm].[u_tt_batch_relation_mounting_positions];

Usuniecie starej tabeli:

DROP TABLE [mipadm].[u_tt_batch_relation_mounting_positions];

Zmiana nazwy:

EXEC sp_rename 'mipadm.u_tt_batch_relation_mounting_positions_partitioned', 'u_tt_batch_relation_mounting_positions';

Dodatkowo zrobiłem indeksy tak samo jak na starej tabeli:

USE [mip1]
GO

CREATE NONCLUSTERED INDEX [idx_batch_relation_id] ON [mipadm].[u_tt_batch_relation_mounting_positions_partitioned]
(
    [tt_relation_id] ASC
)
INCLUDE([position]) 
WITH (
    PAD_INDEX = OFF, 
    STATISTICS_NORECOMPUTE = OFF, 
    SORT_IN_TEMPDB = OFF, 
    DROP_EXISTING = OFF, 
    ONLINE = OFF, 
    ALLOW_ROW_LOCKS = ON, 
    ALLOW_PAGE_LOCKS = ON, 
    OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF
)
ON PartitionScheme_Relation_Id([tt_relation_id]);
GO

Mam kilka pytań odnośnie tego co zrobiłem:

  1. Czy całość partycjonowania jest dobrze wykonana? Czy powinienem coś poprawić i ewentalnie co konkretnie?
  2. Jak się ma sprawa z tworzeniem plików, a konkretnie z przydzielaniem dla nich miejsca? Nie do końca rozumiem na jakiej zasadzie sie przypisuje określoną ilość miejsca dla pliku.
  3. Czy można bezpośrednio partycjonować stara tabele, bez tworzenia nowej? Czy wiąże się to z czymś konkretnym? Czy lepiej pozostać, przy tworzeniu nowej tabeli i przenoszeniu danych do niej ze starej tabeli?
  4. Czy można po skonczonym partycjonowaniu, jakiś czas pozniej "dorobić" kolejną partycje, np. dla 800k? Czy lepiej zrobic partycję "do przodu"?
  5. Czy jest możliwosć automatyzacji partycjonowania i w jaki sposób to zrobić? Jakieś zródło najlepiej, zebym sie zapoznał :)

Bardzo dziękuję każdemu za udzieloną odpowiedz.

0

Partycjonowanie horyzontalne - technika wprowadzona dlatego że silniki baz danych nie są na tyle elastyczne by dać możliwość dodania własnych algorytmów indeksujących a algorytmy ogólne posysają w przypadkach szczególnych. W wyniku tego taki indeks implementujemy po stronie klienta bazy a strukturę samej bazy naginamy pod algorytm. IT w pigułce.

4

@polko13 : Partycjonowanie robi się z jakiegoś powodu, np.:

  • zarządzanie danymi historycznymi (po czasie) - łatwiej wywalić całą partycje niż wybrać rekordy z tabeli i je usunąć, prościej przenieść "stare i rzadko wykorzystywane" dane na wolniejsze dyski
  • usprawnienie zapytań analitycznych, tzw. "partition wise join", gdzie szybciej zrobisz hash joina na mniejszych obiektach (partycjach), niż na tabeli która takich partycji ma dużo (chodzi o to, że partycje często załadujesz w całości do pamięci, a całej tabeli już nie), czy partition purning (pominięcie partycji, które nie są potrzebne do wykonania zapytania)
  • concurrent access - różni użytkownicy mogą pisać po różnych partycjach, które są na różnych dyskach, albo czytać, wówczas zapytanie analityczne może wykonać się efektywniej

Tabela składa się z trzech kolumn tj. id (liczby od 1 i dalej), relation_id (liczby od 1779698 do 682498137) i position (zamienne znaki liter i liczb, np. R3535, które mogą sie powtarzac). Uznałem, że najlepsza do partycjonowania byłaby kolumna relation_id.

Dlaczego najlepsza? Jakie masz zapytania, które mogą skorzystać na partycjonowaniu?

np. select * from tabela where id between 5 and 100; -> partycjonowanie po relation_id nie jest pomocne dla tego zapytania.

1

@loza_prowizoryczna kontynuuję tu, bo komentarze za krótkie.

Chodzi mi o to że baza danych ma możność profilowania ważności danych choćby po logowaniu zapytań - więc wie jakich danych klient żąda najwięcej/najczęściej i na podstawie tego powinna odpowiednio je przemieszczać pod spodem. - no ale tu nawet nie musi przemieszczać, te najczęściej używane dane lądują przecież w cache'u, a mniej używane wypadają z cache'a po pewnym czasie. Niektóre silniki mogą zarządzać storage w bardziej wyrafinowany sposób (Oraclowy ASM) i balansować dane na różne urządzenia. Niektóre macierze mają mechanizmy, które rzadko używane dane przenoszą na wolniejsze dyski (dowiedziałem się o tym przypadkiem analizując problem wydajnościowy, gdzie po 30 dniach dane po cichu trafiały na wolniejsze dyski, a 31-go był wykonywany billing za cały miesiąc i było wolno... im dłużej problem był analizowany, tym system szybciej działał :D)

A chcąc mieć dane w tym samym bloku dyskowym - miałem na myśli to, że bez partycjonowania (mowa o tym udostępniamy przez silnik bazy, a nie jakichś samoróbkach w oparciu o widoki i union all - bo to raz, że upierdliwe, a dwa, że optymalizatory zapytań nie zawsze są wstanie wepchnąć predykaty do widoku, albo widoku w widoku w widoku... ) do tego samego bloku dyskowego trafić mogą dane np. za 2012 i 2013 rok. Podczas gdy partycjonowanie po roku zapewniłoby, że do tego samego bloku dyskowego trafią dane z tej samej partycji. W efekcie chcąc dostać dane za 2012 rok, nie musisz czytać nadmiarowej ilości bloków (bo w bloku masz mix 2012 i 2013), a jedynie te bloki, na których jest partycja za 2012. Dzięki temu możesz rozdzielić I/O dla partycji, nie tylko po dyskach, ale i ścieżkach, które do dysków prowadzą (np. osobny interfejs sieciowy, czy osobne porty na switchu, osobna pula dysków na macierzy itd.).

Tej historii o nakładaniu constraintów i składowanych procedura nie rozumiem szczerze powiedziawszy. Nikt (chyba?) przecież nie robi partycjonowania widokami, tylko korzysta z natywnych mechanizmów bazodanowych. Chyba, że zaczynamy łączyć tematykę partycjonowania poziomego, pionowego i shardingu? Ale wówczas byłoby dobrze ustalić kontekst.

0
yarel napisał(a):
  • no ale tu nawet nie musi przemieszczać, te najczęściej używane dane lądują przecież w cache'u, a mniej używane wypadają z cache'a po pewnym czasie

To jest standard przy tiered storage. Ale jak widzisz ma to swoje ograniczenia - bo to jest system plików a ten ma trochę ciut inną charakterystykę niż baza danych. Dlatego napisałem że ma to sens jeśli jedno nie wie o drugim bo dość łatwo doprowadzić do nieoptymalnego rozłożenia danych (albo nawet zajeżdżenia SSD przy dużych ruchu).

  • Niektóre silniki mogą zarządzać storage w bardziej wyrafinowany sposób (Oraclowy ASM) i balansować dane na różne urządzenia.

No, czyli drogie poczciwe Oracle jednak przygotowało enterprajsowe rozwiązanie w oparciu o wertykalną unifikację. Pytanie czy to produkt wyjątkowy czy konkurencja ma coś podobnego.

A chcąc mieć dane w tym samym bloku dyskowym - miałem na myśli to, że bez partycjonowania (mowa o tym udostępniamy przez silnik bazy, a nie jakichś samoróbkach w oparciu o widoki i union all - bo to raz, że upierdliwe, a dwa, że optymalizatory zapytań nie zawsze są wstanie wepchnąć predykaty do widoku, albo widoku w widoku w widoku... ) do tego samego bloku dyskowego trafić mogą dane np. za 2012 i 2013 rok. Podczas gdy partycjonowanie po roku zapewniłoby, że do tego samego bloku dyskowego trafią dane z tej samej partycji.

Czyli innymi słowy obecnie sposobem na uniknięcie fragmentacji w storage'u bazy jest obecnie dodawanie sztucznych bytów (tabel)? Interesującą drogę w takim razie przebyliśmy od teorii normalizacji ;)

W efekcie chcąc dostać dane za 2012 rok, nie musisz czytać nadmiarowej ilości bloków (bo w bloku masz mix 2012 i 2013), a jedynie te bloki, na których jest partycja za 2012. Dzięki temu możesz rozdzielić I/O dla partycji, nie tylko po dyskach, ale i ścieżkach, które do dysków prowadzą (np. osobny interfejs sieciowy, czy osobne porty na switchu, osobna pula dysków na macierzy itd.).

Nie rozumiem tego czytania nadmiarowych ilości bloków. Ma to jedynie znaczenie gdy:

  • predykat jedzie po kolumnie/kolumnach bez indeksu
  • kolumny te nie mają naturalnego porządku

Tak czy siak wszystko rozbija się o jedno - niemożność dodawania własnych algorytmów indeksujących. Partycjonowanie jako strukturę dla load balancera sobie pominę bo dla mnie to podpada pod samoróbkę ale wyższego rzędu ;)

Tej historii o nakładaniu constraintów i składowanych procedura nie rozumiem szczerze powiedziawszy. Nikt (chyba?) przecież nie robi partycjonowania widokami, tylko korzysta z natywnych mechanizmów bazodanowych. Chyba, że zaczynamy łączyć tematykę partycjonowania poziomego, pionowego i shardingu? Ale wówczas byłoby dobrze ustalić kontekst.

Ustalmy kontekst: moją profesjonalnę przygodę z realnymi DB zakończyłem na InnoDB i hobbystycznych eksperymentach z Postgresem. Obecnie jedyne bazy jakie tykam tylko to sqlite no i na upartego GraphQL. Nie robię na zapleczu i w sumie nie żałuję patrząc gdzie to wszystko podążyło. Jak rozstawałem się z tym tematem to założenia były proste: relacyjne bazy danych były gigantycznymi monolitami opartymi o deklaratywny język do wyciągania danych, był olbrzymi nacisk na normalizację by unikać jak się da redundancji danych + żeby zadeklarowana struktura była jak najbardziej czytelna dla administratora i dała się w miarę prosto obudować procedurami w celu zapewnienia jak najlepszej kontroli nad spójnością danych. Wszystko od storage'u poprzez nawet ukryte indeksy (dodawane w razie potrzeby przez silnik) miało się odbywać pod spodem i być przeźroczyste dla klienta/nawet dla administratora.

Mój komentarz (nawet nie post) dotyczył jedynie tego że zamiast tego ideału z tego co widzę to wszystko podążyło do tego że struktura bazy zamiast odzwierciedlać naturę składowych danych to podążyła w kierunku quasi-optymalizacji dla ułomności silnika co dla ideałów stojących za powstaniem SQL jest praktycznie abominacją :D Oczywiście wszystko wynika pewnie z tego że taniej było postawić jakąś ułomną implementacją MySQL/Postgresa a ułomności wychodzące przy wyższych wolumenach danych łatać ww. technikami.

Koniec rantu, wracam do swojej loży.

0

@loza_prowizoryczna

Nie rozumiem tego czytania nadmiarowych ilości bloków.

Może tak, żeby silnik coś zrobił z danymi, to najpierw musi je odczytać, czyli wykonać operację I/O. Nawet jeśli interesuje Cię tylko 1 bit, to minimalny rozmiar takiej operacji I/O wynika z interfejsu urządzenia (np. stare dyski obrotowe - dostajesz cały sektor, 512 bajtów; dyski SSD - 4kb ; +/- to że masz jakieś enterprajzowe ustrojstwa z większym min. rozmiarem I/O), do tego dochodzi page size (rozmiar strony pamięci), czy rozmiar I/O po stronie samej bazy (może preferować czytanie multiblokami - np. dla systemów, które korzystają bardzo z indeksów). Są to różne koncepty, ale powiązane (przez fakt, że dane z dysku trafiają do pamięci procesu). Jeśli w bloku dyskowym upchniesz 10 wierszy z tej samej partycji, to wykonując 1 I/O masz dane o 10 wierszach. Jeśli nie masz partycjonowania, to może się zdarzyć, że te 10 wierszy jest rozsmarowane po 10 blokach dyskowych (-> wykonasz 10x więcej I/O), żeby dostać te 10 wierszy.

Ma to jedynie znaczenie gdy:
predykat jedzie po kolumnie/kolumnach bez indeksu
kolumny te nie mają naturalnego porządku

Nawet jak masz indeks, to musisz go przeczytać, a wtedy lepiej generować mniej I/O niż więcej :) Indeksy mogą być również partycjonowane.
Co do naturalnego porządku, kolumna z wartościami wskazującymi na Kraj. Nie ma naturalnego porządku (ale Polska górą ;) ) i tu jednak widzę różnicę czy w bloku będziemy mieli 10 krajów, czy 10x ten sam kraj.

Co do quasi-optymalizacji to rozdzielenie modelu fizycznego od logicznego chyba było od dawna. Konkurencja dla Oracle, ma też swoje rozwiązania (storage groups, file groups, dynamic tiering).
Ale tak wracając do głównego wątku, to op konkretny case do przećwiczenia. Nie ma mu co hijackować wątku ;-)

0
yarel napisał(a):

@polko13 : Partycjonowanie robi się z jakiegoś powodu, np.:

  • zarządzanie danymi historycznymi (po czasie) - łatwiej wywalić całą partycje niż wybrać rekordy z tabeli i je usunąć, prościej przenieść "stare i rzadko wykorzystywane" dane na wolniejsze dyski
  • usprawnienie zapytań analitycznych, tzw. "partition wise join", gdzie szybciej zrobisz hash joina na mniejszych obiektach (partycjach), niż na tabeli która takich partycji ma dużo (chodzi o to, że partycje często załadujesz w całości do pamięci, a całej tabeli już nie), czy partition purning (pominięcie partycji, które nie są potrzebne do wykonania zapytania)
  • concurrent access - różni użytkownicy mogą pisać po różnych partycjach, które są na różnych dyskach, albo czytać, wówczas zapytanie analityczne może wykonać się efektywniej

Tabela składa się z trzech kolumn tj. id (liczby od 1 i dalej), relation_id (liczby od 1779698 do 682498137) i position (zamienne znaki liter i liczb, np. R3535, które mogą sie powtarzac). Uznałem, że najlepsza do partycjonowania byłaby kolumna relation_id.

Dlaczego najlepsza? Jakie masz zapytania, które mogą skorzystać na partycjonowaniu?

np. select * from tabela where id between 5 and 100; -> partycjonowanie po relation_id nie jest pomocne dla tego zapytania.

Dziękuję za odpowiedz. Uznałem, że najlepsza, bo:

  1. To zdecydowanie najwieksza tabela w bazie danych w której ucze się/testuje (klon bazy w firmie)
  2. Istnieje procedura, która zbiera dane do różnych tabeli, która następnie wstawia dane do tej tabeli.

Czy jest mi ktoś w stanie konkretnie odpowiedzieć na moje pytania powyżej? Najbardziej zależy mi na informacji, czy to co zrobiłem jest dobrze zrobione. Poprzednio, gdy partycjonowanie tabeli, to partycjonowałem po dacie. Pytania poniżej:

  1. Czy całość partycjonowania jest dobrze wykonana? Czy powinienem coś poprawić i ewentalnie co konkretnie?
  2. Jak się ma sprawa z tworzeniem plików, a konkretnie z przydzielaniem dla nich miejsca? Nie do końca rozumiem na jakiej zasadzie sie przypisuje określoną ilość miejsca dla pliku.
  3. Czy można bezpośrednio partycjonować stara tabele, bez tworzenia nowej? Czy wiąże się to z czymś konkretnym? Czy lepiej pozostać, przy tworzeniu nowej tabeli i przenoszeniu danych do niej ze starej tabeli?
  4. Czy można po skonczonym partycjonowaniu, jakiś czas pozniej "dorobić" kolejną partycje, np. dla 800k? Czy lepiej zrobic partycję "do przodu"?
  5. Czy jest możliwosć automatyzacji partycjonowania i w jaki sposób to zrobić? Jakieś zródło najlepiej, zebym sie zapoznał

Dziękuję za każda udzieloną odpowiedz, jednakżę jestem na takim etapie nauki, ze chciałbym najpierw ten konkretnie aspekt douczyć się, zanim wezmę sie za kolejny. 😀

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.