Home / SQL / T-SQL / SQL Data Manipulation Commands: INSERT, UPDATE, DELETE, and MERGE Explained

SQL Data Manipulation Commands: INSERT, UPDATE, DELETE, and MERGE Explained

On

Mastering SQL data manipulation commands—INSERT, UPDATE, DELETE, and MERGE—is essential for every developer who needs to modify database content. These four Data Manipulation Language (DML) commands allow you to add new records, modify existing data, remove unwanted rows, and intelligently combine insert and update operations. In this comprehensive guide, I'll explain INSERT, UPDATE, DELETE, and MERGE using practical examples from the WideWorldImporters database, helping you understand how to safely and effectively manipulate data in SQL Server.

Data manipulation commands are the workhorses of database applications. Whether you're building a web application, creating data pipelines, or maintaining business systems, you'll use these commands daily. Understanding their syntax, behavior, and best practices is crucial for writing reliable, efficient database code.

Developer Insight: When I started learning SQL, I was terrified of data manipulation commands because mistakes can permanently alter data. The key is understanding transactions, always using WHERE clauses thoughtfully, and testing on sample data first. These commands become second nature with practice.

Understanding INSERT Command

The INSERT command adds new rows to a table. It's how you create records—new customers, new orders, new products—anything that needs to be stored in your database.


Basic INSERT Syntax

INSERT INTO table_name (column1, column2, column3)
VALUES (value1, value2, value3);

Inserting a Single Row

Let's create a test table and insert data. In real scenarios, you'd work with existing tables, but this demonstrates the syntax:

-- First, let's view existing data structure
SELECT TOP 5 
    CustomerID,
    CustomerName,
    PhoneNumber,
    FaxNumber,
    WebsiteURL
FROM Sales.Customers
ORDER BY CustomerID DESC;

-- INSERT example (conceptual - WideWorldImporters has constraints)
-- In practice, you'd insert into your own tables
INSERT INTO YourSchema.YourCustomers (CustomerName, PhoneNumber, WebsiteURL)
VALUES ('Example Corp', '(555) 123-4567', 'https://example.com');

Important: Always specify column names. While you can omit them, explicitly naming columns makes your code maintainable and prevents errors when table structures change.


Inserting Multiple Rows

SQL Server supports inserting multiple rows in a single statement:

-- Insert multiple rows at once (more efficient than separate INSERTs)
INSERT INTO YourSchema.YourProducts (ProductName, UnitPrice, SupplierID)
VALUES 
    ('Product A', 25.99, 4),
    ('Product B', 39.99, 4),
    ('Product C', 15.50, 7);

Developer Insight: Batch inserts are significantly faster than individual INSERT statements. When loading data, always batch when possible. I once optimized a data import from 45 minutes to 3 minutes just by batching 1000 inserts at a time.


INSERT with SELECT (Copying Data)

You can insert data from another table:

-- Example: Create a backup of high-value customers
-- First, you'd create a destination table
INSERT INTO YourSchema.HighValueCustomers (CustomerID, CustomerName, PhoneNumber)
SELECT 
    c.CustomerID,
    c.CustomerName,
    c.PhoneNumber
FROM Sales.Customers c
INNER JOIN Sales.Orders o ON c.CustomerID = o.CustomerID
GROUP BY c.CustomerID, c.CustomerName, c.PhoneNumber
HAVING COUNT(o.OrderID) > 50;

This pattern is incredibly useful for:

  • Creating archive tables
  • Loading data warehouses
  • Copying subsets of data
  • Creating reports tables

INSERT with Default Values

Some columns have default values or allow NULL:

-- Insert using DEFAULT keyword for columns with defaults
INSERT INTO YourSchema.YourOrders (CustomerID, OrderDate, ExpectedDeliveryDate)
VALUES (1, DEFAULT, '2024-12-15');

-- Or omit optional columns entirely
INSERT INTO YourSchema.YourCustomers (CustomerName, PhoneNumber)
VALUES ('New Customer', '(555) 987-6543');
-- Other columns will use their default values or NULL

