Denis Gladkikh
Russian  |  English
rss twitter

08.06.2010 20:04 UTC

TSQL: Передача списка/коллекции/множества в хранимую процедуру

Передача множества в хранимую процедуру довольно-таки частая задача. Встречается, например, при фильтрации какой-нибудь коллекции. Так же это может быть импорт данных в базу данных из внешних источников. Я рассмотрю несколько вариантов, которые можно использовать в вашем приложении: склеивание SQL запроса, передача строки списка параметров, разделенных запятой, Bulk Insert, а так же table-valued parameters (самый интересный вариант, пришедший с MS SQL Server 2008).

Предположим у нас есть список товаров и нам нужно отфильтровать его в зависимости от некоторых категорий товаров (“Телевизоры”, “Игровые приставки”, “DVD-плееры” или списка фирм “Фирма 1”, “Фирма 2”, “Фирма 3”). Изобразим как это может выглядеть в нашей Базе Данных

Ну и чтобы совсем было понятно, накидаем приблизительно интерфейс, который обычно бывает в таких случаях:

Фильтр
Категории
  • Телевизоры
  • Игровые приставки
  • DVD-плееры
  • Холодильники
  • Пылесосы
Фирмы
  • Фирма 1
  • Фирма 2
  • Фирма 3
  • Фирма 4
  • Фирма 5
Товары
Товар Категория Фирма
Телевизор 32 Телевизоры Фирма 1
Пылесос Пылесосы Фирма 3
Игровая приставка Игровые приставки Фирма 5

То есть у нас есть запрос выводящий нам список товаров, и есть возможность отфильтровать его по категориям или по фирмам, причем фильтровать конечно же будем по идентификаторам. Задача ясна. Как же теперь ее решать? Самый просто способ, который используют Junior программисты – это склейка SQL инструкции в коде C#, примерно, это может выглядеть так

List<int> categories = new List<int>() { 1, 2, 3 };
 
