Performa yang sangat aneh dengan indeks XML

32

Pertanyaan saya didasarkan pada ini: https://stackoverflow.com/q/35575990/5089204

Untuk memberikan jawaban di sana saya melakukan skenario-tes berikut.

Skenario pengujian

Pertama saya membuat tabel tes dan mengisinya dengan 100.000 baris. Angka acak (0 hingga 1000) harus mengarah ke ~ 100 baris untuk setiap nomor acak. Angka ini dimasukkan ke dalam varchar col dan sebagai nilai ke dalam XML Anda.

Lalu saya melakukan panggilan seperti OP di sana membutuhkannya dengan .exist () dan dengan .nodes () dengan keuntungan kecil untuk yang kedua, tetapi keduanya membutuhkan waktu 5 hingga 6 detik. Sebenarnya saya melakukan panggilan dua kali: kedua kalinya dalam urutan bertukar dan dengan params pencarian sedikit berubah dan dengan "// item" bukannya path lengkap untuk menghindari positif palsu melalui hasil atau rencana dalam cache.

Lalu saya membuat indeks XML dan melakukan panggilan yang sama

Sekarang - apa yang benar-benar mengejutkan saya! - .nodesdengan jalur penuh jauh lebih lambat dari sebelumnya (9 detik) tetapi .exist()turun menjadi setengah detik, dengan jalur penuh bahkan turun menjadi sekitar 0,10 detik. (Sementara .nodes()dengan jalur pendek lebih baik, tetapi masih jauh di belakang .exist())

Pertanyaan:

Tes saya sendiri memunculkan singkat: indeks XML dapat meledakkan database sangat. Mereka dapat mempercepat banyak hal (s. Edit 2), tetapi dapat memperlambat permintaan Anda juga. Saya ingin memahami cara kerjanya ... Kapan seseorang harus membuat indeks XML? Mengapa .nodes()dengan indeks lebih buruk daripada tanpa indeks? Bagaimana orang bisa menghindari dampak negatif?

CREATE TABLE #testTbl(ID INT IDENTITY PRIMARY KEY, SomeData VARCHAR(100),XmlColumn XML);
GO

DECLARE @RndNumber VARCHAR(100)=(SELECT CAST(CAST(RAND()*1000 AS INT) AS VARCHAR(100)));

INSERT INTO #testTbl VALUES('Data_' + @RndNumber,
'<error application="application" host="host" type="exception" message="message" >
  <serverVariables>
    <item name="name1">
      <value string="text" />
    </item>
    <item name="name2">
      <value string="text2" />
    </item>
    <item name="name3">
      <value string="text3" />
    </item>
    <item name="name4">
      <value string="text4" />
    </item>
    <item name="name5">
      <value string="My test ' +  @RndNumber + '" />
    </item>
    <item name="name6">
      <value string="text6" />
    </item>
    <item name="name7">
      <value string="text7" />
    </item>
  </serverVariables>
</error>');

GO 100000

DECLARE @d DATETIME=GETDATE()
SELECT #testTbl.*
FROM #testTbl
CROSS APPLY XmlColumn.nodes('/error/serverVariables/item[@name="name5" and value/@string="My test 600"]') AS a(b);
SELECT CAST(GETDATE()-@d AS TIME) AS NodesFullPath_no_index;
GO

DECLARE @d DATETIME=GETDATE();
SELECT * 
FROM #testTbl
WHERE XmlColumn.exist('/error/serverVariables/item[@name="name5" and value/@string="My test 600"]') = 1;
SELECT CAST(GETDATE()-@d AS TIME) AS ExistFullPath_no_index;
GO

DECLARE @d DATETIME=GETDATE();
SELECT * 
FROM #testTbl
WHERE XmlColumn.exist('//item[@name="name5" and value/@string="My test 500"]') = 1;
SELECT CAST(GETDATE()-@d AS TIME) AS ExistShortPath_no_index;
GO

DECLARE @d DATETIME=GETDATE()
SELECT #testTbl.*
FROM #testTbl
CROSS APPLY XmlColumn.nodes('//item[@name="name5" and value/@string="My test 500"]') AS a(b);
SELECT CAST(GETDATE()-@d AS TIME) AS NodesShortPath_no_index;
GO

