68

I have a very large database with hundreds of tables, and after many, many product upgrades, I'm sure half of them aren't being used anymore. How can I tell if a table is is actively being selected from? I can't just use Profiler - not only do I want to watch for more than a few days, but there are thousands of stored procedures as well, and profiler won't translate the SP calls into table access calls.

The only thing I can think of is to create a clustered index on the tables of interest, and then monitor the sys.dm_db_index_usage_stats to see if there are any seeks or scans on the clustered index, meaning that data from the table was loaded. However, adding a clustered index on every table is a bad idea (for any number of reasons), as isn't really feasible.

Are there other options I have? I've always wanted a feature like a "SELECT trigger", but there are probably other reasons why SQL Server doesn't have that feature either.

SOLUTION:

Thanks, Remus, for pointing me in the right direction. Using those columns, I've created the following SELECT, which does exactly what I want.

  WITH LastActivity (ObjectID, LastAction) AS 
  (
       SELECT object_id AS TableName,
              last_user_seek as LastAction
         FROM sys.dm_db_index_usage_stats u
        WHERE database_id = db_id(db_name())
        UNION 
       SELECT object_id AS TableName,
              last_user_scan as LastAction
         FROM sys.dm_db_index_usage_stats u
        WHERE database_id = db_id(db_name())
        UNION
       SELECT object_id AS TableName,
              last_user_lookup as LastAction
         FROM sys.dm_db_index_usage_stats u
        WHERE database_id = db_id(db_name())
  )
  SELECT OBJECT_NAME(so.object_id) AS TableName,
         MAX(la.LastAction) as LastSelect
    FROM sys.objects so
    LEFT
    JOIN LastActivity la
      on so.object_id = la.ObjectID
   WHERE so.type = 'U'
     AND so.object_id > 100
