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

Strings aufteilen:Jetzt mit weniger T-SQL

Rund um das Thema Stringsplitting entwickeln sich immer wieder interessante Diskussionen. In zwei früheren Blogbeiträgen, „Split strings the right way – or the next best way“ und „Splitting Strings :A Follow-Up“, habe ich hoffentlich demonstriert, dass die Suche nach der „leistungsstärksten“ T-SQL-Split-Funktion erfolglos ist . Wenn eine Aufteilung tatsächlich erforderlich ist, gewinnt CLR immer, und die nächstbeste Option kann je nach der tatsächlich anstehenden Aufgabe variieren. Aber in diesen Beiträgen habe ich angedeutet, dass eine Aufteilung auf der Datenbankseite möglicherweise gar nicht nötig ist.

SQL Server 2008 führte Tabellenwertparameter ein, eine Möglichkeit, eine „Tabelle“ von einer Anwendung an eine gespeicherte Prozedur zu übergeben, ohne eine Zeichenfolge erstellen und parsen, in XML serialisieren oder mit einer dieser Aufteilungsmethoden umgehen zu müssen. Also dachte ich, ich würde überprüfen, wie diese Methode im Vergleich zum Gewinner unserer vorherigen Tests abschneidet – da es eine praktikable Option sein könnte, ob Sie CLR verwenden können oder nicht. (Für die ultimative Bibel über TVPs lesen Sie bitte den umfassenden Artikel von Erland Sommarskog, MVP von SQL Server.)

Die Tests

Für diesen Test werde ich so tun, als hätten wir es mit einer Reihe von Versionszeichenfolgen zu tun. Stellen Sie sich eine C#-Anwendung vor, die einen Satz dieser Zeichenfolgen übergibt (z. B. die von einer Gruppe von Benutzern gesammelt wurden), und wir müssen die Versionen mit einer Tabelle abgleichen (z. B. die die Dienstversionen angibt, die für eine bestimmte Gruppe gelten von Versionen). Natürlich hätte eine echte Anwendung mehr Spalten als diese, aber nur um etwas Volumen zu schaffen und die Tabelle trotzdem dünn zu halten (ich verwende auch durchgehend NVARCHAR, weil die CLR-Split-Funktion dies übernimmt und ich jede Mehrdeutigkeit aufgrund der impliziten Konvertierung beseitigen möchte). :

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;

Nachdem die Daten vorhanden sind, müssen wir als Nächstes einen benutzerdefinierten Tabellentyp erstellen, der eine Reihe von Zeichenfolgen enthalten kann. Der anfängliche Tabellentyp für diesen String ist ziemlich einfach:

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

Dann brauchen wir ein paar gespeicherte Prozeduren, um die Listen von C# zu akzeptieren. Der Einfachheit halber nehmen wir wieder nur eine Zählung vor, damit wir sicher sein können, einen vollständigen Scan durchzuführen, und wir ignorieren die Zählung in der Anwendung:

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

Beachten Sie, dass ein TVP, das an eine gespeicherte Prozedur übergeben wird, als READONLY markiert werden muss – es gibt derzeit keine Möglichkeit, DML für die Daten auszuführen, wie Sie es für eine Tabellenvariable oder eine temporäre Tabelle tun würden. Erland hat jedoch eine sehr populäre Anfrage an Microsoft gestellt, diese Parameter flexibler zu machen (und viel tiefere Einblicke hinter seine Argumentation hier).

Das Schöne dabei ist, dass sich SQL Server überhaupt nicht mehr mit dem Aufteilen eines Strings befassen muss – weder in T-SQL noch bei der Übergabe an CLR – da er sich bereits in einer Satzstruktur befindet, in der er sich auszeichnet.

Als nächstes eine C#-Konsolenanwendung, die Folgendes tut:

  • Akzeptiert eine Zahl als Argument, um anzugeben, wie viele String-Elemente definiert werden sollen
  • Erstellt mithilfe von StringBuilder eine CSV-Zeichenfolge dieser Elemente, um sie an die gespeicherte CLR-Prozedur zu übergeben
  • Erstellt eine DataTable mit denselben Elementen, die an die gespeicherte TVP-Prozedur übergeben werden
  • Testet auch den Overhead beim Konvertieren einer CSV-Zeichenfolge in eine DataTable und umgekehrt, bevor die entsprechenden gespeicherten Prozeduren aufgerufen werden

Den Code für die C#-App finden Sie am Ende des Artikels. Ich kann C# buchstabieren, aber ich bin keineswegs ein Guru; Ich bin mir sicher, dass Sie dort Ineffizienzen erkennen können, die den Code möglicherweise etwas leistungsfähiger machen. Solche Änderungen sollten sich jedoch auf ähnliche Weise auf den gesamten Testsatz auswirken.

Ich habe die Anwendung 10 Mal mit 100, 1.000, 2.500 und 5.000 Elementen ausgeführt. Die Ergebnisse waren wie folgt (dies zeigt die durchschnittliche Dauer in Sekunden über die 10 Tests):

Leistung beiseite…

Neben dem deutlichen Leistungsunterschied haben TVPs noch einen weiteren Vorteil – Tabellentypen sind viel einfacher zu implementieren als CLR-Assemblys, insbesondere in Umgebungen, in denen CLR aus anderen Gründen verboten wurde. Ich hoffe, dass die Hindernisse für CLR allmählich verschwinden und neue Tools die Bereitstellung und Wartung weniger schmerzhaft machen, aber ich bezweifle, dass die anfängliche Bereitstellung für CLR jemals einfacher sein wird als bei nativen Ansätzen.

Auf der anderen Seite ähneln Tabellentypen zusätzlich zu der schreibgeschützten Einschränkung Aliastypen, da sie im Nachhinein schwer zu ändern sind. Wenn Sie die Größe einer Spalte ändern oder eine Spalte hinzufügen möchten, gibt es keinen ALTER TYPE-Befehl, und um den Typ zu löschen und neu zu erstellen, müssen Sie zuerst Verweise auf den Typ aus allen Prozeduren entfernen, die ihn verwenden . Wenn wir beispielsweise im obigen Fall die VersionString-Spalte auf NVARCHAR(32) erhöhen müssten, müssten wir einen Dummy-Typ erstellen und die gespeicherte Prozedur (und jede andere Prozedur, die ihn verwendet) ändern:

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

(Oder löschen Sie alternativ die Prozedur, löschen Sie den Typ, erstellen Sie den Typ neu und erstellen Sie die Prozedur neu.)

Schlussfolgerung

Die TVP-Methode übertraf die CLR-Splitting-Methode durchweg und um einen größeren Prozentsatz, wenn die Anzahl der Elemente zunahm. Selbst das Hinzufügen des Overheads für die Konvertierung einer vorhandenen CSV-Zeichenfolge in eine DataTable führte zu einer viel besseren End-to-End-Leistung. Ich hoffe also, dass ich Sie, wenn ich Sie noch nicht davon überzeugt hatte, Ihre T-SQL-String-Splitting-Techniken zugunsten von CLR aufzugeben, dazu aufgefordert habe, Tabellenwertparametern eine Chance zu geben. Es sollte einfach zu testen sein, auch wenn Sie derzeit keine DataTable (oder etwas Äquivalentes) verwenden.

Der für diese Tests verwendete C#-Code

Wie gesagt, ich bin kein C#-Guru, also gibt es wahrscheinlich viele naive Dinge, die ich hier mache, aber die Methodik sollte ziemlich klar sein.

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