Overhead Indeks Keunikan

14

Saya telah memiliki perdebatan yang sedang berlangsung dengan berbagai pengembang di kantor saya tentang biaya indeks, dan apakah keunikan itu bermanfaat atau mahal (mungkin keduanya). Inti dari masalah ini adalah sumber daya kita yang bersaing.

Latar Belakang

Saya sebelumnya telah membaca sebuah diskusi yang menyatakan Uniqueindeks bukan biaya tambahan untuk mempertahankan, karena Insertoperasi secara implisit memeriksa di mana itu cocok dengan B-tree, dan, jika duplikat ditemukan dalam indeks non-unik, menambahkan uniquifier ke akhir kunci, tetapi jika tidak memasukkan secara langsung. Dalam urutan peristiwa ini, Uniqueindeks tidak memiliki biaya tambahan.

Rekan kerja saya memerangi pernyataan ini dengan mengatakan bahwa Uniquediberlakukan sebagai operasi kedua setelah mencari posisi baru di B-tree, dan dengan demikian lebih mahal untuk mempertahankannya daripada indeks yang tidak unik.

Paling buruk, saya telah melihat tabel dengan kolom identitas (inheren unik) yang merupakan kunci pengelompokan tabel, tetapi secara eksplisit dinyatakan sebagai non-unik. Di sisi lain yang terburuk adalah obsesi saya terhadap keunikan, dan semua indeks dibuat unik, dan ketika tidak mungkin untuk mendefinisikan hubungan unik yang eksplisit dengan indeks, saya menambahkan PK tabel ke akhir indeks untuk memastikan Keunikan dijamin.

Saya sering terlibat dalam ulasan kode untuk tim dev, dan saya harus bisa memberikan panduan umum agar mereka ikuti. Ya, setiap indeks harus dievaluasi, tetapi ketika Anda memiliki lima server dengan ribuan tabel masing-masing dan sebanyak dua puluh indeks pada sebuah tabel, Anda harus dapat menerapkan beberapa aturan sederhana untuk memastikan tingkat kualitas tertentu.

Pertanyaan

Apakah keunikan memiliki biaya tambahan di belakang Insertdibandingkan dengan biaya mempertahankan indeks yang tidak unik? Kedua, apa yang salah dengan menambahkan Kunci Utama dari tabel sampai akhir indeks untuk memastikan keunikan?

Contoh Definisi Tabel

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);

Contoh

Contoh mengapa saya akan menambahkan Uniquekunci pada akhir indeks ada di salah satu tabel fakta kami. Ada Primary Keyyang merupakan Identitykolom. Namun, Clustered Indexitu bukan kolom skema partisi, diikuti oleh tiga dimensi kunci asing tanpa keunikan. Pilih kinerja pada tabel ini sangat buruk, dan saya sering mendapatkan waktu mencari yang lebih baik menggunakan Primary Keypencarian kunci daripada memanfaatkannya Clustered Index. Tabel lain yang mengikuti desain yang serupa, tetapi Primary Keyditambahkan sampai akhir memiliki kinerja yang jauh lebih baik.

-- 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
sumber

Jawaban:

16

Saya sering terlibat dalam ulasan kode untuk tim dev, dan saya harus bisa memberikan panduan umum agar mereka ikuti.

Lingkungan saya saat ini terlibat memiliki 250 server dengan 2500 database. Saya telah bekerja pada sistem dengan 30.000 basis data . Pedoman pengindeksan harus berkisar pada konvensi penamaan, dll, bukan menjadi "aturan" untuk kolom apa yang dimasukkan dalam indeks - setiap indeks individu harus direkayasa untuk menjadi indeks yang benar untuk aturan bisnis tertentu atau kode yang menyentuh tabel.

Apakah keunikan memiliki biaya tambahan di belakang Insertdibandingkan dengan biaya mempertahankan indeks yang tidak unik? Kedua, apa yang salah dengan menambahkan Kunci Utama dari tabel sampai akhir indeks untuk memastikan keunikan?

