All posts by Miguel

Detecting Long Running Jobs

This script is taken from Thomas LaRock, and I'm shamelessly modifying it a little… just a little.

The original article is here

https://thomaslarock.com/2012/10/how-to-find-currently-running-long-sql-agent-jobs/

 

/*=============================================
  
  Author: Thomas LaRock, http://thomaslarock.com/contact-me/
 
 MODIFIED AND ADAPTED BY MIGUEL QUINTANA 01/27/2017

  Summary: This script will check to see if any currently
			running jobs are running long. 
 
  Variables:
	@MinHistExecutions - Minimum number of job runs we want to consider 
	@MinAvgSecsDuration - Threshold for minimum duration we care to monitor
        @HistoryStartDate - Start date for historical average
        @HistoryEndDate - End date for historical average
 
        These variables allow for us to control a couple of factors. First
        we can focus on jobs that are running long enough on average for
        us to be concerned with (say, 30 seconds or more). Second, we can
        avoid being alerted by jobs that have run so few times that the
        average and standard deviations are not quite stable yet. This script
        leaves these variables at 1.0, but I would advise you alter them
        upwards after testing.
 
  Returns: One result set containing a list of jobs that
	are currently running and are running longer than two standard deviations 
        away from their historical average. The "Min Threshold" column
        represents the average plus two standard deviations. 
 
  Date: October 3rd, 2012
 
  SQL Server Versions: SQL2005, SQL2008, SQL2008R2, SQL2012
 
  You may alter this code for your own purposes. You may republish
  altered code as long as you give due credit. 
 
  THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
  OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
  LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR
  FITNESS FOR A PARTICULAR PURPOSE.
 
=============================================*/
SET NOCOUNT ON

DECLARE   
	@HistoryStartDate datetime 
   ,@HistoryEndDate datetime
   ,@JOB_STATE INT = 4
   -- @JOB_STATE = 4 idle, @JOB_STATE = 1 running
   ,@MinHistExecutions int   = 10.0
  ,@MinAvgSecsDuration int  = 600.0  
   
SET @HistoryStartDate = DateAdd(dd,-30,GETDATE()) --'19000101'
SET @HistoryEndDate = GETDATE()


/***** DO NOT MODIFY BELOW *********/
DECLARE @RESULTS TABLE (
	[job_id] [uniqueidentifier] NOT NULL,
	[JobName] [sysname] NOT NULL,
	[State] [varchar](29) NOT NULL,
	[ExecutionDate] [datetime] NULL,
	[AverageSecs] [numeric](38, 6) NULL,
	[DurationSecs] [float] NULL,
	[capture_time] [datetime] NOT NULL
)

 
DECLARE @currently_running_jobs TABLE (
    job_id UNIQUEIDENTIFIER NOT NULL
    ,last_run_date INT NOT NULL
    ,last_run_time INT NOT NULL
    ,next_run_date INT NOT NULL
    ,next_run_time INT NOT NULL
    ,next_run_schedule_id INT NOT NULL
    ,requested_to_run INT NOT NULL
    ,request_source INT NOT NULL
    ,request_source_id SYSNAME NULL
    ,running INT NOT NULL
    ,current_step INT NOT NULL
    ,current_retry_attempt INT NOT NULL
    ,job_state INT NOT NULL
    ) 
 
--capture details on jobs
INSERT INTO @currently_running_jobs
EXECUTE master.dbo.xp_sqlagent_enum_jobs 1,''
 
;WITH JobHistData AS
(
  SELECT job_id
	,date_executed=msdb.dbo.agent_datetime(run_date, run_time)
	,secs_duration=run_duration/10000*3600
                      +run_duration%10000/100*60
                      +run_duration%100
  FROM msdb.dbo.sysjobhistory
  WHERE step_id = 0   --Job Outcome
  AND run_status = 1  --Succeeded
)
,JobHistStats AS
(
  SELECT job_id
        ,AvgDuration = AVG(secs_duration*1.)
        ,AvgPlus2StDev = AVG(secs_duration*1.) + 2*stdevp(secs_duration)
  FROM JobHistData
  WHERE date_executed >= DATEADD(day, DATEDIFF(day,'19000101',@HistoryStartDate),'19000101')
  AND date_executed < DATEADD(day, 1 + DATEDIFF(day,'19000101',@HistoryEndDate),'19000101')   
  GROUP BY job_id   HAVING COUNT(*) >= @MinHistExecutions
  AND AVG(secs_duration*1.) >= @MinAvgSecsDuration
)

