Nogle interessante diskussioner udvikler sig altid omkring emnet at splitte strenge. I to tidligere blogindlæg, "Opdel strenge på den rigtige måde – eller den næstbedste måde" og "Opdeling af strenge:En opfølgning", håber jeg, at jeg har demonstreret, at det er frugtesløst at jagte den "bedst ydende" T-SQL split-funktion . Når opdeling faktisk er nødvendig, vinder CLR altid, og den næstbedste mulighed kan variere afhængigt af den aktuelle opgave. Men i disse indlæg antydede jeg, at opdeling på databasesiden måske ikke er nødvendig i første omgang.
SQL Server 2008 introducerede tabelværdiparametre, en måde at overføre en "tabel" fra en applikation til en lagret procedure uden at skulle bygge og parse en streng, serialisere til XML eller håndtere nogen af disse opdelingsmetoder. Så jeg tænkte, at jeg ville tjekke, hvordan denne metode kan sammenlignes med vinderen af vores tidligere tests – da det kan være en levedygtig mulighed, uanset om du kan bruge CLR eller ej. (For den ultimative bibel om TVP'er, se venligst andre SQL Server MVP Erland Sommarskogs omfattende artikel.)
Testene
Til denne test vil jeg lade som om, vi har at gøre med et sæt versionsstrenge. Forestil dig en C#-applikation, der passerer i et sæt af disse strenge (f.eks. som er blevet indsamlet fra et sæt brugere), og vi skal matche versionerne mod en tabel (f.eks. som angiver de serviceudgivelser, der er gældende for et specifikt sæt af versioner). Det er klart, at en rigtig applikation ville have flere kolonner end dette, men bare for at skabe noget volumen og stadig holde bordet tyndt (jeg bruger også NVARCHAR hele vejen igennem, fordi det er det, CLR split-funktionen tager, og jeg vil fjerne enhver tvetydighed på grund af implicit konvertering) :
CREATE TABLE dbo.VersionStrings(left_post NVARCHAR(5), right_post NVARCHAR(5)); CREATE CLUSTERED INDEX x ON dbo.VersionStrings(left_post, right_post); ;WITH x AS ( SELECT lp = CONVERT(DECIMAL(4,3), RIGHT(RTRIM(s1.[object_id]), 3)/1000.0) FROM sys.all_objects AS s1 CROSS JOIN sys.all_objects AS s2 ) INSERT dbo.VersionStrings ( left_post, right_post ) SELECT lp - CASE WHEN lp >= 0.9 THEN 0.1 ELSE 0 END, lp + (0.1 * CASE WHEN lp >= 0.9 THEN -1 ELSE 1 END) FROM x;
Nu hvor dataene er på plads, er den næste ting, vi skal gøre, at oprette en brugerdefineret tabeltype, der kan indeholde et sæt strenge. Den indledende tabeltype til at holde denne streng er ret simpel:
CREATE TYPE dbo.VersionStringsTVP AS TABLE (VersionString NVARCHAR(5));
Så har vi brug for et par lagrede procedurer for at acceptere listerne fra C#. For nemheds skyld tager vi igen en optælling, så vi kan være sikre på at udføre en komplet scanning, og vi ignorerer optællingen i applikationen:
CREATE PROCEDURE dbo.SplitTest_UsingCLR @list NVARCHAR(MAX) AS BEGIN SET NOCOUNT ON; SELECT c = COUNT(*) FROM dbo.VersionStrings AS v INNER JOIN dbo.SplitStrings_CLR(@list, N',') AS s ON s.Item BETWEEN v.left_post AND v.right_post; END GO CREATE PROCEDURE dbo.SplitTest_UsingTVP @list dbo.VersionStringsTVP READONLY AS BEGIN SET NOCOUNT ON; SELECT c = COUNT(*) FROM dbo.VersionStrings AS v INNER JOIN @list AS l ON l.VersionString BETWEEN v.left_post AND v.right_post; END GO
Bemærk, at en TVP, der overføres til en lagret procedure, skal markeres som LÆSEKUN – der er i øjeblikket ingen måde at udføre DML på dataene, som du ville gøre for en tabelvariabel eller midlertidig tabel. Erland har dog indsendt en meget populær anmodning om, at Microsoft gør disse parametre mere fleksible (og masser af dybere indsigt bag hans argumentation her).
Det skønne her er, at SQL Server slet ikke længere skal beskæftige sig med at opdele en streng – hverken i T-SQL eller ved at aflevere den til CLR – da den allerede er i en fast struktur, hvor den udmærker sig.
Dernæst en C#-konsolapplikation, der gør følgende:
- Accepterer et tal som et argument for at angive, hvor mange strengelementer der skal defineres
- Bygger en CSV-streng af disse elementer ved hjælp af StringBuilder for at overføre til den lagrede CLR-procedure
- Opbygger en datatabel med de samme elementer, der skal overføres til den lagrede TVP-procedure
- Tester også overheaden ved at konvertere en CSV-streng til en datatabel og omvendt, før de kalder de relevante lagrede procedurer
Koden til C#-appen findes i slutningen af artiklen. Jeg kan stave C#, men jeg er på ingen måde en guru; Jeg er sikker på, at der er ineffektiviteter, du kan se der, som kan få koden til at fungere en smule bedre. Men sådanne ændringer bør påvirke hele sættet af test på lignende måde.
Jeg kørte applikationen 10 gange med 100, 1.000, 2.500 og 5.000 elementer. Resultaterne var som følger (dette viser gennemsnitlig varighed i sekunder på tværs af de 10 tests):
Performance til side...
Ud over den klare ydelsesforskel har TVP'er en anden fordel - tabeltyper er meget nemmere at implementere end CLR-samlinger, især i miljøer, hvor CLR har været forbudt af andre årsager. Jeg håber, at barrierer for CLR gradvist forsvinder, og nye værktøjer gør implementering og vedligeholdelse mindre smertefuld, men jeg tvivler på, at den nemme indledende implementering for CLR nogensinde vil være nemmere end native tilgange.
På den anden side, ud over den skrivebeskyttede begrænsning, er tabeltyper ligesom aliastyper, idet de er svære at ændre efter kendsgerningen. Hvis du vil ændre størrelsen på en kolonne eller tilføje en kolonne, er der ingen ALTER TYPE-kommando, og for at DROP typen og genskabe den, skal du først fjerne referencer til typen fra alle procedurer, der bruger den . Så for eksempel i ovenstående tilfælde, hvis vi skulle øge kolonnen VersionString til NVARCHAR(32), skulle vi oprette en dummy-type og ændre den lagrede procedure (og enhver anden procedure, der bruger den):
CREATE TYPE dbo.VersionStringsTVPCopy AS TABLE (VersionString NVARCHAR(32)); GO ALTER PROCEDURE dbo.SplitTest_UsingTVP @list dbo.VersionStringsTVPCopy READONLY AS ... GO DROP TYPE dbo.VersionStringsTVP; GO CREATE TYPE dbo.VersionStringsTVP AS TABLE (VersionString NVARCHAR(32)); GO ALTER PROCEDURE dbo.SplitTest_UsingTVP @list dbo.VersionStringsTVP READONLY AS ... GO DROP TYPE dbo.VersionStringsTVPCopy; GO
(Eller alternativt, slip proceduren, slip typen, genskab typen og genskab proceduren.)
Konklusion
TVP-metoden overgik konsekvent CLR-opdelingsmetoden og med en større procentdel, da antallet af elementer steg. Selv tilføjelse af omkostningerne ved at konvertere en eksisterende CSV-streng til en DataTable gav meget bedre end-to-end ydeevne. Så jeg håber, at jeg, hvis jeg ikke allerede havde overbevist dig om at opgive dine T-SQL strengopdelingsteknikker til fordel for CLR, har opfordret dig til at give tabelværdisatte parametre et skud. Det burde være nemt at teste, selvom du ikke i øjeblikket bruger en datatabel (eller tilsvarende).
C#-koden, der bruges til disse tests
Som sagt er jeg ingen C#-guru, så der er sikkert masser af naive ting, jeg laver her, men metoden burde være helt klar.
using System; using System.IO; using System.Data; using System.Data.SqlClient; using System.Text; using System.Collections; namespace SplitTester { class SplitTester { static void Main(string[] args) { DataTable dt_pure = new DataTable(); dt_pure.Columns.Add("Item", typeof(string)); StringBuilder sb_pure = new StringBuilder(); Random r = new Random(); for (int i = 1; i <= Int32.Parse(args[0]); i++) { String x = r.NextDouble().ToString().Substring(0,5); sb_pure.Append(x).Append(","); dt_pure.Rows.Add(x); } using ( SqlConnection conn = new SqlConnection(@"Data Source=.; Trusted_Connection=yes;Initial Catalog=Splitter") ) { conn.Open(); // four cases: // (1) pass CSV string directly to CLR split procedure // (2) pass DataTable directly to TVP procedure // (3) serialize CSV string from DataTable and pass CSV to CLR procedure // (4) populate DataTable from CSV string and pass DataTable to TCP procedure // ********** (1) ********** // write(Environment.NewLine + "Starting (1)"); SqlCommand c1 = new SqlCommand("dbo.SplitTest_UsingCLR", conn); c1.CommandType = CommandType.StoredProcedure; c1.Parameters.AddWithValue("@list", sb_pure.ToString()); c1.ExecuteNonQuery(); c1.Dispose(); write("Finished (1)"); // ********** (2) ********** // write(Environment.NewLine + "Starting (2)"); SqlCommand c2 = new SqlCommand("dbo.SplitTest_UsingTVP", conn); c2.CommandType = CommandType.StoredProcedure; SqlParameter tvp1 = c2.Parameters.AddWithValue("@list", dt_pure); tvp1.SqlDbType = SqlDbType.Structured; c2.ExecuteNonQuery(); c2.Dispose(); write("Finished (2)"); // ********** (3) ********** // write(Environment.NewLine + "Starting (3)"); StringBuilder sb_fake = new StringBuilder(); foreach (DataRow dr in dt_pure.Rows) { sb_fake.Append(dr.ItemArray[0].ToString()).Append(","); } SqlCommand c3 = new SqlCommand("dbo.SplitTest_UsingCLR", conn); c3.CommandType = CommandType.StoredProcedure; c3.Parameters.AddWithValue("@list", sb_fake.ToString()); c3.ExecuteNonQuery(); c3.Dispose(); write("Finished (3)"); // ********** (4) ********** // write(Environment.NewLine + "Starting (4)"); DataTable dt_fake = new DataTable(); dt_fake.Columns.Add("Item", typeof(string)); string[] list = sb_pure.ToString().Split(','); for (int i = 0; i < list.Length; i++) { if (list[i].Length > 0) { dt_fake.Rows.Add(list[i]); } } SqlCommand c4 = new SqlCommand("dbo.SplitTest_UsingTVP", conn); c4.CommandType = CommandType.StoredProcedure; SqlParameter tvp2 = c4.Parameters.AddWithValue("@list", dt_fake); tvp2.SqlDbType = SqlDbType.Structured; c4.ExecuteNonQuery(); c4.Dispose(); write("Finished (4)"); } } static void write(string msg) { Console.WriteLine(msg + ": " + DateTime.UtcNow.ToString("HH:mm:ss.fffff")); } } }