Cómo Copiar el Esquema de Tablas Utilizando PowerShell

PROBLEMA: Necesitas copiar el esquema de tablas de un servidor Microsoft SQL a otro servidor SQL.

SOLUCION: Para resolver este problema utilizo PowerShell y el SQLPSX. Si sigues este blog, podrás notar que utilizo la extensión SQLPSX de SQL para PowerShell en todos mis PowerShell scripts de base de datos.

Hay algunas líneas en el código adjunto las cuales hago resaltar porque al escribir el script tuve ciertas dificultades ya que existen dependencias entre las tablas y tipos definidos por el usuario además de la existencia de tablas con particiones. La línea 5 y 6 son para asignar el nombre del servidor y base de datos de destino a variables. En la línea 13 agrego la opción de scripting “NoFileGroup” ya que la base de datos de origen está configurada con múltiples grupos de archivos (filegroups) y mi script termino en error cuando estos filegroups no se encontraron en la base de datos de destino. En este ejemplo, la base de datos de destino cuenta con un solo filegroup. En la línea 15 añado la opción de scripting “WithDependencies” para ayudar con la creación de funciones y esquemas de particiones de tablas. Finalmente en la línea 16, incluyo la opción de scripting “IncludeIfNotExists” para verificar que los objetos de base de datos a crear no existan en la base de datos de destino y así evitar los errores de creación.

CLEAR

$sSRCServer = "TUIRADEVELOPMENT"
$sSRCDatabase = "AdventureWorks"
$sTRGServer = "TUIRA"
$sTRGDatabase = "AdventureWorks"

$oSO = New-SqlScriptingOptions
$oSO.Permissions = $false
$oSO.ClusteredIndexes = $true
$oSO.Indexes = $true
$oSO.Triggers = $true
$oSO.NoFileGroup = $true
$oSO.NonClusteredIndexes = $true
$oSO.WithDependencies = $true
$oSO.IncludeIfNotExists = $true

$oTables = Get-SqlDatabase $sSRCServer $sSRCDatabase|Get-SqlTable

