Sql-server – Determining whether column in view can only contain unique values

sql serverview

I have a SQL Server 2008 database with several views. I'm looking for a way to definitively determine whether a particular view has a column that can only contain unique values (i.e. can be used as a primary key).

Some of these views have simple definitions, but some are also quite complex, so I find that manually analysing each of these views would be very time-consuming.

As well, some of these views have columns that concatenate values from other columns in the view in order to be "unique", but I can't be certain whether these values are actually unique because in some cases I have found that they are not.

One simple approach I had was to use the following query to determine if any duplicate values exist for a particular column in a view:

SELECT   column_name
FROM     view_name
GROUP BY column_name
HAVING   COUNT(column_name) > 1

The above query works fine when there is enough data in the view to catch any non-unique values, but this still isn't definitive.

Is there a way to have SQL Server perform this analysis to determine whether a column in a view can only contain unique values?

Best Answer

Short answer: you'll have to do your own dirty work. Like @mrdenny says, there is no way of automating this task.

Very long answer: SQL Server doesn't have an easy way of determining unique columns in a view, in part because of how complex (and dynamic) views can be. There are two methods to make a first approximation, however, and these methods can work in concert. The first is what you already have: performing queries against the view to see if you can find anything which already violates uniqueness constraints. If it does, you can throw that column (or set of columns) out immediately. If it doesn't, that combination might be a valid unique key, but there is no guarantee.

The second method is to reason from the data model, starting with unique constraints (assuming you have those on your tables!). There are three nice ways that you can have enforced uniqueness on a table: a primary key, a unique key constraint, or a unique index. All three of them show up in the sys.indexes system table and have the is_unique property set to 1. There are also some not-so-nice ways like using triggers to enforce uniqueness.

You could try messing around with sys.dm_sql_referenced_entities and a query like this might be helpful to give you a starting point:

declare @ViewName sysname = 'MYVIEW';

with viewcolumns as
(
    select * from sys.dm_sql_referenced_entities(@ViewName, 'OBJECT') dsre /* where is_selected = 1   --uncomment if using SQL 2012 */
),
uniquereferences as
(
    select
        i.object_id,
        object_schema_name(i.object_id) as SchemaName,
        object_name(i.object_id) as TableName,
        i.name as IndexName,
        c.name as ColumnName,
        case when vc.referenced_entity_name IS NOT NULL then 1 else 0 end as HasReference
    from
        sys.indexes i
        inner join sys.index_columns ic 
            on i.index_id = ic.index_id
            and i.object_id = ic.object_id
        inner join sys.columns c
            on c.column_id = ic.column_id
            and c.object_id = ic.object_id
        left outer join viewcolumns vc
            on vc.referenced_id = c.object_id
            and vc.referenced_minor_id = c.column_id
    where
        i.is_unique = 1
),
sufficientreferences as
(
    select
        object_id,
        IndexName,
        min(HasReference) as HasReference
    from
        uniquereferences
    group by
        object_id,
        IndexName
    having
        min(HasReference) = 1
)
select
    ur.object_id,
    ur.SchemaName,
    ur.TableName,
    ur.IndexName,
    ur.ColumnName
from
    uniquereferences ur
    inner join sufficientreferences sr 
        on ur.IndexName = sr.IndexName
        and ur.object_id = sr.object_id
where
    ur.HasReference = 1
    /* Optional:  remove referenced tables; if you have a 1:1 reference, leave this bit out */
    and not exists
    (
        select
            *
        from
            sys.foreign_keys fk
            inner join viewcolumns vc 
                on fk.parent_object_id = vc.referenced_id
        where
            vc.referenced_minor_id = 0
            and fk.referenced_object_id = ur.object_id

    );

For SQL 2008, the is_selected flag is not available, so there's no way to tell if the column returned by that function is actually part of the SELECT clause or if it is is used in a join or filter. With SQL 2012, you could at least limit your query to the columns that actually are part of the SELECT clause.

What you get from this is not a set of unique keys for the view. What you get is a set of columns which make up unique keys on their underlying tables. The difference is that you could have a reference table with a unique key constraint on the Name column, and that Name column would show up in the above query even if the view joins the reference table to the base table (thereby causing repeated use of the reference table's Name column). To help alleviate that, I have a NOT EXISTS clause which removes cases in which the object is the referenced table in a foreign key relationship with another table in the view, so our unique index for the reference data table should not show up.

What this does allow you to do is reduce your possible answer space. But even then, you'll be doing a lot of spadework. The more complex your views get, the less valuable this is. For example, if you have a UNION ALL in your query, the statement above might show you a candidate column set which is wrong, because those columns might be duplicated in the other half of the UNION ALL. Or if you have cross-server queries, sys.dm_sql_referenced_entities might not even show you any column names. In other words, the query above is a semi-functional aid and certainly not a method of automating the process.

Note that this does depend upon having unique constraints specified. If your only unique keys are surrogate primary keys, it might not be quite as easy to find a candidate column set because not even SQL Server knows that the column combination is supposed to be unique.