Menambahkan kolom kunci utama ke akhir indeks non-unik agar terlihat unik bagi saya sebagai anti-pola. Jika aturan bisnis menentukan data harus unik, maka tambahkan batasan unik ke kolom; yang secara otomatis akan membuat indeks unik. Jika Anda mengindeks kolom untuk kinerja , mengapa Anda menambahkan kolom ke indeks?

Bahkan jika anggapan Anda bahwa menegakkan keunikan tidak menambah overhead tambahan adalah benar (yang bukan untuk kasus tertentu), apa yang Anda selesaikan dengan memperumit indeks?

Dalam contoh spesifik menambahkan kunci utama ke akhir kunci indeks Anda sehingga Anda dapat membuat definisi indeks termasuk UNIQUEpengubah, itu sebenarnya membuat nol perbedaan pada struktur indeks fisik pada disk. Ini karena sifat struktur kunci indeks B-tree, karena mereka selalu harus unik.

Seperti yang dikatakan David Browne dalam komentar:

Karena setiap indeks nonclustered disimpan sebagai indeks unik, tidak ada biaya tambahan dalam memasukkan ke dalam indeks unik. Bahkan satu-satunya biaya tambahan akan gagal mendeklarasikan kunci kandidat sebagai indeks unik, yang akan menyebabkan kunci indeks berkerumun ditambahkan ke kunci indeks.

Ambil contoh minimal lengkap dan terverifikasi berikut ini :

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
);

Saya akan menambahkan dua indeks yang identik kecuali untuk penambahan kunci utama di ujung ekor dari definisi kunci indeks kedua:

CREATE INDEX IndexTest_rowDate_ix01
ON dbo.IndexTest(rowDate);

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

Selanjutnya, kita akan beberapa baris ke tabel:

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()));

Seperti yang Anda lihat di atas, tiga baris berisi nilai yang sama untuk rowDatekolom, dan dua baris berisi nilai unik.

Selanjutnya, kita akan melihat struktur halaman fisik untuk setiap indeks, menggunakan DBCC PAGEperintah tidak berdokumen :

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 *****************************************';

Saya telah melihat output menggunakan Beyond Compare, dan kecuali untuk perbedaan yang jelas di sekitar halaman alokasi ID, dll, kedua struktur indeks itu identik.

masukkan deskripsi gambar di sini

Anda dapat mengambil yang di atas untuk berarti bahwa memasukkan kunci utama dalam setiap indeks, dan mendefinisikan sebagai unik adalah A Good Thing ™ karena memang itulah yang terjadi di bawah penutup. Saya tidak akan membuat asumsi itu, dan akan menyarankan hanya mendefinisikan indeks sebagai unik jika sebenarnya data alami dalam indeks sudah unik.

Ada beberapa sumber yang bagus di Interwebz tentang topik ini, termasuk:

FYI, keberadaan identitykolom saja tidak menjamin keunikan. Anda perlu mendefinisikan kolom sebagai kunci utama atau dengan batasan unik untuk memastikan nilai yang disimpan dalam kolom itu sebenarnya unik. The SET IDENTITY_INSERT schema.table ON;pernyataan akan memungkinkan Anda untuk memasukkan nilai-nilai non-unik ke dalam kolom didefinisikan sebagai identity.

Max Vernon
sumber
5

Hanya add-on untuk jawaban luar biasa Max .

Ketika datang untuk membuat indeks cluster tidak unik, SQL Server menciptakan sesuatu yang disebut Uniquifierdi latar belakang.

Ini Uniquifierdapat menyebabkan masalah potensial di masa depan jika platform Anda memiliki banyak operasi CRUD, karena ini Uniquifierhanya 4 byte besar (integer 32bit dasar). Jadi, jika sistem Anda memiliki banyak operasi CRUD itu mungkin Anda akan menggunakan semua nomor unik yang tersedia dan tiba-tiba Anda akan menerima kesalahan dan itu tidak akan memungkinkan Anda untuk memasukkan data lagi ke dalam tabel Anda (karena itu akan tidak lagi memiliki nilai unik untuk ditetapkan ke baris yang baru Anda masukkan).

Ketika ini terjadi, Anda akan menerima kesalahan ini:

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.

Kesalahan 666 (kesalahan di atas) terjadi ketika uniquifieruntuk satu set kunci non-unik mengkonsumsi lebih dari 2.147.483.647 baris.

