Monday, November 28, 2011

FK Dependency Tree map/ Script Generator

Customers come and go. Free trials and specials pull customers too and away from your software offering. Maybe your product is the flavor of the month for a demographic; only to be dropped the following calendar page. Some customers simply just close up shop. Maybe your registration process isn’t quite as intuitive as it should be; confusing customers and prompting them to start over. Whatever the case may be, chances are you have a lot of unused and abandoned data in your database. Like me you may be faced with the monumental task of not just identifying this data but archiving, migrating or just plain removing it. You may not even get the benefit of a downtime to do it.

So aside from batching out the process to smaller chunks to avoid impacting the entire database and ensuring that the data you are removing or archiving is verified for removal or archival, you might find yourself with the task of properly navigating the treacherous FK relationship path in your database. So if I need to delete all entries for a customer in my multi tenant database, I not only have to make sure I do indeed remove all of their data but I also have to make sure my script knows what order to perform the operations in. I cant exactly delete a customer if they have some phone numbers still dependent on the CustomerID can I. Obviously database design for cascading operations can solve this issue. But if your database design did not already include cascading dml then your going to be making an schema change on every FK in your database. If that change doesn’t make you shudder both at the immensity of the task and the massive delta for your QA process to go through then you I have a different set of problems.

So if you find yourself in a similar predicament as myself I made this handy script that not only walks your FK tree and labels the depth of your leaf’s but will construct statements to perform a dml of your choice(with some tweaking). It will even join a child table to the parent it depends on if the child itself is missing an identifier column for your tennant. It will then return your list of queries to perform your desired action in the proper order. Currently it ignores cyclical  dependencies and any nodes that are tenant unspecific all the way through their dependencies.

declare @columnname varchar(256)
set @columnname = 'clinicid' --Column used as multi tennant identifier


declare	@TblInfo table(
		FK_Table  varchar(max), 
		FK_Column varchar(max), 
		PK_Table  varchar(max), 
		PK_Column varchar(max), 
		Constraint_Name varchar(max),
		columnPresent bit,
		columnPresentPK bit,
		depth int
 )
Declare @FKRelationshipJoins table (
 		FK_table Varchar(max)
 		,PK_table Varchar(max)
		,PK_Column	Varchar(max)
		,FK_Column	Varchar(max)
		,ColumnPresent	Varchar(max)
		,ColumnPresentPK	Varchar(max)
		,FullTBlPath	Varchar(max)
		,FullColPath	varchar(max)
		,OriginalPath Varchar(max)
		,depth	Varchar(max)
		,JoinStmt	Varchar(max)
	)
 
declare @Contstraints table (
		FK_Table  varchar(max), 
		FK_Column varchar(max), 
		PK_Table  varchar(max), 
		PK_Column varchar(max), 
		Constraint_Name varchar(max)
    )
    
    --Collect all FK relationships for database
insert @Contstraints
	SELECT 
		FK_Table  = FK.TABLE_NAME, 
		FK_Column = CU.COLUMN_NAME, 
		PK_Table  = PK.TABLE_NAME, 
		PK_Column = PT.COLUMN_NAME, 
		Constraint_Name = C.CONSTRAINT_NAME 
	FROM 
		INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS C 
		INNER JOIN 
		INFORMATION_SCHEMA.TABLE_CONSTRAINTS FK 
			ON C.CONSTRAINT_NAME = FK.CONSTRAINT_NAME 
		INNER JOIN 
		INFORMATION_SCHEMA.TABLE_CONSTRAINTS PK 
			ON C.UNIQUE_CONSTRAINT_NAME = PK.CONSTRAINT_NAME 
		INNER JOIN 
		INFORMATION_SCHEMA.KEY_COLUMN_USAGE CU 
			ON C.CONSTRAINT_NAME = CU.CONSTRAINT_NAME 
		INNER JOIN 
		( 
			SELECT 
				i1.TABLE_NAME, i2.COLUMN_NAME 
			FROM 
				INFORMATION_SCHEMA.TABLE_CONSTRAINTS i1 
				INNER JOIN 
				INFORMATION_SCHEMA.KEY_COLUMN_USAGE i2 
				ON i1.CONSTRAINT_NAME = i2.CONSTRAINT_NAME 
				WHERE i1.CONSTRAINT_TYPE = 'PRIMARY KEY' 
		) PT 
		ON PT.TABLE_NAME = PK.TABLE_NAME
		order by  FK.TABLE_NAME



--Cte for tracking labeling the leaf position of each table in the fk relationship tree

;
with cte as (
	SELECT 
		FK_Table,  
		FK_Column, 
		PK_Table,
		PK_Column,
		Constraint_Name,
		1 as depth
	FROM 
		@Contstraints q1
		where q1.FK_Table !=q1.PK_Table
	union all
	SELECT 
		q2.FK_Table,  
		q2.FK_Column, 
		q2.PK_Table,
		q2.PK_Column,
		q2.Constraint_Name,
		depth+1 depth
	FROM @Contstraints q2
		 join cte on cte.FK_Table = q2.PK_Table 
		 where q2.FK_Table !=q2.PK_Table
    )
    
--Save results for Relationships and specify whether table has a Multitennant identifier built in	
	insert @TblInfo
	
		Select distinct 
			cte.FK_Table	--Table with FK dependency
			,FK_Column		--Column that contains the FK linked Identifier
			,PK_Table		--Table Fk relatioship is dependent on
			,PK_Column		--Column that dependent fk relatioship references
			,Constraint_Name, --Name of constraint
			(select COUNT(*) from INFORMATION_SCHEMA.COLUMNS
				where TABLE_NAME = cte.FK_Table and COLUMN_NAME = @columnname),
			(select COUNT(*) from INFORMATION_SCHEMA.COLUMNS
				where TABLE_NAME = cte.PK_Table and COLUMN_NAME = @columnname),
		cte.depth 
		from (
			select max(depth)Depth,FK_Table from cte group by FK_Table) q1
			join cte on cte.FK_Table = q1.FK_Table and cte.depth = q1.Depth and cte.FK_Table != cte.PK_Table

 
 
--Builds the queries needs to unique identify records by a multi tennant identifier.
--This is only performed on tables that do not have the Multi tennant column in their definition
;
with QueryBuilder
as 
(		select	FK_table    --Table currently under evaluation
				,PK_table   --Table with Closest FK relationship
				,PK_Column  --Column on table linked to for FK relationship
				,FK_Column  --Column on FK dependent table for FK relationship
				,ColumnPresent --If the multitennant id is available on th current table
				,ColumnPresentPK -- if the multitennant id is available on the up stream joined FK table
				,convert(varchar(max),FK_table) +','+ PK_table FullTblPath --entire path for fk join
				,CONVERT(varchar(max),fk_column ) FullColPath --entire path for fk join
					--Construction of join statement
				,Convert(varchar(max),' from '+FK_table+' where '+FK_Column +' in ( Select '+FK_Column+' from '+FK_table +' join '
						+ PK_table+ ' on '+FK_table +'.'+FK_Column+'='+PK_table+'.'+PK_Column ) JoinStmt 
				,FK_Column OriginalColumn
				, 1 depth --Depth of recursion needed for current join
		from @tblInfo q0 where q0.ColumnPresent =0
		union all
	select	QueryBuilder.FK_table
			,q1.PK_table
			,q1.PK_Column
			,q1.FK_Column
			,q1.ColumnPresent
			,q1.columnPresentPK
			,FullTblPath+','+q1.PK_table FullTblPath
			,FullColPath+','+q1.FK_Column FullColPath
			,JoinStmt+Char(30)+' join '+ q1.PK_table+ ' on '+QueryBuilder.PK_table+'.'+q1.FK_Column+'='+q1.PK_table+'.'+q1.PK_Column JoinStmt
			,QueryBuilder.FK_Column OriginalColumn
			,QueryBuilder.depth  +1 depth 
			from QueryBuilder 
		join @tblInfo q1 on QueryBuilder.PK_table = q1.FK_table
		where q1.ColumnPresent !=1
)