-- INSERT INTO  @RESULTS 
SELECT JHS.job_id	
		,j.name AS [JobName]
      ,case crj.job_state
		when 0 then 'not idle or suspended'
		when 1 then	'Executing'
		when 2 then	'Waiting for thread'
		when 3 then	'Between retries'
		when 4 then	'Idle'
		when 5 then	'Suspended'
		when 7 then	'Performing completion actions'
		else ''
 	   end as [State]
	  ,MAX(act.start_execution_date) AS [ExecutionDate]
      ,AvgDuration AS [AverageSecs]
      ,AvgPlus2StDev AS [DurationSecs]
	  ,GETDATE() AS [email_Done]
FROM JobHistData jd
JOIN JobHistStats jhs on jd.job_id = jhs.job_id
JOIN msdb..sysjobs j on jd.job_id = j.job_id
JOIN @currently_running_jobs crj ON crj.job_id = jd.job_id
JOIN msdb..sysjobactivity AS act ON act.job_id = jd.job_id
AND act.stop_execution_date IS NULL
AND act.start_execution_date IS NOT NULL

WHERE 1=1
AND secs_duration > AvgPlus2StDev
AND DATEDIFF(SS, act.start_execution_date, GETDATE()) > AvgPlus2StDev

-- THIS SECTION EXCLUDE JOBS ALREADY IN THE SYSTEM FOR LESS THAN 30 MINS
AND JHS.job_id NOT IN (SELECT A.JOB_ID FROM DBA.dbo.JOBS_Running_Long A WHERE DATEDIFF(mi,A.CAPTURE_TIME,GETDATE()) < 30)


AND crj.job_state = @JOB_STATE


GROUP BY  JHS.job_id, j.name, AvgDuration, AvgPlus2StDev, crj.job_state

 

 

Duration of SQL Jobs

List SQL Job duration

If you need step details, see http://www.cookingsql.com/2017/02/duration-of-sql-jobs-steps/

/*
DESCRIPTION	List duration of jobs 
CONFIGURATION none
some filtering might be used

Compatibility List
SQL 2008 and up

*/
select 
 j.name as 'JobName',
 msdb.dbo.agent_datetime(run_date, run_time) as 'RunDateTime',
 ((run_duration/10000*3600 + (run_duration/100)%100*60 + run_duration%100 + 31 ) / 60) 
         as 'RunDurationMinutes'
From msdb.dbo.sysjobs j 
INNER JOIN msdb.dbo.sysjobhistory h  ON j.job_id = h.job_id 
where 1=1
and j.enabled = 1   --Only Enabled Jobs
and h.step_id = 0   --Only show the total duration
and j.name like '%backup%'           --Uncomment to search for a single job
/*
and msdb.dbo.agent_datetime(run_date, run_time) 
BETWEEN '01/01/2017' and '02/01/2017'  --Uncomment for date range queries
*/
order by JobName, RunDateTime desc

 

 

Link Server Information

 

A very simply script to list all link servers available on the SQL Server


SELECT 
 ss.name 
,ss.product 
,ss.provider
,ss.data_source 
,case sl.uses_self_credential 
	when 1 then 'Uses Self Credentials' 
    else coalesce(ssp.name,'') 
	end 'Local login'
, coalesce(sl.remote_name,'') 'Remote Login Name'
,case ss.is_rpc_out_enabled
	when 1 then 'True'
	else 'False'
	end 'RPC Out Enabled'
 ,case ss.is_data_access_enabled 
   when 1 then 'True'
   else 'False'
   end 'Data Access Enabled'
,ss.modify_date 
FROM sys.Servers ss 
 LEFT JOIN sys.linked_logins sl ON ss.server_id = sl.server_id 
 LEFT JOIN sys.server_principals ssp ON ssp.principal_id = sl.local_principal_id
 where 1=1
Order by ss.name

 

Growth of Database

The following script provides an educated guess regarding database growth

It does this by examining the size of backups. It queries MSDB for backup sizes, calculated the difference, divided by the number of days in between, and finally averages the difference.