Jadi, Anda harus memiliki ~ 2 miliar baris untuk nilai kunci tunggal, atau Anda harus memodifikasi nilai kunci tunggal ~ 2 miliar kali untuk melihat kesalahan ini. Dengan demikian, sangat tidak mungkin Anda akan mengalami keterbatasan ini.

Chessbrain
sumber
Saya tidak tahu bahwa uniquifier tersembunyi dapat kehabisan ruang utama, tapi saya kira semua hal terbatas dalam beberapa kasus. Sama seperti bagaimana Casedan Ifstruktur dibatasi hingga 10 level, masuk akal bahwa ada juga batas untuk menyelesaikan entitas yang tidak unik. Dengan pernyataan Anda, ini sepertinya hanya berlaku untuk kasus-kasus ketika kunci pengelompokan tidak unik. Apakah ini masalah untuk Nonclustered Indexatau jika kunci clustering Uniquemaka tidak ada masalah untuk Nonclusteredindeks?
Solonotix
Indeks unik adalah (sejauh yang saya tahu) dibatasi oleh ukuran tipe kolom (jadi jika itu adalah tipe BIGINT, Anda memiliki 8bytes untuk bekerja dengannya). Juga, menurut dokumentasi resmi microsoft, ada maksimum 900 byte yang diizinkan untuk indeks berkerumun dan 1700 byte untuk yang tidak berkerumun (karena Anda dapat memiliki lebih dari satu indeks yang tidak berkerumun dan hanya 1 indeks yang dikelompokkan per tabel). docs.microsoft.com/en-us/sql/sql-server/…
Chessbrain
1
@ Solonotix - uniquifier dari indeks berkerumun digunakan dalam indeks non-berkerumun. Jika Anda menjalankan kode dalam contoh saya tanpa kunci primer (sebagai gantinya buat indeks berkerumun), Anda dapat melihat outputnya sama untuk indeks non-unik dan unik.
Max Vernon
-2

Saya tidak akan mempertimbangkan pertanyaan apakah suatu indeks harus unik atau tidak, dan apakah ada biaya tambahan dalam pendekatan ini atau itu. Tetapi beberapa hal mengganggu saya dalam desain umum Anda

  1. dt datetime bukan null default (current_timestamp). Datetime adalah bentuk yang lebih lama atau ini, dan Anda mungkin dapat mencapai setidaknya beberapa penghematan ruang dengan menggunakan datetime2 () dan sysdatetime ().
  2. buat indeks [nonunique_nonclustered_example] pada #test_index (is_deleted) include (val). Ini menggangguku. Lihatlah bagaimana data diakses (saya bertaruh ada lebih dari WHERE is_deleted = 0) dan lihat menggunakan indeks yang difilter. Saya bahkan akan mempertimbangkan untuk menggunakan 2 indeks yang difilter, satu untuk where is_deleted = 0dan yang lainnya untukwhere is_deleted = 1

Pada dasarnya ini lebih mirip latihan pengkodean yang dirancang untuk menguji hipotesis daripada masalah / solusi nyata, tetapi kedua pola tersebut jelas merupakan sesuatu yang saya cari dalam ulasan kode.

Toby
sumber
Paling Anda akan menghemat menggunakan datetime2 daripada datetime adalah 1 byte, dan itu adalah jika presisi Anda kurang dari 3, yang berarti kehilangan presisi pada detik pecahan, yang tidak selalu merupakan solusi yang layak. Adapun indeks contoh yang diberikan, desain itu tetap sederhana untuk fokus pada pertanyaan saya. Sebuah Nonclusteredindeks akan memiliki kunci pengelompokan ditambahkan ke akhir baris data untuk pencarian kunci internal. Dengan demikian, kedua indeks secara fisik sama, yang merupakan poin dari pertanyaan saya.
Solonotix
Pada skala kita menjalankan penghematan satu atau dua byte bertambah dengan cepat. Dan saya berasumsi bahwa karena Anda menggunakan datetime yang tidak tepat, kita dapat mengurangi presisi. Untuk indeks, sekali lagi saya akan menyatakan bahwa kolom bit sebagai kolom utama pada indeks adalah pola yang saya anggap sebagai pilihan yang buruk. Seperti halnya semua hal, jarak tempuh Anda mungkin berbeda. Alas kerugian dari model perkiraan.
Toby
-4

