LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

SQL Server Quick Start and T-SQL Cheat Sheet

sql-servertsqldatabasesmicrosoftsqlbackend

If you already speak SQL — PostgreSQL, MySQL, or anything ANSI-flavored — then Microsoft SQL Server is not a new language so much as a new dialect with its own accent, its own tooling, and a handful of opinions that will trip you up in the first hour. This post is the onramp: enough to connect, run queries, understand the engine’s quirks, and keep one tab open as a T-SQL reference. It assumes you know what a JOIN and an index are, and focuses on what is different about SQL Server rather than re-teaching relational basics.

A note on versions: the current release as of mid-2026 is SQL Server 2025 (internally version 17). It is the biggest developer-facing release in a decade — native regular expressions in T-SQL, a real vector data type for semantic search, native JSON up to 2 GB per row with JSON indexes, REST endpoints built into the engine, and change event streaming to Kafka and Azure Event Hubs. We will touch the highlights, but most of this cheat sheet is dialect bedrock that has been stable for years and will serve you on 2019, 2022, and 2025 alike.


Getting an engine in front of you

You do not need Windows. Since SQL Server 2017 the engine runs natively on Linux, and the fastest way to a working instance is a container. This is the single most useful thing to know coming from the Postgres/MySQL world, where docker run is reflexive.

1
2
3
4
5
6
7
# SQL Server 2025 in a container — accept the EULA, set a strong SA password
docker run -d --name mssql \
  -e "ACCEPT_EULA=Y" \
  -e "MSSQL_SA_PASSWORD=YourStrong!Passw0rd" \
  -e "MSSQL_PID=Developer" \
  -p 1433:1433 \
  mcr.microsoft.com/mssql/server:2025-latest

A few things that surprise newcomers:

  • The default port is 1433, not 5432 (Postgres) or 3306 (MySQL).
  • The password policy is enforced — at least 8 characters across upper, lower, digit, and symbol classes. A weak MSSQL_SA_PASSWORD makes the container start and then immediately exit; check docker logs mssql if it will not stay up.
  • MSSQL_PID=Developer selects the free-for-non-production Developer edition, which is feature-identical to Enterprise. Use it for everything that is not production.
  • SQL Server 2025 defaults to encrypted connections (Encrypt=True) and TLS 1.3. Clients that worked against older servers with no TLS config may now need TrustServerCertificate=True for the self-signed container cert, or a proper certificate. This is a real gotcha — an old connection string can fail against a 2025 server purely on encryption defaults.

Connecting: three clients, one protocol

Client What it is When to reach for it
sqlcmd The command-line client (a modern Go-based sqlcmd has replaced the old ODBC one) Scripting, CI, quick checks, anything headless
Azure Data Studio (ADS) Cross-platform (Win/Mac/Linux) Electron GUI, notebook support Day-to-day querying on a non-Windows workstation
SQL Server Management Studio (SSMS) The heavyweight Windows-only admin GUI Deep administration, execution-plan analysis, the full toolbox

The mental mapping from Postgres: sqlcmd is your psql, Azure Data Studio is your cross-platform pgAdmin/DataGrip, and SSMS is the Windows power-tool with no Postgres equivalent (it does far more than pgAdmin).

1
2
3
4
5
# Connect with the modern sqlcmd (note: -C trusts the self-signed cert)
sqlcmd -S localhost -U sa -P 'YourStrong!Passw0rd' -C

# Run a one-off query and exit
sqlcmd -S localhost -U sa -P 'YourStrong!Passw0rd' -C -Q "SELECT @@VERSION"

The biggest sqlcmd/psql difference: statements do not run until you type GO on its own line. GO is not T-SQL — it is a batch separator the client understands. This confuses everyone at first: you type a SELECT, hit enter, and nothing happens, because the engine has not received a batch yet.

1
2
SELECT name FROM sys.databases;
GO

The T-SQL accent: what is actually different

Here is the core of the cheat sheet — the dialect divergences that matter, side by side with what you already know.

