Comment passer un tableau dans une procédure stockée SQL Server

294

Comment passer un tableau dans une procédure stockée SQL Server?

Par exemple, j'ai une liste d'employés. Je veux utiliser cette liste comme table et la joindre à une autre table. Mais la liste des employés doit être passée en paramètre à partir de C #.

Sergey
la source
Monsieur, j'espère que ce lien vous aidera à passer une liste / un tableau à SQL Server SP
Patrick Choi
C'est la même classe que Paramétrer une clause SQL IN
Lukasz Szozda

Réponses:

438

SQL Server 2008 (ou plus récent)

Tout d'abord, dans votre base de données, créez les deux objets suivants:

CREATE TYPE dbo.IDList
AS TABLE
(
  ID INT
);
GO

CREATE PROCEDURE dbo.DoSomethingWithEmployees
  @List AS dbo.IDList READONLY
AS
BEGIN
  SET NOCOUNT ON;

  SELECT ID FROM @List; 
END
GO

Maintenant dans votre code C #:

// Obtain your list of ids to send, this is just an example call to a helper utility function
int[] employeeIds = GetEmployeeIds();

DataTable tvp = new DataTable();
tvp.Columns.Add(new DataColumn("ID", typeof(int)));

// populate DataTable from your List here
foreach(var id in employeeIds)
    tvp.Rows.Add(id);

using (conn)
{
    SqlCommand cmd = new SqlCommand("dbo.DoSomethingWithEmployees", conn);
    cmd.CommandType = CommandType.StoredProcedure;
    SqlParameter tvparam = cmd.Parameters.AddWithValue("@List", tvp);
    // these next lines are important to map the C# DataTable object to the correct SQL User Defined Type
    tvparam.SqlDbType = SqlDbType.Structured;
    tvparam.TypeName = "dbo.IDList";
    // execute query, consume results, etc. here
}

SQL Server 2005

Si vous utilisez SQL Server 2005, je recommanderais toujours une fonction partagée sur XML. Créez d'abord une fonction:

CREATE FUNCTION dbo.SplitInts
(
   @List      VARCHAR(MAX),
   @Delimiter VARCHAR(255)
)
RETURNS TABLE
AS
  RETURN ( SELECT Item = CONVERT(INT, Item) FROM
      ( SELECT Item = x.i.value('(./text())[1]', 'varchar(max)')
        FROM ( SELECT [XML] = CONVERT(XML, '<i>'
        + REPLACE(@List, @Delimiter, '</i><i>') + '</i>').query('.')
          ) AS a CROSS APPLY [XML].nodes('i') AS x(i) ) AS y
      WHERE Item IS NOT NULL
  );
GO

Maintenant, votre procédure stockée peut simplement être:

CREATE PROCEDURE dbo.DoSomethingWithEmployees
  @List VARCHAR(MAX)
AS
BEGIN
  SET NOCOUNT ON;

  SELECT EmployeeID = Item FROM dbo.SplitInts(@List, ','); 
END
GO

Et dans votre code C #, il vous suffit de passer la liste en tant que '1,2,3,12'...


Je trouve que la méthode de passage à travers les paramètres de valeur de table simplifie la maintenabilité d'une solution qui l'utilise et a souvent des performances accrues par rapport à d'autres implémentations, y compris XML et le fractionnement de chaînes.

Les entrées sont clairement définies (personne n'a à deviner si le délimiteur est une virgule ou un point-virgule) et nous n'avons pas de dépendances sur d'autres fonctions de traitement qui ne sont pas évidentes sans inspecter le code de la procédure stockée.

Comparé aux solutions impliquant un schéma XML défini par l'utilisateur au lieu d'UDT, cela implique un nombre similaire d'étapes, mais d'après mon expérience, il est beaucoup plus simple de gérer, de maintenir et de lire du code.

Dans de nombreuses solutions, il se peut que vous n'ayez besoin que d'un ou de quelques-uns de ces UDT (types définis par l'utilisateur) que vous réutilisez pour de nombreuses procédures stockées. Comme pour cet exemple, l'exigence courante est de passer par une liste de pointeurs d'ID, le nom de la fonction décrit le contexte que ces ID doivent représenter, le nom du type doit être générique.

Aaron Bertrand
la source
3
J'aime l'idée de paramètre de table - jamais pensé à ça - cheers. Pour ce que ça vaut, le délimiteur doit passer dans l'appel de fonction SplitInts ().
Drammy
Comment puis-je utiliser le paramètre table si je n'ai accès qu'à une chaîne séparée par des virgules
bdwain
@bdwain irait à l'encontre du but - vous devez utiliser une fonction de fractionnement pour la diviser en lignes à mettre dans le TVP. Décomposez-le dans votre code d'application.
Aaron Bertrand
1
@AaronBertrand merci pour la réponse, je viens juste de le comprendre. Je dois utiliser un sous sélectionner dans les parenthèses: SELECT [colA] FROM [MyTable] WHERE [Id] IN (SELECT [Id] FROM @ListOfIds).
JaKXz
3
@ th1rdey3 Ils sont implicitement facultatifs. stackoverflow.com/a/18926590/61305
Aaron Bertrand
44

D'après mon expérience, en créant une expression délimitée à partir des ID d'employé, il existe une solution délicate et agréable à ce problème. Vous ne devez créer qu'une expression de chaîne comme ';123;434;365;'in-which 123, 434et il 365s'agit de certains ID d'employé. En appelant la procédure ci-dessous et en lui passant cette expression, vous pouvez récupérer les enregistrements souhaités. Vous pouvez facilement joindre la "autre table" à cette requête. Cette solution convient à toutes les versions de SQL Server. En outre, en comparaison avec l'utilisation d'une table variable ou d'une table temporaire, c'est une solution très rapide et optimisée.

CREATE PROCEDURE dbo.DoSomethingOnSomeEmployees  @List AS varchar(max)
AS
BEGIN
  SELECT EmployeeID 
  FROM EmployeesTable
  -- inner join AnotherTable on ...
  where @List like '%;'+cast(employeeID as varchar(20))+';%'
END
GO
Hamed Nazaktabar
la source
Agréable! J'aime vraiment cette approche où je filtre les clés int! +1
MDV2000
@ MDV2000 merci :) Sur les clés de chaîne, cela a également de bonnes performances car il ne
transforme
Je suis en retard dans le match, mais c'est très intelligent! Fonctionne très bien pour mon problème.
user441058
Ceci est incroyable! Je vais certainement l'utiliser, merci
Omar Ruder
26

Utilisez un paramètre table pour votre procédure stockée.

Lorsque vous le transmettez à partir de C #, vous ajoutez le paramètre avec le type de données SqlDb.Structured.

Voir ici: http://msdn.microsoft.com/en-us/library/bb675163.aspx

Exemple:

// Assumes connection is an open SqlConnection object.
using (connection)
{
// Create a DataTable with the modified rows.
DataTable addedCategories =
  CategoriesDataTable.GetChanges(DataRowState.Added);

// Configure the SqlCommand and SqlParameter.
SqlCommand insertCommand = new SqlCommand(
    "usp_InsertCategories", connection);
insertCommand.CommandType = CommandType.StoredProcedure;
SqlParameter tvpParam = insertCommand.Parameters.AddWithValue(
    "@tvpNewCategories", addedCategories);
tvpParam.SqlDbType = SqlDbType.Structured;

// Execute the command.
insertCommand.ExecuteNonQuery();
}
Levi W
la source
17

Vous devez le passer en tant que paramètre XML.

Edit: code rapide de mon projet pour vous donner une idée:

CREATE PROCEDURE [dbo].[GetArrivalsReport]
    @DateTimeFrom AS DATETIME,
    @DateTimeTo AS DATETIME,
    @HostIds AS XML(xsdArrayOfULong)
AS
BEGIN
    DECLARE @hosts TABLE (HostId BIGINT)

    INSERT INTO @hosts
        SELECT arrayOfUlong.HostId.value('.','bigint') data
        FROM @HostIds.nodes('/arrayOfUlong/u') as arrayOfUlong(HostId)

Ensuite, vous pouvez utiliser la table temporaire pour rejoindre vos tables. Nous avons défini arrayOfUlong comme un schéma XML intégré pour maintenir l'intégrité des données, mais vous n'avez pas à le faire. Je recommanderais de l'utiliser alors voici un code rapide pour vous assurer d'obtenir toujours un XML avec longs.

IF NOT EXISTS (SELECT * FROM sys.xml_schema_collections WHERE name = 'xsdArrayOfULong')
BEGIN
    CREATE XML SCHEMA COLLECTION [dbo].[xsdArrayOfULong]
    AS N'<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="arrayOfUlong">
        <xs:complexType>
            <xs:sequence>
                <xs:element maxOccurs="unbounded"
                            name="u"
                            type="xs:unsignedLong" />
            </xs:sequence>
        </xs:complexType>
    </xs:element>
</xs:schema>';
END
GO
Fedor Hajdu
la source
J'ai pensé que c'était une mauvaise idée d'utiliser des variables de table quand il y a beaucoup de lignes? N'est-ce pas une meilleure performance d'utiliser à la place une table temporaire (#table)?
janders
@ganders: Je dirais vice versa.
abatishchev
14

Le contexte est toujours important, comme la taille et la complexité du tableau. Pour les petites et moyennes listes, plusieurs des réponses publiées ici sont très bien, bien que certaines clarifications doivent être apportées:

  • Pour fractionner une liste délimitée, un séparateur basé sur SQLCLR est le plus rapide. Il existe de nombreux exemples si vous voulez écrire le vôtre, ou vous pouvez simplement télécharger la bibliothèque SQL # gratuite des fonctions CLR (que j'ai écrite, mais la fonction String_Split, et bien d'autres, sont entièrement gratuites).
  • Le fractionnement de tableaux basés sur XML peut être rapide, mais vous devez utiliser du XML basé sur des attributs, pas du XML basé sur des éléments (qui est le seul type indiqué dans les réponses ici, bien que l'exemple XML de @ AaronBertrand soit le meilleur car son code utilise le text()Fonction XML. Pour plus d'informations (analyse des performances, par exemple) sur l'utilisation de XML pour fractionner des listes, consultez «Utilisation de XML pour transmettre des listes en tant que paramètres dans SQL Server» par Phil Factor.
  • L'utilisation de TVP est excellente (en supposant que vous utilisez au moins SQL Server 2008 ou une version plus récente) car les données sont diffusées dans le proc et s'affichent pré-analysées et fortement typées en tant que variable de table. TOUTEFOIS, dans la plupart des cas, le stockage de toutes les données dans DataTablesignifie la duplication des données en mémoire lors de leur copie à partir de la collection d'origine. Par conséquent, l'utilisation de la DataTableméthode de transmission des TVP ne fonctionne pas bien pour de plus grands ensembles de données (c'est-à-dire qu'elle ne s'adapte pas bien).
  • XML, contrairement aux simples listes délimitées d'Ints ou de chaînes, peut gérer plus de tableaux unidimensionnels, tout comme les TVP. Mais tout comme la DataTableméthode TVP, XML n'évolue pas bien car il fait plus que doubler la taille des données en mémoire car il doit en outre prendre en compte les frais généraux du document XML.

Cela dit, SI les données que vous utilisez sont grandes ou pas encore très grandes mais en croissance constante, la IEnumerableméthode TVP est le meilleur choix car elle diffuse les données vers SQL Server (comme la DataTableméthode), MAIS ne le fait pas exiger toute duplication de la collection en mémoire (contrairement à toutes les autres méthodes). J'ai posté un exemple de code SQL et C # dans cette réponse:

Passer le dictionnaire à la procédure stockée T-SQL

Solomon Rutzky
la source
6

Il n'y a pas de prise en charge du tableau dans le serveur SQL mais il existe plusieurs façons de transmettre la collection à un proc stocké.

  1. En utilisant datatable
  2. En utilisant XML, essayez de convertir votre collection au format xml, puis passez-la en tant qu'entrée à une procédure stockée

Le lien ci-dessous peut vous aider

passage d'une collection à une procédure stockée

praveen
la source
5

J'ai cherché à travers tous les exemples et les réponses sur la façon de passer n'importe quel tableau au serveur SQL sans avoir à créer de nouveau type de table, jusqu'à ce que je trouve ce linK , voici comment je l'ai appliqué à mon projet:

--Le code suivant va obtenir un tableau en tant que paramètre et insérer les valeurs de ce - tableau dans une autre table

Create Procedure Proc1 


@UserId int, //just an Id param
@s nvarchar(max)  //this is the array your going to pass from C# code to your Sproc

AS

    declare @xml xml

    set @xml = N'<root><r>' + replace(@s,',','</r><r>') + '</r></root>'

    Insert into UserRole (UserID,RoleID)
    select 
       @UserId [UserId], t.value('.','varchar(max)') as [RoleId]


    from @xml.nodes('//root/r') as a(t)
END 

Je espère que vous l'apprécierez

Adam
la source
2
@zaitsman: CLEANEST ne signifie pas le meilleur ou le plus approprié. On abandonne souvent la flexibilité et / ou la complexité et / ou les performances "appropriées" pour obtenir du code "propre". Cette réponse est "ok" mais uniquement pour les petits ensembles de données. Si le tableau entrant @sest CSV, il serait plus rapide de simplement le diviser (c.-à-d. INSÉRER DANS ... SÉLECTIONNER À PARTIR DE SplitFunction). La conversion en XML est plus lente que CLR, et le XML basé sur les attributs est de toute façon beaucoup plus rapide. Et ceci est une liste simple mais en passant en XML ou TVP peut également gérer des tableaux complexes. Je ne sais pas ce qui est gagné en évitant un simple, ponctuel CREATE TYPE ... AS TABLE.
Solomon Rutzky
5

Cela vous aidera. :) Suivez les étapes suivantes,

  1. Ouvrez le Concepteur de requêtes
  2. Copiez Collez le code suivant tel qu'il est, cela créera la fonction qui convertira la chaîne en Int

    CREATE FUNCTION dbo.SplitInts
    (
       @List      VARCHAR(MAX),
       @Delimiter VARCHAR(255)
    )
    RETURNS TABLE
    AS
      RETURN ( SELECT Item = CONVERT(INT, Item) FROM
          ( SELECT Item = x.i.value('(./text())[1]', 'varchar(max)')
            FROM ( SELECT [XML] = CONVERT(XML, '<i>'
            + REPLACE(@List, @Delimiter, '</i><i>') + '</i>').query('.')
              ) AS a CROSS APPLY [XML].nodes('i') AS x(i) ) AS y
          WHERE Item IS NOT NULL
      );
    GO
  3. Créer la procédure stockée suivante

     CREATE PROCEDURE dbo.sp_DeleteMultipleId
     @List VARCHAR(MAX)
     AS
     BEGIN
          SET NOCOUNT ON;
          DELETE FROM TableName WHERE Id IN( SELECT Id = Item FROM dbo.SplitInts(@List, ',')); 
     END
     GO
  4. Exécutez ce SP. exec sp_DeleteId '1,2,3,12'Il s'agit d'une chaîne d'ID que vous souhaitez supprimer,

  5. Vous convertissez votre tableau en chaîne en C # et le transmettez en tant que paramètre de procédure stockée

    int[] intarray = { 1, 2, 3, 4, 5 };  
    string[] result = intarray.Select(x=>x.ToString()).ToArray();

     

    SqlCommand command = new SqlCommand();
    command.Connection = connection;
    command.CommandText = "sp_DeleteMultipleId";
    command.CommandType = CommandType.StoredProcedure;
    command.Parameters.Add("@Id",SqlDbType.VARCHAR).Value=result ;

Cela supprimera plusieurs lignes, tout le meilleur

Charan Ghate
la source
j'ai utilisé cette fonction d'analyse séparée par des virgules, cela fonctionnerait pour un petit ensemble de données, si vous vérifiez son plan d'exécution, cela entraînera un problème sur un grand ensemble de données et où vous devez répertorier plusieurs csv dans la procédure stockée
Saboor Awan
2

Il m'a fallu beaucoup de temps pour comprendre cela, donc au cas où quelqu'un en aurait besoin ...

Ceci est basé sur la méthode SQL 2005 dans la réponse d'Aaron, et en utilisant sa fonction SplitInts (je viens de supprimer le paramètre delim car je vais toujours utiliser des virgules). J'utilise SQL 2008 mais je voulais quelque chose qui fonctionne avec des ensembles de données typés (XSD, TableAdapters) et je sais que les paramètres de chaîne fonctionnent avec ceux-ci.

J'essayais de faire fonctionner sa fonction dans une clause de type "where in (1,2,3)", et je n'avais aucune chance de manière directe. J'ai donc d'abord créé une table temporaire, puis j'ai fait une jointure interne au lieu de "where in". Voici mon exemple d'utilisation, dans mon cas, je voulais obtenir une liste de recettes qui ne contiennent pas certains ingrédients:

CREATE PROCEDURE dbo.SOExample1
    (
    @excludeIngredientsString varchar(MAX) = ''
    )
AS
    /* Convert string to table of ints */
    DECLARE @excludeIngredients TABLE (ID int)
    insert into @excludeIngredients
    select ID = Item from dbo.SplitInts(@excludeIngredientsString)

    /* Select recipies that don't contain any ingredients in our excluded table */
   SELECT        r.Name, r.Slug
FROM            Recipes AS r LEFT OUTER JOIN
                         RecipeIngredients as ri inner join
                         @excludeIngredients as ei on ri.IngredientID = ei.ID
                         ON r.ID = ri.RecipeID
WHERE        (ri.RecipeID IS NULL)
eselk
la source
De manière générale, il est préférable de ne pas JOINDRE à une variable de table, mais plutôt à une table temporaire. Les variables de table, par défaut, ne semblent avoir qu'une seule ligne, bien qu'il y ait une astuce ou deux autour de cela (consultez l'article excellent et détaillé de @ AaronBertrand: sqlperformance.com/2014/06/t-sql-queries/… ).
Solomon Rutzky
1

Comme d'autres l'ont noté ci-dessus, une façon de procéder consiste à convertir votre tableau en chaîne, puis à diviser la chaîne dans SQL Server.

Depuis SQL Server 2016, il existe un moyen intégré de fractionner les chaînes appelé

STRING_SPLIT ()

Il renvoie un ensemble de lignes que vous pouvez insérer dans votre table temporaire (ou table réelle).

DECLARE @str varchar(200)
SET @str = "123;456;789;246;22;33;44;55;66"
SELECT value FROM STRING_SPLIT(@str, ';')

donnerait:

valeur
-----
  123
  456
  789
  246
   22
   33
   44
   55
   66

Si vous voulez devenir plus amateur:

DECLARE @tt TABLE (
    thenumber int
)
DECLARE @str varchar(200)
SET @str = "123;456;789;246;22;33;44;55;66"

INSERT INTO @tt
SELECT value FROM STRING_SPLIT(@str, ';')

SELECT * FROM @tt
ORDER BY thenumber

vous donnerait les mêmes résultats que ci-dessus (sauf que le nom de la colonne est "numéro"), mais trié. Vous pouvez utiliser la variable de table comme n'importe quelle autre table, vous pouvez donc facilement la joindre à d'autres tables de la base de données si vous le souhaitez.

Notez que votre installation SQL Server doit être au niveau de compatibilité 130 ou supérieur pour que la STRING_SPLIT()fonction soit reconnue. Vous pouvez vérifier votre niveau de compatibilité avec la requête suivante:

SELECT compatibility_level
FROM sys.databases WHERE name = 'yourdatabasename';

La plupart des langages (y compris C #) ont une fonction "join" que vous pouvez utiliser pour créer une chaîne à partir d'un tableau.

int[] myarray = {22, 33, 44};
string sqlparam = string.Join(";", myarray);

Ensuite, vous passez sqlparamcomme paramètre à la procédure stockée ci-dessus.

Patrick Chu
la source
0
CREATE TYPE dumyTable
AS TABLE
(
  RateCodeId int,
  RateLowerRange int,
  RateHigherRange int,
  RateRangeValue int
);
GO
CREATE PROCEDURE spInsertRateRanges
  @dt AS dumyTable READONLY
AS
BEGIN
  SET NOCOUNT ON;

  INSERT  tblRateCodeRange(RateCodeId,RateLowerRange,RateHigherRange,RateRangeValue) 
  SELECT * 
  FROM @dt 
END
Shabir Mustafa
la source