Sepertinya Anda hanya menggunakan PK untuk membuat indeks alternatif yang lebih kecil. Oleh karena itu, kinerja lebih cepat.

Anda melihat ini di perusahaan yang memiliki tabel data besar (misalnya: tabel data master). Seseorang memutuskan untuk memiliki satu indeks berkerumun besar di atasnya mengharapkannya untuk memenuhi kebutuhan berbagai kelompok pelaporan.

Tapi, satu kelompok mungkin hanya membutuhkan beberapa bagian dari indeks itu sementara kelompok lain membutuhkan bagian lain .. sehingga indeks hanya menampar di setiap kolom di bawah matahari untuk "mengoptimalkan kinerja" tidak terlalu membantu.

Sementara itu, memecahnya untuk membuat beberapa, lebih kecil, indeks bertarget, sering memecahkan masalah.

Dan, sepertinya itulah yang Anda lakukan. Anda memiliki indeks pengelompokan besar ini dengan kinerja yang buruk, maka Anda menggunakan PK untuk membuat indeks lain dengan lebih sedikit kolom yang (tidak mengejutkan) memiliki kinerja yang lebih baik.

Jadi, lakukan saja analisis dan cari tahu apakah Anda dapat mengambil indeks berkerumun tunggal dan memecahnya menjadi indeks yang lebih kecil, bertarget yang membutuhkan pekerjaan tertentu.

Anda harus menganalisis kinerja kemudian dari titik berdiri "indeks tunggal vs beberapa indeks", karena ada overhead dalam membuat dan memperbarui indeks. Tetapi, Anda harus menganalisis ini dari perspektif keseluruhan.

EG: itu mungkin kurang intensif sumber daya untuk satu indeks cluster besar, dan lebih intensif sumber daya untuk memiliki beberapa indeks target yang lebih kecil. Tetapi, jika Anda kemudian dapat menjalankan kueri yang ditargetkan di back-end lebih cepat, menghemat waktu (dan uang) di sana, mungkin itu layak dilakukan.

Jadi, Anda harus melakukan analisis end-to-end .. tidak hanya melihat bagaimana hal itu berdampak pada dunia Anda sendiri, tetapi juga bagaimana hal itu berdampak pada pengguna akhir.

Saya hanya merasa seperti Anda salah menggunakan pengidentifikasi PK. Tapi, Anda mungkin menggunakan sistem basis data yang hanya memungkinkan 1 indeks (?), Tetapi Anda dapat menyelinap masuk yang lain jika Anda PK (b / c setiap sistem basis data relasional hari ini tampaknya secara otomatis mengindeks PK). Namun, sebagian besar RDBMS modern harus memungkinkan pembuatan banyak indeks; seharusnya tidak ada batasan jumlah indeks yang dapat Anda buat (sebagai lawan dari batas 1 PK).

Jadi, dengan membuat PK yang hanya bertindak seperti indeks alt .. Anda menggunakan PK Anda, yang mungkin diperlukan jika tabel nanti diperluas dalam peran itu.

Itu tidak berarti meja Anda tidak memerlukan PK .. SOP DB 101 mengatakan "setiap meja harus memiliki PK". Tetapi, dalam situasi pergudangan data atau semacamnya .. memiliki PK di atas meja mungkin hanya overhead tambahan yang tidak Anda butuhkan. Atau, itu bisa berupa pengiriman dewa untuk memastikan Anda tidak menambahkan entri dupe ganda. Ini benar-benar masalah apa yang Anda lakukan dan mengapa Anda melakukannya.

Tapi, tabel besar pasti mendapat manfaat dari memiliki indeks. Tapi, dengan asumsi satu indeks cluster besar akan menjadi yang terbaik adalah hanya ... itu mungkin yang terbaik .. tapi saya akan merekomendasikan pengujian pada tes dan memecah indeks menjadi beberapa indeks yang lebih kecil yang menargetkan skenario kasus penggunaan tertentu.

blahblah
sumber