Understanding UPDATE Command

The UPDATE command modifies existing rows in a table. It's how you correct mistakes, reflect changes in business data, or maintain current information.

Basic UPDATE Syntax

UPDATE table_name
SET column1 = value1, column2 = value2
WHERE condition;

CRITICAL WARNING: The WHERE clause is crucial. Without it, UPDATE changes every row in the table!


Updating Single Rows

-- Example: Update a specific customer's phone number
-- (Conceptual example - actual table has constraints)
UPDATE Sales.Customers
SET PhoneNumber = '(555) 999-8888'
WHERE CustomerID = 1;

Always verify your WHERE clause first:

-- STEP 1: Verify which rows will be affected
SELECT CustomerID, CustomerName, PhoneNumber
FROM Sales.Customers
WHERE CustomerID = 1;

-- STEP 2: If correct, run the UPDATE
UPDATE Sales.Customers
SET PhoneNumber = '(555) 999-8888'
WHERE CustomerID = 1;

Developer Insight: I have a personal rule: always run a SELECT with the same WHERE clause before running UPDATE or DELETE. This saved me countless times when I realized my WHERE clause was wrong before modifying data.


Updating Multiple Columns

-- Update several columns at once
UPDATE Warehouse.StockItems
SET UnitPrice = 29.99,
    RecommendedRetailPrice = 39.99,
    LastEditedWhen = SYSDATETIME()
WHERE StockItemID = 100;

UPDATE with Calculations

You can use calculations in UPDATE statements:

-- Increase all prices by 10%
UPDATE Warehouse.StockItems
SET UnitPrice = UnitPrice * 1.10,
    RecommendedRetailPrice = RecommendedRetailPrice * 1.10
WHERE SupplierID = 4;

-- Adjust stock levels based on sales
UPDATE Warehouse.StockItemHoldings
SET QuantityOnHand = QuantityOnHand - 5
WHERE StockItemID = 10
  AND QuantityOnHand >= 5;  -- Ensure we don't go negative

UPDATE with JOIN

One of SQL Server's most powerful features is updating based on data from other tables:

-- Update customer delivery cities based on their postal code
UPDATE c
SET c.DeliveryCityID = city.CityID
FROM Sales.Customers c
INNER JOIN Application.Cities city ON c.DeliveryPostalCode = city.LatestRecordedPopulation
WHERE c.DeliveryCityID IS NULL;

Real-World Example: Update order status based on delivery confirmation:

-- Mark orders as completed when delivery is confirmed
UPDATE o
SET o.PickingCompletedWhen = SYSDATETIME()
FROM Sales.Orders o
INNER JOIN (
    SELECT OrderID
    FROM Sales.Invoices
    WHERE ConfirmedDeliveryTime IS NOT NULL
) AS confirmed ON o.OrderID = confirmed.OrderID
WHERE o.PickingCompletedWhen IS NULL;

Understanding DELETE Command

The DELETE command removes rows from a table permanently (unless you rollback the transaction).

Basic DELETE Syntax

DELETE FROM table_name
WHERE condition;

CRITICAL WARNING: Just like UPDATE, omitting the WHERE clause deletes all rows in the table!


Deleting Specific Rows

-- Delete a specific customer record
DELETE FROM YourSchema.YourCustomers
WHERE CustomerID = 999;

-- Delete based on condition
DELETE FROM YourSchema.YourOrders
WHERE OrderDate < '2020-01-01'
  AND CustomerID = 500;

DELETE with JOIN

Delete rows based on related table data:

-- Delete inactive customers (those with no orders in 2 years)
DELETE c
FROM YourSchema.YourCustomers c
LEFT JOIN Sales.Orders o 
    ON c.CustomerID = o.CustomerID 
    AND o.OrderDate >= DATEADD(YEAR, -2, GETDATE())
WHERE o.OrderID IS NULL;

DELETE vs TRUNCATE

TRUNCATE is a faster way to remove all rows, but it has limitations:

-- DELETE: Removes rows one by one, can use WHERE, slower
DELETE FROM YourSchema.YourTemporaryData;