/*
DESCRIPTION    Estimate Database growth based on backups.

Assuming backup size grows at the same rate as the Database size.  
First calculate difference in size between backups,
and then divides it by the number of days the backup took place
Finaly, averages the result.
CONFIGURATION    
    Set @LastXday to limit the number of days to search for
    Set @dbname for the database
 
Compatibility list:
MSSQL2005
MSSQL2008
MSSQL2012

 */ 
declare @dbname varchar(500)
declare @LastXdays nvarchar(2)
set @dbname = 'AdventureWorks'
set @LastXdays = '150'

;WITH BK_Growth (BKsize,BKDate,rownumber) as(  
	SELECT
		 backup_size
		,backup_start_date
		,row_number() OVER 
				(ORDER BY database_name,
				[type], backup_start_date DESC)
				AS rownumber
	FROM msdb.dbo.backupset
	WHERE
		database_name like @dbname
		and DATEDIFF(dd,([backup_finish_date]),GETDATE()) < @LastXdays
		and type = 'D'
)
select 
@dbname 'dbname'
,CAST( CAST(avg(100*((
(prev.BKSize - cur.BKSize)/cur.BKSize)/(DATEDIFF(dd,cur.BKDate,Prev.BKDate))
)) as NUMERIC(5,3))
as VARCHAR(5))+' %' 'Average Daily Growth'

,     (CAST(mfrows.RowSize AS FLOAT)*8)/1024 RowSizeMB
,     (CAST(mflog.LogSize AS FLOAT)*8)/1024 LogSizeMB
 
from BK_Growth cur
inner join BK_Growth prev on CUR.rownumber = (PREV.rownumber+1)

cross apply sys.databases db
    LEFT JOIN (
    SELECT database_id, SUM(size) RowSize 
    FROM sys.master_files 
    WHERE type = 0 and database_id = DB_ID(@DBName)
    GROUP BY database_id, type
        ) mfrows ON mfrows.database_id = db.database_id
    
    LEFT JOIN (
    SELECT database_id, SUM(size) LogSize 
    FROM sys.master_files 
    WHERE type = 1  and database_id = DB_ID(@DBName)
    GROUP BY database_id, type
        ) mflog ON mflog.database_id = db.database_id

WHERE mfrows.RowSize is not null
GROUP BY mfrows.RowSize, mflog.LogSize        

 

Historical Backup Report

The Greatest Backup Report

In some organization an important Key Point Indicator (KPI) has to do with backup success rates. This KPI attempts to measure what percentage of backups have taken place. However, many times this KPI has not been clearly defined.

I always ask "what constitute a backup failure"?  If a backup fails to take place but I fix it the next morning, is that still a fail backup?  or if a Full Backup fails but the Tlogs backups still take place, so if a failure occurs I'm still able to recover with no data loss, is that still a failed backup? This is the equivalent of if a tree fails in the forest, but I repair it without anyone knowing, did it still fall?

If we are reporting against SQL Jobs success rates, then we can run into a situation where if the job is misconfigured and it fails to account for newly created databases, we could run into a situation where we have 100% success rate but have databases with no backups.

Some might argue that, using the true definition, "a successful backup is one that you could restore".  This is fine and dandy, but how often are we able to restore all our database backups in a daily manner?

As you can see, there areextremes, but we still need to provide an honest measure for the KPI.

A good compromise would be to report against the actual backup as recorded in MSDB versus the expetected schedule of said backup

In this sense, we are not interested just on the last backup, but we are interested to know if all backups are running at regular intervals, and what that interval is.

This is a first iteration of the script where for a single DB might look like this:

1-Bk_Report_history

Based on the results, we can assume that the Backup Schedule probably is as follow:
Full 7 days.
Diff 1 day.
Tlog: Hourly.

We can also assume that the first value for Full is 5.  Since 5 is less than 7, not an issue,  still within schedule.
The last  value for Diff is 2.  Since 2 is more than 1, possible and issue. However, it probably means that a full backup was taken instead.
There is a value Tlog that is 2. Since 2 more than 1, there was a missed backup in that time period.

 

Another example:

2-Bk_Report_history

We can tell that FULL failed, since the first value is 12, and 12 is more than 7, therefore a Full Backup is missing.
We can also tell that DIFF failed, since the first value is   2, and   2 is more than 1, therefore a Diff Backup is missing.

