いくつかの興味深い議論は、常に文字列の分割のトピックを中心に展開されます。以前の2つのブログ投稿、「文字列を正しい方法で分割する-または次善の方法」と「文字列を分割する:フォローアップ」で、「最高のパフォーマンス」のT-SQL分割関数を追跡しても効果がないことを示したことを願っています。 。分割が実際に必要な場合、CLRが常に優先され、次に最適なオプションは、実際のタスクによって異なります。しかし、それらの投稿で、データベース側での分割はそもそも必要ないかもしれないとほのめかしました。
SQL Server 2008では、テーブル値パラメーターが導入されました。これは、文字列の作成と解析、XMLへのシリアル化、またはこの分割方法のいずれかを処理することなく、アプリケーションからストアドプロシージャに「テーブル」を渡す方法です。それで、この方法が以前のテストの勝者とどのように比較されるかを確認しようと思いました。CLRを使用できるかどうかにかかわらず、実行可能なオプションである可能性があるためです。 (TVPの究極の聖書については、SQL ServerMVPの仲間であるErlandSommarskogの包括的な記事を参照してください。)
テスト
このテストでは、バージョン文字列のセットを扱っているふりをします。これらの文字列のセット(たとえば、ユーザーのセットから収集されたもの)を渡すC#アプリケーションを想像してください。バージョンをテーブル(たとえば、特定のセットに適用可能なサービスリリースを示す)と照合する必要があります。バージョンの)。明らかに、実際のアプリケーションにはこれよりも多くの列がありますが、ボリュームを作成し、テーブルをスキニーに保つためです(CLR分割関数が取るものであり、暗黙的な変換によるあいまいさを排除したいので、全体でNVARCHARも使用します) :
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;
データが配置されたので、次に行う必要があるのは、文字列のセットを保持できるユーザー定義のテーブルタイプを作成することです。この文字列を保持する最初のテーブルタイプは非常に単純です:
CREATE TYPE dbo.VersionStringsTVP AS TABLE (VersionString NVARCHAR(5));
次に、C#からリストを受け入れるために、いくつかのストアドプロシージャが必要です。簡単にするために、ここでも、完全なスキャンを確実に実行できるようにカウントを取得し、アプリケーションではカウントを無視します。
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 ストアドプロシージャに渡されるTVPは、読み取り専用としてマークする必要があることに注意してください。現在、テーブル変数または一時テーブルの場合のように、データに対してDMLを実行する方法はありません。ただし、Erlandは、Microsoftがこれらのパラメータをより柔軟にする(そしてここでの彼の議論の背後にある多くのより深い洞察)という非常に人気のある要求を提出しました。
ここでの利点は、SQL Serverが文字列の分割をまったく処理する必要がないことです。これは、T-SQLでも、CLRに渡す場合でも、すでに優れた構造になっているためです。
次に、次のことを行うC#コンソールアプリケーション:
- 定義する文字列要素の数を示す引数として数値を受け入れます
- StringBuilderを使用して、これらの要素のCSV文字列を作成し、CLRストアドプロシージャに渡します
- TVPストアドプロシージャに渡すのと同じ要素を使用してDataTableを構築します
- 適切なストアドプロシージャを呼び出す前に、CSV文字列をDataTableに、またはその逆に変換するオーバーヘッドもテストします。
C#アプリのコードは、記事の最後にあります。私はC#を綴ることができますが、決して教祖ではありません。コードのパフォーマンスを少し向上させる可能性のある非効率性がそこにあると確信しています。ただし、そのような変更は、同様の方法で一連のテスト全体に影響を与えるはずです。
100、1,000、2,500、5,000の要素を使用してアプリケーションを10回実行しました。結果は次のとおりです(これは、10回のテストの平均継続時間を秒単位で示しています):
パフォーマンスはさておき…
明らかなパフォーマンスの違いに加えて、TVPには別の利点があります。テーブルタイプは、特に他の理由でCLRが禁止されている環境では、CLRアセンブリよりも展開がはるかに簡単です。 CLRへの障壁が徐々になくなり、新しいツールによって展開と保守の負担が軽減されることを期待していますが、CLRの初期展開の容易さがネイティブアプローチよりも簡単になるとは思えません。
一方、読み取り専用の制限に加えて、テーブルタイプは、事後に変更するのが難しいという点でエイリアスタイプに似ています。列のサイズを変更したり、列を追加したりする場合、ALTER TYPEコマンドはありません。タイプを削除して再作成するには、まず、タイプを使用しているすべてのプロシージャからタイプへの参照を削除する必要があります。 。したがって、たとえば上記の場合、VersionString列をNVARCHAR(32)に増やす必要がある場合は、ダミー型を作成し、ストアドプロシージャ(およびそれを使用する他のプロシージャ)を変更する必要があります。
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
(または、プロシージャを削除し、タイプを削除し、タイプを再作成して、プロシージャを再作成します。)
結論
TVP方式は、一貫してCLR分割方式を上回り、要素数が増えるにつれて高い割合でパフォーマンスを向上させました。既存のCSV文字列をDataTableに変換するオーバーヘッドを追加しても、エンドツーエンドのパフォーマンスが大幅に向上しました。したがって、CLRを優先してT-SQL文字列分割手法を放棄することをまだ確信していない場合は、テーブル値パラメーターを試してみることをお勧めします。現在DataTable(または同等のもの)を使用していない場合でも、簡単にテストできるはずです。
これらのテストに使用されるC#コード
私が言ったように、私はC#の第一人者ではないので、ここで行っているナイーブなことはおそらくたくさんありますが、方法論は非常に明確である必要があります。
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"));
}
}
}