CRUD (Create, Read, Update, Delete) forms the core of application database interaction. SQL Server extends standard SQL with OUTPUT, MERGE, table hints, and rich transaction control — use them to build reliable, auditable data pipelines.

INSERT Patterns

  -- Single row
INSERT INTO dbo.Users (Name, Email, Status)
VALUES (N'Alice', N'[email protected]', N'active');

-- Multiple rows
INSERT INTO dbo.Users (Name, Email, Status)
VALUES
    (N'Bob', N'[email protected]', N'active'),
    (N'Carol', N'[email protected]', N'pending');

-- Insert from query
INSERT INTO dbo.ArchiveUsers (UserID, Name, Email, ArchivedAt)
SELECT UserID, Name, Email, SYSUTCDATETIME()
FROM dbo.Users
WHERE Status = N'inactive';
  

Return Generated Identity

  INSERT INTO dbo.Orders (CustomerID, Total)
VALUES (42, 149.99);

SELECT SCOPE_IDENTITY() AS new_order_id;  -- Same session/scope
-- Prefer OUTPUT for triggers/multiple inserts:
INSERT INTO dbo.Orders (CustomerID, Total)
OUTPUT INSERTED.OrderID
VALUES (42, 149.99);
  

@@IDENTITY reflects any identity in the session — can surprise you if triggers insert elsewhere. Prefer SCOPE_IDENTITY() or OUTPUT.

SELECT with Filtering and Pagination

  SELECT UserID, Name, Email, CreatedAt
FROM dbo.Users
WHERE Status = N'active'
  AND CreatedAt >= '2024-01-01'
ORDER BY CreatedAt DESC
OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY;
  

SQL Server 2012+ standard pagination — always pair ORDER BY with OFFSET/FETCH.

  -- IN and BETWEEN
SELECT * FROM dbo.Products WHERE CategoryID IN (1, 3, 5);
SELECT * FROM dbo.Orders WHERE Total BETWEEN 50.00 AND 200.00;

-- Pattern matching — leading wildcard prevents index use
SELECT * FROM dbo.Users WHERE Email LIKE N'%@example.com';  -- Scan likely
SELECT * FROM dbo.Users WHERE Email LIKE N'[email protected]';  -- Seek possible
  

Keyset Pagination (Performance)

  DECLARE @last_id INT = 1000;

SELECT TOP 20 OrderID, CustomerID, Total, CreatedAt
FROM dbo.Orders
WHERE OrderID > @last_id
ORDER BY OrderID;
  

Keyset beats OFFSET on deep pages — O(1) vs scanning skipped rows.

UPDATE Patterns

  -- Simple update
UPDATE dbo.Users
SET Status = N'inactive', ModifiedAt = SYSUTCDATETIME()
WHERE LastLoginAt < '2023-01-01';

-- Conditional update with safety
UPDATE dbo.Products
SET Stock = Stock - 1
WHERE ProductID = 42 AND Stock > 0;

IF @@ROWCOUNT = 0
    RAISERROR(N'Insufficient stock or product not found', 16, 1);
  

Join Update

  UPDATE o
SET o.DiscountPct = 10, o.ModifiedAt = SYSUTCDATETIME()
FROM dbo.Orders o
INNER JOIN dbo.Customers c ON o.CustomerID = c.CustomerID
WHERE c.Country = N'US' AND o.Status = N'Pending';
  

Always preview with SELECT:

  SELECT o.OrderID, o.DiscountPct
FROM dbo.Orders o
INNER JOIN dbo.Customers c ON o.CustomerID = c.CustomerID
WHERE c.Country = N'US' AND o.Status = N'Pending';
  

DELETE Patterns

  -- Simple delete
DELETE FROM dbo.Sessions WHERE ExpiresAt < SYSUTCDATETIME();

-- Join delete
DELETE oi
FROM dbo.OrderItems oi
INNER JOIN dbo.Orders o ON oi.OrderID = o.OrderID
WHERE o.Status = N'cancelled';
  

Soft Delete Alternative

  UPDATE dbo.Users SET IsDeleted = 1, DeletedAt = SYSUTCDATETIME()
WHERE UserID = 100;
  

Prefer soft deletes when audit or recovery requirements exist — index with filtered WHERE IsDeleted = 0.

