Bug: Local View + Synonym + Remote Query

Recently I came across an interesting bug when upgrading a SQL Server 2000 box to 2008 R2. With SQL Server 2005, 2008, and 2008 R2, there is a situation where you are calling a local view that is also using a synonym and accessing a remote table and then the remote table’s index is rebuilt. In this situation the synonym should not fundamentally change the way the query operates but in the versions listed you will receive the below error.

The OLE DB provider "SQLNCLI" for linked server "REMOTESERVER" reported a change in schema version between compile time ("170677722198508") and run time ("170677722198546") for table ""tempdb"."dbo"."remotetable"".

This occurs because the execution plan of the view is set at compile time and the act of creating and dropping an index is in fact a schema change. As described here, with an index rebuild you are affecting a schema change and therefore the schema versions will be different and throw the error above.

This bug has been accepted by Microsoft (as referenced here) and has been fixed in SQL Server 2012 RTM. See below for a test case that reproduces this bug.

I have used the same test case with 2012 and witnessed that the bug is in fact fixed.

Work Arounds:

  • Re-write the process to exclude any one of the necessary factors which cause the bug.
  • Create a local view on the remote server and referencing that in the synonym.
  • Use sp_refreshview before calling the view.
    • This was the simplest fix in my situation because no one wanted to affect a code change, this ran in a SQL Agent job and the view was only used once a month. So I just added a new step prior to the stored procedure call to refresh the view.

Test Case:

The following script runs in SQLCMD mode and will generate the necessary test case and provides print out statements to walk you through to successful and buggy situations.

The test script was pulled from a post here by Jasper Smith.

Download: linked server view synonym tests.txt

Detach and Attach User Databases

The Need

I’ve found myself, more times than once, needing to change disk drives that my user databases are found on. This could come up because you are moving a server and need to switch SANs, migrate from local storage to SAN storage, or need to move to a new LUN because it was over expanded and your SAN techs can’t shrink it without a drop/create.

This exercise can be done with the GUI or with typed T-SQL but in my cases there are usually a couple of dozen databases with file in a number of different sub-directories. Today I’m going to go over each method but conclude on a T-SQL script that I wrote to do almost all of the work for us.

Method #1: T-SQL

To detach a database and re-attach you will be using two different types of T-SQL. For the detach method you will be using the stored procedure sp_detach_db while for the attachment you will be using a CREATE DATABASE command with the FOR ATTACH option.

The syntax for the detachment on SQL Server 7.0 through 2012:

USE master;
GO
EXEC sp_detach_db @dbname = N'AdventureWorks2012';
GO

Syntax for the attachment on SQL Server 2005 through 2012:

USE master;
GO
CREATE DATABASE MyAdventureWorks 
    ON (FILENAME = 'C:\MySQLServer\AdventureWorks2012_Data.mdf'),
    (FILENAME = 'C:\MySQLServer\AdventureWorks2012_Log.ldf')
    FOR ATTACH;
GO

Syntax for the attachment on SQL Server 7.0 and 2000:

use master
go
sp_attach_db 'mydb','E:\Sqldata\mydbdata.mdf','E:\Sqldata\mydblog.ldf'
go

Method #2: GUI via SSMS

  1. Detach
    1. Right click the database that you wish to detach and select Tasks->detach.
    2. Check “Drop Connections” so that your detach won’t be prone to timing out due to a persisted connection.
    3. Click OK.
  2. Attach
    1. Right click Databases and select Attach…
    2. When the Attach Databases window opens, click Add…
    3. Locate and select the mdf file for your database and click OK.
    4. When your mdf file is selected the “database details” group box will be populated with a list of all of the files that are associated to the mdf. In my screenshot below the log file was discovered because it was in the same directory. If your ldf or ndf files are not in that same location there will be text in the “Message” field that will indicate that you need to tell it where the file is. You would, in this case, click the ellipsis that I have highlighted and navigate to the file in question.
    5. Click OK and your databases will be attached.

Method #3: T-SQL Script Generation

