All posts by Miguel

Failed Job History

List all jobs that have failed in the last week

 

/*
DESCRIPTION    List Jobs that have failed in the last 7 days.
CONFIGURATION   none
 
Compatibility list:
MSSQL2005
MSSQL2008
MSSQL2000
 */

SELECT 
DISTINCT msdb.dbo.agent_datetime(run_date, run_time) as 'RunDateTime',
t2.name 'Job Name',
T1.step_id AS 'Step_id', 
T1.step_name AS 'Step Name',
 
case
when run_status=0 then 'Failed'
when run_status=1 then 'Succeeded'
when run_status=2 then 'Retry'
when run_status=3 then 'Canceled'
else ''
end as Status,
LEFT(T1.[message],500) AS 'Error Message'

  
 FROM msdb..sysjobhistory T1 
 JOIN msdb..sysjobs T2 ON T1.job_id = T2.job_id
 
  WHERE 
  T1.run_status NOT IN (1,4) 
  AND T1.step_id != 0 
  and msdb.dbo.agent_datetime(run_date, run_time) > DateAdd(day,-7,GetDate())

Searching the Event Viewer

 

Reading the Event Viewer is always useful to find error messages and poke around looking for clues to what's happening on the server.

The problem is that the interface is slow, not easy to filter results, and it's cumbersome to only see results corresponding to a specific timeframe. Once we get familiar with the powershell cmdlets, it becomes an efficient way to read the Event Viewer logs.

To read the event log, use the cmdlet Get-EventLog. Next choose if you want log you want to read using -LogName Application or -LogName System or -Logname Security

You can mix and match pretty much all of the parameters and techniques below:

  • If you want the 10 -newest events:
Get-EventLog -LogName Application -Newest 10
  • To actually read the messages, pipe the output to a Formatted list, and use the -Property to select what to see (use wild cards to see them all):
Get-EventLog -LogName Application -Newest 10 | Format-List -Property EntryType, Message, Source, TimeWritten
  • If you have too many results on your screen, you can use Out-Host -Paging, very similar to the "more" command in a Unix shell, to "page out" through your results
Get-EventLog -LogName Application -Newest 100 | Out-Host -Paging

 

Now that you can display your logs, we'll work on filtering the results:

  • The first set of useful filter is the -BEFORE and -AFTER parameters. Per example if you want the events between two dates:
Get-EVentLog -LogName Application -Before '02/22/2016 15:00' -After '02/22/2016 12:00'
  • I really like to use -BEFORE with -NEWEST, so I can get "all 10 previous events prior to the date in BEFORE".
Get-EVentLog -LogName Application -Before '02/22/2016 15:00' -Newest 10
  • The -EntryType is useful to only see certain type. It accepts entries such as Error, Information, FailureAudit, SuccessAudit, and Warning. For Example, if you want to see the entries, but not having to sift through the "Login Successful" entries, you can specify what to you want to see:
Get-EventLog -LogName Application -Newest 20 -EntryType 'Error','Information','FailureAudit','Warning'
  • To list events only from a specific -source, like MSSQLSERVER$
Get-EventLog -LogName Application -Newest 20 -EntryType 'Error','Information','FailureAudit','Warning' -Source MSSQL*
  • To search for a string, like "error" within the -message:
Get-EventLog -LogName Application -Newest 20 -EntryType 'Error','Information','FailureAudit','Warning' -Message '*error*'
  • However, if you want to search and filter out some strings, you will need to pipe the objects to a filter using where-Object
Get-EventLog -LogName Application -EntryType 'Error','Information','FailureAudit','Warning' -Newe
st 20 | Where-Object {$_.Message -notlike "Security policies*" -and $_.Message -notlike "Error reading*"}

  • Lastly, since most servers clear or recycle the event log, It's always important to know what's the earliest event recorded… and you know that by running:
Get-EventLog -LogName System -message '*cleared.'

or you can also filter based on source:

Get-EventLog -LogName System -Soruce '*Event'

Finding Location of ErrorLog

Note: While this script will find the errorlog on all servers with a single SQL instance, it requires Powershell version 3 when multiple versions are present (looking into fixing it). We cannot use the #Require -Version parameter because it's only available on versions 4 and higher.

 

Finding the Location of the ErrorLog on a server can be done by reading the start up parameters of SQL.

