Przekazywanie parametrów tablicy do procedury składowanej

53

Mam proces, który pobiera kilka rekordów (1000) i działa na nich, a kiedy skończę, muszę oznaczyć ich dużą liczbę jako przetworzonych. Mogę to wskazać za pomocą dużej listy identyfikatorów. Staram się unikać wzorca „aktualizacji w pętli”, dlatego chciałbym znaleźć bardziej skuteczny sposób na przesłanie tego worka identyfikatorów do przechowywanego proc. MS SQL Server 2008.

Propozycja 1 - Parametry wyceniane w tabeli. Mogę zdefiniować typ tabeli z polem identyfikatora i wysłać tabelę zawierającą identyfikatory do aktualizacji.

Propozycja 2 - parametr XML (varchar) z OPENXML () w treści proc.

Propozycja nr 3 - parsowanie listy. Wolę raczej tego unikać, jeśli to możliwe, ponieważ wydaje się to nieporęczne i podatne na błędy.

Jakieś preferencje wśród nich lub pomysły, które przegapiłem?

D. Lambert
źródło
Jak dostajesz dużą listę identyfikatorów?
Larry Coleman
Ściągam je wraz z danymi „ładunku” za pośrednictwem innego przechowywanego procesu. Nie muszę jednak aktualizować wszystkich tych danych - wystarczy zaktualizować flagę na niektórych rekordach.
D. Lambert

Odpowiedzi:

42

Najlepsze w historii artykuły na ten temat są autorstwa Erlanda Sommarskoga:

Obejmuje wszystkie opcje i całkiem dobrze wyjaśnia.

Przepraszam za krótką odpowiedź, ale artykuł Erlanda na temat tablic jest jak książki Joe Celko o drzewach i innych smakołykach SQL :)

Marian
źródło
23

Jest świetna dyskusja na ten temat na StackOverflow, która obejmuje wiele podejść. Ten, który preferuję w SQL Server 2008+, to używanie parametrów o wartościach przechowywanych w tabeli . Jest to zasadniczo rozwiązanie problemu dla programu SQL Server - przekazanie listy wartości do procedury składowanej.

Zaletami tego podejścia są:

  • wykonaj jedno wywołanie procedury składowanej ze wszystkimi danymi przekazanymi jako 1 parametr
  • dane wejściowe do tabeli są uporządkowane i silnie wpisane
  • brak budowania ciągów / parsowania lub obsługi XML
  • może łatwo używać danych wejściowych z tabeli do filtrowania, łączenia lub cokolwiek innego

Jednak zwróć uwagę: jeśli wywołasz procedurę składowaną, która korzysta z TVP za pośrednictwem ADO.NET lub ODBC i spojrzysz na działanie za pomocą SQL Server Profiler, zauważysz, że SQL Server otrzymuje kilka INSERTinstrukcji do załadowania TVP, po jednej dla każdego wiersza w TVP , a następnie wezwanie do zabiegu. Jest to zgodne z projektem . Ta partia INSERTs musi być kompilowana za każdym razem, gdy wywoływana jest procedura, i stanowi niewielki narzut. Jednak nawet z tym narzutem TVP nadal odrzucają inne podejścia pod względem wydajności i użyteczności w większości przypadków użycia.

Jeśli chcesz dowiedzieć się więcej, Erland Sommarskog ma pełną wiedzę na temat działania parametrów wycenianych w tabeli i podaje kilka przykładów.

Oto inny przykład, który wymyśliłem:

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
Nick Chammas
źródło
Po uruchomieniu pojawia się błąd: Msg 2715, poziom 16, stan 3, procedura tvp_test, wiersz 4 [partia wsadowa wiersz 4] kolumna, parametr lub zmienna nr 2: nie można znaleźć typu danych id_list. Parametr lub zmienna „@customer_list” ma nieprawidłowy typ danych. Msg 1087, poziom 16, stan 1, procedura tvp_test, wiersz 13 [Batch Start Line 4] Musi zadeklarować zmienną tabeli „@customer_list”.
Damian
@Damian - Czy CREATE TYPEinstrukcja na początku działała poprawnie? Jaką wersję programu SQL Server używasz?
Nick Chammas
W kodzie SP wstawiłeś to zdanie „SELECT @ param1 AS param1; ' . Jaki jest cel Nie używasz ani parametru 1, więc dlaczego umieściłeś to jako parametr w nagłówku SP?
EAmez
@EAmez - To był tylko arbitralny przykład. Nie o to @customer_listchodzi @param1. Przykład pokazuje po prostu, że można mieszać różne typy parametrów.
Nick Chammas
21

Cały temat omawiany jest w ostatecznym artykule Erlanda Sommarskoga: „Tablice i listy w SQL Server” . Wybierz wersję, którą chcesz wybrać.

