Indeks pada kolom Persistent Computed membutuhkan pencarian kunci untuk mendapatkan kolom dalam ekspresi yang dihitung

24

Saya memiliki kolom yang tetap dihitung pada tabel yang hanya terdiri kolom gabungan, misalnya

CREATE TABLE dbo.T 
(   
    ID INT IDENTITY(1, 1) NOT NULL CONSTRAINT PK_T_ID PRIMARY KEY,
    A VARCHAR(20) NOT NULL,
    B VARCHAR(20) NOT NULL,
    C VARCHAR(20) NOT NULL,
    D DATE NULL,
    E VARCHAR(20) NULL,
    Comp AS A + '-' + B + '-' + C PERSISTED NOT NULL 
);

Dalam hal Compini tidak unik, dan D adalah valid dari tanggal setiap kombinasi A, B, C, oleh karena itu saya menggunakan kueri berikut untuk mendapatkan tanggal akhir untuk masing-masing A, B, C(pada dasarnya tanggal mulai berikutnya untuk nilai Comp yang sama):

SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 t2.D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY t2.D
            )
FROM    dbo.T t1
WHERE   t1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY t1.Comp;

Saya kemudian menambahkan indeks ke kolom yang dihitung untuk membantu dalam permintaan ini (dan juga yang lain):

CREATE NONCLUSTERED INDEX IX_T_Comp_D ON dbo.T (Comp, D) WHERE D IS NOT NULL;

Namun rencana kueri mengejutkan saya. Saya akan berpikir bahwa karena saya memiliki klausa di mana menyatakan itu D IS NOT NULLdan saya menyortir Comp, dan tidak merujuk kolom di luar indeks bahwa indeks pada kolom dihitung dapat digunakan untuk memindai t1 dan t2, tetapi saya melihat indeks berkerumun memindai.

masukkan deskripsi gambar di sini

Jadi saya memaksakan penggunaan indeks ini untuk melihat apakah itu menghasilkan rencana yang lebih baik:

SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 t2.D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY t2.D
            )
FROM    dbo.T t1 WITH (INDEX (IX_T_Comp_D))
WHERE   t1.D IS NOT NULL
ORDER BY t1.Comp;

Yang memberi rencana ini

masukkan deskripsi gambar di sini

Ini menunjukkan bahwa pencarian kunci sedang digunakan, rinciannya adalah:

masukkan deskripsi gambar di sini

Sekarang, menurut dokumentasi SQL-Server:

Anda dapat membuat indeks pada kolom yang dihitung yang didefinisikan dengan ekspresi deterministik, tetapi tidak tepat, jika kolom ditandai PERSISTED dalam pernyataan CREATE TABLE atau ALTER TABLE. Ini berarti bahwa Database Engine menyimpan nilai yang dihitung dalam tabel, dan memutakhirkannya ketika kolom lain yang bergantung pada kolom yang dihitung diperbarui. Mesin Database menggunakan nilai-nilai tetap ini ketika itu membuat indeks pada kolom, dan ketika indeks direferensikan dalam kueri. Opsi ini memungkinkan Anda untuk membuat indeks pada kolom yang dihitung ketika Database Engine tidak dapat membuktikan dengan akurat apakah suatu fungsi yang mengembalikan ekspresi kolom yang dihitung, khususnya fungsi CLR yang dibuat dalam .NET Framework, keduanya deterministik dan tepat.

Jadi jika, seperti dokumen mengatakan "Mesin Basis Data menyimpan nilai yang dihitung dalam tabel" , dan nilainya juga disimpan dalam indeks saya, mengapa Pencarian Kunci diperlukan untuk mendapatkan A, B dan C ketika mereka tidak dirujuk dalam pertanyaannya sama sekali? Saya berasumsi mereka sedang digunakan untuk menghitung Comp, tetapi mengapa? Juga, mengapa kueri dapat menggunakan indeks aktif t2, tetapi tidak aktif t1?

Kueri dan DDL pada SQL Fiddle

NB Saya telah menandai SQL Server 2008 karena ini adalah versi yang menjadi masalah utama saya, tetapi saya juga mendapatkan perilaku yang sama pada tahun 2012.

GarethD
sumber

Jawaban:

20

Mengapa Pencarian Kunci diperlukan untuk mendapatkan A, B dan C ketika mereka tidak dirujuk dalam kueri sama sekali? Saya berasumsi mereka sedang digunakan untuk menghitung Comp, tetapi mengapa?