The start up parameters  are stored in the Registry, however the location can change based on the version of SQL server, whether there is a default instance or named instance, or even if there are many SQL instances present on the server.  However, we know that parameters are stored inside:

$RegKey = "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL??*\MSSQLServer\parameters"

1-Registry

Once we find the exact Registry location, we need to consider that there maybe more than three entries there, depending on the number of start up parameters, which can be affected by the presence of trace flags, so we would want to return all the SQLArg entries.

The following commands return a PS Object with the Registry information:

$RegKey = "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL??*\MSSQLServer\parameters"

# Stores a list of the SQLArg Registry Keys
$Param1 = Get-Item $RegKey | Select-Object -ExpandProperty property

$Param1 | ForEach-Object { New-Object psobject -Property @{"property"=$_; "Value" = (Get-ItemProperty -Path $RegKey -Name $_).$_}} 

2-Objet_Errorlog

If we want to filter down and only get the ErrorLog information, we would run the following instead:

# Searches for ALL SQL servers installed on machine
$RegKey = "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL??*\MSSQLServer\parameters"

#  -e for errorlog.  -d for master file
$Filter = "-e"

# Stores a list of the SQLArg Registry Keys
$Param1 = Get-Item $RegKey | Select-Object -ExpandProperty property
# Creates an Object, and filters those starting with -e
# Finally, removes -e from the string
$Param2 = $Param1 | ForEach-Object { New-Object psobject -Property @{"property"=$_; "Value" = (Get-ItemProperty -Path $RegKey -Name $_).$_}} 


IF ( (($param1 | Where-Object {$_ -eq "SQLArg0"}).count -gt 1) -and ($PSVersionTable.PSVersion.Major -lt 3) )

{ 
write-host "sorry, not supported" 
}

ELSE
{

(($Param2 | Where-Object {$_.Value -like ($Filter+"*") }).Value).Replace($Filter,"")
}



3-Just_Errorlog

SysAdmins on SQL

The following query well known query will list all logins with SYSADMIN access to a sql Server:

    SELECT  p.name 'LoginName',
	    p.type_desc

    FROM    sys.server_principals p
    JOIN	sys.syslogins s ON p.sid = s.sid
	WHERE   
		p.type_desc IN ('SQL_LOGIN', 'WINDOWS_LOGIN', 'WINDOWS_GROUP')
		-- Logins that are not process logins
        AND p.name NOT LIKE '##%'
		-- exclude disabled
		and p.is_disabled = 0
		-- Logins that are sysadmins
		AND s.sysadmin = 1
ORDER BY Type

The problem is that the query may return Windows Groups, and we might want to know membership to those groups.

Which is why we rather run the following query instaead:


/*
DESCRIPTION:  List users with SYSADMIN access 
If a Windows group has sysadmin access
it will list its members

Lists only enabled users at SQL level
cannot check if valid AD account

CONFIGURATION: None
 
Compatibility list:
MSSQL2005
MSSQL2008
MSSQL2008R2
MSSQL2012
DOES NOT WORK

*/

DECLARE @logininfo table
( [LoginName] sysname
, [type] char(8), [priv] char(9), [mpln] sysname
, [GroupMembership] sysname
)

DECLARE @GroupName sysname

DECLARE cur CURSOR FOR

	SELECT
			p.name
    FROM    sys.server_principals p
    JOIN	sys.syslogins s ON p.sid = s.sid
	WHERE   
			p.type_desc IN ('WINDOWS_GROUP')
			-- exclude disabled
			and p.is_disabled = 0
			-- Logins that are sysadmins
			AND s.sysadmin = 1

OPEN cur
FETCH NEXT FROM cur INTO @GroupName
WHILE @@FETCH_STATUS = 0
BEGIN

	INSERT INTO @logininfo
	EXEC master..xp_logininfo 
	@acctname = @GroupName,
	@option = 'members'

FETCH NEXT FROM cur INTO @GroupName
END
CLOSE cur
DEALLOCATE cur

SELECT 
	LoginName,
	'WINDOWS_GROUP' 'Type_Desc',
	GroupMembership 'GroupMembership'
