The building blocks every SQL Server developer must know: data types, DDL statements, the basic DML operations, and database design principles — normalization, relationship patterns, and PK strategy.
Data Types
Choosing the right data types
Picking the right type saves space, speeds up queries, and prevents subtle bugs. Use NVARCHAR for Unicode text, DECIMAL(19,4) for money (not MONEY), DATETIME2 over legacy DATETIME, and ROWVERSION for optimistic concurrency.
-- ── Numeric types ─────────────────────────────
INT -- 4 bytes, -2.1B to 2.1B
BIGINT -- 8 bytes, ±9.2 quintillion (use when > 2.1B rows)
SMALLINT -- 2 bytes, -32 768 to 32 767
TINYINT -- 1 byte, 0–255
DECIMAL(19,4) -- exact fixed-point — always use for money
NUMERIC(10,2) -- alias for DECIMAL
FLOAT -- 8-byte IEEE 754 approximate — ❌ never for money
BIT -- 0 / 1 / NULL (maps to bool in .NET)
-- ❌ MONEY / SMALLMONEY — proprietary, rounding surprises
-- ✅ DECIMAL(19,4) is portable, exact, and index-friendly
-- ── String types ───────────────────────────────
VARCHAR(200) -- ANSI, 1 byte/char, max 8 000
NVARCHAR(200) -- Unicode UTF-16, 2 bytes/char, max 4 000
NVARCHAR(MAX) -- up to 2 GB — avoid in indexes and key columns
CHAR(10) -- fixed-width ANSI, pads with spaces
NCHAR(10) -- fixed-width Unicode
-- Rule of thumb:
-- • User-facing text (names, titles) → NVARCHAR
-- • Internal codes / flags (ISO codes, status) → VARCHAR or CHAR
-- • Never store numbers / dates as strings
-- ── Date / time ────────────────────────────────
DATE -- date only (3 bytes)
TIME(7) -- time only (5 bytes)
DATETIME2(7) -- date + time, high precision (6–8 bytes) ✅
DATETIMEOFFSET -- date + time + UTC offset (10 bytes)
-- ❌ DATETIME — only 1/300 s precision, ends 9999 — use DATETIME2
-- ❌ SMALLDATETIME — ends 2079 — avoid in new schemas
-- ── Optimistic concurrency ─────────────────────
ROWVERSION -- 8-byte, auto-updated on every write
-- detect concurrent changes without locking:
-- WHERE RowVer = @capturedVersion
-- ── Other ──────────────────────────────────────
UNIQUEIDENTIFIER -- GUID (16 bytes) — use NEWSEQUENTIALID()
-- not NEWID() to reduce page fragmentation
VARBINARY(MAX) -- binary blobs (prefer file storage for > 1 MB)
XML -- native XML with XQuery support
DDL
CREATE, ALTER & DROP — table lifecycle
Always specify a schema (e.g. dbo), use IDENTITY for surrogate keys, and add constraints at table-creation time — not as an afterthought.
-- Create a table with common patterns
CREATE TABLE dbo.Products (
ProductId INT IDENTITY(1,1) NOT NULL,
Name NVARCHAR(200) NOT NULL,
Price DECIMAL(10,2) NOT NULL CONSTRAINT CK_Price_Positive CHECK (Price >= 0),
CategoryId INT NOT NULL,
IsActive BIT NOT NULL DEFAULT 1,
CreatedAt DATETIME2(7) NOT NULL DEFAULT SYSUTCDATETIME(),
UpdatedAt DATETIME2(7) NULL,
CONSTRAINT PK_Products PRIMARY KEY CLUSTERED (ProductId),
CONSTRAINT FK_Products_Categories
FOREIGN KEY (CategoryId) REFERENCES dbo.Categories (CategoryId)
ON DELETE RESTRICT
ON UPDATE CASCADE
);
-- Add a column safely (non-breaking)
ALTER TABLE dbo.Products
ADD Description NVARCHAR(MAX) NULL;
-- Rename a column (use sp_rename)
EXEC sp_rename 'dbo.Products.IsActive', 'IsEnabled', 'COLUMN';
-- Drop a table (check existence first)
IF OBJECT_ID('dbo.Products', 'U') IS NOT NULL
DROP TABLE dbo.Products;
DML
INSERT, UPDATE, DELETE & MERGE
OUTPUT lets you capture affected rows in a single statement. MERGE handles upsert atomically but watch for edge cases with multiple source matches.
-- INSERT — multi-row, returns new IDs
INSERT INTO dbo.Products (Name, Price, CategoryId)
OUTPUT INSERTED.ProductId, INSERTED.Name
VALUES
(N'Coffee', 3.50, 1),
(N'Latte', 4.50, 1),
(N'Muffin', 2.25, 2);
-- UPDATE with OUTPUT
UPDATE dbo.Products
SET Price = Price * 1.10,
UpdatedAt = SYSUTCDATETIME()
OUTPUT DELETED.Price AS OldPrice,
INSERTED.Price AS NewPrice,
INSERTED.ProductId
WHERE CategoryId = 1
AND IsEnabled = 1;
-- DELETE — remove stale records
DELETE FROM dbo.Products
WHERE IsEnabled = 0
AND UpdatedAt < DATEADD(DAY, -90, SYSUTCDATETIME());
-- MERGE — upsert from staging table
MERGE dbo.Products AS target
USING dbo.ProductsStaging AS src
ON target.ProductId = src.ProductId
WHEN MATCHED THEN
UPDATE SET target.Price = src.Price,
target.UpdatedAt = SYSUTCDATETIME()
WHEN NOT MATCHED BY TARGET THEN
INSERT (Name, Price, CategoryId)
VALUES (src.Name, src.Price, src.CategoryId)
WHEN NOT MATCHED BY SOURCE THEN
DELETE;
DB Design
Normalization, relationships & PK strategy
Good schema design prevents data anomalies before they happen. Aim for 3NF in OLTP schemas, use surrogate INT IDENTITY PKs by default, and always enforce relationships with FK constraints.
-- ── 1NF: Atomic values — no repeating groups ─
-- ❌ Comma-list in a single column is un-queryable
CREATE TABLE dbo.Articles (
Id INT PRIMARY KEY,
Title NVARCHAR(200),
Tags VARCHAR(500) -- "sql,tsql,database" — breaks 1NF
);
-- ✅ Separate entity table (also enables indexing)
CREATE TABLE dbo.ArticleTags (
ArticleId INT NOT NULL REFERENCES dbo.Articles(Id),
Tag VARCHAR(50) NOT NULL,
CONSTRAINT PK_ArticleTags PRIMARY KEY (ArticleId, Tag)
);
-- ── 3NF: Remove transitive dependencies ───────
-- ❌ City depends on ZipCode, not on CustomerId
CREATE TABLE dbo.Customers_Bad (
CustomerId INT PRIMARY KEY,
ZipCode CHAR(5),
City VARCHAR(100) -- transitive: ZipCode → City
);
-- ✅ Extract the dependency to its own table
CREATE TABLE dbo.ZipCodes (
ZipCode CHAR(5) NOT NULL PRIMARY KEY,
City NVARCHAR(100) NOT NULL
);
CREATE TABLE dbo.Customers (
CustomerId INT NOT NULL IDENTITY PRIMARY KEY,
ZipCode CHAR(5) REFERENCES dbo.ZipCodes(ZipCode)
);
-- ── Relationship patterns ──────────────────────
-- 1:N → FK on the "many" side
-- 1:1 → FK + UNIQUE constraint on child table
-- M:N → junction / bridge table
-- M:N junction table (Products ↔ Categories)
CREATE TABLE dbo.ProductCategories (
ProductId INT NOT NULL REFERENCES dbo.Products(ProductId),
CategoryId INT NOT NULL REFERENCES dbo.Categories(CategoryId),
SortOrder TINYINT NOT NULL DEFAULT 0,
CONSTRAINT PK_ProductCategories PRIMARY KEY (ProductId, CategoryId)
);
-- ── PK strategy comparison ─────────────────────
-- ✅ INT IDENTITY — compact (4 B), sequential, fast inserts (default choice)
-- ✅ BIGINT IDENTITY — when you expect > 2.1 B rows
-- ⚠ UNIQUEIDENTIFIER with NEWSEQUENTIALID() — for distributed / replication
-- (NEWID() causes page fragmentation — avoid as clustered PK)
-- ❌ Natural keys (email, SSN) — they change and cause cascade updates
-- ── Naming conventions (consistency beats style) ─
-- Tables : PascalCase, singular → dbo.Customer, dbo.OrderLine
-- Columns : PascalCase → CustomerId, CreatedAt
-- PKs : PK_TableName
-- FKs : FK_ChildTable_ParentTable
-- Indexes : IX_TableName_Columns, UX_ for unique
-- Procs : usp_VerbNoun → usp_CreateOrder, usp_GetCustomer
-- Functions: fn_VerbNoun → fn_FullName, fn_CustomerOrders