Apakah aturan WHERE-JOIN-ORDER- (SELECT) untuk pesanan kolom indeks salah?

9

Saya mencoba meningkatkan kueri (sub-) ini menjadi bagian dari kueri yang lebih besar:

select SUM(isnull(IP.Q, 0)) as Q, 
        IP.OPID 
    from IP
        inner join I
        on I.ID = IP.IID
    where 
        IP.Deleted=0 and
        (I.Status > 0 AND I.Status <= 19) 
    group by IP.OPID

Penjaga Rencana Explorer menunjukkan beberapa Pencarian Kunci yang relatif mahal untuk tabel dbo. [I] dilakukan oleh permintaan di atas.

Tabel dbo.I

    CREATE TABLE [dbo].[I] (
  [ID]  UNIQUEIDENTIFIER NOT NULL,
  [OID]  UNIQUEIDENTIFIER NOT NULL,
  []  UNIQUEIDENTIFIER NOT NULL,
  []  UNIQUEIDENTIFIER NOT NULL,
  [] UNIQUEIDENTIFIER NULL,
  []  UNIQUEIDENTIFIER NOT NULL,
  []  CHAR (3) NOT NULL,
  []  CHAR (3)  DEFAULT ('EUR') NOT NULL,
  []  DECIMAL (18, 8) DEFAULT ((1)) NOT NULL,
  [] CHAR (10)  NOT NULL,
  []  DECIMAL (18, 8) DEFAULT ((1)) NOT NULL,
  []  DATETIME  DEFAULT (getdate()) NOT NULL,
  []  VARCHAR (35) NULL,
  [] NVARCHAR (100) NOT NULL,
  []  NVARCHAR (100) NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  [Status]  INT DEFAULT ((0)) NOT NULL,
  []  DECIMAL (18, 2)  NOT NULL,
  [] DECIMAL (18, 2)  NOT NULL,
  [] DECIMAL (18, 2)  NOT NULL,
  [] DATETIME DEFAULT (getdate()) NULL,
  []  DATETIME NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  [] TINYINT  DEFAULT ((0)) NOT NULL,
  []  DATETIME NULL,
  []  VARCHAR (50) NULL,
  []  DATETIME  DEFAULT (getdate()) NOT NULL,
  []  VARCHAR (50) NOT NULL,
  []  DATETIME NULL,
  []  VARCHAR (50) NULL,
  []  ROWVERSION NOT NULL,
  []  DATETIME NULL,
  []  INT  NULL,
  [] TINYINT DEFAULT ((0)) NOT NULL,
  [] UNIQUEIDENTIFIER NULL,
  []  TINYINT DEFAULT ((0)) NOT NULL,
  []  TINYINT  DEFAULT ((0)) NOT NULL,
  [] NVARCHAR (50)  NULL,
  [] TINYINT DEFAULT ((0)) NOT NULL,
  []  UNIQUEIDENTIFIER NULL,
  [] UNIQUEIDENTIFIER NULL,
  []  TINYINT  DEFAULT ((0)) NOT NULL,
  []  TINYINT DEFAULT ((0)) NOT NULL,
  []  UNIQUEIDENTIFIER NULL,
  []  DECIMAL (18, 2)  NULL,
  []  DECIMAL (18, 2)  NULL,
  [] DECIMAL (18, 2)  DEFAULT ((0)) NOT NULL,
  [] UNIQUEIDENTIFIER NULL,
  [] DATETIME NULL,
  [] DATETIME NULL,
  []  VARCHAR (35) NULL,
  [] DECIMAL (18, 2)  DEFAULT ((0)) NOT NULL,
  CONSTRAINT [PK_I] PRIMARY KEY NONCLUSTERED ([ID] ASC) WITH (FILLFACTOR = 90),
  CONSTRAINT [FK_I_O] FOREIGN KEY ([OID]) REFERENCES [dbo].[O] ([ID]),
  CONSTRAINT [FK_I_Status] FOREIGN KEY ([Status]) REFERENCES [dbo].[T_Status] ([Status])
);                  


