it-swarm-eu.dev

Předávání parametrů pole uložené proceduře

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?

54
D. Lambert

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í :)

44
Marian

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:

  • proveďte jedno uložené volání procedury se všemi vašimi daty předanými jako 1 parametr
  • tabulka je strukturovaná a silně typovaná
  • žádné vytváření řetězců/zpracování nebo manipulace s XML
  • lze snadno použít tabulkový vstup k filtrování, připojení nebo jakémuukoli jinému

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 INSERTs 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
23
Nick Chammas

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

  • CSV, rozdělte, jak se vám líbí (obvykle používám tabulku čísel)
  • XML a analyzovat (lépe s SQL Server 2005+)
  • Vytvořte dočasnou tabulku na klientovi

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

21
gbn

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;
}
14
A-K

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
9
jcolebrand

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í.

5
Robert Miller

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.

5
jcolebrand

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:

  • Úvod
  • Pozadí
  • Parametry s hodnotou tabulky v T-SQL
  • Předávání parametrů s hodnotou tabulky z ADO .NET
    • Použití seznamu
    • Použití datové tabulky
    • Použití DataReader
    • Závěrečné poznámky
  • Používání parametrů s hodnotou tabulky z jiných API
    • ODBC
    • OLE DB
    • ADO
    • LINQ a Entity Framework
    • JDBC
    • PHP
    • Perl
    • Co když vaše API nepodporuje TVP
  • Úvahy o výkonu
    • Na straně serveru
    • Na straně klienta
    • Primární klíč nebo ne?
  • Poděkování a zpětná vazba
  • Historie revizí

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ě.

2
bernd_k

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é. : -)

1
Ankit Bhalala