SQL Server (T-SQL) cheat sheet
T-SQL specifics that diverge from ANSI / Postgres / MySQL: IDENTITY, TOP, OFFSET ... FETCH, CONVERT style codes, MERGE, OUTPUT, system catalogs (sys.*), and the + concat operator.
Type system & IDENTITY #
-- Auto-increment.
id INT IDENTITY(1, 1) PRIMARY KEY -- (seed, increment)
id BIGINT IDENTITY(1, 1) PRIMARY KEY -- safer for growth
-- Get last insert ID.
SELECT SCOPE_IDENTITY(); -- recommended (current scope)
SELECT @@IDENTITY; -- current session, any scope (avoid)
SELECT IDENT_CURRENT('table_name'); -- last identity for a specific table
-- Common types.
TINYINT (0-255), SMALLINT, INT, BIGINT
DECIMAL(p, s), NUMERIC(p, s) -- synonyms
MONEY, SMALLMONEY -- 4-decimal-place fixed-point (limited)
DATE, TIME, DATETIME2, DATETIMEOFFSET, SMALLDATETIME
DATETIME -- legacy: 3.33ms precision, 1753-9999 only
VARCHAR(n), VARCHAR(MAX) -- 8000 bytes / 2 GB
NVARCHAR(n), NVARCHAR(MAX) -- Unicode (always prefer for new code)
UNIQUEIDENTIFIER -- GUID; default via NEWID() or NEWSEQUENTIALID()
BIT -- boolean (0 / 1 / NULL)TOP, OFFSET FETCH, pagination #
-- Old-school: TOP.
SELECT TOP 20 * FROM users ORDER BY created_at DESC;
SELECT TOP 20 PERCENT * FROM users;
-- ANSI: OFFSET ... FETCH (2012+).
SELECT * FROM users
ORDER BY created_at DESC
OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY;
-- TOP with WITH TIES (rare but useful).
SELECT TOP 10 WITH TIES *
FROM user_revenue
ORDER BY revenue DESC;Concatenation #
-- + is the legacy concat operator. Watch type-conversion errors.
SELECT first_name + ' ' + last_name AS full_name FROM users;
-- This errors if any operand is numeric without explicit CAST.
-- CONCAT (2012+) — null-tolerant (treats NULL as empty string).
SELECT CONCAT(first_name, ' ', last_name) FROM users;
-- CONCAT_WS (2017+) — separator, skips NULLs.
SELECT CONCAT_WS(', ', city, state, zip) FROM addresses;
-- STRING_AGG (2017+).
SELECT STRING_AGG(name, ', ') WITHIN GROUP (ORDER BY name) FROM tags;Date functions #
GETDATE() -- server-local
SYSDATETIME() -- higher precision, server-local
GETUTCDATE() -- UTC
SYSDATETIMEOFFSET() -- with timezone offset
-- Parts.
YEAR(d), MONTH(d), DAY(d)
DATEPART(year, d), DATEPART(quarter, d), DATEPART(weekday, d)
DATENAME(month, d) -- 'April'
-- Math.
DATEADD(DAY, 7, d)
DATEDIFF(DAY, start, end) -- (unit, start, end) — boundary-counting!
EOMONTH(d) -- last day of month
DATEFROMPARTS(2026, 4, 26)
-- Truncate (2022+).
DATETRUNC(MONTH, d)
-- Pre-2022 truncate workaround.
DATEFROMPARTS(YEAR(d), MONTH(d), 1) -- first of month
-- Format / parse.
FORMAT(d, 'yyyy-MM-dd') -- .NET-style format strings
CONVERT(VARCHAR, d, 120) -- '2026-04-26 14:30:00'
CONVERT(DATE, '26/04/2026', 103) -- 103 = "dd/mm/yyyy" style code
TRY_CONVERT(DATE, 'not-a-date') -- NULL on failureCAST / CONVERT / TRY_* #
-- ANSI cast.
CAST(value AS INT)
CAST('2026-04-26' AS DATE)
-- T-SQL CONVERT (with optional style code for dates).
CONVERT(VARCHAR(20), GETDATE(), 120) -- '2026-04-26 14:30:00'
CONVERT(VARCHAR, GETDATE(), 103) -- '26/04/2026'
-- Safe variants — return NULL on failure.
TRY_CAST('not-a-number' AS INT) -- NULL
TRY_CONVERT(DATE, 'invalid') -- NULL
TRY_PARSE('26/04/2026' AS DATE USING 'en-GB') -- locale-awareMERGE, OUTPUT, RETURNING-equivalent #
-- MERGE (full feature set).
MERGE INTO target AS t
USING source AS s
ON t.id = s.id
WHEN MATCHED AND s.is_deleted = 1 THEN DELETE
WHEN MATCHED THEN UPDATE SET t.value = s.value
WHEN NOT MATCHED THEN INSERT (id, value) VALUES (s.id, s.value);
-- OUTPUT — return rows from a DML statement.
INSERT INTO users (email)
OUTPUT INSERTED.id, INSERTED.created_at
VALUES ('a@x.com');
UPDATE users
SET plan = 'pro'
OUTPUT DELETED.plan AS old_plan, INSERTED.plan AS new_plan
WHERE id = 1;
DELETE FROM users
OUTPUT DELETED.email
WHERE id = 1;JSON (2016+) #
JSON_VALUE(payload, '$.user_id') -- scalar
JSON_QUERY(payload, '$.user') -- nested object/array
ISJSON(string) -- validate
-- Modify.
JSON_MODIFY(payload, '$.user.plan', 'pro')
-- Parse to rowset (very useful).
SELECT *
FROM OPENJSON(@json_doc)
WITH (id INT, email NVARCHAR(200), plan NVARCHAR(20));
-- Build (2022+).
SELECT JSON_OBJECT('id': id, 'email': email) FROM users;
SELECT JSON_ARRAY(name, email) FROM users;System catalogs and DMVs #
-- Tables and columns.
SELECT * FROM sys.tables;
SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('orders');
sp_help 'orders'; -- columns + constraints + indexes
-- Foreign keys.
SELECT
OBJECT_NAME(parent_object_id) AS child_table,
OBJECT_NAME(referenced_object_id) AS parent_table
FROM sys.foreign_keys;
-- Active queries.
SELECT session_id, status, start_time, command, text
FROM sys.dm_exec_requests
CROSS APPLY sys.dm_exec_sql_text(sql_handle);
-- Most expensive queries.
SELECT TOP 20
total_worker_time / execution_count AS avg_cpu_time,
execution_count,
query_text = SUBSTRING(t.text, qs.statement_start_offset / 2,
(qs.statement_end_offset - qs.statement_start_offset) / 2)
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) t
ORDER BY avg_cpu_time DESC;Locking and isolation #
BEGIN TRANSACTION;
-- ...DML...
COMMIT;
-- ROLLBACK;
-- Read uncommitted (dirty reads).
SELECT * FROM orders WITH (NOLOCK); -- per-query
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- per-session
-- Other table hints (use deliberately).
WITH (READPAST) -- skip locked rows
WITH (UPDLOCK, HOLDLOCK) -- "select for update"-ish
-- Find blocking.
SELECT blocking_session_id, session_id, wait_type, wait_time, command
FROM sys.dm_exec_requests
WHERE blocking_session_id <> 0;FAQ #
How do I do pagination in SQL Server?
Modern: ORDER BY col OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY, the ANSI standard, supported in 2012+. Legacy: SELECT TOP 20 ... (can't express offset directly). High offsets are slow on large tables. For deep pagination, use keyset pagination (WHERE id > last_seen_id ORDER BY id) which scales O(log N) with an index.
IDENTITY or SEQUENCE: which auto-increment to use?
IDENTITY is per-table, simpler, the traditional choice. SEQUENCE (2012+) is independent of any table, can be shared across tables, supports caching for high-throughput inserts. Use IDENTITY for typical single-table primary keys; SEQUENCE when multiple tables need shared, gap-tolerant IDs.
How do I do an upsert in SQL Server?
MERGE is the canonical option, with full MATCHED / NOT MATCHED BY TARGET / NOT MATCHED BY SOURCE branches. Note: Microsoft itself recommends caution due to historical concurrency bugs around MERGE. For simple upserts, an IF EXISTS + UPDATE else INSERT pattern in a transaction is sometimes safer.
What's the OUTPUT clause for?
OUTPUT returns rows from a DML statement: INSERT INTO t (...) OUTPUT INSERTED.id, INSERTED.created_at VALUES (...). Useful for audit trails, archive-then-delete patterns, and getting auto-generated values back without a second roundtrip. Combines well with MERGE for change-tracking.
DATETIME vs DATETIME2: which one?
DATETIME2 for new code. DATETIME is the legacy type with 3.33ms precision and a 1753-9999 range. DATETIME2 has 100-nanosecond precision and a 0001-9999 range. Same storage cost, more accuracy. The only reason to use DATETIME is compatibility with very old code.
How do I find slow queries in SQL Server?
sys.dm_exec_query_stats joined with sys.dm_exec_sql_text shows historical query performance with avg CPU time, executions, etc. For real-time: sys.dm_exec_requests. For wait analysis: sys.dm_os_wait_stats. The Activity Monitor in SSMS surfaces this graphically.
T-SQL's underrated combo: OUTPUT + MERGE. MERGE handles the upsert, OUTPUT returns exactly what changed (inserted, updated, deleted), perfect for downstream change-data-capture without a separate audit trigger. The combination doesn't have a clean Postgres equivalent.