Podsumowanie, dla wersji wcześniejszych niż SQL Server 2008, w których TVP przebijają resztę

  • CSV, podziel się, jak chcesz (zazwyczaj używam tabeli Numbers)
  • XML i parsowanie (lepiej w SQL Server 2005+)
  • Utwórz tymczasową tabelę na kliencie

Ten artykuł i tak warto przeczytać, aby zobaczyć inne techniki i sposób myślenia.

Edycja: późna odpowiedź dla ogromnych list w innym miejscu: Przekazywanie parametrów tablicy do procedury składowanej

gbn
źródło
14

Wiem, że spóźniłem się na tę imprezę, ale miałem taki problem w przeszłości, musiałem wysłać do 100 tysięcy bigintów i wykonałem kilka testów porównawczych. Ostatecznie wysłaliśmy je w formacie binarnym, jako obraz - to było szybsze niż wszystko inne dla liczb do 100 000.

Oto mój stary kod (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

Poniższy kod upakowuje liczby całkowite do binarnego obiektu blob. Odwracam tutaj kolejność bajtów:

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;
}
AK
źródło
9

Jestem rozdarty między odsyłaniem cię do SO lub odpowiedzeniem tutaj, bo to jest prawie pytanie programistyczne. Ale skoro mam już rozwiązanie, którego używam ... opublikuję to;)

Działa to w ten sposób, że podajesz ciąg rozdzielany przecinkami (prosty podział, nie robi podziałów w stylu CSV) do procedury składowanej jako varchar (4000), a następnie podajesz tę listę do tej funkcji i dostajesz poręczną tabelę, stół tylko varcharów.

Pozwala to na przesłanie wartości tylko identyfikatorów, które chcesz przetworzyć, i możesz w tym momencie wykonać proste łączenie.

Alternatywnie możesz zrobić coś z CLR DataTable i zasilić to, ale to trochę więcej narzutów do wsparcia i każdy rozumie listy 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
jcolebrand
źródło
Cóż, szczególnie starałem się unikać listy rozdzielanej przecinkami, aby nie musiałem pisać czegoś takiego, ale ponieważ jest już napisany, myślę, że musiałbym wrzucić to rozwiązanie z powrotem do miksu. ;-)
D. Lambert
1
Mówię, że wypróbowane i prawdziwe jest najłatwiejsze. Możesz wydzielić listę oddzieloną przecinkami w C # w ciągu kilku sekund kodu i możesz wrzucić ją do tej funkcji (po wprowadzeniu do sproc) wystarczająco szybko i nie musisz nawet o tym myśleć. ~ I wiem, że powiedziałeś, że nie chcesz używać funkcji, ale myślę, że jest to najprostszy (może nie najskuteczniejszy)
jcolebrand
5

Regularnie otrzymuję zestawy 1000 wierszy i 10000 wierszy wysyłanych z naszej aplikacji do przetworzenia przez różne procedury składowane programu SQL Server.

Aby sprostać wymaganiom dotyczącym wydajności, korzystamy z TVP, ale musisz zaimplementować własny streszczenie dbDataReader, aby rozwiązać niektóre problemy z wydajnością w domyślnym trybie przetwarzania. Nie będę wchodził w hows i dlaczego, ponieważ są poza zakresem tej prośby.

Nie zastanawiałem się nad przetwarzaniem XML, ponieważ nie znalazłem implementacji XML, która pozostaje wydajna z ponad 10.000 „wierszy”.

Przetwarzanie list może być obsługiwane przez przetwarzanie tabel liczb jednowymiarowych i dwuwymiarowych (liczb). Wykorzystaliśmy je z powodzeniem w różnych obszarach, ale dobrze zarządzani TVP są bardziej wydajni, gdy jest ich więcej niż kilkaset „wierszy”.

Podobnie jak w przypadku wszystkich opcji dotyczących przetwarzania programu SQL Server, musisz dokonać wyboru na podstawie modelu użytkowania.

Robert Miller
źródło
5

W końcu dostałem szansę na wykonanie TableValuedParameters i działają one świetnie, więc wkleję cały kod lotta, który pokazuje, jak ich używam, wraz z próbką z mojego bieżącego kodu: (uwaga: używamy ADO .NETTO)

Uwaga: piszę kod dla usługi i mam wiele predefiniowanych bitów kodu w drugiej klasie, ale piszę to jako aplikację konsolową, dzięki czemu mogę debugować, więc zgarnąłem wszystko z aplikacja konsoli. Przepraszam za mój styl kodowania (jak na stałe parametry połączenia), ponieważ było to coś w rodzaju „zbuduj jeden, aby go wyrzucić”. Chciałem pokazać, jak korzystam z a, List<customObject>i łatwo wepchnąć do bazy danych jako tabelę, której mogę użyć w procedurze przechowywanej. Kod C # i TSQL poniżej:

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

