Wskaźnik niepowtarzalności narzutów

14

Prowadzę ciągłą debatę z różnymi programistami w moim biurze na temat kosztu indeksu i tego, czy unikalność jest korzystna czy kosztowna (prawdopodobnie obie). Sednem problemu są nasze konkurujące zasoby.

tło

Wcześniej czytałem dyskusję, w której stwierdzono, że Uniqueindeks nie jest dodatkowym kosztem do utrzymania, ponieważ Insertoperacja domyślnie sprawdza, gdzie mieści się w drzewie B, a jeśli duplikat zostanie znaleziony w nieunikalnym indeksie, dołącza unikalizator do koniec klucza, ale w przeciwnym razie wstawia bezpośrednio. W tej sekwencji zdarzeń Uniqueindeks nie ma dodatkowych kosztów.

Mój współpracownik zwalcza to stwierdzenie, mówiąc, że Uniquejest egzekwowane jako druga operacja po poszukiwaniu nowej pozycji w drzewie B, a zatem jego utrzymanie jest bardziej kosztowne niż indeks nieunikalny.

W najgorszym przypadku widziałem tabele z kolumną tożsamości (z natury unikatową), która jest kluczem do klastrowania tabeli, ale jest wyraźnie określona jako nie-unikalna. Z drugiej strony najgorsza jest moja obsesja na punkcie wyjątkowości, a wszystkie indeksy są tworzone jako unikalne, a gdy nie jest możliwe zdefiniowanie wyraźnie unikalnej relacji do indeksu, dołączam PK tabeli na końcu indeksu, aby zapewnić wyjątkowość jest gwarantowana.

Często biorę udział w recenzowaniu kodu dla zespołu programistów i muszę być w stanie podać ogólne wytyczne dla nich. Tak, każdy indeks powinien zostać oceniony, ale jeśli masz pięć serwerów z tysiącami tabel i aż dwadzieścia indeksów w tabeli, musisz być w stanie zastosować kilka prostych reguł, aby zapewnić określony poziom jakości.

Pytanie

Czy wyjątkowość ma dodatkowy koszt zaplecza w Insertporównaniu z kosztem utrzymania nieunikalnego indeksu? Po drugie, co jest złego w dodawaniu klucza podstawowego tabeli na końcu indeksu, aby zapewnić unikalność?

Przykładowa definicja tabeli

create table #test_index
    (
    id int not null identity(1, 1),
    dt datetime not null default(current_timestamp),
    val varchar(100) not null,
    is_deleted bit not null default(0),
    primary key nonclustered(id desc),
    unique clustered(dt desc, id desc)
    );

create index
    [nonunique_nonclustered_example]
on #test_index
    (is_deleted)
include
    (val);

create unique index
    [unique_nonclustered_example]
on #test_index
    (is_deleted, dt desc, id desc)
include
    (val);

Przykład

Przykład dodania Uniqueklucza na końcu indeksu znajduje się w jednej z naszych tabel faktów. Jest Primary Keyto jest Identitykolumna. Jednakże, Clustered Indexto zamiast kolumny schemat partycjonowania, a następnie trzech zagranicznych kluczowych wymiarów bez wyjątkowości. Wybrana wydajność na tym stole jest fatalna i często uzyskuję lepsze czasy wyszukiwania, korzystając Primary Keyz wyszukiwania kluczowego, zamiast korzystać z niego Clustered Index. Inne tabele, które mają podobny projekt, ale mają Primary Keydołączone na końcu, mają znacznie lepszą wydajność.

-- date_int is equivalent to convert(int, convert(varchar, current_timestamp, 112))
if not exists(select * from sys.partition_functions where [name] = N'pf_date_int')
    create partition function 
        pf_date_int (int) 
    as range right for values 
        (19000101, 20180101, 20180401, 20180701, 20181001, 20190101, 20190401, 20190701);
go

if not exists(select * from sys.partition_schemes where [name] = N'ps_date_int')
    create partition scheme 
        ps_date_int
    as partition 
        pf_date_int all 
    to 
        ([PRIMARY]);
go

