Where T-SQL parts ways with standard SQL
TOP and WITH TIES, OFFSET/FETCH, IDENTITY that never gives numbers back, and why the N in N'text' matters.
After this lesson you can
- Choose between TOP, TOP WITH TIES and OFFSET/FETCH and say why each needs an ORDER BY
- Get a new IDENTITY value safely, and explain why identity gaps are normal
- Pick NVARCHAR or VARCHAR and write string literals that survive the trip
If you already know SQL, most of T-SQL reads the same. The differences are small, and they are exactly where people coming from PostgreSQL or MySQL write a query that runs and returns the wrong thing.
TOP, WITH TIES and OFFSET/FETCH
SQL Server has no LIMIT. It has TOP (n), which takes the first n
rows in the order of the query's ORDER BY. Without an ORDER BY,
"the first three" means whichever three the engine happened to produce,
and that can change between runs.
When rows tie on the sort key, TOP (3) still stops at three and picks
among the tied rows arbitrarily. TOP (3) WITH TIES keeps going while
the next row ties with the third on every ORDER BY column.
For paging, OFFSET ... FETCH is the standard form, and it is only
allowed after an ORDER BY:
SELECT id, name, price
FROM products
ORDER BY price DESC, id -- unique, so page 2 is well defined
OFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY;
CREATE TABLE products (id INT IDENTITY(1,1) PRIMARY KEY, name NVARCHAR(50) NOT NULL, price DECIMAL(8,2) NOT NULL);INSERT INTO products (name, price) VALUES (N'Kettle', 120), (N'Toaster', 95), (N'Blender', 95), (N'Mixer', 95), (N'Grill', 80), (N'Fan', 40), (N'Lamp', 25);SELECT 'TOP (3)' AS query, COUNT(*) AS rows_returnedFROM (SELECT TOP (3) id FROM products ORDER BY price DESC) AS tUNION ALLSELECT 'TOP (3) WITH TIES', COUNT(*)FROM (SELECT TOP (3) WITH TIES id FROM products ORDER BY price DESC) AS tUNION ALLSELECT 'OFFSET 3 FETCH NEXT 3', COUNT(*)FROM (SELECT id FROM products ORDER BY price DESC, id OFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY) AS t;IDENTITY never hands numbers back
INT IDENTITY(1,1) generates the key on insert. Read the value you just
generated with SCOPE_IDENTITY() (this scope only) or, better, the
OUTPUT clause, which also works for multi-row inserts. @@IDENTITY
returns the last identity generated in the session in any scope, so a
trigger that inserts into an audit table changes what it returns.
An identity value is consumed even when the insert is rolled back, so
gaps are normal. Never use IDENTITY where the business needs an
unbroken sequence, such as invoice numbers in some jurisdictions.
CREATE TABLE tickets (id INT IDENTITY(1,1) PRIMARY KEY, title NVARCHAR(50) NOT NULL);INSERT INTO tickets (title) VALUES (N'first'); BEGIN TRAN;INSERT INTO tickets (title) VALUES (N'rolled back');ROLLBACK; DECLARE @new TABLE (id INT);INSERT INTO tickets (title) OUTPUT inserted.id INTO @new VALUES (N'third'); SELECT t.id, t.title, (SELECT id FROM @new) AS output_clause_id, SCOPE_IDENTITY() AS scope_identityFROM tickets AS tORDER BY t.id;NVARCHAR, the N prefix and collation
VARCHAR stores characters in the code page of the column's collation
(or as UTF-8, if the column uses one of the _UTF8 collations added in
SQL Server 2019); NVARCHAR stores UTF-16, two bytes per character for
most scripts. A literal written as 'text' is VARCHAR in the
database's code page, so it is converted before it reaches your
column; N'text' is NVARCHAR. Characters the code page cannot
represent become question marks, silently.
Comparisons follow the collation. The one used here,
SQL_Latin1_General_CP1_CI_AS, is case-insensitive (CI), so
'abc' = 'ABC' is true. LEN ignores trailing spaces;
DATALENGTH counts bytes.
SELECT 'ნინო' AS without_n, N'ნინო' AS with_n, LEN(N'abc ') AS len_with_trailing_spaces, DATALENGTH(N'abc') AS nvarchar_bytes, DATALENGTH('abc') AS varchar_bytes, CASE WHEN 'abc' = 'ABC' THEN 'equal' ELSE 'different' END AS case_compare;Common mistakes
TOPwithoutORDER BY. It returns some rows, not the first ones, and a different plan can return different ones tomorrow.- Paging on a non-unique sort key. With
ORDER BY pricealone, rows with the same price can appear on two pages or on none; add a unique tie-breaker such asid. - Expecting identity values to be contiguous. The rolled-back insert above burnt id 2 for good.
- Writing Unicode text without
N. The first column of the last example shows what the database stored: four question marks.
What interviewers ask
SCOPE_IDENTITY(),@@IDENTITYorOUTPUT?@@IDENTITYcrosses scopes, so a trigger can change it;SCOPE_IDENTITY()is safe for one row;OUTPUT inserted.idis safe for many.- What does
WITH TIESdo, and what does it need? It returns extra rows that tie with the last one on the wholeORDER BYlist, so it needs anORDER BY.
Try it yourself
2 visible tests · 3 hidden testsTables products(id, name, price) and page_request(page_no, page_size). page_request has exactly one row, and page_no counts
from 1.
The product list is ordered by price descending; products with the
same price are listed by id ascending. Return page page_no of that
list, with page_size products per page: columns id, name and
price, in list order. A page past the end of the list returns no
rows.
CREATE TABLE products (id INT PRIMARY KEY, name NVARCHAR(50) NOT NULL, price DECIMAL(8,2) NOT NULL);CREATE TABLE page_request (page_no INT NOT NULL, page_size INT NOT NULL);INSERT INTO products VALUES (1, N'Kettle', 120), (2, N'Toaster', 95), (3, N'Blender', 95), (4, N'Mixer', 95), (5, N'Grill', 80), (6, N'Fan', 40), (7, N'Lamp', 25);INSERT INTO page_request VALUES (2, 3);Sign up to run the tests, check the hidden ones and save your progress. Sign up