Będę również konstruktywnie krytykować mój styl kodowania, jeśli masz to do zaoferowania (wszystkim czytelnikom, którzy napotkają to pytanie), ale proszę, zachowaj konstruktywność;) ... Jeśli naprawdę mnie chcesz, znajdź mnie na czacie tutaj . Mam nadzieję, że dzięki temu fragmentowi kodu można zobaczyć, jak mogą korzystać z tego, List<Current>co ja zdefiniowałem jako tabelę w bazie danych iw List<T>aplikacji.

jcolebrand
źródło
3

Wybrałbym propozycję nr 1 lub, alternatywnie, stworzyłem tabelę scratch, która po prostu przechowuje przetworzone identyfikatory. Wstaw do tej tabeli podczas przetwarzania, a po zakończeniu wywołaj procedurę podobną do poniższej:

BEGIN TRAN

UPDATE dt
SET processed = 1
FROM dataTable dt
JOIN processedIds pi ON pi.id = dt.id;

TRUNCATE TABLE processedIds

COMMIT TRAN

Zrobisz wiele wstawek, ale będą przy małym stoliku, więc powinno być szybko. Możesz także wsadowo wstawiać swoje wkładki za pomocą ADO.net lub dowolnego adaptera danych, którego używasz.

Eric Humphrey - lotsahelp
źródło
2

Tytuł pytania obejmuje zadanie przesyłania danych z aplikacji do procedury składowanej. Ta część jest wykluczona przez pytanie, ale spróbuję też na to odpowiedzieć.

W kontekście sql-server-2008, jak określono w tagach, jest kolejny świetny artykuł autorstwa E. Sommarskog Tablice i listy w SQL Server 2008 . BTW znalazłem to w artykule, do którego Marian odniósł się w swojej odpowiedzi.

Zamiast podawać link, cytuję jego listę treści:

  • Wprowadzenie
  • tło
  • Parametry wyceniane w tabeli w języku T-SQL
  • Przekazywanie parametrów wycenianych w tabeli z ADO .NET
    • Korzystanie z listy
    • Korzystanie z DataTable
    • Korzystanie z czytnika danych
    • Uwagi końcowe
  • Korzystanie z parametrów wycenianych w tabeli z innych interfejsów API
    • ODBC
    • OLE DB
    • KOROWODY
    • LINQ i Entity Framework
    • JDBC
    • PHP
    • Perl
    • Co jeśli Twój interfejs API nie obsługuje TVP
  • Uwagi dotyczące wydajności
    • Po stronie serwera
    • Strona klienta
    • Klucz podstawowy czy nie?
  • Podziękowania i opinie
  • Historia zmian

Poza wymienionymi tam technikami mam wrażenie, że w niektórych przypadkach zbiorcze kopiowanie i wkładka zbiorcza zasługują na uwzględnienie w ogólnym przypadku.

bernd_k
źródło
1

Przekazywanie parametrów tablicy do procedury składowanej

Dla najnowszej wersji MS SQL 2016

W MS SQL 2016 wprowadzono nową funkcję: SPLIT_STRING () do analizowania wielu wartości.

To może łatwo rozwiązać twój problem.

Dla starszej wersji MS SQL

Jeśli używasz starszej wersji, wykonaj następujące czynności:

Najpierw Wykonaj jedną funkcję:

 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

Po wykonaniu tej czynności, po prostu przekaż swój ciąg do tej funkcji za pomocą separatora.

Mam nadzieję, że ci to pomoże. :-)

Ankit Bhalala
źródło
-1

Użyj tego, aby utworzyć „tabelę typów”. prosty przykład dla użytkownika

CREATE TYPE unit_list AS TABLE (
    ItemUnitId int,
    Amount float,
    IsPrimaryUnit bit
);

GO
 CREATE TYPE specification_list AS TABLE (
     ItemSpecificationMasterId int,
    ItemSpecificationMasterValue varchar(255)
);

GO
 declare @units unit_list;
 insert into @units (ItemUnitId, Amount, IsPrimaryUnit) 
  values(12,10.50, false), 120,100.50, false), (1200,500.50, true);

 declare @spec specification_list;
  insert into @spec (ItemSpecificationMasterId,temSpecificationMasterValue) 
   values (12,'test'), (124,'testing value');

 exec sp_add_item "mytests", false, @units, @spec


//Procedure definition
CREATE PROCEDURE sp_add_item
(   
    @Name nvarchar(50),
    @IsProduct bit=false,
    @UnitsArray unit_list READONLY,
    @SpecificationsArray specification_list READONLY
)
AS


BEGIN
    SET NOCOUNT OFF     

    print @Name;
    print @IsProduct;       
    select * from @UnitsArray;
    select * from @SpecificationsArray;
END
Dinesh Vaitage
źródło