if not exists(select * from sys.objects where [object_id] = OBJECT_ID(N'dbo.bad_fact_table'))
    create table dbo.bad_fact_table
        (
        id int not null, -- Identity implemented elsewhere, and CDC populates
        date_int int not null,
        dt date not null,
        group_id int not null,
        group_entity_id int not null, -- member of group
        fk_id int not null,
        -- tons of other columns
        primary key nonclustered(id, date_int),
        index [ci_bad_fact_table] clustered (date_int, group_id, group_entity_id, fk_id)
        )
    on ps_date_int(date_int);
go

if not exists(select * from sys.objects where [object_id] = OBJECT_ID(N'dbo.better_fact_table'))
    create table dbo.better_fact_table
        (
        id int not null, -- Identity implemented elsewhere, and CDC populates
        date_int int not null,
        dt date not null,
        group_id int not null,
        group_entity_id int not null, -- member of group
        -- tons of other columns
        primary key nonclustered(id, date_int),
        index [ci_better_fact_table] clustered(date_int, group_id, group_entity_id, id)
        )
    on ps_date_int(date_int);
go
Solonotix
źródło

Odpowiedzi:

16

Często biorę udział w recenzowaniu kodu dla zespołu programistów i muszę być w stanie podać ogólne wytyczne dla nich.

Środowisko, w którym obecnie pracuję, ma 250 serwerów z 2500 bazami danych. Pracowałem na systemach z 30 000 bazami danych . Wytyczne dotyczące indeksowania powinny obracać się wokół konwencji nazewnictwa itp., A nie „regułami”, jakie kolumny należy uwzględnić w indeksie - każdy indywidualny indeks powinien być zaprojektowany tak, aby był poprawnym indeksem dla tej konkretnej reguły biznesowej lub kodu dotykającego tabeli.

Czy wyjątkowość ma dodatkowy koszt zaplecza w Insertporównaniu z kosztem utrzymania nieunikalnego indeksu? Po drugie, co jest złego w dodawaniu klucza podstawowego tabeli na końcu indeksu, aby zapewnić unikalność?

Dodanie kolumny klucza podstawowego na końcu nieunikalnego indeksu, aby uczynić go wyjątkowym, wydaje mi się anty-wzorkiem. Jeśli reguły biznesowe nakazują, aby dane były unikalne, dodaj unikalne ograniczenie do kolumny; który automatycznie utworzy unikalny indeks. Jeśli indeksujesz kolumnę pod kątem wydajności , dlaczego miałbyś dodawać kolumnę do indeksu?

Nawet jeśli twoje przypuszczenie, że wymuszanie wyjątkowości nie dodaje żadnego dodatkowego obciążenia, jest prawidłowe (co nie jest w niektórych przypadkach), co rozwiązujesz, niepotrzebnie komplikując indeks?

W konkretnym przypadku dodania klucza podstawowego na końcu klucza indeksu, aby można było wprowadzić definicję indeksu zawierającą UNIQUEmodyfikator, w rzeczywistości nie ma on różnicy w stosunku do fizycznej struktury indeksu na dysku. Wynika to z natury struktury kluczy indeksów B-drzewa, ponieważ zawsze muszą być unikalne.

Jak wspomniał David Browne w komentarzu:

Ponieważ każdy indeks nieklastrowany jest przechowywany jako indeks unikalny, wstawienie do indeksu unikalnego nie wiąże się z dodatkowymi kosztami. W rzeczywistości jedynym dodatkowym kosztem byłoby niezadeklarowanie klucza kandydującego jako unikalnego indeksu, co spowodowałoby dołączenie klastrowanych kluczy indeksu do kluczy indeksu.

Weź następujący minimalnie kompletny i weryfikowalny przykład :

USE tempdb;

DROP TABLE IF EXISTS dbo.IndexTest;
CREATE TABLE dbo.IndexTest
(
    id int NOT NULL
        CONSTRAINT IndexTest_pk
        PRIMARY KEY
        CLUSTERED
        IDENTITY(1,1)
    , rowDate datetime NOT NULL
);

Dodam dwa indeksy, które są identyczne, z wyjątkiem dodania klucza podstawowego na końcu drugiej definicji klucza indeksu:

CREATE INDEX IndexTest_rowDate_ix01
ON dbo.IndexTest(rowDate);

CREATE UNIQUE INDEX IndexTest_rowDate_ix02
ON dbo.IndexTest(rowDate, id);

Następnie przejdziemy do kilku wierszy do tabeli:

INSERT INTO dbo.IndexTest (rowDate)
VALUES (DATEADD(SECOND, 0, GETDATE()))
     , (DATEADD(SECOND, 0, GETDATE()))
     , (DATEADD(SECOND, 0, GETDATE()))
     , (DATEADD(SECOND, 1, GETDATE()))
     , (DATEADD(SECOND, 2, GETDATE()));

Jak widać powyżej, trzy wiersze zawierają tę samą wartość dla rowDatekolumny, a dwa wiersze zawierają unikalne wartości.

Następnie przyjrzymy się fizycznym strukturom stron dla każdego indeksu, używając nieudokumentowanej DBCC PAGEkomendy:

DECLARE @dbid int = DB_ID();
DECLARE @fileid int;
DECLARE @pageid int;
DECLARE @indexid int;

SELECT @fileid = ddpa.allocated_page_file_id
    , @pageid = ddpa.allocated_page_page_id
FROM sys.indexes i 
CROSS APPLY sys.dm_db_database_page_allocations(DB_ID(), i.object_id, i.index_id, NULL, 'LIMITED') ddpa
WHERE i.name = N'IndexTest_rowDate_ix01'
    AND ddpa.is_allocated = 1
    AND ddpa.is_iam_page = 0;

PRINT N'*************************************** IndexTest_rowDate_ix01 *****************************************';
DBCC TRACEON(3604);
DBCC PAGE (@dbid, @fileid, @pageid, 1);
DBCC TRACEON(3604);
PRINT N'*************************************** IndexTest_rowDate_ix01 *****************************************';

SELECT @fileid = ddpa.allocated_page_file_id
    , @pageid = ddpa.allocated_page_page_id
FROM sys.indexes i 
CROSS APPLY sys.dm_db_database_page_allocations(DB_ID(), i.object_id, i.index_id, NULL, 'LIMITED') ddpa
WHERE i.name = N'IndexTest_rowDate_ix02'
    AND ddpa.is_allocated = 1
    AND ddpa.is_iam_page = 0;

PRINT N'*************************************** IndexTest_rowDate_ix02 *****************************************';
DBCC TRACEON(3604);
DBCC PAGE (@dbid, @fileid, @pageid, 1);
DBCC TRACEON(3604);
PRINT N'*************************************** IndexTest_rowDate_ix02 *****************************************';

Przyjrzałem się wynikowi za pomocą Beyond Compare i oprócz oczywistych różnic wokół identyfikatorów stron alokacji itp. Dwie struktury indeksu są identyczne.

wprowadź opis zdjęcia tutaj

Możesz uznać, że powyższe oznacza, że ​​dołączenie klucza podstawowego do każdego indeksu i zdefiniowanie go jako unikalnego jest A Good Thing ™, ponieważ i tak dzieje się to pod przykryciem. Nie przyjąłbym tego założenia i sugerowałbym zdefiniowanie indeksu jako unikalnego, jeśli w rzeczywistości dane naturalne w tym indeksie są już unikalne.

Istnieje kilka doskonałych zasobów w Interwebz na ten temat, w tym:

Do Twojej wiadomości, sama obecność identitykolumny nie gwarantuje wyjątkowości. Musisz zdefiniować kolumnę jako klucz podstawowy lub z unikalnym ograniczeniem, aby upewnić się, że wartości przechowywane w tej kolumnie są w rzeczywistości unikalne. SET IDENTITY_INSERT schema.table ON;Zestawienie pozwoli Ci wstawić do non-unikatowych wartości w kolumnie zdefiniowanej jako identity.

Max Vernon
źródło
5

Tylko dodatek do doskonałej odpowiedzi Maxa .

Jeśli chodzi o tworzenie nie unikalnego indeksu klastrowego, SQL Server i tak tworzy coś zwanego a Uniquifierw tle.

Może Uniquifierto powodować potencjalne problemy w przyszłości, jeśli Twoja platforma ma wiele operacji CRUD, ponieważ Uniquifierjest to tylko 4 bajty (podstawowa 32-bitowa liczba całkowita). Tak więc, jeśli twój system ma wiele operacji CRUD, możliwe jest, że zużyjesz wszystkie dostępne unikalne numery i nagle otrzymasz błąd i nie pozwoli ci to na wstawienie więcej danych do twoich tabel (ponieważ to spowoduje nie ma już żadnych unikalnych wartości, które można by przypisać do nowo wstawionych wierszy).

