SQLServerCentral Article

Finding Column Dependencies

,

Introduction

Have you ever tried to determine column dependencies within your database? If yes, did you also try to determine these dependencies across different databases?

The first times I tried to analyze my databases, I became frustrated very quickly. All my efforts to analyze the code of views and SPs to get the necessary information led to the feeling that this is far too complex for me to handle within a short period of time.

But then after long hours of research, finally a thought reached my mind which turned out to be quite useful:

The concept

SQL Server provides the SHOWPLAN_XML SET statement, which lets you output the query plan of queries when set to ON. This query plan contains column references even across databases.

Lets have a look at a quick example:

   SET SHOWPLAN_XML ON
   GO
   SELECT * FROM sys.objects
   

If you execute these two batches, you should be able to click on the resulting execution plan. And inside this plan, you should be able to see a lot of the "ColumnReference" elements. Isn't this exactly what we need? Ok, it is still a little unorganized, since these references are spread among all levels of the XML document.

So in the next step we will try to extract only the relevant ColumnReference elements.

Extracting the relevant information

To store the column references, we also need a table:

CREATE TABLE [AdventureWorks].[dbo].[ColDep](
   [ID] [int] IDENTITY(1,1) NOT NULL,
   [Source]   [nvarchar] (255) NOT NULL,
   [Database] [nvarchar] (255) NOT NULL,
   [Schema]   [nvarchar] (255) NOT NULL,
   [Table]    [nvarchar] (255) NOT NULL,
   [Column]   [nvarchar] (255) NOT NULL
) ON [PRIMARY]

Now we need some external VB code, because there is no direct way to work with the resulting XML plan of the Showplan feature (of course thiscould be written in almost any kind of language):

Imports System.Data.SqlClient
Imports System.Xml
Public Module modColDepCheck
   ' Connect to the local server (.) and use a trusted connection
   Const cstrConnectionString = "Data Source=.;Trusted_Connection=yes;"
   Const cstrSQLText = "SELECT FirstName FROM Person.Contact WHERE LastName <> ''"
   ' Analyze the query stored in cstrSQLText    
   Sub Main()
      Dim lcmdCommand As New SqlCommand
      Dim lxmlReader As Xml.XmlReader
      Using lcnConnPlan As New SqlConnection(cstrConnectionString)
         lcnConnPlan.Open()
         lcmdCommand.Connection = lcnConnPlan
         lcmdCommand.CommandType = CommandType.Text
         ' Set Showplan_XML On so we can get the query plan with the dependencies inside
         lcmdCommand.CommandText = "USE AdventureWorks"
         lcmdCommand.ExecuteNonQuery()
         lcmdCommand.CommandText = "SET SHOWPLAN_XML ON"
         lcmdCommand.ExecuteNonQuery()
         ' Load the XML Plan 
         lcmdCommand.CommandText = cstrSQLText
         lxmlReader = lcmdCommand.ExecuteXmlReader()
         ' Now we parse the plan and insert each instance of ColumnReference into our 
         ' tracking table.
         Using lcnConnInsert As New SqlConnection(cstrConnectionString)
            ' Initialize insert connection, command + params.
            lcnConnInsert.Open()
            lcmdCommand.Connection = lcnConnInsert
            lcmdCommand.CommandText = "INSERT INTO AdventureWorks.dbo.ColDep SELECT @application, @database,@schema,@table,@column"
            lcmdCommand.Parameters.Add(New SqlParameter("@application", SqlDbType.NVarChar))
            lcmdCommand.Parameters.Add(New SqlParameter("@database", SqlDbType.NVarChar))
            lcmdCommand.Parameters.Add(New SqlParameter("@schema", SqlDbType.NVarChar))
            lcmdCommand.Parameters.Add(New SqlParameter("@table", SqlDbType.NVarChar))
            lcmdCommand.Parameters.Add(New SqlParameter("@column", SqlDbType.NVarChar))
            lcmdCommand.Parameters("@application").Value = cstrSQLText
            ' As long as we have elements to come...
            While lxmlReader.Read()
               If lxmlReader.Name = "ColumnReference" And lxmlReader.GetAttribute("Database") <> "" Then
                  lcmdCommand.Parameters("@database").Value = lxmlReader.GetAttribute("Database")
                  lcmdCommand.Parameters("@schema").Value = lxmlReader.GetAttribute("Schema")
                  lcmdCommand.Parameters("@table").Value = lxmlReader.GetAttribute("Table")
                  lcmdCommand.Parameters("@column").Value = lxmlReader.GetAttribute("Column")
                  lcmdCommand.ExecuteNonQuery()
               End If 'lxmlReader.Name = "ColumnReference" And lxmlReader.GetAttribute("Database") <> "" Then
            End While ' lxmlReader.Read()
         End Using 'lcnConnInsert 
      End Using ' lcnConnPlan
      Console.WriteLine("Done. Hit any key to continue...")
      Console.ReadKey()
      End
   End Sub
