web-dev-qa-db-fra.com

Passer des paramètres de tableau à une procédure stockée

J'ai un processus qui saisit un tas d'enregistrements (des milliers) et les opère, et quand j'ai fini, je dois en marquer un grand nombre comme traités. Je peux l'indiquer avec une grande liste d'identifiants. J'essaie d'éviter le modèle "mises à jour en boucle", donc je voudrais trouver un moyen plus efficace d'envoyer ce sac d'ID dans un proc stocké MS SQL Server 2008.

Proposition n ° 1 - Tableau des paramètres de valeur. Je peux définir un type de table avec juste un champ ID et envoyer une table pleine d'ID à mettre à jour.

Proposition n ° 2 - paramètre XML (varchar) avec OPENXML () dans le corps du proc.

Proposition n ° 3 - Analyse syntaxique des listes. Je préfère éviter cela, si possible, car cela semble lourd et sujet aux erreurs.

Une préférence parmi celles-ci ou des idées que j'ai manquées?

54
D. Lambert

Erland Sommarskog a publié les meilleurs articles à ce sujet:

Il couvre toutes les options et explique assez bien.

Désolé pour la brièveté de la réponse, mais l'article d'Erland sur les tableaux est comme les livres de Joe Celko sur les arbres et autres friandises SQL :)

44
Marian

Il y a une grande discussion à ce sujet sur StackOverflow qui couvre de nombreuses approches. Celui que je préfère pour SQL Server 2008+ consiste à utiliser paramètres table. Il s'agit essentiellement de la solution de SQL Server à votre problème: passer une liste de valeurs à une procédure stockée.

Les avantages de cette approche sont:

  • effectuer un appel de procédure stockée avec toutes vos données transmises en 1 paramètre
  • l'entrée de table est structurée et fortement typée
  • pas de construction/analyse de chaîne ou gestion de XML
  • peut facilement utiliser l'entrée de table pour filtrer, joindre ou quoi que ce soit

Cependant, prenez note: Si vous appelez une procédure stockée qui utilise des TVP via ADO.NET ou ODBC et prenez un regardez l'activité avec SQL Server Profiler, vous remarquerez que SQL Server reçoit plusieurs instructions INSERT pour charger le TVP, ne pour chaque ligne du TVP , suivi de l'appel à la Ceci est par conception . Ce lot de INSERTs doit être compilé à chaque appel de la procédure et constitue une petite surcharge. Cependant, même avec cette surcharge, les TVP continuent - blow away autres approches en termes de performances et de convivialité pour la majorité des cas d'utilisation.

Si vous voulez en savoir plus, Erland Sommarskog a le skinny complet sur le fonctionnement des paramètres table et fournit plusieurs exemples.

Voici un autre exemple que j'ai concocté:

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
23
Nick Chammas

Le sujet entier est discuté sur le article définitif par Erland Sommarskog: "Tableaux et liste dans SQL Server" . Faites votre choix de la version à choisir.

Résumé, pour pré SQL Server 2008 où les TVP l'emportent sur le reste

  • CSV, divisez comme vous le souhaitez (j'utilise généralement un tableau de nombres)
  • XML et analyse (mieux avec SQL Server 2005+)
  • Créer une table temporaire sur le client

L'article vaut quand même la peine d'être lu pour voir d'autres techniques et réflexions.

Edit: réponse tardive pour énorme listes ailleurs: Passage des paramètres du tableau à une procédure stockée

21
gbn

Je sais que je suis en retard pour cette fête, mais j'ai eu un tel problème dans le passé, d'avoir à envoyer jusqu'à 100 000 numéros bigint, et j'ai fait quelques repères. Nous avons fini par les envoyer au format binaire, sous forme d'image - ce qui était plus rapide que tout le reste pour des nombres allant jusqu'à 100K.

Voici mon ancien code (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

Le code suivant regroupe des entiers dans un blob binaire. J'inverse l'ordre des octets ici:

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;
}
14
A-K

Je suis déchiré entre vous référer à SO ou y répondre ici, 'cos c'est presque une question de programmation. Mais puisque j'ai déjà une solution que j'utilise ... je posterai cette ;)

La façon dont celle-ci fonctionne est que vous introduisez une chaîne délimitée par des virgules (simple fractionnement, ne fait pas de fractionnement de style CSV) dans la procédure stockée sous la forme d'un varchar (4000), puis alimentez cette liste dans cette fonction et récupérez un tableau pratique, un tableau de varchars seulement.

Cela vous permet d'envoyer uniquement les valeurs des ID que vous souhaitez traiter, et vous pouvez faire une simple jointure à ce stade.

Alternativement, vous pouvez faire quelque chose avec une table de données CLR et l'introduire, mais c'est un peu plus de charge à prendre en charge et tout le monde comprend les listes 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
9
jcolebrand

Je reçois régulièrement des ensembles de 1 000 lignes et 10 000 lignes envoyées depuis notre application pour être traitées par diverses procédures stockées SQL Server.

Pour répondre aux exigences de performances, nous utilisons des TVP, mais vous devez implémenter votre propre résumé du dbDataReader pour surmonter certains problèmes de performances dans son mode de traitement par défaut. Je n'entrerai pas dans le comment et le pourquoi car ils sont hors de portée pour cette demande.

Je n'ai pas envisagé le traitement XML car je n'ai pas trouvé d'implémentation XML qui reste performante avec plus de 10 000 "lignes".

Le traitement de liste peut être géré par un traitement de table de pointage (nombres) à une ou deux dimensions. Nous les avons utilisés avec succès dans divers domaines, mais les TVP bien gérés sont plus performants quand il y a plus de quelques centaines de "lignes".

Comme pour tous les choix concernant le traitement SQL Server, vous devez faire votre choix en fonction du modèle d'utilisation.

5
Robert Miller

J'ai enfin eu la chance de faire des paramètres TableValuedParameters et ils fonctionnent très bien, donc je vais coller un code entier qui montre comment je les utilise, avec un échantillon de certains de mon code actuel: (note: nous utilisons ADO .NET)

Notez également: j'écris du code pour un service, et j'ai beaucoup de bits de code prédéfinis dans l'autre classe, mais j'écris ceci en tant qu'application console afin de pouvoir le déboguer, j'ai donc déchiré tout cela à partir de l'application console. Excusez mon style de codage (comme les chaînes de connexion codées en dur) car il s'agissait en quelque sorte de "créer un pour le jeter". Je voulais montrer comment j'utilise un List<customObject> et poussez-le facilement dans la base de données sous forme de tableau, que je peux utiliser dans la procédure stockée. Code C # et TSQL ci-dessous:

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

De plus, je prendrai des critiques constructives sur mon style de codage si vous avez cela à offrir (à tous les lecteurs qui rencontrent cette question) mais s'il vous plaît restez constructif;) ... Si vous voulez vraiment de moi, trouvez-moi dans le salon de discussion ici . Avec un peu de chance, avec ce morceau de code, on peut voir comment ils peuvent utiliser le List<Current> comme je l'ai défini comme une table dans la base de données et un List<T> dans leur application.

5
jcolebrand

Je choisirais la proposition n ° 1 ou, comme alternative, je créerais une table de travail qui contiendrait uniquement les identifiants traités. Insérez dans cette table pendant le traitement, puis une fois terminé, appelez un proc similaire à ci-dessous:

BEGIN TRAN

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

TRUNCATE TABLE processedIds

COMMIT TRAN

Vous ferez de nombreux encarts, mais ils seront sur une petite table, donc ça devrait être rapide. Vous pouvez également regrouper vos encarts à l'aide d'ADO.net ou de l'adaptateur de données que vous utilisez.

Le titre de la question comprend la tâche de transmission des données d'une application dans la procédure stockée. Cette partie est exclue par le corps de la question, mais permettez-moi d'essayer d'y répondre également.

Dans le contexte de sql-server-2008 comme spécifié par les balises, il y a un autre excellent article par E. Sommarskog Tableaux et listes dans SQL Server 2008 . BTW Je l'ai trouvé dans l'article auquel Marian a fait référence dans sa réponse.

Au lieu de simplement donner le lien, je cite sa liste de contenu:

  • Introduction
  • Contexte
  • Paramètres table dans T-SQL
  • Passer des paramètres table à partir de ADO .NET
    • Utilisation d'une liste
    • Utilisation d'un DataTable
    • Utilisation d'un DataReader
    • Remarques finales
  • Utilisation de paramètres table à partir d'autres API
    • ODBC
    • OLE DB
    • ADO
    • LINQ et Entity Framework
    • JDBC
    • PHP
    • Perl
    • Que faire si votre API ne prend pas en charge les TVP
  • Considérations sur les performances
    • Du côté serveur
    • Côté client
    • Clé primaire ou pas?
  • Remerciements et rétroaction
  • Historique des révisions

Au-delà des techniques mentionnées ici, j'ai le sentiment que, dans certains cas, la copie en vrac et l'insert en vrac méritent d'être mentionnés pour couvrir le cas général.

2
bernd_k

Passer des paramètres de tableau à une procédure stockée

Pour la dernière version de MS SQL 2016

Avec MS SQL 2016, ils introduisent une nouvelle fonction: SPLIT_STRING () pour analyser plusieurs valeurs.

Cela peut résoudre votre problème facilement.

Pour la version antérieure de MS SQL

Si vous utilisez une version plus ancienne, suivez cette étape:

Faites d'abord une fonction:

 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

Après avoir fait cela, passez simplement votre chaîne à cette fonction avec séparateur.

J'espère que ça t'aide. : -)

1
Ankit Bhalala