Database
 sql >> Teknologi Basis Data >  >> RDS >> Database

Kejutan dan Asumsi Kinerja :STRING_SPLIT()

Lebih dari tiga tahun yang lalu sekarang, saya memposting seri tiga bagian tentang pemisahan string:

  • Pisahkan string dengan cara yang benar – atau cara terbaik berikutnya
  • Memisahkan String :Tindak Lanjut
  • Memisahkan String :Sekarang dengan lebih sedikit T-SQL

Kemudian di bulan Januari, saya menghadapi masalah yang sedikit lebih rumit:

  • Membandingkan metode pemisahan string/penggabungan

Selama ini, kesimpulan saya adalah:BERHENTI MELAKUKAN INI DI T-SQL . Gunakan CLR atau, lebih baik lagi, berikan parameter terstruktur seperti DataTables dari aplikasi Anda ke parameter nilai tabel (TVP) dalam prosedur Anda, hindari semua konstruksi string dan dekonstruksi sama sekali – yang sebenarnya merupakan bagian dari solusi yang menyebabkan masalah kinerja.

Dan kemudian SQL Server 2016 datang...

Ketika RC0 dirilis, fungsi baru didokumentasikan tanpa banyak kemeriahan:STRING_SPLIT . Contoh singkat:

SELECT * FROM STRING_SPLIT('a,b,cd', ','); /* hasil:nilai -------- a b cd*/

Ini menarik perhatian beberapa rekan, termasuk Dave Ballantyne, yang menulis tentang fitur utama – tetapi cukup baik untuk menawarkan hak penolakan pertama pada perbandingan kinerja.

