大量のレコード(1000件)を取得して操作するプロセスがあり、完了したら、多数のレコードを処理済みとしてマークする必要があります。 IDの大きなリストでこれを示すことができます。 「ループ内の更新」パターンを回避しようとしているので、このIDのバッグをMS SQL Server 2008のストアドプロシージャに送信するより効率的な方法を見つけたいと思います。
提案#1-テーブル値パラメーター。 IDフィールドだけでテーブルタイプを定義し、IDでいっぱいのテーブルを送信して更新できます。
プロポーザル#2-プロシージャ本体にOPENXML()を含むXMLパラメータ(varchar)。
提案#3-リストの解析。扱いにくく、エラーが発生しやすいように見えるので、できれば回避します。
これらの好み、または私が見逃したアイデアはありますか?
この問題に関する史上最高の記事は、Erland Sommarskogによるものです。
彼はすべてのオプションをカバーし、かなりよく説明しています。
答えが短かすぎて申し訳ありませんが、Erlandの配列に関する記事は、Joe Celkoによる木やその他のSQLの扱いに関する本のようなものです:)
StackOverflow については、多くのアプローチをカバーするすばらしい議論があります。 SQL Server 2008+で私が好むのは、-- テーブル値パラメーターを使用することです。これは基本的に、SQL Serverの問題に対する解決策です。値のリストをストアドプロシージャに渡します。
このアプローチの利点は次のとおりです:
ただし、注意してください:ADO.NETまたはODBCを介してTVPを使用するストアドプロシージャを呼び出し、 SQL Serverプロファイラを使用してアクティビティを見ると、SQL ServerがTVPをロードするためのいくつかのINSERT
ステートメントを受信していることがわかります TVPの各行に1つ の後に、これは 設計による です。このINSERT
sのバッチは、プロシージャが呼び出されるたびにコンパイルする必要があり、小さなオーバーヘッドを構成します。ただし、このオーバーヘッドがあっても、TVPは依然として- 吹き飛ばす ほとんどのユースケースのパフォーマンスと使いやすさに関する他のアプローチ。
さらに詳しく知りたい場合は、Erland Sommarskogが 完全な細い を使用して、テーブル値パラメーターの動作といくつかの例を示します。
ここに私が作り出した別の例があります:
CREATE TYPE id_list AS TABLE (
id int NOT NULL PRIMARY KEY
);
GO
CREATE PROCEDURE [dbo].[tvp_test] (
@param1 INT
, @customer_list id_list READONLY
)
AS
BEGIN
SELECT @param1 AS param1;
-- join, filter, do whatever you want with this table
-- (other than modify it)
SELECT *
FROM @customer_list;
END;
GO
DECLARE @customer_list id_list;
INSERT INTO @customer_list (
id
)
VALUES (1), (2), (3), (4), (5), (6), (7);
EXECUTE [dbo].[tvp_test]
@param1 = 5
, @customer_list = @customer_list
;
GO
DROP PROCEDURE dbo.tvp_test;
DROP TYPE id_list;
GO
主題全体については、Erland Sommarskogによるthedefinitive article: "Arrays and List in SQL Server" で説明されています。選択するバージョンを選択してください。
要約、preSQL Server 2008の場合、TVPが残りのSQLPに勝る
とにかく、この記事を読んで、他の手法や考え方を確認してください。
編集:hugeリストの別の場所への回答が遅い: 配列パラメーターをストアドプロシージャに渡す
私はこのパーティーに遅れていることを知っていますが、過去にそのような問題があり、最大10万のビギント数を送信しなければならず、いくつかのベンチマークを行いました。結局、それらをイメージとしてバイナリ形式で送信することになりました。これは、100,000までの数で他のすべてよりも高速でした。
これが私の古い(SQL Server 2005)コードです。
SELECT Number * 8 + 1 AS StartFrom ,
Number * 8 + 8 AS MaxLen
INTO dbo.ParsingNumbers
FROM dbo.Numbers
GO
CREATE FUNCTION dbo.ParseImageIntoBIGINTs ( @BIGINTs IMAGE )
RETURNS TABLE
AS RETURN
( SELECT CAST(SUBSTRING(@BIGINTs, StartFrom, 8) AS BIGINT) Num
FROM dbo.ParsingNumbers
WHERE MaxLen <= DATALENGTH(@BIGINTs)
)
GO
次のコードは、整数をバイナリBLOBにパックしています。ここでバイトの順序を逆にしています:
static byte[] UlongsToBytes(ulong[] ulongs)
{
int ifrom = ulongs.GetLowerBound(0);
int ito = ulongs.GetUpperBound(0);
int l = (ito - ifrom + 1)*8;
byte[] ret = new byte[l];
int retind = 0;
for(int i=ifrom; i<=ito; i++)
{
ulong v = ulongs[i];
ret[retind++] = (byte) (v >> 0x38);
ret[retind++] = (byte) (v >> 0x30);
ret[retind++] = (byte) (v >> 40);
ret[retind++] = (byte) (v >> 0x20);
ret[retind++] = (byte) (v >> 0x18);
ret[retind++] = (byte) (v >> 0x10);
ret[retind++] = (byte) (v >> 8);
ret[retind++] = (byte) v;
}
return ret;
}
SOに言及するか、ここで答えるのは難しいのですが、これはほとんどプログラミングの質問です。しかし、すでに使用している解決策があるので...投稿しますそれ ;)
これが機能する方法は、コンマ区切りの文字列(単純な分割、CSVスタイルの分割を行わない)をvarchar(4000)としてストアドプロシージャにフィードし、そのリストをこの関数にフィードして、便利なテーブルを戻すことです。 varcharのみのテーブル。
これにより、処理したいIDのみの値を送信することができ、その時点で単純な結合を行うことができます。
あるいは、CLR DataTableを使用して何かを実行し、それをフィードすることもできますが、これはサポートするのに少しオーバーヘッドがあり、誰もがCSVリストを理解しています。
USE [Database]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER FUNCTION [dbo].[splitListToTable] (@list nvarchar(MAX), @delimiter nchar(1) = N',')
RETURNS @tbl TABLE (value varchar(4000) NOT NULL) AS
/*
http://www.sommarskog.se/arrays-in-sql.html
This guy is apparently THE guy in SQL arrays and lists
Need an easy non-dynamic way to split a list of strings on input for comparisons
Usage like thus:
DECLARE @sqlParam VARCHAR(MAX)
SET @sqlParam = 'a,b,c'
SELECT * FROM (
select 'a' as col1, '1' as col2 UNION
select 'a' as col1, '2' as col2 UNION
select 'b' as col1, '3' as col2 UNION
select 'b' as col1, '4' as col2 UNION
select 'c' as col1, '5' as col2 UNION
select 'c' as col1, '6' as col2 ) x
WHERE EXISTS( SELECT value FROM splitListToTable(@sqlParam,',') WHERE x.col1 = value )
*/
BEGIN
DECLARE @endpos int,
@startpos int,
@textpos int,
@chunklen smallint,
@tmpstr nvarchar(4000),
@leftover nvarchar(4000),
@tmpval nvarchar(4000)
SET @textpos = 1
SET @leftover = ''
WHILE @textpos <= datalength(@list) / 2
BEGIN
SET @chunklen = 4000 - datalength(@leftover) / 2
SET @tmpstr = @leftover + substring(@list, @textpos, @chunklen)
SET @textpos = @textpos + @chunklen
SET @startpos = 0
SET @endpos = charindex(@delimiter, @tmpstr)
WHILE @endpos > 0
BEGIN
SET @tmpval = ltrim(rtrim(substring(@tmpstr, @startpos + 1,
@endpos - @startpos - 1)))
INSERT @tbl (value) VALUES(@tmpval)
SET @startpos = @endpos
SET @endpos = charindex(@delimiter, @tmpstr, @startpos + 1)
END
SET @leftover = right(@tmpstr, datalength(@tmpstr) / 2 - @startpos)
END
INSERT @tbl(value) VALUES (ltrim(rtrim(@leftover)))
RETURN
END
さまざまなSQL Serverストアドプロシージャで処理するために、アプリケーションから送信された数千行と10000行のセットを定期的に受け取ります。
パフォーマンスの要求を満たすにはTVPを使用しますが、デフォルトの処理モードでのパフォーマンスの問題を克服するには、dbDataReaderの独自の抽象を実装する必要があります。方法と理由については、このリクエストの対象外であるため、説明しません。
10,000を超える「行」でパフォーマンスを維持するXML実装が見つからなかったため、XML処理については考慮しませんでした。
リスト処理は、1次元および2次元の集計(数値)テーブル処理で処理できます。私たちはさまざまな分野でこれらをうまく使用してきましたが、適切に管理されたTVPは、数百以上の「行」がある場合にパフォーマンスが向上します。
SQL Server処理に関するすべての選択と同様に、使用モデルに基づいて選択を行う必要があります。
ようやくTableValuedParametersを実行する機会を得て、それらがうまく機能するので、現在のコードの一部のサンプルを使用して、それらの使用方法を示すロッタコード全体を貼り付けます(注:ADOを使用します)。 。ネット)
また注意:私はサービスのコードをいくつか書いており、他のクラスには事前定義されたコードビットがたくさんありますが、デバッグできるようにこれをコンソールアプリとして書いているので、これをすべてからリッピングしましたコンソールアプリ。私のコーディングスタイル(ハードコードされた接続文字列のような)は一種の「捨てるために構築する」ものだったので、失礼します_List<customObject>
_の使用方法を示し、それをテーブルとしてデータベースに簡単にプッシュし、ストアドプロシージャで使用できるようにしました。以下のC#およびTSQLコード:
_using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using a;
namespace a.EventAMI {
class Db {
private static SqlCommand SqlCommandFactory(string sprocName, SqlConnection con) { return new SqlCommand { CommandType = CommandType.StoredProcedure, CommandText = sprocName, CommandTimeout = 0, Connection = con }; }
public static void Update(List<Current> currents) {
const string CONSTR = @"just a hardwired connection string while I'm debugging";
SqlConnection con = new SqlConnection( CONSTR );
SqlCommand cmd = SqlCommandFactory( "sprocname", con );
cmd.Parameters.Add( "@CurrentTVP", SqlDbType.Structured ).Value = Converter.GetDataTableFromIEnumerable( currents, typeof( Current ) ); //my custom converter class
try {
using ( con ) {
con.Open();
cmd.ExecuteNonQuery();
}
} catch ( Exception ex ) {
ErrHandler.WriteXML( ex );
throw;
}
}
}
class Current {
public string Identifier { get; set; }
public string OffTime { get; set; }
public DateTime Off() {
return Convert.ToDateTime( OffTime );
}
private static SqlCommand SqlCommandFactory(string sprocName, SqlConnection con) { return new SqlCommand { CommandType = CommandType.StoredProcedure, CommandText = sprocName, CommandTimeout = 0, Connection = con }; }
public static List<Current> GetAll() {
List<Current> l = new List<Current>();
const string CONSTR = @"just a hardcoded connection string while I'm debugging";
SqlConnection con = new SqlConnection( CONSTR );
SqlCommand cmd = SqlCommandFactory( "sprocname", con );
try {
using ( con ) {
con.Open();
using ( SqlDataReader reader = cmd.ExecuteReader() ) {
while ( reader.Read() ) {
l.Add(
new Current {
Identifier = reader[0].ToString(),
OffTime = reader[1].ToString()
} );
}
}
}
} catch ( Exception ex ) {
ErrHandler.WriteXML( ex );
throw;
}
return l;
}
}
}
-------------------
the converter class
-------------------
using System;
using System.Collections;
using System.Data;
using System.Reflection;
namespace a {
public static class Converter {
public static DataTable GetDataTableFromIEnumerable(IEnumerable aIEnumerable) {
return GetDataTableFromIEnumerable( aIEnumerable, null );
}
public static DataTable GetDataTableFromIEnumerable(IEnumerable aIEnumerable, Type baseType) {
DataTable returnTable = new DataTable();
if ( aIEnumerable != null ) {
//Creates the table structure looping in the in the first element of the list
object baseObj = null;
Type objectType;
if ( baseType == null ) {
foreach ( object obj in aIEnumerable ) {
baseObj = obj;
break;
}
objectType = baseObj.GetType();
} else {
objectType = baseType;
}
PropertyInfo[] properties = objectType.GetProperties();
DataColumn col;
foreach ( PropertyInfo property in properties ) {
col = new DataColumn { ColumnName = property.Name };
if ( property.PropertyType == typeof( DateTime? ) ) {
col.DataType = typeof( DateTime );
} else if ( property.PropertyType == typeof( Int32? ) ) {
col.DataType = typeof( Int32 );
} else {
col.DataType = property.PropertyType;
}
returnTable.Columns.Add( col );
}
//Adds the rows to the table
foreach ( object objItem in aIEnumerable ) {
DataRow row = returnTable.NewRow();
foreach ( PropertyInfo property in properties ) {
Object value = property.GetValue( objItem, null );
if ( value != null )
row[property.Name] = value;
else
row[property.Name] = "";
}
returnTable.Rows.Add( row );
}
}
return returnTable;
}
}
}
USE [Database]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROC [dbo].[Event_Update]
@EventCurrentTVP Event_CurrentTVP READONLY
AS
/****************************************************************
author cbrand
date
descrip I'll ask you to forgive me the anonymization I've made here, but hope this helps
caller such and thus application
****************************************************************/
BEGIN TRAN Event_Update
DECLARE @DEBUG INT
SET @DEBUG = 0 /* test using @DEBUG <> 0 */
/*
Replace the list of outstanding entries that are still currently disconnected with the list from the file
This means remove all existing entries (faster to truncate and insert than to delete on a join and insert, yes?)
*/
TRUNCATE TABLE [database].[dbo].[Event_Current]
INSERT INTO [database].[dbo].[Event_Current]
([Identifier]
,[OffTime])
SELECT [Identifier]
,[OffTime]
FROM @EventCurrentTVP
IF (@@ERROR <> 0 OR @DEBUG <> 0)
BEGIN
ROLLBACK TRAN Event_Update
END
ELSE
BEGIN
COMMIT TRAN Event_Update
END
USE [Database]
GO
CREATE TYPE [dbo].[Event_CurrentTVP] AS TABLE(
[Identifier] [varchar](20) NULL,
[OffTime] [datetime] NULL
)
GO
_
また、(この質問に出くわすすべての読者に)提供するコードスタイルがある場合は、建設的な批判をしますが、建設的にしてください;)...本当に私が欲しい場合は、こちらのチャットルームで私を見つけてください。うまくいけば、このコードのチャンクを使用すると、_List<Current>
_をdbのテーブルとして定義し、アプリで_List<T>
_を定義しているので、どのように使用できるかを確認できます。
私は提案#1を使用するか、代替として、処理済みIDのみを保持するスクラッチテーブルを作成します。処理中にそのテーブルに挿入し、終了したら、次のようなprocを呼び出します。
BEGIN TRAN
UPDATE dt
SET processed = 1
FROM dataTable dt
JOIN processedIds pi ON pi.id = dt.id;
TRUNCATE TABLE processedIds
COMMIT TRAN
多くの挿入を実行しますが、それらは小さなテーブルへの挿入であるため、高速である必要があります。 ADO.netまたは使用しているデータアダプターを使用して、挿入をバッチ処理することもできます。
質問のタイトルには、アプリケーションからストアドプロシージャにデータを送信するタスクが含まれています。その部分は質問の本文では除外されていますが、私もこれに答えてみましょう。
タグで指定されたsql-server-2008のコンテキストには、E。Sommarskogによる別の優れた記事 SQL Server 2008の配列とリスト があります。ところで私はマリアンが彼の答えで言及した記事でそれを見つけました。
リンクを提供するだけでなく、コンテンツのリストを引用します。
そこで述べられている手法以外にも、バルクコピーやバルクインサートについては、一般的なケースで説明する価値があると感じる場合があります。
配列パラメーターをストアード・プロシージャーに渡す
MS SQL 2016最新バージョンの場合
MS SQL 2016では、複数の値を解析するための新しい関数SPLIT_STRING()が導入されています。
これで問題を簡単に解決できます。
MS SQL以前のバージョンの場合
古いバージョンを使用している場合は、次の手順に従ってください。
最初に1つの関数を作成:
ALTER FUNCTION [dbo].[UDF_IDListToTable]
(
@list [varchar](MAX),
@Seperator CHAR(1)
)
RETURNS @tbl TABLE (ID INT)
WITH
EXECUTE AS CALLER
AS
BEGIN
DECLARE @position INT
DECLARE @NewLine CHAR(2)
DECLARE @no INT
SET @NewLine = CHAR(13) + CHAR(10)
IF CHARINDEX(@Seperator, @list) = 0
BEGIN
INSERT INTO @tbl
VALUES
(
@list
)
END
ELSE
BEGIN
SET @position = 1
SET @list = @list + @Seperator
WHILE CHARINDEX(@Seperator, @list, @position) <> 0
BEGIN
SELECT @no = SUBSTRING(
@list,
@position,
CHARINDEX(@Seperator, @list, @position) - @position
)
IF @no <> ''
INSERT INTO @tbl
VALUES
(
@no
)
SET @position = CHARINDEX(@Seperator, @list, @position) + 1
END
END
RETURN
END
これを行った後、文字列をセパレーター付きのこの関数に渡します。
これがお役に立てば幸いです。 :-)