MERGE (Upsert)

  MERGE dbo.Settings AS target
USING (VALUES (N'theme', N'dark'), (N'locale', N'en-US')) AS source (KeyName, Value)
ON target.KeyName = source.KeyName
WHEN MATCHED AND target.Value <> source.Value THEN
    UPDATE SET Value = source.Value, UpdatedAt = SYSUTCDATETIME()
WHEN NOT MATCHED BY TARGET THEN
    INSERT (KeyName, Value) VALUES (source.KeyName, source.Value)
WHEN NOT MATCHED BY SOURCE AND target.KeyName LIKE N'temp_%' THEN
    DELETE;
  

MERGE is powerful but has locking and bug history — test thoroughly; some teams prefer separate INSERT/UPDATE procedures.

OUTPUT Clause

  -- Audit inserts
INSERT INTO dbo.Users (Name, Email)
OUTPUT INSERTED.UserID, INSERTED.Name, INSERTED.Email, N'INSERT' AS action
INTO dbo.UserAudit (UserID, Name, Email, Action)
VALUES (N'Dave', N'[email protected]');

-- Capture deleted rows
DELETE FROM dbo.TempImport
OUTPUT DELETED.ImportID, DELETED.RawData, SYSUTCDATETIME()
INTO dbo.TempImportArchive (ImportID, RawData, DeletedAt);
  

OUTPUT cannot reference tables being modified in the same statement (except into @table or OUTPUT INTO).

Transactional CRUD

  BEGIN TRANSACTION;

BEGIN TRY
    INSERT INTO dbo.Orders (CustomerID, Total) VALUES (1, 99.99);
    DECLARE @order_id INT = SCOPE_IDENTITY();

    INSERT INTO dbo.OrderItems (OrderID, ProductID, Quantity, UnitPrice)
    VALUES (@order_id, 10, 2, 29.99), (@order_id, 11, 1, 49.99);

    UPDATE dbo.Customers SET OrderCount = OrderCount + 1 WHERE CustomerID = 1;

    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
    THROW;
END CATCH;
  

Bulk Insert

  BULK INSERT dbo.StagingProducts
FROM 'C:\import\products.csv'
WITH (
    FIRSTROW = 2,
    FIELDTERMINATOR = ',',
    ROWTERMINATOR = '\n',
    TABLOCK,
    BATCHSIZE = 10000
);
  

For ongoing ETL, SSIS or Azure Data Factory handle validation and error rows better than ad-hoc BULK INSERT.

Common Mistakes

Mistake Consequence
UPDATE/DELETE without WHERE Full table modification
No transaction on multi-step writes Partial inconsistent state
SELECT * in production APIs Breaks on schema change, wastes I/O
Large OFFSET pagination Slow deep pages
Ignoring lock escalation Blocking during bulk updates

Performance Tips

  • Batch large deletes — DELETE TOP (5000) ... WHILE @@ROWCOUNT > 0
  • Use TABLOCK hint only during maintenance windows
  • Update statistics after large data changes
  • Parameterize queries from applications — plan reuse

Production Scenario: Safe Bulk Status Update

  -- Step 1: Staging table with IDs to update
CREATE TABLE #to_update (UserID INT PRIMARY KEY);
INSERT INTO #to_update SELECT UserID FROM dbo.Users WHERE LastLoginAt < '2022-01-01';

-- Step 2: Batch update
WHILE 1 = 1
BEGIN
    UPDATE TOP (1000) u
    SET Status = N'archived'
    FROM dbo.Users u
    INNER JOIN #to_update t ON u.UserID = t.UserID
    WHERE u.Status <> N'archived';

    IF @@ROWCOUNT = 0 BREAK;
    WAITFOR DELAY '00:00:01';  -- Reduce blocking
END;
  

Troubleshooting

Symptom Cause Fix
Duplicate key on INSERT Race or missing UNIQUE Use MERGE or catch 2627
0 rows updated WHERE too restrictive Verify SELECT preview
Timeout on DELETE Lock blocking Smaller batches, off-peak
Trigger doubled rows Nested triggers Review trigger logic

Wrap related writes in transactions, preview destructive statements, and log changes with OUTPUT or temporal tables for production-grade CRUD.