Ini sebagian besar merupakan latihan akademis, karena dengan serangkaian batasan yang besar dan kuat pada iterasi pertama fitur, itu mungkin tidak akan layak untuk sejumlah besar kasus penggunaan. Berikut adalah daftar pengamatan yang saya dan Dave lakukan, beberapa di antaranya mungkin menjadi pemecah kesepakatan dalam skenario tertentu:

  • fungsi ini mengharuskan database berada di tingkat kompatibilitas 130;
  • hanya menerima pembatas satu karakter;
  • tidak ada cara untuk menambahkan kolom keluaran (seperti kolom yang menunjukkan posisi ordinal dalam string);
    • terkait, tidak ada cara untuk mengontrol penyortiran – satu-satunya opsi adalah arbitrer dan alfabet ORDER BY value;
  • sejauh ini, selalu memperkirakan 50 baris keluaran;
  • saat menggunakannya untuk DML, dalam banyak kasus Anda akan mendapatkan table spool (untuk perlindungan Hallowe'en);
  • NULL masukan mengarah ke hasil kosong;
  • tidak ada cara untuk menekan predikat, seperti menghilangkan duplikat atau string kosong karena pembatas berurutan;
  • tidak ada cara untuk melakukan operasi terhadap nilai keluaran sampai setelah fakta (misalnya, banyak fungsi pemisahan melakukan LTRIM/RTRIM atau konversi eksplisit untuk Anda – STRING_SPLIT memuntahkan semua yang jelek, seperti spasi awal).

Jadi dengan keterbatasan tersebut, kita dapat beralih ke beberapa pengujian kinerja. Mengingat rekam jejak Microsoft dengan fungsi bawaan yang memanfaatkan CLR di balik selimut (cough FORMAT() batuk ), saya ragu apakah fungsi baru ini dapat mendekati metode tercepat yang saya uji hingga saat ini.

Mari kita gunakan pemisah string untuk memisahkan string angka yang dipisahkan koma, dengan cara ini teman baru kita JSON bisa ikut dan bermain juga. Dan kami akan mengatakan bahwa tidak ada daftar yang dapat melebihi 8.000 karakter, jadi tidak ada MAX jenis diperlukan, dan karena itu angka, kita tidak perlu berurusan dengan sesuatu yang eksotis seperti Unicode.

Pertama, mari kita buat fungsi kita, beberapa di antaranya saya adaptasi dari artikel pertama di atas. Saya meninggalkan pasangan yang menurut saya tidak akan bersaing; Saya akan menyerahkannya sebagai latihan kepada pembaca untuk mengujinya.

    Tabel Angka

    Yang ini lagi membutuhkan beberapa pengaturan, tetapi ini bisa menjadi tabel yang cukup kecil karena batasan buatan yang kami tempatkan:

    ATUR NOCOUNT AKTIF; MENYATAKAN @UpperLimit INT =8000;;DENGAN n AS( SELECT x =ROW_NUMBER() OVER (ORDER BY s1.[object_id]) FROM sys.all_objects AS s1 CROSS GABUNG sys.all_objects AS s2)SELECT Number =x INTO dbo.Numbers FROM n WHERE x ANTARA 1 DAN @UpperLimit;GOCREATE UNIK CLUSTERED INDEX n PADA dbo.Numbers(Number);

    Maka fungsinya:

    BUAT FUNGSI dbo.SplitStrings_Numbers( @List varchar(8000), @Delimiter char(1))RETURNS TABLE WITH SCHEMABINDINGAS RETURN ( SELECT [Value] =SUBSTRING(@List, [Number], CHARINDEX(@Delimiter, @List + @Delimiter, [Number]) - [Number]) FROM dbo.Numbers WHERE Number <=LEN(@List) AND SUBSTRING(@Delimiter + @List, [Number], 1) =@Delimiter );

    JSON

    Berdasarkan pendekatan yang pertama kali diungkapkan oleh tim mesin penyimpanan, saya membuat pembungkus serupa di sekitar OPENJSON , perhatikan bahwa pembatas harus berupa koma dalam kasus ini, atau Anda harus melakukan substitusi string tugas berat sebelum meneruskan nilai ke fungsi asli:

    CREATE FUNCTION dbo.SplitStrings_JSON( @List varchar(8000), @Delimiter char(1) -- diabaikan tetapi membuat pengujian otomatis lebih mudah)RETURNS TABLE WITH SCHEMABINDINGAS RETURN (SELECT value FROM OPENJSON( CHAR(91) + @List + CHAR(93) ));

    CHAR(91)/CHAR(93) masing-masing hanya mengganti [ dan ] karena masalah pemformatan.

    XML

    CREATE FUNCTION dbo.SplitStrings_XML( @List varchar(8000), @Delimiter char(1))RETURNS TABLE WITH SCHEMABINDINGAS RETURN (SELECT [value] =y.i.value('(./text()))[1]', 'varchar(8000)') FROM (PILIH x =CONVERT(XML, '' + REPLACE(@List, @Delimiter, '') + '').query ('.') ) SEBAGAI LINTAS BERLAKU x.nodes('i') AS y(i));

    CLR

    Saya sekali lagi meminjam kode pemisahan terpercaya Adam Machanic dari hampir tujuh tahun yang lalu, meskipun mendukung Unicode, MAX jenis, dan pembatas multi-karakter (dan sebenarnya, karena saya tidak ingin mengacaukan kode fungsi sama sekali, ini membatasi string input kami menjadi 4.000 karakter, bukan 8.000):

    BUAT FUNGSI dbo.SplitStrings_CLR( @List nvarchar(MAX), @Delimiter nvarchar(255))RETURNS TABLE ( nilai nvarchar(4000) )NAMA EKSTERNAL CLRUtilities.UserDefinedFunctions.SplitString_Multi;

    STRING_SPLIT

    Untuk konsistensi, saya membungkus STRING_SPLIT :

    BUAT FUNGSI dbo.SplitStrings_Native( @List varchar(8000), @Delimiter char(1))RETURNS TABLE WITH SCHEMABINDINGAS RETURN (SELECT nilai FROM STRING_SPLIT(@List, @Delimiter));

Data Sumber &Pemeriksaan Kesehatan

Saya membuat tabel ini sebagai sumber input string ke fungsi:

BUAT TABEL dbo.SourceTable( RowNum int IDENTITY(1,1) PRIMARY KEY, StringValue varchar(8000));;DENGAN x AS ( SELECT TOP (60000) x =STUFF((SELECT TOP (ABS(o.[object_id] % 20)) ',' + CONVERT(varchar(12), c.[object_id]) FROM sys.all_columns AS c WHERE c.[object_id]  

Sekedar referensi, mari validasi bahwa 50.000 baris berhasil masuk ke tabel, dan periksa panjang rata-rata string dan jumlah rata-rata elemen per string:

SELECT [Nilai] =COUNT(*), AvgStringLength =AVG(1.0*LEN(StringValue)), AvgElementCount =AVG(1.0*LEN(StringValue)-LEN(REPLACE(StringValue, ',','')) ) DARI dbo.SourceTable; /* hasil:Nilai AvgStringLength AbgElementCount ------ --------------- --------------- 50000 108.476380 8.911840*/ 

Dan terakhir, mari pastikan setiap fungsi mengembalikan data yang tepat untuk RowNum yang diberikan , jadi kami hanya akan memilih satu secara acak dan membandingkan nilai yang diperoleh melalui setiap metode. Hasil Anda akan bervariasi tentu saja.

SELECT f.value FROM dbo.SourceTable AS s CROSS APPLY dbo.SplitStrings_/* metode */(s.StringValue, ',') AS f WHERE s.RowNum =37219 ORDER BY f.value;

Benar saja, semua fungsi berfungsi seperti yang diharapkan (pengurutan bukan numerik; ingat, fungsi menghasilkan string):

Contoh kumpulan keluaran dari setiap fungsi

Pengujian Kinerja

SELECT SYSDATETIME();GODECLARE @x VARCHAR(8000);SELECT @x =f.value FROM dbo.SourceTable AS s CROSS APPLY dbo.SplitStrings_/* metode */(s.StringValue,',') AS f;PERGI 100PILIH SYSDATETIME();

Saya menjalankan kode di atas 10 kali untuk setiap metode, dan rata-rata waktunya untuk masing-masing metode. Dan di sinilah kejutan datang untuk saya. Mengingat keterbatasan dalam STRING_SPLIT asli fungsi, asumsi saya adalah bahwa itu dilemparkan bersama dengan cepat, dan kinerja itu akan memberikan kepercayaan untuk itu. Wah hasilnya beda dari yang saya harapkan :

Durasi rata-rata STRING_SPLIT dibandingkan dengan metode lain

Pembaruan 20-03-2016

Berdasarkan pertanyaan di bawah dari Lars, saya menjalankan tes lagi dengan beberapa perubahan:

  • Saya memantau instance saya dengan SQL Sentry Performance Advisor untuk merekam profil CPU selama pengujian;
  • Saya menangkap statistik menunggu tingkat sesi di antara setiap batch;
  • Saya menyisipkan penundaan di antara kumpulan sehingga aktivitas akan terlihat berbeda secara visual di dasbor Performance Advisor.

Saya membuat tabel baru untuk menangkap informasi statistik tunggu:

CREATE TABLE dbo.Timings( dt datetime, test varchar(64), point varchar(64), session_id smallint, wait_type nvarchar(60), wait_time_ms bigint,);

Kemudian kode untuk setiap tes diubah menjadi ini:

TUNGGU PENUNDAAN '00:00:30'; DECLARE @d DATETIME =SYSDATETIME(); INSERT dbo.Timings(dt, test, point, wait_type, wait_time_ms)SELECT @d, test =/* 'method' */, point ='Start', wait_type, wait_time_msFROM sys.dm_exec_session_wait_stats WHERE session_id =@@SPID;GO DECLARE @x VARCHAR(8000);SELECT @x =f.value FROM dbo.SourceTable AS s CROSS APPLY dbo.SplitStrings_/* metode */(s.StringValue, ',') AS fGO 100 DECLARE @d DATETIME =SYSDATETIME(); INSERT dbo.Timings(dt, test, point, wait_type, wait_time_ms)SELECT @d, /* 'method' */, 'End', wait_type, wait_time_msFROM sys.dm_exec_session_wait_stats WHERE session_id =@@SPID;

Saya menjalankan tes dan kemudian menjalankan kueri berikut:

-- memvalidasi bahwa pengaturan waktu berada di rata-rata yang sama seperti tes tes sebelumnyaSELECT, DATEDIFF(SECOND, MIN(dt), MAX(dt)) FROM dbo.Timings WITH (NOLOCK)GROUP BY test ORDER BY 2 DESC; -- menentukan jendela untuk diterapkan ke dasbor Performance AdvisorSELECT MIN(dt), MAX(dt) FROM dbo.Timings; -- dapatkan statistik tunggu yang terdaftar untuk setiap tes sessionSELECT, wait_type, delta FROM( SELECT f.test, rn =RANK() OVER (PARTITION BY f.point ORDER BY f.dt), f.wait_type, delta =f.wait_time_ms - COALESCE(s.wait_time_ms, 0) FROM dbo.Timings AS f LEFT OUTER JOIN dbo.Timings AS s ON s.test =f.test AND s.wait_type =f.wait_type AND s.point ='Start' WHERE f.point ='End') AS x WHERE delta> 0ORDER BY rn, delta DESC;

Dari kueri pertama, pengaturan waktu tetap konsisten dengan pengujian sebelumnya (saya akan memetakannya lagi tetapi itu tidak akan mengungkapkan sesuatu yang baru).

Dari kueri kedua, saya dapat menyorot rentang ini di dasbor Performance Advisor, dan dari sana mudah untuk mengidentifikasi setiap batch:

Batch yang diambil pada bagan CPU di dasbor Performance Advisor

Jelas, semua metode *kecuali* STRING_SPLIT mematok satu inti selama pengujian (ini adalah mesin quad-core, dan CPU stabil pada 25%). Kemungkinan Lars menyindir di bawah STRING_SPLIT lebih cepat dengan mengorbankan CPU, tetapi tampaknya tidak demikian.

Akhirnya, dari kueri ketiga, saya dapat melihat statistik tunggu berikut yang bertambah setelah setiap batch:

Per-Sesi Menunggu, dalam milidetik

Waktu tunggu yang ditangkap oleh DMV tidak sepenuhnya menjelaskan durasi kueri, tetapi berfungsi untuk menunjukkan di mana tambahan menunggu terjadi.

Kesimpulan

Sementara CLR kustom masih menunjukkan keuntungan besar dibandingkan pendekatan T-SQL tradisional, dan menggunakan JSON untuk fungsi ini tampaknya tidak lebih dari hal baru, STRING_SPLIT adalah pemenang yang jelas – sejauh satu mil. Jadi, jika Anda hanya perlu membagi string dan dapat mengatasi semua keterbatasannya, sepertinya ini adalah opsi yang jauh lebih layak daripada yang saya harapkan. Semoga di build mendatang kita akan melihat fungsionalitas tambahan, seperti kolom output yang menunjukkan posisi ordinal setiap elemen, kemampuan untuk memfilter duplikat dan string kosong, dan pembatas multi-karakter.

Saya membahas beberapa komentar di bawah ini dalam dua posting lanjutan:

  • STRING_SPLIT() di SQL Server 2016 :Tindak Lanjut #1
  • STRING_SPLIT() di SQL Server 2016 :Tindak Lanjut #2

  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Fungsi Penyembunyian Data Mana yang Harus Saya Gunakan?

  2. Bagaimana cara menjatuhkan kolom dalam SQL?

  3. Menggunakan JShell di Java 9 di NetBeans 9.0, Bagian 2

  4. Kiat UniVerse

  5. GUNAKAN PETUNJUK dan DISABLE_OPTIMIZED_NESTED_LOOP