web-dev-qa-db-fra.com

Existe-t-il un moyen de générer un script de création de table dans TSQL?

Existe-t-il un moyen de générer un script de création à partir d'une table existante uniquement en T-SQL (c'est-à-dire sans utiliser SMO, car T-SQL n'a pas accès à SMO). Disons qu'une procédure stockée qui reçoit un nom de table et renvoie une chaîne qui contient le script de création pour la table donnée?

Permettez-moi maintenant de décrire la situation à laquelle je suis confronté, car il peut y avoir une manière différente d'aborder cela. J'ai une instance avec plusieurs dizaines de bases de données. Ces bases de données ont toutes le même schéma, toutes les mêmes tables, index et ainsi de suite. Ils ont été créés dans le cadre d'une installation de logiciel tiers. J'ai besoin d'avoir un moyen de travailler avec eux afin de pouvoir agréger leurs données de manière ad hoc. Les gens sympas de dba.se m'ont déjà aidé ici Comment créer un déclencheur dans une autre base de données?

Actuellement, je dois trouver un moyen de faire un choix à partir d'une table dans toutes les bases de données. J'ai enregistré tous les noms de base de données dans une table appelée Databasees et j'ai écrit le script suivant pour exécuter une instruction select sur chacun d'eux:

IF OBJECT_ID('tempdb..#tmp') IS NOT NULL
DROP TABLE #tmp

select * into #tmp from Database1.dbo.Table1 where 1=0
DECLARE @statement nvarchar(max) = 
  N'insert into #tmp select * from Table1 where Column1=0 and Cloumn2 =1'

DECLARE @LastDatabaseID INT
SET @LastDatabaseID = 0

DECLARE @DatabaseNameToHandle varchar(60)
DECLARE @DatabaseIDToHandle int

SELECT TOP 1 @DatabaseNameToHandle = Name,
@DatabaseIDToHandle = Database_Ref_No
FROM Databasees
WHERE Database_Ref_No > @LastDatabaseID
ORDER BY Database_Ref_No

WHILE @DatabaseIDToHandle IS NOT NULL
BEGIN

  DECLARE @sql NVARCHAR(MAX) = QUOTENAME(@DatabaseNameToHandle) + '.dbo.sp_executesql'
  EXEC @sql @statement

  SET @LastDatabaseID = @DatabaseIDToHandle
  SET @DatabaseIDToHandle = NULL

  SELECT TOP 1 @DatabaseNameToHandle = Name,
  @DatabaseIDToHandle = Database_Ref_No
  FROM Databasees
  WHERE Database_Ref_No > @LastDatabaseID
  ORDER BY Database_Ref_No
END

select * from #tmp
DROP TABLE #tmp

Cependant, le script ci-dessus échoue avec le message suivant:

Une valeur explicite pour la colonne d'identité dans la table '#tmp' ne peut être spécifiée que lorsqu'une liste de colonnes est utilisée et IDENTITY_INSERT est ON.

Ajout de ceci:

SET IDENTITY_INSERT #tmp ON

n'aide pas, car je ne peux pas spécifier la liste des colonnes et la garder générique.

En SQL, il n'y a aucun moyen de désactiver l'identité sur une table donnée. Vous pouvez uniquement déposer une colonne et ajouter une colonne, ce qui, évidemment, change l'ordre des colonnes. Et si l'ordre des colonnes change, vous devez à nouveau spécifier la liste des colonnes, ce serait différent selon la table que vous interrogez.

Je pensais donc que si je pouvais obtenir le script de création de table dans mon code T-SQL, je pouvais le manipuler avec des expressions de manipulation de chaîne pour supprimer la colonne d'identité et également ajouter une colonne pour le nom de la base de données au jeu de résultats.

Quelqu'un peut-il penser à un moyen relativement facile de réaliser ce que je veux?

22
Andrew Savinykh

En 2007, j'ai demandé un moyen simple de générer un CREATE TABLE script via T-SQL plutôt que d'utiliser l'interface utilisateur ou SMO. J'ai été sommairement rejeté .

Cependant, SQL Server 2012 rend cela très simple. Imaginons que nous ayons une table avec le même schéma sur plusieurs bases de données, par exemple dbo.whatcha:

CREATE TABLE dbo.whatcha
(
  id INT IDENTITY(1,1), 
  x VARCHAR(MAX), 
  b DECIMAL(10,2), 
  y SYSNAME
);

Le script suivant utilise le nouveau sys.dm_exec_describe_first_results_set fonction de gestion dynamique pour récupérer les types de données appropriés pour chacune des colonnes (et en ignorant la propriété IDENTITY). Il crée la table #tmp dont vous avez besoin, insère à partir de chacune des bases de données de votre liste, puis sélectionne à partir de #tmp, le tout dans un seul lot SQL dynamique et sans utiliser une boucle WHILE (ce qui ne fait pas mieux, juste plus simple à regarder et vous permet d'ignorer Database_Ref_No entièrement :-)).

SET NOCOUNT ON;

DECLARE @sql NVARCHAR(MAX), @cols NVARCHAR(MAX) = N'';

SELECT @cols += N',' + name + ' ' + system_type_name
  FROM sys.dm_exec_describe_first_result_set(N'SELECT * FROM dbo.whatcha', NULL, 1);

SET @cols = STUFF(@cols, 1, 1, N'');

SET @sql = N'CREATE TABLE #tmp(' + @cols + ');'

DECLARE @dbs TABLE(db SYSNAME);

INSERT @dbs VALUES(N'db1'),(N'db2');
  -- SELECT whatever FROM dbo.databases

SELECT @sql += N'
  INSERT #tmp SELECT ' + @cols + ' FROM ' + QUOTENAME(db) + '.dbo.tablename;'
  FROM @dbs;