-- TRUNCATE: Removes all rows at once, much faster, no WHERE clause
TRUNCATE TABLE YourSchema.YourTemporaryData;

Key Differences:

  • TRUNCATE is faster (doesn't log individual row deletions)
  • TRUNCATE can't use WHERE (all or nothing)
  • TRUNCATE resets IDENTITY counters
  • TRUNCATE requires ALTER permission, not just DELETE
  • TRUNCATE can't be used if table has foreign key references

Developer Insight: Use TRUNCATE for clearing staging tables or temporary data where you want to remove everything. Use DELETE when you need selective removal or when foreign keys prevent TRUNCATE.


Understanding MERGE Command

MERGE combines INSERT, UPDATE, and DELETE into a single atomic operation. It compares a source to a target and performs different actions based on whether matches exist.

Basic MERGE Syntax

MERGE INTO target_table AS target
USING source_table AS source
ON target.key_column = source.key_column
WHEN MATCHED THEN
    UPDATE SET target.column = source.column
WHEN NOT MATCHED BY TARGET THEN
    INSERT (columns) VALUES (source.columns)
WHEN NOT MATCHED BY SOURCE THEN
    DELETE;

Practical MERGE Example: Synchronizing Stock Levels

-- Create a staging table with updated stock information
CREATE TABLE #StockUpdates (
    StockItemID INT,
    NewQuantity INT,
    NewReorderLevel INT
);

-- Insert some sample data
INSERT INTO #StockUpdates VALUES (1, 500, 50), (2, 300, 30);

-- MERGE to synchronize stock holdings
MERGE Warehouse.StockItemHoldings AS target
USING #StockUpdates AS source
ON target.StockItemID = source.StockItemID
WHEN MATCHED THEN
    UPDATE SET 
        target.QuantityOnHand = source.NewQuantity,
        target.ReorderLevel = source.NewReorderLevel,
        target.LastEditedWhen = SYSDATETIME()
WHEN NOT MATCHED BY TARGET THEN
    INSERT (StockItemID, QuantityOnHand, ReorderLevel)
    VALUES (source.StockItemID, source.NewQuantity, source.NewReorderLevel);

-- Clean up
DROP TABLE #StockUpdates;

MERGE for Customer Synchronization

-- Synchronize customer data from external source
MERGE Sales.Customers AS target
USING YourSchema.ExternalCustomers AS source
ON target.CustomerID = source.CustomerID
WHEN MATCHED AND (
    target.CustomerName != source.CustomerName OR
    target.PhoneNumber != source.PhoneNumber
) THEN
    UPDATE SET
        target.CustomerName = source.CustomerName,
        target.PhoneNumber = source.PhoneNumber,
        target.LastEditedWhen = SYSDATETIME()
WHEN NOT MATCHED BY TARGET THEN
    INSERT (CustomerName, PhoneNumber, WebsiteURL)
    VALUES (source.CustomerName, source.PhoneNumber, source.WebsiteURL)
WHEN NOT MATCHED BY SOURCE THEN
    DELETE;

Developer Insight: MERGE is perfect for ETL (Extract, Transform, Load) processes where you're synchronizing data from external systems. I use it extensively in data warehouse loading—one statement handles all the logic that would otherwise require separate INSERT, UPDATE, and DELETE operations.


MERGE OUTPUT Clause

Track what MERGE actually did:

MERGE Warehouse.StockItemHoldings AS target
USING #StockUpdates AS source
ON target.StockItemID = source.StockItemID
WHEN MATCHED THEN UPDATE SET target.QuantityOnHand = source.NewQuantity
WHEN NOT MATCHED BY TARGET THEN INSERT (StockItemID, QuantityOnHand) 
    VALUES (source.StockItemID, source.NewQuantity)
OUTPUT 
    $action AS Action,
    INSERTED.StockItemID,
    INSERTED.QuantityOnHand AS NewQuantity,
    DELETED.QuantityOnHand AS OldQuantity;

The OUTPUT clause shows you exactly what changed—invaluable for auditing and troubleshooting.


