Jak przekazać tablicę do procedury przechowywanej programu SQL Server

293

Jak przekazać tablicę do procedury przechowywanej programu SQL Server?

Na przykład mam listę pracowników. Chcę użyć tej listy jako tabeli i połączyć ją z inną tabelą. Ale listę pracowników należy przekazać jako parametr z C #.

Siergiej
źródło
proszę pana, mam nadzieję, że ten link pomoże ci przekazać listę / tablicę do SQL Server SP
patrick choi
Jest to ta sama klasa, co Parameterize an SQL IN
Łukasz Szozda

Odpowiedzi:

437

SQL Server 2008 (lub nowszy)

Najpierw w swojej bazie danych utwórz następujące dwa obiekty:

CREATE TYPE dbo.IDList
AS TABLE
(
  ID INT
);
GO

CREATE PROCEDURE dbo.DoSomethingWithEmployees
  @List AS dbo.IDList READONLY
AS
BEGIN
  SET NOCOUNT ON;

  SELECT ID FROM @List; 
END
GO

Teraz w kodzie C #:

// Obtain your list of ids to send, this is just an example call to a helper utility function
int[] employeeIds = GetEmployeeIds();

DataTable tvp = new DataTable();
tvp.Columns.Add(new DataColumn("ID", typeof(int)));

// populate DataTable from your List here
foreach(var id in employeeIds)
    tvp.Rows.Add(id);

using (conn)
{
    SqlCommand cmd = new SqlCommand("dbo.DoSomethingWithEmployees", conn);
    cmd.CommandType = CommandType.StoredProcedure;
    SqlParameter tvparam = cmd.Parameters.AddWithValue("@List", tvp);
    // these next lines are important to map the C# DataTable object to the correct SQL User Defined Type
    tvparam.SqlDbType = SqlDbType.Structured;
    tvparam.TypeName = "dbo.IDList";
    // execute query, consume results, etc. here
}

SQL Server 2005

Jeśli korzystasz z programu SQL Server 2005, nadal polecałbym funkcję podziału zamiast XML. Najpierw utwórz funkcję:

CREATE FUNCTION dbo.SplitInts
(
   @List      VARCHAR(MAX),
   @Delimiter VARCHAR(255)
)
RETURNS TABLE
AS
  RETURN ( SELECT Item = CONVERT(INT, Item) FROM
      ( SELECT Item = x.i.value('(./text())[1]', 'varchar(max)')
        FROM ( SELECT [XML] = CONVERT(XML, '<i>'
        + REPLACE(@List, @Delimiter, '</i><i>') + '</i>').query('.')
          ) AS a CROSS APPLY [XML].nodes('i') AS x(i) ) AS y
      WHERE Item IS NOT NULL
  );
GO

Teraz procedura składowana może być po prostu:

CREATE PROCEDURE dbo.DoSomethingWithEmployees
  @List VARCHAR(MAX)
AS
BEGIN
  SET NOCOUNT ON;

  SELECT EmployeeID = Item FROM dbo.SplitInts(@List, ','); 
END
GO

A w kodzie C # wystarczy przekazać listę jako '1,2,3,12'...


Uważam, że metoda przekazywania parametrów wycenianych w tabeli upraszcza konserwację rozwiązania, które z niej korzysta i często ma większą wydajność w porównaniu z innymi implementacjami, w tym XML i dzielenie ciągów.

Dane wejściowe są jasno zdefiniowane (nikt nie musi zgadywać, czy separatorem jest przecinek czy średnik) i nie mamy zależności od innych funkcji przetwarzania, które nie są oczywiste bez sprawdzenia kodu procedury przechowywanej.

W porównaniu z rozwiązaniami obejmującymi zdefiniowany przez użytkownika schemat XML zamiast UDT, wymaga to podobnej liczby kroków, ale z mojego doświadczenia wynika, że ​​kod jest o wiele prostszy w zarządzaniu, utrzymywaniu i czytaniu.

W wielu rozwiązaniach może być potrzebny tylko jeden lub kilka z tych UDT (typy zdefiniowane przez użytkownika), których ponownie używasz do wielu procedur przechowywanych. Podobnie jak w tym przykładzie, powszechnym wymogiem jest przekazywanie listy wskaźników identyfikatorów, nazwa funkcji opisuje kontekst, jaki powinny reprezentować te identyfikatory, nazwa typu powinna być ogólna.

Aaron Bertrand
źródło
3
Podoba mi się pomysł parametru tabeli - nigdy o tym nie myślałem - wiwaty. Do tego, co jest warte, separator musi przejść do wywołania funkcji SplitInts ().
Drammy
Jak mogę użyć parametru tabeli, jeśli mam dostęp tylko do łańcucha rozdzielanego przecinkami
bdwain
@bdwain pokonałby cel - trzeba by użyć funkcji podziału, aby rozbić go na rzędy i umieścić w TVP. Podziel go na kod aplikacji.
Aaron Bertrand
1
@AaronBertrand dzięki za odpowiedź, właściwie to po prostu wymyśliłem. Muszę używać sub wybrać w nawiasach: SELECT [colA] FROM [MyTable] WHERE [Id] IN (SELECT [Id] FROM @ListOfIds).
JaKXz,
3
@ th1rdey3 Są domyślnie opcjonalne. stackoverflow.com/a/18926590/61305
Aaron Bertrand
44

Z mojego doświadczenia wynika, że ​​tworzenie identyfikatora oddzielonego od identyfikatora pracownika jest trudnym i przyjemnym rozwiązaniem tego problemu. Należy utworzyć tylko wyrażenie łańcuchowe, takie jak ';123;434;365;'in-which 123, 434i 365są to niektóre identyfikatory pracowników. Wywołując poniższą procedurę i przekazując do niej to wyrażenie, możesz pobrać żądane rekordy. Łatwo możesz dołączyć „kolejną tabelę” do tego zapytania. To rozwiązanie jest odpowiednie we wszystkich wersjach serwera SQL. Ponadto, w porównaniu z użyciem tabeli zmiennych lub tabeli temperatur, jest to bardzo szybsze i zoptymalizowane rozwiązanie.

CREATE PROCEDURE dbo.DoSomethingOnSomeEmployees  @List AS varchar(max)
AS
BEGIN
  SELECT EmployeeID 
  FROM EmployeesTable
  -- inner join AnotherTable on ...
  where @List like '%;'+cast(employeeID as varchar(20))+';%'
END
GO
Hamed Nazaktabar
źródło
Miły! Naprawdę podoba mi się to podejście, w którym filtruję klucze int! +1
MDV2000
@ MDV2000 dzięki :) Na kluczach łańcuchowych ma to również dobrą wydajność, ponieważ nie
rzutuje
Spóźniłem się na grę, ale to bardzo sprytne! Działa świetnie dla mojego problemu.
user441058,
To jest niesamowite! Na pewno skorzystam z tego, dziękuję
Omar Ruder,
26

Użyj parametru o wartościach przechowywanych w tabeli dla procedury składowanej.

Po przekazaniu go z C # dodasz parametr o typie danych SqlDb.Structured.

Zobacz tutaj: http://msdn.microsoft.com/en-us/library/bb675163.aspx

Przykład:

// Assumes connection is an open SqlConnection object.
using (connection)
{
// Create a DataTable with the modified rows.
DataTable addedCategories =
  CategoriesDataTable.GetChanges(DataRowState.Added);

// Configure the SqlCommand and SqlParameter.
SqlCommand insertCommand = new SqlCommand(
    "usp_InsertCategories", connection);
insertCommand.CommandType = CommandType.StoredProcedure;
SqlParameter tvpParam = insertCommand.Parameters.AddWithValue(
    "@tvpNewCategories", addedCategories);
tvpParam.SqlDbType = SqlDbType.Structured;

// Execute the command.
insertCommand.ExecuteNonQuery();
}
Levi W
źródło
17

Musisz przekazać go jako parametr XML.

Edytuj: szybki kod z mojego projektu, aby dać ci pomysł:

CREATE PROCEDURE [dbo].[GetArrivalsReport]
    @DateTimeFrom AS DATETIME,
    @DateTimeTo AS DATETIME,
    @HostIds AS XML(xsdArrayOfULong)
AS
BEGIN
    DECLARE @hosts TABLE (HostId BIGINT)

    INSERT INTO @hosts
        SELECT arrayOfUlong.HostId.value('.','bigint') data
        FROM @HostIds.nodes('/arrayOfUlong/u') as arrayOfUlong(HostId)

