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

53

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

Proposition n ° 1 - Paramètres valorisés de la table. Je peux définir un type de table avec seulement un champ ID et envoyer une table contenant des ID à mettre à jour.

Proposition n ° 2 - Paramètre XML (varchar) avec OPENXML () dans le corps de la procédure.

Proposition n ° 3 - Analyse de la liste. Je préférerais éviter cela, si possible, car cela semble difficile à manier et sujet aux erreurs.

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

D. Lambert
la source
Comment obtenez-vous la grande liste d'identifiants?
Larry Coleman
Je les tire avec les données "charge utile" via un autre proc stocké. Je n'ai cependant pas besoin de mettre à jour toutes ces données - il suffit de mettre à jour un drapeau sur certains enregistrements.
D. Lambert

Réponses:

42

Les meilleurs articles sur ce sujet sont par Erland Sommarskog:

Il couvre toutes les options et explique plutôt bien.

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

Marian
la source
23

Il existe une grande discussion à ce sujet sur StackOverflow qui couvre de nombreuses approches. Celui que je préfère pour SQL Server 2008+ consiste à utiliser des paramètres table . C’est essentiellement la solution de SQL Server à votre problème: transmettre une liste de valeurs à une procédure stockée.

Les avantages de cette approche sont les suivants:

  • faire un appel de procédure stockée avec toutes vos données transmises en tant que 1 paramètre
  • l'entrée de table est structurée et fortement typée
  • pas de construction / analyse de chaîne ni manipulation de XML
  • peut facilement utiliser les entrées de table pour filtrer, joindre, ou autre chose

Cependant, prenez note que si vous appelez une procédure stockée qui utilise des TVP via ADO.NET ou ODBC et regardez l'activité avec SQL Server Profiler, vous remarquerez que SQL Server reçoit plusieurs INSERTinstructions pour charger la TVP, une pour chaque ligne. dans le TVP , suivi de l'appel de la procédure. C'est par conception . Ce lot de INSERTs doit être compilé chaque fois que la procédure est appelée et constitue une petite surcharge. Cependant, même avec cette surcharge, TVPs encore soufflent loin d' autres approches en termes de performance et de facilité d' utilisation pour la majorité des cas d'utilisation.