Limiting rows: TOP and OFFSET/FETCH, not LIMIT

There is no LIMIT. To grab the first N rows, use TOP:

1
SELECT TOP (10) * FROM Orders ORDER BY CreatedAt DESC;

For pagination, the ANSI-standard OFFSET ... FETCH is the right tool (and it requires an ORDER BY):

1
2
3
SELECT * FROM Orders
ORDER BY CreatedAt DESC
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;   -- page 3, 10 per page

Identifiers and strings

  • Bracket-quote identifiers with [ ]: SELECT [Order].[Total] FROM [Order]. Double quotes work only when QUOTED_IDENTIFIER is on (it usually is), but brackets are the idiomatic SQL Server way.
  • Unicode string literals take an N prefix: N'café'. Without the N, a literal is non-Unicode varchar and can mangle characters. When in doubt, prefix it. This is the single most common encoding bug newcomers hit.
  • String concatenation is +, not ||: SELECT FirstName + ' ' + LastName. Beware: NULL + 'x' is NULL. Use CONCAT() to treat NULLs as empty strings.

NULL-handling and common functions

You want Postgres/MySQL SQL Server (T-SQL)
Coalesce two values COALESCE(a, b) ISNULL(a, b) or COALESCE(a, b)
Current timestamp now() SYSDATETIME() / GETDATE()
Cast a::int CAST(a AS int) / TRY_CAST(a AS int)
String length length(s) LEN(s) (trims trailing spaces!) / DATALENGTH(s)
Substring substring(s,1,3) SUBSTRING(s,1,3)
Conditional CASE CASE or IIF(cond, a, b)
Auto-increment SERIAL / AUTO_INCREMENT IDENTITY(1,1)

Two traps worth memorizing: LEN() ignores trailing spaces (use DATALENGTH for byte length), and SQL Server’s + concatenation propagates NULL while CONCAT() does not.

MERGE: upsert in one statement

SQL Server has had MERGE for years (Postgres only gained it recently). It does insert/update/delete against a target from a source in one statement:

1
2
3
4
5
6
7
MERGE INTO Inventory AS target
USING (VALUES (101, 5)) AS source (ProductId, Qty)
   ON target.ProductId = source.ProductId
WHEN MATCHED THEN
   UPDATE SET target.Qty = target.Qty + source.Qty
WHEN NOT MATCHED THEN
   INSERT (ProductId, Qty) VALUES (source.ProductId, source.Qty);

A caution from the field: MERGE has historically had subtle concurrency and bug edge cases. For high-contention simple upserts, many practitioners still prefer an explicit UPDATE then INSERT ... WHERE NOT EXISTS under a transaction. Know MERGE, use it deliberately.

Common table expressions and window functions

These are ANSI-standard and behave the way you expect — good news, since they cover most of the analytical work you will do.

1
2
3
4
5
6
7
8
9
WITH ranked AS (
    SELECT
        CustomerId,
        OrderTotal,
        ROW_NUMBER() OVER (PARTITION BY CustomerId
                           ORDER BY OrderTotal DESC) AS rn
    FROM Orders
)
SELECT * FROM ranked WHERE rn = 1;   -- top order per customer

The full window-function vocabulary is here: ROW_NUMBER, RANK, DENSE_RANK, NTILE, LAG/LEAD, running aggregates with OVER (... ROWS BETWEEN ...). One nicety: SQL Server’s STRING_AGG(expr, ',') is the equivalent of Postgres string_agg / MySQL GROUP_CONCAT.


Data types you need to get right

Category Use this Avoid / why
Variable text nvarchar(n) or nvarchar(max) nvarchar is Unicode; varchar is single-byte. Use n-types unless you have a reason not to
Fixed text char / nchar Pads with spaces; rarely what you want
Whole numbers int, bigint tinyint is 0–255 only (not signed!)
Money/exact decimal decimal(p,s) Never float/real for currency
Date + time datetime2(n) The old datetime has a 3.33 ms resolution and a 1753 floor — prefer datetime2
Date only date
Boolean bit (0/1) There is no real boolean type; bit holds 0, 1, or NULL
Unique id uniqueidentifier (GUID) Random GUIDs fragment clustered indexes; consider NEWSEQUENTIALID()
JSON json (native, 2025) or nvarchar(max) 2025 adds a real json type + indexing; older versions store JSON as text

