Mám proces, který popadne spoustu záznamů (1000) a pracuje na nich, a až budu hotový, musím je označit jako zpracované velké množství. Mohu to označit velkým seznamem ID. Snažím se vyhnout vzorci „aktualizace ve smyčce“, takže bych chtěl najít účinnější způsob, jak poslat tuto identifikační kartu do MS SQL Server 2008 uloženého proc.
Návrh č. 1 - Tabulka s hodnotami parametrů. Dokážu definovat typ tabulky bez ID pole a odeslat do tabulky plné ID k aktualizaci.
Návrh č. 2 - Parametr XML (varchar) s OPENXML () v těle proc.
Návrh č. 3 - Analýza seznamu. Raději bych se tomu vyhnul, je-li to možné, protože se to zdá být nepraktické a náchylné k chybám.
Máte nějaké preference, nebo nějaké nápady, které mi chyběly?
Nejlepší články, které se v této věci objevily, jsou Erland Sommarskog:
Pokryje všechny možnosti a vysvětluje docela dobře.
Omlouváme se za krátkou odpověď, ale Erlandův článek o Arraysovi je jako knihy Joe Celka o stromech a jiných SQL zachází :)
O tom je velká diskuse na StackOverflow , která pokrývá mnoho přístupů. Ten, který preferuji pro SQL Server 2008+, je použít parametry s tabulko. Toto je v podstatě řešení serveru SQL Server pro váš problém - předání seznamu hodnot uložené proceduře.
Výhody tohoto přístupu jsou:
Všimněte si však: Pokud voláte uloženou proceduru, která používá TVP přes ADO.NET nebo ODBC) a vezměte podívejte se na aktivitu s SQL Server Profiler, všimnete si, že SQL Server obdrží několik příkazů INSERT
pro načtení TVP, jeden pro každý řádek v TVP , následovaný voláním Toto je záměrně . Tuto dávku INSERT
s je třeba kompilovat pokaždé, když je procedura vyvolána, a představuje malou režii. Nicméně i s touto režií, TVP stále - odfouknout další přístupy z hlediska výkonu a použitelnosti pro většinu případů použití.
Pokud se chcete dozvědět více, Erland Sommarskog má plné hubené o tom, jak fungují parametry s hodnotou tabulky, a uvádí několik příkladů.
Tady je další příklad, který jsem vymyslel:
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
Celý předmět je diskutován v konečném článku Erland Sommarskog: "Pole a seznam na serveru SQL" . Vyberte si, kterou verzi si vybrat.
Shrnutí pro pre SQL Server 2008, kde TVP převyšují zbytek
Tento článek stojí za přečtení, abych viděl další techniky a myšlení.
Upravit: pozdní odpověď na obrovské seznamy jinde: Předávání parametrů pole do uložené procedury
Vím, že jsem na tuto stranu pozdě, ale měl jsem v minulosti takový problém, musel jsem poslat až 100 000 bigintových čísel a udělal několik benchmarků. Nakonec jsme je poslali v binárním formátu jako obrázek - to bylo rychlejší než všechno ostatní, až na 100 000 čísel.
Zde je můj starý (SQL Server 2005) kód:
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
Následující kód balí celá čísla do binární blob. Zde obracím pořadí bytů:
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;
}
Jsem roztrhaná mezi tím, že vás odkazuji na SO nebo na odpověď zde, 'protože toto je téměř programovací otázka. Ale protože už mám řešení, které používám ... budu příspěvek že ;)
Funguje to tak, že do uložené procedury vložíte řetězec oddělený čárkami (jednoduché rozdělení, nedělí rozdělení stylu CSV) jako varchar (4000) a poté tento seznam vložíte do této funkce a získáte užitečnou tabulku zpět, tabulka jen varcharů.
To vám umožní odesílat hodnoty pouze ID, která chcete zpracovat, a v tomto bodě můžete provést jednoduché připojení.
Alternativně byste mohli udělat něco s CLR DataTable a nakrmit to, ale to je trochu více režijní podpory a každý rozumí seznamům 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
Pravidelně přijímám sady 1000 řádků a 10 000 řádků odesílaných z naší aplikace ke zpracování různými uloženými procedurami serveru SQL.
Abychom splnili požadavky na výkon, používáme TVP, ale pro překonání některých problémů s výkonem ve výchozím režimu zpracování musíte implementovat svůj vlastní souhrn dbDataReaderu. Nebudu se zabývat tím, jak a proč, protože jsou mimo rozsah této žádosti.
Zpracování XML jsem neuvažoval, protože jsem nenašel implementaci XML, která zůstává výkonná s více než 10 000 „řádky“.
Zpracování seznamu lze zpracovat jednorozměrným a dvojrozměrným součtovým (číslováním) zpracováním tabulky. Úspěšně jsme je použili v různých oblastech, ale dobře spravované TVP jsou výkonnější, pokud existuje více než několik set řádků.
Stejně jako u všech voleb týkajících se zpracování serveru SQL, musíte provést výběr podle modelu použití.
Nakonec jsem dostal šanci udělat nějaké TableValuedParameters a fungují skvěle, takže vložím celý lotta kód, který ukazuje, jak je používám, se ukázkou z některého z mého aktuálního kódu: (poznámka: používáme ADO .SÍŤ)
Také si všimněte: Píšu nějaký kód pro službu a mám spoustu předdefinovaných bitů kódu v jiné třídě, ale píšu to jako aplikace konzoly, abych ji mohl ladit, takže jsem to všechno vytrhl z aplikace konzoly. Promiňte můj kódovací styl (jako pevně zakódované připojovací řetězce), protože to bylo něco jako „sestavení jednoho zahodit“. Chtěl jsem ukázat, jak používám List<customObject>
a snadno ji vsuňte do databáze jako tabulku, kterou mohu použít v uložené proceduře. Níže uvedený kód C # a 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
Také budu konstruktivně kritizovat můj styl kódování, pokud to chcete nabídnout (všem čtenářům, kteří se setkají s touto otázkou), ale prosím, nechte jej konstruktivní;) ... Pokud mě opravdu chcete, najdete mě v chatovací místnosti zde . Doufejme, že s tímto kusem kódu je vidět, jak mohou používat List<Current>
jak jsem ji definoval jako tabulku v db a List<T>
v jejich aplikaci.
Buď bych šel s návrhem č. 1, nebo jako alternativu vytvořil stírací tabulku, která obsahuje pouze zpracovaná ID. Vložte do této tabulky během zpracování a poté, co skončíte, zavolejte proc podobný níže:
BEGIN TRAN
UPDATE dt
SET processed = 1
FROM dataTable dt
JOIN processedIds pi ON pi.id = dt.id;
TRUNCATE TABLE processedIds
COMMIT TRAN
Uděláte mnoho příloh, ale budou u malého stolu, takže by to mělo být rychlé. Můžete také dávkovat své přílohy pomocí ADO.net nebo jakéhokoli datového adaptéru, který používáte.
Název otázky zahrnuje úkol přenášet data z aplikace do uložené procedury. Tato část je vyloučena tělem otázky, ale dovolte mi to také odpovědět.
V kontextu sql-server-2008, jak je uvedeno ve značkách, existuje další skvělý článek od E. Sommarskog pole a seznamy v SQL Server 2008 . BTW jsem to našel v článku Marian, na který se odkazuje v jeho odpovědi.
Namísto uvedení odkazu odkazuji na jeho obsah:
Kromě zde zmíněných technik mám pocit, že v některých případech si zaslouží hromadnou kopii a hromadnou vložku, aby se zmínila o obecném případě.
Předávání parametrů pole uložené proceduře
Pro nejnovější verzi MS SQL 2016
S MS SQL 2016 zavádějí novou funkci: SPLIT_STRING () pro analýzu více hodnot.
To může váš problém vyřešit snadno.
Pro starší verzi MS SQL
Pokud používáte starší verzi, postupujte takto:
First Make one function:
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
Poté, co to provedete, předejte svůj řetězec této funkci s oddělovačem.
Doufám, že je to pro vás užitečné. : -)