CREATE PRIMARY XML INDEX PXML_test_XmlColum1 ON #testTbl(XmlColumn);
CREATE XML INDEX IXML_test_XmlColumn2 ON #testTbl(XmlColumn) USING XML INDEX PXML_test_XmlColum1 FOR PATH;
GO

DECLARE @d DATETIME=GETDATE()
SELECT #testTbl.*
FROM #testTbl
CROSS APPLY XmlColumn.nodes('/error/serverVariables/item[@name="name5" and value/@string="My test 600"]') AS a(b);
SELECT CAST(GETDATE()-@d AS TIME) AS NodesFullPath_with_index;
GO

DECLARE @d DATETIME=GETDATE();
SELECT * 
FROM #testTbl
WHERE XmlColumn.exist('/error/serverVariables/item[@name="name5" and value/@string="My test 600"]') = 1;
SELECT CAST(GETDATE()-@d AS TIME) AS ExistFullPath_with_index;
GO

DECLARE @d DATETIME=GETDATE();
SELECT * 
FROM #testTbl
WHERE XmlColumn.exist('//item[@name="name5" and value/@string="My test 500"]') = 1;
SELECT CAST(GETDATE()-@d AS TIME) AS ExistShortPath_with_index;
GO

DECLARE @d DATETIME=GETDATE()
SELECT #testTbl.*
FROM #testTbl
CROSS APPLY XmlColumn.nodes('//item[@name="name5" and value/@string="My test 500"]') AS a(b);
SELECT CAST(GETDATE()-@d AS TIME) AS NodesShortPath_with_index;
GO

DROP TABLE #testTbl;

EDIT 1 - Hasil

Ini adalah salah satu hasil dengan SQL Server 2012 yang diinstal secara lokal pada laptop sedang. Dalam tes ini saya tidak dapat mereproduksi dampak negatif yang ekstrem NodesFullPath_with_index, meskipun lebih lambat daripada tanpa indeks ...

NodesFullPath_no_index    6.067
ExistFullPath_no_index    6.223
ExistShortPath_no_index   8.373
NodesShortPath_no_index   6.733

NodesFullPath_with_index  7.247
ExistFullPath_with_index  0.217
ExistShortPath_with_index 0.500
NodesShortPath_with_index 2.410

EDIT 2 Uji dengan XML lebih besar

Menurut saran TT, saya menggunakan XML di atas, tetapi menyalin item-node untuk mencapai sekitar 450 item. Saya membiarkan hit-node sangat tinggi di XML (karena saya pikir itu .exist()akan berhenti pada hit pertama, sementara .nodes()akan melanjutkan)

Membuat indeks-XML meledakkan file-mdf menjadi ~ 21GB, ~ 18GB tampaknya termasuk dalam indeks (!!!)

NodesFullPath_no_index    3min44
ExistFullPath_no_index    3min39
ExistShortPath_no_index   3min49
NodesShortPath_no_index   4min00

NodesFullPath_with_index  8min20
ExistFullPath_with_index  8,5 seconds !!!
ExistShortPath_with_index 1min21
NodesShortPath_with_index 13min41 !!!
Shnugo
sumber

Jawaban:

33

Tentu ada banyak hal yang terjadi di sini sehingga kita hanya perlu melihat ke mana arahnya.

Pertama, perbedaan waktu antara SQL Server 2012 dan SQL Server 2014 adalah karena estimator kardinalitas baru di SQL Server 2014. Anda dapat menggunakan tanda jejak di SQL Server 2014 untuk memaksa estimator lama dan kemudian Anda akan melihat waktu yang sama karakteristik dalam SQL Server 2014 seperti pada SQL Server 2012.

Membandingkan nodes()vs exist()tidak adil karena mereka tidak akan mengembalikan hasil yang sama jika ada lebih dari satu elemen yang cocok dalam XML untuk satu baris. exist()akan mengembalikan satu baris dari tabel dasar terlepas, sedangkan nodes()berpotensi dapat memberi Anda lebih dari satu baris yang dikembalikan untuk setiap baris dalam tabel dasar.
Kami tahu data tetapi SQL Server tidak dan harus membangun rencana kueri yang mempertimbangkan itu.

Untuk membuat nodes()kueri setara dengan exist()kueri, Anda bisa melakukan sesuatu seperti ini.

SELECT testTbl.*
FROM testTbl
WHERE EXISTS (
             SELECT *
             FROM XmlColumn.nodes('/error/serverVariables/item[@name="name5" and value/@string="My test 600"]') AS a(b)
             )

Dengan kueri seperti itu, tidak ada perbedaan antara menggunakan nodes()atau exist()dan itu karena SQL Server membangun hampir rencana yang sama untuk dua versi yang tidak menggunakan indeks dan persis rencana yang sama ketika indeks digunakan. Itu benar untuk SQL Server 2012 dan SQL Server 2014.

Bagi saya di SQL Server 2012 kueri tanpa indeks XML membutuhkan waktu 6 detik menggunakan versi modifikasi dari nodes()kueri di atas. Tidak ada perbedaan antara menggunakan jalur lengkap atau jalur pendek. Dengan indeks XML di tempat versi jalur lengkap adalah yang tercepat dan memakan waktu 5 ms dan menggunakan jalur pendek memakan waktu sekitar 500 ms. Meneliti rencana kueri akan memberi tahu Anda mengapa ada perbedaan tetapi versi singkatnya adalah bahwa ketika Anda menggunakan jalur pendek, SQL Server mencari dalam indeks di jalur pendek (rentang mencari menggunakan like) dan mengembalikan 700.000 baris sebelum membuang baris yang tidak cocok pada nilainya. Saat menggunakan path lengkap, SQL Server dapat menggunakan ekspresi path secara langsung bersama dengan nilai node untuk melakukan pencarian dan mengembalikan hanya 105 baris dari awal untuk dikerjakan.

Menggunakan SQL Server 2014 dan penduga kardinalitas baru, tidak ada perbedaan dalam kueri ini saat menggunakan indeks XML. Tanpa menggunakan indeks, kueri masih membutuhkan jumlah waktu yang sama tetapi 15 detik. Jelas bukan peningkatan di sini saat menggunakan barang baru.

Tidak yakin apakah saya benar-benar kehilangan jejak tentang pertanyaan Anda sebenarnya karena saya memodifikasi kueri menjadi setara, tetapi inilah yang saya yakini sekarang.

Mengapa nodes()kueri (versi asli) dengan indeks XML pada tempatnya lebih lambat secara signifikan saat indeks tidak digunakan?

Yah, jawabannya adalah bahwa pengoptimal paket permintaan SQL Server melakukan sesuatu yang buruk dan yang memperkenalkan operator spool. Saya tidak tahu mengapa tetapi kabar baiknya adalah bahwa ia sudah tidak ada lagi dengan estimator kardinalitas baru di SQL Server 2014.
Dengan tidak ada indeks di tempat permintaan membutuhkan waktu sekitar 7 detik tidak peduli apa pun estimator kardinalitas digunakan. Dengan indeks dibutuhkan 15 detik dengan estimator lama (SQL Server 2012) dan sekitar 2 detik dengan estimator baru (SQL Server 2014).

Catatan: Temuan di atas valid dengan data pengujian Anda. Mungkin ada cerita yang sangat berbeda untuk diceritakan jika Anda mengubah ukuran, bentuk, atau bentuk XML. Tidak ada cara untuk mengetahui dengan pasti tanpa pengujian dengan data yang sebenarnya Anda miliki di tabel.

Cara kerja indeks XML

Indeks XML di SQL Server diimplementasikan sebagai tabel internal. Indeks XML primer membuat tabel dengan kunci utama dari tabel dasar ditambah kolom id simpul, total 12 kolom. Ini akan memiliki satu baris per element/node/attribute etc.sehingga tabel tentu saja bisa menjadi sangat besar tergantung pada ukuran XML yang disimpan. Dengan indeks XML primer di tempat SQL Server dapat menggunakan kunci utama dari tabel internal untuk menemukan node XML dan nilai-nilai untuk setiap baris dalam tabel dasar.

