Як передати масив у збережену процедуру SQL Server


294

Як передати масив у збережену процедуру SQL Server?

Наприклад, у мене є список працівників. Я хочу використовувати цей список як таблицю і з'єднати його з іншою таблицею. Але список працівників слід передавати як параметр із C #.


сер, сподіваюся, що це посилання допоможе вам Передача списку / масиву до SP SQL Server
Патрік Чой

Відповіді:


438

SQL Server 2008 (або новіший)

По-перше, у своїй базі даних створіть наступні два об’єкти:

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

Тепер у вашому коді 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

Якщо ви використовуєте SQL Server 2005, я все-таки рекомендую розділити функцію по XML. Спочатку створіть функцію:

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

Тепер ваша збережена процедура може бути просто:

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

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

І у своєму C # коді просто потрібно передати список як '1,2,3,12'...


Я вважаю, що метод проходження параметрів, що оцінюються в таблиці, спрощує ремонтопридатність рішення, яке його використовує, і часто має підвищену продуктивність порівняно з іншими реалізаціями, включаючи XML та розбиття рядків.

Вхідні дані чітко визначені (ніхто не повинен здогадуватися, чи роздільник є комою чи крапкою з двокрапки), і ми не маємо залежностей від інших функцій обробки, які не очевидні без перевірки коду для збереженої процедури.

У порівнянні з рішеннями, що містять визначені користувачем схеми XML замість UDT, це включає аналогічну кількість кроків, але, на мій досвід, набагато простіший код для управління, обслуговування та читання.

У багатьох рішеннях вам може знадобитися лише один або кілька цих UDT (типів, визначених користувачем), які ви повторно використовуєте для багатьох збережених процедур. Як і в цьому прикладі, загальною вимогою є проходження списку покажчиків ідентифікаторів, ім'я функції описує, який контекст повинні представляти ці ІД, назва типу має бути загальним.


3
Мені подобається ідея параметрів таблиці - ніколи про це не думав - ура. Для чого варто розмежувачу, потрібно перейти у виклик функції SplitInts ().
Драмі

Як я можу використовувати параметр таблиці, якщо
матиму

@bdwain переможе мету - вам потрібно буде використовувати роздільну функцію, щоб розбити її на рядки, щоб розмістити в TVP. Розбийте його у коді програми.
Аарон Бертран

1
@AaronBertrand дякую за відповідь, я насправді просто зрозумів це. Я повинен використовувати суб вибрати в дужках: SELECT [colA] FROM [MyTable] WHERE [Id] IN (SELECT [Id] FROM @ListOfIds).
JaKXz

3
@ th1rdey3 Вони неявно необов’язкові. stackoverflow.com/a/18926590/61305
Аарон Бертран

44

Виходячи з мого досвіду, створюючи розмежоване вираження з ідентифікаторів співробітників, є складне і приємне рішення цієї проблеми. Ви повинні створити тільки вираз рядки , як ';123;434;365;'в-якої 123, 434і 365деякі employeeIDs. Зателефонувавши до наведеної нижче процедури та передавши їй цей вираз, ви можете отримати бажані записи. Ви легко можете приєднати "іншу таблицю" до цього запиту. Це рішення підходить у всіх версіях SQL-сервера. Крім того, порівняно з використанням таблиці змінної або temp, це дуже швидке та оптимізоване рішення.

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

Приємно! Мені дуже подобається такий підхід, коли я фільтрую по клавішах int! +1
MDV2000

@ MDV2000 спасибі :) На строкових клавішах це також має хороші показники через те, що не
викидає

Я запізнююсь на гру, але це дуже розумно! Підходить для моєї проблеми.
користувач441058

Це дивно! Я обов’язково цим скористаюся, дякую
Омар Рудер

26

Використовуйте табличний параметр для збереженої процедури.

Коли ви передасте його з C #, ви додасте параметр із типом даних SqlDb.Structured.

Дивіться тут: http://msdn.microsoft.com/en-us/library/bb675163.aspx

Приклад:

// 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();
}

17

Потрібно передати його як параметр XML.

Редагувати: швидкий код з мого проекту, щоб дати вам уявлення:

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)

Тоді ви можете використовувати таблицю темп, щоб з'єднатися зі своїми таблицями. Ми визначили arrayOfUlong як вбудовану XML-схему для підтримки цілісності даних, але вам цього не потрібно робити. Я рекомендую використовувати його, тому ось короткий код, щоб переконатися, що ви завжди отримуєте XML з longs.

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