The headline surprises for a Postgres refugee: there is no boolean type (use bit), no array type (normalize, or use JSON), and the default datetime is a legacy minefield — reach for datetime2.

Identity columns and getting the new key back

1
2
3
4
5
6
7
8
CREATE TABLE Customer (
    CustomerId int IDENTITY(1,1) PRIMARY KEY,
    Email      nvarchar(256) NOT NULL UNIQUE,
    CreatedAt  datetime2 NOT NULL DEFAULT SYSDATETIME()
);

INSERT INTO Customer (Email) VALUES (N'a@example.com');
SELECT SCOPE_IDENTITY();          -- the id just inserted in this scope

Use SCOPE_IDENTITY() rather than @@IDENTITY — the latter can return an id from a trigger that fired on your insert, a classic foot-gun. For bulk inserts that need the keys back, the OUTPUT clause is cleaner:

1
2
3
INSERT INTO Customer (Email)
OUTPUT inserted.CustomerId, inserted.Email
VALUES (N'b@example.com'), (N'c@example.com');

Indexes and execution plans

SQL Server’s indexing has one concept that dominates everything: the clustered index is the table. The clustered index defines the physical order of the rows, so a table has at most one. By default a PRIMARY KEY becomes the clustered index. Everything else is a nonclustered index, which stores the key columns plus a pointer back to the clustered key.

   Clustered index (the table itself, sorted by key)
   ┌──────────────────────────────────────────┐
   │ CustomerId | Email        | CreatedAt ... │   ← rows live here, in key order
   └──────────────────────────────────────────┘
                     ▲
                     │ lookup by clustered key
   Nonclustered index on (Email)
   ┌────────────────────────────┐
   │ Email  →  CustomerId (ptr) │   ← small, points back to the table
   └────────────────────────────┘

Practical consequences:

  • A query filtering on a nonclustered-indexed column does a key lookup back into the clustered index for any columns the index does not contain. Avoid the lookup by covering the query: add the needed columns with INCLUDE.
1
2
3
CREATE NONCLUSTERED INDEX IX_Customer_Email
ON Customer (Email)
INCLUDE (CreatedAt);     -- now a query on Email returning CreatedAt needs no lookup
  • Read the plan. In Azure Data Studio or SSMS, enable “Include Actual Execution Plan” (Ctrl+M in SSMS). The graphical plan reads right-to-left. Watch for Key Lookups (often fixable with INCLUDE), Scans where you expected Seeks, and fat arrows indicating large row counts. The text equivalents are SET STATISTICS IO ON (logical reads per table) and SET STATISTICS TIME ON.
1
2
3
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
SELECT CreatedAt FROM Customer WHERE Email = N'a@example.com';

A “Seek” means the engine jumped straight to the rows via the index; a “Scan” means it read the whole structure. Turning Scans into Seeks is most of practical query tuning.


Stored procedures and functions

T-SQL’s procedural side is more central to SQL Server culture than to Postgres — a lot of business logic lives in stored procedures. The basics:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
CREATE OR ALTER PROCEDURE dbo.GetCustomerOrders
    @CustomerId int,
    @Since      datetime2 = NULL          -- optional parameter with default
AS
BEGIN
    SET NOCOUNT ON;                        -- suppress "N rows affected" chatter
    SELECT OrderId, OrderTotal, CreatedAt
    FROM Orders
    WHERE CustomerId = @CustomerId
      AND (@Since IS NULL OR CreatedAt >= @Since);
END;
GO

EXEC dbo.GetCustomerOrders @CustomerId = 42;

