Передача параметрів масиву в збережену процедуру


53

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

Пропозиція №1 - Параметри таблиці. Я можу визначити тип таблиці w / просто поле ідентифікатора та надіслати таблицю, повну ідентифікаторів для оновлення.

Пропозиція №2 - параметр XML (varchar) з OPENXML () в тілі процесора.

Пропозиція №3 - Розбір списку Я вважаю за краще уникати цього, якщо це можливо, оскільки це здається громіздким і схильним до помилок.

Будь-які переваги серед них, чи якісь ідеї, які я пропустив?


Як ви отримуєте великий список ідентифікаторів?
Ларрі Коулман

Я перетягую їх разом із даними "корисного навантаження" через інший збережений прок. Мені не потрібно оновлювати всі ці дані - просто оновіть прапор для певних записів.
Д. Ламберт

Відповіді:


42

Найкращі статті з цього питання - Ерланд Соммарського:

Він охоплює всі варіанти і пояснює досить добре.

Вибачте за короткість відповіді, але стаття Ерланда про Arrays - це як книги Джо Селко про дерева та інші SQL-обробки :)


23

Про це чудово обговорюється на StackOverflow, який охоплює багато підходів. Я вважаю за краще для SQL Server 2008+ використовувати параметри, що оцінюються за таблицею . По суті, це рішення вашої проблеми SQL Server - передача списку значень збереженій процедурі.

Перевагами такого підходу є:

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

Однак зверніть увагу: Якщо ви зателефонуєте на збережену процедуру, яка використовує TVP через ADO.NET або ODBC, і подивіться на активність із програмою SQL Server Profiler, ви помітите, що SQL Server отримує кілька INSERTзаяв для завантаження TVP, по одному для кожного рядка в ТВП з подальшим викликом до процедури. Це за дизайном . Ця партія INSERTs повинна бути складена щоразу, коли викликається процедура, і становить невеликі накладні витрати. Тим НЕ менше, навіть при цьому накладні витрати, TVPs ще здувати інші підходи з точки зору продуктивності і зручності використання для більшості випадків використання.

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

Ось ще один приклад, який я придумав:

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

Коли я запускаю це, я отримую помилку: Msg 2715, Рівень 16, Стан 3, Процедура tvp_test, Рядок 4 [Початковий рядок 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 сервері" . Виберіть, яку саме версію вибрати.

Підсумовуємо , до попереднього SQL Server 2008, де TVP козирують решту

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

Статтю варто прочитати в будь-якому випадку, щоб побачити інші прийоми та мислення.

Редагувати: пізня відповідь для величезних списків в іншому місці: передача параметрів масиву в збережену процедуру


14

Я знаю, що я запізнююсь на цю партію, але в мене була така проблема в минулому, що довелося надсилати до 100 КБ bigint номерів, і я зробив кілька орієнтирів. Ми в кінцевому підсумку надсилали їх у двійковому форматі, як зображення - це було швидше, ніж все інше, до 100 К чисел

Ось мій старий (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

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

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

Це дозволяє вам надсилати значення лише ідентифікаторів, які ви хочете обробити, і ви можете виконати просте з'єднання в цей момент.

Крім того, ви можете зробити щось із CLR DataTable і подати його, але це трохи більше накладних витрат на підтримку, і всі розуміють списки 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 # за кілька секунд коду, і ви зможете вкинути його в цю функцію (після того, як потрапили в паросток), і вам не доведеться навряд чи навіть думати про це. ~ І я знаю, ви сказали, що не хочете використовувати функцію, але я думаю, що це найпростіший спосіб (можливо, не найефективніший)
jcolebrand

5

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

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

Я не вважав обробку XML, оскільки не знайшов реалізацію XML, яка залишається ефективною з більш ніж 10000 "рядків".

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

Як і у всіх варіантах, що стосуються обробки SQL Server, ви повинні зробити вибір на основі моделі використання.


5

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

Також зверніть увагу: я пишу якийсь код для послуги, і в мене є багато заздалегідь визначених бітів коду в іншому класі, але я пишу це як консольний додаток, щоб можу його налагодити, тому я видобув усе це з додаток консолі. Вибачте мій стиль кодування (на зразок рядків жорсткого коду), оскільки це було свого роду "побудувати один для викидання". Я хотів показати, як я використовую a 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>як я це визначив як таблицю в db та a 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-сервера-2008, визначеного тегами, є ще одна чудова стаття Е. Соммарського масивів та списків у SQL Server 2008 . До речі, я знайшов це у статті, про яку Маріан згадував у своїй відповіді.

Замість того, щоб просто надати посилання, я цитую його перелік вмісту:

  • Вступ
  • Фон
  • Параметри з табличною оцінкою в T-SQL
  • Передача параметрів таблиці, оцінених з ADO .NET
    • Використання списку
    • Використання таблиці даних
    • Використання DataReader
    • Заключні зауваження
  • Використання параметрів табличного значення з інших API
    • ODBC
    • OLE DB
    • ADO
    • LINQ та Entity Framework
    • JDBC
    • PHP
    • Perl
    • Що робити, якщо ваш API не підтримує телевізійні точки
  • Розгляд продуктивності
    • На стороні сервера
    • Клієнтська сторона
    • Первинний ключ чи ні?
  • Подяка та відгуки
  • Історія редагування

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


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.