Kolom A, B, and C yang dirujuk dalam rencana permintaan - mereka digunakan oleh mencari di T2.

Juga, mengapa kueri dapat menggunakan indeks pada t2, tetapi tidak pada t1?

Pengoptimal memutuskan bahwa pemindaian indeks berkerumun lebih murah daripada pemindaian indeks yang tidak disaring dan kemudian melakukan pencarian untuk mengambil nilai untuk kolom A, B, dan C.

Penjelasan

Pertanyaan sebenarnya adalah mengapa pengoptimal merasa perlu untuk mengambil A, B, dan C untuk mencari indeks sama sekali. Kami akan mengharapkannya untuk membaca Compkolom menggunakan pemindaian indeks nonclustered, dan kemudian melakukan pencarian pada indeks yang sama (alias T2) untuk menemukan catatan Top 1.

Pengoptimal kueri memperluas referensi kolom yang dihitung sebelum optimasi dimulai, untuk memberikan kesempatan menilai biaya berbagai rencana kueri. Untuk beberapa pertanyaan, memperluas definisi kolom yang dikomputasi memungkinkan pengoptimal untuk menemukan rencana yang lebih efisien.

Ketika pengoptimal menemui subquery yang berkorelasi, ia mencoba untuk 'membuka gulungannya' ke bentuk yang menurutnya lebih mudah untuk dipikirkan. Jika tidak dapat menemukan penyederhanaan yang lebih efektif, ia terpaksa menulis ulang subquery yang berkorelasi sebagai berlaku (gabungan yang berkorelasi):

Terapkan penulisan ulang

Kebetulan bahwa ini berlaku membuka gulungan menempatkan pohon permintaan logis ke dalam bentuk yang tidak berfungsi dengan baik dengan normalisasi proyek (tahap selanjutnya yang terlihat untuk mencocokkan ekspresi umum dengan kolom dihitung, antara lain).

Dalam kasus Anda, cara kueri ditulis berinteraksi dengan detail internal pengoptimal sehingga definisi ekspresi yang diperluas tidak dicocokkan kembali ke kolom yang dihitung, dan Anda berakhir dengan pencarian yang mereferensikan kolom A, B, and Calih-alih kolom yang dihitung Comp,. Ini adalah akar permasalahannya.

Penanganan masalah

Satu ide untuk mengatasi efek samping ini adalah menulis kueri sebagai berlaku secara manual:

SELECT
    T1.ID,
    T1.Comp,
    T1.D,
    CA.D2
FROM dbo.T AS T1
CROSS APPLY
(  
    SELECT TOP (1)
        D2 = T2.D
    FROM dbo.T AS T2
    WHERE
        T2.Comp = T1.Comp
        AND T2.D > T1.D
    ORDER BY
        T2.D ASC
) AS CA
WHERE
    T1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY
    T1.Comp;

Sayangnya, kueri ini tidak akan menggunakan indeks yang difilter seperti yang kami harapkan. Tes ketimpangan pada kolom Ddi dalam berlaku menolak NULLs, sehingga predikat yang tampaknya berlebihan WHERE T1.D IS NOT NULLdioptimalkan pergi.

Tanpa predikat eksplisit itu, logika pencocokan indeks yang disaring memutuskan tidak dapat menggunakan indeks yang difilter. Ada beberapa cara untuk mengatasi efek samping kedua ini, tetapi yang paling mudah adalah dengan mengubah tanda silang berlaku untuk penerapan luar (mencerminkan logika penulisan ulang pengoptimal yang dilakukan sebelumnya pada subquery yang dikorelasikan):

SELECT
    T1.ID,
    T1.Comp,
    T1.D,
    CA.D2
FROM dbo.T AS T1
OUTER APPLY
(  
    SELECT TOP (1)
        D2 = T2.D
    FROM dbo.T AS T2
    WHERE
        T2.Comp = T1.Comp
        AND T2.D > T1.D
    ORDER BY
        T2.D ASC
) AS CA
WHERE
    T1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY
    T1.Comp;

Sekarang optimizer tidak perlu menggunakan penulisan ulang yang berlaku itu sendiri (sehingga pencocokan kolom yang dikomputasi berfungsi seperti yang diharapkan) dan predikatnya juga tidak dioptimalkan, sehingga indeks yang difilter dapat digunakan untuk kedua operasi akses data, dan pencarian menggunakan Compkolom di kedua sisi:

Paket Aplikasi Luar

Ini umumnya lebih disukai daripada menambahkan A, B, dan C sebagai INCLUDEdkolom dalam indeks yang difilter, karena ini mengatasi akar penyebab masalah, dan tidak perlu memperluas indeks secara tidak perlu.

Kolom yang dihitung terus-menerus

Sebagai catatan tambahan, tidak perlu untuk menandai kolom yang dihitung sebagai PERSISTED, jika Anda tidak keberatan mengulangi definisinya dalam CHECKbatasan:

CREATE TABLE dbo.T 
(   
    ID integer IDENTITY(1, 1) NOT NULL,
    A varchar(20) NOT NULL,
    B varchar(20) NOT NULL,
    C varchar(20) NOT NULL,
    D date NULL,
    E varchar(20) NULL,
    Comp AS A + '-' + B + '-' + C,

    CONSTRAINT CK_T_Comp_NotNull
        CHECK (A + '-' + B + '-' + C IS NOT NULL),

    CONSTRAINT PK_T_ID 
        PRIMARY KEY (ID)
);

CREATE NONCLUSTERED INDEX IX_T_Comp_D
ON dbo.T (Comp, D) 
WHERE D IS NOT NULL;

Kolom yang dihitung hanya diperlukan PERSISTEDdalam kasus ini jika Anda ingin menggunakan NOT NULLbatasan atau untuk referensi Compkolom secara langsung (alih-alih mengulangi definisinya) dalam CHECKkendala.

Paul White mengatakan GoFundMonica
sumber
2
1 BTW Saya menemukan kasus lain pencarian berlebihan sementara melihat ini bahwa Anda mungkin (atau mungkin tidak) menemukan minat. SQL Fiddle .
Martin Smith
@ MartinSmith Ya itu menarik. Aturan umum lain menulis ulang ( FOJNtoLSJNandLASJN) yang menghasilkan hal-hal yang tidak berfungsi seperti yang kita harapkan, dan meninggalkan sampah (BaseRow / Checksums) yang berguna dalam beberapa jenis paket (mis. Kursor) tetapi tidak diperlukan di sini.
Paul White mengatakan GoFundMonica
Ah Chkitu checksum! Terima kasih, saya tidak yakin tentang itu. Awalnya saya berpikir mungkin ada hubungannya dengan kendala pemeriksaan.
Martin Smith
6

Meskipun ini mungkin sedikit kebetulan karena sifat buatan dari data pengujian Anda, karena seperti yang Anda sebutkan SQL 2012 saya mencoba menulis ulang:

SELECT  ID,
        Comp,
        D,
        D2 = LEAD(D) OVER(PARTITION BY COMP ORDER BY D)
FROM    dbo.T 
WHERE   D IS NOT NULL
ORDER BY Comp;

Ini menghasilkan rencana berbiaya rendah yang bagus menggunakan indeks Anda dan dengan bacaan yang jauh lebih rendah daripada opsi lain (dan hasil yang sama untuk data pengujian Anda).

Biaya Plan Explorer untuk empat opsi: Asli;  asli dengan petunjuk;  luar berlaku dan Timbal

Saya menduga data Anda yang sebenarnya lebih rumit sehingga mungkin ada beberapa skenario di mana kueri ini berperilaku semantik berbeda dengan Anda, tetapi kadang-kadang menunjukkan fitur baru dapat membuat perbedaan nyata.

Saya melakukan percobaan dengan beberapa data yang lebih bervariasi dan menemukan beberapa skenario untuk dicocokkan dan beberapa tidak:

--Example 1: results matched
TRUNCATE TABLE dbo.t

-- Generate some more interesting test data
;WITH cte AS
(
SELECT TOP 1000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
    CROSS JOIN master.sys.columns c2
    CROSS JOIN master.sys.columns c3
)
INSERT T (A, B, C, D)
SELECT  'A' + CAST( a.rn AS VARCHAR(5) ),
        'B' + CAST( a.rn AS VARCHAR(5) ),
        'C' + CAST( a.rn AS VARCHAR(5) ),
        DATEADD(DAY, a.rn + b.rn, '1 Jan 2013')
