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


74

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

Як я можу це зробити найпростішим способом?



3
@ Джон Сондерс, існує безліч запитань про "передавати масив як параметр" sql-сервера. Однак це має додатковий поворот: видалити всі рядки, крім переданої в параметрах частини запитання. Як результат, я не думаю, що це дублікат.
КМ.

Джон Сондерс, я не знаю, що зробив пошук, але не знайшов того, що шукав. Чи є в цьому проблема?
markiz

1
якщо ви робите великий обсяг викликів процедур і вам потрібно щоразу створювати xml-рядок, ви не є ефективним, і метод чистого sql був би кращим. якщо у вас вже є рядок xml або ви виконуєте невеликий обсяг викликів процедур, xml буде в порядку.
гонщик x

Відповіді:


43

Використовуйте збережену процедуру:

РЕДАГУВАТИ: Доповнення до списку серіалізації (або чогось іншого):

List<string> testList = new List<int>();

testList.Add(1);
testList.Add(2);
testList.Add(3);

XmlSerializer xs = new XmlSerializer(typeof(List<int>));
MemoryStream ms = new MemoryStream();
xs.Serialize(ms, testList);

string resultXML = UTF8Encoding.UTF8.GetString(ms.ToArray());

Результат (готовий до використання з параметром XML):

<?xml version="1.0"?>
<ArrayOfInt xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <int>1</int>
  <int>2</int>
  <int>3</int>
</ArrayOfInt>

ОРИГІНАЛЬНА ПОСТА:

Передача XML як параметра:

<ids>
    <id>1</id>
    <id>2</id>
</ids>

CREATE PROCEDURE [dbo].[DeleteAllData]
(
    @XMLDoc XML
)
AS
BEGIN

DECLARE @handle INT

EXEC sp_xml_preparedocument @handle OUTPUT, @XMLDoc

DELETE FROM
    YOURTABLE
WHERE
    YOUR_ID_COLUMN NOT IN (
        SELECT * FROM OPENXML (@handle, '/ids/id') WITH (id INT '.') 
    )
EXEC sp_xml_removedocument @handle


4
+1 Це чудове рішення, ЯКЩО ваші дані вже в структурі XML - не так гаряче, коли ви додаєте накладні витрати на побудову XML на стороні клієнта.
RolandTumble

1
Згаданий RolandTumble вимагає перетворення мого масиву даних iput (List) у XML, тому я вважаю, що це не найкраще рішення у моєму випадку.
markiz

Ви можете використовувати Serialize для цього ... Це більш надійне використання XML, ніж розділення рядків ...
Zanoni

Чи можете ви пояснити, як я можу серіалізувати List <strings> або список списків? Я не хочу створювати об'єкт-обгортку лише для серіалізації xml.
markiz

8
Просто додати до оновлення тут. вам потрібно використовувати xpath, щоб отримати список ints: SELECT * FROM OPENXML (@xmlHandle, '/ ArrayOfInt / int') WITH (id INT '.')

56

Якщо ви використовуєте Sql Server 2008 або новішої версії, ви можете використовувати щось, що називається параметром табличного значення (TVP), замість того, щоб серіалізувати та десеріалізувати дані списку кожного разу, коли ви хочете передати їх збереженій процедурі.

Почнемо із створення простої схеми, яка буде нашим ігровим майданчиком:

CREATE DATABASE [TestbedDb]
GO


USE [TestbedDb]
GO

    /* First, setup the sample program's account & credentials*/
CREATE LOGIN [testbedUser] WITH PASSWORD=N'µ×?
?S[°¿Q­¥½q?_Ĭ¼Ð)3õļ%dv', DEFAULT_DATABASE=[master], DEFAULT_LANGUAGE=[us_english], CHECK_EXPIRATION=OFF, CHECK_POLICY=ON
GO

CREATE USER [testbedUser] FOR LOGIN [testbedUser] WITH DEFAULT_SCHEMA=[dbo]
GO

EXEC sp_addrolemember N'db_owner', N'testbedUser'
GO


    /* Now setup the schema */
CREATE TABLE dbo.Table1 ( t1Id INT NOT NULL PRIMARY KEY );
GO

INSERT INTO dbo.Table1 (t1Id)
VALUES
    (1),
    (2),
    (3),
    (4),
    (5),
    (6),
    (7),
    (8),
    (9),
    (10);
GO

З нашою схемою та зразками даних на місці, ми тепер готові створити нашу збережену процедуру TVP:

CREATE TYPE T1Ids AS Table (
        t1Id INT
);
GO


CREATE PROCEDURE dbo.FindMatchingRowsInTable1( @Table1Ids AS T1Ids READONLY )
AS
BEGIN
        SET NOCOUNT ON;

        SELECT Table1.t1Id FROM dbo.Table1 AS Table1
        JOIN @Table1Ids AS paramTable1Ids ON Table1.t1Id = paramTable1Ids.t1Id;
END
GO

Маючи як свою схему, так і API, ми можемо викликати збережену процедуру TVP із нашої програми так:

        // Curry the TVP data
        DataTable t1Ids = new DataTable( );
        t1Ids.Columns.Add( "t1Id",
                           typeof( int ) );

        int[] listOfIdsToFind = new[] {1, 5, 9};
        foreach ( int id in listOfIdsToFind )
        {
            t1Ids.Rows.Add( id );
        }
        // Prepare the connection details
        SqlConnection testbedConnection =
                new SqlConnection(
                        @"Data Source=.\SQLExpress;Initial Catalog=TestbedDb;Persist Security Info=True;User ID=testbedUser;Password=letmein12;Connect Timeout=5" );

        try
        {
            testbedConnection.Open( );

            // Prepare a call to the stored procedure
            SqlCommand findMatchingRowsInTable1 = new SqlCommand( "dbo.FindMatchingRowsInTable1",
                                                                  testbedConnection );
            findMatchingRowsInTable1.CommandType = CommandType.StoredProcedure;

            // Curry up the TVP parameter
            SqlParameter sqlParameter = new SqlParameter( "Table1Ids",
                                                          t1Ids );
            findMatchingRowsInTable1.Parameters.Add( sqlParameter );

            // Execute the stored procedure
            SqlDataReader sqlDataReader = findMatchingRowsInTable1.ExecuteReader( );

            while ( sqlDataReader.Read( ) )
            {
                Console.WriteLine( "Matching t1ID: {0}",
                                   sqlDataReader[ "t1Id" ] );
            }
        }
        catch ( Exception e )
        {
            Console.WriteLine( e.ToString( ) );
        }
  /* Output:
   * Matching t1ID: 1
   * Matching t1ID: 5
   * Matching t1ID: 9
   */

Можливо, існує менш болісний спосіб зробити це за допомогою більш абстрактного API, такого як Entity Framework. Однак я не маю часу переконатися в цьому зараз.


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

1
Мені подобається такий підхід. Дякую, що поділились.
Джастін,

Щодо повторного використання, я вважаю, що це, безумовно, найкращий підхід. Розглянемо наступне поширене реальне застосування: У мене є пошук, який генерує набір результатів, що включає унікальний первинний ключ. Потім я хочу передати цей набір результатів іншій операції (наприклад, отримання подальшої інформації для таблиці Excel). За допомогою цього методу я можу легко отримати список ідентифікаторів з початкового набору результатів і передати список у свою другу фазу, що зберігається, яка приймає єдиний параметр "@IDs T1IDs READONLY". Тим самим я обминаю важке навантаження первинного пошуку.
mrmillsy

20

це найкраще джерело:

http://www.sommarskog.se/arrays-in-sql.html

створити функцію розділення за допомогою посилання та використовувати її як:

DELETE YourTable
    FROM YourTable                           d
    LEFT OUTER JOIN dbo.splitFunction(@Parameter) s ON d.ID=s.Value
    WHERE s.Value IS NULL

Я віддаю перевагу підходу до таблиці чисел

Це код, заснований на наведеному вище посиланні, який повинен зробити це за вас ...

Перш ніж використовувати мою функцію, вам потрібно створити таблицю "допоміжні", вам потрібно зробити це лише один раз для кожної бази даних:

CREATE TABLE Numbers
(Number int  NOT NULL,
    CONSTRAINT PK_Numbers PRIMARY KEY CLUSTERED (Number ASC)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
DECLARE @x int
SET @x=0
WHILE @x<8000
BEGIN
    SET @x=@x+1
    INSERT INTO Numbers VALUES (@x)
END

використовуйте цю функцію, щоб розділити рядок, яка не циклічна і дуже швидка:

CREATE FUNCTION [dbo].[FN_ListToTable]
(
     @SplitOn              char(1)              --REQUIRED, the character to split the @List string on
    ,@List                 varchar(8000)        --REQUIRED, the list to split apart
)
RETURNS
@ParsedList table
(
    ListValue varchar(500)
)
AS
BEGIN

/**
Takes the given @List string and splits it apart based on the given @SplitOn character.
A table is returned, one row per split item, with a column name "ListValue".
This function workes for fixed or variable lenght items.
Empty and null items will not be included in the results set.


Returns a table, one row per item in the list, with a column name "ListValue"

EXAMPLE:
----------
SELECT * FROM dbo.FN_ListToTable(',','1,12,123,1234,54321,6,A,*,|||,,,,B')

    returns:
        ListValue  
        -----------
        1
        12
        123
        1234
        54321
        6
        A
        *
        |||
        B

        (10 row(s) affected)

**/



----------------
--SINGLE QUERY-- --this will not return empty rows
----------------
INSERT INTO @ParsedList
        (ListValue)
    SELECT
        ListValue
        FROM (SELECT
                  LTRIM(RTRIM(SUBSTRING(List2, number+1, CHARINDEX(@SplitOn, List2, number+1)-number - 1))) AS ListValue
                  FROM (
                           SELECT @SplitOn + @List + @SplitOn AS List2
                       ) AS dt
                      INNER JOIN Numbers n ON n.Number < LEN(dt.List2)
                  WHERE SUBSTRING(List2, number, 1) = @SplitOn
             ) dt2
        WHERE ListValue IS NOT NULL AND ListValue!=''



RETURN

END --Function FN_ListToTable

Ви можете використовувати цю функцію як таблицю в об'єднанні:

SELECT
    Col1, COl2, Col3...
    FROM  YourTable
        INNER JOIN dbo.FN_ListToTable(',',@YourString) s ON  YourTable.ID = s.ListValue

ось ваше видалення:

DELETE YourTable
    FROM YourTable                                d
    LEFT OUTER JOIN dbo.FN_ListToTable(',',@Parameter) s ON d.ID=s.ListValue
    WHERE s.ListValue IS NULL

@KM: Чому максимум 8000 символів? Чи можемо ми використовувати рядки довші за це?
Pandincus

ви можете зробити вхідний параметр varchar (max), лише переконайтеся, що у вашій таблиці Numbers є достатньо рядків. Я просто використовую 8000, тому що я ніколи не розбиваю довгі рядки. Ви можете побачити цю іншу мою відповідь: stackoverflow.com/questions/4227552/…, де я оновив функцію розділення для використання функції таблиці, а також покращив генерацію таблиці Numbers.
КМ.

12

Ви можете спробувати це:



DECLARE @List VARCHAR(MAX)

SELECT @List = '1,2,3,4,5,6,7,8'

EXEC(
'DELETE
FROM TABLE
WHERE ID NOT IN (' + @List + ')'
)


6
... до тих пір, поки ви впевнені, що параметр @List не заповнюється введенням користувачем
Джо

Краса - це простота.
wwmbes

4
declare @ids nvarchar(1000)

set @ids = '100,2,3,4,5' --Parameter passed

set @ids = ',' + @ids + ','

select   *
from     TableName 
where    charindex(',' + CAST(Id as nvarchar(50)) + ',', @ids) > 0

Бінго Віталівс! Перший приз - це найменший код, який видає саме те, що потрібно за найменший час. Вбудовані пробіли можуть спричинити проблеми в "@ids".
wwmbes

@Vitalivs Дякую за відповідь .. це рятуй мене :)
Аребхі Шрідаран

3

Ви можете скористатися тимчасовою таблицею, яку збережена процедура очікує на існування. Це буде працювати на старих версіях SQL Server, які не підтримують XML тощо.

CREATE TABLE #temp
(INT myid)
GO
CREATE PROC myproc
AS
BEGIN
    DELETE YourTable
    FROM YourTable                    
    LEFT OUTER JOIN #temp T ON T.myid=s.id
    WHERE s.id IS NULL
END

1

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


0

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


1
@Zanoni, чому б не довіряти розділеним рядкам?
КМ.

0

Мені подобається цей, оскільки він підходить для передачі як XElement, який підходить для SqlCommand

(Вибачте, це VB.NET, але ви зрозуміли)

<Extension()>
Public Function ToXml(Of T)(array As IEnumerable(Of T)) As XElement
   Return XElement.Parse(
           String.Format("<doc>{0}</doc>", String.Join("", array.Select(Function(s) String.Concat("<d>", s.ToString(), "</d>")))), LoadOptions.None)
 End Function

Це збережений процес sql, скорочений, не повний!

СТВОРИТИ ПРОЦЕДУРУ [dbo]. [Myproc] (@blah xml)
ЯК ... ДЕ ЯК ВХОДИТИ (ВБЕРІТЬ doc.t.value ('.', 'Int') з @ netwerkids.nodes (N '/ doc / d' ) як doc (t))



0

Ви можете використовувати функцію STRING_SPLIT у SQL Server. Ви можете перевірити документацію тут .

DECLARE @YourListOfIds VARCHAR(1000) -- Or VARCHAR(MAX) depending on what you need

SET @YourListOfIds = '1,2,3,4,5,6,7,8'

SELECT * FROM YourTable
WHERE Id IN(SELECT CAST(Value AS INT) FROM STRING_SPLIT(@YourListOfIds, ','))
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.