GROUP BY OBJECT_NAME(so.object_id)
ORDER BY OBJECT_NAME(so.object_id)
SqlRyan
  • 33,116
  • 33
  • 114
  • 199
  • Is there a way to capture this data into a table when the server shuts down? This would allow long term tracking. – AMissico Jul 25 '13 at 23:23
  • 3
    Great question. Just for anyone else looking at this, if you are using 2008, you could replace the queries inside the CTE with the following; `SELECT object_id AS TableName, (SELECT Max(v) FROM (VALUES (last_user_seek), (last_user_scan), (last_user_lookup)) AS value(v)) as [LastAction] FROM sys.dm_db_index_usage_stats u WHERE database_id = db_id(db_name())` Example to from [here](http://stackoverflow.com/a/6871572/685760) – Mr Moose Sep 03 '15 at 07:15

6 Answers6

52

Look in sys.dm_db_index_usage_stats. The columns last_user_xxx will contain the last time the table was accessed from user requests. This table resets its tracking after a server restart, so you must leave it running for a while before relying on its data.

Remus Rusanu
  • 288,378
  • 40
  • 442
  • 569
  • 3
    Wait - is this as awesome as I think it is? I'll do some testing, but it appears that I can monitor the "HEAP" virtual index on the table for any seeks/scans/lookups, and that tells me exactly what I want to know. You, sir, are my hero if this is the case. – SqlRyan Jan 28 '10 at 17:39
  • 3
    It does what you think it does and more. You can differentiate between tables that are not used (0 heap or 1 clustered index) and non-clustered indexes never used so you can discard unnecessary non-clustered indexes. Just remeber that it resets when the server restarts (actually when a database comes online it resets for that db, details...) – Remus Rusanu Jan 28 '10 at 17:51
  • Outstanding. I'll update the question with the magic query I developed using your tip, but it's spectacular. – SqlRyan Jan 28 '10 at 18:11
  • 5
    Just remember that no usage for a while may not mean no usage. Annual reports may only be run once a year. Tables to support some of these infrequent tasks may not see a lot of action (or any) the rest of the time. – HLGEM Jan 28 '10 at 18:45
  • Good point. It turns out that the database wasn't nearly as "ununsed" as I expected, and since our server has been up since October, we have a pretty solid snapshot of activity, including through year end. Thanks again for all the help! – SqlRyan Jan 28 '10 at 19:34
  • Is there a way to capture this data into a table when the server shuts down? This would allow long term tracking. – AMissico Jul 25 '13 at 23:24
6

Re: Profiler, if you monitor for SP:StmtCompleted, that will capture all statements executing within a stored procedure, so that will catch table accesses within a sproc. If not everything goes through stored procedures, you may also need the SQL:StmtCompleted event.

There will be a large number of events so it's probably still not practical to trace over a long time due to the size of trace. However, you could apply a filter - e.g. where TextData contains the name of your table you want to check for. You could give a list of table names to filter on at any one time and work through them gradually. So you should not get any trace events if none of those tables have been accessed.

Even if you feel it's not a suitable/viable approach for you, I thought it was worth expanding on.

Another solution would be to do a global search of your source code to find references to the tables. You can query the stored procedure definitions to check for matches for a given table, or just generate a complete database script and do a Find on that for table names.

AdaTheDev
  • 142,592
  • 28
  • 206
  • 200
  • 4
    +1 for source code search. Some applications have very infrequently-used features that might never be caught by profiling/auditing. The only way to be sure is to actually check the programs that use the database! – Aaronaught Jan 28 '10 at 15:50
  • Yep. I would trust a source code search a bit more (as long as you check ALL source code!) as that doesn't involve monitoring for actual hits on the table which could potentially be rare/require a long time to flag up for (how long do you monitor for?!). – AdaTheDev Jan 28 '10 at 15:56
  • The application that goes against this database does enough ad-hoc SQL that a stored proc code search wouldn't catch everything - the procs are mostly there for reporting and scheduled stuff. I like this time, though - I never realized the difference between the SP and SQL versions of "StmtCompleted" – SqlRyan Jan 28 '10 at 16:01
  • @rwmnau: If it uses a lot of ad-hoc SQL then it should be even easier to find table references in the source code, no? – Aaronaught Jan 28 '10 at 16:13
  • Yes and no - we have the code for the SPs and reports, but the application is proprietary, and I don't have access to the source code. We've had it long enough that we know pretty well how it works, but I can't check the code to know for sure. My point was that I can't just do a syscomments search on the SP code to find table names, since the app does plenty of ad hoc that wouldn't show up in an SP. – SqlRyan Jan 28 '10 at 16:37
3

For SQL Server 2008 you should take a look at SQL Auditing. This allows you to audit many things including selects on a table and reports to a file or Events Log.

dr.
  • 1,429
  • 12
  • 18
1

The following query uses the query plan cache to see if there's a reference to a table in any of the existing plans in cache. This is not guaranteed to be 100% accurate (since query plans are flushed out if there are memory constraints) but can be used to get some insights on table use.

SELECT schema_name(schema_id) as schemaName, t.name as tableName,
    databases.name,
dm_exec_sql_text.text AS TSQL_Text,
dm_exec_query_stats.creation_time, 
dm_exec_query_stats.execution_count,
dm_exec_query_stats.total_worker_time AS total_cpu_time,
dm_exec_query_stats.total_elapsed_time, 
dm_exec_query_stats.total_logical_reads, 
dm_exec_query_stats.total_physical_reads, 
dm_exec_query_plan.query_plan
FROM sys.dm_exec_query_stats 
CROSS APPLY sys.dm_exec_sql_text(dm_exec_query_stats.plan_handle)
CROSS APPLY sys.dm_exec_query_plan(dm_exec_query_stats.plan_handle)
INNER JOIN sys.databases ON dm_exec_sql_text.dbid = databases.database_id
RIGHT JOIN sys.tables t (NOLOCK) ON cast(dm_exec_query_plan.query_plan as varchar(max)) like '%' + t.name + '%'
Dharmendar Kumar 'DK'
  • 2,082
  • 17
  • 18
0

I had in mind to play with user permissions for different tables, but then I remembered you can turn on trace with an ON LOGON trigger you might benefit from this:

CREATE OR REPLACE TRIGGER SYS.ON_LOGON_ALL

AFTER LOGON ON DATABASE
WHEN (

USER 'MAX'

)
BEGIN

EXECUTE IMMEDIATE 'ALTER SESSION SET SQL_TRACE TRUE';

--EXECUTE IMMEDIATE 'alter session set events ''10046 trace name context forever level 12''';

EXCEPTION

WHEN OTHERS THEN

NULL;

END;

/

Then you can check your trace files.

Pentium10
  • 204,586
  • 122
  • 423
  • 502
-1

This solution works better for me then the solution above. But, is still limted that the server was not re-started as well, but still gives you a good idea of tables not used.

SELECT [name]
      ,[object_id]
      ,[principal_id]
      ,[schema_id]
      ,[parent_object_id]
      ,[type]
      ,[type_desc]
      ,[create_date]
      ,[modify_date]
      ,[is_ms_shipped]
      ,[is_published]
      ,[is_schema_published]
  FROM [COMTrans].[sys].[all_objects]
  where object_id not in (
select object_id from sys.dm_db_index_usage_stats

)
and type='U'
order by name
codefreak
  • 3
  • 2