--Selects all the queries produced appending the where clause for the filtering of multitennant clinics)		
insert @FKRelationshipJoins
Select	Querybuilder.FK_table
		,PK_table
		,PK_Column	
		,FK_Column	
		,ColumnPresent	
		,ColumnPresentPK	
		,FullTblPath
		,FullColPath	
		,OriginalColumn
		,Querybuilder.depth	
		,JoinStmt+' where '+@columnname+' = @tennantIdentifier'+')' JoinStmtfiltered
		from (
				select max(depth)Depth,FK_Table from querybuilder group by FK_Table) q1
			join querybuilder on querybuilder.FK_Table = q1.FK_Table and querybuilder.depth = q1.Depth
		where columnPresentPK = 1
				
					
		select * from @TblInfo 
		order by depth desc
		select * from @FKRelationshipJoins
		--select FK_table,FullPath, (select FK_Column from @TblInfo q1 where q1.FK_Table=q2.FK_table and '%'+PK_table+'%'  from @FKRelationshipJoins q2
		--select distinct FK_Table from @Contstraints 

		--Retuns all Tables that have FK relationships but do not have the multitennant column identifier anywhere in their leaf nodes
		select * from @TblInfo where FK_Table not in (select FK_Table from @FKRelationshipJoins)
		and (select COUNT(*) from INFORMATION_SCHEMA.COLUMNS
				where TABLE_NAME = FK_Table and COLUMN_NAME = @columnname) =0
				
				
		--Returns all base tables that have the specified Multi tennant Column in their columnlist but do not have a constraint creted for it
		select * from INFORMATION_SCHEMA.TABLES t 
		where TABLE_NAME not in ( select fk_table from @TblInfo)
		and (select COUNT(*) from INFORMATION_SCHEMA.COLUMNS c where c.TABLE_NAME = t.TABLE_NAME  and c.COLUMN_NAME = @columnname) >=1 
		and TABLE_TYPE = 'Base Table'
		
		--Returns all Tables that have constraints but are not being picked up by our Constrain mappings
		--select * from @Contstraints q0 where FK_table not in(select fk_table from @TblInfo  q1 )
		
		
		--Master Join Statement. This statement is current tailored for a delete but it could be easily
		-- changed to be and other type of DML by simply changing the statement prefixing the Join stmnt and the sort order
		
		
				
		select FK_Table,'delete '+JoinStmt+'--LeafNode depth:'+Convert(varchar(2),depth) JoinStmt ,depth from (
		select q1.FK_Table
		,coalesce(q2.JoinStmt,(' from '+ q1.FK_table+' where '+ @columnname+'=@tennantIdentifier')) as JoinStmt
		,Coalesce(q1.Depth,'0' ) depth from @TblInfo  q1
		left join @FKRelationshipJoins q2 on q2.FK_table=q1.FK_Table
		union all
		select TABLE_NAME,' from '+ TABLE_NAME+' where '+ @columnname+'=@tennantIdentifier ','0' depth from INFORMATION_SCHEMA.TABLES t 
		where TABLE_NAME not in ( select fk_table from @TblInfo)
		and (select COUNT(*) from INFORMATION_SCHEMA.COLUMNS c where c.TABLE_NAME = t.TABLE_NAME  and c.COLUMN_NAME = @columnname) >=1 
		and TABLE_TYPE = 'Base Table') q4
		where FK_Table not in
		(		select FK_Table from @TblInfo where FK_Table not in (select FK_Table from @FKRelationshipJoins)
						and (select COUNT(*) from INFORMATION_SCHEMA.COLUMNS
								where TABLE_NAME = FK_Table and COLUMN_NAME = @columnname) =0)
		order by q4.depth 
		--Desc For delete ASC for insert - Updates are tricy and cannot be guaranteed to work.
		desc
		--asc

Friday, November 18, 2011

Syntax Highlighting on Blogger

Given the Volatile nature of the internet I have opted to repost a very useful and straightforward blog post from BlogSpot. The Post outlines how to add syntax highlighting to your Blogger blog account. Also If you are using Windows Live Writer to post your blogs there is a plugin to allow you to add  Syntax Highlighting for your language of choice right from the app.

1.Login to your blogger dashboard--> layout- -> Edit HTML
2.Now Scroll down to where you see </head> tag .
3.Copy below code and paste it just before the </head> tag.

<link href='http://alexgorbatchev.com/pub/sh/2.1.382/styles/shCore.css' rel='stylesheet' type='text/css'/> 
<link href='http://alexgorbatchev.com/pub/sh/2.1.382/styles/shThemeDefault.css' rel='stylesheet' type='text/css'/> 
<script src='http://alexgorbatchev.com/pub/sh/2.1.382/scripts/shCore.js' type='text/javascript'/> 
<script src='http://alexgorbatchev.com/pub/sh/2.1.382/scripts/shBrushCpp.js' type='text/javascript'/> 
<script src='http://alexgorbatchev.com/pub/sh/2.1.382/scripts/shBrushCSharp.js' type='text/javascript'/> 
<script src='http://alexgorbatchev.com/pub/sh/2.1.382/scripts/shBrushCss.js' type='text/javascript'/> 
<script src='http://alexgorbatchev.com/pub/sh/2.1.382/scripts/shBrushJava.js' type='text/javascript'/> 
<script src='http://alexgorbatchev.com/pub/sh/2.1.382/scripts/shBrushJScript.js' type='text/javascript'/> 
<script src='http://alexgorbatchev.com/pub/sh/2.1.382/scripts/shBrushPhp.js' type='text/javascript'/> 
<script src='http://alexgorbatchev.com/pub/sh/2.1.382/scripts/shBrushPython.js' type='text/javascript'/> 
<script src='http://alexgorbatchev.com/pub/sh/2.1.382/scripts/shBrushRuby.js' type='text/javascript'/> 
<script src='http://alexgorbatchev.com/pub/sh/2.1.382/scripts/shBrushSql.js' type='text/javascript'/> 
<script src='http://alexgorbatchev.com/pub/sh/2.1.382/scripts/shBrushVb.js' type='text/javascript'/> 
<script src='http://alexgorbatchev.com/pub/sh/2.1.382/scripts/shBrushXml.js' type='text/javascript'/> 
<script src='http://alexgorbatchev.com/pub/sh/2.1.382/scripts/shBrushPerl.js' type='text/javascript'/> 
<script language='javascript'> 
SyntaxHighlighter.config.bloggerMode = true;
SyntaxHighlighter.config.clipboardSwf = &#39;http://alexgorbatchev.com/pub/sh/2.1.382/scripts/clipboard.swf&#39;;
SyntaxHighlighter.defaults[&#39;toolbar&#39;] = true;
SyntaxHighlighter.all();
</script>
<script language='javascript'> 
SyntaxHighlighter.config.bloggerMode = true;
SyntaxHighlighter.config.clipboardSwf = &#39;http://alexgorbatchev.com/pub/sh/2.1.382/scripts/clipboard.swf&#39;;
SyntaxHighlighter.defaults[&#39;toolbar&#39;] = true;
SyntaxHighlighter.all();
</script>

4.Now Click on "Save Templates".
5.Whenever you have to write some codes in your posts, click on "Edit Html" tab of your post editor and write the codes between the following tags :

<pre class="brush: sql;">

...Your html-escaped code goes here...

</pre>

Friday, November 11, 2011

Long running jobs(SQL)

What do you do when you have a job that takes a significant amount of time to process. Well the obvious solution is either improve its performance to speed up the job or wait. When the latter is the only choice you find yourself wanting to know how long the job took or wanting to know when its finished.

This can be achieved fairly easily just by adding alerts into the job. Simple to do and built into the engine, awesome. But what do you do when a job runs continually or to be more precise, runs as soon as it finishes. Maybe you have a reporting job that is constantly needing refreshed data on as fast as an interval as it allows. Maybe you have a log shipping scenario where if you interrupt the entire process it will brake the configuration but preventing it from running again once its finished will not and don't want to make a notification that spams your inbox? What if you need to perform tasks between occurrences of a job that are one off requests? Most of these scenarios would either require diligent supervision and manual interaction.

This is what I wrote SP_Jobstatus for. It waits for a job to finish before returning an output and will hold a query open for as long as needed until the job reaches the desired status. It takes two parameters: @jobname is the freindly name of the sql server job. @WaitForStart is a switch that controls whether you are waiting for the job to start or waiting for it to end.
Create procedure [dbo].[SP_Jobstatus] @job_name nvarchar(max),@waitforstart bit = 0
as

begin

create table #tmpJobStatus (
Job_ID uniqueidentifier
,Last_Run_Date bigint
,Last_Run_Time bigint
,Next_Run_Date bigint
,Next_Run_Time bigint
,Next_Run_Schedule_ID int 
,Requested_To_Run int 
,Request_Source int
,Request_Source_ID varchar(30)
,Running int
,Current_Step int
,Current_Retry_Attempt int
,[State] int)


Declare @var sysname
Declare @job_ID UNIQUEIDENTIFIER 
--set @job_name = 

select @var = SUSER_SNAME()
SET NOCOUNT ON 
EXECUTE msdb..sp_verify_job_identifiers '@job_name',
'@job_id',
@job_name OUTPUT,
@job_id OUTPUT

insert into #tmpJobStatus 
EXECUTE master.dbo.xp_sqlagent_enum_jobs 1 ,@var ,@job_ID

declare @time datetime = getdate()

declare @runstatus int
select @runstatus = Running from #tmpJobStatus
select @runstatus


while((@runstatus !=0 and @waitforstart = 0) or (@runstatus =0 and @waitforstart = 1 and datediff(second,@time,getdate() ) <20))
begin

truncate table #tmpJobStatus 

insert into #tmpJobStatus 
EXECUTE master.dbo.xp_sqlagent_enum_jobs 1 ,@var ,@job_ID 

select @runstatus = Running from #tmpJobStatus 
waitfor delay '00:00:15' 
end 
drop table #tmpJobStatus 
end 
Currently it is using a temp table and truncate for speed but an in memory table var would work as well with some tweeking.