Indeks XML sekunder terdiri dari tiga jenis. Saat Anda membuat indeks XML sekunder, ada indeks non-cluster yang dibuat di tabel internal dan, tergantung pada jenis indeks sekunder apa yang Anda buat, indeks kolom dan urutan kolom akan berbeda.

Dari CREATE XML INDEX (Transact-SQL) :

VALUE
Membuat indeks XML sekunder pada kolom dengan kolom utama (nilai simpul dan jalur) dari indeks XML primer.

PATH
Membuat indeks XML sekunder pada kolom yang dibangun di atas nilai jalur dan nilai simpul dalam indeks XML primer. Dalam indeks sekunder PATH, nilai jalur dan simpul adalah kolom kunci yang memungkinkan pencarian yang efisien saat mencari jalur.

PROPERTI
Menciptakan indeks XML sekunder pada kolom (nilai PK, jalur dan simpul) dari indeks XML primer di mana PK adalah kunci utama dari tabel dasar.

Jadi ketika Anda membuat indeks PATH, kolom pertama dalam indeks itu adalah ekspresi jalur dan kolom kedua adalah nilai di simpul itu. Sebenarnya, path disimpan dalam semacam format terkompresi dan dibalik. Bahwa itu disimpan terbalik adalah apa yang membuatnya berguna dalam pencarian menggunakan ekspresi jalur pendek. Dalam kasus jalur pendek yang Anda cari //item/value/@string, //item/@namedan //item. Karena jalur disimpan terbalik di kolom, SQL Server dapat menggunakan rentang pencarian dengan like = '€€€€€€%tempat €€€€€€jalur dibalik. Ketika Anda menggunakan path lengkap, tidak ada alasan untuk digunakan likekarena seluruh path dikodekan dalam kolom dan nilainya juga dapat digunakan dalam mencari predikat.

Pertanyaan anda :

Kapan seseorang harus membuat indeks XML?

Sebagai pilihan terakhir jika pernah. Lebih baik mendesain basis data Anda sehingga Anda tidak harus menggunakan nilai-nilai di dalam XML untuk difilter dalam klausa where. Jika Anda tahu sebelumnya bahwa Anda perlu melakukan itu, Anda dapat menggunakan promosi properti untuk membuat kolom yang dihitung yang dapat Anda indeks jika diperlukan. Sejak SQL Server 2012 SP1, Anda juga memiliki indeks XML selektif yang tersedia. Cara kerja di belakang layar hampir sama dengan indeks XML biasa, hanya Anda yang menentukan ekspresi jalur dalam definisi indeks dan hanya node yang cocok yang diindeks. Dengan begitu Anda bisa menghemat banyak ruang.

Mengapa .nodes () dengan indeks lebih buruk daripada tanpa?

Ketika ada indeks XML yang dibuat pada tabel, SQL Server akan selalu menggunakan indeks itu (tabel internal) untuk mendapatkan data. Keputusan itu dilakukan sebelum pengoptimal memiliki suara dalam apa yang cepat dan apa yang tidak cepat. Input ke optimizer ditulis ulang sehingga menggunakan tabel internal dan setelah itu terserah optimizer untuk melakukan yang terbaik seperti dengan kueri reguler. Ketika tidak ada indeks yang digunakan, ada beberapa fungsi bernilai tabel yang digunakan sebagai gantinya. Intinya adalah bahwa Anda tidak dapat mengetahui apa yang akan lebih cepat tanpa pengujian.

Bagaimana orang bisa menghindari dampak negatif?

Pengujian

Mikael Eriksson
sumber
2
Ide-ide Anda tentang perbedaan .nodes()dan .exist()meyakinkan. Juga fakta bahwa indeks dengan full path searchlebih cepat tampaknya mudah dimengerti. Ini berarti: Jika Anda membuat indeks XML, Anda harus selalu menyadari pengaruh negatif dengan XPath generik ( //atau *atau ..atau [filter]atau apa pun tidak sekadar XPath ...). Sebenarnya Anda harus menggunakan jalur lengkap saja - undian belakang yang cukup hebat ...
Shnugo