FROM @logininfo  
UNION ALL
SELECT  p.name 'LoginName',
		p.type_desc,
		'' 'GroupMembership'
    FROM    sys.server_principals p
    JOIN	sys.syslogins s ON p.sid = s.sid
	WHERE   
		p.type_desc IN ('SQL_LOGIN', 'WINDOWS_LOGIN')
		-- Logins that are not process logins
        AND p.name NOT LIKE '##%'
		-- exclude disabled
		and p.is_disabled = 0
		-- Logins that are sysadmins
		AND s.sysadmin = 1
ORDER BY GroupMembership, Type_Desc
        

 

Get Tivoli Info

IBM Tivoli Monitoring Service is a software used to monitor SQL servers

In order to install it, the team supporting IBM Tivoli will ask you for the information below.

The information needs to be input in a spreadsheet, which is why it's presented "horizontally" rather than in a vertical table.

(the "creation of ID" is at the bottom of the post)


/*
DESCRIPTION    Get INFO for setting up Tivoli Monitoring
 
CONFIGURATION   none
 
Compatibility list:
MSSQL2005
MSSQL2008
MSSQL2000 -- Tivoli Not supported
 
 */

SET NOCOUNT ON
DECLARE @hostname NVARCHAR(40)
DECLARE @instancename SQL_VARIANT
DECLARE @DBHome NVARCHAR(1000)
DECLARE @Version  SQL_VARIANT
DECLARE @SQLBitLevel NVARCHAR(6)
DECLARE @OSBitLevel NVARCHAR(6)
DECLARE @ErrorLog NVARCHAR(100)
DECLARE @LoginName NVARCHAR(8)
DECLARE @Passwd NVARCHAR(8)

/*---- working variables ----*/
DECLARE @Inst VARCHAR(100)
DECLARE @InstanceLocation VARCHAR(200)
DECLARE @RegKeyLoc	VARCHAR(200)

-- This section concatenate the hostnames
-- if this is a cluster

 SELECT @hostname=coalesce(@hostname,'') + ' ; ' + Nodename
FROM ::fn_virtualservernodes()

SET @hostname= coalesce((SUBSTRING(@hostname,4,len(@hostname))), convert(VARCHAR(40),serverproperty('ComputerNamePhysicalNetBIOS')))
SET @InstanceName = SERVERPROPERTY('servername')

-- Location of Instance on REGISTRY		
SET @Inst = isnull(convert(VARCHAR, ServerProperty('InstanceName')),'MSSQLSERVER')

	EXEC master..xp_regread @rootkey='HKEY_LOCAL_MACHINE',
		@key='Software\Microsoft\Microsoft SQL Server\Instance Names\SQL',
		@value_name=@Inst,
		@value=@InstanceLocation OUTPUT

-- Location of the Binaries

	SET @RegKeyLoc = 'Software\Microsoft\Microsoft SQL Server\'+@InstanceLocation+'\Setup'

	EXEC master..xp_regread @rootkey='HKEY_LOCAL_MACHINE',
		@key=@RegKeyLoc,
		@value_name='SQLPath',
		@value=@DBHome OUTPUT
  
-- Location of Errorlog
SET @RegKeyLoc= 'Software\Microsoft\Microsoft SQL Server\'+@InstanceLocation+'\MSSQLServer\Parameters'

	EXEC master..xp_regread @rootkey='HKEY_LOCAL_MACHINE',
		@key=@RegKeyLoc,
		@value_name='SQLArg1',
		@value=@ErrorLog OUTPUT

SET @ErrorLog= REPLACE(@ErrorLog,'-e','')

Set @Version = serverproperty('edition')

SET @SQLBitLevel=
	case   
		when (charindex('64',convert(nVARCHAR(30),@version))>0) then  '64-bit'
		else '32-bit'
	end  

Set @OSBitLevel=@SQLBitLevel
Set @LoginName = 'Tivoli'

SELECT 
  @hostname		'hostname'
, @instancename 'instance name'
, @DBHome		'DB Home'
, @Version		'Version'
, @SQLBitLevel	'SQL Bit Level'
, @OSBitLevel	'OS Bit Level'
, @ErrorLog		'ErrorFullPath'
, @LoginName	'login name'
, @Passwd		'password'



Usually, Tivoli requires a SQL login, and here is the code that creates the ID:


IF EXISTS (SELECT name FROM SYSLOGINS WHERE name = 'Tivoli')
ALTER LOGIN [tivoli] WITH PASSWORD=N'Tiv0l1paSS' UNLOCK
ELSE
CREATE LOGIN [Tivoli] WITH PASSWORD=N'Tiv0l1paSS' MUST_CHANGE, DEFAULT_DATABASE=[master], CHECK_EXPIRATION=ON, CHECK_POLICY=ON