GO
CREATE CLUSTERED INDEX [CIX_Invoice]
  ON [dbo].[I]([OID] ASC) WITH (FILLFACTOR = 90);

Tabel dbo.IP

CREATE TABLE [dbo].[IP] (
 [ID] UNIQUEIDENTIFIER DEFAULT (newid()) NOT NULL,
 [IID] UNIQUEIDENTIFIER NOT NULL,
 [OID] UNIQUEIDENTIFIER NOT NULL,
 [Deleted] TINYINT DEFAULT ((0)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 []UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] INT NOT NULL,
 [] VARCHAR (35) NULL,
 [] NVARCHAR (100) NOT NULL,
 [] NTEXT NULL,
 [] DECIMAL (18, 4) DEFAULT ((0)) NOT NULL,
 [] NTEXT NULL,
 [] NTEXT NULL,
 [] DECIMAL (18, 4) DEFAULT ((0)) NOT NULL,
 [] DECIMAL (18, 4) DEFAULT ((0)) NOT NULL,
 [] DECIMAL (4, 2) NOT NULL,
 [] INT DEFAULT ((1)) NOT NULL,
 [] DATETIME DEFAULT (getdate()) NOT NULL,
 [] VARCHAR (50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
 [] DATETIME NULL,
 [] VARCHAR (50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
 [] ROWVERSION NOT NULL,
 [] INT DEFAULT ((1)) NOT NULL,
 [] DATETIME NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] DECIMAL (18, 4) DEFAULT ((1)) NOT NULL,
 [] DECIMAL (18, 4) DEFAULT ((1)) NOT NULL,
 [] INT DEFAULT ((0)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 []UNIQUEIDENTIFIER NULL,
 []NVARCHAR (35) NULL,
 [] VARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] VARCHAR (12) NULL,
 [] VARCHAR (4) NULL,
 [] NVARCHAR (50) NULL,
 [] NVARCHAR (50) NULL,
 [] VARCHAR (35) NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] NVARCHAR (50) NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] DECIMAL (18, 2) NULL,
 []TINYINT DEFAULT ((1)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] TINYINT DEFAULT ((1)) NOT NULL,
 CONSTRAINT [PK_IP] PRIMARY KEY NONCLUSTERED ([ID] ASC) WITH (FILLFACTOR = 90),
 CONSTRAINT [FK_IP_I] FOREIGN KEY ([IID]) REFERENCES [dbo].[I] ([ID]) ON DELETE CASCADE NOT FOR REPLICATION,
 CONSTRAINT [FK_IP_XType] FOREIGN KEY ([XType]) REFERENCES [dbo].[xTYPE] ([Value]) NOT FOR REPLICATION
);

GO
CREATE CLUSTERED INDEX [IX_IP_CLUST]
 ON [dbo].[IP]([IID] ASC) WITH (FILLFACTOR = 90);

Tabel "I" memiliki sekitar 100.000 baris, indeks berkerumun memiliki 9.386 halaman.
Tabel IP adalah "anak" - tabel I dan memiliki sekitar 175.000 baris.

Saya mencoba menambahkan indeks baru mengikuti aturan urutan kolom indeks: "WHERE-JOIN-ORDER- (SELECT)"

https://www.mssqltips.com/sqlservertutorial/3208/use-where-join-orderby-select-column-order-when-creating-indexes/

untuk mengatasi pencarian kunci dan membuat pencarian indeks:

CREATE NONCLUSTERED INDEX [IX_I_Status_1]
    ON [dbo].[Invoice]([Status], [ID])

Permintaan yang diekstrak segera menggunakan indeks ini. Tetapi permintaan yang lebih besar asli itu adalah bagian dari, tidak. Bahkan tidak menggunakannya ketika saya memaksanya untuk menggunakan WITH (INDEX (IX_I_Status_1)).

Setelah beberapa saat, saya memutuskan untuk mencoba indeks baru lainnya dan mengubah urutan kolom yang diindeks:

CREATE NONCLUSTERED INDEX [IX_I_Status_2]
    ON [dbo].[Invoice]([ID], [Status])

WOHA! Indeks ini digunakan oleh kueri yang diekstraksi dan juga oleh kueri yang lebih besar!

Kemudian saya membandingkan statistik IO yang diekstraksi dengan memaksanya untuk menggunakan [IX_I_Status_1] dan [IX_I_Status_2]:

Hasil [IX_I_Status_1]:

Table 'I'. Scan count 5, logical reads 636, physical reads 16, read-ahead reads 574
Table 'IP'. Scan count 5, logical reads 1134, physical reads 11, read-ahead reads 1040
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0

Hasil [IX_I_Status_2]:

Table 'I'. Scan count 1, logical reads 615, physical reads 6, read-ahead reads 631
Table 'IP'. Scan count 1, logical reads 1024, physical reads 5, read-ahead reads 1040
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0,  read-ahead reads 0

OK, saya bisa mengerti bahwa permintaan mega-besar-monster mungkin terlalu kompleks untuk membuat SQL server menangkap rencana eksekusi yang ideal dan mungkin kehilangan indeks baru saya. Tapi saya tidak mengerti mengapa indeks [IX_I_Status_2] tampaknya lebih cocok dan lebih efisien untuk kueri.

Karena kueri pertama-tama dari tabel filter I oleh kolom STATUS dan kemudian bergabung dengan tabel IP, saya tidak mengerti mengapa [IX_I_Status_2] lebih baik dan digunakan oleh Sql Server daripada [IX_I_Status_1]?

Magier
sumber
Ya itu menggunakan indeks ini jika kriteria filter memenuhi. Ia melakukan pemindaian indeks (sama dengan IX_I_Status_2) dan dibandingkan dengan ini menghemat 1 pembacaan fisik. tapi saya harus "memasukkan (status)" ke indeks ini karena status dalam output dan sekali lagi melihat ke atas.
Magier
Catatan sisi lucu: Setelah saya sekarang menerapkan indeks terbaik, saya bisa mencari tahu ([IX_I_Status_2]) dan menjalankan kueri lagi, sekarang saya mendapatkan saran indeks yang hilang: CREATE INDEX NONCLUSTERED [<Nama Indeks Hilang, sysname,>] ON [ dbo]. [I] ([Status]) TERMASUK ([ID]) Ini adalah saran yang buruk dan menurunkan kinerja kueri. TY Sql server :)
Magier

Jawaban:

19

Apakah aturan WHERE-JOIN-ORDER- (SELECT) untuk pesanan kolom indeks salah?

Setidaknya itu adalah saran yang tidak lengkap dan berpotensi menyesatkan (saya tidak repot-repot membaca seluruh artikel). Jika Anda akan membaca hal-hal di Internet (termasuk ini), Anda harus menyesuaikan jumlah kepercayaan Anda sesuai dengan seberapa baik Anda sudah tahu dan percaya pada penulis, tetapi selalu kemudian verifikasi untuk diri Anda sendiri.

Ada sejumlah "aturan praktis" untuk membuat indeks, tergantung pada skenario yang tepat, tetapi tidak ada yang benar-benar pengganti yang baik untuk memahami masalah inti untuk diri sendiri. Baca tentang implementasi indeks dan operator rencana eksekusi di SQL Server, ikuti beberapa latihan, dan dapatkan pemahaman yang baik tentang bagaimana indeks dapat digunakan untuk membuat rencana eksekusi lebih efisien. Tidak ada jalan pintas yang efektif untuk mencapai pengetahuan dan pengalaman ini.

Secara umum, saya dapat mengatakan bahwa indeks Anda paling sering memiliki kolom yang digunakan untuk tes kesetaraan terlebih dahulu, dengan ketidaksetaraan yang terakhir, dan / atau disediakan oleh filter pada indeks. Ini bukan pernyataan lengkap, karena indeks juga dapat memberikan pesanan, yang mungkin lebih berguna daripada mencari langsung ke satu atau lebih kunci dalam beberapa situasi. Misalnya, pemesanan dapat digunakan untuk menghindari pengurutan, untuk mengurangi biaya opsi penggabungan fisik seperti penggabungan gabungan, untuk mengaktifkan agregat aliran, menemukan beberapa baris kualifikasi pertama dengan cepat ... dan seterusnya.

