sql >> Databasteknik >  >> RDS >> Database

Splitting Strings:Nu med mindre T-SQL

Några intressanta diskussioner utvecklas alltid kring ämnet att dela strängar. I två tidigare blogginlägg, "Dela strängar på rätt sätt – eller näst bästa sätt" och "Dela strängar:En uppföljning", hoppas jag att jag har visat att det är fruktlöst att jaga den "bäst presterande" T-SQL-delade funktionen . När delning faktiskt är nödvändig vinner CLR alltid, och det näst bästa alternativet kan variera beroende på den faktiska uppgiften. Men i de inläggen antydde jag att uppdelning på databassidan kanske inte är nödvändig i första hand.

SQL Server 2008 introducerade tabellvärdade parametrar, ett sätt att överföra en "tabell" från en applikation till en lagrad procedur utan att behöva bygga och analysera en sträng, serialisera till XML eller ta itu med någon av dessa uppdelningsmetoder. Så jag tänkte kolla hur den här metoden står sig i jämförelse med vinnaren av våra tidigare tester – eftersom det kan vara ett gångbart alternativ, oavsett om du kan använda CLR eller inte. (För den ultimata bibeln om TVP:er, se andra SQL Server MVP Erland Sommarskogs omfattande artikel.)

Testen

För det här testet ska jag låtsas att vi har att göra med en uppsättning versionssträngar. Föreställ dig en C#-applikation som skickar in en uppsättning av dessa strängar (t.ex. som har samlats in från en uppsättning användare) och vi måste matcha versionerna mot en tabell (säg, som indikerar de tjänsteutgåvor som är tillämpliga på en specifik uppsättning av versioner). Uppenbarligen skulle en riktig applikation ha fler kolumner än så här, men bara för att skapa lite volym och fortfarande hålla tabellen mager (jag använder också NVARCHAR genomgående eftersom det är vad CLR-delningsfunktionen tar och jag vill eliminera all tvetydighet på grund av 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 när data är på plats är nästa sak vi behöver göra att skapa en användardefinierad tabelltyp som kan hålla en uppsättning strängar. Den ursprungliga tabelltypen för att hålla denna sträng är ganska enkel:

CREATE TYPE dbo.VersionStringsTVP AS TABLE (VersionString NVARCHAR(5));

Sedan behöver vi ett par lagrade procedurer för att acceptera listorna från C#. För enkelhetens skull, återigen, tar vi bara en räkning så att vi kan vara säkra på att utföra en fullständig skanning, och vi ignorerar räkningen 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

Observera att en TVP som skickas in i en lagrad procedur måste markeras som READONLY – det finns för närvarande inget sätt att utföra DML på data som du skulle göra för en tabellvariabel eller temptabell. Men Erland har skickat in en mycket populär begäran om att Microsoft ska göra dessa parametrar mer flexibla (och massor av djupare insikter bakom hans argument här).

Det fina här är att SQL Server inte längre behöver ta itu med att dela en sträng alls – varken i T-SQL eller överlämna den till CLR – eftersom den redan är i en fast struktur där den utmärker sig.

Därefter en C#-konsolapplikation som gör följande:

  • Accepterar ett tal som ett argument för att indikera hur många strängelement som ska definieras
  • Byger en CSV-sträng av dessa element, med StringBuilder, för att skickas till den lagrade CLR-proceduren
  • Skapar en datatabell med samma element för att skickas till den lagrade TVP-proceduren
  • Testar även overheaden för att konvertera en CSV-sträng till en datatabell och vice versa innan de lämpliga lagrade procedurerna anropas

Koden för C#-appen finns i slutet av artikeln. Jag kan stava C#, men jag är inte på något sätt en guru; Jag är säker på att det finns ineffektivitet du kan upptäcka där som kan få koden att prestera lite bättre. Men alla sådana ändringar bör påverka hela uppsättningen av tester på liknande sätt.

Jag körde applikationen 10 gånger med 100, 1 000, 2 500 och 5 000 element. Resultaten var följande (detta visar genomsnittlig varaktighet, i sekunder, över de 10 testerna):

Prestandan åt sidan...

Förutom den tydliga prestandaskillnaden har TVP:er ytterligare en fördel – tabelltyper är mycket enklare att distribuera än CLR-enheter, särskilt i miljöer där CLR har förbjudits av andra skäl. Jag hoppas att hinder för CLR gradvis försvinner, och nya verktyg gör driftsättning och underhåll mindre smärtsamt, men jag tvivlar på att den enkla initiala implementeringen för CLR någonsin kommer att bli lättare än inhemska tillvägagångssätt.

Å andra sidan, utöver den skrivskyddade begränsningen, är tabelltyper som aliastyper genom att de är svåra att ändra i efterhand. Om du vill ändra storleken på en kolumn eller lägga till en kolumn finns det inget ALTER TYPE-kommando, och för att SLAPPA typen och återskapa den måste du först ta bort referenser till typen från alla procedurer som använder den . Så till exempel i ovanstående fall om vi behövde öka VersionString-kolumnen till NVARCHAR(32), måste vi skapa en dummytyp och ändra den lagrade proceduren (och alla andra förfaranden som använder 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, släpp proceduren, släpp typen, återskapa typen och återskapa proceduren.)

Slutsats

TVP-metoden överträffade konsekvent CLR-delningsmetoden och med en större procentandel när antalet element ökade. Även att lägga till omkostnader för att konvertera en befintlig CSV-sträng till en DataTable gav mycket bättre resultat från slut till slut. Så jag hoppas att jag, om jag inte redan hade övertygat dig om att överge dina T-SQL-strängdelningstekniker till förmån för CLR, har uppmanat dig att ge tabellvärdade parametrar ett försök. Det borde vara lätt att testa även om du för närvarande inte använder en DataTable (eller motsvarande).

C#-koden som används för dessa tester

Som jag sa, jag är ingen C#-guru, så det finns förmodligen många naiva saker jag gör här, men metodiken borde vara ganska tydlig.

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"));
    }
  }
}

  1. SQLite CHECK-begränsningar

  2. Hur man returnerar utdata från lagrad procedur till en variabel i sql-server

  3. Hur använder du WITH-satsen i MySQL?

  4. Hur man får det korta månadsnamnet från ett datum i MariaDB