Showing posts with label database. Show all posts
Showing posts with label database. Show all posts

2024-11-19

List orphan database users

Some days ago I was asked - politely - to move some databases between two SQL Server installations. Unfortunately the service had failed on the source, so I have no access to the logins definitions.
This give me a lot of orphan database users.
To make thing more entertaining some of the databases has compability level 80, and this can not be changed.
To get a list of orphan database users, I have to combine two methods. This is because the compability level 80 databases does not have the system view database_principals.

DECLARE @db_name AS nvarchar(128); -- Database name
DECLARE @_msg AS nvarchar(2047); -- Execution message
DECLARE @_cmd AS nvarchar(MAX); -- Dynamic command
CREATE TABLE ##orphan_user (
[db_name] nvarchar(128) NOT NULL,
[user_name] nvarchar(128) NOT NULL
);

DECLARE db_cursor CURSOR FAST_FORWARD FOR
SELECT [name]
FROM [master].[sys].[databases]
WHERE [database_id] > 4;

OPEN db_cursor;
FETCH NEXT FROM db_cursor INTO @db_name;
WHILE @@FETCH_STATUS = 0
BEGIN
IF (SELECT [compatibility_level] FROM [master].[sys].[databases] WHERE [name] = @db_name) < 90 BEGIN SET @_msg = N'The database [' + @db_name + N'] is SQL Server 2000 or older.'; SET @_cmd = 'INSERT INTO ##orphan_user ' + N'SELECT ''' + @db_name + N''' AS [db_name], [sysusers].[name] AS [user_name] ' + N'FROM [' + @db_name + N'].[dbo].[sysusers] ' + N'LEFT OUTER JOIN [master].[dbo].[syslogins] ON [sysusers].[sid] = [master].[dbo].[syslogins].[sid] ' + N'WHERE [sysusers].[uid] > 4 AND [sysusers].[issqlrole]=0 AND [sysusers].[isapprole]=0 AND [master].[dbo].[syslogins].[sid] IS NULL;';
END
ELSE
BEGIN
SET @_msg = N'The database [' + @db_name + N'] is SQL Server 2005 or newer.';
SET @_cmd = N'INSERT INTO ##orphan_user ' +
N'SELECT ''' + @db_name + N''' AS [db_name], [database_principals].[name] AS [user_name] ' +
N'FROM [' + @db_name + N'].[sys].[database_principals] ' +
N'WHERE [database_principals].[type] IN (''S'',''U'',''G'') ' +
N'AND [database_principals].[is_fixed_role] = 0 ' +
N'AND [database_principals].[name] NOT IN (N''dbo'', N''guest'', N''sys'', N''INFORMATION_SCHEMA'');';
END
RAISERROR(@_msg,0,0) WITH NOWAIT;
EXECUTE (@_cmd);

FETCH NEXT FROM db_cursor INTO @db_name;
END

CLOSE db_cursor;
DEALLOCATE db_cursor;


SELECT * FROM [##orphan_user];

SELECT DISTINCT [user_name] FROM [##orphan_user]
ORDER BY [user_name] DESC

DROP TABLE [##orphan_user];


I execute only a part of the script where I fill the temporary table. Then I can filter and sort the data to handle the logins.

2019-03-06

Rename TFS Collection databasefiles

During TFS (2017) restore on a new server the wizard might rename datafiles.

If the TFS Collection database is defined with multiple datafiles (mdf & ndf) where the files are name with a postfix like "*_<number>", e.g. "TFS_SQLAdmin_Collection_2.ndf".

Datafiles are renamed with a replacement of number last in name with a GUID. This put you in a unpleasant situation with unnecessary long file names.

To fix this you have to detatch the database, rename the files and attach the database with the new file names.


2015-09-22

Move table to filegroup


Why?

Dealing with a database that has been around for some time may require some basic reconfiguration. In this case I had one (or more...) database that had some performance issues. One major cause was that the database only had one datafile (mdf) and the I/O was running serial. I could add some extra datafiles, but to benefit from more datafiles they should be of equal size. This would either require a massive extra amount of storage or a trade off.
I went for a solution with one new filegroup that had several datafiles for parallel I/O. To use the new filegroup I had to move data to the new filegroup. Usually by moving clustered indexes. The data was chosen with the developers and expert users on what was used most for central activities.

Prepare storage

Due to the IT infrastructure I work in I added some extra drives to the database server. On each drive I created a folder in the root to hold the datafiles.
To make the folders useful to the database service the service account was made owner of the folder. As we use Active Directory Managed Service Accounts (MSA) as service account for SQL Server services the service account name must be entered with a trailing dollar-sign.

Get index info

This statement I used to get some basic info about the indexes on a given table, so I could have an idea on what I was about to touch.
SELECT
  [tables].[name] AS [table_name],
  [schemas].[name] AS [schema_name],
  [indexes].[name] AS [index_name],
  [idx_phys_stats].[index_type_desc],
  [idx_phys_stats].[page_count]*8 AS [index_size_in_kb],
  ([idx_phys_stats].[page_count]*8.0)/(1024*1024) AS [index_size_in_mb]
FROM [master].[sys].[dm_db_index_physical_stats](DB_ID('BoPind'),OBJECT_ID('STOCKFLOW'),NULL,NULL,'LIMITED') AS [idx_phys_stats] -- filter by parameter values
  INNER JOIN [sys].[tables] ON [idx_phys_stats].[object_id] = [tables].[object_id]
  INNER JOIN [sys].[schemas] ON [tables].[schema_id] = [schemas].[schema_id]
  INNER JOIN [sys].[indexes] ON [idx_phys_stats].[index_id] = [indexes].[index_id] AND [idx_phys_stats].[object_id] = [indexes].[object_id];

The statement is inspired by the answer by Mikael Eriksson to the question „T-SQL query to get Index fragmentation information“ on stackoverflow (StackExchange).

Create Filegroup

ALTER DATABASE [my_database] ADD FILEGROUP [standard];
ALTER DATABASE [my_database] ADD FILE (
NAME = N'standard_0',
FILENAME = N'D:\MSSQL.Data\my_database.standard_0.mdf',
SIZE = 42GB,
FILEGROWTH = 1GB ) TO FILEGROUP [standard];
ALTER DATABASE [my_database] ADD FILE (
NAME = N'standard_1',
FILENAME = N'D:\MSSQL.Data\my_database.standard_1.ndf',
SIZE = 42GB,
FILEGROWTH = 1GB ) TO FILEGROUP [standard];

etcetera...

Setting the filegroup as default
ALTER DATABASE [my_database] MODIFY FILEGROUP [standard] DEFAULT;

The existence of the filegroup can be verified immidetaly with
SELECT * FROM [my_database].[sys].[filegroups];

Move table

The data are in a clustered index – if such is defined on the table. This article „Move an Existing Index to a Different Filegroup“ has a central remark about using SQL Server Management Studio GUI to move a table to anorther filegroup:
You cannot move indexes created using a UNIQUE or PRIMARY KEY constraint using Management Studio.
To move these indexes use the CREATE INDEX statement with the (DROP_EXISTING=ON) option in Transact-SQL.
This makes sense as uniquenes is a central property of a primary key. Remember that a primary key does not require a clustered index and vice versa.
Move clustered index
Generating a initial script using Management Studio is a quick way to get a good start.
USE [my_database];
CREATE CLUSTERED INDEX [IX_1st_table] ON [dbo].[1st_table] (
  [1st_table_id] ASC,
  [1st_table_col2] ASC,
  [1st_table_col3] ASC
)WITH (DROP_EXISTING = ON, ONLINE = OFF) ON [standard];

The ONLINE = OFF part is to be sure not to collide with KB2969896 on installations not fully patched.
Move non-clustered index
Generate initial script using Management Studio...
USE [my_database];
CREATE NONCLUSTERED INDEX [ix_2nd_table] ON [dbo].[2nd_table] (
  [2nd_table_code] ASC
)
INCLUDE (
  [2nd_table_col2],
  [2nd_table_col3]
) WITH (DROP_EXISTING = ON, ONLINE = OFF) ON [standard];

This is also an example on moving a including index. But that does not make a signifikant difference.
Move unique (clustered) index
In this case I didn't get much help from Management Studio script generation. Most of the script is done by hand.
USE [my_database];
CREATE UNIQUE CLUSTERED INDEX [PK_3rd_table] ON [dbo].[3rd_table]([3rd_table_id])
WITH (DROP_EXISTING =  ON )
ON [standard];

Move Primary Key index
Also inn this case I didn't get much help from Management Studio script generation.
Please notice that the activity is by several statements. To ensure the integrity of the data the statements are put in a transaction.
BEGIN TRANSACTION [move_pk_idx] WITH MARK 'Change 42';
USE [my_database];
ALTER TABLE [dbo].[4th_table] DROP CONSTRAINT [PK_4th_table];
ALTER TABLE [dbo].[my_table] ADD  CONSTRAINT [PK_4th_table] PRIMARY KEY NONCLUSTERED ([4th_table_id] ASC)
WITH (ONLINE = OFF) ON [standard];
COMMIT TRANSACTION [move_pk_idx];

This solution can be used for other constrains like foreign key.
I was inspired by this discussion on StachExchange: „Move Primary Key to Filegroup (SQL Server 2012)“.

Discussion

When moving large amounts of data I wrapped the T-SQL statements in a timing structure like discribed in „Job history template using SQL Server Agent tokens“.

2015-08-14

Attach database

I had to get some data from a old database, where the server is recycled. But at that time I was present enough to get a copy of the data files of the database (mdf & ndf).
The quick solution is to attach the database on my workstation where I have a SQL Server Developer Edition running. This statement should do the trick:
CREATE DATABASE [Repository_old] ON PRIMARY
  (FILENAME = 'C:\Program Files\Microsoft SQL Server\MSSQL12.MSSQL2014\MSSQL\DATA\Repository.mdf'),
  (FILENAME = 'C:\Program Files\Microsoft SQL Server\MSSQL12.MSSQL2014\MSSQL\DATA\Repository_2.ndf')
FOR ATTACH_REBUILD_LOG


But when I tried to attach the database I got a error on file access.
Msg 5120, Level 16, State 101, Line nn
Unable to open the physical file "C:\Program Files\Microsoft SQL Server\MSSQL12.MSSQL2014\MSSQL\DATA\Repository.mdf". Operating system error 5: "5(Access denied.)".

A quick look on the other SQL Server files in the same folder indicated that the service account should be owner of the files. The error message is on the first file in the statement, but this goes on all files for the database.
This can be done in Windows Explorer through the properties of the file (right-click > properties). On the Security tab you click the Advanced button. Then you get another window named Advanced Security Settings for ... In this windows Owner tab you click the Edit button. Now you might have to log on as you are required to be local administrator.
Click the button Other users or groups to enter the service account. This is not straight forward if you are using a virtual service account. First you have to pick the computer as location. Then you can limit the object type to Builtin service accounts.
Finally entering the name of the service account is a little tricky - in my case I entered "nt service\mssql$mssql2014". Notice the space after "nt"!
I am working on the named instance MSSQL2014 which gives the name of the account after the backslash.
Before clicking the OK button I do recommend that you verify the account name by clicking Check names. If the name is changed to capitals, the "nt service\" is removed and the name is underlined it is OK.

The service account is (default) a Virtual Service Account. This is a local edition of a Managed Service Account. To get the name of the service account you can look in the Server Facet of the instance with Management Studio.

But still I got error on file access.
Msg 5120, Level 16, State 101, Line nn
Unable to open the physical file "C:\Program Files\Microsoft SQL Server\MSSQL12.MSSQL2014\MSSQL\DATA\Repository.mdf". Operating system error 5: "5(Access denied.)".

I usually work on installation where DBAs are isolated from other admins. In this case the database files are moved/copied around using one account and the attach statement is executed with another account. When I then added the user of SQL Server Management Studio (SSMS) who executes the statement to the files with Full Control permissions everything went smooth.
I first tried to limit the rights for the SSMS user the Read permissions, but that didn't work as I got this error
Msg 1813, Level 16, State 2, Line nn
Could not open new database 'Repository'. CREATE DATABASE is aborted.


In this case I did not have the transaction log file (ldf), but I really didn't need it. This is why the statement is with FOR ATTACH_REBUILD_LOG. There are some issues you have to consider like a clean shutdown of the database. Please take a look at the documentation for CREATE DATABASE.
The original transaction log file could not be found by the instance, as expected. This generates a failure message like
File activation failure. The physical file name "H:\MSSQL\Data\Repository_log.ldf" may be incorrect.
But right after the new transaction log file is created automatic
New log file 'C:\Program Files\Microsoft SQL Server\MSSQL12.MSSQL2014\MSSQL\DATA\Repository_old_log.ldf' was created.

2012-08-23

Back from vacation


What happened while I was away on vacation?
SELECT
  [databases].[name],
  [server_principals].[name] AS [database_owner_name],
  [databases].[create_date],
  [databases].[state_desc]
FROM [master].[sys].[databases]
INNER JOIN [master].[sys].[server_principals] ON
  [databases].[owner_sid] = [server_principals].[sid]
WHERE
  [databases].[database_id] >= 4 AND  -- Filter out system databases
  [databases].[create_date] >= '2012-07-27';

2012-05-25

Describe database role

When you script out a database role in SQL Server Management Studio (SSMS) or another tool like Red-Gate SQL Compare, you most likely will get just the creation but the rights.
Yesterday I needed the rights for a database role on tables and other database objects. This I got by a small T-SQL script:

:Setvar _RoleName "sqlanchor_user"

DECLARE @script nvarchar(MAX) = N'USE [' + DB_NAME() + N'];' + CHAR(13) + N'GO' + CHAR(13);

SELECT @script += N'CREATE ROLE [' + [db_role].[name] + N'] AUTHORIZATION [' + [owner].[name] + N'];' + CHAR(13)
FROM [sys].[database_principals] AS [db_role]
INNER JOIN [sys].[database_principals] AS [owner]
  ON [db_role].[owning_principal_id] = [owner].[principal_id]
WHERE [db_role].[type] = N'R' AND [db_role].[name] = N'$(_RoleName)';

-- Database object rights
SELECT @script += 'GRANT ' + [db_perm].[permission_name] + ' ON [' + USER_NAME(OBJECTPROPERTY([db_perm].[major_id], 'OwnerId')) + '].[' + OBJECT_NAME([db_perm].[major_id]) + '] TO [' + [db_role].[name] + N'];' + CHAR(13) COLLATE Latin1_General_CI_AS
FROM [sys].[database_permissions] AS [db_perm]
INNER JOIN [sys].[database_principals] AS [db_role]
  ON [db_perm].[grantee_principal_id] = [db_role].[principal_id]
WHERE db_perm.[class] = 1  -- class 1 : Db Object or Column
  AND [db_role].[name] = N'$(_RoleName)';

PRINT @script;


The script only show how to create the database role and assign rights on database objects.
Rights on schemas for example is not by this script. Consider the script as a spike solution, that can be extended.
Take a look in the documentation for "sys.database_permissions" on the possibilities.

2012-02-11

Change database owner

I am about to join an internal course, and would like to prepare by installing the SQL Server Database Product Samples from CodePlex.
After the installation, the databases were owner by my login, and I would like to have the owner changed to "sa". I my case it is renamed, but that is another story.

Renaming a database in SQL Server Management Studio is done on the properties of the database in the page "Files".
When I ask for the change script by clicking the Script drop-down, I get a script that uses the stored procedure sp_changedbowner.
The article on this procedure in Microsoft documentation notes that this feature will be removed, and that I should use ALTER AUTHORIZATION instead.
Then I rewrote the statement to use the recommendation
ALTER AUTHORIZATION ON DATABASE::[AdventureWorks] TO [sql_sa];
Please recall that I have renamed "sa".

The samples generates several databases. Six in my case.
Instead of generating a script for each database, I made a script to generate these scripts
SELECT N'ALTER AUTHORIZATION ON DATABASE::[' + [databases].[name] + N'] TO [sql_sa];' AS [DCL]
FROM [master].[sys].[databases]
WHERE [databases].[owner_sid] != (
  SELECT [server_principals].[sid]
  FROM [master].[sys].[server_principals]
  WHERE [server_principals].[name] = N'sql_sa'
);

I could have generated the scripts for the first principal, as "sa" always is the first, but maybe another day I would like to use another principal.

The generated scripts are like this
ALTER AUTHORIZATION ON DATABASE::[AdventureWorks2008R2] TO [sql_sa];
ALTER AUTHORIZATION ON DATABASE::[AdventureWorksDW2008R2] TO [sql_sa];
ALTER AUTHORIZATION ON DATABASE::[AdventureWorksLT2008R2] TO [sql_sa];
ALTER AUTHORIZATION ON DATABASE::[AdventureWorks] TO [sql_sa];
ALTER AUTHORIZATION ON DATABASE::[AdventureWorksDW] TO [sql_sa];
ALTER AUTHORIZATION ON DATABASE::[AdventureWorksLT] TO [sql_sa];

Dedicated Database Owner

By default the owner is the login that creates or restores the database. This is not a great situation to have a person as owner due to several security issues. Finding databases with the owner not being "sa" can be done with a script like below. To fence a database more in it can be assigned its own dedicated owner which is indicated in the column [expected_owner_name].

SELECT
	[databases].[name] AS [database_name],
	[server_principals].[name] AS [current_owner_name],
	[databases].[name] + N'_owner' AS [expected_owner_name]
FROM [master].[sys].[databases]
INNER JOIN [master].[sys].[server_principals] ON [databases].[owner_sid] = [server_principals].[sid]
WHERE [server_principals].[principal_id] > 1

The dedicated database owner is implemented as a SQL Login where the password is generated on creation. For that I use a script made by Pinal Dave and presented in this post. Creating the owner and the setting it can be done with a script like this:

-- Create owner
DECLARE @_login sysname = N'sqladmin_inventory_owner';  -- Copy from expected_owner_name
DECLARE @_database sysname = N'sqladmin_inventory';     -- Copy from database_name

DECLARE @_stmt_0 nvarchar(max) = N'CREATE LOGIN [' + @_login
	+ N'] WITH PASSWORD=''' + @_password
	+ N''', DEFAULT_DATABASE=[' + @_database
	+ N'],CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF;';
--RAISERROR( @_stmt_0, 0,0 ) WITH NOWAIT;  -- Debug
EXECUTE [master].[sys].[sp_executesql] @stmt = @_stmt_0;

DECLARE @_stmt_1 nvarchar(max) = N'ALTER LOGIN [' + @_login + N'] DISABLE;';
EXECUTE [master].[sys].[sp_executesql] @stmt = @_stmt_1;

-- Change database owner
DECLARE @_stmt_2 nvarchar(max) = N'ALTER AUTHORIZATION ON DATABASE::[' + @_database
	+ N'] TO [' + @_login + N'];';
EXECUTE [master].[sys].[sp_executesql] @stmt = @_stmt_2;

Using parameters with sp_executesql in a DCL statement fails with syntax errors. This is why I dynamically create the statement without using parameters. Not nice but working.

History

  • 2025-05-23 : Section about Dedicated Database Owner added. Links changed from MSDN to current location.
  • 2012-02-11 : Post created.

2011-09-27

Script a database definition

Recreating a database in a higher environment is a common task. Quite often it is not that smart just to restore a back, as you then get database users, file sizes, obsolete schemas etc into the higher environment.
Scripting out a database can be done from SQL Server Management Studio, but this is a manual operation. When you are serving a small army of developers, they need a definition of the production database on a regular basis. Doing that by SQL Server Management Studio quickly gets boring.
There are third party tools like Red Gate SQL Compare that can generate a schema snapshot or a direct comparison, but such a tool is expensive for a simple task like this. In many other situations I am really happy with such tools, but that is another story.
This PowerShell script generates the T-SQL script to create a database and its structure. The script can be edited and reviewed before execution, and can serve as documentation in a Change procedure.
[string]$SqlServerName = '(local)'
[string]$DbName = 'AdventureWorks'

[string]$SqlBatchSeperator = 'GO'  # SQL Server Management Studio default: GO

"/*`nT-SQL script to define the structure of the database '$($DbName)' on the server '$($SqlServerName)'."
"Script generated $($(Get-Date).ToUniversalTime().ToString('s')) UTC"
"  by the user $([Security.Principal.WindowsIdentity]::GetCurrent().Name)"
"  on the computer $($env:COMPUTERNAME)"
"  with the script file '$($MyInvocation.InvocationName)'"
"*/"

[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.Smo') | Out-Null
$Server = New-Object Microsoft.SqlServer.Management.Smo.Server $SqlServerName
$Options = New-Object Microsoft.SqlServer.Management.Smo.ScriptingOptions
$Options.IncludeHeaders = $true
$Options.Permissions = $true

"`n--*** Logins ***"
$Server.Logins | Where-Object { ($_.Name -ne 'sa') -and ($_.Name -notlike '##MS_Policy*') -and ($_.Name -notlike 'NT *') } | ForEach-Object { $_.Script($Options);$SqlBatchSeperator }
"`n--*** Database definition ***"
$Server.Databases[$DbName].Script();$SqlBatchSeperator
"`n--EXECUTE [$DbName].dbo.sp_changedbowner @loginame = N'sa', @map = true;`n$SqlBatchSeperator"
"`nALTER AUTHORIZATION ON DATABASE::[$DbName] TO [sa];`n$SqlBatchSeperator"
"`nUSE [$DbName];`n$SqlBatchSeperator"
"`n`n--*** Userdefined applicationroles ***"
$Server.Databases[$DbName].ApplicationRoles | Where-Object { $_.IsFixedRole -eq $false } | ForEach-Object { $_.Script($Options);$SqlBatchSeperator }
"`n`n--*** Userdefined databaseroles ***"
$Server.Databases[$DbName].Roles | Where-Object { $_.IsFixedRole -eq $false } | ForEach-Object { $_.Script($Options);$SqlBatchSeperator }
"`n`n--*** Userdefined schemas ***"
$Server.Databases[$DbName].Schemas | Where-Object { $_.IsSystemObject -eq $false } | ForEach-Object { $_.Script($Options);$SqlBatchSeperator }

$Options.IncludeDatabaseRoleMemberships = $true

"`n`n--*** Database users ***"
$Server.Databases[$DbName].Users | Where-Object { $_.IsSystemObject -eq $false } | ForEach-Object { $_.Script($Options);$SqlBatchSeperator }

$Options.DriAll = $true
$Options.Indexes = $true

"`n`n--*** Userdefined assemblies ***"
$Server.Databases[$DbName].Assemblies | Where-Object { $_.IsSystemObject -eq $false } | ForEach-Object { $_.Script($Options);$SqlBatchSeperator }
"`n`n--*** Userdefined certificates ***"
$Server.Databases[$DbName].Certificates | Where-Object { $_.IsSystemObject -eq $false } | ForEach-Object { $_.Script($Options);$SqlBatchSeperator }
"`n`n--*** Userdefined defaults ***"
$Server.Databases[$DbName].Defaults | Where-Object { $_.IsSystemObject -eq $false } | ForEach-Object { $_.Script($Options);$SqlBatchSeperator }
"`n`n--*** Userdefined fulltext catalogs ***"
$Server.Databases[$DbName].FullTextCatalogs | Where-Object { $_.IsSystemObject -eq $false } | ForEach-Object { $_.Script($Options);$SqlBatchSeperator }
"`n`n--*** Userdefined rules ***"
$Server.Databases[$DbName].Rules | Where-Object { $_.IsSystemObject -eq $false } | ForEach-Object { $_.Script($Options);$SqlBatchSeperator }
"`n`n--*** Userdefined stored procedures ***"
$Server.Databases[$DbName].StoredProcedures | Where-Object { $_.IsSystemObject -eq $false } | ForEach-Object { $_.Script($Options);$SqlBatchSeperator }
"`n`n--*** Userdefined tables with indexes ***"
$Server.Databases[$DbName].Tables | Where-Object { $_.IsSystemObject -eq $false } | ForEach-Object { $_.Script($Options);$SqlBatchSeperator }
"`n`n--*** Userdefined Triggers ***"
$Server.Databases[$DbName].Triggers | Where-Object { $_.IsSystemObject -eq $false } | ForEach-Object { $_.Script($Options);$SqlBatchSeperator }
"`n`n--*** Userdefined functions ***"
$Server.Databases[$DbName].UserDefinedFunctions | Where-Object { $_.IsSystemObject -eq $false } | ForEach-Object { $_.Script($Options);$SqlBatchSeperator }
"`n`n--*** Userdefined datatypes ***"
$Server.Databases[$DbName].UserDefindTypes | Where-Object { $_.IsSystemObject -eq $false } | ForEach-Object { $_.Script($Options);$SqlBatchSeperator }
"`n`n--*** Userdefined views ***"
$Server.Databases[$DbName].Views | Where-Object { $_.IsSystemObject -eq $false } | ForEach-Object { $_.Script($Options);$SqlBatchSeperator }


The script covers the basics, and I think that it is easy to fit it to a equal task.

The script can be used in a Visual Studio solution to compare with a database in development. Such a comparison can give a deployment script for a higher and isolated environment.

Reference

TechNet Library: SMO "Server Class"

2009-09-26

Database version

When the daily administration of several database instances is done using a standard database, it is rather nice to be able to check that all management database have the same structure.
A quick solution is to calculate a checksum of the database objects and its columns.

To hold the version number of the database and its checksum a (singleton) table is created.
CREATE TABLE [dbo].[Database_Version]
(
    [Database_Version_No] nvarchar(128) NOT NULL,
    [Database_Version_Checksum] bigint NOT NULL,
    CONSTRAINT PK_Database_Version PRIMARY KEY ([Database_Version_No])
);
The table can be initialized by a INSERT statement:
INSERT INTO [dbo].[Database_Version] ([Database_Version_No], [Database_Version_Checksum])
VALUES ('0', 42);

The table is used by two stored procedures - one to read:
CREATE PROCEDURE [dbo].[Get_Database_Version]
AS
BEGIN
    SET NOCOUNT ON;
   
    -- Check stored checksum
    --   Calculate checksum
    DECLARE @objects_checksum bigint;
    SELECT @objects_checksum = SUM(CAST(CHECKSUM(OBJECT_DEFINITION([object_id])) AS bigint))
    FROM sys.objects
    WHERE is_ms_shipped = 0;
    DECLARE @columns_checksum bigint;
    SELECT @columns_checksum = SUM(CAST(CHECKSUM(OBJECT_DEFINITION([column_id])) AS bigint))
    FROM sys.columns;
    --   Check is the checksum stored is equal to the calculated checksum
    IF (@objects_checksum + @columns_checksum) <> (
        SELECT [Database_Version_Checksum]
        FROM [dbo].[Database_Version]
    )
    BEGIN
        DECLARE @calculated_checksum nvarchar(128) = CAST((@objects_checksum + @columns_checksum) AS nvarchar(128));
        DECLARE @stored_checksum nvarchar(128) = CAST((SELECT [Database_Version_Checksum] FROM [dbo].[Database_Version]) AS nvarchar(128));
        RAISERROR(N'The calculated checksum (%s) is not equal to the stored checksum (%s).',10, 1,@calculated_checksum,@stored_checksum) WITH NOWAIT;
    END
   
    -- Get stored checksum
    SELECT [Database_Version_No], [Database_Version_Checksum]
    FROM [dbo].[Database_Version];
END
...and one to update:
CREATE PROCEDURE [dbo].[Set_Database_Version]
    @Database_Version_No nvarchar(128)
AS
BEGIN
    SET NOCOUNT ON;
   
    -- Check if the given Database Version value is different from the existing
    IF @Database_Version_No = (
        SELECT [Database_Version_No]
        FROM [dbo].[Database_Version]
    )
    RAISERROR('You have to update the Database Version to another value than the existing one.', 18, 0) WITH NOWAIT;

    -- Calculate checksum
    DECLARE @objects_checksum bigint;
    SELECT @objects_checksum = SUM(CAST(CHECKSUM(OBJECT_DEFINITION([object_id])) AS bigint))
    FROM sys.objects
    WHERE is_ms_shipped = 0;
    DECLARE @columns_checksum bigint;
    SELECT @columns_checksum = SUM(CAST(CHECKSUM(OBJECT_DEFINITION([column_id])) AS bigint))
    FROM sys.columns;
   
    -- Check is the checksum stored is equal to the calculated checksum
    IF (@objects_checksum + @columns_checksum) = (
        SELECT [Database_Version_Checksum]
        FROM [dbo].[Database_Version]
    )
    RAISERROR('The calculated checksum is equal to the stored checksum.',18, 1) WITH LOG;
   
    -- @objects_checksum + @columns_checksum
    UPDATE [dbo].[Database_Version]
    SET [dbo].[Database_Version].[Database_Version_No] = @Database_Version_No,
        [dbo].[Database_Version].Database_Version_Checksum = @objects_checksum + @columns_checksum;
END


The get procedure also checks if the stored checksum is like to actual calculated checksum. Of course the checksum inserted above is not correct, so the get procedure raise an error:
EXEC [dbo].[Get_Database_Version];
{Message} The calculated checksum (3381089942582) is not equal to the stored checksum (42).

The update procedure calculate a checksum and update the stored checksum. The version number is actually just a string, so you can make your own version standard.
EXEC [dbo].[Set_Database_Version] @Database_Version_No = '3.1';
The get procedure now executes with success.
Database_Version_No   Database_Version_Checksum
-------------------   -------------------------
3.1                   3381089942582

If a object is changes, e.g. a column is added
ALTER TABLE [dbo].[Database_Version] ADD
    [Database_Version_Description] nvarchar(MAX) NULL;

Now the get procedure raises a error
The calculated checksum (3383237426229) is not equal to the stored checksum (3381089942582).
If the column is dropped,
ALTER TABLE [dbo].[Database_Version]
    DROP COLUMN [Database_Version_Description];

the get procedure executes with success.

Discussion
The database version objects are in the schema "dbo", but you might want to keep this schema clean.
When the get procedure fails, you have no indication on what is different. You could use a comparison tool like Red Gate SQL Compare.
There is no timestamp in the solution, but you could format the version as a timestamp, e.g. "2009-09-17A".
The version checksum calculation is somewhat simple. It could also include indexes, users, files and what else you need.
A version and checksum could be calculated and stored for each database. Some changes to the version objects are needed, but it should not be a problem to the dedicated DBA.