web-dev-qa-db-fra.com

Récupérer la définition de colonne pour le jeu de résultats de procédure stockée

Je travaille avec des procédures stockées dans SQL Server 2008 et j'ai appris que je dois INSERT INTO une table temporaire qui a été prédéfinie afin de travailler avec les données. C'est bien, sauf comment savoir comment définir ma table temporaire, si ce n'est pas moi qui ai écrit la procédure stockée autre que la liste de sa définition et la lecture du code?

Par exemple, à quoi ressemblerait ma table temporaire pour "EXEC sp_stored_procedure"? Il s'agit d'une simple procédure stockée, et je pourrais probablement deviner les types de données, mais il semble qu'il doit y avoir un moyen de simplement lire le type et la longueur des colonnes renvoyées par l'exécution de la procédure.

34
cjbarth

Supposons donc que vous ayez une procédure stockée dans tempdb:

USE tempdb;
GO

CREATE PROCEDURE dbo.my_procedure
AS
BEGIN
    SET NOCOUNT ON;

    SELECT foo = 1, bar = 'tooth';
END
GO

Il existe un moyen assez compliqué de déterminer les métadonnées que la procédure stockée générera. Il y a plusieurs mises en garde, y compris la procédure ne peut produire qu'un seul jeu de résultats, et qu'une meilleure estimation sera faite sur le type de données si elle ne peut pas être déterminée avec précision. Il nécessite l'utilisation de OPENQUERY et d'un serveur lié en boucle avec le 'DATA ACCESS' propriété définie sur true. Vous pouvez vérifier sys.servers pour voir si vous avez déjà un serveur valide, mais créons simplement un manuellement appelé loopback:

EXEC master..sp_addlinkedserver 
    @server = 'loopback',  
    @srvproduct = '',
    @provider = 'SQLNCLI',
    @datasrc = @@SERVERNAME;

EXEC master..sp_serveroption 
    @server = 'loopback', 
    @optname = 'DATA ACCESS',
    @optvalue = 'TRUE';

Maintenant que vous pouvez l'interroger en tant que serveur lié, vous pouvez utiliser le résultat de toute requête (y compris un appel de procédure stockée) en tant que SELECT normal. Vous pouvez donc le faire (notez que le préfixe de la base de données est important, sinon vous obtiendrez les erreurs 11529 et 2812):

SELECT * FROM OPENQUERY(loopback, 'EXEC tempdb.dbo.my_procedure;');

Si nous pouvons effectuer un SELECT *, nous pouvons également effectuer un SELECT * INTO:

SELECT * INTO #tmp FROM OPENQUERY(loopback, 'EXEC tempdb.dbo.my_procedure;');

Et une fois que cette table #tmp existe, nous pouvons déterminer les métadonnées en disant (en supposant que SQL Server 2005 ou supérieur):

SELECT c.name, [type] = t.name, c.max_length, c.[precision], c.scale
  FROM sys.columns AS c
  INNER JOIN sys.types AS t
  ON c.system_type_id = t.system_type_id
  AND c.user_type_id = t.user_type_id
  WHERE c.[object_id] = OBJECT_ID('tempdb..#tmp');

(Si vous utilisez SQL Server 2000, vous pouvez faire quelque chose de similaire avec syscolumns, mais je n'ai pas d'instance 2000 à portée de main pour valider une requête équivalente.)

Résultats:

name      type    max_length precision scale
--------- ------- ---------- --------- -----
foo       int              4        10     0
bar       varchar          5         0     0

À Denali, ce sera beaucoup, beaucoup, beaucoup plus facile. Encore une fois, il y a toujours une limitation du premier jeu de résultats, mais vous n'avez pas besoin de configurer un serveur lié et de sauter à travers tous ces cercles. Vous pouvez simplement dire:

DECLARE @sql NVARCHAR(MAX) = N'EXEC tempdb.dbo.my_procedure;';

SELECT name, system_type_name
    FROM sys.dm_exec_describe_first_result_set(@sql, NULL, 1);

Résultats:

name      system_type_name
--------- ----------------
foo       int             
bar       varchar(5)      

Jusqu'à Denali, je suggère qu'il serait plus facile de simplement retrousser vos manches et de déterminer vous-même les types de données. Non seulement parce qu'il est fastidieux de suivre les étapes ci-dessus, mais également parce que vous êtes beaucoup plus susceptible de faire une supposition correcte (ou au moins plus précise) que le moteur, car le type de données que le moteur suppose sera basé sur l'exécution sortie, sans aucune connaissance externe du domaine des valeurs possibles. Ce facteur restera également vrai à Denali, alors n'ayez pas l'impression que les nouvelles fonctionnalités de découverte de métadonnées sont un atout, elles rendent simplement ce qui précède un peu moins fastidieux.

Oh et pour d'autres problèmes potentiels avec OPENQUERY, voir l'article d'Erland Sommarskog ici:

http://www.sommarskog.se/share_data.html#OPENQUERY

52
Aaron Bertrand

Une manière moins sophistiquée (qui pourrait être suffisante dans certains cas): éditez votre SP d'origine, après le SELECT final et avant la clause FROM ajoutez INSERT INTO tmpTable pour enregistrer le résultat SP dans tmpTable).

Exécutez le SP modifié, de préférence avec des paramètres significatifs afin d'obtenir des données réelles. Restaurez le code d'origine de la procédure.

Vous pouvez maintenant obtenir le script de tmpTable à partir de SQL Server Management Studio ou interroger sys.columns pour obtenir les descriptions des champs.

7
Luca

Il semble que dans SQL 2012, il y ait un nouveau SP pour aider à cela.

exec sp_describe_first_result_set N'PROC_NAME'

https://docs.Microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-describe-first-result-set-transact-sql

6
T Brown

Voici du code que j'ai écrit. L'idée est (comme quelqu'un d'autre l'a dit) est d'obtenir le code SP, de le modifier et de l'exécuter. Cependant, mon code ne change pas le SP d'origine.

Première étape, obtenez la définition du SP, supprimez la partie 'Créer' et supprimez le 'AS' après la déclaration des paramètres, s'il existe.

Declare @SPName varchar(250)
Set nocount on

Declare @SQL Varchar(max), @SQLReverse Varchar(MAX), @StartPos int, @LastParameterName varchar(250) = '', @TableName varchar(36) = 'A' + REPLACE(CONVERT(varchar(36), NewID()), '-', '')

Select * INTO #Temp from INFORMATION_SCHEMA.PARAMETERS where SPECIFIC_NAME = 'ADMIN_Sync_CompareDataForSync'

if @@ROWCOUNT > 0
    BEGIN
        Select @SQL = REPLACE(ROUTINE_DEFINITION, 'CREATE PROCEDURE [' + ROUTINE_SCHEMA + '].[' + ROUTINE_NAME + ']', 'Declare') 
        from INFORMATION_SCHEMA.ROUTINES 
        where ROUTINE_NAME = @SPName

        Select @LastParameterName = PARAMETER_NAME + ' ' + DATA_TYPE + 
            CASE WHEN CHARACTER_MAXIMUM_LENGTH is not null THEN '(' + 
                CASE WHEN CHARACTER_MAXIMUM_LENGTH = -1 THEN 'MAX' ELSE CONVERT(varchar,CHARACTER_MAXIMUM_LENGTH) END + ')' ELSE '' END 
        from #Temp 
        WHERE ORDINAL_POSITION = 
            (Select MAX(ORDINAL_POSITION) 
            From #Temp)

        Select @StartPos = CHARINDEX(@LastParameterName, REPLACE(@SQL, '  ', ' '), 1) + LEN(@LastParameterName)
    END
else
    Select @SQL = REPLACE(ROUTINE_DEFINITION, 'CREATE PROCEDURE [' + ROUTINE_SCHEMA + '].[' + ROUTINE_NAME + ']', '') from INFORMATION_SCHEMA.ROUTINES where ROUTINE_NAME = @SPName

DROP TABLE #Temp

Select @StartPos = CHARINDEX('AS', UPPER(@SQL), @StartPos)

Select @SQL = STUFF(@SQL, @StartPos, 2, '')

(Notez la création d'un nouveau nom de table basé sur un identifiant unique) Recherchez maintenant le dernier mot 'From' dans le code en supposant que c'est le code qui fait la sélection qui renvoie le jeu de résultats.

Select @SQLReverse = REVERSE(@SQL)

Select @StartPos = CHARINDEX('MORF', UPPER(@SQLReverse), 1)

Modifiez le code pour sélectionner l'ensemble de résultats dans une table (la table basée sur l'identifiant unique)

Select @StartPos = LEN(@SQL) - @StartPos - 2

Select @SQL = STUFF(@SQL, @StartPos, 5, ' INTO ' + @TableName + ' FROM ')

EXEC (@SQL)

L'ensemble de résultats est maintenant dans une table, peu importe si la table est vide!

Permet d'obtenir la structure de la table

Select * from INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @TableName

Vous pouvez maintenant faire votre magie avec ce

N'oubliez pas de laisser tomber cette table unique

Select @SQL = 'drop table ' + @TableName

Exec (@SQL)

J'espère que cela t'aides!

6
Johan Smith