ENGINEERING NOTE / 01 SQL SERVERAUTOMATION

The Lookups I Stopped Skipping

SSMS will run a stored procedure on a keystroke and hand it whatever you have highlighted. What you can pass it is narrower than it looks, and that constraint decided the design.

FROM THE ARCHIVE ORIGINALLY PUBLISHED UPDATED 12 MIN READ

SQL-SERVERSSMSTOOLINGT-SQLPRODUCTIVITY

TABLE OF CONTENTS
  1. What SSMS actually gives you
  2. One procedure behind every key
  3. Every shortcut shows its work
  4. The characters that don’t survive the paste
  5. What ended up on the keys
  6. The type parameter
  7. What it installs, and whose code it is
  8. What I would change today
  9. Key takeaways

There is a category of lookup you skip because it costs more than the answer is worth. What columns does this table actually have. Is this index being read or only maintained. Where else in the database does that column name appear. None of these are hard questions — they are four clicks through Object Explorer, or a query you have to stop and remember, and in the middle of looking at something else you decide you probably already know.

You usually do already know. The problem is the times you don’t, and you carried on anyway.

SHORTCUT KEYS USED
8
PROCEDURES IT NEEDS
3
CLICKS TO RUN ONE
0

So this is a tooling note rather than an incident: sp_QueryShortcuts. Its change log starts in March 2014 and I only wrote it up three years later, which is the right order — it earned the write-up by still being on my keyboard after three years, not by being a good idea.

The interesting part is not the shortcut list. It is that SSMS gives you a very narrow hook, and almost every design decision here falls out of how narrow it is.

What SSMS actually gives you§

Under Tools → Options → Environment → Keyboard → Query Shortcuts, SSMS lets you bind a string of T-SQL to a key. Press the key and it runs that string.

The SSMS Query Shortcuts options page, with Alt+F1 bound to sp_help, Ctrl+1 to sp_who, Ctrl+2 to sp_lock, and Ctrl+3 through Ctrl+0 each bound to EXEC sp_QueryShortcuts followed by their own number and a trailing comma.
Twelve slots. Three arrive bound, one stays empty, and eight carry the same procedure with a different number.

The part that makes it useful is what happens when you have text selected in the editor: SSMS appends the selection to the bound string and executes the result. Bind Ctrl+3 to this —

EXEC sp_QueryShortcuts 3, 

— highlight dbo.Orders, press Ctrl+3, and the batch that reaches the server is:

EXEC sp_QueryShortcuts 3, dbo.Orders

That is the whole mechanism. It is also the whole constraint.

A screen capture in SSMS: the word Orders is highlighted in the query editor, Ctrl+3 is pressed, and a grid of rows from the Orders table appears, with the Messages tab showing the SELECT TOP 1000 statement that was generated and run.
Highlight, press, read. The Messages tab shows the statement it built — every shortcut prints what it ran.
  1. 01 SELECT

    Highlight an object name, a column name, or a fragment of text in the query editor.

  2. 02 COMPOSE

    SSMS concatenates the bound string and the selection. No escaping, no quoting — plain concatenation.

  3. 03 PARSE

    SQL Server parses the result as one batch. Anything the selection breaks, it breaks here, before any code of mine runs.

  4. 04 DISPATCH

    sp_QueryShortcuts receives the shortcut number and the text, and decides what to do with it.

  5. 05 RESULT OBSERVED

    One or two result sets, in the same window, without leaving the keyboard.

Note the trailing comma in the bound string, and note that it is load-bearing in both directions. It is what lets the selection arrive as a second parameter — and it is also why pressing the key with nothing highlighted is a syntax error rather than a no-op. That is a fair trade, but it is worth knowing before you wonder why an empty selection shouts at you.

One procedure behind every key§

SSMS offers twelve shortcut slots. Three arrive pre-bound — Ctrl+1 to sp_who, Ctrl+2 to sp_lock, Alt+F1 to sp_help — and those are worth leaving alone. Of the nine that remain, eight are Ctrl+3 through Ctrl+0: one hand, one row, no reaching. That is the whole reason there are eight shortcuts rather than nine. Ctrl+F1 is free too, and I never once reached for it.