Last Example:

3-Bk_Report_history

In here, we can see that someone was taking backups outside our normal backup schedule, sine most backups are going to TDP and we have a few backups going to local disk.

 

Finally, the script itself:

The Script is fully customizable to report for a certain number of days and either all databases or just one:

 

/*
DESCRIPTION	
List Database Backup Age, per database, and the intervale between backups

CONFIGURATION	top
SET @LastXDays for how far back you query MSDB
Set @DiffDays if you want to limit Diff.
Set @TlogDays if you want to limit the Tlog 

Set @dbname to NULL for all DBs, or to a single DB

Author		 mquinta@us.ibm.com

Compatibility list: 
MSSQL2005
MSSQL2008
MSSQL2012


*/

DECLARE @dbname varchar(500)
DECLARE @LastXdays nvarchar(2)
DECLARE @TlogDays int
DECLARE @DiffDays int

-- Configuration
--  how much history we query
SET @LastXdays = 28

-- limt the results for Differential backups
SET @DiffDays = 7
-- Limit results for Tlogs. Recommended 1 or 2 days
SET @TlogDays = 1  

-- SET database. If Null, it'll do all databases
SET @dbname = null  
-- SET @dbname = 'adventureworks'
----  end configuration


;WITH BackupAgeTable AS
(  
-- query info for backups, and place it in a 
-- Common Table Expression, adding a row_number row.
SELECT
	 s.database_name
	,s.backup_size
	,s.backup_start_date
	,s.type
	,m.physical_device_name
	,m.device_type
-- Ordering will group by datbase, then type of BK and then sort by date
	,row_number() OVER (ORDER BY s.database_name,
								 s.[type],
								 s.backup_start_date DESC)
						AS rownumber
FROM SYS.DATABASES d   
INNER JOIN msdb.dbo.backupSET s ON d.database_id = db_id(s.database_name)
INNER JOIN msdb.dbo.backupmediafamily m ON s.media_SET_id = m.media_SET_id
WHERE
	1=1
	and DATEDIFF(dd,(s.backup_start_date),GETDATE()) < = @LastXdays
	and s.database_name like coalesce (@dbname,'%')
		
)

SELECT 
	cur.database_name
	, CASE cur.type
		WHEN 'D' THEN 'Full'
		WHEN 'I' THEN 'Diff'
		WHEN 'L' THEN 'TLog'
		else 'other'
	END AS backup_type
	, cast(cur.backup_size/1024/1024 as numeric (10,1)) 'SizeMB'
	, CASE cur.device_type
		when 2 then 'Disk'
		when 7 then 'Virtual' -- Like TDP
	end as 'DeviceType'
	, cur.physical_device_name
	, cur.backup_start_date
-- Using COALESCE because first row will be NULL and we need backup age instead
	, case cur.type
		when 'D' then 
		COALESCE (DATEDIFF(dd,CUR.backup_start_date,PREV.backup_start_date),
				  DATEDIFF(dd,CUR.backup_start_date,GETDATE()))
		when 'I' then
		COALESCE (DATEDIFF(dd,CUR.backup_start_date,PREV.backup_start_date),
				  DATEDIFF(dd,CUR.backup_start_date,GETDATE()))
		when 'L' then
		COALESCE (DATEDIFF(hh,CUR.backup_start_date,PREV.backup_start_date),
				  DATEDIFF(hh,CUR.backup_start_date,GETDATE()))
		else 'other'			
	END as 'interval of Backups'
-- Using CASE statement because we want HOURs for Tlogs Backups and DAYS for the rest	
	, case cur.type
		when 'D' then 'days'
		when 'I' then 'days'
		when 'L' then 'hours'
		else 'other'
	END as 'units (days/hours)'

FROM BackupAgeTable CUR
-- Left join over itsef but with a offSET of one row
-- to be able to make a DIFF between dates of two consecutive rows
LEFT join BackupAgeTable PREV 
	on CUR.rownumber = (PREV.rownumber+1)
	   and cur.database_name=prev.database_name
	   and cur.type = prev.type 
where 1=1
-- Clever filtering to limit resutls
      and(   (cur.type = 'D')-- and datediff(dd,cur.backup_start_date,getdate())< @LastXdays)
	  or (cur.type = 'I' and datediff(dd,cur.backup_start_date,getdate())< @DiffDays )
	  or (cur.type = 'L' and datediff(dd,cur.backup_start_date,getdate())< @TlogDays )
	 )

-- The UNION ALL checks for DBs with no backups
UNION ALL
SELECT DISTINCT
	  SD.name 'database'
	, 'NO BACKUPS' 'TYPE'
	, NULL 'SizeMB'
	, NULL 'DeviceType'
	, NULL 'PhysicalDevice'
	, NULL 'BackupStartDate'
	, NULL 'Interval'
	, 'NO BACKUPS'	'Units'
FROM 
	 master..sysdatabases as SD

LEFT JOIN msdb..backupSET bp on SD.name = bp.database_name 
		 AND DATEDIFF(dd,(bp.backup_start_date),GETDATE()) <= @LastXdays
WHERE 1=1
and bp.database_name is NULL
and sd.name not in ('tempDB','model')


ORDER BY 8 DESC ,1,2, 6
-- First order by column 8 so databases with no backups
-- are listed on top

 

Find Out Who Patched SQL

 

One important information to have is finding out when SQL was installed, and by whom.

This information is stored on the Setup Boot strap. If those files are available, we can find out this information using the function below.

No parameters needed. If you need to run this against multiple servers, see this article.


< #
.Synopsis
   Searches all Summary.txt file and read their information
.DESCRIPTION
   Searches all Summary.txt file and read their information. It will list all instances on each summary.txt file
   If no patch ever, it'll ignore it
.EXAMPLE
   Get-WhoPatchSQL

   Name              Value                                                                                                      
----                 ----                                                                                                        
1 Install Date     6/19/2015 12:05:16 PM                                                                                        
1 Instance 1       STAGING           ENTERPRISE                                                                              
1 Package Name     SQLServer2008R2-KB2630458-x64.exe                                                                           
1 Requested Action   Patch                                                                                                        
1 UserName           mquintana                                                                                                    
2 Install Date       6/19/2015 11:39:34 AM                                                                                        
2 Instance 1         MSSQLSERVER          Standard                                                                                
2 Package Name       SQLServer2012-KB2976982-x64.exe                                                                              
2 Requested Action   Patch                                                                                                        
2 UserName           mquintana 

.EXAMPLE
   Another example of how to use this cmdlet
#>
function Get-WhoPatchedSQL
{
    [CmdletBinding()]
    Param ( )

    Begin {  }
    Process
    {
    
$Root = "C:\Program Files\Microsoft SQL Server\"
$File=Get-ChildItem -Recurse -Path $Root -Filter Summary.txt | select-String -Pattern "patch" | group path | select name

# $Results = @{}  Use this for Powershell v2.0
$Results =[ordered] @{}
$j=1
ForEach ($SummaryFile in $File)
{

# Get information from summary text file


$Time      = Get-Content $SummaryFile.Name | select-string -pattern "Start time:" | Select -First 1
$RulesFile = Get-Content $SummaryFile.Name | Select-String -Pattern "Rules report file:"
$Action    = Get-Content $SummaryFile.Name | Select-String -Pattern "Requested action:" 
$Package   = Get-Content $SummaryFile.Name | Select-String -Pattern "PackageName:" | Select -First 1

###########
# Finds the Rules Report file folder, and seek within that folder the location of install log
# next, looks inside file for install user
$InstallPath = Split-Path (($Rulesfile -split ("Rules report file:") | Select-Object -Last 1).Trim())

$InstallLog=Get-ChildItem ($InstallPath+"\*\sql_engine_core_inst_Cpu??_1.log")

# Get the user who install it, from the log file
$User=Get-Content $InstallLog  | Select-String -pattern "C:\\Users\\" | Select-Object -First 1

########### process the data #########

$UserName    =         (($User -split ("C:\\Users\\")       | select-object -last 1).split("\\") | Select-Object -First 1)

$InstallDate = Get-Date ($Time -split ("Start time:")       | Select-Object -Last 1).trim()

$RequestedAction =    ($Action -split ("Requested action:") | Select-Object -Last 1).trim()

$PackageName =       ($Package -split ("PackageName:")      | Select-Object -Last 1).trim()

# Gather results and display them


$a = Get-Content $SummaryFile.Name | Select-String -pattern "Updated product edition:"
$b = Get-Content $SummaryFile.Name | Select-String -Pattern "User Input Settings:"

# The information we are looking for is between lines $a and $b
# Which we save on $InstData, and add to our results object
#
#  MSSQLSERVER  ENTERPRISE
#  SQLINST1     STANDARD
# 

$InstData = (Get-Content $SummaryFile.Name)[($a.LineNumber+1)..($b.LineNumber-3)]

# We add the info to the hash table



For ($i=0;$i -lt ($b.LineNumber-3 - $a.LineNumber+1)-1;$i=$i+1)
{
$Results += @{"$j Instance $($i+1)"= $Instdata[$i].trim() }
}

$Results+= @{"$j UserName"         = $UserName
            "$j Install Date"     = $InstallDate
            "$j Requested Action" = $RequestedAction
            "$j Package Name"     = $PackageName}


$j=$j+1
}

$Results.GetEnumerator() | Sort -Property Name



    }

}

