Now that we’ve explored, in preceding levels, some of the information that is available about indexes, triggers, keys and distribution statistics, we can concentrate on the tables themselves and their columns.
- References to and from tables
- Getting the properties of tables
- Finding out about table columns
- Finding which tables have a particular column
- Finding potential problems in tables
- The tables that are wide (more than 15 in this example; you can modify to taste)
- Tables that are heaps
- Tables that are undocumented using extended properties
- Tables without a Primary Key
- Tables with no indexes at all
- Tables with no candidate key (unique constraint on column(s))
- Tables with disabled Index(es)
- Tables with disabled constraint(s)
- Tables with untrusted constraint(s)
- Tables with a disabled Foreign Key(s)
- Tables with untrusted Foreign Key(s)
- Tables unrelated to any other table
- Tables with unintelligible column names
- Tables with a foreign key that has no index
- Tables with a GUID in a clustered Index
- Tables with non-compliant column names
- Tables with a trigger that hasn’t got NOCOUNT ON
- Tables that are not referenced by any procedure, view or function
- Tables with a disabled trigger
- Tables that can't be indexed
- Table has unique constraint that is NULLable
- Getting information on table keys and indexes
- Determining dependency in tables
References to and from tables
Tables can be referenced by other objects such as views and stored procedures. The following code shows us what objects reference the table HumanResources.employee in AdventureWorks2012. (All our examples were tested on this database).
SELECT coalesce(object_schema_name(Referencing_ID)+'.','')+ --likely schema name object_name(Referencing_ID)+ --definite entity name coalesce('.'+col_name(referencing_ID,referencing_minor_id),'') AS [referencing], referencing_minor_ID FROM sys.sql_expression_dependencies INNER JOIN sys.objects o ON referencing_ID=o.object_ID WHERE referenced_id =object_id('HumanResources.employee') AND is_schema_bound_reference=0
We can also see what is referenced by any child object of a table, by which I mean objects such as triggers and check constraints,
SELECT coalesce(Referenced_server_name+'.','')+ --possible server name if cross-server coalesce(referenced_database_name+'.','')+ --possible database name if cross-database coalesce(referenced_schema_name+'.','')+ --likely schema name coalesce(referenced_entity_name,'') + --very likely entity name coalesce('.'+col_name(referenced_ID,referenced_minor_id),'')AS [referenced], convert(varchar(128),coalesce(object_schema_name(Referencing_ID)+'.','')+ --likely schema name object_name(Referencing_ID)+ --definite entity name coalesce('.'+col_name(referencing_ID,referencing_minor_id),'')) +' ('+ lower(replace(type_desc,'_',' '))+')' AS [referencing] FROM sys.sql_expression_dependencies INNER JOIN sys.objects o ON referencing_ID=o.object_ID WHERE parent_object_id=object_id('HumanResources.employee')
We are only looking at references in code here, including the code within check constraints, rather than those references defined by foreign key constraints. If we perform the same query but using a table with a complex trigger, such as ‘ales.SalesOrderDetail we can find some surprising dependencies.
As you can see, tables can reference other tables, via a trigger in this case, but you’ll notice that these code references (called ‘soft’ references) are not the only type. There are these ‘soft’ dependencies; references to other objects in SQL code that are exposed by sys.sql_expression_dependencies, and ‘hard’ dependencies that are exposed by the object catalog views. ‘Hard’ dependencies are inherent in the structure of the database, whereas code can reference objects in another database on the same server or on another server. If foreign key constraints have been properly added to enforce the integrity of these references, we can determine this network of dependencies. If, for example we wanted to find out what tables HumanResources.employee references, and the tables that reference it, we would use this code…
SELECT object_schema_name(parent_object_ID)+'.' +object_name(parent_object_ID)AS referrer, object_schema_name(referenced_object_ID)+'.' +object_name(referenced_object_ID) AS referenced FROM sys.foreign_keys WHERE parent_object_ID = object_id('HumanResources.employee') OR referenced_object_ID = object_id('HumanResources.employee')
But if you link tables via foreign key references that aren’t enforced by foreign key constraints, then you can’t detect these references this way, and you can only determine them if a procedure, view or function links the two tables.
Getting the properties of tables
Here is a statement that gives a summary of the main features of your database’s tables, providing the following information
- indexes: whether it has an index of any type, a clustered index, a primary key, a nonclustered index, an active full-text index
- constraints: Whether there is a CHECK constraint, a UNIQUE constraint, a DEFAULT constraint, or a FOREIGN KEY constraint. It also tells you if the table is referenced by a FOREIGN KEY constraint.
- triggers Whether the table has an INSERT trigger, an UPDATE trigger or a DELETE trigger.
- special types of columns: whether the table has an identity column. a ROWGUIDCOL for a uniqueidentifier, any legacy large object (text, ntext, or image) column or a timestamp column.
SELECT Object_Schema_name(t.object_ID)+'.'+t.name AS [Qualified Name], --questions about indexes CASE WHEN OBJECTPROPERTYEX(object_id,'TableHasIndex') = 0 THEN 'no' ELSE 'yes' END AS [Any index],--Table has an index of any type. CASE WHEN OBJECTPROPERTYEX(object_id,'TableHasClustIndex') = 0 THEN 'no' ELSE 'yes' END AS [Clustered Index],--Table has a clustered index. CASE WHEN OBJECTPROPERTYEX(object_id,'TableHasPrimaryKey') = 0 THEN 'no' ELSE 'yes' END AS [Primary Key],--Table has a primary key CASE WHEN OBJECTPROPERTYEX(object_id,'TableHasNonclustIndex') = 0 THEN 'no' ELSE 'yes' END AS [nonCl Index],--Table has a nonclustered index. CASE WHEN OBJECTPROPERTYEX(object_id,'TableHasActiveFulltextIndex') = 0 THEN 'no' ELSE 'yes' END AS [FT index],--Table has an active full-text index. --questions about constraints CASE WHEN OBJECTPROPERTYEX(object_id,'TableHasCheckCnst') = 0 THEN 'no' ELSE 'yes' END AS [Check Cnst],--Table has a CHECK constraint. CASE WHEN OBJECTPROPERTYEX(object_id,'TableHasUniqueCnst') = 0 THEN 'no' ELSE 'yes' END AS [Unique Cnst],--Table has a UNIQUE constraint. CASE WHEN OBJECTPROPERTYEX(object_id,'TableHasDefaultCnst') = 0 THEN 'no' ELSE 'yes' END AS [Default Cnst],--Table has a DEFAULT constraint. CASE WHEN OBJECTPROPERTYEX(object_id,'TableHasForeignKey') = 0 THEN 'no' ELSE 'yes' END AS [FK Cnst],--Table has a FOREIGN KEY constraint. CASE WHEN OBJECTPROPERTYEX(object_id,'TableHasForeignRef') = 0 THEN 'no' ELSE 'yes' END AS [FK Ref],--Table is referenced by a FOREIGN KEY constraint. --questions about triggers CASE WHEN OBJECTPROPERTYEX(object_id,'TableHasInsertTrigger') = 0 THEN 'no' ELSE 'yes' END AS [Insert Tgr],--Object has an INSERT trigger. CASE WHEN OBJECTPROPERTYEX(object_id,'TableHasUpdateTrigger') = 0 THEN 'no' ELSE 'yes' END AS [Update Tgr],--Table has an UPDATE trigger. CASE WHEN OBJECTPROPERTYEX(object_id,'TableHasDeleteTrigger') = 0 THEN 'no' ELSE 'yes' END AS [Delete Tgr],--Table has a DELETE trigger. --questions about types of columns CASE WHEN OBJECTPROPERTYEX(object_id,'TableHasIdentity') = 0 THEN 'no' ELSE 'yes' END AS [Identity Col],--Table has an identity column. CASE WHEN OBJECTPROPERTYEX(object_id,'TableHasRowGuidCol') = 0 THEN 'no' ELSE 'yes' END AS [ROWGUIDCOL],--has a ROWGUIDCOL for a uniqueidentifier col. CASE WHEN OBJECTPROPERTYEX(object_id,'TableHasTextImage') = 0 THEN 'no' ELSE 'yes' END AS [Has Lob],--Table has a text, ntext, or image column. CASE WHEN OBJECTPROPERTYEX(object_id,'TableHasTimestamp') = 0 THEN 'no' ELSE 'yes' END AS [Timestamp]--Table has a timestamp column. FROM sys.tables t ORDER BY [Qualified Name]
Finding out about table columns
You can determine the columns and their datatypes quite easily from queries accessing the catalog views.
For example, if you want to quickly find out about the columns of the person.contact table, (or person.person in later versions of AdventureWorks) you can run this statement, to get a quick summary.
SELECT coalesce(object_schema_name(obj.object_ID)+'.','')+obj.name as ItsName, col.name+' '+t.name+ --now we get the datatype's details CASE WHEN t.name IN ('char', 'varchar','nchar','nvarchar') THEN '('+ --we fetch the length of the data CASE WHEN col.max_length=-1 THEN 'MAX' ELSE convert(VARCHAR(4), --get the length CASE WHEN t.name IN ('nchar','nvarchar') THEN col.max_length/2 ELSE col.max_length END ) END+')' WHEN t.name IN ('decimal','numeric') --we need scale and precision THEN '('+ convert(VARCHAR(4),col.precision)+',' + convert(VARCHAR(4),col.Scale)+')' ELSE '' --now all we have to do is to get the column's extended properties END+ coalesce (' /* '+convert(varchar(128),ep.value)+ ' */','') as TheColumn FROM sys.all_objects obj -- from all the objects (system and database) INNER JOIN sys.all_columns col --to get all the columns ON col.object_ID=obj.object_ID INNER JOIN sys.types t --to get the details of the types ON col.user_type_id=t.user_type_id LEFT OUTER JOIN sys.extended_properties ep --and the documentation ON col.object_id = ep.major_ID AND col.column_ID = minor_ID AND class=1 WHERE obj.object_ID=object_id('person.contact') ORDER BY column_ID
You will immediately see from this listing the great advantage of having extended properties set properly on columns!
I’ve written this as a general column-lister for any table, view or table-valued function. If you need to see what is in sys.tables, for example, you can use the same query.
Finding which tables have a particular column
So how do you find what tables have a particular column name? Let’s see what tables contain a column called ProductID.
SELECT OBJECT_SCHEMA_NAME(o.Object_ID)+'.'+OBJECT_NAME(o.Object_ID) +' ('+ LOWER(REPLACE(o.type_desc,'_',' '))+') ' + COALESCE (' /* '+CONVERT(VARCHAR(128),ep.value)+ ' */','') AS TheObject FROM sys.All_Columns c INNER JOIN sys.all_Objects o ON c.object_ID=o.object_ID LEFT OUTER JOIN sys.extended_properties ep --and the documentation ON c.object_id = ep.major_ID AND c.column_ID = minor_ID AND class=1 WHERE c.name LIKE 'ProductID'
I’ve added the type of object as a convenience. After all it isn’t just tables that have columns!
Finding potential problems in tables
Tables can have a number of problems that can potentially present performance difficulties. It is easy to search your tables for possible issues.
The tables that are wide (more than 15 in this example; you can modify to taste):
SELECT OBJECT_SCHEMA_NAME(t.object_id) + '.' + OBJECT_NAME(t.object_id) AS TheTable, CONVERT(VARCHAR(5), COUNT(*)) + ' columns wide (more than 15 columns)' AS smell FROM sys.columns c INNER JOIN sys.tables t ON c.object_id = t.object_id GROUP BY t.object_id HAVING COUNT(*) > 15;
Tables that are heaps:
SELECT DISTINCT OBJECT_SCHEMA_NAME(t.Object_ID) + '.' + OBJECT_NAME(t.Object_ID) AS TheTable, 'heap' AS smell FROM sys.indexes /* see whether the table is a heap */ INNER JOIN sys.tables t ON t.object_ID = sys.indexes.object_ID WHERE sys.indexes.type = 0;
Tables that are not documented with extended properties;
SELECT OBJECT_SCHEMA_NAME(Object_ID) + '.' + OBJECT_NAME(Object_ID) AS TheTable, 'Undocumented table' AS smell FROM sys.objects s /* it has no extended properties */ LEFT OUTER JOIN sys.extended_properties ep ON s.object_ID = ep.major_ID AND minor_ID = 0 WHERE type_desc = 'USER_TABLE' AND ep.value IS NULL;
Tables without a Primary Key:
SELECT OBJECT_SCHEMA_NAME(Object_ID) + '.' + OBJECT_NAME(Object_ID) AS TheTable, 'No primary key' AS smell FROM sys.tables /* see whether the table has a primary key */ WHERE OBJECTPROPERTY(OBJECT_ID, 'TableHasPrimaryKey') = 0;
Tables with no indexes at all:
SELECT OBJECT_SCHEMA_NAME(Object_ID) + '.' + OBJECT_NAME(Object_ID) AS TheTable, 'No index at all' AS smell FROM sys.tables /* see whether the table has any index */ WHERE OBJECTPROPERTY(OBJECT_ID, 'TableHasIndex') = 0;
Tables with no candidate key (unique constraint on column(s)):
SELECT OBJECT_SCHEMA_NAME(Object_ID) + '.' + OBJECT_NAME(Object_ID) AS TheTable, 'No candidate key' AS smell FROM sys.tables /* if no unique constraint then it isn't relational */ WHERE OBJECTPROPERTY(OBJECT_ID, 'TableHasUniqueCnst') = 0;
Tables with disabled Index(es):
SELECT OBJECT_SCHEMA_NAME(Object_ID) + '.' + OBJECT_NAME(Object_ID) AS TheTable, 'disabled Index(es)' AS smell FROM sys.indexes /* don't leave these lying around */ WHERE is_disabled = 1;
Tables with disabled constraint(s):
SELECT OBJECT_SCHEMA_NAME(Parent_Object_ID) + '.' + OBJECT_NAME(Parent_Object_ID) AS TheTable, 'disabled constraint(s)' AS smell FROM sys.check_constraints /* hmm. i wonder why */ WHERE is_disabled = 1;
Tables with untrusted constraint(s):
SELECT OBJECT_SCHEMA_NAME(Parent_Object_ID) + '.' + OBJECT_NAME(Parent_Object_ID) AS TheTable, 'untrusted constraint(s)' AS smell FROM sys.check_constraints /* ETL gone bad? */ WHERE is_not_trusted = 1;
Tables with a disabled Foreign Key(s):
SELECT OBJECT_SCHEMA_NAME(Parent_Object_ID) + '.' + OBJECT_NAME(Parent_Object_ID) AS TheTable, 'disabled FK' AS smell FROM sys.foreign_keys /* build script gone bad? */ WHERE is_disabled = 1;
Tables with untrusted Foreign Key(s):
SELECT OBJECT_SCHEMA_NAME(Parent_Object_ID) + '.' + OBJECT_NAME(Parent_Object_ID) AS TheTable, 'untrusted FK' AS smell FROM sys.foreign_keys /* Why do you have untrusted FKs? Constraint was enabled without checking existing rows; therefore, the constraint may not hold for all rows. */ WHERE is_not_trusted = 1;
Tables unrelated to any other table:
SELECT OBJECT_SCHEMA_NAME(Object_ID) + '.' + OBJECT_NAME(Object_ID) AS TheTable, 'unrelated to any other table' AS smell FROM sys.tables /* found a simpler way! */ WHERE OBJECTPROPERTYEX(OBJECT_ID, 'TableHasForeignKey') = 0 AND OBJECTPROPERTYEX(OBJECT_ID, 'TableHasForeignRef') = 0;
Tables with unintelligible column names:
SELECT DISTINCT OBJECT_SCHEMA_NAME(Object_ID) + '.' + OBJECT_NAME(Object_ID) AS TheTable, 'unintelligible column names' AS smell FROM sys.columns /* column names with no letters in them */ WHERE name COLLATE Latin1_General_CI_AI NOT LIKE '%[A-Z]%' COLLATE Latin1_General_CI_AI;
Tables with a foreign key that has no index:
SELECT OBJECT_SCHEMA_NAME(keys.Parent_Object_ID) + '.' + OBJECT_NAME(keys.Parent_Object_ID) AS TheTable, 'foreign key ' + keys.Name + ' has no index' AS smell FROM sys.foreign_keys keys INNER JOIN sys.foreign_key_columns TheColumns ON keys.Object_ID = constraint_object_id LEFT OUTER JOIN sys.index_columns ic ON ic.object_ID = TheColumns.parent_Object_Id AND ic.column_ID = TheColumns.parent_Column_Id AND TheColumns.constraint_column_ID = ic.key_ordinal WHERE ic.object_ID IS NULL;
Tables with a GUID in a clustered Index:
SELECT OBJECT_SCHEMA_NAME(Ic.Object_ID) + '.' + OBJECT_NAME(Ic.Object_ID) AS TheTable, COL_NAME(Ic.Object_Id, Ic.Column_Id) + ' is a GUID in a clustered index' AS smell /* GUID in a clusterd IX */ FROM Sys.Index_Columns AS Ic INNER JOIN sys.columns c ON c.object_ID = Ic.object_ID AND c.column_ID = Ic.column_ID INNER JOIN sys.types t ON t.system_type_id = c.system_type_id INNER JOIN sys.indexes i ON i.object_ID = Ic.object_ID AND i.index_ID = Ic.index_ID WHERE t.name = 'uniqueidentifier' AND type_desc = 'CLUSTERED' AND OBJECTPROPERTY(Ic.OBJECT_ID, 'IsSystemTable') = 0;
Tables with non-compliant column names:
SELECT DISTINCT OBJECT_SCHEMA_NAME(Object_ID) + '.' + OBJECT_NAME(Object_ID) AS TheTable, 'non-compliant column names' AS smell FROM sys.columns /* column names that need delimiters*/ WHERE name COLLATE Latin1_General_CI_AI LIKE '%[^_@$#A-Z0-9]%' COLLATE Latin1_General_CI_AI;
Tables with a trigger that doesn’t set NOCOUNT ON:
/* Triggers lacking `SET NOCOUNT ON`, which can cause unexpected results when INSERT statements subsequently use the OUTPUT clause */SELECT OBJECT_SCHEMA_NAME(ta.Object_ID) + '.' + OBJECT_NAME(ta.Object_ID) AS TheTable, 'This table''s trigger, ' + OBJECT_NAME(tr.object_ID) + ', hasn’’t got NOCOUNT ON' AS smell FROM sys.tables ta /* see whether the table has any index */ INNER JOIN sys.triggers tr ON tr.parent_ID = ta.object_ID INNER JOIN sys.sql_modules mo ON tr.object_ID = mo.object_ID WHERE definition NOT LIKE '%set nocount on%';
Tables that are not referenced by any procedure, view or function:
SELECT OBJECT_SCHEMA_NAME(Object_ID) + '.' + OBJECT_NAME(Object_ID) AS TheTable, 'not referenced by procedure, view or function' AS smell FROM sys.tables /* found a simpler way! */ LEFT OUTER JOIN sys.sql_expression_dependencies ON referenced_id = sys.tables.object_id WHERE referenced_id IS NULL;
Tables with a disabled trigger:
SELECT DISTINCT OBJECT_SCHEMA_NAME(Parent_ID) + '.' + OBJECT_NAME(Parent_ID) ) AS TheTable, 'has a disabled trigger' AS smell FROM sys.triggers WHERE is_disabled = 1 AND parent_ID > 0;
Tables that can't be indexed:
SELECT OBJECT_SCHEMA_NAME(Object_ID) + '.' + OBJECT_NAME(Object_ID) ) AS TheTable, 'can''t be indexed' AS smell FROM sys.tables WHERE OBJECTPROPERTY(OBJECT_ID, 'IsIndexable') = 0;
Table has unique constraint that is NULLable:
SELECT DISTINCT OBJECT_SCHEMA_NAME(keys.Parent_Object_ID) + '.' + OBJECT_NAME(keys.Parent_Object_ID) AS TheTable, 'has a unique unique constraint on a NULLable column' AS smell FROM sys.Key_Constraints keys INNER JOIN sys.Index_columns TheColumns ON keys.Parent_Object_ID = TheColumns.Object_ID AND unique_index_ID = index_ID INNER JOIN sys.columns c ON TheColumns.object_ID = c.object_ID AND TheColumns.column_ID = c.column_ID WHERE type = 'UQ' AND is_nullable = 1;
Getting information on table keys and indexes
Sometimes, you need to generate a report of the number of the various types of indexes and keys associated with a table. Here is a query that provides the total number of indexes, and provides the number of this total that are unique indexes, unique keys, primary keys, clustered indexes and so on, using the object catalog views.
SELECT OBJECT_SCHEMA_NAME(a.object_ID) + '.' + OBJECT_NAME(a.object_ID) AS [Table], SUM(CASE WHEN a.name IS NULL THEN 0 ELSE 1 END) AS [indexes], SUM(CASE WHEN a.is_unique <> 0 THEN 1 ELSE 0 END) AS Unique_indexes, SUM(CASE WHEN a.is_unique_constraint <> 0 THEN 1 ELSE 0 END) AS [Unique Key], SUM(CASE WHEN a.is_primary_key <> 0 THEN 1 ELSE 0 END ) AS [Primary Key], SUM(CASE WHEN a.type = 1 THEN 1 ELSE 0 END ) AS [Clustered], SUM(CASE WHEN a.type = 2 THEN 1 ELSE 0 END ) AS [Non-clustered], SUM(CASE WHEN a.type = 3 THEN 1 ELSE 0 END ) AS [XML], SUM(CASE WHEN a.type = 4 THEN 1 ELSE 0 END ) AS [Spatial], SUM(CASE WHEN a.type = 5 THEN 1 ELSE 0 END ) AS [Clustered Columnstore], SUM(CASE WHEN a.type = 6 THEN 1 ELSE 0 END ) AS [Nonclustered Columnstore] FROM sys.indexes a INNER JOIN sys.tables ON a.object_ID = sys.tables.object_id WHERE OBJECT_SCHEMA_NAME(a.object_ID) <> 'sys' -- and a.name is not null GROUP BY a.object_ID;
Using a similar technique, you can get some valuable information about columns in your tables: how many columns there are, their nullability, how many are computed, replicated, sparse and so on.
SELECT OBJECT_SCHEMA_NAME(c.object_ID) + '.' + OBJECT_NAME(c.object_ID) AS [Table], MAX(column_ID) AS [total], SUM(CONVERT(INT, c.is_nullable)) AS [Nullable], SUM(CONVERT(INT, c.is_computed)) AS [computed], SUM(CONVERT(INT, c.is_replicated)) AS [replicated], SUM(CONVERT(INT, c.is_sparse)) AS [sparse], SUM(CONVERT(INT, c.is_xml_document)) AS [XML Doc] FROM sys.columns c INNER JOIN sys.tables t ON c.object_ID = t.object_id GROUP BY c.Object_id ORDER BY total DESC;
Determining dependency in tables
Sometimes, it is important to do a series of operations on a group of tables in a specific order for that operation. If, for example, you need to read data into tables after you’ve done a database build, then the simplest way is to read the data into the tables that do not reference any other tables first. Then you read in the tables that reference only the tables whose data you’ve loaded in. Then you just keep repeating this, checking that you don’t read a table twice. Eventually, you either hit a reference that can’t be resolved, (in which you’d need another slower technique), or you’ve got an excellent fast way of populating a database.
The following batch produces a result that has the tables in the right order for the insertion of data, and you can use it for any operation that needs to be in a similar dependency order. (Deleting tables would be in the reverse order to this!)
SET NOCOUNT ON; DECLARE @Rowcount INT, @ii INT; CREATE TABLE #tables ( TheObject_ID INT, --the tables' object ID TheName SYSNAME, --the name of the table TheSchema SYSNAME, --the schema where it lives HasIdentityColumn INT, --whether it has an identity column TheOrder INT DEFAULT 0 ); --we update this later to impose an order --let's do a topological sort to create the right dependency order -- (For background, see https://en.wikipedia.org/wiki/Topological_sorting) --first we read in all the tables from the database. INSERT INTO #tables(TheObject_ID, TheName, TheSchema, HasIdentityColumn) SELECT TheTable.OBJECT_ID, TheTable.NAME, TheSchema.NAME, CASE WHEN identityColumns.Object_id IS NULL THEN 0 ELSE 1 END FROM sys.tables TheTable INNER JOIN sys.schemas TheSchema ON TheSchema.SCHEMA_ID = TheTable.schema_ID LEFT JOIN (SELECT DISTINCT Object_id FROM sys.columns WHERE is_identity = 1 ) identityColumns ON TheTable.object_id = identityColumns.object_id; /* We'll use a SQL 'set-based' form of the topological sort First, find a list of "start nodes" which have no incoming edges and insert them into a set S; at least one such node must exist in an acyclic graph*/--flag all the immediately safe tables to insert data in UPDATE #tables SET TheOrder = 1 FROM #tables parent --do not reference any other table and aren't referenced by anything LEFT OUTER JOIN sys.foreign_Keys referenced ON referenced.referenced_Object_ID = parent.TheObject_ID LEFT OUTER JOIN sys.foreign_Keys referencing ON referencing.parent_Object_ID = parent.TheObject_ID WHERE referenced.parent_object_ID IS NULL AND referencing.parent_Object_ID IS NULL; UPDATE #tables SET TheOrder = 2 FROM #tables parent --do not reference any other table but might be referenced LEFT OUTER JOIN sys.foreign_Keys referencing ON referencing.parent_Object_ID = parent.TheObject_ID AND referencing.referenced_Object_ID <> parent.TheObject_ID WHERE referencing.parent_Object_ID IS NULL AND TheOrder = 0; --i.e. it hasn't been ordered yet SELECT @Rowcount = 100, @ii = 3; --and then do tables successively as they become 'safe' WHILE @Rowcount > 0 BEGIN UPDATE #tables SET TheOrder = @ii WHERE #tables.TheObject_ID IN ( SELECT parent.TheObject_ID FROM #tables parent INNER JOIN sys.foreign_Keys ON sys.foreign_Keys.parent_Object_ID = parent.TheObject_ID INNER JOIN #tables referenced ON sys.foreign_Keys.referenced_Object_ID = referenced.TheObject_ID AND sys.foreign_Keys.referenced_Object_ID <> parent.TheObject_ID WHERE parent.TheOrder = 0 --i.e. it hasn't been ordered yet GROUP BY parent.TheObject_ID HAVING --where all its referenced tables have been ordered SUM (CASE WHEN referenced.TheOrder = 0 THEN-20000 ELSE referenced.TheOrder END) > 0 ); SET @Rowcount = @@Rowcount; SET @ii = @ii + 1; IF @ii > 100 BREAK; END; SELECT TheObject_ID, TheSchema+'.'+TheName, TheOrder FROM #tables ORDER BY TheOrder; IF @ii > 100 --not a directed acyclic graph (DAG). RAISERROR('Cannot load in tables with mutual references in foreign keys', 16, 1 ); IF EXISTS (SELECT * FROM #tables WHERE TheOrder = 0) RAISERROR('could not do the topological sort', 16, 1); DROP table #tables
In the next level, we’ll deal with the problems of writing database CREATE and ALTER scripts that have to work appropriately whatever the state of the database. We will be describing many system views and Information_Schema Views that are needed to write DDL code in such a way that it works error-free without assumptions as to whether the objects that already exist, doesn’t get executed twice, or executed in the wrong circumstances.