Eight keys, then, and the obvious design is eight small procedures, one per key.

The entry point:

ALTER PROCEDURE [dbo].[sp_QueryShortcuts]
    (@Action INT, @Object NVARCHAR(1000), @AddParam NVARCHAR(1000) = NULL)
WITH RECOMPILE
AS
BEGIN

SET TRAN ISOLATION LEVEL READ UNCOMMITTED
SET NOCOUNT ON

Two lines there are worth more than they look. READ UNCOMMITTED is because a tool you press without thinking must never be the thing that blocks the server you are trying to look at. WITH RECOMPILE is because every branch below builds a different statement, so a cached plan for one shortcut is worthless to the next.

Installing it in master with the sp_ prefix is what makes it work from any database context without qualification — the same special case that makes sp_who and sp_help resolve everywhere. That went in three years after the first version, and the change log records what it bought: “Removed @DBName from parametres since it’s not needed after using sp_ prefix.” One naming convention deleted a parameter from every call.

Every shortcut shows its work§

The last two lines of the procedure, after every branch has built its statement:

  PRINT @SQL
  EXEC sp_executesql @SQL WITH RECOMPILE

That PRINT is the best decision in the whole thing, and it cost one line. Every shortcut leaves the statement it generated in the Messages tab, so the tool is never a black box: when a result looks wrong you can read exactly what was asked, and when a result looks interesting you can lift the query out and keep working on it.

A shortcut that hides its query saves you typing once. A shortcut that shows its query teaches you the catalogue views, and then you do not need it.

The characters that don’t survive the paste§

Now the constraint. SSMS concatenates your selection into a batch with no quoting of any kind, so the selection has to be something SQL Server will accept in an EXEC argument list. A bare object name is fine. A pattern is not.

Highlight %Record% and the batch becomes EXEC sp_QueryShortcuts 5, %Record%, which fails to parse. My procedure never runs, so it cannot help. Whatever the fix was, it had to happen in the text the user types.

The answer was two stand-in characters. The setup script says why in one sentence: “This was added so that when passing object names you don’t have to wrap them in single quotes for the shortcut to work.”

You typeIt meansExampleBecomes
#% — wildcardab#xyzab%xyz
@. — schema separatorhr@Usershr.Users

They are not the same mechanism, which took me a while to appreciate about my own code. # is a substitution, done on the way in and applied to both parameters:

SET @Object   = REPLACE(@Object,   '#', '%');
SET @AddParam = REPLACE(@AddParam, '#', '%');

@ is not substituted at all. It is a second separator the name parser accepts alongside ., so the two-part name never has to reach the batch in a form the batch would reject:

IF CHARINDEX('.', @_Object_) > 0
  BEGIN  -- split schema from object on a dot
  END;
IF CHARINDEX('@', @_Object_) > 0
  BEGIN  -- ...or on an at sign
  END;

Both forms work when you call the procedure by hand. Only the second survives being pasted in unquoted by SSMS.

A screen capture in SSMS: Sales@Orders is highlighted, Ctrl+3 is pressed, rows appear, and the Messages tab shows the generated statement selecting from the three-part name WideWorldImporters.Sales.Orders.
Sales@Orders reaches the batch intact, and comes back as [WideWorldImporters].[Sales].[Orders].

And you do not have to supply the schema at all. When the highlighted text has no separator, the procedure looks the schema up:

SELECT TOP 1 @_Schema_ = S.name
FROM   sys.objects AS O
JOIN   sys.schemas AS S ON S.schema_id = O.schema_id
WHERE  O.name = @_Object_
ORDER  BY O.schema_id ASC

Lowest schema_id wins, and it falls back to dbo. That is a guess, and it is the right kind: it is correct almost always, it is cheap when it is wrong — you get the other table’s rows and immediately see it — and it means the common case is one word.

The search shortcuts wrap what you give them in % at both ends, so # only has to supply the wildcards in the middle:

WHERE o.Name LIKE '%' + @_Object_ + '%'

Finding every object whose name matches %Record%Temperature% is therefore selecting Record#Temperature and pressing Ctrl+5:

A screen capture in SSMS: Record#Temperature is highlighted, Ctrl+5 is pressed, and a result grid lists two stored procedures, RecordColdRoomTemperatures and RecordVehicleTemperature, with their type, schema and creation date.
Record#Temperature becomes LIKE '%Record%Temperature%'. Two procedures, named from memory, found without knowing either name.

What ended up on the keys§

Eight keys, chosen by how often I found myself wanting the answer rather than by what was interesting to build:

KeyWhat it does
Ctrl+3SELECT TOP 1000 from the highlighted table or view, ordered by the first column descending
Ctrl+4sp_help for a user table, sp_helptext2 for everything else
Ctrl+5Find database objects whose name matches the highlighted text
Ctrl+6sp_WhoIsActive — S for defaults, P to include plans, D for detail
Ctrl+7Index usage and calculated index performance for a table, plus missing indexes and their estimated benefit
Ctrl+8Find columns or parameters by name, and return the objects that contain them
Ctrl+9List an object’s columns or parameters — and emit them as a parameter list to paste into a procedure
Ctrl+0Search inside the definitions of programmability objects. Multi-word text goes in square brackets: [INSERT INTO Users]

Ctrl+3 takes a second argument: a column name to order by ascending, that same column name with # appended to order descending, or a value, which becomes a WHERE clause against the first column.

Two of these earn their place for reasons worth separating out.

Ctrl+9 emitting a paste-ready parameter list is the only one that writes code rather than answering a question, and it is the one I used most. Turning a table’s column list into a procedure’s parameter list is pure transcription — exactly the kind of task where a human introduces a typo and a machine does not.

Ctrl+7 returning index usage and missing indexes as two result sets is the one that changed behaviour. Those two questions are always asked together, and the answer to either one alone is misleading — a missing-index suggestion means something different when the existing indexes on the table are already unused.

They compose, which is the part that does not come across in a list:

A longer screen capture in SSMS: Ctrl+3 on a table returns its rows; Ctrl+5 with a type argument finds the stored procedures whose names contain that table's name; Ctrl+4 on one of those procedures scripts out its full definition — three lookups without leaving the editor.
Rows, then the procedures that touch the table, then one of those procedures scripted out. Three questions, three keystrokes, one window.

The type parameter§

Several of the search shortcuts take a SQL Server object type as a second argument, so Ctrl+5 can look for procedures without also finding the tables they read.

The durable way to get that list is to ask the instance you are actually on, since the set grows between versions:

SELECT DISTINCT type, type_desc
FROM   sys.objects
ORDER  BY type;
A screen capture in SSMS: the text Orders, U is highlighted, Ctrl+5 is pressed, and the results are narrowed to two user tables named Orders and PurchaseOrders, showing their schema, object id and creation date.
Orders, U — the text, then the type. Only user tables come back.

The list as it stood when I wrote this:

TypeDescription
AFAggregate function (CLR)
CCHECK constraint
DDEFAULT (constraint or stand-alone)
FFOREIGN KEY constraint
FNSQL scalar function
FSAssembly (CLR) scalar function
FTAssembly (CLR) table-valued function
IFSQL inline table-valued function
ITInternal table
PSQL stored procedure
PCAssembly (CLR) stored procedure
PGPlan guide
PKPRIMARY KEY constraint
RRule (old-style, stand-alone)
RFReplication filter procedure
SSystem base table
SNSynonym
SOSequence object
SQService queue
TAAssembly (CLR) DML trigger
TFSQL table-valued function
TRSQL DML trigger
TTTable type
UTable (user-defined)
UQUNIQUE constraint
VView
XExtended stored procedure

What it installs, and whose code it is§