# Get-WhoPatchedSQL

 

All About Running commands Remotely

 

The Invoke-Command is a very powerful way to run commands remotely.

When running commands remotely, you are essentially opening a new session on the target computer and outputting the results back to the local console.

In order to run commands remotely, we need to have a list of computers, in a string form. For our examples we'll save that list on a variable $ServerList To populate the variable we can do this:

$ServerList = "Server1", "Server2", "Server3", "Server4"

If the servers are listed on a notepad (one server per line), and saved as "c:\Temp\inventory.txt", we do this:

$ServerList= Get-Content -Path "c:\Temp\inventory.txt"

We will explore four very basic techniques.

  1. Run a command remotely.
  2. Run a saved file (script or function) remotely
  3. Run a locally defined function remotely
  4. Run successive commands remotely.

If we need to display the Event Viewer and see the last 10 event for system

invoke-command -ComputerName $ServerList -ScriptBlock {Get-EventLog -LogName System -Newest 10}

If we have a file, let's say C:\PS\Get-SQLParam.ps1, and we need to run it remotely, even if the remote servers can't access your local C:\ drive (in other words, you don't need to save it on a share) we do the following:

invoke-command -ComputerName $ServerList -FilePath c:\PS\Get-SQLParam.ps1

If we have a function, Get-WhoInstalledSQL, already dot-sourced on our console, and we need to run it remotely:

invoke-command -ComputerName $ServerList -ScriptBlock ${Function:\Get-SQLParam}

If we have a series of commands to run remotely, you can get crafty with "one liners" or put them on a file… or you can open a new session, a persisted one, like this:

$s = New-PSSession -ComputerName Server01, Server02, Server03
Invoke-Command -Session $s -ScriptBlock {$p = Get-Process PowerShell}
Invoke-Command -Session $s -ScriptBlock {$p.VirtualMemorySize}

Find Out Who installed SQL

 

One important information to have is finding out when SQL was installed, and by whom.

This information is stored on the Setup Boot strap. If those files are available, we can find out this information using the function below.

No parameters needed. If you need to run this against multiple servers, see this article.



function Get-WhoInstalledSQL {
<#
.SYNOPSIS

.DESCRIPTION

.PARAMETER

.EXAMPLE

#>

[Cmdletbinding()]
    Param (
          
    ) 

       PROCESS {

        $r           = @()
        $UserName    = @()
        $InstanceName= @()
        $CreationTime= @()   
        $computername=$env:COMPUTERNAME

# $rootfolder
$r = "C:\Program Files\Microsoft SQL Server","C:\Program Files (x86)\Microsoft SQL Server"

# Finds all configurationfile.ini

$InstallFile = (Get-ChildItem -Recurse -path $r -include "configurationfile.ini" ) #| select fullname).fullname

# Test for existence of INI file, if empty we are done.
if (!$InstallFile) { write-Warning " The Setup Bootstrap folder does not have this info (no INI files)" }

# this ELSE essentially exits the function
ELSE {   # else 1

    $listconfig = @()

    FOREACH ($configfile in $InstallFile)
    {
    # Checks each file for two conditions. If met, add it to the array
    $Cond1 = get-content $configFile | Select-String -pattern "ACTION=`"Install*"
    $Cond2 = get-content $configFile | Select-String -pattern "FEATURES=SQLENGINE"

    if ($cond1 -and $cond2) {$listconfig += $configfile }
    }

    # Test that if we have INI file, it is for INSTALL actions.  if empty we are done.
    if (!$listconfig) {
      write-Warning " The Setup Bootstrap folder does not have this info (config found, but no install INI)" 
    } # end FOREACH
    
# this ELSE essentially exits the function
    ELSE {  # else 2

        # At this point, we have the configurationFile.ini 
        # There are log files at this folder level who keep information of the username used at the time of installation
        # The log for the engine is sql_engine_core_inst_cpu??_1.log. 
        # Where it can be 32 or 64 bits
        # Assuming that many config files could be found
        # But the FOrwill execute only for one , the first one found

        for ($i=0;$i -lt $listconfig.length;$i++)
        {
            $InstallPath = split-path $listconfig[$i] -parent
            $installLog = Get-ChildItem -Recurse -path $InstallPath -include "sql_engine_core_inst_Cpu??_1.log"  | sort LastWriteTime | select -First 1
            # Ok we got the file, do we have a match?
            # We do this check to make sure we have the right file, and it hasn't been deleted/modified
            # IF we do not find it, we cant know who installed it, we simply say that user not found
       
            if(!$installLog) { $UserName= "Not_Found" }
            
            ELSE {
                    # Get the user who install it, from the log file
                    $Line=Get-Content $installLog  | Select-String -pattern "C:\\Users\\" | Select-Object -First 1
                    $UserName += (($Line -split("C:\\Users\\")  | select-object -last 1).split("\\") | Select-Object -First 1)
                 }
        
           # SQL Instance Name from INI file
            $InstanceName +=(Get-Content $listconfig[$i] | Select-String -pattern "INSTANCEID=") -replace "INSTANCEID=","" -replace "`"",""
            
           # Get CreationTime attribute  from INI file
            $CreationTime +=(Get-ChildItem $listconfig[$i] | select creationtime).creationtime
         } # end for


    }  # closing the ELSE 1 statement  
    }  # closing the ELSE 2 statement

# Let's tally up our findings in a PSObject

for ($i=0;$i -lt $InstanceName.length;$i++)
{
 New-Object PSObject -Property @{
            "ComputerName"=$computername
            "InstanceName"=$InstanceName[$i]
            "CreationTime"=$CreationTime[$i]
            "UserName"    =$UserName[$i] 
            }
}

   $table

    }     # end process

}
# Get-WhoInstalledSQL | Format-List

 

Delete files based on Age

The Get-ChildItem cmdlet is a powerful tool to search and find files on a Directory, such as old files (over certain age)

To delete all files on a directory based on age (remember to remove the -whatif safety), and filter based on extension:


# delete files older than date, recursively
# remove the whatif for execution

# define the age, in number of days.
$ts =5
# get file path to delete old files
$oldfilesPath = 'C:\Temp'
# for current directory you may use
# $oldfilesPath = '.'

# identify any type of filter. use *.* for all files
$extension = '*.bak'

Get-ChildItem -Path $oldfilesPath -Recurse -filter $extension | Where-Object {($_.LastWriteTime -lt ((Get-Date) - (New-TimeSpan -Days $ts)))} | remove-item -whatif

 

If you need to delete more than one extension (but not all of them), you would have to use this, less efficient command:


# delete files older than date, recursively
# remove the whatif for execution

# define the age, in number of days.
$ts =5
# get file path to delete old files
$oldfilesPath = "C:\temp"
# for current directory you may use
# $oldfilesPath = "."

$ManyExt = '.bak', '.trn'

Get-ChildItem -Path "$oldfilesPath" -Recurse | Where-Object {($_.Extension -in $ManyExt) -and ($_.LastWriteTime -lt ((Get-Date) - (New-TimeSpan -Days $ts)))} | remove-item -whatif

Notice that this time we filter based on extension, no wildcards needed

The above scripts will only delete files. After that operation is completed, you might need to delete empty folders

$LogPath = 'C:\temp' # replace this with your root path...
Get-ChildItem -LiteralPath $LogPath -Force -Recurse | Where-Object {
    $_.PSIsContainer -and `
    @(Get-ChildItem -LiteralPath $_.Fullname -Force -Recurse | Where { -not $_.PSIsContainer }).Count -eq 0 } |
    Remove-Item -Recurse -WhatIf