ALTER SERVER ROLE [sysadmin] ADD MEMBER [Tivoli]

All the basics for running Scripts in Powershell

Basics for Running Scripts in PowerShell

Once you start creating and writing PowerShell commands, it can become quite troublesome to move them to a server or another machine, specially when the commands are long, complex, or they define functions intended to be reusable.

Let's take the following example

Below, we see a screenshot of a "one-liner" command to find the location of the ErrorLog on a SQL Server:

0-OneLiner

The Actual script looks like this:

((Get-Item -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL??*\MSSQLServer\parameters" | Select-Object -ExpandProperty property | ForEach-Object { New-Object psobject -Property @{"property"=$_; "Value" = (Get-ItemProperty -Path $RegKey -Name $_).$_}} | Where-Object {$_.Value -like "-e*"}).Value).Replace("-e","")

Wouldn't be difficult to type that in? or even to cut&paste into a console?

The obvious solution is to place them in a file. A file with a list of commands is sometimes called a script, and we'll use the term interchangeable in this article. PowerShell scripts are generally named <Verb>-<Noun>.ps1  (More Best Practices here )

Example:  Get-SQLParam.ps1

$RegKey = "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL??*\MSSQLServer\parameters"

$Param = Get-Item $RegKey | Select-Object -ExpandProperty property |
 ForEach-Object {New-Object psobject -Property @{"property"=$_; "Value" = (Get-ItemProperty -Path $RegKey -Name $_).$_}
                } 
$Param.Value

To call the file, while in the same directory, use the notation:

  .\Get-SQLParam.ps1 or . .\Get-SQLParam.ps1 , depending if you want to dot-source the file.

 

Setting Execution Policies

If you are an Administrator on a server, you should be able to run all commands on PowerShell directly, however, if you want to run commands saved on a file, you'll need to check if the execution policy allows it.

To check what is the current execution policy:

	Get-ExecutionPolicy


PowerShell has a number of execution policies that defines what files or scripts are allowed to run:

  •     Restricted: Default execution policy, does not run scripts, interactive commands only.
  •     All Signed: Runs scripts; all scripts and configuration files must be signed by a publisher that you trust; opens you to the risk of running signed (but malicious) scripts, after confirming that you trust the publisher.
  •     Remote Signed: Local scripts run without signature. Any scripts saved in an UNC path and downloaded scripts need a digital signature. Scripts saved locally are fine.
  •     Unrestricted:Runs scripts; all scripts and configuration files downloaded from communication applications such as Microsoft Outlook, Internet Explorer, Outlook Express and Windows Messenger run after confirming that you understand the file originated from the Internet; no digital signature is required; opens you to the risk of running unsigned, malicious scripts downloaded from these applications

See this link for more information regarding Execution Policies.

The default execution policy one is Restricted.  Attempting to run a script under this policy will yield an error:

File <path to the file> cannot be loaded because the execution of scripts is disabled on this system.

Changing the Execution Policy to "unrestricted" will certainly let you run all your scripts, however it will leave the system exposed because it might allow potential malware run scripts stored remotely. Strongly not recommended.

The most secure setting is "All Signed", but it's not practical when visiting one server and that's it. ( Click here to learn how to sign your scripts)

A a happy compromise regarding security is using RemoteSigned, where you can run your own scripts stored locally, but not those stored in a network location. It even allows you to run scripts on a remote system, as long as the scripts are stored local to your console.

To change the Execution Policy, run:

Set-ExecutionPolicy RemoteSigned -Force

The "-Force" parameter suppresses the confirmation screen.

In the screenshot below we see what happen when you try to run a script and the policy is set to "restricted". Next we change the policy and we are able to run our script:

1-SetPolicy

There are a number of ways to bypass the Execution Policy, but it will NOT set global variables or define functions. In other words, if you define a function or variables on a script and call it using the method below, it will not be available for the current session (the equivalent of not sourcing your script, more on that below).
You might want to bypass the policy when your script simply outputs information to the host and you do not need variables or functions later on.  In our example, we are simply outputting registry key values corresponding to the SQL Server start up parameters, so it's a good candidate to run without changing the execution policies:

	Get-Content .\Get-SQLParam.ps1 | PowerShell.exe -noprofile -
	

Please note the <dash><noprofile><space><dash> syntax.

2-ByPassExecPol

More about these methods in this link

 

Running Scripts

OK, so we are ready to run a file with commands, known as a PoweShell script.

 There are two ways of running the commands in a file. You either source the file, or you don't.

If you define a variable or function within a script, and are not interested in bringing that variable or function over to the current session, you do not need to source the script.

Per example, in the screenshot below, we first show the content of the file, and you can tell that we have stored our results, an object, in the $Param variable.

Next we run the script by typing .\Get-SQLParam.ps1  . After we execute the script, we check the value of $Param and see that it's empty.  Therefore, the object stored on $Param did not come to the our session, even though the script did output the needed information.

3-NotSourced

On the other hand, if you define a variable or function within a file, and want to bring over a variable or function, you source the script.

Per example, in the screenshot below we again first list the content of the script (we don't need to "Get-Content" every time but we do it for demonstration purposes), then run the sourced script, by typing dot-space-dot-slash-name:  . .\Get-SQLParam.ps1

After we execute the script, we check the value of $Param and see that is not empty this time: it has the value assigned within the script.

4-Sourced

Since in our script we actually defined a PowerShell Custom Object, and we are sourcing the script, we are bringing over the entire object to our current session:

5-Object

First, we show that the variable $Param is empty. Next we run a dot-sourced script. Then we show that the variable $Param is an actual PS Object.

Please note that variables defined inside a function will not be available, unless the function specifically returns such variables, as we explain below.

Running Functions defined on a Script

OK, let's transform our script into a function:

Function Get-SQLParam {
$RegKey = "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL??*\MSSQLServer\parameters"

$Param = Get-Item $RegKey | Select-Object -ExpandProperty property |
 ForEach-Object {
                New-Object psobject -Property @{“property”=$_; “Value” = (Get-ItemProperty -Path $RegKey -Name $_).$_}
                } 
$Param
}

Notice that we changed $Param.Value for $Param because we want the script to return an object, not a string (see best practices link above)

 

Below, we first list the content of the script, where you can see that now it is a function (again, no need for this step, but it's helpful for demonstration purposes)

Next we run the script, but do not source it. We will attempt to run the function defined inside the script, and we will encounter an error saying that the cmdlet is not recognized. To be expected when we do not source a script when running it.

6-FunctionNotSourced

We received the error because the function was defined inside the script but never sourced, therefore it does not exists within our session.

If we run the script again, but this time we source it, we will see that now the function is available for us to use:

7-FunctionSourced

Notice that this time we run (Get-SQLParam).Value because we are only interested in the "Value" field of the PowerShell object defined within the function.

Additionally, because the object $Param was defined inside the function, but it was not capture nor returned, then it's not available for the current session.

If you would like the entire object available for the current session, we can do the following:

8-PassingValueFunction

Here we assign the output of the Function to a variable $Results. Hence, storing the object on that variable for later use.

 

To Source or to Not Source

When is important to source a script?

If you have functions defined inside a file or create objects that you need in your current session, you must source the script.

Whenever you are running scripts with the intention to simply output information to the console, you may not source your scripts.

 

 

Running on Virtual or Physical

The trick to find out if SQL is running on a physical server or a VM is to read the ERRORLOG. Every time SQL starts (or the ERRORLOG is recycled), the "system manufacturer" is logged.

If it says "VMWare, Inc", then the server is running in a virtual environment. If it doesn't say "VMWare, Inc". then it's a physical server.


exec sp_readerrorlog 0,1,'System Manufacturer'



 

Example 

This server is running on a Virtual Machine:

Virtual

 

This Server is running on a Physical Machine (and the server was made by IBM):

Physical

 

Cluster Info

Basic info for a cluster.

Pay attention to "where am I"… so you know if you are in the right node.


/*
Basic Cluster Info
*/
DECLARE @name AS VARCHAR(40)

SELECT @name=COALESCE(@name,'') + ' ; ' + Nodename
FROM ::fn_virtualservernodes()

SELECT
 SERVERPROPERTY('isclustered')	 'Is Cluster?'
,SUBSTRING(@name,4,len(@name))	 'NodesOFCluster'
,SERVERPROPERTY('ComputerNamePhysicalNetBIOS')	  'ActiveNode'
,HOST_NAME()			 'where am I?'
,SERVERPROPERTY('ServerName')	 'SQLServerName'
,COALESCE(SERVERPROPERTY('InstanceName'),'DEFAULT') 'InstanceName'

Example

Here we have a THREE nodes cluster, and SQL is running on Server B.  We are running our SSMS on ServerC.

ClusterInfo

SQL Version

Basic Server Info to obtain SQL Version and OS Version.


/*
Basic Info
*/

SELECT
SERVERPROPERTY('ServerName')	 'SQLServerName',
COALESCE(SERVERPROPERTY('InstanceName'),'DEFAULT') 'InstanceName',
SERVERPROPERTY('ProductVersion') 'ProductVersion',
SERVERPROPERTY('ProductLevel')   'ProductLevel',
SERVERPROPERTY('Edition')		 'Edition',
CASE 
WHEN RIGHT(SUBSTRING(@@VERSION, CHARINDEX('Windows NT', @@VERSION), 14), 3)= 6.2 then 'Windows 2012'
WHEN RIGHT(SUBSTRING(@@VERSION, CHARINDEX('Windows NT', @@VERSION), 14), 3)= 6.1 then 'Windows 2008R2'
WHEN RIGHT(SUBSTRING(@@VERSION, CHARINDEX('Windows NT', @@VERSION), 14), 3)= 6.0 then 'Windows 2008'
WHEN RIGHT(SUBSTRING(@@VERSION, CHARINDEX('Windows NT', @@VERSION), 14), 3)= 5.2 then 'Windows 2003'
WHEN RIGHT(SUBSTRING(@@VERSION, CHARINDEX('Windows NT', @@VERSION), 14), 3)= 5.1 then 'Windows XP'
WHEN RIGHT(SUBSTRING(@@VERSION, CHARINDEX('Windows NT', @@VERSION), 14), 3)= 5.0 then 'Windows 2000'

ELSE RIGHT(SUBSTRING(@@VERSION, CHARINDEX('Windows NT', @@VERSION), 14), 3)
END	'Windows Version'

Example

SQLVersion

Shrink a Data File

Normally, you would never want to shrink data files. Not even in case of emergency when running out of space. If you were to shrink the data file, the database would simply grow again and you end up with the same problem. If the database grew, it is because the database needed the space. If it is partially empty, it is because some data was removed, however, it is very likely that it would be needed again so it is a bad idea to shrink it again. If there was an error that caused the database to grow, or there was a change in process, or something extremely rare happened, you might need to shrink a database file. Since shrinking a database will hit performance and possible introduce some locks on the tables, if you must do it, it is better to do it little by little, using a script similar to this below. Remember, after the shrink is done, your indexes will be fragmented, so it is recommended to rebuild all your indexes (and hence the DB will grow a little)

In order to use this script, you should first find if there is space available on the data files, and find the logical name of the data file to shrink.

To do that, run this script first http://www.cookingsql.com/2016/01/data-file-size-and-free-space-one-database/ and use its output to run the script below.

 

/*
DESCRIPTION   Shrink a single DATA file, in increments.
It will loop to execute the DBCC SHRINKFILE command to shrink the database file by the desired increment
until it reaches the target free space.


CONFIGURATION    TOP.
Run this script in the database with the file to be shrunk.
1.  Make sure to set the USE statement for the correct database
2. Set @DBFileName  Enter the Logical File name (not the physical name)
3. Set @TargetFreeMB to the desired file free space in MB after shrink.
4. Set @ShrinkIncrementMB to the increment to shrink file by in MB

 

Compatibility list:
MSSQL2005
MSSQL2008/SQL2008R2
MSSQL2012
MSSQL2014 
 
*/
SET NOCOUNT ON


declare @DBFileName sysname
declare @TargetFreeMB int
declare @ShrinkIncrementMB int

/*------ CONFIGURATION ---------*/

--USE DATABASE 

-- Set Name of Database file to shrink
set @DBFileName = 'AdventureWorks2012_Data2'

-- Set Desired file free space in MB after shrink
-- Remember, it is not the target file size but how much empty space will be left after the shrink operation
-- @SizeMB = @TargetFreeMB + @usedSpace
set @TargetFreeMB = 1300

-- Set Increment to shrink file by in MB
-- Make sure the @TargetFreeMB + @ShrinkIncrement is bigger than @EmptyDB
set @ShrinkIncrementMB = 300

/*------ END CONFIGURATION ---------*/

-- Declare variables
declare @sql varchar(8000)
declare @SizeMB int
declare @EmptyMB int
declare @UsedMB int

-- Check Input Values

IF NOT EXISTS (select name from sysfiles where name = @DBFileName)
begin
print 'The Data File does not exist on the current database. You either did not set
the USE statement or did not specify the Logical File name correctly
Terminating Batch'
GOTO EOL
end

select
@SizeMB = CAST(size/128.0 AS int)
,@UsedMB = CAST(FILEPROPERTY(name, 'SpaceUsed' ) AS int)/128.0
,@EmptyMB = @SizeMB - @UsedMB
 
FROM sys.database_files
where name = @DBFileName



IF @EmptyMB < @TargetFreeMB
BEGIN
print 'Cannot proceed with operation because the target free space: ' +cast(@TargetFreeMB as nvarchar(10)) +'MB (user entry)
is bigger than the current free space: '+cast(@EmptyMB as nvarchar(10))+'MB
Terminating Batch'
GOTO EOL
END

IF @EmptyMB < @TargetFreeMB+@ShrinkIncrementMB
BEGIN
print 'Cannot proceed with operation because the Shrink Increment: ' +cast(@ShrinkIncrementMB as nvarchar(10)) +'MB (user entry)
Plus the Target Free '+cast(@TargetFreeMB as nvarchar(10))+'MB is bigger than the empty Space '+cast(@EmptyMB as nvarchar(10))+'MB
Terminating Batch'
GOTO EOL
END

 
declare @loopExit int
set @loopExit =  0


-- Loop until file at desired size
while  (@SizeMB > @UsedMB+@TargetFreeMB+@ShrinkIncrementMB) and (@loopExit < 10)
	begin

		set @sql =
		'dbcc shrinkfile ( '+@DBFileName+', '+convert(varchar(20),@SizeMB-@ShrinkIncrementMB)+' ) WITH NO_INFOMSGS'

		print 'Start ' + @sql + ' at '+convert(varchar(30),getdate(),121)
		exec ( @sql )

		print 'Done at '+convert(varchar(30),getdate(),121)

		-- Sets again Original Values
		select
		@SizeMB = CAST(size/128.0 AS int)
		,@UsedMB = CAST(FILEPROPERTY(name, 'SpaceUsed' ) AS int)/128.0
		,@EmptyMB = @SizeMB - @UsedMB
		,@loopExit = @loopExit+1
		FROM sys.database_files
		where name = @DBFileName

	end
------------------------------------

IF (@loopExit = 10)
BEGIN
PRINT 'The SHRINK FILE operation completed 10 loops
This could mean that the file is not shrinking
or you need to increase the "shrink increment value"
Proceed with CAUTION
'
GOTO EOL
END

-- Show Size, Space Used, Unused Space, and Name of all database files


SELECT   
b.name
, b.type_desc
, CAST(b.size/128.0 AS int) 'SizeMB'  
, CAST(b.size/128.0 - CAST(FILEPROPERTY(b.name, 'SpaceUsed' ) AS int)/128.0 AS int) 'SpaceUsedMB'  
, ceiling(b.size/128.0 -CAST(FILEPROPERTY(b.name,'SpaceUsed' ) AS int)/128.0) 'FreeSpaceMB'
, CAST(ceiling(100 * (b.size/128.0 -CAST(FILEPROPERTY(b.name,'SpaceUsed' ) AS int)/128.0)/(b.size/128.0)) AS varchar(8)) + '%'  'FreeSpace'

,  CASE
	WHEN b.is_percent_growth = 0 THEN LTRIM(STR(b.growth * 8.0 / 1024,10,1)) + ' MB, '  
    ELSE 'By ' + CAST(b.growth AS VARCHAR) + ' percent, '
 END
 + CASE  
    WHEN b.max_size = -1 THEN 'unrestricted growth'
	ELSE 'restricted growth to ' + LTRIM(STR(b.max_size * 8.0 / 1024,10,1)) + ' MB'  
    END AS Autogrow
, cast(b.max_size/128.0 as int) 'MaxSize'
, physical_name
 
FROM sys.database_files b



EOL: