将参数数组传递给存储过程


74

我需要将“ id”数组传递给存储过程,以删除表中除与数组中的id匹配的行以外的所有行。

我如何以最简单的方式做到这一点?



3
@John Saunders,有许多“将数组作为参数传递” sql服务器问题。但是,这增加了一个问题,即删除问题的传入参数部分之外的所有行。结果,我不认为它是重复的。
KM。

约翰·桑德斯(John Saunders),我不知道自己是否在搜索,但没有找到我想要的东西。这有问题吗?
markiz

1
如果您要进行大量的过程调用,并且每次都需要构建一个xml字符串,则说明效率不高,纯sql方法会更好。如果您已经有了xml字符串,或者正在执行少量的过程调用,那么xml就可以了。
赛车手x 2009年

Answers:


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 2009年

1
RolandTumble提到,这将需要将我的iput数据数组(列表)转换为XML,因此我认为这并不是我的最佳解决方案。
markiz

您可以使用Serialize来做到这一点...使用XML比分割字符串更可靠...
Zanoni

您能解释一下如何序列化List <strings>或arraylist吗?我不想仅为XML序列化创建包装对象。
markiz

8
只是为了在此处添加更新。您需要使用xpath来获取整数列表: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(例如实体框架)来执行此操作可能会比较容易。但是,我现在没有时间亲自看一看。


我认为这是最好的方法。我很震惊,你对此没有票。所以我给了你一个。

1
我喜欢这种方法。感谢分享。
贾斯汀

对于可重用性,我认为这是迄今为止最好的方法。考虑以下常见的实际应用程序:我有一个搜索,它会生成一个包含唯一主键的结果集。然后,我希望将该结果集传递给另一个操作(例如,为excel电子表格检索更多信息)。使用这种方法,我可以轻松地从初始结果集中检索ID列表,并将该列表传递到我的第二阶段存储过程,该过程接受单个参数“ @IDs T1IDs READONLY”。因此,我回避了初始搜索的繁重工作。
mrmillsy 2013年

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 2010年

您可以将输入参数设为varchar(max),只需确保Numbers表具有足够的行。我只使用8000,因为我从不拆分长字符串。您可以看到我的其他答案:stackoverflow.com/questions/4227552/…在这里,我更新了split函数以使用表函数,并且还改进了Numbers表的生成。
KM。

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参数
乔(Joe)

美丽就是简单。
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

宾果Vitalivs!一等奖是最少的代码,并且可以在最短的时间内准确生成所需的内容。嵌入的空格可能会在“ @ids”中引起麻烦。
wwmbes

@Vitalivs感谢您的回答..它救了我:)
Arebhy Sridaran

3

您可以使用存储过程期望存在的临时表。这将在不支持XML等的旧版SQL Server上运行。

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


0

关于使用XML数据类型而不是传递数组呢?我发现一个更好的解决方案在SQL 2005中运行良好


1
@Zanoni,为什么不信任分割字符串?
KM。

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存储的proc,简称,不完整!

创建程序[dbo]。[myproc](@blah xml)
AS ...在@ netwerkids.nodes(N'/ doc / d'中从(SELECT doc.t.value('。','int')中输入SomeID) )作为doc(t))



0

您可以在SQL Server中使用STRING_SPLIT函数。您可以在此处查看文档。

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, ','))
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.