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.
I'd say the easiest possible way to do this is through PowerShell and SMO. Take the following code for instance:
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") |
Out-Null
$SqlServerNames = "Server1", "Server2"
$SqlJobName = "YourJob"
foreach ($SqlServerName in $SqlServerNames) {
$SqlServer = New-Object Microsoft.SqlServer.Management.Smo.Server($SqlServerName)
$SqlServer.JobServer.Jobs[$SqlJobName].Start()
}
All this does it loop through a list of servers (explicitly stated at as string array variable, but can easily be obtained also from a SQL Server database table, a text file, etc.), connects to the current server, and then starts the job with the job named whatever you set the $SqlJobName
variable to.
It's also worth noting that the Start()
function will not wait for the job to complete before continuing code execution.
As you can see, in very few lines of PowerShell code you are able to accomplish this task. But I'd take it a step further, such as error handling. You don't want this to bomb out and not know what failed, when it failed, and what servers it did/didn't run on.
foreach ($SqlServerName in $SqlServerNames) {
try {
$SqlServer = New-Object Microsoft.SqlServer.Management.Smo.Server($SqlServerName)
$SqlServer.JobServer.Jobs[$SqlJobName].Start()
}
catch {
# add your error handling code here
## for instance, write the error to a text file
}
}
And, as always with any code, test this out in a non-production environment to ensure that it does what you think it will and should do. Re-work it for your needs/environment.
As per RoKa's comment:
Can this be edited to check for whether the job is currently running, error handling?
Great point, and definitely something to check for (as well as if the job actually exists by checking for a non-null value):
foreach ($SqlServerName in $SqlServerNames) {
try {
$SqlServer = New-Object Microsoft.SqlServer.Management.Smo.Server($SqlServerName)
$SqlJob = $SqlServer.JobServer.Jobs[$SqlJobName]
if (($SqlJob) -and ($SqlJob.CurrentRunStatus -eq [Microsoft.SqlServer.Management.Smo.Agent.JobExecutionStatus]::Idle)) {
$SqlJob.Start()
}
else {
# handle/log accordingly if the job doesn't exist or it's not idle
}
}
catch {
# add your error handling code here
## for instance, write the error to a text file
}
}
EDIT: While re-reading your question, I see SQL Server 2000. Are those going to be the target instances? If that is the case, and you do decide to go with the PowerShell/SMO answer I provided, then definitely definitely definitely test and ensure it is going to do what you want. Test this out very far from production and be 100% sure it does what you think it will.
EDIT: It looks like SMO versions before SQL Server 2012 (i.e. 2005, 2008, and 2008 R2) support managing SQL Server 2000 instances. I'm finding this information through this BOL reference.
Best Answer
A good start would be to look at the exec_query_stats view. You can get the execution count and total elapsed time and a lot more, but this is only from queries that have been cached.
You might want to issue a CHECKPOINT and DBCC DROPCLEANBUFFERS after you've installed your new views to clear the cache.