Gdy tak się stanie, pojawi się ten błąd:

The maximum system-generated unique value for a duplicate group 
was exceeded for index with partition ID (someID). 

Dropping and re-creating the index may resolve this;
otherwise, use another clustering key.

Błąd 666 (powyższy błąd) występuje, gdy uniquifierdla jednego zestawu nieunikalnych kluczy zużywa ponad 2 147 483 647 wierszy.

Tak więc będziesz musiał mieć ~ 2 miliardy wierszy dla jednej wartości klucza, lub musisz zmodyfikować jedną wartość klucza ~ 2 miliardy razy, aby zobaczyć ten błąd. W związku z tym nie jest bardzo prawdopodobne, że spotkasz się z tym ograniczeniem.

Chessbrain
źródło
Nie miałem pojęcia, że ​​ukryty unikatowiec może zabraknąć miejsca na klawisze, ale chyba wszystkie rzeczy są ograniczone. Podobnie jak w przypadku, gdy Casei Ifstruktury są ograniczone do 10 poziomów, sensowne jest, że istnieje również limit rozwiązywania nieunikalnych bytów. W twoim oświadczeniu brzmi to tak, jakby dotyczyło tylko przypadków, gdy klucz klastrowania nie jest unikalny. Czy jest to problem dla Nonclustered Indexczy klucz klastrowania jest, Uniqueczy nie ma problemu z Nonclusteredindeksami?
Solonotix,
Indeks Unique jest (o ile wiem) ograniczony przez rozmiar typu kolumny (więc jeśli jest to typ BIGINT, masz do dyspozycji 8 bajtów). Ponadto, zgodnie z oficjalną dokumentacją Microsoft, dozwolone jest maksymalnie 900 bajtów dla indeksu klastrowanego i 1700 bajtów dla nieklastrowanego (ponieważ możesz mieć więcej niż jeden indeks nieklastrowany i tylko 1 indeks klastrowany na tabelę). docs.microsoft.com/en-us/sql/sql-server/…
Chessbrain
1
@ Solonotix - w indeksach nieklastrowanych używany jest unikalny z indeksu klastrowanego. Jeśli uruchomisz kod w moim przykładzie bez klucza podstawowego (zamiast tego utwórz indeks klastrowany), zobaczysz, że dane wyjściowe są takie same dla indeksów nieunikalnych i indeksów unikatowych.
Max Vernon
-2

Nie zamierzam zastanawiać się nad tym, czy indeks powinien być unikalny, czy też nie, i czy w tym podejściu jest więcej kosztów ogólnych. Ale kilka rzeczy przeszkadzało mi w twoim ogólnym projekcie

  1. dt data / godzina nie jest wartością domyślną zerową (aktualny znacznik_czasu). Datetime jest starszą wersją lub taką, i możesz być w stanie osiągnąć co najmniej pewne oszczędności miejsca za pomocą datetime2 () i sysdatetime ().
  2. utwórz indeks [nonunique_nonclustered_example] na #test_index (is_deleted) include (val). Niepokoi mnie to. Zobacz, jak mają być dostępne dane (założę się, że jest ich więcej WHERE is_deleted = 0) i skorzystaj z filtrowanego indeksu. Rozważałbym nawet użycie 2 filtrowanych indeksów, jednego dla where is_deleted = 0drugiego, a drugiego dlawhere is_deleted = 1

Zasadniczo wygląda to bardziej na ćwiczenie kodowania zaprojektowane do testowania hipotezy, a nie prawdziwy problem / rozwiązanie, ale te dwa wzorce są zdecydowanie czymś, czego szukam w recenzjach kodu.