StringBuilder sbSql = new StringBuilder();
sbSql.Append(
  @"
    select i.Name as ItemName, f.Name as FirmName, c.Name as CategoryName 
    from Item i
      inner join Firm f on i.FirmId = f.FirmId
      inner join Category c on i.CategoryId = c.CategoryId
    where c.CategoryId in (");
if (categories.Count > 0)
{
  for (int i = 0; i < categories.Count; i ++)
  {
    if (i != 0)
      sbSql.Append(",");
    sbSql.Append(categories[i]);
  }
}
else
{
  sbSql.Append("-1"); // It is for empty result when no one category selected
}
sbSql.Append(")");
 
string sqlQuery = sbSql.ToString();
 
DataTable table = new DataTable();
using (SqlConnection connection = new SqlConnection("Data Source=(local);Initial Catalog=TableParameters;Integrated Security=SSPI;"))
{
  connection.Open();
  using (SqlCommand command = new SqlCommand(sqlQuery, connection))
  {
    using (SqlDataAdapter dataAdapter = new SqlDataAdapter(command))
    {
      dataAdapter.Fill(table);
    }
  }
}
 
//TODO: Working with table

Для того, чтобы писать поменьше кода будем фильтровать только по категориям. В приведенном коде первая строка – это список идентификаторов категорий, которые выбрал пользователь (выбранные checkbox'ы), само собой нам нет необходимости хранить имена категорий, для фильтрации хватит и идентификаторов. Проблемы этого решения очевидны – в некоторых случаях подверженность SQL-инъекциям (например, в случае строк-идентификаторов, которые мы получаем с веб-формы - пользователь с легкостью может их подменить), не очень приятное сопровождение кода, при достаточно большом количестве категорий в фильтре строка запроса будет быстро расти. И еще одна проблема – такой код невозможно поместить в хранимую процедуру (можно конечно клеить запрос и на SQL сервере). Это решение можно назвать Решение 0, так как оно применяется либо из-за лени, либо потому что так быстро.

Решение 1. Строка – список значений, разделенных запятой

Все остальные варианты будут использоваться в связке с хранимыми процедурами. Первый вариант – это передача параметра – строки, которая состоит из списка идентификаторов, разделенных запятой, например так ‘1,2,3,4,’. Первое, что нужно сделать - это создать функцию, которая будет из этой строки создавать таблицу и возвращать ее, назовем данную функцию Split:

if object_id('Split') is not null 
    drop function split
go
 
create function dbo.Split
(
    @String int
)
returns @SplittedValues table
(
    Id varchar(50) primary key
)
as
begin
    declare @SplitLength int, @Delimiter varchar(5)
    
    set @Delimiter = ','
    
    while len(@String) > 0
    begin 
        select @SplitLength = (case charindex(@Delimiter,@String) when 0 then
            len(@String) else charindex(@Delimiter,@String) -1 end)
 
        insert into @SplittedValues
        select cast(substring(@String,1,@SplitLength) as int)
    
        select @String = (case (len(@String) - @SplitLength) when 0 then  ''
            else right(@String, len(@String) - @SplitLength - 1) end)
    end 
return  
end 

Теперь мы можем использовать эту функцию в нашей хранимой процедуре для поиска продуктов

if object_id('FindItems') is not null 
    drop proc FindItems
go
 
set ansi_nulls on
go 
set quoted_identifier on
go
 
create proc FindItems
(
    @categories varchar(max)
)
as
begin
    
  select i.Name as ItemName, f.Name as FirmName, c.Name as CategoryName 
  from Item i
    inner join Firm f on i.FirmId = f.FirmId
    inner join Category c on i.CategoryId = c.CategoryId
    inner join dbo.Split(@categories) cf on c.CategoryId = cf.Id
end 

Ну и соответственно C# код, при помощи которого мы сможем получить список продуктов:

List<int> categories = new List<int>() { 1, 2, 3 };
 
DataTable table = new DataTable();
using (SqlConnection connection = new SqlConnection("Data Source=(local);Initial Catalog=TableParameters;Integrated Security=SSPI;"))
{
  connection.Open();
  using (SqlCommand command = new SqlCommand("FindItems", connection) { CommandType = CommandType.StoredProcedure })
  {
    command.Parameters.AddWithValue("@categories", string.Join(",", categories));
    using (SqlDataAdapter dataAdapter = new SqlDataAdapter(command))
    {
      dataAdapter.Fill(table);
    }
  }
}
 
//TODO: Working with table

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

Решение 2. BULK INSERT

Проблемы, которые были в Решение 1 можно решить при помощи Bulk Insert – эта процедура будет копировать из серверного кода C# из объекта DataTable в экземпляр SQL приложения во временную таблицу данные, с которыми мы потом сможем работать. Давайте сначала перепишем нашу процедуру FindItems

if object_id('FindItems') is not null 
    drop proc FindItems
go
 
set ansi_nulls on
go 
set quoted_identifier on
go
 
create proc FindItems
as
begin
    if object_id('tempdb..#FilterCategory') is null 
    begin
        raiserror('#FilterCategory(id int) should be created', 16, 1)
        return
    end
    
    select i.Name as ItemName, f.Name as FirmName, c.Name as CategoryName 
    from Item i
        inner join Firm f on i.FirmId = f.FirmId
        inner join Category c on i.CategoryId = c.CategoryId
        inner join #FilterCategory cf on c.CategoryId = cf.Id
end 

Теперь эта процедура будет ожидать, что перед тем как ее будут использовать создадут временную табличку #FilterCategory, которую она уже будет использовать. Кода на C# нам придется писать побольше чем в прошлый, давайте создадим отдельный класс-репозиторий ItemsRepository

public class ItemsRepository
{
  public static DataTable FindItems(List<int> categories)
  {
    DataTable tbCategories = new DataTable("FilterCategory");
    tbCategories.Columns.Add("Id", typeof (int));
    categories.ForEach(x => tbCategories.Rows.Add(x));
 
    using (
      SqlConnection connection =
        new SqlConnection("Data Source=(local);Initial Catalog=TableParameters;Integrated Security=SSPI;"))
    {
      connection.Open();
      using (SqlTransaction transaction = connection.BeginTransaction())
      {
        try
        {
          string tableName = string.Format("tempdb..#{0}", tbCategories.TableName);
 
          CreateTableOnSqlServer(connection, transaction, tbCategories, tableName);
          CopyDataToSqlServer(connection, transaction, tbCategories, tableName);
 
          DataTable result = new DataTable();
          using (SqlCommand command = new SqlCommand("FindItems", connection, transaction)
                                        {CommandType = CommandType.StoredProcedure})
          {
            using (SqlDataAdapter dataAdapter = new SqlDataAdapter(command))
            {
              dataAdapter.Fill(result);
            }
          }
          transaction.Commit();
          return result;
        }
        catch
        {
          transaction.Rollback();
          throw;
        }
      }
    }
  }
 
  private static void CopyDataToSqlServer(SqlConnection connection, SqlTransaction transaction, DataTable table,
                                          string tableName)
  {
    using (SqlBulkCopy bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.Default, transaction)
                                    {
                                      DestinationTableName = tableName
                                    })
    {
      bulkCopy.WriteToServer(table);
    }
  }
 
  private static void CreateTableOnSqlServer(SqlConnection connection, SqlTransaction transaction, DataTable table,
                                             string tableName)
  {
    StringBuilder sb = new StringBuilder();
 
    sb.AppendFormat("create table {0}(", tableName);
    foreach (DataColumn column in table.Columns)
    {
      sb.AppendFormat("{0} {1} {2}",
                      table.Columns.IndexOf(column) == 0 ? string.Empty : ",",
                      column.ColumnName, GetSqlType(column.DataType));
    }
    sb.Append(")");
 
    using (SqlCommand command = new SqlCommand(sb.ToString(), connection, transaction))
    {
      command.ExecuteNonQuery();
    }
  }
 
  private static string GetSqlType(Type type)
  {
    if (type == typeof (string))
      return string.Format("{0}(max)", SqlDbType.VarChar);
    else if (type == typeof (int))
      return SqlDbType.Int.ToString();
    else if (type == typeof (bool))
      return SqlDbType.Bit.ToString();
    else if (type == typeof (DateTime))
      return SqlDbType.DateTime.ToString();
    else if (type == typeof (Single))
      return SqlDbType.Float.ToString();
    else throw new NotImplementedException();
  }
}

Метод FindItems создает объект DataTable, записывает в него список идентификаторов категорий, по которым хотим отфильтровать, дальше метод открывает новую транзакцию, создает на сервере временную табличку #FilterCategories, копирует содержимое DataTable в эту таблицу и вызывает хранимую процедуру FindItems. Замечу, что временные таблицы tempdb..#<TableName> живут только в определенном Scope, в нашем случае это транзакция (потому если несколько пользователей вызовут этот метод в один момент, то ничего страшного не будет и они друг другу не помешают), и потому что таблица живет только на время жизни транзакции, то и удалится она при завершении транзакции (правда, все равно рекомендуют удалять временные таблицы самим именно тогда, когда она уже вам не нужна).

Я этот подход особенно часто использовал при импорте данных из внешних источников, вроде Excel файлы или какие-нибудь другие.

Давайте найдем минусы данного подхода. Минус “больше кода” сразу выбрасываем, так как это все можно зарефакторить и вынести в специальные классы во внутренний фреймворк и забыть. Другие минусы – это лишние создания временных таблиц, ну и соответственно лишние запросы к базе данных. Так же могут быть проблемы, если внутри одной хранимой процедуры запускаете другую, которая может сама создает временную таблицу с таким же именем, либо когда происходит рекурсивный вызов. Еще недостаток данного подхода в тестировании самой процедуры, в смысле в работе с ней из Management Studio, нужно постоянно писать скрипт для создания временной таблицы (а это еще нужно вспомнить какая у нее структура, да как называется).

create table #FilterCategory(id int)
insert into #FilterCategory ( id ) values  ( 1  )
insert into #FilterCategory ( id ) values  ( 2 )
insert into #FilterCategory ( id ) values  ( 3  )
insert into #FilterCategory ( id ) values  ( 4  )
 
exec FindItems
 
drop table  #FilterCategory

Решение 3. Table-Valued Parameters (Database Engine)

И последнее решение – это использование table-valued parameters (о которых к сожалению я узнал не так давно, надо внимательнее смотреть What’s new в новых версиях продуктов, которые мы используем). Этот подход очень похож на BULK-INSERT, только немного упрощает его. Использовать его можно с базами данных MS SQL 2008 и выше. Опять переписываем процедуру FindItems, не забываем создать тип-таблицу Identifiers

if object_id('FindItems') is not null 
    drop proc FindItems
go
 
if exists(select * from sys.types where name = 'Identifiers')
    drop type Identifiers
go
 
create type Identifiers AS TABLE 
( id int primary key);
go
 
set ansi_nulls on
go 
set quoted_identifier on
go
 
create proc FindItems
(
    @categories Identifiers readonly
)
as
begin
    select i.Name as ItemName, f.Name as FirmName, c.Name as CategoryName 
    from Item i
        inner join Firm f on i.FirmId = f.FirmId
        inner join Category c on i.CategoryId = c.CategoryId
        inner join @categories cf on c.CategoryId = cf.Id
end 
go

Ну и переписываем теперь серверный код

List<int> categories = new List<int>() { 1, 2, 3 };
 
DataTable tbCategories = new DataTable("FilterCategory");
tbCategories.Columns.Add("Id", typeof(int));
categories.ForEach(x => tbCategories.Rows.Add(x));
 
DataTable table = new DataTable();
using (SqlConnection connection = new SqlConnection("Data Source=(local);Initial Catalog=TableParameters;Integrated Security=SSPI;"))
{
  connection.Open();
  using (SqlCommand command = new SqlCommand("FindItems", connection) { CommandType = CommandType.StoredProcedure })
  {
    command.Parameters.AddWithValue("@categories", tbCategories);
    using (SqlDataAdapter dataAdapter = new SqlDataAdapter(command))
    {
      dataAdapter.Fill(table);
    }
  }
}
 

Он стал намного проще, чем был с Bulk Insert и работать в Management Studio с процедурой стало чуть-чуть попроще

declare @categories Identifiers
 
insert into @categories ( id ) values  ( 1  )
insert into @categories ( id ) values  ( 2 )
insert into @categories ( id ) values  ( 3  )
insert into @categories ( id ) values  ( 4  )
 
exec FindItems @categories

У table-valued parameters есть некоторые ограничения, вроде того, что данные параметры всегда должны быть readonly. По поводу производительности в сравнении с Bulk Insert в этой статье Table-Valued Parameters (Database Engine) приводится таблица, в которой поясняется когда лучше использовать table-valued parameters, а когда Bulk Insert. А в целом когда какой подход выбирать – решать вам.

.NETTSQLSQL ServerTransact SQLBulk InsertTable-Valued ParametersStored Procedures

Вас также может заинтересовать

rss twitter

Комментарии (73)

Дмитрий Пялов ( 08.06.2010 20:19 UTC ) #
avatar
Интересная сводка, но хотелось бы рассмотреть еще одно решение - параметры типа XML, которое так же доступно в SQL Server 2005

Особенно интересно, какая разница в производительности между Table-Valued Parameters и XML Parameters
Denis Gladkikh ( 08.06.2010 20:31 UTC ) #
avatar
Дмитрий, интересное замечание, спасибо, про XML Parameters я что-то позабыл, а точнее использовал их, если не изменяет память, всего один раз. Если будет время постараюсь их тоже рассмотреть и дополнить эту статью.
Максим ( 08.06.2010 20:37 UTC ) #
avatar
Да, XML определенно стоит рассмотреть. Ибо с его использованием кода как на C#, так и на SQL меньше.
force ( 08.06.2010 20:53 UTC ) #
avatar
C XML обычно много возни с подготовкой, но зато дубовый и эффективный вариант.

bulk insert, насколько я помню ещё дополнительных прав требует к базе, которых может и не быть, но вариант самый эффективный для множества однотипных данных, например активного логописания.

ЗЫ: А вообще приколько выглядит как с каждым более правильным вариантом количество кода растёт просто безумно :)
ЗЗЫ: В пером варианте с помощью линковского Convert можно очень сильно улучшить читаемость (убрать некрасивый цикл)
Vasiliy Novikov ( 08.06.2010 21:26 UTC ) #
avatar
Есть ещё один экзотический способ - SQLCLR - т.е. создать CLR User defined type, который будет инкапсулировать список значений и CLR-функцию, которая будет его разворачивать.
Что касается производительности - проводил небольшое тестирование, общие выводы:
1. На малых размерах списков выигрывает решение, описанное в самом начале статьи - склейка SQL инструкции.
2. На больших объёмах данных соответсвенно выигрывают Table-valued параметры, XML, SQLCLR (другие способы не тестировал), при чём между собой они поделили следующие места (первый - лучший): SQLCLR, Table-valued параметры, XML
Также поэксперементировал с оптимизацией формирования Table-valued параметра, т.к. DataTable сам по себе несёт некоторый оверхед, а именно вручную реализовывал DbDataReader. Ускорение производительности на графиках было заметно, но слабо, зато нарузка на GC снизилась более заметно
Все тесты и результаты очень относительны, по-этому думаю, что не стоит приводить конкретные цифры
Monsignor ( 09.06.2010 02:56 UTC ) #
avatar
А будет ли дополнение к статье с примерами XML и SQLCLR?
Scratch ( 09.06.2010 03:15 UTC ) #
avatar
Люто реквестирую тест XML варианта
Denis Gladkikh ( 09.06.2010 04:07 UTC ) #
avatar
force, мне тоже казалось всегда что c xml только возни больше, но как оказалось - народ действительно использует xml-parameters.

force, если можешь приведи, плз, пример как можно ни Linq склеить categories, у меня что-то никак не вышло придумать как с запятыми справиться.

Vasiliy Novikov спасибо SQLCRL тоже интересный вариант.

Monsignor, Scratch попозже сделаю дополнение к статье с xml-parameters и SQLCRL и попробую все протестировать на объемах данных вроде 10, 100, 1000, 10000 - посмотрим что будет лучше.
DeAmon ( 09.06.2010 04:14 UTC ) #
avatar
Можете все свои варианты "сжечь" или отдать в учебники.

// это входящая строка в вызове процедуры
DECLARE @str varchar (100)
SET @str = '|100|200|300|1001|900|'
 
SELECT
  *
FROM
 [order]
WHERE
  @str like '%|' + cast (order_id as varchar) + '|%'

Тестировалось на миллионных таблицах. Работает быстро. Очень быстро. Индексы все используются.
DeAmon ( 09.06.2010 04:27 UTC ) #
avatar
передать два набора данных то же можно с помощью этого способа

// это входящая строка в вызове процедуры
DECLARE @str varchar (100)
SET @str = '|100|200|300|1001|900|(привет)(пока)(надоже)'

// используем первый набор
SELECT
*
FROM
[order]
WHERE
@str like '%|' + cast (order_id as varchar) + '|%'

//используем второй
SELECT
*
FROM
[order]
WHERE
@str like '%(' + cast (order_name as varchar) + ')%'
Denis Gladkikh ( 09.06.2010 04:29 UTC ) #
avatar
DeAmon, а если @str будет содержать 100 индексов? А что если нужно будет выбирать композитными ключами? А что если выбирать нужно будет не по целочисленным, а по строковым значениям (в которых может быть любой символ)?
DeAmon ( 09.06.2010 04:40 UTC ) #
avatar
*а если @str будет содержать 100 индексов?
размер параметра может быть очень большой, помоему 2 Гб. Ничего страшного. Кстати интересно посмотреть на веб форму, где 100 раз галочек поставить.

*А что если нужно будет выбирать композитными ключами?
а в чём проблема?

*А что если выбирать нужно будет не по целочисленным, а по строковым значениям (в которых может быть любой символ)?
разделитель может быть ЛЮБОЙ. Если совсем плохо предварительно можно зареплейсить в спецсимвол и обратно перереплейсить.

А вообще в приведённом примере никаких таких вещей не требуется. Задача классическая и применяемая в 99%.
DeAmon ( 09.06.2010 04:57 UTC ) #
avatar
Но соглашусь что для передачи НЕ ОПРЕДЕЛЕННОГО набора данных (например таблицы с неизвестным или не оговорённым количеством строк) - метод не пойдёт. Тогда только выше обозначенные. Но такие задачи весьма неюзабильные 8-\. Чаще такие как в примере.
force ( 09.06.2010 05:04 UTC ) #
avatar
Denis Gladkikh про склейку: как-то так:
String.Join(",", categories.ConvertAll(c => c.ToString()).ToArray())
zandroid ( 09.06.2010 05:12 UTC ) #
avatar
Первый вариант (склейка запроса) можно переделать из строки в склейку запроса на Linq2Sql, тогда уйдут проблемы с инъекциями, подготовку запроса до его выполнения можно будет передавать между несколькими методами, будет компилируемость (с проверкой типов и корректности операторов) и пр... НО надо будет прикручивать Linq2Sql.

ЗЫ
В таком случае можно использовать как коллекцию (что приведено у автора), так и множество условий (... where ... and ... and ...)
Denis Gladkikh ( 09.06.2010 05:12 UTC ) #
avatar
force, точно! Join использую же в след примере, а тут ступор какой-то был :)
Denis Gladkikh ( 09.06.2010 05:17 UTC ) #
avatar
DeAmon, ну в принципе конечно можно зарезервировать символы, но один раз такие зарезервированные символы меня обожгли. У меня просто сомнения в этом методе - напишу тесты и сравню. Спасибо.
Nezumikun ( 09.06.2010 05:41 UTC ) #
avatar
Кстати метод с LIKE можно использовать не только в запросах, если в процедуре есть какая-то логика с ветвлением, завязанным на входные параметры (например, режимы работы процедуры). Удобно :)
Сергей ( 09.06.2010 06:26 UTC ) #
avatar
Дорогой Автор!
А откуда тебе стало известно о вышеописанных четырех подходах?
Это всё -- личный опыт, либо какие-то прочитаны в других статьях?
Fox ( 09.06.2010 06:30 UTC ) #
avatar
Вариант "@str like '%|' + cast (order_id as varchar) + '|%'" конечно забавный и полезный. НО насчет "Индексы все используются." вы к сожалению не правы.

