Database
 sql >> Database >  >> RDS >> Database

Dividere le stringhe:ora con meno T-SQL

Alcune discussioni interessanti si evolvono sempre intorno all'argomento della divisione delle stringhe. In due precedenti post del blog, "Dividi le stringhe nel modo giusto - o nel modo migliore successivo" e "Splitting Strings:A Follow-Up", spero di aver dimostrato che inseguire la funzione di divisione T-SQL "con le migliori prestazioni" è inutile . Quando la divisione è effettivamente necessaria, CLR vince sempre e l'opzione migliore successiva può variare a seconda dell'attività effettiva da svolgere. Ma in quei post ho accennato al fatto che la divisione sul lato database potrebbe non essere necessaria in primo luogo.

SQL Server 2008 ha introdotto parametri con valori di tabella, un modo per passare una "tabella" da un'applicazione a una stored procedure senza dover compilare e analizzare una stringa, serializzare in XML o gestire una di queste metodologie di suddivisione. Quindi ho pensato di controllare come questo metodo si confronta con il vincitore dei nostri test precedenti, poiché potrebbe essere un'opzione praticabile, indipendentemente dal fatto che tu possa utilizzare CLR o meno. (Per la Bibbia definitiva sui TVP, vedere l'articolo completo del collega MVP di SQL Server Erland Sommarskog.)

Le prove

Per questo test farò finta di avere a che fare con un insieme di stringhe di versione. Immagina un'applicazione C# che passa in un set di queste stringhe (ad esempio, che sono state raccolte da un set di utenti) e dobbiamo confrontare le versioni con una tabella (ad esempio, che indica le versioni del servizio applicabili a un set specifico di versioni). Ovviamente una vera applicazione avrebbe più colonne di questa, ma solo per creare un po' di volume e mantenere la tabella magra (uso anche NVARCHAR dappertutto perché è quello che prende la funzione di divisione CLR e voglio eliminare qualsiasi ambiguità dovuta alla conversione implicita) :

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;

Ora che i dati sono a posto, la prossima cosa che dobbiamo fare è creare un tipo di tabella definito dall'utente che può contenere un set di stringhe. Il tipo di tabella iniziale per contenere questa stringa è piuttosto semplice:

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

Quindi abbiamo bisogno di un paio di stored procedure per accettare gli elenchi da C#. Per semplicità, ancora una volta, faremo un conteggio in modo da poter essere sicuri di eseguire una scansione completa e ignoreremo il conteggio nell'applicazione:

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

Si noti che un TVP passato a una procedura memorizzata deve essere contrassegnato come READONLY:al momento non è possibile eseguire DML sui dati come si farebbe per una variabile di tabella o una tabella temporanea. Tuttavia, Erland ha presentato una richiesta molto popolare affinché Microsoft renda questi parametri più flessibili (e molte informazioni più approfondite dietro la sua argomentazione qui).

Il bello qui è che SQL Server non ha più a che fare con la divisione di una stringa, né in T-SQL né nel passarla a CLR, poiché è già in una struttura prestabilita in cui eccelle.

Successivamente, un'applicazione console C# che esegue le seguenti operazioni:

  • Accetta un numero come argomento per indicare quanti elementi stringa devono essere definiti
  • Costruisce una stringa CSV di quegli elementi, utilizzando StringBuilder, da passare alla stored procedure CLR
  • Costruisce una DataTable con gli stessi elementi da passare alla stored procedure TVP
  • Verifica anche l'overhead della conversione di una stringa CSV in una DataTable e viceversa prima di chiamare le procedure memorizzate appropriate

Il codice per l'app C# si trova alla fine dell'articolo. Posso scrivere C#, ma non sono affatto un guru; Sono sicuro che ci sono inefficienze che puoi individuare lì che potrebbero migliorare un po' le prestazioni del codice. Ma qualsiasi modifica di questo tipo dovrebbe influenzare l'intera serie di test in modo simile.

Ho eseguito l'applicazione 10 volte utilizzando 100, 1.000, 2.500 e 5.000 elementi. I risultati sono stati i seguenti (questo mostra la durata media, in secondi, nei 10 test):

Prestazioni a parte...

Oltre alla chiara differenza di prestazioni, i TVP hanno un altro vantaggio:i tipi di tabella sono molto più semplici da implementare rispetto agli assiemi CLR, specialmente in ambienti in cui CLR è stato vietato per altri motivi. Spero che le barriere al CLR stiano gradualmente scomparendo e che i nuovi strumenti stiano rendendo l'implementazione e la manutenzione meno dolorose, ma dubito che la facilità dell'implementazione iniziale per CLR sarà mai più facile degli approcci nativi.

D'altra parte, oltre alla limitazione di sola lettura, i tipi di tabella sono come i tipi di alias in quanto sono difficili da modificare a posteriori. Se vuoi modificare la dimensione di una colonna o aggiungere una colonna, non c'è alcun comando ALTER TYPE, e per DROP il tipo e ricrearlo, devi prima rimuovere i riferimenti al tipo da tutte le procedure che lo stanno utilizzando . Quindi, ad esempio nel caso precedente, se avessimo bisogno di aumentare la colonna VersionString a NVARCHAR(32), dovremmo creare un tipo fittizio e modificare la procedura memorizzata (e qualsiasi altra procedura che la sta utilizzando):

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

(O in alternativa, elimina la procedura, elimina il tipo, ricrea il tipo e ricrea la procedura.)

Conclusione

Il metodo TVP ha costantemente sovraperformato il metodo di suddivisione CLR e di una percentuale maggiore all'aumentare del numero di elementi. Anche l'aggiunta dell'overhead della conversione di una stringa CSV esistente in un DataTable ha prodotto prestazioni end-to-end molto migliori. Quindi spero che, se non ti avessi già convinto ad abbandonare le tue tecniche di divisione delle stringhe T-SQL a favore di CLR, ti ho esortato a provare i parametri con valori di tabella. Dovrebbe essere facile da testare anche se al momento non stai utilizzando una DataTable (o un equivalente).

Il codice C# utilizzato per questi test

Come ho detto, non sono un guru del C#, quindi probabilmente ci sono molte cose ingenue che sto facendo qui, ma la metodologia dovrebbe essere abbastanza chiara.

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