foreach ($oTable in $oTables){
    $oT = $oTable.Name
    $oS = $oTable.Schema
    $DDL = Get-SqlScripter $oTable -scriptingOptions $oSO
    invoke-sqlcmd -ServerInstance "$sTRGServer" -Database "$sTRGDatabase" `
         -Query "$DDL"
}

Esta solución puede ser convertida a una función de PowerShell y utilizar parámetros para vincular las opciones de scripting. Las variables de servidor y base de datos de destino pueden contener una matriz (array) con nombres de servidores y base de datos para así copiar el esquema de tablas a múltiples servidores. Un formulario web puede ser creado utilizando WPF para facilitar el uso de esta solución. ¿Qué te parece esta solución?

Artículos Relacionados

In English

Cómo Mover el Archivo LOG De Una Base de Datos

Algunas veces he tenido la necesidad de mover los archivos LOG de la base de datos de un disco duro a otro. En este artículo, voy a describir el código T-SQL que utilizo para lograr este objetivo.

Primero selecciono la base de datos y ejecuto el procedimiento de systema SP_HELPFILE para obtener el nombre y PATH donde se encuentran los archivos de la base de datos.

USE NorthWind
GO
SET NOCOUNT ON
EXEC SP_HELPFILE

En mi ordenador estan en C:Program FilesMicrosoft SQL ServerMSSQL.1MSSQLDATA. Cambio la base de datos a master y separo la base de datos NorthWind

USE master
GO

EXEC SP_DETACH_DB 'NorthWind'
GO

Ahora abro una sesión de Windows Explorer para mover el archivo LOG a su nueva localidad. En mi ordenador lo copio a D:Program FilesMicrosoft SQL ServerMSSQL.1MSSQLDATA y regreso a mi sesión de analizador de consultas (Query Analyzer) para ejecutar el procedimiento de systema SP_ATTACH_DB y asi pegar la base de datos utilizando el nuevo PATH donde se encuentran los archivos de la base de datos

EXEC SP_ATTACH_DB 'NorthWind'
'C:Program FilesMicrosoft SQL ServerMSSQL.1MSSQLDATANorthWind_Data.mdf',
'D:Program FilesMicrosoft SQL ServerMSSQL.1MSSQLDATANorthWind_Log.ldf'
GO

Cambio la base de datos nuevamente y ejecuto el procedimiento de systema SP_HELPFILE para verificar que la base de datos esta utilizando el archivo LOG en su nueva localidad. Tambien ejecuto el procedimiento de systema SP_HELPDB para verificar que la base de datos esta en linea.

USE NorthWind
GO
EXEC SP_HELPFILE
GO
EXEC SP_HELPDB 'NorthWind'
GO

Updated on 04/18/2011

El procedimiento almacenado sp_attach_db va a ser descontinuado en versiones proximas del SQL Server. Se recomienda que utilize el CREATE DATABASE database_name FOR ATTACH.

USE master
GO

CREATE DATABASE [NorthWind]
    ON (FILENAME = N'C:Program FilesMicrosoft SQL ServerMSSQL.1MSSQLDATANorthWind_Data.mdf'),
       (FILENAME = N'D:Program FilesMicrosoft SQL ServerMSSQL.1MSSQLDATANorthWind_Log.ldf')
   FOR ATTACH
GO

Espero que encuentren ese pequeño artículo de ayuda.

Cómo Generar Una Secuencia de Comandos Para Transferir el Esquema de Una Base de Datos.

El poder generar una secuencia de comandos (Script) para generar todos los objetos de una base de datos es una función necesaria para cualquier administrador. Puedes salvar estos Scripts para comparar las diferentes versiones o para una migración de la base de datos a otro ordenador. La utilidad SCPTXFR te permite generar esta secuencia de comandos para crear el esquema de tu base de datos y guardarlo en uno o varios archivos.

Puedes utilizar el Administrador Corporativo (Enterprise Manager – EM) para crear un Script de tu base de datos. También puedes copiar tu base de datos de manera predefinida utilizando los Servicios de Transformación de Datos (DTS). Pero si lo que necesitas es crear un Script con el esquema de tu base de datos, el SCPTXFR puede ser tu solución.

La utilidad SCPTXFR se encuentra bajo el directorio C:’Program Files’Microsoft SQL Server’MSSQL’Upgrade del MS SQL Server 7.0 y el MS SQL Server 2000. Esta utilidad fue creada con el propósito de migrar base de datos del MS SQL 6.5 al MS SQL 7.0.

Para ver la lista de opciones y parámetros de esta utilidad puedes ejecutar el comando SCPTXFR /? Dentro de la carpeta donde se encuentra el archivo de esta utilidad. En mi ordenador se encuentra en
C:’Program Files’Microsoft SQL Server’MSSQL’Upgrade

SCPTXFR /s {server} /d {database} {[/I] | [/P {password}]}
{[/F {script files directory}] | [/f {single script file}]}
/q /r /O /T /A /E /C /N /X /H /G /Y /?

Cómo Insertar Registros de un Documento XML al Servidor SQL 2000 Utilizando el XML Bulk Load

Los registros de un documento XML pueden ser insertados fácilmente a una base de datos de MS SQL 2000 utilizando el objeto XML Bulk Load. El XML Bulk Load es parte del SQLXML Data Access Component y funciona de manera similar al BULK INSERT o la utilidad bcp. Es un objeto tipo COM que te permite insertar grandes cantidades de registros de modo rápido y eficiente.

Nota: Es preferible usar esta solución cuando el tamaño del documento XML es mayor de 5MB y se aplica al servidor SQL 2000 solamente. El servidor SQL 2005 tiene el XML Bulk Load incorporado y puedes ver ejemplos de cómo utilizarlo en la documentación del mismo.
Cabe destacar que el servidor SQL 2000 cuenta con la función OPENXML que facilita la manipulación de registros en un documento XML pero el uso de esta consume muchos recursos ya que el documento XML debe ser asignado a una variable. Más adelante veremos un ejemplo de cómo utilizar esta función.

Requerimientos
• SQLXML
• MS SQL 2000
• Conocimiento de XML
• El código fuente de este artículo

Empezamos por crear la tabla donde vamos a introducir los registros que aparecen en el documento XML. Este paso no es necesario como veremos más adelante en este artículo.

USE myBaseDeDatos;
GO
CREATE TABLE dbo.SalesOrderDetail (
SalesOrderID int NOT NULL ,
LineNumber tinyint NOT NULL ,
ProductID int NULL ,
SpecialOfferID int NULL ,
CarrierTrackingNumber nvarchar(25) NULL ,
OrderQty smallint NOT NULL ,
UnitPrice money NOT NULL ,
UnitPriceDiscount float NOT NULL,
ModifiedDate datetime NOT NULL,
rowguid uniqueidentifier NOT NULL,
LineTotal int NOT NULL);
GO

A seguir, crearemos el esquema XSD (click here)que describe el documento XML. En el primer elemento de este esquema podrás observar que hago referencia a la tabla “SalesOrderDetail”. Este elemento es utilizado para crear un vínculo entre el documento XML y la tabla donde se insertarán los registros si la tabla existe en la base de datos o en caso de que no exista, la tabla será creada con este nombre. Los elementos que siguen nombran las columnas de la tabla que existe o que se va a crear al igual que el tipo de data de las mismas.

Ya que tenemos el documento XML y el esquema XSD, procedemos a crear un pequeño programa en VBScript para ejecutar el único método del XML Bulk Load (Execute) el cual requiere de dos parámetros, el nombre del esquema (XSD) y el nombre del documento XML. Abre una sesión del Notepad y copia el texto que aparece en el Código3. Salva este archivo con el nombre BulkLoad.vbs en la misma carpeta donde tienes el documento XML y el XSD.

Option Explicit

Dim strMsg
Dim objBL
set objBL = CreateObject("SQLXMLBulkLoad.SQLXMLBulkload.3.0")
with objBL
.ConnectionString = "provider=SQLOLEDB;data source=localhost;database=dev;integrated security=SSPI"
.ErrorLogFile = "e:'error.log"
.SchemaGen = true
.Execute "Salesorderdetail.xsd", "Salesorderdetail.xml"
end with
set objBL=Nothing
strMsg = "Done!" & vbCrLf
msgbox(strMsg)

En el Código4 tenemos un procedimento almazenado que utiliza la función OPENXML y los procedimientos de systema sp_xml_preparedocument y sp_xml_removedocument para manipular un documento XML. Ahora, ya estamos listos para hacer nuestras pruebas y comparar el XML Bulk Load contra el OPENXML.

CREATE PROCEDURE dbo.prcINSERTSalesOrderDetail
       @strXML ntext

AS

SET NOCOUNT ON

DECLARE @iDoc int

EXEC @iDoc OUTPUT
   , @strXML

INSERT INTO dbo.SalesOrderDetail (
       SalesOrderID,
       LineNumber,
       ProductID,
       SpecialOfferID,
       CarrierTrackingNumber,
       OrderQty,
       UnitPrice,
       UnitPriceDiscount,
       ModifiedDate,
       rowguid,
       LineTotal)
(
SELECT *
  FROM OpenXML(@iDoc, '/table/row',2)
  WITH (SalesOrderID int,
        LineNumber tinyint,
        ProductID int,
        SpecialOfferID int,
        CarrierTrackingNumber nvarchar(25),
        OrderQty smallint,
        UnitPrice money,
        UnitPriceDiscount float,
        ModifiedDate varchar(30),
        rowguid varchar(60),
        LineTotal float)
)

EXEC sp_xml_removedocument @iDoc
GO

Ahora voy a describir los pasos que debes seguir para hacer las pruebas con el archivo ZIP que acompaña este artículo.
1. Descargar el archizo ZIP aqui.
2. Abrir una sesión del Analizador de consultas (Query Analyzer) y ejecutar el texto del archivo DataBaseObjects.sql
3. Ejecutar el 1_OPENXMLLoad.vbs. Inserta registros utilizando la función OPENXML.
4. Borrar todos los registros de la tabla SalesOrderDetail ejecutando el TRUNCATE TABLE SalesOrderDetail.
5. Ejecutar el 2_BulkLoad.vbs. Inserta registros utilizando el XML Bulk Upload.
6. Borrar todos los registros de la tabla SalesOrderDetail ejecutando el TRUNCATE TABLE SalesOrderDetail.
7. Ejecutar el 3_BulkLoad.vbs

En el paso 2, creamos la base de datos “dev”, la tabla “SalesOrderDetail” y el procedimiento almacenado “prcInsertCustomer”. En el paso 3 ejecutamos el 1_OPENXMLLoad.vbs el cual inserta los registros del documento salesorderdetail_OPENXML.xml a la tabla “SalesOrderDetail” la cual debe tener 67,599 registros. Después de remover los registros de la tabla “SalesOrderDetail” deben ejecutar el 2_BLoad.vbs el cual inserta los mismos 67,599 registros a la tabla “SalesOrd
erDetail” pero esta vez utilizando el XML Bulk Load. Es aquí donde podemos comparar la eficiencia y velocidad del XML Bulk Upload contra la función OPENXML. Además de ser más rápido, el XML Bulk Upload puede manejar documentos XML más grandes ya que estos no son cargados en la memoria. En el último paso, ejecutamos el 3_BulkLoad.vbs el cual inserta 121,371 registros de un documento XML de 60MB en menos tiempo y utilizando menos recursos de sistema que cuando ejecutamos el paso número tres.

Puedes descargar el código aquí, y no te olvides de añadir tus comentarios.

RECURSOS: SQLXML 3.0

Buena Idea Mal Implementada

Una de las razones por la cual decidí escribir este artículo es porque alguien me sugirió utilizar el procedimiento de sistema SP_DEPENDS para encontrar las tablas que se usan en un SP (procedimiento almacenado). Al iniciar mis pruebas con el SP_DEPENDS utilizando la base de datos AdventureWorks todo parecía andar muy bien, pero cuando lo utilicé en una base de datos que ha estado funcionando por varios años, descubrí algunas fallas en él.

La idea principal del procedimiento SP_DEPENDS y la tabla de sistema SYSDEPENDS es la de tener un mecanismo que permita guardar información de la dependencia entre los objetos de la base de datos. Por ejemplo, si tenemos TABLA A, TABLA B y un SP C que haga referencia a ambas tablas mediante una consulta SELECT, podríamos ejecutar el SP_DEPENDS pasando el nombre de cualquiera de los tres objetos y veríamos que A esta relacionado a C, B esta relacionado a C y que C esta relacionado a A y B. Ahora bien, si ejecutamos el comando DROP TABLE A y luego recreamos la TABLA A, lo lógico seria que si ejecutamos el SP_DEPENDS nuevamente deberíamos obtener el mismo resultado que vimos anteriormente –pero no es así.

La falla esta en que cuando ejecutamos el comando DROP , todos los registros relacionados con el objeto que estamos eliminando se borran del SYSDEPENDS y aunque volvamos a crear el mismo objeto, las relaciones no se crean nuevamente. Volvamos al ejemplo anterior para explicarlo más detalladamente. Si se ejecuta un DROP TABLE A, entonces todos los registros en la tabla SYSDEPENDS de la TABLA A se borran y la dependencia A > C se desaparece. Al ejecutar el SP_DEPENDS con el objeto C como parámetro podremos observar que la unica relacion que existe es con la TABLA B aunque se aya creado la TABLA A nuevamente.

Así que bien, manténganse alejados del uso del SP_DEPENDS y SYSDEPENDS por ahora. Yo estaré haciendo pruebas con el servidor SQL 2005 a ver si Microsoft se tomó el tiempo y arregló este problema.

Puedes descargar el código aquí y no te olvides de añadir tus comentarios.

RECURSOS: –The Guru’s Guide to Transact-SQL
Transact-SQL Programming

The Object Within

Finding tables referenced in a stored procedure

Recently, I was asked for an easy way to list the tables used in a stored procedure. I looked in my list of previously used scripts and found one that did not fully satisfy the requirements of the task. So I decided to come up with a solution that will accurately list all tables referenced in a stored procedure. The solution is pretty straight forward and I’ll describe some of the 12 steps in the code that helped me find all referenced tables within it.

Overview of the problem
Sometimes it is necessary to know which tables are referenced or used in a stored procedure. If the stored procedure you’re working with, is one of those mega procedures of thousands of lines of code, it will be very tedious to go through line by line to find all tables referenced within the procedure.

While developing this solution, I encountered some issues rooted in the fact that stored procedures may be written in many different ways, especially the areas that make reference to tables. For example, take a look at the code in List 1 and see three ways you may write the same query.

There were so many variables that made the development of this solution very interesting. The sample code in List 1 reveals only the SELECT statement with three possible ways I would find reference to tables in stored procedures. Take into consideration that we need to account for the UPDATE, INSERT, and DELETE statements and all the different ways we may find them in the body of stored procedures.

SELECT a.Col1
       b.Col2
  FROM Table1 a,
       Table2 b
 WHERE a.ID = b.ID
SELECT a.Col1,
       b.Col2
  FROM Table1 a, Table2 b
 WHERE a.ID = b.ID
SELECT a.Col1,
       b.Col2
  FROM Table1 a
 INNER JOIN Table2 b ON a.ID = b.ID

First, I created two temporary tables. Table one (dbo.#Tables) will hold the name of tables and views from the database and table two (dbo.#Procedure) will hold the text from the stored procedure. Later in the script, I’ll verify the list of possible table names and view names found in the stored procedure (dbo.#Procedure) against the values in the first table (dbo.#Tables)to validate possible matches with the real object names.

Table one is populated with data from the SYSOBJECTS table WHERE type IN(‘U’,’V’). Notice in List 2 how I convert the data extracted from sysname data type to varchar to avoid any data comparison problems later in the code. I also made sure to TRIM the values from the SYSOBJECTS table.

INSERT dbo.#Tables(TableName)
SELECT CONVERT(VARCHAR(35), LTRIM(RTRIM(Name)))
  FROM dbo.SYSOBJECTS
 WHERE Type IN ('U','V')

You may need to check the maximum object name length in your SYSOBECTS table and update the table definition for dbo.#Tables and the CONVERT statement in List 2 in case the value of 35 is not long enough.

I used the SP_HELPTEXT extended procedure to populate table two. This process will insert one record per line of code returned by the extended procedure.

The following steps will modify the values stored in table two. I replaced any tabs, line feeds, and carriage returns with two spaces which will enable me to use the CHARINDEX() function later on in the solution. I also removed any rows with comments, temporary objects and the CREATE PROCEDURE statement since none of these rows should have the table names I’m looking for. I then looked for the first reference to a real table and removed all rows previous to this row.

The code in List 3 shows how I marked any rows with possible table names by setting the "PossibleTable" column to 1 and then removed all rows with NULL value in the same column.

SELECT @PossibleTable = Texto
  FROM dbo.Procedure
 WHERE Texto LIKE '%,%' AND
       Texto NOT LIKE '%(%';

IF @PossibleTable IS NOT NULL
   BEGIN
   INSERT dbo.#Procedure (PossibleTable, Texto)
   SELECT 1 AS PossibleTable,
          TableNames
     FROM dbo.fnParseCommaDelimitedList (@PossibleTable);
END

The UDF (User Defined Function) in List 4 is used on those rows that may have multiple table names separated by commas. The UDF takes one single parameter of up to 255 characters long and returns a table. It basically inserts one row in the return table for each value separated by a comma from the input parameter.

CREATE FUNCTION dbo.fnParseCommaDelimitedList (
       @myString VARCHAR (255))

RETURNS

@TableNames TABLE (
            TableNames VARCHAR (255))

AS

BEGIN

DECLARE @myPosition AS INT, 
        @myLen AS INT, 
        @myStart AS INT;

SELECT @myPosition = -1,
       @myString = @myString + ',',
       @myLen = LEN(@myString),
       @myStart = 1;

WHILE @myPosition < @myLen
      BEGIN

      INSERT @TableNames
      SELECT SUBSTRING(@myString, @myStart, CHARINDEX(',', @myString, 
                                 @myPosition + 1) - @myStart);

      SELECT @myPosition = CHARINDEX(',', @myString, @myPosition + 1);

      SELECT @myStart = @myPosition;
END
RETURN;
END

Conclusion
I have tested this solution against many stored procedures written in many different ways and I have found one condition that will cause some problems. It is on how you write your stored procedures and the width of your lines of code. You’ll need to increase the length of the "Texto" column in the dbo.#Procedure table If there are lines of code in your procedure longer than 255 characters. Make sure you have the necessary user rights to create the objects for this solution.

Download the code 01042006.sql and feel free to post your comments about this solution.

RESOURCES: BOL (MS SQL Books Online)

Top 25 Most Expensive Stored Procedures

USE: These queries will display the top 25 most expensive stored procedures that are still in the cache.

SET NOCOUNT ON

--[ REVIEW MOST EXPENSIVE TOP 25 CACHED STORED PROCEDURES ]
--[ LOGICAL READS RELATE TO MEMORY PRESSURE               ]
SELECT TOP(25) A.name [SP Name]
     , B.total_logical_reads [TotalLogicalReads]
     , B.total_logical_reads / B.execution_count [AvgerageLogicalReads]
     , B.execution_count
     , ISNULL(B.execution_count / DATEDIFF(SECOND, B.cached_time, GETDATE()), 0) [Calls/Second]
     , B.total_elapsed_time
     , B.total_elapsed_time / B.execution_count [AverageElapsedTime]
     , B.cached_time
  FROM sys.procedures A
 INNER JOIN sys.dm_exec_procedure_stats B ON A.[object_id] = B.[object_id]
 WHERE B.database_id = DB_ID()
 ORDER BY B.total_logical_reads DESC;
 
 
 
--[ REVIEW MOST EXPENSIVE TOP 25 CACHED STORED PROCEDURES ]
--[ PHYSICAL READS RELATE TO IO PRESSURE                  ]
SELECT TOP(25) A.name [SP Name]
     , B.total_physical_reads [TotalphysicalReads]
     , B.total_physical_reads / B.execution_count [AvgeragePhysicalReads]
     , B.execution_count
     , ISNULL(B.execution_count / DATEDIFF(SECOND, B.cached_time, GETDATE()), 0) [Calls/Second]
     , B.total_elapsed_time
     , B.total_elapsed_time / B.execution_count [AvgerageElapsedTime]
     , B.cached_time
  FROM sys.procedures A
 INNER JOIN sys.dm_exec_procedure_stats B ON A.[object_id] = B.[object_id]
 WHERE B.database_id = DB_ID()
 ORDER BY B.total_physical_reads DESC;

 

Script Out All Indexes and Primary Keys from a Database

USE: To generate T-SQL code for all indexes and PK constraints from a database.

CLEAR 

$sSRCServer = "TPW520"
$sSRCDatabase = "iSQLPS" 

$oSO = New-SqlScriptingOptions
$oSO.ClusteredIndexes = $true
$oSO.Indexes = $true
$oSO.NonClusteredIndexes = $true
$oSO.IncludeIfNotExists = $true

$oTables = Get-SqlDatabase $sSRCServer $sSRCDatabase|Get-SqlTable

foreach ($oTable in $oTables){
    $oT = $oTable.Indexes
    
	foreach ($oIndex in $oT) {
		$DDL = Get-SqlScripter $oIndex -scriptingOptions $oSO
	    
		"$DDL `nGO `n`n"
	}
}

 