Transaction Management with DML Commands

All data manipulation commands should be wrapped in transactions for safety:

-- Start a transaction
BEGIN TRANSACTION;

    -- Perform data modifications
    UPDATE Warehouse.StockItems
    SET UnitPrice = UnitPrice * 1.10
    WHERE SupplierID = 4;
    
    -- Check the results
    SELECT StockItemID, StockItemName, UnitPrice
    FROM Warehouse.StockItems
    WHERE SupplierID = 4;
    
    -- If everything looks good, commit
    COMMIT TRANSACTION;
    
    -- If something is wrong, rollback
    -- ROLLBACK TRANSACTION;

Developer Insight: I never run UPDATE or DELETE in production without a transaction wrapped around it. Even if I'm 99% sure my WHERE clause is correct, that 1% has bitten me before. Test in a transaction, verify the results, then commit.


Common Mistakes to Avoid

Mistake 1: Forgetting WHERE Clause

Problem: Accidentally modifying or deleting all rows.

-- DISASTER: Updates ALL customers
UPDATE Sales.Customers
SET PhoneNumber = '(555) 000-0000';

-- CORRECT: Updates specific customer
UPDATE Sales.Customers
SET PhoneNumber = '(555) 000-0000'
WHERE CustomerID = 1;

Prevention: Always test with SELECT first, use transactions, and be paranoid about WHERE clauses.


Mistake 2: Not Checking Foreign Key Constraints

Problem: Trying to delete parent records that have child records.

-- ERROR: Can't delete customer with existing orders
DELETE FROM Sales.Customers
WHERE CustomerID = 1;
-- Error: FK constraint violation

Solution: Either delete child records first or use CASCADE DELETE (if appropriate for your business logic).


Mistake 3: Inefficient UPDATE with Subqueries

Problem: Using correlated subqueries in UPDATE that run once per row.

-- SLOW: Subquery runs for each row
UPDATE Warehouse.StockItems
SET UnitPrice = (
    SELECT AVG(UnitPrice) * 1.1
    FROM Warehouse.StockItems si2
    WHERE si2.SupplierID = StockItems.SupplierID
);

-- FASTER: Use UPDATE with JOIN
UPDATE si
SET si.UnitPrice = calc.AvgPrice * 1.1
FROM Warehouse.StockItems si
INNER JOIN (
    SELECT SupplierID, AVG(UnitPrice) AS AvgPrice
    FROM Warehouse.StockItems
    GROUP BY SupplierID
) calc ON si.SupplierID = calc.SupplierID;

Mistake 4: Not Using Transactions for Related Changes

Problem: Partial completion when multiple statements should succeed or fail together.

-- BAD: No transaction
UPDATE Sales.Orders SET OrderStatus = 'Shipped' WHERE OrderID = 1;
INSERT INTO Shipping.Shipments (OrderID, ShipDate) VALUES (1, GETDATE());
-- If second statement fails, first already committed!

-- GOOD: Use transaction
BEGIN TRANSACTION;
    UPDATE Sales.Orders SET OrderStatus = 'Shipped' WHERE OrderID = 1;
    INSERT INTO Shipping.Shipments (OrderID, ShipDate) VALUES (1, GETDATE());
COMMIT TRANSACTION;

Mistake 5: MERGE Without Proper Termination

Problem: MERGE statements must end with a semicolon.

-- WRONG: No semicolon
MERGE target USING source ON condition
WHEN MATCHED THEN UPDATE SET col = val

-- CORRECT: Include semicolon
MERGE target USING source ON condition
WHEN MATCHED THEN UPDATE SET col = val;

Best Practices for Data Manipulation

1. Always use transactions for critical data changes.

2. Test with SELECT first before UPDATE or DELETE.

3. Include WHERE clauses unless you genuinely want to affect all rows.

4. Use specific column names in INSERT statements.

5. Consider concurrency with appropriate locking hints if needed.

6. Log audit information (who changed what, when).

7. Handle errors properly with TRY-CATCH blocks.