SET @sql += N'
  SELECT ' + @cols + ' FROM #tmp;';

PRINT @sql;
-- EXEC sp_executesql @sql;

La sortie PRINT résultante:

CREATE TABLE #tmp(id int,x varchar(max),b decimal(10,2),y nvarchar(128));
  INSERT #tmp SELECT id,x,b,y FROM [db1].dbo.tablename;
  INSERT #tmp SELECT id,x,b,y FROM [db2].dbo.tablename;
  SELECT id,x,b,y FROM #tmp;

Lorsque vous êtes sûr qu'il fait ce que vous attendez, décommentez simplement le EXEC.

(Cela vous fait confiance que le schéma est le même; il ne valide pas qu'une ou plusieurs des tables ont depuis été modifiées et peut échouer en conséquence.)

28
Aaron Bertrand

Il n'est pas possible dans T-SQL de générer un script de création complet d'une table. Au moins, il n'y a pas de construction. vous pouvez toujours écrire votre propre "générateur" en parcourant les informations sys.columns.

Mais dans votre cas, vous n'avez pas besoin d'obtenir le script de création complet. Tout ce dont vous avez besoin est d'empêcher le SELECT INTO De copier la propriété d'identité. La façon la plus simple de le faire est d'ajouter un calcul à cette colonne. Donc au lieu de

select * into #tmp from Database1.dbo.Table1 where 1=0

tu dois écrire

select id*0 as id, other, column, names into #tmp from Database1.dbo.Table1 where 1=0

Pour générer cette instruction, vous pouvez à nouveau utiliser sys.columns comme dans ce SQL Fiddle

Configuration du schéma MS SQL Server 2008 :

CREATE TABLE dbo.testtbl(
    id INT IDENTITY(1,1),
    other NVARCHAR(MAX),
    [column] INT,
    [name] INT
);

Les deux colonnes dont nous avons besoin sont name et is_identity: Requête 1 :

SELECT name,is_identity
  FROM sys.columns
 WHERE object_id = OBJECT_ID('dbo.testtbl');

Résultats:

|   NAME | IS_IDENTITY |
|--------|-------------|
|     id |           1 |
|  other |           0 |
| column |           0 |
|   name |           0 |

Avec cela, nous pouvons utiliser une instruction CASE pour générer chaque colonne pour la liste des colonnes:

Requête 2 :

SELECT ','+ 
    CASE is_identity
    WHEN 1 THEN QUOTENAME(name)+'*0 AS '+QUOTENAME(name)
    ELSE QUOTENAME(name)
    END
  FROM sys.columns
 WHERE object_id = OBJECT_ID('dbo.testtbl');

Résultats:

|        COLUMN_0 |
|-----------------|
| ,[id]*0 AS [id] |
|        ,[other] |
|       ,[column] |
|         ,[name] |

Avec une petite astuce XML, nous pouvons concaténer tout cela ensemble pour obtenir la liste complète des colonnes:

Requête 3 :

SELECT STUFF((
  SELECT ','+ 
      CASE is_identity
      WHEN 1 THEN QUOTENAME(name)+'*0 AS '+QUOTENAME(name)
      ELSE QUOTENAME(name)
      END
    FROM sys.columns
   WHERE object_id = OBJECT_ID('dbo.testtbl')
   ORDER BY column_id
     FOR XML PATH(''),TYPE
  ).value('.','NVARCHAR(MAX)'),1,1,'')

Résultats:

|                               COLUMN_0 |
|----------------------------------------|
| [id]*0 AS [id],[other],[column],[name] |

Gardez à l'esprit que vous ne pouvez pas créer une table #temp à l'aide de SQL dynamique et l'utiliser en dehors de cette instruction car la table #temp devient hors de portée une fois votre instruction sql dynamique terminée. Vous devez donc soit compresser tout votre code dans la même chaîne SQL dynamique, soit utiliser une vraie table. Si vous devez être en mesure d'exécuter plusieurs de ces scripts/procédures en même temps, vous devez nous fournir un nom de table aléatoire, sinon ils se marcheront les uns sur les autres. Quelque chose comme QUOTENAME(N'temp_'+CAST(NEWID() AS NVARCHAR(40)) devrait faire un assez bon nom.


Au lieu de copier toutes les données, vous pouvez également utiliser une technique similaire pour générer automatiquement une vue pour chaque table qui regroupe toutes les incarnations de cette table dans toutes les bases de données. En fonction de la taille de la table, cela peut toutefois être plus rapide ou plus lent, vous devez donc le tester. Si vous suivez cette voie, je placerais ces vues dans une base de données distincte.

5
Sebastian Meine

Il existe un bon script pour y parvenir dans l'article SQLServerCentral:

La dernière version actuelle du script est également disponible sous forme de texte ici (stormrage.com).

Je souhaite qu'il y ait un moyen d'inclure tout le script ici, car cela fonctionne pour moi. Le script est tout simplement trop long pour être collé ici.

Copyright:

--#################################################################################################
-- copyright 2004-2013 by Lowell Izaguirre scripts*at*stormrage.com all rights reserved.
-- http://www.stormrage.com/SQLStuff/sp_GetDDL_Latest.txt
--Purpose: Script Any Table, Temp Table or Object
--
-- see the thread here for lots of details: http://www.sqlservercentral.com/Forums/Topic751783-566-7.aspx

-- You can use this however you like...this script is not rocket science, but it took a bit of work to create.
-- the only thing that I ask
-- is that if you adapt my procedure or make it better, to simply send me a copy of it,
-- so I can learn from the things you've enhanced.The feedback you give will be what makes
-- it worthwhile to me, and will be fed back to the SQL community.
-- add this to your toolbox of helpful scripts.
--#################################################################################################
3
Marcello Miorelli