To view a query plan, you need SHOWPLAN permission on the database level at least. You have this if you have CONTROL DATABASE, or CONTROL SERVER, or if you have ALTER TRACE at the instance level. I know this last one because it's mentioned in Books Online on the 'Database Permissions' page, not because it's particularly intuitive.
As a consultant, I sometimes deal with customers who are reluctant to grant me the kind of access level that I would like to have to work on queries. SHOWPLAN is something that I will almost always request, though, and generally it's considered harmless enough. I want to be able to show plans, so SHOWPLAN is part of what I like to have when writing any kind of query. Actually, I often find myself requesting ALTER TRACE, because it covers SHOWPLAN across all databases. Without it, you can find yourself in a situation where you sometimes get this error
, because a view, function, or stored procedure accesses a database that you haven't been granted access to. Maybe it contains sensitive information – maybe you don't have security clearance, for example, but there is a table in that database that is referenced for part of of process you need to look into. I'm not going to get into the why, or the reasons why you could request better access, or anything like that – that's not the point of this post. The point of this post is to talk about something which I learned about SHOWPLAN across databases that aren't actually used in query. And it's part of this month's T-SQL Tuesday, hosted by Mike Donnelly (@SQLMD).
I was thinking about this situation though – having cross-database queries and not having SHOWPLAN on all of the referenced databases – and about the fact that views often contain more information than you necessarily need. This got me back to my Redundant Joins material (which I should reblog about, as I haven't written about it properly on sqlblog.com), and that the Query Optimizer can simplify out joins which aren't actually used at all.
Something occurred to me which I didn't know the answer to, so I did a bit of research, found the answer, making it something I wanted to write about for this T-SQL Tuesday about new things learned.
Imagine a view, a table-valued function, a sub-query, just some table expression, which references (joins to) a lookup table but doesn't need to. If we're not interested in the data in the lookup table, this join is only needed if it's matching multiple rows, or being used as a filter (which can't happen if it's a left outer join), or if it's a right outer or full outer join (and therefore wanting to return all the rows in the lookup table, even those not mentioned in the left-hand set). If it's not used, it's redundant, and won't be part of the query plan.
Annoyingly, the simplification phase, when redundant joins are removed, is done AFTER permissions are checked. This is easy to demonstrate. Let's consider a user which has VIEW DEFINITION rights on a table, but not SELECT rights. This user can run sp_help, and see all the metadata associated with the table. This user can query sys.columns and see the rows there, one for each column in the table. But to run the query SELECT TOP (0) * FROM dbo.someTable; , which is purely metadata, permission is denied.
The reason I know it's only metadata is because running as a more-privileged user, the query plan shows me this (as shown here, using AdventureWorks2012.Production.Production instead of dbo.soimeTable).
This query does not select data from the table. If it did, we'd see a Seek or a Scan here. This query never needs to access the table. It is explicitly told to fetch no rows from it. The only thing we use here is metadata, and we do have permission to get that.
And yet the less-privileged user can't run this query. Metadata isn't a problem, but the permissions are tested first, and the query is rejected.
Permissions are checked once the query has been parsed. If an object is used in the query, then SELECT permission is required. If an object is updated, then UPDATE permission is needed, even if it's logically impossible to update any actual rows (try WHERE 1=2 if you need to check).
Now once a plan is in cache, VIEW SERVER STATE is needed to be able to view it. And if you have VIEW SERVER STATE, then you can view the plans that are in cache, even if you don't have permissions to run the queries.
…which brings me to SHOWPLAN.
SHOWPLAN is different to VIEW SERVER STATE – it doesn't apply to the plan cache. The plan cache is an instance-level thing, and a database permission like SHOWPLAN isn't going to cut it.
To view the plan of a query that's not in cache, you need SHOWPLAN permission. And you need to be able to run the query – even if the query isn't actually going to touch the tables. I wouldn't mind being able to look at plans to offer tuning advice without having to have permission to run the query, but this is just one of those things.
Sadly, it extends to databases. If a database is referenced by a query, even if it's not used, then you need to have SHOWPLAN permission on that database (or ALTER TRACE at the instance level, as I mentioned earlier).
So if a view references a database for a lookup, and your query uses that database, you won't be able to see the query plan of any query that uses it. You can have SHOWPLAN permission in the database where your data is, and with another user, you could verify that your plan doesn't even touch the other database. But if it mentions it at all, you need SHOWPLAN on that database.
The script below will let you reproduce this if you want.
create login test with password ='test' go create database test go use test go create user test for login test alter role db_owner add member test go create table dbo.test (test int); go grant showplan to test go use AdventureWorks2012 go create user test for login test grant select on Production.Product to test deny showplan to test go use test go execute as login = 'test' go select t.* from dbo.test t left join (select top (1) 1 from AdventureWorks2012.Production.Product) t2(c) on t.test = t2.c go revert go execute as login = 'test' go set showplan_xml on go select t.* from dbo.test t left join (select top (1) 1 from AdventureWorks2012.Production.Product) t2(c) on t.test = t2.c go set showplan_xml off go revert go --Original user: set showplan_xml on go select t.* from dbo.test t left join (select top (1) 1 from AdventureWorks2012.Production.Product) t2(c) on t.test = t2.c go set showplan_xml off go ----Cleanup --use AdventureWorks2012 --go --drop user test --drop database test --drop login test