Below is the script I wrote to write all of my detach and attach statements for me. It first prints out the detach statements but it queries for the database file locations and builds your attachment statements as well.

Important: Once you run the detach scripts you will no longer be able to query the file information so don’t make the mistake of running these in the same query window as the print statements.

DECLARE @name VARCHAR(50)
DECLARE @path VARCHAR(MAX)
DECLARE @execSQL VARCHAR(MAX)
DECLARE @filename VARCHAR(MAX)
DECLARE @database_ID INT

--Place the directory paths here
DECLARE @newdatapath VARCHAR(MAX) = 'Q:\InstanceName\'
DECLARE @newlogpath VARCHAR(MAX) = 'Z:\InstanceName\'

--populate file locations
SELECT name, physical_name AS CurrentLocation, database_id
INTO #temptable
FROM sys.master_files
WHERE [database_id] > 4

--detach
DECLARE detach_cursor CURSOR FAST_FORWARD FOR
SELECT name, physical_name AS CurrentLocation, database_id
FROM sys.master_files
WHERE file_id = 1
AND name <> 'master'
AND name <> 'tempdev'
AND name <> 'msdbdata'
AND name <> 'modeldev'

OPEN detach_cursor
FETCH NEXT FROM detach_cursor INTO @name, @path, @database_ID

PRINT  'USE master;' + CHAR(13) + CHAR(10)

