Передача параметров массива в хранимую процедуру


53

У меня есть процесс, который захватывает кучу записей (1000-х) и оперирует ими, и когда я закончу, мне нужно пометить большое количество их как обработанных. Я могу указать это большим списком идентификаторов. Я пытаюсь избежать шаблона «обновления в цикле», поэтому я хотел бы найти более эффективный способ отправки этого пакета идентификаторов в хранимый процесс MS SQL Server 2008.

Предложение № 1 - Таблица значений параметров. Я могу определить тип таблицы с помощью поля идентификаторов и отправить таблицу, полную идентификаторов, для обновления.

Предложение № 2 - параметр XML (varchar) с OPENXML () в теле протокола.

Предложение № 3 - Разбор списка. Я бы предпочел избежать этого, если это возможно, так как это кажется громоздким и подверженным ошибкам.

Любое предпочтение среди них, или какие-либо идеи, которые я пропустил?


Как вы получаете большой список идентификаторов?
Ларри Коулман

Я собираю их вместе с данными «полезной нагрузки» через другой сохраненный процесс. Мне не нужно обновлять все эти данные, просто обновите флаг для определенных записей.
Д. Ламберт

Ответы:


42

Лучшие из когда-либо опубликованных статей на этот счет Эрланд Соммарског:

Он охватывает все варианты и объясняет довольно хорошо.

Извините за краткость ответа, но статья Эрланда о массивах похожа на книги Джо Селко о деревьях и других SQL-трактатах :)


23

На StackOverflow обсуждается множество вопросов, которые охватывают множество подходов. В SQL Server 2008+ я предпочитаю использовать табличные параметры . По сути, это решение SQL Server для вашей проблемы - передача списка значений хранимой процедуре.

Преимущества этого подхода:

  • сделать один вызов хранимой процедуры со всеми вашими данными, переданными как 1 параметр
  • вход таблицы структурирован и строго типизирован
  • нет построения / разбора строк или обработки XML
  • может легко использовать ввод таблицы для фильтрации, объединения или чего-либо еще

Однако обратите внимание: если вы вызываете хранимую процедуру, которая использует TVP через ADO.NET или ODBC, и смотрите на действия с SQL Server Profiler, вы заметите, что SQL Server получает несколько INSERTоператоров для загрузки TVP, по одному для каждой строки. в твп с последующим вызовом на процедуру. Это по замыслу . Эта партия INSERTs должна быть скомпилирована каждый раз, когда вызывается процедура, и представляет собой небольшие накладные расходы. Тем не менее, даже с этими накладными расходами, TVP все еще отказываются от других подходов с точки зрения производительности и удобства использования для большинства случаев использования.

Если вы хотите узнать больше, Erland Sommarskog имеет полное представление о том, как работают табличные параметры, и предоставляет несколько примеров.

Вот еще один пример, который я придумал:

CREATE TYPE id_list AS TABLE (
    id int NOT NULL PRIMARY KEY
);
GO

CREATE PROCEDURE [dbo].[tvp_test] (
      @param1           INT
    , @customer_list    id_list READONLY
)
AS
BEGIN
    SELECT @param1 AS param1;

    -- join, filter, do whatever you want with this table 
    -- (other than modify it)
    SELECT *
    FROM @customer_list;
END;
GO

DECLARE @customer_list id_list;

INSERT INTO @customer_list (
    id
)
VALUES (1), (2), (3), (4), (5), (6), (7);

EXECUTE [dbo].[tvp_test]
      @param1 = 5
    , @customer_list = @customer_list
;
GO

DROP PROCEDURE dbo.tvp_test;
DROP TYPE id_list;
GO

Когда я запускаю это, я получаю сообщение об ошибке: Сообщение 2715, Уровень 16, Состояние 3, Процедура tvp_test, Строка 4 [Batch Start Line 4] Столбец, параметр или переменная # 2: Не удается найти тип данных id_list. Параметр или переменная '@customer_list' имеет недопустимый тип данных. Сообщение 1087, Уровень 16, Состояние 1, Процедура tvp_test, Строка 13 [Строка пакетного запуска 4] Необходимо объявить табличную переменную "@customer_list".
Дамиан

@Damian - CREATE TYPEУтверждение в начале прошло успешно? Какую версию SQL Server вы используете?
Ник Чаммас

В коде SP это предложение встроено в строку `SELECT @ param1 AS param1; ' , Какова цель? Вы не используете или param1, так почему вы указали это в качестве параметра в заголовке SP?
EAmez

@EAmez - Это был просто произвольный пример. Дело @customer_listне в этом @param1. Пример просто демонстрирует, что вы можете смешивать разные типы параметров.
Ник Чаммас

21

Вся тема обсуждается в окончательной статье Эрланда Соммарского: «Массивы и список в SQL Server» . Выберите, какую версию выбрать.

Резюме, для до SQL Server 2008, где TVP превосходят остальных

  • CSV, разделите, как вам нравится (я обычно использую таблицу чисел)
  • XML и анализ (лучше с SQL Server 2005+)
  • Создать временную таблицу на клиенте

Статью в любом случае стоит прочитать, чтобы увидеть другие приемы и мышление.

Изменить: поздний ответ для огромных списков в другом месте: передача параметров массива в хранимую процедуру


14

Я знаю, что опаздываю на эту вечеринку, но у меня была такая проблема в прошлом, когда мне приходилось отправлять до 100 тысяч номеров bigint, и я сделал несколько тестов. В итоге мы отправили их в двоичном формате в виде изображения - это было быстрее, чем все остальное, до 100K номеров.

Вот мой старый (SQL Server 2005) код:

SELECT  Number * 8 + 1 AS StartFrom ,
        Number * 8 + 8 AS MaxLen
INTO    dbo.ParsingNumbers
FROM    dbo.Numbers
GO

CREATE FUNCTION dbo.ParseImageIntoBIGINTs ( @BIGINTs IMAGE )
RETURNS TABLE
AS RETURN
    ( SELECT    CAST(SUBSTRING(@BIGINTs, StartFrom, 8) AS BIGINT) Num
      FROM      dbo.ParsingNumbers
      WHERE     MaxLen <= DATALENGTH(@BIGINTs)
    )
GO

Следующий код упаковывает целые числа в двоичный двоичный объект. Я меняю порядок байтов здесь:

static byte[] UlongsToBytes(ulong[] ulongs)
{
int ifrom = ulongs.GetLowerBound(0);
int ito   = ulongs.GetUpperBound(0);
int l = (ito - ifrom + 1)*8;
byte[] ret = new byte[l];
int retind = 0;
for(int i=ifrom; i<=ito; i++)
{
ulong v = ulongs[i];
ret[retind++] = (byte) (v >> 0x38);
ret[retind++] = (byte) (v >> 0x30);
ret[retind++] = (byte) (v >> 40);
ret[retind++] = (byte) (v >> 0x20);
ret[retind++] = (byte) (v >> 0x18);
ret[retind++] = (byte) (v >> 0x10);
ret[retind++] = (byte) (v >> 8);
ret[retind++] = (byte) v;
}
return ret;
}

9

Я разрываюсь между ссылкой на SO или ответом здесь, потому что это почти вопрос программирования. Но так как у меня уже есть решение, которое я использую ... Я опубликую это;)

Этот способ работает так: вы вводите строку с разделителями-запятыми (простое разбиение, не разбивает стиль CSV) в хранимую процедуру как varchar (4000), а затем передаете этот список в эту функцию и возвращаете удобную таблицу, стол только varchars.

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

С другой стороны, вы могли бы сделать что-то с таблицей данных CLR и передать это, но это требует больше поддержки, и все понимают списки CSV.

USE [Database]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER FUNCTION [dbo].[splitListToTable] (@list      nvarchar(MAX), @delimiter nchar(1) = N',')
      RETURNS @tbl TABLE (value     varchar(4000)      NOT NULL) AS
/*
http://www.sommarskog.se/arrays-in-sql.html
This guy is apparently THE guy in SQL arrays and lists 

Need an easy non-dynamic way to split a list of strings on input for comparisons

Usage like thus:

DECLARE @sqlParam VARCHAR(MAX)
SET @sqlParam = 'a,b,c'

SELECT * FROM (

select 'a' as col1, '1' as col2 UNION
select 'a' as col1, '2' as col2 UNION
select 'b' as col1, '3' as col2 UNION
select 'b' as col1, '4' as col2 UNION
select 'c' as col1, '5' as col2 UNION
select 'c' as col1, '6' as col2 ) x 
WHERE EXISTS( SELECT value FROM splitListToTable(@sqlParam,',') WHERE x.col1 = value )

*/
BEGIN
   DECLARE @endpos   int,
           @startpos int,
           @textpos  int,
           @chunklen smallint,
           @tmpstr   nvarchar(4000),
           @leftover nvarchar(4000),
           @tmpval   nvarchar(4000)

   SET @textpos = 1
   SET @leftover = ''
   WHILE @textpos <= datalength(@list) / 2
   BEGIN
      SET @chunklen = 4000 - datalength(@leftover) / 2
      SET @tmpstr = @leftover + substring(@list, @textpos, @chunklen)
      SET @textpos = @textpos + @chunklen

      SET @startpos = 0
      SET @endpos = charindex(@delimiter, @tmpstr)

      WHILE @endpos > 0
      BEGIN
         SET @tmpval = ltrim(rtrim(substring(@tmpstr, @startpos + 1,
                                             @endpos - @startpos - 1)))
         INSERT @tbl (value) VALUES(@tmpval)
         SET @startpos = @endpos
         SET @endpos = charindex(@delimiter, @tmpstr, @startpos + 1)
      END

      SET @leftover = right(@tmpstr, datalength(@tmpstr) / 2 - @startpos)
   END

   INSERT @tbl(value) VALUES (ltrim(rtrim(@leftover)))
   RETURN
END

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

1
Я говорю, попробовал и правда проще всего. Вы можете выложить список разделенных запятыми в C # за несколько секунд кода, и вы можете добавить его в эту функцию (после добавления в sproc) достаточно быстро, и вам даже не придется даже думать об этом. ~ И я знаю, что вы сказали, что не хотите использовать функцию, но я думаю, что это самый простой способ (возможно, не самый эффективный)
jcolebrand

5

Я регулярно получаю наборы из 1000 строк и 10000 строк, отправленные из нашего приложения для обработки различными хранимыми процедурами SQL Server.

Чтобы удовлетворить требования к производительности, мы используем TVP, но вы должны реализовать свой собственный конспект dbDataReader, чтобы преодолеть некоторые проблемы с производительностью в режиме обработки по умолчанию. Я не буду вдаваться в «как» и «почему», поскольку они выходят за рамки этого запроса.

Я не рассматривал обработку XML, поскольку я не нашел реализацию XML, которая остается производительной с более чем 10 000 «строк».

Обработка списков может быть выполнена с помощью обработки одноразмерных и двумерных числовых таблиц. Мы успешно использовали их в различных областях, но хорошо управляемые TVP более эффективны, когда имеется более двух сотен «рядов».

Как и во всех вариантах обработки SQL Server, вы должны сделать свой выбор на основе модели использования.


5

Наконец-то у меня появился шанс сделать некоторые TableValuedParameters, и они отлично работают, поэтому я собираюсь вставить целый лот-код, который показывает, как я их использую, с примером из моего текущего кода: (примечание: мы используем ADO .СЕТЬ)

Также обратите внимание: я пишу некоторый код для службы, и у меня есть много предопределенных битов кода в другом классе, но я пишу это как консольное приложение, чтобы я мог отлаживать его, поэтому я скопировал все это из консольное приложение. Извините, мой стиль кодирования (например, жестко закодированные строки подключения), так как он был своего рода "создать один, чтобы выбросить". Я хотел показать, как я использую, List<customObject>и легко помещать его в базу данных в виде таблицы, которую я могу использовать в хранимой процедуре. C # и код TSQL ниже:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using a;

namespace a.EventAMI {
    class Db {
        private static SqlCommand SqlCommandFactory(string sprocName, SqlConnection con) { return new SqlCommand { CommandType = CommandType.StoredProcedure, CommandText = sprocName, CommandTimeout = 0, Connection = con }; }

        public static void Update(List<Current> currents) {
            const string CONSTR = @"just a hardwired connection string while I'm debugging";
            SqlConnection con = new SqlConnection( CONSTR );

            SqlCommand cmd = SqlCommandFactory( "sprocname", con );
            cmd.Parameters.Add( "@CurrentTVP", SqlDbType.Structured ).Value = Converter.GetDataTableFromIEnumerable( currents, typeof( Current ) ); //my custom converter class

            try {
                using ( con ) {
                    con.Open();
                    cmd.ExecuteNonQuery();
                }
            } catch ( Exception ex ) {
                ErrHandler.WriteXML( ex );
                throw;
            }
        }
    }
    class Current {
        public string Identifier { get; set; }
        public string OffTime { get; set; }
        public DateTime Off() {
            return Convert.ToDateTime( OffTime );
        }

        private static SqlCommand SqlCommandFactory(string sprocName, SqlConnection con) { return new SqlCommand { CommandType = CommandType.StoredProcedure, CommandText = sprocName, CommandTimeout = 0, Connection = con }; }

        public static List<Current> GetAll() {
            List<Current> l = new List<Current>();

            const string CONSTR = @"just a hardcoded connection string while I'm debugging";
            SqlConnection con = new SqlConnection( CONSTR );

            SqlCommand cmd = SqlCommandFactory( "sprocname", con );

            try {
                using ( con ) {
                    con.Open();
                    using ( SqlDataReader reader = cmd.ExecuteReader() ) {
                        while ( reader.Read() ) {
                            l.Add(
                                new Current {
                                    Identifier = reader[0].ToString(),
                                    OffTime = reader[1].ToString()
                                } );
                        }
                    }

                }
            } catch ( Exception ex ) {
                ErrHandler.WriteXML( ex );
                throw;
            }

            return l;
        }
    }
}

-------------------
the converter class
-------------------
using System;
using System.Collections;
using System.Data;
using System.Reflection;

namespace a {
    public static class Converter {
        public static DataTable GetDataTableFromIEnumerable(IEnumerable aIEnumerable) {
            return GetDataTableFromIEnumerable( aIEnumerable, null );
        }

        public static DataTable GetDataTableFromIEnumerable(IEnumerable aIEnumerable, Type baseType) {
            DataTable returnTable = new DataTable();

            if ( aIEnumerable != null ) {
                //Creates the table structure looping in the in the first element of the list
                object baseObj = null;

                Type objectType;

                if ( baseType == null ) {
                    foreach ( object obj in aIEnumerable ) {
                        baseObj = obj;
                        break;
                    }

                    objectType = baseObj.GetType();
                } else {
                    objectType = baseType;
                }

                PropertyInfo[] properties = objectType.GetProperties();

                DataColumn col;

                foreach ( PropertyInfo property in properties ) {
                    col = new DataColumn { ColumnName = property.Name };
                    if ( property.PropertyType == typeof( DateTime? ) ) {
                        col.DataType = typeof( DateTime );
                    } else if ( property.PropertyType == typeof( Int32? ) ) {
                        col.DataType = typeof( Int32 );
                    } else {
                        col.DataType = property.PropertyType;
                    }
                    returnTable.Columns.Add( col );
                }

                //Adds the rows to the table

                foreach ( object objItem in aIEnumerable ) {
                    DataRow row = returnTable.NewRow();

                    foreach ( PropertyInfo property in properties ) {
                        Object value = property.GetValue( objItem, null );
                        if ( value != null )
                            row[property.Name] = value;
                        else
                            row[property.Name] = "";
                    }

                    returnTable.Rows.Add( row );
                }
            }
            return returnTable;
        }

    }
}

USE [Database]
GO

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER PROC [dbo].[Event_Update]
    @EventCurrentTVP    Event_CurrentTVP    READONLY
AS

/****************************************************************
    author  cbrand
    date    
    descrip I'll ask you to forgive me the anonymization I've made here, but hope this helps
    caller  such and thus application
****************************************************************/

BEGIN TRAN Event_Update

DECLARE @DEBUG INT

SET @DEBUG = 0 /* test using @DEBUG <> 0 */

/*
    Replace the list of outstanding entries that are still currently disconnected with the list from the file
    This means remove all existing entries (faster to truncate and insert than to delete on a join and insert, yes?)
*/
TRUNCATE TABLE [database].[dbo].[Event_Current]

INSERT INTO [database].[dbo].[Event_Current]
           ([Identifier]
            ,[OffTime])
SELECT [Identifier]
      ,[OffTime]
  FROM @EventCurrentTVP

IF (@@ERROR <> 0 OR @DEBUG <> 0) 
BEGIN
ROLLBACK TRAN Event_Update
END
ELSE
BEGIN
COMMIT TRAN Event_Update
END

USE [Database]
GO

CREATE TYPE [dbo].[Event_CurrentTVP] AS TABLE(
    [Identifier] [varchar](20) NULL,
    [OffTime] [datetime] NULL
)
GO

Кроме того, я буду конструктивно критиковать мой стиль кодирования, если у вас есть что предложить (всем читателям, которые сталкиваются с этим вопросом), но, пожалуйста, оставьте его конструктивным;) ... Если вы действительно хотите меня, найдите меня в чате здесь , Надеюсь, с помощью этого фрагмента кода можно увидеть, как они могут использовать, List<Current>как я определил, таблицу в БД и А List<T>в своем приложении.


3

Я бы либо пошел с предложением № 1, либо, в качестве альтернативы, создал бы скретч-таблицу, которая просто содержит обработанные идентификаторы. Вставьте в эту таблицу во время обработки, затем, после завершения, вызовите процедуру, подобную приведенной ниже:

BEGIN TRAN

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

TRUNCATE TABLE processedIds

COMMIT TRAN

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


2

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

В контексте sql-server-2008, как указано в тегах, есть еще одна замечательная статья E. Sommarskog Arrays and Lists в SQL Server 2008 . Кстати, я нашел это в статье, на которую ссылается Мэриан в своем ответе.

Вместо того, чтобы просто дать ссылку, я цитирую ее список содержания:

  • Введение
  • Фон
  • Табличные параметры в T-SQL
  • Передача табличных параметров из ADO .NET
    • Использование списка
    • Использование DataTable
    • Использование DataReader
    • Заключительные замечания
  • Использование табличных параметров из других API
    • ODBC
    • OLE DB
    • АДО
    • LINQ и Entity Framework
    • JDBC
    • PHP
    • Perl
    • Что если ваш API не поддерживает TVP
  • Вопросы производительности
    • Серверный
    • Сторона клиента
    • Первичный ключ или нет?
  • Благодарности и отзывы
  • лист регистраций изменений

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


1

Передача параметров массива в хранимую процедуру

Для MS SQL 2016 последняя версия

В MS SQL 2016 они вводят новую функцию: SPLIT_STRING () для анализа нескольких значений.

Это может решить вашу проблему легко.

Для MS SQL Старая версия

Если вы используете более старую версию, чем следовать этому шагу:

Сначала сделайте одну функцию:

 ALTER FUNCTION [dbo].[UDF_IDListToTable]
 (
    @list          [varchar](MAX),
    @Seperator     CHAR(1)
  )
 RETURNS @tbl TABLE (ID INT)
 WITH 

 EXECUTE AS CALLER
 AS
  BEGIN
    DECLARE @position INT
    DECLARE @NewLine CHAR(2) 
    DECLARE @no INT
    SET @NewLine = CHAR(13) + CHAR(10)

    IF CHARINDEX(@Seperator, @list) = 0
    BEGIN
    INSERT INTO @tbl
    VALUES
      (
        @list
      )
END
ELSE
BEGIN
    SET @position = 1
    SET @list = @list + @Seperator
    WHILE CHARINDEX(@Seperator, @list, @position) <> 0
    BEGIN
        SELECT @no = SUBSTRING(
                   @list,
                   @position,
                   CHARINDEX(@Seperator, @list, @position) - @position
               )

        IF @no <> ''
            INSERT INTO @tbl
            VALUES
              (
                @no
              )

        SET @position = CHARINDEX(@Seperator, @list, @position) + 1
    END
END
RETURN
END

Сделав это, просто передайте свою строку этой функции с разделителем.

Я надеюсь, что это полезно для вас. :-)


-1

Используйте это, чтобы создать «создать таблицу типов». простой пример для пользователя

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

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

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

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

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


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


BEGIN
    SET NOCOUNT OFF     

    print @Name;
    print @IsProduct;       
    select * from @UnitsArray;
    select * from @SpecificationsArray;
END
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.