FROM cte a
    CROSS JOIN cte b
WHERE a.rn % 3 = 0
 AND b.rn % 5 = 0
ORDER BY 1, 2, 3
GO


-- Original query
SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY D
            )
INTO #tmp1
FROM    dbo.T t1 
WHERE   t1.D IS NOT NULL
ORDER BY t1.Comp;
GO

SELECT  ID,
        Comp,
        D,
        D2 = LEAD(D) OVER(PARTITION BY COMP ORDER BY D)
INTO #tmp2
FROM    dbo.T 
WHERE   D IS NOT NULL
ORDER BY Comp;
GO


-- Checks ...
SELECT * FROM #tmp1
EXCEPT
SELECT * FROM #tmp2

SELECT * FROM #tmp2
EXCEPT
SELECT * FROM #tmp1


Example 2: results did not match
TRUNCATE TABLE dbo.t

-- Generate some more interesting test data
;WITH cte AS
(
SELECT TOP 1000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
    CROSS JOIN master.sys.columns c2
    CROSS JOIN master.sys.columns c3
)
INSERT T (A, B, C, D)
SELECT  'A' + CAST( a.rn AS VARCHAR(5) ),
        'B' + CAST( a.rn AS VARCHAR(5) ),
        'C' + CAST( a.rn AS VARCHAR(5) ),
        DATEADD(DAY, a.rn, '1 Jan 2013')
FROM cte a

-- Add some more data
INSERT dbo.T (A, B, C, D)
SELECT A, B, C, D 
FROM dbo.T
WHERE DAY(D) In ( 3, 7, 9 )


INSERT dbo.T (A, B, C, D)
SELECT A, B, C, DATEADD( day, 1, D )
FROM dbo.T
WHERE DAY(D) In ( 12, 13, 17 )


SELECT * FROM #tmp1
EXCEPT
SELECT * FROM #tmp2

SELECT * FROM #tmp2
EXCEPT
SELECT * FROM #tmp1

SELECT * FROM #tmp2
INTERSECT
SELECT * FROM #tmp1


select * from #tmp1
where comp = 'A2-B2-C2'

select * from #tmp2
where comp = 'A2-B2-C2'
wBob
sumber
1
Yah itu menggunakan indeks tetapi hanya sampai titik tertentu. Jika compbukan kolom yang dihitung Anda tidak melihat jenisnya.
Martin Smith
Terima kasih. Skenario aktual saya tidak jauh lebih rumit dan LEADfungsinya bekerja persis seperti yang saya inginkan pada contoh lokal saya tahun 2012 express. Sayangnya, ketidaknyamanan kecil ini bagi saya belum dianggap sebagai alasan yang cukup baik untuk meningkatkan server produksi ...
GarethD
-1

Ketika saya mencoba melakukan tindakan yang sama, saya mendapat hasil yang lain. Pertama, rencana eksekusi saya untuk tabel tanpa indeks terlihat sebagai berikut:masukkan deskripsi gambar di sini

Seperti yang dapat kita lihat dari Pemindaian Indeks Berkelompok (t2), predikat digunakan untuk menentukan baris yang diperlukan untuk dikembalikan (karena kondisi):

masukkan deskripsi gambar di sini

Ketika indeks ditambahkan, tidak masalah apakah itu didefinisikan oleh operator DENGAN atau tidak, rencana eksekusi menjadi sebagai berikut:

masukkan deskripsi gambar di sini

Seperti yang dapat kita lihat, Pemindaian Indeks Berkelompok digantikan oleh Pemindaian Indeks. Seperti yang kita lihat di atas, SQL Server menggunakan kolom sumber dari kolom yang dihitung untuk melakukan pencocokan kueri bersarang. Selama pemindaian indeks berkerumun semua nilai ini dapat diperoleh dalam waktu yang sama (tidak diperlukan operasi tambahan). Ketika indeks ditambahkan, pemfilteran baris yang diperlukan dari tabel (di pilih utama) berkinerja sesuai dengan indeks, tetapi nilai-nilai kolom sumber untuk kolom yang dihitung compmasih perlu diperoleh (operasi terakhir Nested Loop) .

masukkan deskripsi gambar di sini

Karena ini operasi Pencarian Kunci digunakan - untuk mendapatkan data dari kolom sumber yang dihitung.

PS Tampak seperti bug di SQL Server.

Sandr
sumber