WHILE @@FETCH_STATUS = 0
BEGIN
	SET @execSQL = 'EXEC sp_detach_db @dbname = N''' + DB_NAME(@database_ID) + ''';'
	PRINT @execSQL

	FETCH NEXT FROM detach_cursor INTO @name, @path, @database_ID
END
CLOSE detach_cursor
DEALLOCATE detach_cursor

--attach
PRINT CHAR(13) + CHAR(10) + CHAR(13) + CHAR(10)

DECLARE attach_db_cursor CURSOR FAST_FORWARD
FOR
        SELECT DISTINCT [database_id]
        FROM    #temptable

OPEN attach_db_cursor
FETCH NEXT FROM attach_db_cursor INTO @database_ID

PRINT 'USE master;' + CHAR(13) + CHAR(10)

WHILE ( SELECT  fetch_status
        FROM    sys.dm_exec_cursors(@@SPID)
        WHERE   name = 'attach_db_cursor'
      ) = 0 
      BEGIN

			SET @execSQL = 'CREATE DATABASE ' + DB_NAME(@database_ID) + ' ON' + CHAR(13) + CHAR(10)

            DECLARE attach_file_cursor CURSOR FAST_FORWARD
            FOR
                    SELECT  [name]
                          , [CurrentLocation]
                          , [database_id]
                    FROM    #temptable
                    WHERE [database_id] = @database_ID

            OPEN attach_file_cursor
            FETCH NEXT FROM attach_file_cursor INTO @name, @path, @database_ID

            WHILE ( SELECT  fetch_status
                    FROM    sys.dm_exec_cursors(@@SPID)
                    WHERE   name = 'attach_file_cursor'
                  ) = 0 
                  BEGIN
                        SET @filename = @path
                        WHILE CHARINDEX('\', @filename, 0) <> 0 
                              BEGIN
                                    SET @filename = RIGHT(@filename,LEN(@filename) - CHARINDEX('\',@filename, 0))
                              END

						IF RIGHT(@filename,3) = 'ldf'
						BEGIN
							SET @execSQL = @execSQL + '(FILENAME = ''' + @newlogpath + @filename + '''),' + CHAR(13) + CHAR(10)
						END
						ELSE
						BEGIN
							SET @execSQL = @execSQL + '(FILENAME = ''' + @newdatapath + @filename + '''),' + CHAR(13) + CHAR(10)
						END

                        FETCH NEXT FROM attach_file_cursor INTO @name, @path,
                              @database_ID
                  END

                  SET @execSQL = LEFT(@execSQL,LEN(@execSQL)-3) + CHAR(13) + CHAR(10)
                  SET @execSQL = @execSQL + ' FOR ATTACH;'

                  CLOSE attach_file_cursor
				  DEALLOCATE attach_file_cursor

				  PRINT @execSQL

             FETCH NEXT FROM attach_db_cursor INTO @database_ID
      END

CLOSE attach_db_cursor
DEALLOCATE attach_db_cursor

DROP TABLE #temptable

I currently have this script setup to accept a data and log path in the variables @newdatapath and @newlogpath at the top of the script. This is for if you know the directories that you would like to move the databases to and if they are all the same for each database. I find this situation common but I have come across situations where I wanted to move drives but not change the folder structure. In that case I would build the folder structure and then replace the below lines of the script.

--Before
IF RIGHT(@filename,3) = 'ldf'
BEGIN
	SET @execSQL = @execSQL + '(FILENAME = ''' + @newlogpath + @filename + '''),' + CHAR(13) + CHAR(10)
END
ELSE
BEGIN
	SET @execSQL = @execSQL + '(FILENAME = ''' + @newdatapath + @filename + '''),' + CHAR(13) + CHAR(10)
END

--With change
IF RIGHT(@filename,3) = 'ldf'
BEGIN
	SET @execSQL = @execSQL + '(FILENAME = ''' + REPLACE(REPLACE(@path,'E:\','Q:\'),'F:\','Z:\') + '''),' + CHAR(13) + CHAR(10)
END
ELSE
BEGIN
	SET @execSQL = @execSQL + '(FILENAME = ''' + REPLACE(REPLACE(@path,'E:\','Q:\'),'F:\','Z:\') + '''),' + CHAR(13) + CHAR(10)
END

Success

Using method #3 I was able to relocate the database files on 2 servers with 5 instances each and 1-2 dozen databases per instance in less than 10 minutes.

Now I just need to move the system databases.

SQL Server 2008 R2 – Service Pack 2

Quick Announcement:

I just wanted to throw a quick announcement out that SQL Server 2008 R2 SP2 is available.

Service Pack 2 includes Cumulative Updates 1 through 5 from SQL Server 2008 R2 SP1 and is registered as version # 10.50.4000.0.

You can see a complete list of bugs fixed by this SP in KB2630458.

Downloads:

SQL Server 2008 R2 SP2

SQL Server 2008 R2 SP2 – Express

SQL Server 2008 R2 SP2 – Feature Pack

UPDATE:

Just about a week later Microsoft has already released CU1 for SP2. See additional details here. In addition to the 35 bug fixes, one good thing about CU1 is that is comes with CU 6 and 7 from SP1 which was not prepackaged with SP2.

How to: Transfer Logins to a New Server

Often enough we encounter a situation where we need security to be migrated. Microsoft hasn’t made this task as easy as moving other things such as SQL Agent jobs since restoring the master database to another server is tricky at best but they do provide us some tools.

A couple of common reasons that you’d want to move logins is if you are standing up a test server and want the servers to match identically or you are standing up a disaster recovery server and might need to re-sync logins as changes are made to the primary.

Preferred Method: T-SQL

I personally prefer using T-SQL over SSIS whenever the situation leaves to the two methods on an even playing field so I use the Microsoft provided stored procedures even though I will go over the SSIS method below.

sp_help_revlogin is a stored procedure which will satisfy two main feats while printing out all of the necessary create statements for all of your instance logins. The first is that it returns as part of the create statements all of the SIDs for your SQL accounts. Domain accounts don’t require this step but when creating a new SQL account and then restoring a legacy database to that instance you often are left with orphaned users because the database level user exists and so does the login but their SIDs don’t match. The second feat is that it returns the SQL account passwords in hex format so that you aren’t exposing the passwords to anyone. But DBAs who have no need to know those passwords can still create the new user.

As found in KB918992 and KB246133 there are two stored procedures that Microsoft provides but are not installed implicitly. The first is sp_hexadecimal which is a function used to pull SQL account passwords in hex so that they aren’t directly exposed to the person executing the stored procedure sp_help_revlogin. sp_help_revlogin is the primary procedure that you execute and it takes a single optional parameter @login_name (sysname). @login_name is NULL by default and omitting it will return all logins for the instance. If you pass in the login name then you will receive the statement for just that user.

NOTE: The KB articles where you get the stored procedure only list versions 7.0 through 2005 as applicable but I have tested this on 2008 and 2008 R2 and it works fine.

 Example Syntax:

EXEC dbo.sp_help_revlogin
--OR
EXEC dbo.sp_help_revlogin @login_name = 'BlogTester' -- sysname

 Example Output:

/* sp_help_revlogin script 
** Generated Aug 20 2012  8:24AM on V-DEV-DB-011\vb18 */

-- Login: BlogTester
CREATE LOGIN [BlogTester] WITH PASSWORD = 0x01000D1F43BB2A1F306EC90F5352291E8DD273549DB4AF950845 HASHED, SID = 0xD831296B0274D448A5748A52B8C796EA, DEFAULT_DATABASE = [master], CHECK_POLICY = OFF, CHECK_EXPIRATION = OFF

Permissions:

Only members of the fixed server role ‘sysadmin’ can access the system table master..sysxlogins which is required to execute sp_help_revlogin therefore you will need to be a sysadmin in order to run this procedure.

SSIS Method: Transfer Logins Task

BIDS provides us with the Transfer Logins Task. This task will transfer logins from one SQL Server source to one SQL Server destination. It can transfer all logins, a single login, or specific logins selected as a collection. It also can be configured to overwrite, skip, or throw an error when it reaches a login that already exists.

See the technet post for more details.

Permissions:

This method also requires you to be a sysadmin of the source server, in addition, it requires sysadmin to the destination server.

Beware of this method

The SSIS Method does not pull over existing passwords like the T-SQL method does. Instead it will assign random passwords to each of the SQL accounts that it creates. You will then after to go through and manually (or via script) update every password for the accounts that were created.

Important Planning Note

The create statements with both methods will include the default database for the login. If this is not set to master or a database that already exists on the destination then there will be errors and at least the create statement in question will fail. I find it easiest to handle security after I’ve done most other tasks involved in my migration or instance sync.

PowerShell SQL Connection Test

In the interest of providing minimal surface area exposure to my servers I have taken on the common practice of not installing development tools on any of my Production servers. In almost all cases this is no big deal because I’d prefer to use SSMS from a remote central location anyways but there are a few situations where you would want to know if you can connect locally even though your remote connections fail. Below is a quick and easy PowerShell script for testing a SQL connection, it doesn’t have to be run locally but this is the situation where I like to use it.

Parameter:

This script takes a single string parameter for the server name. This function can easily be modified to accept an array of server names or even read from a text file server list.

Script Version:
param
(
[parameter(Mandatory=$TRUE,Position=0)]
[String] $Server
)

$connectionString = "Data Source=$Server;Integrated Security=true;Initial Catalog=master;Connect Timeout=3;"
$sqlConn = new-object ("Data.SqlClient.SqlConnection") $connectionString
trap
{
Write-Error "Cannot connect to $Server.";
continue
}
$sqlConn.Open()

if ($sqlConn.State -eq 'Open')
{
$sqlConn.Close();
"Opened successfully."
}

Function Version:
Function Test-SQLConn ($Server)
{
$connectionString = "Data Source=$Server;Integrated Security=true;Initial Catalog=master;Connect Timeout=3;"
$sqlConn = new-object ("Data.SqlClient.SqlConnection") $connectionString
trap
{
Write-Error "Cannot connect to $Server.";
continue
}
$sqlConn.Open()

if ($sqlConn.State -eq 'Open')
{
$sqlConn.Close();
"Opened successfully."
}
}

Huge distribution database

The Problem:

Today I noticed the data drive of one of my Production servers growing. It wasn’t growing overly fast and it had been quite some time since the last SAN drive expansion so I gave the thumbs up to my SAN techs to add 5GBs of space. I started with 5GBs just out of a hunch even though I knew in a couple weeks I’d be giving the green light to add more space again.

It turned out that adding 5GBs at first was a good idea because what I assumed with normal data growth turned out to be a ballooning distribution database. When I noticed my disk space alert again in the same day is when I got wise because this server should not grow 5 GBs in a day.

After a quick look it was easy to notice the distribution database was the issue with it being 48GBs which is far above normal for the publications that it handles.

The Cause:

I went ahead and pulled up my friend, the replication monitor, and noticed a red X over the publisher with the ballooned distribution database. Upon further investigation I noticed 5 publications were red and the offending subscription in each was for the same subscriber (our source database for our data warehouse which is used to perform warehouse tuning operations separate from the transactional data).

I pulled up the subscriber and noticed that it has no subscriptions. What apparently happened was, one of my DBAs had done a restore without using the KEEP_REPLICATION flag. This restored the database who was a subscriber but broke the transfer. Since the source still had the subscription registered in it all the transactions for the last 72 hours were being held and not purged because the default retention period for a distribution database is 72 hours.

The Fix:

So what I ended up doing was first dropping all of the subscriptions that were broken and then recreating them. This fixed the root cause but my distribution database was still large and had 0% free space to shrink.

What I had to do next was hit up the Google. I knew that the job ‘Distribution clean up: distribution’ was running and would eventually clean up my database but when I have a problem I like to stick to it until fixed and this job was running slowly. That is when I found clsilva’s post Replication: Distribution Cleanup and Paul Ibison’s post How to Massively Optimise Transactional Replication.

The command that clsilva talks about, EXEC dbo.sp_MSdistribution_cleanup @min_distretention = 0, @max_distretention = 72, is actually exactly what the job, Distribution clean up: distribution is running that is slow.

This process is slow because it is trying to purge a row count, which in my case, is very large. I ended up stopping this job and running the commands myself in steps as recommended by clsilva. I started with a 2 hour block and found that I was deleting records at a 2 row per second rate. This was sufficient evidence that to me that the process was running successfully but was simply very slow. When you have a working system stored procedure I never dive down deeper and try to tweak things myself. So what I ended doing was executed a script that ran this command decrementing the max and min values by 10 hours a piece in separate batches. The only purpose of this was so that I could have a 10 hour incremented print out in SSMS that would reassure me and give me a pseudo-progress bar.

The End

Upon completion of my script I shrunk my distribution database down to its normal size and went on with my day.

Automated Unattended (Silent) SQL Server Installs with PowerShell

Last time we covered the basics of unattended installs in my post, Unattended (Silent) SQL Server Installations. Here I will be providing and explaining my PowerShell script for facilitating these installations.

Download the script here: InstallSQL_Silent_SQLHammer.zip.
This script is also Featured on SQLServerCentral.com.

As the help context describes, this script is intended to perform unattended installations (not upgrades) of stand-a-lone or clustered SQL Server instances. It will create the configuration.ini file necessary and it will produce the Add Node.ini file if you are installing on a Windows Cluster, in addition to providing you the command line execution string and/or run the install itself.

Caveats:

  • SSRS and SSAS are not supported in this script.
  • Upgrades are not supported.
  • It assumes that your Product Key is stored with your installation media. This would be found at <media_root>\<platform ie. x64>\Default Setup.ini and would look like this PID=”AAAAA-12345-BBBBB-67890-CCCCC”.
  • It assumes that your clustered network name is default for your IP address (Cluster Network 1).
  • The “UseDefaults” feature is available but commented out because I removed all of my company’s standard settings so that you can fill in your own. Since the options are not set to any real settings I hard coded $Script:DefaultChoice =  ”NO” in the main section.

Code Structure:

When writing this script I had modular coding in mind. My script has four main sections, Help Context which is used to make this script compatible with PowerShell’s native Get-Help cmdlet. Includes which is a Windows Form Assembly, a COM object and a Clipboard Alias. Functions which is the bulk of the code to run the interview, file writing, and installation. Finally, main, a term many C programmers are familiar with. The main section is simply where I placed all of the logic for traveling through the functions. I attempted to minimize the amount of code in the main function and maximize segregation of the functions so that they could be easily re-ordered or modified with as little code change as necessary, a feature that many script writers omit from their designs.

I chose to use variable pass ins for the function parameters but I used $Script: scoped variables for setting variables from within the functions. I imagine arguments could be made as to whether this was the best choice or not but for this post’s purposes it’s only important that you understand the logic flow.

High-Level Work Flow Explanation:

The script begins by loading the Help Context, the Includes and Functions.

It then transitions into the main section where the first five function calls are not specific to this installation. You are prompted to choice if you’d like to use defaults or not (commented out so you can setup your own defaults with the commented code shell), there is a welcome message in the form of a Windows dialog pop-up, you are prompted to select an environment if you are using defaults, then you select your installation type (clustered, stand-alone, or add node) before setting your path for the configuration file(s).

NOTE: In the SetFilePath function there is a nice feature that allows you to navigate to your configuration file location using a dialog box. I did not use this dialog feature for selecting the directories for the database files because in a cluster your available storage is not always on the node that you are running this script, therefore you will not be capable of selecting the drive(s). As noted above, this code exists right here in the SetFilePath function so if you wanted you could take this and incorporate this feature into the Stand-Alone installations very easily. I did not do this myself because I may install 1 stand-alone instance for every 30 clustered instances and the inconvenience was not large.

Next you will hit the primary switch of the script where it will select different function logic based on your installation type. Both stand-alone and clustered installations have a similar flow even though what is written to the configuration file(s) is different so I will speak to them both interchangeably.

Once into the meat of the script you will either begin answering a bunch of questions or your DefaultChoice option will be nicely filling these answers in for you. The function WriteNonConfigurableOptions will initialize and output to your configuration file settings which are either mandatory or things that you shouldn’t need to change from installation to installation.

NOTE: All file writes are Appends so if you ever exit the script or make a mistake you haven’t lost your work. The script is not designed to pick-up where it left off if you accidentally exited but your config file will be partially created so you can use it as a reference or just manually expand from there. Also, it is useful if you make a typo. The lines are appended so if a typo occurs you can open you config and just edit the last line that was written, save, and move on with the script.

Once the non-configurable options are written, we move on to the interview. You will be asked for network name, instance name, feature selection, sysadmin accounts, service accounts, and file directories. All of these are in a text based prompting format and where you are free-form typing there are examples for syntax.

NOTE: If you see an example like this: (Do not include the trailing ‘\’) eg. J: or J:\SQLServer, please don’t input J:\ or J:\SQLServer\.

Upon completion of the interview you will be provided an exit message and then asked if you would like to execute now. The alternative is to simply print out the command line executable and copy it to your clipboard. If you choose to execute now then you must be running this script locally from the host that you are intending upon installing it on. Running it from the PowerShell console on the host using “\\workstation\PSscripts\InstallSQL_Silent.ps1″ will not work, nor will it work if you run it from your workstation’s PS console.

In most cases I simply select “NO” so that it will print the command line and copy it to my clipboard. I will usually then paste it into a text file along with the configuration files for later use. This way I can always reuse the command line and files without having to do much thinking.

Add Node Section:

The last part that has not been discussed is the add node section. You will notice in the main switch if you have $InstallChoice -eq “INSTALLCLUSTER” there is a function called WriteAddNodeFile. This function works because all of your variables that are needed have already been set from the full interview. Since these variables are not set already when the ADDNODE selection is made the script runs through a truncated version of the interview and gets you finished rather quickly using the same work-flow as the other types.

Wrap-Up:

Please play around with my script and let me know what you think of it. I HIGHLY ENCOURAGE  suggestions, complaints or anything of the sort. If you work in a situation which I didn’t not account for, let me know and I’ll provide updates. My goal is to be able to provide a script that is 100% universal. This script is not there yet but with your help it can be.