Как передать массив в хранимую процедуру SQL Server


294

Как передать массив в хранимую процедуру SQL Server?

Например, у меня есть список сотрудников. Я хочу использовать этот список в качестве таблицы и объединить ее с другой таблицей. Но список сотрудников должен быть передан как параметр из C #.


сэр, надеюсь, эта ссылка поможет вам передать список / массив в SQL Server SP
Патрик Чой

Ответы:


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

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

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

3
@ th1rdey3 Они неявно необязательны. stackoverflow.com/a/18926590/61305
Аарон Бертран

44

Исходя из моего опыта, путем создания выражения с разделителями из employeeID, есть хитрое и хорошее решение для этой проблемы. Вы должны только создать строковое выражение, например, ';123;434;365;'in-which 123, 434и 365это несколько employeeID. Вызвав приведенную ниже процедуру и передав ей это выражение, вы можете получить нужные записи. Вы можете легко присоединить «другую таблицу» к этому запросу. Это решение подходит во всех версиях SQL-сервера. Кроме того, по сравнению с использованием табличной переменной или временной таблицы, это очень быстрое и оптимизированное решение.

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 спасибо :) На строковых ключах это также имеет хорошую производительность из-за отсутствия приведения столбцов int к varchar ...
Хамед Назактабар

Я опоздал на игру, но это очень умно! Отлично работает для моей проблемы.
user441058

Это потрясающе! Я обязательно воспользуюсь этим, спасибо
Омар Рудер

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 с длинными.

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

Я думал, что это плохая идея использовать переменные таблицы, когда есть много строк? Не лучше ли использовать вместо этого временную (#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 или новее), поскольку данные передаются в proc и отображаются предварительно проанализированными и строго типизированными как табличная переменная. ОДНАКО в большинстве случаев хранение всех данных DataTableозначает дублирование данных в памяти, когда они копируются из оригинальной коллекции. Следовательно, использование DataTableметода передачи в TVP не работает хорошо для больших наборов данных (т.е. не хорошо масштабируется).
  • XML, в отличие от простых разделенных списков Ints или Strings, может обрабатывать более чем одномерные массивы, как TVP. Но также, как и DataTableметод TVP, XML не масштабируется, так как он более чем удваивает размер данных в памяти, поскольку ему необходимо дополнительно учитывать накладные расходы XML-документа.

С учетом всего вышесказанного, ЕСЛИ данные, которые вы используете, являются большими или не очень большими, но все же постоянно растут, то IEnumerableметод TVP является лучшим выбором, так как он передает данные на SQL Server (как DataTableметод), НО не делает требуется любое дублирование коллекции в памяти (в отличие от любого другого метода). Я разместил пример кода SQL и C # в этом ответе:

Передача словаря в хранимую процедуру T-SQL


6

В sql server нет поддержки массива, но есть несколько способов передать коллекцию в сохраненный процесс.

  1. Используя датируемые
  2. Используя XML.Try, преобразуйте свою коллекцию в формат xml, а затем передайте ее в качестве входных данных для хранимой процедуры.

Ссылка ниже может помочь вам

передача коллекции в хранимую процедуру


5

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

Следующий код получит массив в качестве параметра и вставит значения этого массива в другую таблицу.

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: ЧИСТЫЙ не означает лучший или наиболее подходящий. Часто приходится отказываться от гибкости и / или «подходящей» сложности и / или производительности, чтобы получить «чистый» код. Этот ответ здесь «хорошо», но только для небольших наборов данных. Если входящий массив @sCSV, то было бы быстрее просто разделить это (то есть INSERT INTO ... SELECT FROM SplitFunction). Преобразование в XML медленнее, чем в CLR, а XML на основе атрибутов намного быстрее. И это простой список, но передача в XML или TVP также может обрабатывать сложные массивы. Не уверен, что получается, избегая простого, одноразового CREATE TYPE ... AS TABLE.
Соломон Руцкий

5

Это поможет вам. :) Следуйте следующим шагам,

  1. Откройте конструктор запросов
  2. Скопируйте Вставьте следующий код, как он есть, он создаст функцию, которая преобразует строку в 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 ;

Это удалит несколько строк, всего наилучшего


Я использовал эту функцию отдельного разбора запятых, она будет работать для небольшого набора данных, если вы проверите план выполнения, это вызовет проблему с большим набором данных и где вам придется несколько списков CSV в хранимой процедуре
Saboor Awan

2

Мне потребовалось много времени, чтобы понять это, поэтому на случай, если это кому-нибудь понадобится ...

Это основано на методе SQL 2005 в ответе Аарона и использовании его функции SplitInts (я просто удалил параметр delim, так как я всегда буду использовать запятые). Я использую 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

даст вам те же результаты, что и выше (за исключением того, что имя столбца «thenumber»), но отсортированы. Вы можете использовать переменную таблицы как любую другую таблицу, так что вы можете легко объединить ее с другими таблицами в БД, если хотите.

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

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.