Cheat sheet

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 #

sql
-- 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 #

sql
-- 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 #

sql
-- + 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 #

sql
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 failure

CAST / CONVERT / TRY_* #

sql
-- 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-aware

MERGE, OUTPUT, RETURNING-equivalent #

sql
-- 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+) #

sql
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 #

sql
-- 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 #

sql
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.

Tip

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.