sql >> Database >  >> RDS >> Database

Strings splitsen:nu met minder T-SQL

Er ontstaan ​​altijd enkele interessante discussies rond het onderwerp van het splitsen van snaren. In twee eerdere blogposts, "Snaren op de juiste manier splitsen - of de volgende beste manier" en "Splitting Strings:A Follow-Up", hoop ik dat ik heb aangetoond dat het najagen van de "best presterende" T-SQL-splitfunctie vruchteloos is . Wanneer splitsen echt nodig is, wint CLR altijd, en de op één na beste optie kan variëren, afhankelijk van de daadwerkelijke taak. Maar in die berichten liet ik doorschemeren dat het splitsen aan de databasekant in de eerste plaats misschien niet nodig is.

SQL Server 2008 introduceerde parameters met tabelwaarde, een manier om een ​​"tabel" door te geven van een toepassing naar een opgeslagen procedure zonder een string te bouwen en te ontleden, te serialiseren naar XML of om te gaan met een van deze splitsingsmethodologieën. Dus ik dacht dat ik zou controleren hoe deze methode zich verhoudt tot de winnaar van onze vorige tests - omdat het een haalbare optie kan zijn, of je CLR nu kunt gebruiken of niet. (Zie het uitgebreide artikel van collega SQL Server MVP Erland Sommarskog voor de ultieme bijbel over TVP's.)

De testen

Voor deze test ga ik doen alsof we te maken hebben met een set versiestrings. Stel je een C#-toepassing voor die een set van deze tekenreeksen doorgeeft (bijvoorbeeld die zijn verzameld van een reeks gebruikers) en we moeten de versies vergelijken met een tabel (bijvoorbeeld die de servicereleases aangeeft die van toepassing zijn op een specifieke set van versies). Het is duidelijk dat een echte applicatie meer kolommen zou hebben dan dit, maar alleen om wat volume te creëren en toch de tabel mager te houden (ik gebruik ook overal NVARCHAR omdat dat is wat de CLR-splitsingsfunctie nodig heeft en ik wil elke dubbelzinnigheid elimineren als gevolg van impliciete conversie) :

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 de gegevens op hun plaats zijn, is het volgende dat we moeten doen een door de gebruiker gedefinieerd tabeltype maken dat een reeks tekenreeksen kan bevatten. Het initiële tabeltype om deze string vast te houden is vrij eenvoudig:

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

Dan hebben we een aantal opgeslagen procedures nodig om de lijsten uit C# te accepteren. Nogmaals, voor de eenvoud nemen we gewoon een telling zodat we er zeker van kunnen zijn dat we een volledige scan uitvoeren, en we negeren de telling in de toepassing:

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

Merk op dat een TVP die wordt doorgegeven aan een opgeslagen procedure, gemarkeerd moet worden als READONLY - er is momenteel geen manier om DML op de gegevens uit te voeren zoals u zou doen voor een tabelvariabele of tijdelijke tabel. Erland heeft echter een zeer populair verzoek ingediend aan Microsoft om deze parameters flexibeler te maken (en veel dieper inzicht achter zijn argument hier).

Het mooie hiervan is dat SQL Server helemaal niet meer te maken heeft met het splitsen van een string - noch in T-SQL, noch bij het overdragen aan CLR - omdat het al in een vaste structuur zit waarin het uitblinkt.

Vervolgens een C#-consoletoepassing die het volgende doet:

  • Accepteert een getal als argument om aan te geven hoeveel tekenreekselementen moeten worden gedefinieerd
  • Bouwt een CSV-tekenreeks van die elementen, met behulp van StringBuilder, om door te geven aan de opgeslagen CLR-procedure
  • Bouwt een gegevenstabel met dezelfde elementen om door te geven aan de opgeslagen procedure voor TVP
  • Test ook de overhead van het converteren van een CSV-tekenreeks naar een gegevenstabel en vice versa voordat de juiste opgeslagen procedures worden aangeroepen

De code voor de C#-app vindt u aan het einde van het artikel. Ik kan C# spellen, maar ik ben geenszins een goeroe; Ik weet zeker dat er inefficiënties zijn die je daar kunt ontdekken, waardoor de code misschien wat beter presteert. Maar dergelijke wijzigingen zouden de hele reeks tests op een vergelijkbare manier moeten beïnvloeden.

Ik heb de applicatie 10 keer uitgevoerd met 100, 1.000, 2.500 en 5.000 elementen. De resultaten waren als volgt (dit is de gemiddelde duur, in seconden, over de 10 tests):

Prestaties terzijde…

Naast het duidelijke prestatieverschil hebben TVP's nog een ander voordeel:tafeltypes zijn veel eenvoudiger te implementeren dan CLR-assemblages, vooral in omgevingen waar CLR om andere redenen verboden is. Ik hoop dat de belemmeringen voor CLR geleidelijk verdwijnen en dat nieuwe tools de implementatie en het onderhoud minder pijnlijk maken, maar ik betwijfel of het gemak van de initiële implementatie voor CLR ooit gemakkelijker zal zijn dan de native benaderingen.

Aan de andere kant, naast de alleen-lezen beperking, zijn tabeltypen als aliastypen in die zin dat ze achteraf moeilijk te wijzigen zijn. Als u de grootte van een kolom wilt wijzigen of een kolom wilt toevoegen, is er geen ALTER TYPE-opdracht, en om het type te DROPPEN en opnieuw te maken, moet u eerst verwijzingen naar het type verwijderen uit alle procedures die het gebruiken . Dus als we bijvoorbeeld in het bovenstaande geval de kolom VersionString moeten vergroten naar NVARCHAR(32), moeten we een dummy-type maken en de opgeslagen procedure wijzigen (en elke andere procedure die deze gebruikt):

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

(Of laat de procedure vallen, laat het type vallen, maak het type opnieuw en maak de procedure opnieuw.)

Conclusie

De TVP-methode presteerde consequent beter dan de CLR-splitsingsmethode, en met een groter percentage naarmate het aantal elementen toenam. Zelfs het toevoegen van de overhead van het converteren van een bestaande CSV-string naar een DataTable leverde veel betere end-to-end-prestaties op. Dus ik hoop dat, als ik je niet al had overtuigd om je T-SQL-tekenreekssplitsingstechnieken op te geven ten gunste van CLR, ik je dringend heb verzocht om tabelwaardeparameters een kans te geven. Het zou gemakkelijk moeten zijn om uit te testen, zelfs als u momenteel geen DataTable (of een equivalent) gebruikt.

De C#-code die voor deze tests wordt gebruikt

Zoals ik al zei, ik ben geen C#-goeroe, dus er zijn waarschijnlijk genoeg naïeve dingen die ik hier doe, maar de methodologie moet vrij duidelijk zijn.

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. Oracle-gegevenstype:moet ik VARCHAR2 of CHAR gebruiken?

  2. Een tabel bijwerken in Oracle als een veldwaarde Null is en bepalen of de update succesvol is

  3. Hoe kopieer ik een set gegevens diep en verander ik FK-referenties om naar alle kopieën te verwijzen?

  4. Hoe te bestellen op datum in SQLite