Notes that matter:

  • dbo is the default schema — the equivalent of Postgres public. Qualify objects as dbo.Thing.
  • Variables are @-prefixed; parameters too.
  • SET NOCOUNT ON at the top of every procedure avoids extra round-trip metadata and is near-universal practice.
  • CREATE OR ALTER (since 2016) is the idempotent form you want in migration scripts.
  • Functions come in flavors: scalar functions (avoid them in WHERE/SELECT over large sets — they historically killed parallelism and performance) and inline table-valued functions, which the optimizer can fold into the query and are the performant choice.

Backup and restore

The fundamentals every admin needs on day one. SQL Server backs up to .bak files and the model is straightforward once you know the recovery models.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- Full backup
BACKUP DATABASE AppDb
TO DISK = N'/var/opt/mssql/backup/AppDb_full.bak'
WITH FORMAT, COMPRESSION, STATS = 10;

-- Restore (MOVE relocates the data/log files to valid paths)
RESTORE DATABASE AppDb
FROM DISK = N'/var/opt/mssql/backup/AppDb_full.bak'
WITH MOVE 'AppDb'     TO '/var/opt/mssql/data/AppDb.mdf',
     MOVE 'AppDb_log' TO '/var/opt/mssql/data/AppDb_log.ldf',
     REPLACE;

The one concept to understand is the recovery model:

  • SIMPLE — no transaction-log backups, log truncates automatically. Easiest; you can only restore to the last full/differential backup. Fine for dev and many internal apps.
  • FULL — every change is logged until you back up the log; enables point-in-time restore but the log grows until you take log backups. Required for most production. Forgetting to back up the log under FULL is the classic “why is my disk full?” incident — the log grows forever.
1
2
ALTER DATABASE AppDb SET RECOVERY FULL;
BACKUP LOG AppDb TO DISK = N'/var/opt/mssql/backup/AppDb_log.trn';

What is new in SQL Server 2025 (and worth knowing)

You can be productive without any of this, but a few 2025 features change what is reasonable to do in the database:

  • Native RegEx in T-SQLREGEXP_LIKE, REGEXP_REPLACE, and friends, finally ending the era of nightmarish nested PATINDEX/CHARINDEX hacks for pattern work.
  • The vector data type and vector search — store embeddings and run nearest-neighbor similarity directly, no separate vector database required. Combined with the ability to call external AI models from T-SQL, this brings semantic search into the engine.
  • Native JSON type with indexing — up to 2 GB per row, with indexes on JSON path expressions so filtering/sorting on a JSON field is no longer a full scan.
  • Built-in REST endpoints and GraphQL — expose data over HTTP through the engine itself.
  • Change Event Streaming — push row changes to Kafka or Azure Event Hubs for real-time pipelines, plus Fabric Mirroring for analytics.
  • Security defaults tightened — encrypted connections (Encrypt=True) and TLS 1.3 by default, which, again, is the thing most likely to break an old connection string.

The newcomer gotcha list

Keep these pinned; they account for most of the “why doesn’t this work” moments in week one:

  1. GO is required to send a batch in sqlcmd/SSMS — it is a client directive, not SQL.
  2. No LIMIT — use TOP (n) or OFFSET/FETCH (which needs ORDER BY).
  3. Prefix Unicode literals with NN'text' — or risk silent character corruption.
  4. + concatenates and propagates NULL; CONCAT() does not. There is no ||.
  5. No boolean type — use bit (0/1/NULL).
  6. datetime is legacy — use datetime2.
  7. LEN() trims trailing spaces — use DATALENGTH() for true length.
  8. SCOPE_IDENTITY(), not @@IDENTITY, to get the inserted id safely.
  9. dbo. is the default schema — qualify your objects.
  10. 2025 encrypts by default — add TrustServerCertificate=True for self-signed dev certs or supply a real certificate.

With those internalized, the rest of T-SQL is the SQL you already know. Keep this page open in a tab, lean on Azure Data Studio for cross-platform querying, run the engine in a container for everything non-production, and read your execution plans — that combination will make you dangerous on SQL Server within a week.


Sources:

Comments