Сиквел не настолько умный, чтобы использовать индексы после cast и конкатенации, что легко проверяется анализом плана выполнения.


Вообще, выбор варианта сильно зависит от соотношения количества записей в таблице и количества ключей для поиска. В случае когда таблица большая (например как у меня около 3М записей) а количество ключей небольшое (до сотни) то очень хорошо работает "Решение 1" - потому что накладные расходы на парсинг получаются сильно меньше чем на сам поиск.


Чтобы не быть голословным - таблица 2789334 записей. Ключ - типа uniqueidentifier. Запрос с

INNER JOIN dbo.ParmsToList('3F530CBD-27BA-4746-BC81-0ACF270FFD77,15B5CDCE-A9F0-4E22-877D-DF1F3AD332B1') T ON T.Value = ПСР.RowPointer

выполняется 271 миллисекунду. А запрос с
where @param like '%(' + cast(RowPointer as nvarchar(38)) + '%)' (параметр задан как set @param = '(3F530CBD-27BA-4746-BC81-0ACF270FFD77)(15B5CDCE-A9F0-4E22-877D-DF1F3AD332B1)') выполняется 15 секунд!
Что называется - почувствуйте разницу. Разумеется, по полю RowPointer есть индекс...

P.S. для чистоты эксперимента сделал поле типа varchar наложил на него индекс и попробовал выражение с like - индекс все равно НЕ используется даже без cast - видимо конкатенация не дает сиквелу такой возможности.
enrey ( 09.06.2010 06:31 UTC ) #
avatar
Третий пример (Table Parameters) очень крут. Век живи,как говорится... Надо обязательно попробовать.
К сожалению, sql server 2008 ...
На sql server 2000 (!) мы решаем эту проблему используя xml , так что комментарий выше про xml в 2005 не совсем верен. Предлагаю дополнить ваш интересный пост. Выглядит примерно так:

CREATE procedure [add_Cards]
@Parent_id int,
@QParams text
as
begin

DECLARE
@idoc int

EXEC sp_xml_preparedocument @idoc OUTPUT, @QParams

SELECT Card_id FROM OPENXML (@idoc, '/Root/Item',1) WITH (Card_id int)

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

EXEC sp_xml_removedocument @idoc
END


Ну а в C# соответственно так:


StringBuilder sb = new StringBuilder();

sb.Append("");

sb.Append("");

foreach (MyStruct sn in MyList)
{
sb.Append(" sb.Append(sn.MyId);
sb.Append("\" />");
}

sb.Append("");

SqlCommand spp = ...
spp.Parameters["@QParams"].Value = sb.ToString();

Передавал 100 000 id-ников. Работает очень быстро :)
XOR ( 09.06.2010 06:46 UTC ) #
avatar
На мой взгляд, вариант DeAmon'а кардинально неверен. да, индексы могут использоваться, но делается Index Scan. Т.е. бежим по индексам, и каждый ключ оттуда сравниваем со входной строкой. В принципе, это немногим лучше, чем Full Table Scan. А вот если передавать таблицу или XML, то будет (Clustered) Index Seek - что нам и надо.

А еще есть классический FAQ про это. Table-valued там не было, когда я его читал последний раз. Там по всем тестам выигрывал XML-вариант. SQLCLR где-то вышел боком.
Вячеслав ( 09.06.2010 06:59 UTC ) #
avatar
а если так через LINQ?

List catIds = new List {1,2,3 };// коллекция идентификаторов категорий
DataContext dc = new DataContext();
List productlist = (from a in dc.products where catIds.Contains(a.CategoryID) == true select a).ToList();
Denis Gladkikh ( 09.06.2010 07:18 UTC ) #
avatar
Сергей все личный опыт, потому тут xml например нет, потому как было один раз, когда действительно работали с xml

Fox спасибо, так и думал что в варианте DeAmon так и будет, хотел отложить для тестирования, которое проведу когда будет время.

enrey, спасибо - отличное применение. Вообще работа с xml очень хорошо описана в книжках Кена Хендерсона.

Вячеслав нужно смотреть что этот Linq склеит, есть подозрение что нулевой вариант (ну или приближенный к нему, типа "(... and ... and ... and ... )").
Евгений Веприков ( 09.06.2010 07:35 UTC ) #
avatar
http://www.sql.ru/forum/actualthread.aspx?bid=1&tid=363122&hl=
Denis Gladkikh ( 09.06.2010 07:37 UTC ) #
avatar
enrey sql 2000 серверу уже больше 10 лет (или около того), вот живучая скотина :) Не переходите на более новую версию из-за лицензий? Или просто по правилу "Работает - не трожь!".
Сергей ( 09.06.2010 08:44 UTC ) #
avatar
Дорогой Автор!

Перед тем, как что-то изобретать, даже если это сделано на основании собственного опыта, разумно исследовать уже существующие подходы для решения проблемы. Или в университете так не учат?

Если бы Вы потратили хотя бы 5 минут на поверхностный обзор существующих решений, поискав в google по запросу вроде такого: "sql stored procedure pass array", то уже второй сверху поисковый результат привел бы вас к "Xml-подходу", который у Вас не рассмотрен.

На мой взгляд, это сводит на нет ценность тех усилий, которые Вы приложили, чтобы сделать свой обзор.
Denis Gladkikh ( 09.06.2010 09:38 UTC ) #
avatar
Уважаемый, Сергей. Пускай так - это ваше мнение. Я не утверждаю что у меня 100% покрытие темы, дополним доделаем. Интересны тесты - сделаем не проблема. А раз я не написал про xml - это прям как вас обидел, там вторым пунктом ваша статья что ли? :) А то, что я написал, найдет своего читателя - не беспокойтесь. ;)

P.S. Сергей, любую статью можно дополнить и допилить для этого как раз и сделаны комментарии, а занудствовать тут не к чему.
Сергей ( 09.06.2010 10:23 UTC ) #
avatar
Денис,

Ваша защитная реакция мне понятна, но выглядит как оправдания, не более того.
Eugene ( 09.06.2010 10:25 UTC ) #
avatar
http://www.sommarskog.se/arrays-in-sql.html
Makc ( 09.06.2010 10:33 UTC ) #
avatar
Спасибо, отличная статься.

От себя добавлю, что удаление объекта в таком виде:

if object_id('Split') is not null
drop function split
go

Может подвести, т.к. 'Split' - не всегда может быть функцией. Поэтому у себя я добавляю еще проверку на тип объекта.
P.S. а универсального drop-а не существует? Все, что находил - это определение функции, которая по имени объекта определяет его тип и выполняет соответствующую функцию (например, как здесь).
Andrey ( 09.06.2010 10:44 UTC ) #
avatar
Сергей, а что изобрёл Денис? Он нашёл новый для него подход и поделился, а то часто "мужики-то не знают" :) Вполне нормальная и полезная статья.
Denis Gladkikh ( 09.06.2010 10:59 UTC ) #
avatar
Сергей, ха-ха. Вы действительно думаете, что мне есть интерес перед вам оправдываться? :) Не льстите себе. Я всегда отношусь с уважением к критике.

Eugene, спасибо! Отличное дополнение, изучу при возможности.

Makc, согласен, редко но такое может быть - а) лучше использовать в имени и схемы б) нотация тоже бывает полезна.
Сергей ( 09.06.2010 12:15 UTC ) #
avatar
Денис,

>Вы действительно думаете, что мне есть интерес перед вам оправдываться? :)

Нет, не думаю, что передо мной

>Я всегда отношусь с уважением к критике.

Оправдания и уважительное отношение к критике не противоречат друг другу
Сергей ( 09.06.2010 12:24 UTC ) #
avatar
Andrey,

>Сергей, а что изобрёл Денис?

Мне стоило написать это слово в кавычках, чтобы не возникало вопросов.

Вообще, если подобные статьи по каждому из приведенных подходов уже существуют, то почему бы не привести на них ссылки? В противном случае создается впечатление, что анализ существующих подходов вообще не производился. (Здесь я уже начинаю повторяться)

> Вполне нормальная и полезная статья.
Я не отрицаю того факта, что кому-то она и полезна, например Вам, Andrey
Vladislav ( 09.06.2010 12:32 UTC ) #
avatar
Спасибо за статью.

Вопрос: Кроме DataTable, в качестве параметра для Table-Value варианта, ничего использовать нельзя?

Заметка на тему склейки

List categories = new List() { 1, 2, 3, 4, 5 };
categories.Select(x => x.ToString()).Aggregate((x, y) => x + "," + y);
Denis Gladkikh ( 09.06.2010 13:03 UTC ) #
avatar
Vladislav, отсюда: System.Data.SqlClient supports populating table-valued parameters from DataTable, DbDataReader or System.Collections.Generic.IEnumerable<SqlDataRecord> objects.

Сергей, ок, давайте на этом и закончим.
Andrey ( 09.06.2010 13:57 UTC ) #
avatar
Денис, позволь выскажу своё последнее замечание Сергею. Сергей, лично мне эта статья не полезна, т.к. я сам про это писал ещё в сентябре 2008. Но считаю полезным простые и иногда элементарные вещи повторять. Ведь новичков всё больше и больше. А старички часто по старинке работают.
aek ( 09.06.2010 15:24 UTC ) #
avatar
Кстати, еще одна штуку, кажется, никто не упомянул. Табличные переменные как параметры в хранимой процедуре имеют еще один существенный аспект - для них не поддерживается статистика. Это значит, что при построении плана выполнения SQL Server всегда будет считать, что передается 1 значение. Для случаев, когда выгодно получить разные планы для разных параметров, это может быть существенным ограничением.
Dmitry ( 09.06.2010 16:06 UTC ) #
avatar
XML определённо красивее. Плюс, это универсально, в MySQL тоже можно распарсить его.
igor ( 09.06.2010 16:08 UTC ) #
avatar
>>И еще одна проблема – такой код невозможно поместить в хранимую процедуру (можно конечно клеить запрос и на SQL сервере). Это решение можно назвать Решение 0, так как оно применяется либо из-за лени, либо потому что так быстро.

разве нельза поместить? сделать нечто типа:

CREATE PROCEDURE new_proc
@categories VARCHAR(MAX)
....
SELECT * FROM some_table WHERE id IN (@categories);
....
DeColores ( 09.06.2010 16:26 UTC ) #
avatar
Вставлю 5 копеек, как SQL Server DBD:
В том, что в проверке на существование функции Split не указан тип объекта - нет ничего страшного. Ситуация, когда на одной БД возникло пересечение имен объектов разных типов плоха настолько, что никакой проверкой типа не поможешь. :)
Плохо то, что функция split реализована неоптимально. В инете много решений, работающих существенно быстрее, например с использованием опорной таблицы чисел.
Да и в примерах кода для CLR от самого Microsoft есть готовая функция (файл split.cs, класс StringSplitter, всего 50 строк, 2/3 из них - комментарии), нужно только открыть проект в VS и сделать Deploy.

Касаемо использования XML - да, в общем случае это лучший вариант. Два года назад перешел на него (передача параметров в процедуры), больше ничего не хочу. :)
Конечно, XML более требователен к памяти и процессору, что вполне понятно. Да и "нужно уметь готовить" в том плане, что каждый раз приходится выбирать, что быстрее - OPENXML или XQuery.
Denis Gladkikh ( 09.06.2010 16:50 UTC ) #
avatar
igor, так работать не будет.

DeColores, спасибо за 5 копеек :)
DeAmon ( 10.06.2010 03:28 UTC ) #
avatar
*Касаемо использования XML - да, в общем случае это лучший вариант.

Помоему, общий случай - это совсем не лучший случай. Идя в угоду универсализму подхода, начинаем терять на избыточности кода. Зачем для простых задач со статичными справочниками несколько сотен тысяч записях и простыми запросами с фильтрами тащить XML нагромаждения? В угоду продвижения технологий?
Был озвучен классический пример, но начали приплетать какие то XML, CLR, Table-Valued подходы - для чего? Что бы сделать простое сложным? Или из-за того что изначально не сделанно правильно, теперь "нахлабучиваем"? С другой стороны я совершенно понимаю программистическое желание - сначала что то во что то положить, потому это где то использнуть 8). Но то ли это место? ;)

Помоему, XML метод нужно применять в "забавно" исключительных случаях, например с изменямыми и неописанными формами - карточек "чего то либо", ну например открывается форма с кучей полей и табл.частей и надо по-изменять и нажать кнопку "Сохранить". И то данные формы являются скорее исключением из правил и их нужно избегать при "архитектурения", тем более в вебе.
Denis Gladkikh ( 10.06.2010 06:11 UTC ) #
avatar
DeAmon, по мне так xml нужно применять тогда, когда xml, либо в случае если действительно пишешь ПО для нескольких БД. А так мне этот подход пока не очень ясен, чем же он так хорош. Посмотрим, попробуем.
ddd ( 10.06.2010 06:27 UTC ) #
avatar
А чего никто не хочет сделать проще?
И высокие материи не нужны и по индексу бегает правильно.

declare @s varchar(8000)
declare @t table (id int primary key clustered)

set @s='27427,27428,27429,27430,27431'

insert into @t
exec ('select ключ from таблица where ключ in ('+@s+')')

select * from @t
DeAmon ( 10.06.2010 06:57 UTC ) #
avatar
*если действительно пишешь ПО для нескольких БД.

причём тут XML? 8-\ вообще не вижу связи. Или кроссплатформенный текстовый файл это круто? А парсеры XML-евские во всех БД в сохранёнках существуют?
Дмитрий Пялов ( 10.06.2010 06:59 UTC ) #
avatar
DeAmon,

Вот банальный пример из практики - в нашем продукте есть индексатор для полнотекстового поиска - на веб-сервере индексируемый контент бьется на стемы, затем стемы через XML (с примитивнейшей структурой) вместе с другими параметрами передаются в хранимую процедуру, которая раскладывает их по полочкам.

Количество стемов в одном элементе контента, естественно, не определено.

И как бы вы передавали эти данные на сервер?
Fox ( 10.06.2010 07:01 UTC ) #
avatar
ddd А так делать никто не хочет потому, что это готовый SQL-Injection

Кроме того - попробуйте запустить эту конструкцию если в качестве ключей в @s будут строки содержащие запятые, кавычки и прочие приятные символы.

Еще один "побочный эффект" - так как запрос компилируется каждый раз - то во первых это само по себе приличные затраты (при большом числе вызовов), а во вторых не будет работать оптимизатор/статистика.