Saya menjadi sedikit kabur di sini, karena memilih indeks yang ideal untuk permintaan tergantung pada banyak faktor - ini adalah topik yang sangat luas.

Bagaimanapun, bukan hal yang aneh untuk menemukan sinyal yang saling bertentangan untuk indeks 'terbaik' dalam kueri. Misalnya, predikat gabungan Anda ingin baris dipesan satu cara untuk gabungan bergabung, grup dengan ingin baris diurutkan cara lain untuk agregat aliran, dan menemukan baris yang memenuhi syarat menggunakan predikat klausa mana yang akan menyarankan indeks lainnya.

Alasan pengindeksan adalah seni dan juga sains adalah bahwa kombinasi yang ideal tidak selalu memungkinkan secara logis. Memilih indeks kompromi terbaik untuk beban kerja (bukan hanya satu permintaan) membutuhkan keterampilan analitik, pengalaman, dan pengetahuan khusus sistem. Jika mudah , alat otomatis akan menjadi sempurna, dan konsultan penyesuaian kinerja akan jauh lebih sedikit dalam permintaan.

Sejauh menyangkut saran indeks yang hilang: ini adalah oportunistik. Pengoptimal membawa mereka ke perhatian Anda ketika mencoba untuk mencocokkan predikat dan urutan pengurutan yang diperlukan untuk indeks yang tidak ada. Saran karena itu didasarkan pada upaya pencocokan tertentu dalam konteks spesifik dari variasi sub-rencana tertentu yang sedang dipertimbangkan pada saat itu.

Dalam konteks, saran selalu masuk akal, dalam hal mengurangi perkiraan biaya akses data, sesuai dengan model pengoptimal. Itu tidak melakukan analisis yang lebih luas dari permintaan secara keseluruhan (apalagi beban kerja yang lebih luas), jadi Anda harus menganggap saran ini sebagai petunjuk lembut bahwa orang yang terampil perlu melihat indeks yang tersedia, dengan saran sebagai permulaan point (dan biasanya tidak lebih dari itu).

Dalam kasus Anda, (Status) INCLUDE (ID)saran mungkin muncul ketika sedang melihat kemungkinan bergabung hash atau bergabung (contoh nanti). Dalam konteks yang sempit itu, saran itu masuk akal. Untuk kueri secara keseluruhan, mungkin tidak. Indeks (ID, Status)memungkinkan loop bersarang bergabung dengan IDsebagai referensi luar: pencarian kesetaraan IDdan ketidaksetaraan Statusper iterasi.

Salah satu pilihan indeks yang mungkin adalah:

CREATE INDEX i1 ON dbo.I (ID, [Status]);
CREATE INDEX i1 ON dbo.IP (Deleted, OPID, IID) INCLUDE (Q);

... yang menghasilkan rencana seperti:

Kemungkinan rencana

Saya tidak mengatakan indeks ini optimal untuk Anda; mereka kebetulan bekerja untuk menghasilkan rencana yang tampak masuk akal bagi saya, tanpa bisa melihat statistik untuk tabel yang terlibat, atau definisi lengkap dan pengindeksan yang ada. Juga, saya tidak tahu apa-apa tentang beban kerja yang lebih luas atau permintaan nyata.

Atau (hanya untuk menunjukkan salah satu dari banyak kemungkinan tambahan):

CREATE INDEX i1 ON dbo.I ([Status]) INCLUDE (ID);
CREATE INDEX i1 ON dbo.IP (Deleted, IID, OPID) INCLUDE (Q);

Memberi:

Rencana alternatif

Rencana eksekusi dibuat menggunakan SQL Sentry Plan Explorer .

Paul White 9
sumber