Applies to SQL Server 2019, 2022 and 2025.
T-SQL reserved keywords are the words SQL Server needs for its own grammar: SELECT, ORDER, KEY, USER, PERCENT and about 180 others. You can still use one as a table or column name, but only if you delimit it every time you refer to it. I try not to, and when I inherit a database that does, I bracket everything and move on.
What makes a word one of the T-SQL reserved keywords
The parser reads a statement left to right, and a reserved word tells it what kind of clause comes next. If it meets ORDER where it expects a column name, it assumes you’ve started an ORDER BY and stops with “Incorrect syntax near the keyword”. It doesn’t matter that you meant a column.
Microsoft keeps three lists on the same page:
- Transact-SQL reserved keywords. These are the ones that break things today. The list hasn’t changed across 2019, 2022 and 2025.
- ODBC reserved keywords. The same as the ISO standard list. SQL Server doesn’t enforce them, but some drivers and tools may expect you to avoid them.
- Future keywords. Words such as ROLE, ROLLUP, RESULT and HOUR that could become reserved in a later release. Legal now, but a poor choice for a new design.
Plenty of common words aren’t on the T-SQL list at all. Name, Status, Type and Date all work as column names without brackets. That surprises people, as does the fact that data type names such as INT aren’t reserved either.
Using a reserved word as an identifier
The fix is a delimited identifier. Square brackets always work. Double quotes also work, but only while QUOTED_IDENTIFIER is ON.
-- Fails: ORDER and KEY are reserved
-- CREATE TABLE dbo.Order (OrderID int NOT NULL, Key int NULL);
-- Works: delimit them
CREATE TABLE dbo.[Order]
(
OrderID int NOT NULL PRIMARY KEY,
[Key] int NULL,
[Percent] decimal(5,2) NULL,
Status varchar(20) NULL -- not reserved, no brackets needed
);
INSERT INTO dbo.[Order] (OrderID, [Key], [Percent], Status)
VALUES (1, 10, 12.50, 'Open');
SELECT o.OrderID, o.[Key], o.[Percent], o.Status
FROM dbo.[Order] AS o
ORDER BY o.[Key];
DROP TABLE dbo.[Order];
Variable and parameter names are exempt, because the @ tells the parser what it’s looking at. @Order and @Key are fine.
Testing a list of names
There’s no system view that lists reserved words, so when I’m reviewing a proposed schema I let the parser tell me. This script tries each word as an unquoted column name in a table variable and records whether it compiled. A syntax error inside sp_executesql is caught by the outer TRY…CATCH, so the loop carries on.
DECLARE @Words TABLE (Word sysname NOT NULL);
INSERT INTO @Words (Word)
VALUES (N'Order'), (N'Key'), (N'User'), (N'Percent'),
(N'Name'), (N'Status'), (N'Date'), (N'Type');
DECLARE @Result TABLE (Word sysname NOT NULL, IsReserved bit NOT NULL);
DECLARE @Word sysname, @sql nvarchar(400);
DECLARE w CURSOR LOCAL FAST_FORWARD FOR SELECT Word FROM @Words;
OPEN w;
FETCH NEXT FROM w INTO @Word;
WHILE @@FETCH_STATUS = 0
BEGIN
-- Deliberately unquoted: we want the parser to reject reserved words.
-- Only feed this your own list of plain words.
SET @sql = N'DECLARE @t TABLE (' + @Word + N' int);';
BEGIN TRY
EXEC sys.sp_executesql @sql;
INSERT INTO @Result (Word, IsReserved) VALUES (@Word, 0);
END TRY
BEGIN CATCH
INSERT INTO @Result (Word, IsReserved) VALUES (@Word, 1);
END CATCH;
FETCH NEXT FROM w INTO @Word;
END;
CLOSE w;
DEALLOCATE w;
SELECT Word, IsReserved
FROM @Result
ORDER BY IsReserved DESC, Word;
Order, Key, User and Percent come back as reserved; Name, Status, Date and Type don’t. It only checks the current T-SQL list, so I still look at the future keywords list by eye.
The common mistake
The mistake I see most is trusting SSMS colouring. People assume a word shown in blue or pink is reserved and one in black is safe. The editor highlights plenty of words that aren’t reserved (INT, for one), and the colour tells you nothing about the future keywords list. The only real check is the Microsoft list or the parser itself.
The second mistake is quoting with double quotes because it “works on my machine”. “Order” is only an identifier while QUOTED_IDENTIFIER is ON. Most modern clients set it on, but some older tools and connection settings don’t, and then “Order” is read as a string literal and the statement fails in a confusing way. Brackets don’t depend on any setting, which is why I stick to them; I explain the double quotes rule in my SQL Best Practices post.
Watch out for
- Dynamic SQL. Wrap object names in QUOTENAME() rather than adding brackets by hand. It handles reserved words and names that contain a closing bracket.
- Column aliases. SELECT COUNT(*) AS Percent fails just like a column name would. Pick a different alias or bracket it.
- Generated code. ORMs and scripting tools usually bracket everything. Hand-written reports against the same tables often don’t, so a reserved column name bites the second person, not the first.
- Future keywords. A column called Role or Result works today, but there’s no promise it will after an upgrade. Avoid them in new designs.
- Renaming later is expensive. Every view, procedure and report that touches the column has to change. It’s far cheaper to choose a better name at design time.
Microsoft’s full lists are on the Reserved keywords (Transact-SQL) page. For more of my T-SQL and DBA notes, see the SQL Server DBA Handbook.
This topic came up in the SQLServerCentral Question of the Day: Reserved Words I.
Clive Roberts DBA