End Module

This code does the following: It activates the XML Showplan for the connection. From this point onwards, the connection will never execute statements until it encounters a SET SHOWPLAN_XML OFF statement. It will only output the XML query plan. We then issue the statement we want to check for column dependencies. The result will be an XML stream which we need to parse using the Read() and the GetAttribute() Methods of the XmlReader. The occurrences are directly saved into the ColDep table.

Try to play around with the cstrSQLText constant (the input query). As an example, you could use the following statement:

SELECT LastName FROM Person.Contact

It should return only the LastName column, since it is the only one being used.

Now try the following statement

      SELECT * FROM Person.Contact
      

The result should be all 15 columns being used.

And what happens if we use a view which uses an asterisk, which is based on a table that was modified without recompiling the view? Here is the script you need to simulate this

-- Create the table with one column
CREATE TABLE Test (OriginalColumn int NOT NULL)
GO
-- Create a view with asterisk. 
CREATE VIEW TestView AS SELECT * FROM Test
GO
-- Now add a column
ALTER TABLE Test ADD ColumnAdded int NULL
-- The table now returns all columns including the new one
SELECT * FROM Test
-- As expected, the view still returns only the columns
-- that were available when the view was created and not
-- the ColumnAdded column.
SELECT * FROM TestView
GO
-- Lets see now what the SHOWPLAN_XML returns
SET SHOWPLAN_XML ON
GO
SELECT * FROM TestView
GO

As expected, the query plan also only includes the columns that were available when the view was created.

You can try examples of your own. How about converts? Case statements? Anything that you can think of. Most of the stuff should work.

Restrictions

This method has some restrictions you need to keep in mind:

  • Temporary tables created with SELECT INTO statements:
  • If you try to execute the following query with SET SHOWPLAN_XML OFF, you should have no issue.
    SELECT 1 a INTO #temp 
    SELECT * FROM #temp
    DROP TABLE #temp
    GO
    

    But if you try to execute it with SET SHOWPLAN_XML ON, you will get an error "Invalid object name '#temp'." The reason for this is the fact that the table does not yet exist when the query is compiled. In regular execution mode, this would cause a re-compile of the query right after the creation of the temporary table. (This is actually the reason why creating temporary tables in stored procedures is usually a bad idea). More information about query recompiles can be found here: http://www.microsoft.com/technet/prodtechnol/sql/2005/recomp.mspx.

  • 2. Dynamic SQL - Dynamic SQL cannot be parsed correctly. The query plan for dynamic SQL is the statement itself with no details. Try any kind of dynamic SQL statement to see the results of the SHOWPLAN_XML.

Conclusions

The discussed method is easy to implement and should be a good tool to view column dependencies across databases.

It can be used in an even more automated process which checks each view / trigger / stored procedure / function in every database

to generate an inventory.

You still have to keep the restrictions regarding dynamic SQL and temporary tables from SELECT INTO statements in mind.

Rate

5 (2)

You rated this post out of 5. Change rating

Share

Share

Rate

5 (2)

You rated this post out of 5. Change rating