Si vous souhaitez en savoir plus, Erland Sommarskog comprend parfaitement le fonctionnement des paramètres de 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
Nick Chammas
la source
Lorsque je lance ceci, une erreur se produit: Msg 2715, niveau 16, état 3, procédure tvp_test, ligne 4 [Batch Start Line 4] Colonne, paramètre ou variable n ° 2: impossible de trouver le type de données id_list. Le paramètre ou la variable '@customer_list' a un type de données non valide. Msg 1087, niveau 16, état 1, procédure tvp_test, ligne 13 [ligne de début de lot 4] doit déclarer la variable de table "@customer_list".
Damian
@ Damian - L' CREATE TYPEinstruction au début a-t-elle fonctionné avec succès? Quelle version de SQL Server utilisez-vous?
Nick Chammas
Dans le code SP, vous avez cette phrase inline `SELECT @ param1 AS param1; ' . Quel est le but? Vous ne faites pas usage ou param1 alors pourquoi avez-vous mis cela comme paramètre dans l'en-tête SP?
EAmez
@EAmez - Ce n'était qu'un exemple arbitraire. Le point n'est @customer_listpas @param1. L'exemple montre simplement que vous pouvez mélanger différents types de paramètres.
Nick Chammas le
21

L’ensemble du sujet est traité dans l’ article définitif d’Erland Sommarskog: "Tableaux et liste dans SQL Server" . Faites votre choix de la version à choisir.

Résumé, pour les versions antérieures à 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 mérite quand même une lecture pour voir d’autres techniques et réflexions.

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

gbn
la source
14

Je sais que je suis en retard pour ce parti, mais j’avais un problème de ce type dans le passé, obligé d’envoyer jusqu’à 100 000 gros chiffres et j’ai fait quelques points de repère. Nous avons fini par les envoyer au format binaire, sous forme d'image - plus rapide que tout le reste, jusqu'à 100 000 numéros.

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 contient des entiers dans un blob binaire. Je suis en train d'inverser 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;
}
AK
la source
9

Je suis déchirée entre vous parler de SO et répondre ici, car c'est presque une question de programmation. Mais comme j'ai déjà une solution que j'utilise ... je vais poster ça;)

En procédant ainsi, vous introduisez une chaîne délimitée par des virgules (simple scission, mais pas de scission de style CSV) dans la procédure stockée sous forme de varchar (4000), puis vous insérez cette liste dans cette fonction et récupérez une table pratique, une table de varchars seulement.

Cela vous permet d’envoyer les valeurs des identifiants que vous souhaitez traiter et vous pouvez effectuer une jointure simple à ce stade.

Alternativement, vous pouvez faire quelque chose avec un CLR DataTable et l'introduire, mais cela prend un peu plus de temps pour supporter 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
jcolebrand
la source
Eh bien, j'essayais précisément d'éviter la liste délimitée par des virgules pour ne pas avoir à écrire quelque chose comme ça, mais comme c'est déjà écrit, je suppose que je devrais jeter cette solution dans le mélange. ;-)
D. Lambert le
1
Je dis essayé et vrai est le plus facile. Vous pouvez cracher une liste séparée par des virgules en C # en quelques secondes de code, et vous pouvez l'insérer assez rapidement dans cette fonction (après l'avoir insérée dans votre sproc), sans même avoir à y penser. ~ Et je sais que vous avez dit que vous ne vouliez pas utiliser une fonction, mais je pense que c'est la façon la plus simple (peut-être pas la plus efficace)
jcolebrand
5

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

Pour répondre aux exigences de performances, nous utilisons des programmes de partenariat, mais vous devez implémenter votre propre résumé de dbDataReader pour résoudre certains problèmes de performances dans son mode de traitement par défaut. Je n'entrerai pas dans les tenants et aboutissants 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 traité par un traitement de table de décompte (nombres) à une dimension et à deux dimensions. Nous les avons utilisées avec succès dans divers domaines, mais les points de contrôle tactiles bien gérés sont plus performants quand il existe plus de deux cents "lignes".

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

Robert Miller
la source
5

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

Remarque: 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 que je puisse le déboguer, alors j'ai tout extrait de l'application de la console. Excusez mon style de codage (comme les chaînes de connexion codées en dur) car il s’agissait en quelque sorte de "construire celui à jeter". Je voulais montrer comment utiliser un List<customObject>et l'insérer facilement dans la base de données sous forme de tableau, que je peux utiliser dans la procédure stockée. C # et TSQL code 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

Aussi, je vais prendre 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 gardez-le constructif;) ... Si vous voulez vraiment de moi, retrouvez-moi dans le forum de discussion ici . Espérons que, avec ce morceau de code, on peut voir comment ils peuvent utiliser le code List<Current>que je définis comme une table dans la base de données et un List<T>dans leur application.

jcolebrand
la source
3

Je voudrais soit aller avec la proposition n ° 1 ou, au lieu de cela, créer une table de travail qui ne contient que les identifiants traités. Insérez dans cette table pendant le traitement, puis une fois terminé, appelez un proc similaire à celui-ci:

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 beaucoup d'inserts, mais ils seront sur une petite table, donc ça devrait être rapide. Vous pouvez également grouper vos insertions en utilisant ADO.net ou l’adaptateur de données que vous utilisez.

Eric Humphrey - Aide au frais
la source
2

Le titre de la question inclut la tâche permettant de transmettre les données d'une application dans la procédure stockée. Cette partie est exclue par le corps de la question, mais permettez-moi de répondre également à cette question.

Dans le contexte de sql-server-2008, tel que spécifié par les balises, il existe un autre excellent article de E. Sommarskog Arrays and Lists dans SQL Server 2008 . BTW je l'ai trouvé dans l'article Marian cité dans sa réponse.

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

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

Au-delà des techniques mentionnées ici, j’ai le sentiment que, dans certains cas, la copie en bloc et l’insert en masse méritent d’être mentionnés dans les grandes lignes.

Bernd_k
la source
1

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

Pour MS SQL 2016 dernière version

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

Cela peut résoudre votre problème facilement.

Pour MS SQL Ancienne Version

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

D'abord créer 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 créé ceci, passez votre chaîne à cette fonction avec separator.

J'espère que cela vous aide. :-)

Ankit Bhalala
la source
-1

Utilisez-le pour créer "create type table". exemple simple pour l'utilisateur

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
Dinesh Vaitage
la source