Następnie możesz użyć tabeli tymczasowej, aby połączyć się ze swoimi tabelami. Zdefiniowaliśmy arrayOfUlong jako wbudowany schemat XML, aby zachować integralność danych, ale nie musisz tego robić. Polecam go użyć, więc oto krótki kod, aby upewnić się, że zawsze otrzymujesz XML z długimi.

IF NOT EXISTS (SELECT * FROM sys.xml_schema_collections WHERE name = 'xsdArrayOfULong')
BEGIN
    CREATE XML SCHEMA COLLECTION [dbo].[xsdArrayOfULong]
    AS N'<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="arrayOfUlong">
        <xs:complexType>
            <xs:sequence>
                <xs:element maxOccurs="unbounded"
                            name="u"
                            type="xs:unsignedLong" />
            </xs:sequence>
        </xs:complexType>
    </xs:element>
</xs:schema>';
END
GO
Fedor Hajdu
źródło
Myślałem, że to zły pomysł, aby używać zmiennych tabeli, gdy jest wiele wierszy? Czy nie jest lepsza wydajność zamiast używać tabeli temp (#table)?
ganders
@ ganders: Powiedziałbym odwrotnie.
abatishchev
14

Kontekst jest zawsze ważny, na przykład rozmiar i złożoność tablicy. W przypadku list od małych do średnich kilka opublikowanych tutaj odpowiedzi jest w porządku, choć należy wyjaśnić:

  • Do podziału rozdzielanej listy najszybszy jest rozdzielacz oparty na SQLCLR. Istnieje wiele przykładów, jeśli chcesz napisać własny lub możesz po prostu pobrać darmowy kod SQL # bibliotekę funkcji CLR (którą napisałem, ale funkcja String_Split i wiele innych są całkowicie bezpłatne).
  • Dzielenie tablic opartych na XML może być szybkie, ale musisz użyć XML opartego na atrybutach, a nie XML opartego na elementach (co jest jedynym typem pokazanym w odpowiedziach tutaj, chociaż przykład XML @ AaronBertrand jest najlepszy, ponieważ jego kod używa text()Funkcja XML Aby uzyskać więcej informacji (np. Analizę wydajności) na temat używania XML do dzielenia list, zobacz „Używanie XML do przekazywania list jako parametrów w SQL Server” Phila Factora.
  • Korzystanie z TVP jest świetne (zakładając, że używasz co najmniej SQL Server 2008 lub nowszej wersji), ponieważ dane są przesyłane strumieniowo do proc i pokazują wstępnie przeanalizowane i silnie wpisane jako zmienne tabelowe. JEDNAK w większości przypadków przechowywanie wszystkich danych DataTableoznacza duplikowanie danych w pamięci podczas kopiowania z oryginalnej kolekcji. Stąd korzystanie zDataTable metody przekazywania w TVP nie działa dobrze w przypadku większych zestawów danych (tj. Nie jest dobrze skalowane).
  • XML, w przeciwieństwie do prostych rozdzielonych list Ints lub Ciągów, może obsługiwać więcej niż jednowymiarowe tablice, podobnie jak TVP. Ale podobnie jak DataTablemetoda TVP, XML nie skaluje się dobrze, ponieważ podwaja rozmiar danych w pamięci, ponieważ musi dodatkowo uwzględniać narzut dokumentu XML.

Biorąc to wszystko pod uwagę, JEŻELI używane dane są duże lub jeszcze niezbyt duże, ale stale rosną, to IEnumerablemetoda TVP jest najlepszym wyborem, ponieważ przesyła dane do SQL Server (jakDataTable metoda), ALE nie wymagają jakiegokolwiek powielenia kolekcji w pamięci (w przeciwieństwie do innych metod). W tej odpowiedzi opublikowałem przykład kodu SQL i C #:

Przekaż słownik do procedury składowanej T-SQL

Solomon Rutzky
źródło
6

Serwer SQL nie obsługuje tablicy, ale istnieje kilka sposobów przekazywania kolekcji do przechowywanego proc.

  1. Korzystając z danych
  2. Korzystając z XML.Try przekonwertuj swoją kolekcję w formacie xml, a następnie przekaż ją jako dane wejściowe do procedury składowanej

Poniższy link może ci pomóc

przekazanie kolekcji do procedury składowanej

praveen
źródło
5

Przeszukiwałem wszystkie przykłady i odpowiedzi, jak przekazać dowolną tablicę do serwera SQL bez kłopotów z tworzeniem nowego typu tabeli, dopóki nie znalazłem tego linK , poniżej jest to, jak zastosowałem go do mojego projektu:

- Poniższy kod pobiera tablicę jako parametr i wstawia wartości tej tablicy do innej tabeli

Create Procedure Proc1 


@UserId int, //just an Id param
@s nvarchar(max)  //this is the array your going to pass from C# code to your Sproc

AS

    declare @xml xml

    set @xml = N'<root><r>' + replace(@s,',','</r><r>') + '</r></root>'

    Insert into UserRole (UserID,RoleID)
    select 
       @UserId [UserId], t.value('.','varchar(max)') as [RoleId]


    from @xml.nodes('//root/r') as a(t)
END 

Mam nadzieję, że ci się spodoba

Adam
źródło
2
@zaitsman: CLEANEST nie oznacza najlepszego ani najbardziej odpowiedniego. Często rezygnuje się z elastyczności i / lub „odpowiedniej” złożoności i / lub wydajności, aby uzyskać „czysty” kod. Ta odpowiedź tutaj jest „OK”, ale tylko w przypadku małych zestawów danych. Jeśli tablicą przychodzącą @sjest CSV, szybsze byłoby po prostu podzielenie tego (tj. INSERT INTO ... SELECT FROM SplitFunction). Konwersja na XML przebiega wolniej niż CLR, a XML oparty na atrybutach i tak jest znacznie szybszy. Jest to prosta lista, ale przekazywanie w XML lub TVP może również obsługiwać złożone tablice. Nie jestem pewien, co zyskasz, unikając prostego, jednorazowego działania CREATE TYPE ... AS TABLE.
Solomon Rutzky
5

To ci pomoże. :) Wykonaj kolejne kroki,

  1. Otwórz projektanta zapytań
  2. Kopiuj Wklej następujący kod w obecnej postaci, utworzy on funkcję, która przekształci ciąg na Int

    CREATE FUNCTION dbo.SplitInts
    (
       @List      VARCHAR(MAX),
       @Delimiter VARCHAR(255)
    )
    RETURNS TABLE
    AS
      RETURN ( SELECT Item = CONVERT(INT, Item) FROM
          ( SELECT Item = x.i.value('(./text())[1]', 'varchar(max)')
            FROM ( SELECT [XML] = CONVERT(XML, '<i>'
            + REPLACE(@List, @Delimiter, '</i><i>') + '</i>').query('.')
              ) AS a CROSS APPLY [XML].nodes('i') AS x(i) ) AS y
          WHERE Item IS NOT NULL
      );
    GO
  3. Utwórz następującą procedurę składowaną

     CREATE PROCEDURE dbo.sp_DeleteMultipleId
     @List VARCHAR(MAX)
     AS
     BEGIN
          SET NOCOUNT ON;
          DELETE FROM TableName WHERE Id IN( SELECT Id = Item FROM dbo.SplitInts(@List, ',')); 
     END
     GO
  4. Wykonaj ten SP. Używając exec sp_DeleteId '1,2,3,12'tego jest ciąg identyfikatorów, które chcesz usunąć,

  5. Konwertujesz tablicę na ciąg w C # i przekazujesz ją jako parametr procedury składowanej

    int[] intarray = { 1, 2, 3, 4, 5 };  
    string[] result = intarray.Select(x=>x.ToString()).ToArray();

     

    SqlCommand command = new SqlCommand();
    command.Connection = connection;
    command.CommandText = "sp_DeleteMultipleId";
    command.CommandType = CommandType.StoredProcedure;
    command.Parameters.Add("@Id",SqlDbType.VARCHAR).Value=result ;

Spowoduje to usunięcie wielu wierszy, wszystkiego najlepszego

Charan Ghate
źródło
użyłem tej oddzielnej funkcji analizowania przecinków, działałoby to w przypadku małego zestawu danych, jeśli sprawdzisz jego plan wykonania, spowoduje to problem w przypadku dużego zestawu danych i gdzie musisz mieć wiele list csv w procedurze przechowywanej
Saboor Awan
2

Zajęło mi to dużo czasu, aby to zrozumieć, więc na wypadek, gdyby ktoś tego potrzebował ...

Jest to oparte na metodzie SQL 2005 w odpowiedzi Aarona i użyciu jego funkcji SplitInts (właśnie usunąłem parametr delim, ponieważ zawsze będę używać przecinków). Korzystam z SQL 2008, ale chciałem czegoś, co będzie działać z typowymi zestawami danych (XSD, TableAdapters) i wiem, że parametry ciągów działają z nimi.

Próbowałem zmusić jego funkcję do działania w klauzuli typu „gdzie w (1,2,3)” i nie miałem szczęścia w prosty sposób. Więc najpierw stworzyłem tabelę tymczasową, a następnie wykonałem wewnętrzne połączenie zamiast „gdzie w”. Oto moje przykładowe użycie, w moim przypadku chciałem uzyskać listę przepisów, które nie zawierają niektórych składników:

CREATE PROCEDURE dbo.SOExample1
    (
    @excludeIngredientsString varchar(MAX) = ''
    )
AS
    /* Convert string to table of ints */
    DECLARE @excludeIngredients TABLE (ID int)
    insert into @excludeIngredients
    select ID = Item from dbo.SplitInts(@excludeIngredientsString)

    /* Select recipies that don't contain any ingredients in our excluded table */
   SELECT        r.Name, r.Slug
FROM            Recipes AS r LEFT OUTER JOIN
                         RecipeIngredients as ri inner join
                         @excludeIngredients as ei on ri.IngredientID = ei.ID
                         ON r.ID = ri.RecipeID
WHERE        (ri.RecipeID IS NULL)
eselk
źródło
Ogólnie rzecz biorąc, najlepiej nie ŁĄCZYĆ się ze Zmienną Tabeli, ale zamiast Tabeli Temp. Zmienne tabelowe domyślnie wydają się mieć tylko jeden wiersz, choć istnieje jedna lub dwie sztuczki (sprawdź doskonały i szczegółowy artykuł @ AaronBertrand: sqlperformance.com/2014/06/t-sql-queries/… ).
Solomon Rutzky
1

Jak zauważyli inni powyżej, jednym ze sposobów jest przekonwertowanie tablicy na ciąg, a następnie podzielenie ciągu wewnątrz SQL Server.

Począwszy od SQL Server 2016, istnieje wbudowany sposób na dzielenie ciągów o nazwie

STRING_SPLIT ()

Zwraca zestaw wierszy, które można wstawić do tabeli tymczasowej (lub tabeli rzeczywistej).

DECLARE @str varchar(200)
SET @str = "123;456;789;246;22;33;44;55;66"
SELECT value FROM STRING_SPLIT(@str, ';')

dałoby:

wartość
-----
  123
  456
  789
  246
   22
   33
   44
   55
   66

Jeśli chcesz zdobyć bardziej fantazyjne:

DECLARE @tt TABLE (
    thenumber int
)
DECLARE @str varchar(200)
SET @str = "123;456;789;246;22;33;44;55;66"

INSERT INTO @tt
SELECT value FROM STRING_SPLIT(@str, ';')

SELECT * FROM @tt
ORDER BY thenumber

dałoby takie same wyniki jak powyżej (z wyjątkiem tego, że nazwa kolumny to „numer”), ale posortowane. Możesz używać zmiennej tabeli jak każdej innej tabeli, więc możesz łatwo połączyć ją z innymi tabelami w bazie danych, jeśli chcesz.

Należy pamiętać, że instalacja programu SQL Server musi mieć poziom zgodności 130 lub wyższy, aby STRING_SPLIT()funkcja została rozpoznana. Możesz sprawdzić poziom zgodności za pomocą następującego zapytania:

SELECT compatibility_level
FROM sys.databases WHERE name = 'yourdatabasename';

Większość języków (w tym C #) ma funkcję „łączyć”, za pomocą której można utworzyć ciąg z tablicy.

int[] myarray = {22, 33, 44};
string sqlparam = string.Join(";", myarray);

Następnie przekazujesz sqlparamjako parametr powyższej procedury składowanej.

Patrick Chu
źródło
0
CREATE TYPE dumyTable
AS TABLE
(
  RateCodeId int,
  RateLowerRange int,
  RateHigherRange int,
  RateRangeValue int
);
GO
CREATE PROCEDURE spInsertRateRanges
  @dt AS dumyTable READONLY
AS
BEGIN
  SET NOCOUNT ON;

  INSERT  tblRateCodeRange(RateCodeId,RateLowerRange,RateHigherRange,RateRangeValue) 
  SELECT * 
  FROM @dt 
END
Shabir Mustafa
źródło