Я думав, що було поганою ідеєю використовувати змінні таблиці, коли є багато рядків? Чи не краща ефективність замість цього використовувати таблицю temp (#table)?
ґендер

@ganders: Я б сказав навпаки.
абатищев

14

Контекст завжди важливий, наприклад, розмір і складність масиву. У списках малого та середнього розміру декілька відповідей, розміщених тут, є просто чудовими, хоча слід уточнити деякі пояснення:

  • Для розбиття обмеженого списку найшвидшим є спліттер на основі SQLCLR. Існує чимало прикладів, якщо ви хочете написати свій власний або просто можете завантажити безкоштовний SQL # бібліотеку CLR функцій (про яку я писав, але функція String_Split та багато інших абсолютно безкоштовні).
  • Розщеплення на основі XML масивів може бути швидким, але вам потрібно використовувати XML на основі атрибутів, а не XML на основі елементів (що є єдиним типом, показаним у відповідях тут, хоча приклад XML @ AaronBertrand є найкращим, оскільки його код використовує text()Функція XML. Для отримання додаткової інформації (тобто аналізу продуктивності) про використання XML для розділення списків, перегляньте "Використання XML для передачі списків як параметрів у SQL Server" від Phil Factor.
  • Використовувати TVP - це чудово (якщо припустити, що ви використовуєте принаймні SQL Server 2008 або новішу версію), оскільки дані передаються в прок. В більшості випадків зберігання всіх даних DataTableозначає копіювання даних у пам’яті під час копіювання з оригінальної колекції. Отже, використання DataTableметоду передачі в ТВП не працює добре для більших наборів даних (тобто не масштабує).
  • XML, на відміну від простих розмежених списків Ints або Strings, може обробляти більше, ніж одновимірні масиви, як і TVP. Але так само, як і DataTableметод TVP, XML не розширює масштаб, тому що більше, ніж в два рази збільшує розмір даних у пам'яті, оскільки йому необхідно додатково враховувати накладні витрати документа XML.

З урахуванням сказаного, якщо дані, які ви використовуєте, великі або не дуже великі, але постійно зростають, то IEnumerableметод TVP - найкращий вибір, оскільки він передає дані на SQL Server (як DataTableметод), але НЕ вимагають будь-якого дублювання колекції в пам'яті (на відміну від будь-якого з інших методів). У цій відповіді я розмістив приклад коду SQL та C #:

Передати словник у збережену процедуру T-SQL


6

Немає підтримки для масиву на сервері sql, але є кілька способів, за допомогою яких можна передати колекцію збереженій програмі.

  1. За допомогою даних
  2. Використовуючи XML.Try конвертуйте свою колекцію у формат XML та передайте її як вхід до збереженої процедури

Нижче посилання може вам допомогти

передача колекції у збережену процедуру


5

Я шукав усі приклади та відповіді, як передавати будь-який масив на sql-сервер без клопоту зі створення нового типу таблиці, поки я не знайшов цю посилання , нижче - як я застосував його до свого проекту:

- Наступний код отримає масив як параметр і вставить значення цього - масиву в іншу таблицю

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 

Сподіваюся, вам сподобається


2
@zaitsman: CLEANEST не означає найкращого або найдоцільнішого. Часто відмовляється від гнучкості та / або "відповідної" складності та / або продуктивності для отримання "чистого" коду. Ця відповідь тут "добре", але лише для невеликих наборів даних. Якщо вхідний масив @sє CSV, то це було б швидше просто розділити це (тобто ВСТАВИТИ У ... ВИБІРАТИ Із SplitFunction). Перетворення в XML відбувається повільніше, ніж CLR, і XML на основі атрибутів у будь-якому разі набагато швидший. І це простий список, але передається в XML або TVP також може обробляти складні масиви. Не впевнений, що отримується, уникаючи простого, разового CREATE TYPE ... AS TABLE.
Соломон Руцький

5

Це вам допоможе. :) Виконайте наступні кроки,

  1. Відкрийте конструктор запитів
  2. Скопіюйте Вставте наступний код таким, який він є, він створить функцію, яка перетворить String в 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. Створіть наступну збережену процедуру

     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. Виконайте цей SP, використовуючи exec sp_DeleteId '1,2,3,12'цей рядок, який потрібно видалити,

  5. Ви перетворюєте масив у рядок у C # і передаєте його як параметр Збережена процедура

    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 ;

Це видалить кілька рядків, Все найкраще


я використав цю функцію окремого розбору комами, вона буде працювати для невеликих наборів даних, якщо ви перевіряєте її план виконання, це спричинить проблему з великим набором даних, і де вам доведеться
множити

2

Мені знадобилося багато часу, щоб розібратися в цьому, тому, якщо комусь це потрібно ...

Це ґрунтується на методі SQL 2005 у відповіді Аарона та на його функції SplitInts (я щойно видалив параметр розділення, оскільки я завжди буду використовувати коми). Я використовую SQL 2008, але я хотів щось, що працює з набраними наборами даних (XSD, TableAdapters), і я знаю, що з ними працюють параметри строків.

Я намагався змусити його функцію працювати в пункті "де в (1,2,3)", і не маючи удачі прямолінійному шляху. Тому я створив спочатку таблицю темпів, а потім зробив внутрішнє з'єднання замість "де в". Ось мій приклад використання, в моєму випадку я хотів отримати список рецептів, які не містять певних інгредієнтів:

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)

Взагалі кажучи, найкраще не приєднуватися до змінної таблиці, а замість таблиці темп. За замовчуванням у змінних таблиці, як видається, є лише один рядок, хоча навколо цього є хитрість або два (див. Чудову та детальну статтю @ AaronBertrand: sqlperformance.com/2014/06/t-sql-queries/… ).
Соломон Руцький

1

Як зазначали інші, один із способів зробити це - перетворити масив у рядок і потім розділити рядок всередині SQL Server.

Станом на SQL Server 2016, існує вбудований спосіб розділити рядки під назвою

STRING_SPLIT ()

Він повертає набір рядків, які ви можете вставити у вашу тимчасову таблицю (або реальну таблицю).

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

принесе:

значення
-----
  123
  456
  789
  246
   22
   33
   44
   55
   66

Якщо ви хочете пофантазувати:

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

дасть вам ті ж результати, що і вище (за винятком назви стовпців "тоді число"), але відсортовано. Ви можете використовувати змінну таблиці, як і будь-яку іншу таблицю, так що ви можете легко приєднати її до інших таблиць у БД, якщо хочете.

Зверніть увагу, що для встановлення STRING_SPLIT()функції SQL Server установка сумісності повинна бути вище 130 або вище . Ви можете перевірити рівень сумісності за допомогою наступного запиту:

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

Більшість мов (включаючи C #) мають функцію "приєднання", яку можна використовувати для створення рядка з масиву.

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

Потім ви передаєте sqlparamяк свій параметр збережену вище процедуру.


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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.