The 2017 setup script created three procedures in master, and only one of them was mine:

  • sp_WhoIsActive by Adam Machanic — the standard answer to “what is running right now”, and what sits behind Ctrl+6. The copy frozen into my script was v11.17, from October 2016.
  • sp_helptext2 by Ranga Babu — what Ctrl+4 calls for anything that is not a user table. It exists because the built-in sp_helptext splits lines at 256 characters, which mangles exactly the wide, deeply indented code you most want to read.
  • sp_QueryShortcuts — the dispatcher above.

Two thirds of what that installed was other people’s work, and the shortcuts are worth more because of it. Ctrl+6 is a keystroke wrapped around sp_WhoIsActive; the value is Adam Machanic’s, and the shortcut only removes the typing.

Which is why the script you can download here is not the script I ran.

So: sp_QueryShortcuts.sql, with the original header and change log intact, and a preamble naming both prerequisites and where to get them. Install them into master alongside it and all eight keys work. Install neither and six still do — Ctrl+6 fails outright, and Ctrl+4 keeps working for user tables, because that branch calls the built-in sp_help.

It works out better than the bundle did. The change log’s first entry is from March 2014, and the sp_WhoIsActive frozen into the script is from October 2016 — pinned by whichever afternoon I last copied it in, rather than by any decision. Naming a dependency instead of copying it means you get the current one, and in this case it also means you get one under a licence that has since changed.

What I would change today§

Stop vendoring other people’s procedures. I framed this as a maintenance argument when I first wrote it down — a pinned copy goes stale, and staleness is invisible. Then I read the licence header I had been carrying around for years. That v11.17 of sp_WhoIsActive was free to use and explicitly not free to redistribute, and a setup script that installs it for you is redistribution whatever the intent. sp_helptext2 turned out no better: published on a site whose terms require written permission to reproduce, and derived from a Microsoft system procedure besides.

The split above is what I should have shipped in the first place, and staleness is the smaller of the two reasons. Worth saying plainly, because it generalises past this script: a convenience installer that vendors somebody else’s code is a licensing decision, and it is very easy to make without noticing you have made one.

Quote the identifiers. The generated statements interpolate the object name into strings, with [ and ] stripped but no QUOTENAME around the result — so a name with a quote in it produces a batch nobody meant to write. In normal use the threat model is thin: the text comes from your own editor, running as you, on a server you are already connected to, so the worst case is injecting into yourself. It stops being thin the moment anything else calls this procedure, and that is exactly the kind of assumption that is true right up until someone finds the tool useful.

Stop overloading #. It means “wildcard” everywhere except at the end of Ctrl+3’s order-by argument, where it means “descending”. Two meanings for one character, distinguished only by position, in an interface whose whole premise is that you are typing fast without thinking about it. A separate character, or ordering by a signed column position, would both have been better than the shortcut I took.

Fail politely on an empty selection. Pressing a key with nothing highlighted gives you a parser error about a comma, which tells you nothing. A default of NULL on the text parameter and one printed line of help would turn the most common mistake into the discoverability feature the tool never had.

Reconsider the whole hook. This is built on a feature of one client. SSMS is the only tool I know of that will hand a stored procedure your text selection on a keystroke — the newer editors people write T-SQL in do not expose an equivalent. That is not a reason to have avoided it in 2017, when SSMS was where the work happened. It is a reason to know that the pattern lives and dies with the client, and that the eight questions it answers would need a different home in a different editor.

Key takeaways§

The cost of a lookup decides whether you do it. Not its difficulty — none of these queries are difficult. When checking takes four clicks and a context switch, you check when you are suspicious. When it takes one chord, you check when you are curious, and the difference between those two habits is most of what a tool like this is for.

Design to the narrowest part of the interface. The @ and # substitutions look like an odd affectation until you know that the selection is concatenated into a batch unquoted. Everything downstream was free to be sensible; that one joint was not, and it set the shape of the whole thing.

Pair the questions that are only meaningful together. Index usage and missing indexes in one keystroke is worth more than either alone, because neither answer means what it appears to mean without the other. That is a better use of a scarce key than one more thing that could have been a query.

A shortcut around someone else’s tool is still mostly their tool. The keystroke is mine; sp_WhoIsActive is Adam Machanic’s, and it is doing the work.