Create Time String

USE: This function will return a string representation of time.

CREATE FUNCTION [dbo].[fnCreateTimeString] (
       @seconds int)

RETURNS varchar(75) AS 

BEGIN
    DECLARE @h int,
            @m int,
            @s int,
            @secs int,
            @BuildDate varchar(75),
            @hour varchar(75),
            @minute varchar(50),
            @second varchar(25)

    SELECT @secs = @seconds
    SELECT @h = @secs / 3600
    SELECT @secs = @secs - (@h * 3600)
    SELECT @m = @secs / 60
    SELECT @secs = @secs - (@m * 60)
    SELECT @s = @secs

    SELECT @second = CASE WHEN @s = 1 THEN CAST(@s AS varchar) +' second'
                                      ELSE CAST(@s AS varchar) +' seconds' END

    SELECT @minute = CASE WHEN @m = 1 THEN CAST(@m AS varchar) +' minute with '+ @second
                                      ELSE CAST(@m AS varchar) +' minutes with '+ @second END

    SELECT @hour = CASE WHEN @h = 1 THEN CAST(@h AS varchar) +' hour '+ @minute + @second
                                    ELSE CAST(@h AS varchar) +' hours '+ @minute + @second END

    IF @h = 0
       BEGIN
       IF @m = 0
          BEGIN
          SELECT @BuildDate = @second
       END
       ELSE
          BEGIN
          SELECT @BuildDate = @minute
       END
    END
    ELSE
       BEGIN
       SELECT @BuildDate = @hour
    END

    RETURN CONVERT(varchar(75), @BuildDate)
END
GO