Еще 5 копеек по поводу xml. Решение само по себе неплохое - но не пытайтесь совать его везде без разбору. Парсинг xml штука затейливая и иногда можно получить падение производительности на порядок "на ровном месте". Например - с целочисленными ключами запрос работает 0,5 сек. А тот же запрос но по ключу decimal или, скажем, unuqueidentifier вдруг дает 5сек.
Да и формировать xml на клиенте не в пример сложнее (в "Дельфе" выгрузка в comma-delimited это просто Параметр := TStrings.DelimitedText. А выгрузка в xml это уже целая история с географией...
ddd ( 10.06.2010 07:22 UTC ) #
avatar
ниче я не понимаю в ваших высоких материях,
потому, что это готовый SQL-Injection
что можно злого вставить в текстовый параметр со списком ключей?

попробуйте запустить эту конструкцию если в качестве ключей в @s будут строки содержащие запятые

тоже не понимаю в чем проблема - текст должен быть в кавычках. Вы вообще запросы писали когда нить?

да, exec не очень оптимально, но зато как удобно.
обработать оптимизатору его будет совсем несложно.
Fox ( 10.06.2010 07:53 UTC ) #
avatar
ddd Ооо! Вы не поверите, но вставить в список ключей можно ЧТО УГОДНО.

Просто приведу пример (учтите - это только один из вариантов - самый простой):

declare @s nvarchar(max)

set @s = '1);select * from ТаблицаСПаролями where 1 in(1'

exec ('SELECT * FROM Заказы where КодЗаказа in('+@s+')')

Рекомендую сходить почитать например вот это: http://ru.wikipedia.org/wiki/%D0%92%D0%BD%D0%B5%D0%B4%D1%80%D0%B5%D0%BD%D0%B8%D0%B5_SQL-%D0%BA%D0%BE%D0%B4%D0%B0
Тут, конечно, только основы. Зато написано неплохо.

"обработать оптимизатору его будет совсем несложно."
Конечно, если у вас запрос к одной таблице по одному полю - то не сложно. А если это join 5-7 таблиц? Для запросов оформленных в виде ХП оптимизатор сохраняет планы - это раз. Два - для ХП ведется статистика (по используемым ключам и т.п.) и если вы вызываете одну ХП несколько раз - то вы обнаружите, что время исполнения (при разных параметрах!) будет уменьшаться от первого и где-то до 5-го вызова. И разница будет заметна невооруженным глазом. Иногда "в разы" - спасибо оптимизатору.

А вот динамический SQL оптимизатору не по зубам. И планы он будет строить каждый раз. И статистику ему хранить просто негде (точнее не к чему привязать).
Это не значит что сиквел вообще не будет пытаться что-то сделать для оптимизации. Будет. Но вы связываете ему руки.

Но все-таки оптимизация это мелочь. Если у вас не миллионы записей и не куча таблиц в запросе этим можно пренебречь. А вот безопасностью пренебрегать НЕЛЬЗЯ. НИКОГДА.

Вы можете сказать "а кому это надо, меня взламывать". Или "да ну, в этом проекте это не важно". Но! Вы думаете спустя пару лет вы будете анализировать и переделывать свое решение для другого более серьезного проекта? Помилуйте, вы его просто скопипастите!
Любой программист использует ранее написанный код. И "легкий" но не качественный код имеет дурную привычку тиражироваться в таком количестве копий, что диву даешься...
DeAmon ( 10.06.2010 07:57 UTC ) #
avatar
*через XML (с примитивнейшей структурой) вместе с другими параметрами передаются в хранимую процедуру.

В принципе пойдёт предложенный мною способ с LIKE. Один из параметров вызова хранит набор стемов. Единственное что можно уткнутся в Index Scan по char-у. Но это надо рассматривать ситуацию на практике.
SIM ( 10.06.2010 08:07 UTC ) #
avatar
Как вариант есть еще Common Table Expression (CTE), или в данном случае Recursive CTE:


DECLARE @str VARCHAR(MAX)
SET @str = '|100|200|300|100|900|'

WITH GetID (id, pos) AS
(
SELECT
id = CAST(NULL AS INT),
pos = 2
UNION ALL
SELECT
ID = CAST(SUBSTRING(@str, g.pos, 3) AS INT),
POS = CAST(CHARINDEX('|', @str, g.pos) AS INT) + 1
FROM
GetID g
WHERE
g.pos < LEN(@str)
)

SELECT *
FROM sometable t INNER JOIN GetID g ON t.id = g.id;


Но к сожалению, я только начал постигать CTE и пока это решение мне кажется каким-то неестественным.
ddd ( 10.06.2010 08:11 UTC ) #
avatar
Не убедили. Пароли вы никак не получите, потому что я вставляю перечень ключей в табличную переменную и потом буду ее использовать.

Джойнить 5-7 таблиц - тоже не напугали. В чем сложность то.

Если сильно хочется оптимизатор, то можно сначала моим методом заполнить временную табличку (или несколько) а потом вызвать свою мегапроцедуру по поиску. Чем вам xml то упрощает жизнь объясните?
ddd ( 10.06.2010 08:15 UTC ) #
avatar
с with тоже очень удачный пример - одобрям.
Сиквел почему то с ним очень быстро работает.
SIM ( 10.06.2010 08:19 UTC ) #
avatar
в примере с WITH первой строкой излишне выводится NULL, но пока я не осознал толком как правильно написать.

про XML: лично я люблю им пользоваться, но пугает строчка в BOL в описании sp_xml_preparedocument (TSQL):

...
Note:
A parsed document is stored in the internal cache of SQL Server. The MSXML parser uses one-eighth the total memory available for SQL Server. To avoid running out of memory, run sp_xml_removedocument to free up the memory.
...
ddd ( 10.06.2010 08:32 UTC ) #
avatar
С WITH есть засада:
The statement terminated. The maximum recursion 100 has been exhausted before statement completion.
Fox ( 10.06.2010 08:48 UTC ) #
avatar
dddНе убедили. Пароли вы никак не получите, потому что я вставляю перечень ключей в табличную переменную и потом буду ее использовать

Гммм... Ну я же написал - это ПРОСТЕЙШИЙ пример.
Вы что же, не верите, что Ваше решение можно обойти или перепрыгнуть? Вот еще пример (последний! - я не буду опровергать каждый ваш вариант - просто помните - что на каждую хитрую жопу есть...):

declare @s nvarchar(max)

declare @t table (id int primary key clustered)

set @s = '1);declare @Пароль nvarchar(128) SELECT @Пароль=Пароль FROM Пароли raiserror(@Пароль, 14, 1)--'

insert into @t
exec ('SELECT Код FROM Заказы where Код in('+@s+')')


Принцип-то простой. Если вы допускаете исполнение пользовательского кода (а exec это оно и есть - как его склоняй) - то Вас рано или поздно хакнут. И будет ОЧЕНЬ неприятно. Потому как придется ПЕРЕДЕЛЫВАТЬ весь код. Не просто поправлять - а именно переделывать, архитектурно.

P.S. Кстати - получение паролей это как-бы и не очень нужно. Это я только для примера. Потому как нормальные люди все равно их не хранят - а хранят, как минимум, хэши.
А вот нагадить в базе - или вписать какие-нибудь свои данные (например занести себя в список администраторов ;-) ваша конструкция не мешает вообще.
Добавьте в строку "insert into ATest select 1;" и насладитесь результатом. DELETE, кстати, сработает ничуть не хуже.
Да и DDL-код по моему тоже отработает - но проверять не буду. И этого хватит...
Denis Gladkikh ( 10.06.2010 08:59 UTC ) #
avatar
ddd, "The maximum recursion" раньше вроде был меньше 100, около 30 или 40. Или это вы уже сами дефолт изменили?

А по поводу паролей, вам же сказали что пример самый простой. Сделает "insert into Заказы(..) select * Пароли " так чтобы эти заказы потом посмотреть. Либо вообще просто захотят у вас все сломать и просто сделают "Drop Пароли". Найти метод всегда можно.
Denis Gladkikh ( 10.06.2010 19:33 UTC ) #
avatar
Ага, вот и заключение Тесты методов передачи списковых переменных в хранимую процедуру MS SQL 2008, хотя у меня тоже все таки осталось желание проделать эту работу.
Monsignor ( 11.06.2010 03:48 UTC ) #
avatar
Эх, а SQLCLR там нету...
Vasiliy Novikov ( 11.06.2010 08:03 UTC ) #
avatar
Могу предложить код SQLCLR для теста
Тип:

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">    [ style="color:#2B91AF">Serializable]

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">    [ style="color:#2B91AF">SqlUserDefinedType(Format.UserDefined,
MaxByteSize=-1)]

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">    public structManagedGuidList :
IEnumerable
<Guid>,
INullable,
IBinarySerialize

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">    {

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">        public staticManagedGuidList Null

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
{

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
get

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
{

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">               
return newManagedGuidList();

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
}

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
}

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">        private ICollection< style="color:#2B91AF">Guid> source;

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">        public ManagedGuidList( style="color:#2B91AF">ICollection<Guid>
source)

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
{

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
this.source = source;

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
}

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;color:blue;mso-ansi-language:EN-US">       
#region
style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">
INullable Members

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">        public bool
IsNull

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
{

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
get

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
{

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">               
return this.source
== null;

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
}

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
}

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;color:blue;mso-ansi-language:EN-US">       
#endregion
style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">        public IEnumerator< style="color:#2B91AF">Guid> GetEnumerator()

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
{

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
return this.source.GetEnumerator();

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
}

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">        public overridestring ToString()

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
{

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
if (this.IsNull)

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
{

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">               
return null;

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
}

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
var strBuilder =
new
StringBuilder();

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
foreach (var
guid in this.source)

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
{

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">               
if (strBuilder.Length > 0)

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">               
{

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">                   
strBuilder.Append(" ");

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">               
}

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">               
strBuilder.Append(guid.ToString());

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
}

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
return strBuilder.ToString();

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
}

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">        IEnumerator
IEnumerable
.GetEnumerator()

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
{

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
return this.GetEnumerator();

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
}

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">        private staticreadonly char[]
SplitSeparators = new[] {
' ', ',',
';' };

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">        public staticManagedGuidList Parse( style="color:#2B91AF">SqlString s)

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
{

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
if (s.IsNull)

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
{

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">               
return Null;

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
}

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
var guidStrs =
s.Value.Split(SplitSeparators, StringSplitOptions.RemoveEmptyEntries);

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
var guids = newList<Guid>(guidStrs.Length);

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
guids.AddRange(guidStrs.Select(guidStr => newGuid(guidStr)).ToList());

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
return newManagedGuidList(guids);

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
}

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">        public void
Read(BinaryReader reader)

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
{

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
var size = reader.ReadInt32();

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
if (size < 0)

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
{

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">               
this.source =
null
;

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">               
return;

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
}

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
var newSource =
new
Guid[size];

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
for (var
i = 0; i < size; ++i)

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
{

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">               
newSource[i] = newGuid(reader.ReadBytes(16));

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
}

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
this.source = newSource;

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
}

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">        public void
Write(BinaryWriter writer)

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
{

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
if (this.IsNull)

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
{

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">               
writer.Write(-1);

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
}

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
writer.Write(this.source.Count);

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
foreach (var
item in this.source)

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
{

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">               
writer.Write(item.ToByteArray());

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
}

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
}

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
[SqlFunction(Name =
"EnumerateManagedGuidList", FillRowMethodName
= "Enumerate_FillRow", TableDefinition =
"Item uniqueidentifier")]

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">        public staticIEnumerable Enumerate_GetItems( style="color:#2B91AF">ManagedGuidList list)

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
{

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">           
return list;

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">       
}

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US"> 

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">        public staticvoid Enumerate_FillRow(object
listItem, out
SqlGuid
item)

style="font-size:9.5pt;font-family:Consolas;mso-ansi-language:EN-US">        {

           
item = (Guid)listItem;

       
}

    }

 



И TSql код его использования:
select TestTable.* from TestTable inner join dbo.EnumerateManagedGuidList(@guids) as guids on TestTable.ID = guids.Item
Vasiliy Novikov ( 11.06.2010 08:05 UTC ) #
avatar
Упс, как-то не очень получилось с форматированием. Вставляю так:

[Serializable]
[SqlUserDefinedType(Format.UserDefined, MaxByteSize=-1)]
public struct ManagedGuidList : IEnumerable, INullable, IBinarySerialize
{
public static ManagedGuidList Null
{
get
{
return new ManagedGuidList();
}
}

private ICollection source;

public ManagedGuidList(ICollection source)
{
this.source = source;
}

#region INullable Members

public bool IsNull
{
get
{
return this.source == null;
}
}

#endregion

public IEnumerator GetEnumerator()
{
return this.source.GetEnumerator();
}

public override string ToString()
{
if (this.IsNull)
{
return null;
}

var strBuilder = new StringBuilder();
foreach (var guid in this.source)
{
if (strBuilder.Length > 0)
{
strBuilder.Append(" ");
}

strBuilder.Append(guid.ToString());
}

return strBuilder.ToString();
}

IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}

private static readonly char[] SplitSeparators = new[] { ' ', ',', ';' };

public static ManagedGuidList Parse(SqlString s)
{
if (s.IsNull)
{
return Null;
}

var guidStrs = s.Value.Split(SplitSeparators, StringSplitOptions.RemoveEmptyEntries);
var guids = new List(guidStrs.Length);
guids.AddRange(guidStrs.Select(guidStr => new Guid(guidStr)).ToList());
return new ManagedGuidList(guids);
}

public void Read(BinaryReader reader)
{
var size = reader.ReadInt32();
if (size < 0)
{
this.source = null;
return;
}

var newSource = new Guid[size];
for (var i = 0; i < size; ++i)
{
newSource[i] = new Guid(reader.ReadBytes(16));
}

this.source = newSource;
}

public void Write(BinaryWriter writer)
{
if (this.IsNull)
{
writer.Write(-1);
}

writer.Write(this.source.Count);
foreach (var item in this.source)
{
writer.Write(item.ToByteArray());
}
}

[SqlFunction(Name = "EnumerateManagedGuidList", FillRowMethodName = "Enumerate_FillRow", TableDefinition = "Item uniqueidentifier")]
public static IEnumerable Enumerate_GetItems(ManagedGuidList list)
{
return list;
}

public static void Enumerate_FillRow(object listItem, out SqlGuid item)
{
item = (Guid)listItem;
}
}

select TestTable.* from TestTable inner join dbo.EnumerateManagedGuidList(@guids) as guids on TestTable.ID = guids.Item
Denis Gladkikh ( 11.06.2010 08:11 UTC ) #
avatar
Vasiliy Novikov, Спасибо!

P.S. Надо делать предпросмотр комментариев :(
ddd ( 11.06.2010 08:16 UTC ) #
avatar
Настоятельно рекомендую почитать
http://russian.joelonsoftware.com/Articles/BacktoBasics.html
Vasiliy Novikov ( 11.06.2010 08:37 UTC ) #
avatar
И вдогонку, я вижу там Generic'и съелись. Там везде идёт ICollectio[Guid], IEnumerable[Guid] и т.д.
Denis Gladkikh ( 11.06.2010 10:04 UTC ) #
avatar
ddd, спасибо, интересно. Сначала автора не посмотрел и подумал - неужели в России кто-то так пишет...
ddd ( 11.06.2010 10:17 UTC ) #
avatar
Мужик мировой вообще (был руководителем разработки Екселя), очень полезно почитать все статьи.
Евгений ( 13.06.2010 19:24 UTC ) #
avatar
Можно кстати статьи не читать, а прочитать сразу книгу, называется "Джоэл о программировании", она основана на его статьях, но есть еще много чего.
DeAmon ( 15.06.2010 06:44 UTC ) #
avatar
ну что? всё? XML форевер? ;)
Denis Gladkikh ( 15.06.2010 07:37 UTC ) #
avatar
DeAmon, все при своих мнениях :)
sdelaem-site.com.ua ( 14.07.2010 21:30 UTC ) #
avatar
спасибо за статью, очень нужно было))
to-ken ( 28.08.2010 06:26 UTC ) #
avatar
http://www.faqdot.net/post/ArrayInStoreProcedure.aspx

На XML. Коротко и просто.
Добавить комментарий

Если вы хотите получать уведомления о новых комментариях к данному топику, укажите, пожалуйста, email и отметьте соответствующий пункт в форме.

 

busy 

Размещенная на сайте информация является моим личным мнением.