Toby
źródło
Najwięcej zaoszczędzisz używając datetime2 zamiast datetime to 1 bajt, to znaczy, jeśli twoja precyzja jest mniejsza niż 3, co oznaczałoby utratę precyzji w ułamkach sekund, co nie zawsze jest realnym rozwiązaniem. Jeśli chodzi o podany przykładowy indeks, projekt był prosty, aby skupić się na moim pytaniu. W Nonclusteredindeksie klucz klastrowania zostanie dołączony na końcu wiersza danych w celu wyszukiwania kluczy wewnętrznie. Jako takie, dwa indeksy są fizycznie takie same, co było celem mojego pytania.
Solonotix,
Na skali uruchamiamy zapisywanie jednego lub dwóch bajtów szybko. I założyłem, że skoro używasz nieprecyzyjnej daty, możemy zmniejszyć precyzję. W przypadku indeksów ponownie powiem, że kolumny bitowe, ponieważ kolumny wiodące w indeksach są wzorcem, który traktuję jako zły wybór. Podobnie jak w przypadku wszystkich rzeczy, przebieg może się różnić. Niestety wady przybliżonego modelu.
Toby
-4

Wygląda na to, że po prostu używasz PK, aby utworzyć alternatywny, mniejszy indeks. Dlatego wydajność na nim jest szybsza.

Widać to w firmach, które mają ogromne tabele danych (np .: tabele danych podstawowych). Ktoś decyduje się na jeden ogromny indeks klastrowany, oczekując, że spełni on potrzeby różnych grup sprawozdawczych.

Ale jedna grupa może potrzebować tylko kilku części tego indeksu, podczas gdy inna grupa potrzebuje innych części ... więc indeks uderzający w każdą kolumnę pod słońcem w celu „zoptymalizowania wydajności” tak naprawdę nie pomaga.

Tymczasem rozbicie go w celu utworzenia wielu mniejszych ukierunkowanych wskaźników często rozwiązuje problem.

I wydaje się, że to właśnie robisz. Masz ten masywny indeks klastrowy o okropnej wydajności, a następnie używasz PK do utworzenia kolejnego indeksu z mniejszą liczbą kolumn, które (co nie dziwi) ma lepszą wydajność.

Więc po prostu przeanalizuj i dowiedz się, czy możesz wziąć pojedynczy indeks klastrowany i podzielić go na mniejsze, ukierunkowane indeksy, których potrzebują określone zadania.

Trzeba będzie wtedy przeanalizować wydajność z punktu widzenia „pojedynczego indeksu vs. wielu indeksów”, ponieważ tworzenie i aktualizacja indeksów wiąże się z dodatkowymi kosztami. Ale musisz to przeanalizować z ogólnej perspektywy.

EG: może być mniej zasobochłonny w stosunku do jednego ogromnego indeksu klastrowego, a bardziej zasobochłonny, aby mieć kilka mniejszych indeksów ukierunkowanych. Jeśli jednak będziesz w stanie szybciej uruchamiać ukierunkowane zapytania na zapleczu, oszczędzając czas (i pieniądze), być może warto.

Musisz więc przeprowadzić kompleksową analizę ... nie tylko spojrzeć na to, jak wpływa na twój świat, ale także na użytkowników końcowych.

Po prostu czuję, że źle używasz identyfikatora PK. Ale możesz używać systemu baz danych, który pozwala tylko na 1 indeks (?), Ale możesz się zakraść, jeśli masz PK (b / c każdy system relacyjnych baz danych w tych dniach wydaje się automatycznie indeksować PK). Jednak większość nowoczesnych RDBMS powinna umożliwiać tworzenie wielu indeksów; liczba indeksów, które możesz wprowadzić, nie powinna być ograniczona (w przeciwieństwie do limitu 1 PK).

Tak więc, tworząc PK, który działa jak indeks altowy, zużywasz PK, co może być potrzebne, jeśli później tabela zostanie rozszerzona.

Nie oznacza to, że twój stół nie potrzebuje PK. SOP DB 101 mówi „każdy stół powinien mieć PK”. Ale w sytuacji hurtowni danych itp. Posiadanie PK na stole może być dodatkowym narzutem, którego nie potrzebujesz. Lub może to być wysłanie boga, aby upewnić się, że nie dodajesz podwójnie wpisów duplikatów. To naprawdę zależy od tego, co robisz i dlaczego to robisz.

Ale ogromne tabele zdecydowanie korzystają z indeksów. Ale założenie, że jeden masywny indeks klastrowy będzie najlepszy, to po prostu ... może być najlepszy ... ale zaleciłbym przetestowanie w środowisku testowym rozbijania indeksu na wiele mniejszych indeksów ukierunkowanych na konkretne scenariusze przypadków użycia.

bla bla
źródło