8. Batch large operations to avoid locking tables for extended periods.

-- Example of good practices
BEGIN TRY
    BEGIN TRANSACTION;
    
    -- Audit: Log who is making changes
    DECLARE @UserID INT = 1; -- Current user
    DECLARE @ChangeDate DATETIME2 = SYSDATETIME();
    
    -- Make the change
    UPDATE Warehouse.StockItems
    SET UnitPrice = UnitPrice * 1.05,
        LastEditedBy = @UserID,
        LastEditedWhen = @ChangeDate
    WHERE SupplierID = 4
      AND UnitPrice < 100;
    
    -- Verify expected rows affected
    IF @@ROWCOUNT > 100
        THROW 50001, 'Too many rows affected', 1;
    
    COMMIT TRANSACTION;
    PRINT 'Update successful';
    
END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;
    
    PRINT 'Error: ' + ERROR_MESSAGE();
END CATCH;

Summary and Key Takeaways

Mastering SQL data manipulation commands is essential for database development. Here's what you need to remember:

INSERT adds new rows:

  • Specify column names explicitly
  • Batch multiple inserts for efficiency
  • Use INSERT...SELECT to copy data
  • Consider default values and identity columns

UPDATE modifies existing rows:

  • ALWAYS use WHERE clause (unless intentionally updating all rows)
  • Test with SELECT first
  • Can update based on joins to other tables
  • Use transactions for safety

DELETE removes rows:

  • ALWAYS use WHERE clause (unless intentionally deleting all rows)
  • Consider foreign key constraints
  • TRUNCATE is faster but less flexible
  • Test with SELECT first

MERGE combines INSERT/UPDATE/DELETE:

  • Atomic upsert operations
  • Perfect for data synchronization
  • Must end with semicolon
  • Use OUTPUT clause for auditing

Universal Best Practices:

  • Always use transactions for critical changes
  • Test in development before production
  • Consider concurrent access and locking
  • Implement proper error handling
  • Log changes for audit trails

Developer Insight: The WideWorldImporters database has excellent referential integrity constraints that prevent many common mistakes. When learning, appreciate how constraints protect data quality. In your own databases, implement similar protections. The best code prevents errors before they happen.

Next Steps: Practice these commands on test tables in WideWorldImporters. Start with simple single-row operations, then progress to batch operations and complex MERGE statements. Always use transactions and verify your WHERE clauses. The confidence to modify production data comes from disciplined practice and healthy paranoia about data integrity.


Frequently Asked Questions

Q: What's the difference between DELETE and TRUNCATE?

A: DELETE removes rows one at a time and can use a WHERE clause to selectively remove specific rows. It's logged per row and can be rolled back. TRUNCATE removes all rows at once, is much faster, resets identity counters, but cannot use WHERE clauses and requires ALTER permission instead of DELETE. TRUNCATE also can't be used if foreign keys reference the table. Use DELETE for selective removal or when foreign keys exist; use TRUNCATE to quickly clear all data from staging tables where you need maximum performance.

Q: How do I safely test UPDATE or DELETE statements in production?

A: Always wrap your statement in a transaction with explicit BEGIN TRANSACTION. Run the UPDATE or DELETE, then immediately SELECT the affected rows to verify the changes are correct. Use @@ROWCOUNT to check how many rows were affected. If everything looks correct, COMMIT TRANSACTION. If anything is wrong, ROLLBACK TRANSACTION to undo the changes. Additionally, always test your WHERE clause first by running a SELECT with the same WHERE clause to see exactly which rows will be affected.

Q: When should I use MERGE instead of separate INSERT and UPDATE statements?

A: Use MERGE when you need to synchronize data between two sources and need atomic behavior—either all changes succeed or all fail together. MERGE is ideal for ETL processes, maintaining dimension tables in data warehouses, or syncing data from external systems. It's particularly useful when you don't know in advance whether records exist (requiring UPDATE) or are new (requiring INSERT). However, for simple single-operation scenarios, plain INSERT or UPDATE statements are often clearer and easier to maintain.

Tags: