Data Manipulation: INSERT, UPDATE, DELETE
Learning Objectives
- Master INSERT statements for adding data
- Use UPDATE statements safely and efficiently
- Perform DELETE operations with proper conditions
- Handle bulk operations and imports
- Understand transaction safety for DML operations
- Work with INSERT...SELECT and REPLACE statements
- Implement upsert operations and conflict handling
DML Commands Overview
Data Manipulation Language (DML) commands are used to manage data within database tables. These are the CRUD operations that power every application! 💾
DML vs DDL Key Difference
INSERT Statements
graph LR
A[INSERT INTO] --> B[Single Row]
A --> C[Multiple Rows]
A --> D[INSERT SELECT]
A --> E[INSERT IGNORE]
A --> F[ON DUPLICATE KEY]
A --> G[REPLACE INTO]
style A fill:#dcfce7
style B fill:#dbeafe
style C fill:#dbeafe
style D fill:#fef3c7
style E fill:#fee2e2
style F fill:#e9d5ff
style G fill:#e0f2fe
INSERT Statement Variations
-- ========================================
-- BASIC INSERT STATEMENTS
-- ========================================
-- Insert single row with all columns
INSERT INTO users (username, email, password_hash, created_at)
VALUES ('johndoe', 'john@example.com', 'hashed_password_here', NOW());
-- Insert with column names (recommended)
INSERT INTO products (name, price, stock_quantity)
VALUES ('Laptop', 999.99, 50);
-- Insert without column names (not recommended)
INSERT INTO categories
VALUES (NULL, 'Electronics', 'electronics', 'Electronic devices', NULL);
-- Insert with DEFAULT values
INSERT INTO users (username, email, password_hash)
VALUES ('janedoe', 'jane@example.com', DEFAULT); -- Uses column default
-- Insert NULL values
INSERT INTO products (name, price, description)
VALUES ('Mystery Item', 0.01, NULL);
-- ========================================
-- MULTIPLE ROW INSERTS
-- ========================================
-- Insert multiple rows in one statement (faster)
INSERT INTO products (name, price, stock_quantity) VALUES
('iPhone 15', 1299.00, 100),
('Samsung Galaxy', 999.00, 75),
('Google Pixel', 799.00, 50),
('OnePlus', 699.00, 30);
-- Insert with mixed values
INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES
(1001, 5, 2, 29.99),
(1001, 8, 1, 149.99),
(1002, 5, 3, 29.99),
(1002, 12, 1, 599.99);
-- ========================================
-- INSERT...SELECT
-- ========================================
-- Copy data from another table
INSERT INTO users_archive
SELECT * FROM users
WHERE created_at < '2024-01-01';
-- Insert with transformation
INSERT INTO customer_summary (customer_id, total_orders, total_spent)
SELECT
customer_id,
COUNT(order_id) as total_orders,
SUM(total_amount) as total_spent
FROM orders
GROUP BY customer_id;
-- Insert from multiple tables
INSERT INTO order_report (order_date, customer_name, total_amount)
SELECT
o.order_date,
c.customer_name,
o.total_amount
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.status = 'completed';
-- ========================================
-- INSERT IGNORE (Skip errors)
-- ========================================
-- Ignore duplicate key errors
INSERT IGNORE INTO tags (name, slug) VALUES
('Technology', 'technology'), -- Will insert
('Technology', 'technology'), -- Will be ignored (duplicate)
('Science', 'science'); -- Will insert
-- Check how many rows were actually inserted
SELECT ROW_COUNT(); -- Returns number of inserted rows
-- ========================================
-- ON DUPLICATE KEY UPDATE (Upsert)
-- ========================================
-- Update if exists, insert if not
INSERT INTO product_views (product_id, view_count, last_viewed)
VALUES (101, 1, NOW())
ON DUPLICATE KEY UPDATE
view_count = view_count + 1,
last_viewed = NOW();
-- More complex upsert
INSERT INTO user_stats (user_id, login_count, last_login, total_purchases)
VALUES (123, 1, NOW(), 0)
ON DUPLICATE KEY UPDATE
login_count = login_count + 1,
last_login = VALUES(last_login), -- Use the new value
total_purchases = total_purchases; -- Keep existing value
-- Bulk upsert
INSERT INTO inventory (product_id, quantity, updated_at) VALUES
(1, 100, NOW()),
(2, 150, NOW()),
(3, 200, NOW())
ON DUPLICATE KEY UPDATE
quantity = VALUES(quantity),
updated_at = VALUES(updated_at);
-- ========================================
-- REPLACE INTO (Delete + Insert)
-- ========================================
-- Replace deletes existing row and inserts new
REPLACE INTO settings (user_id, setting_key, setting_value)
VALUES (1, 'theme', 'dark');
-- Multiple replace
REPLACE INTO cache_table (cache_key, cache_value, expires_at) VALUES
('user_1_profile', '{"name":"John"}', DATE_ADD(NOW(), INTERVAL 1 HOUR)),
('user_2_profile', '{"name":"Jane"}', DATE_ADD(NOW(), INTERVAL 1 HOUR));
-- ========================================
-- INSERT WITH SUBQUERIES
-- ========================================
-- Insert with calculated values
INSERT INTO monthly_reports (month, year, total_sales, avg_order_value)
SELECT
MONTH(order_date) as month,
YEAR(order_date) as year,
SUM(total_amount) as total_sales,
AVG(total_amount) as avg_order_value
FROM orders
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 1 MONTH)
GROUP BY YEAR(order_date), MONTH(order_date);
-- Insert with EXISTS check
INSERT INTO notifications (user_id, message, created_at)
SELECT
u.user_id,
'Your order has shipped!',
NOW()
FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.user_id
AND o.status = 'shipped'
AND o.notified = FALSE
);
-- ========================================
-- PREPARED STATEMENTS (For security)
-- ========================================
-- Prepare statement
PREPARE insert_user FROM
'INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)';
-- Execute with values
SET @username = 'newuser';
SET @email = 'new@example.com';
SET @password = 'hashed_password';
EXECUTE insert_user USING @username, @email, @password;
-- Deallocate
DEALLOCATE PREPARE insert_user;
INSERT Best Practices
UPDATE Statements
UPDATE Statement Examples
-- ========================================
-- BASIC UPDATE STATEMENTS
-- ========================================
-- Update single column
UPDATE users
SET last_login = NOW()
WHERE user_id = 123;
-- Update multiple columns
UPDATE products
SET
price = 899.99,
stock_quantity = 100,
updated_at = NOW()
WHERE product_id = 456;
-- Update with arithmetic
UPDATE products
SET
price = price * 1.10, -- 10% price increase
stock_quantity = stock_quantity - 5
WHERE category_id = 3;
-- Update with CASE statement
UPDATE orders
SET status = CASE
WHEN total_amount < 100 THEN 'small_order'
WHEN total_amount < 500 THEN 'medium_order'
ELSE 'large_order'
END
WHERE status IS NULL;
-- ========================================
-- UPDATE WITH CONDITIONS
-- ========================================
-- Complex WHERE conditions
UPDATE users
SET
account_status = 'inactive',
deactivated_at = NOW()
WHERE
last_login < DATE_SUB(NOW(), INTERVAL 6 MONTH)
AND account_status = 'active'
AND user_type != 'admin';
-- Update with subquery in WHERE
UPDATE products
SET is_featured = TRUE
WHERE product_id IN (
SELECT product_id
FROM order_items
GROUP BY product_id
ORDER BY SUM(quantity) DESC
LIMIT 10
);
-- Update with EXISTS
UPDATE customers c
SET is_vip = TRUE
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
GROUP BY o.customer_id
HAVING SUM(total_amount) > 10000
);
-- ========================================
-- UPDATE WITH JOINS
-- ========================================
-- Update with INNER JOIN
UPDATE orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
SET
o.customer_type = c.customer_type,
o.customer_region = c.region
WHERE o.customer_type IS NULL;
-- Update with LEFT JOIN
UPDATE products p
LEFT JOIN product_reviews pr ON p.product_id = pr.product_id
SET p.average_rating = (
SELECT AVG(rating)
FROM product_reviews
WHERE product_id = p.product_id
)
WHERE pr.product_id IS NOT NULL;
-- Multiple table update
UPDATE
orders o,
order_items oi,
products p
SET
o.status = 'out_of_stock',
oi.status = 'cancelled',
p.stock_quantity = 0
WHERE
o.order_id = oi.order_id
AND oi.product_id = p.product_id
AND p.discontinued = TRUE;
-- ========================================
-- UPDATE WITH LIMIT AND ORDER BY
-- ========================================
-- Update only first 10 rows
UPDATE products
SET discount_percent = 20
WHERE category_id = 5
ORDER BY created_at DESC
LIMIT 10;
-- Update oldest records
UPDATE user_logs
SET archived = TRUE
WHERE created_at < '2024-01-01'
ORDER BY created_at ASC
LIMIT 1000;
-- ========================================
-- SAFE UPDATE PRACTICES
-- ========================================
-- Always use transactions for important updates
START TRANSACTION;
UPDATE accounts
SET balance = balance - 100
WHERE account_id = 1001;
UPDATE accounts
SET balance = balance + 100
WHERE account_id = 1002;
-- Check the changes
SELECT * FROM accounts WHERE account_id IN (1001, 1002);
-- If correct:
COMMIT;
-- If wrong:
-- ROLLBACK;
-- Test with SELECT first
SELECT * FROM users
WHERE last_login < DATE_SUB(NOW(), INTERVAL 1 YEAR);
-- Then UPDATE
UPDATE users
SET status = 'dormant'
WHERE last_login < DATE_SUB(NOW(), INTERVAL 1 YEAR);
-- ========================================
-- UPDATE WITH VALUES FROM ANOTHER TABLE
-- ========================================
-- Update from temporary table
CREATE TEMPORARY TABLE price_updates (
product_id INT,
new_price DECIMAL(10,2)
);
INSERT INTO price_updates VALUES
(1, 29.99),
(2, 39.99),
(3, 49.99);
UPDATE products p
INNER JOIN price_updates pu ON p.product_id = pu.product_id
SET p.price = pu.new_price;
-- Update with correlated subquery
UPDATE orders o
SET total_amount = (
SELECT SUM(quantity * unit_price)
FROM order_items oi
WHERE oi.order_id = o.order_id
)
WHERE status = 'pending';
-- ========================================
-- BULK UPDATE OPTIMIZATION
-- ========================================
-- Use CASE for different updates
UPDATE users
SET role = CASE user_id
WHEN 1 THEN 'admin'
WHEN 2 THEN 'editor'
WHEN 3 THEN 'author'
ELSE role
END
WHERE user_id IN (1, 2, 3);
-- Batch updates with prepared statements
PREPARE update_price FROM
'UPDATE products SET price = ? WHERE product_id = ?';
SET @price1 = 99.99; SET @id1 = 1;
EXECUTE update_price USING @price1, @id1;
SET @price2 = 149.99; SET @id2 = 2;
EXECUTE update_price USING @price2, @id2;
DEALLOCATE PREPARE update_price;
DELETE Statements
DELETE Command Warning
DELETE Statement Variations
-- ========================================
-- BASIC DELETE STATEMENTS
-- ========================================
-- Delete single row
DELETE FROM users
WHERE user_id = 999;
-- Delete with multiple conditions
DELETE FROM orders
WHERE
status = 'cancelled'
AND created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);
-- Delete with IN clause
DELETE FROM products
WHERE product_id IN (101, 102, 103);
-- Delete with subquery
DELETE FROM comments
WHERE post_id IN (
SELECT post_id
FROM posts
WHERE status = 'deleted'
);
-- ========================================
-- DELETE WITH LIMIT
-- ========================================
-- Delete only first N rows
DELETE FROM user_logs
WHERE created_at < '2024-01-01'
ORDER BY created_at ASC
LIMIT 1000;
-- Delete oldest records in batches
DELETE FROM activity_log
WHERE log_date < DATE_SUB(NOW(), INTERVAL 90 DAY)
ORDER BY log_date ASC
LIMIT 10000;
-- ========================================
-- DELETE WITH JOINS
-- ========================================
-- Delete with INNER JOIN
DELETE o
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
WHERE c.status = 'banned';
-- Delete from multiple tables
DELETE o, oi
FROM orders o
INNER JOIN order_items oi ON o.order_id = oi.order_id
WHERE o.created_at < '2023-01-01';
-- Delete with LEFT JOIN (delete orphaned records)
DELETE oi
FROM order_items oi
LEFT JOIN orders o ON oi.order_id = o.order_id
WHERE o.order_id IS NULL;
-- ========================================
-- SAFE DELETE PRACTICES
-- ========================================
-- Always test with SELECT first
SELECT COUNT(*)
FROM users
WHERE last_login < DATE_SUB(NOW(), INTERVAL 2 YEAR);
-- Then delete
DELETE FROM users
WHERE last_login < DATE_SUB(NOW(), INTERVAL 2 YEAR);
-- Use transactions for critical deletes
START TRANSACTION;
-- Check what will be deleted
SELECT * FROM orders WHERE customer_id = 123;
SELECT * FROM order_items WHERE order_id IN (
SELECT order_id FROM orders WHERE customer_id = 123
);
-- Perform deletion
DELETE FROM order_items WHERE order_id IN (
SELECT order_id FROM orders WHERE customer_id = 123
);
DELETE FROM orders WHERE customer_id = 123;
DELETE FROM customers WHERE customer_id = 123;
-- Verify
SELECT COUNT(*) FROM orders WHERE customer_id = 123;
COMMIT; -- or ROLLBACK if something went wrong
-- ========================================
-- SOFT DELETE PATTERN
-- ========================================
-- Instead of DELETE, use UPDATE with deleted flag
ALTER TABLE users ADD COLUMN deleted_at TIMESTAMP NULL;
-- Soft delete
UPDATE users
SET deleted_at = NOW()
WHERE user_id = 123;
-- Query only active records
SELECT * FROM users
WHERE deleted_at IS NULL;
-- Create view for active records
CREATE VIEW active_users AS
SELECT * FROM users
WHERE deleted_at IS NULL;
-- Restore soft deleted record
UPDATE users
SET deleted_at = NULL
WHERE user_id = 123;
-- Permanently delete old soft-deleted records
DELETE FROM users
WHERE deleted_at < DATE_SUB(NOW(), INTERVAL 90 DAY);
-- ========================================
-- TRUNCATE vs DELETE
-- ========================================
-- DELETE: Slow for large tables, can rollback, triggers fire
DELETE FROM large_table; -- Deletes row by row
-- TRUNCATE: Fast, cannot rollback, no triggers, resets AUTO_INCREMENT
TRUNCATE TABLE large_table; -- Drops and recreates table
-- DELETE with specific conditions
DELETE FROM logs WHERE created_at < '2024-01-01';
-- TRUNCATE for complete cleanup (be very careful!)
TRUNCATE TABLE temporary_data;
-- ========================================
-- CASCADE DELETE
-- ========================================
-- Setup foreign key with CASCADE DELETE
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT,
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
ON DELETE CASCADE -- Automatically deletes orders when customer deleted
);
-- When you delete parent, children are auto-deleted
DELETE FROM customers WHERE customer_id = 100;
-- This automatically deletes all orders for customer 100
-- ========================================
-- ARCHIVING BEFORE DELETION
-- ========================================
-- Archive data before deleting
CREATE TABLE orders_archive LIKE orders;
-- Copy old data to archive
INSERT INTO orders_archive
SELECT * FROM orders
WHERE order_date < DATE_SUB(NOW(), INTERVAL 2 YEAR);
-- Delete archived data from main table
DELETE FROM orders
WHERE order_date < DATE_SUB(NOW(), INTERVAL 2 YEAR);
-- ========================================
-- PERFORMANCE OPTIMIZATION
-- ========================================
-- Delete in smaller batches for large datasets
DELIMITER $$
CREATE PROCEDURE delete_old_logs()
BEGIN
DECLARE rows_affected INT DEFAULT 1;
WHILE rows_affected > 0 DO
DELETE FROM logs
WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)
LIMIT 1000;
SET rows_affected = ROW_COUNT();
-- Optional: Add delay to reduce load
DO SLEEP(0.1);
END WHILE;
END$$
DELIMITER ;
CALL delete_old_logs();
Bulk Operations and Performance
Performance Best Practices
-- ========================================
-- BULK INSERT OPTIMIZATION
-- ========================================
-- Disable indexes temporarily for bulk insert
ALTER TABLE large_table DISABLE KEYS;
INSERT INTO large_table (col1, col2, col3) VALUES
-- Insert thousands of rows
(val1, val2, val3),
(val4, val5, val6),
-- ... many more rows
(valN, valN+1, valN+2);
ALTER TABLE large_table ENABLE KEYS;
-- Use LOAD DATA INFILE for CSV imports
LOAD DATA INFILE '/tmp/data.csv'
INTO TABLE products
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS -- Skip header
(name, @price, @stock, description)
SET
price = CAST(@price AS DECIMAL(10,2)),
stock_quantity = CAST(@stock AS UNSIGNED),
created_at = NOW();
-- ========================================
-- BATCH PROCESSING
-- ========================================
-- Process updates in batches
SET @batch_size = 1000;
SET @offset = 0;
SET @total_rows = (SELECT COUNT(*) FROM users);
WHILE @offset < @total_rows DO
UPDATE users
SET processed = TRUE
WHERE processed = FALSE
LIMIT @batch_size;
SET @offset = @offset + @batch_size;
-- Optional delay to prevent locking
DO SLEEP(0.5);
END WHILE;
-- ========================================
-- MONITORING DML PERFORMANCE
-- ========================================
-- Check affected rows
UPDATE products SET price = price * 1.1 WHERE category_id = 5;
SELECT ROW_COUNT(); -- Number of rows updated
-- Profile query execution
SET profiling = 1;
DELETE FROM old_logs WHERE created_at < '2023-01-01';
SHOW PROFILES;
SHOW PROFILE FOR QUERY 1;
-- Check table locks
SHOW PROCESSLIST;
SHOW ENGINE INNODB STATUS;
-- ========================================
-- OPTIMIZATION TIPS
-- ========================================
/*
1. Bulk Operations:
- Use multi-row INSERT instead of multiple single INSERTs
- Use INSERT...SELECT for table-to-table copies
- Use LOAD DATA INFILE for large CSV imports
2. Updates:
- Update indexes columns sparingly
- Use JOIN instead of subqueries when possible
- Batch large updates to avoid long locks
3. Deletes:
- Archive before deleting
- Use soft deletes for important data
- Delete in small batches for large datasets
4. General:
- Use transactions for data consistency
- Test with EXPLAIN before running
- Monitor slow query log
- Keep statistics updated with ANALYZE TABLE
*/
Practice Exercise
E-Commerce Data Management
💡 Click for Solution
-- 1. Insert 5 new products
INSERT INTO products (name, category_id, price, stock_quantity, description) VALUES
('Wireless Mouse', 1, 29.99, 150, 'Ergonomic wireless mouse with USB receiver'),
('Mechanical Keyboard', 1, 89.99, 75, 'RGB backlit mechanical keyboard'),
('USB-C Hub', 1, 49.99, 200, '7-in-1 USB-C hub with HDMI'),
('Laptop Stand', 2, 39.99, 100, 'Adjustable aluminum laptop stand'),
('Webcam HD', 3, 79.99, 50, '1080p HD webcam with microphone');
-- 2. Update prices with 10% discount
START TRANSACTION;
-- First check what will be updated
SELECT product_id, name, price, price * 0.9 as new_price
FROM products
WHERE category_id = 1;
-- Apply discount
UPDATE products
SET
price = ROUND(price * 0.9, 2),
discount_percent = 10,
updated_at = NOW()
WHERE category_id = 1;
COMMIT;
-- 3. Insert an order with multiple items
START TRANSACTION;
-- Insert order
INSERT INTO orders (customer_id, order_date, status, total_amount)
VALUES (123, NOW(), 'pending', 0);
SET @order_id = LAST_INSERT_ID();
-- Insert order items
INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES
(@order_id, 1, 2, 29.99),
(@order_id, 2, 1, 89.99),
(@order_id, 3, 3, 49.99);
-- Update order total
UPDATE orders o
SET total_amount = (
SELECT SUM(quantity * unit_price)
FROM order_items oi
WHERE oi.order_id = o.order_id
)
WHERE order_id = @order_id;
COMMIT;
-- 4. Update stock after order
UPDATE products p
INNER JOIN order_items oi ON p.product_id = oi.product_id
SET p.stock_quantity = p.stock_quantity - oi.quantity
WHERE oi.order_id = @order_id;
-- Check for low stock
SELECT product_id, name, stock_quantity
FROM products
WHERE stock_quantity < 10;
-- 5. Delete cancelled orders older than 30 days
START TRANSACTION;
-- Archive before deleting
CREATE TABLE IF NOT EXISTS orders_archive LIKE orders;
INSERT INTO orders_archive
SELECT * FROM orders
WHERE status = 'cancelled'
AND order_date < DATE_SUB(NOW(), INTERVAL 30 DAY);
-- Delete order items first (or use CASCADE)
DELETE oi FROM order_items oi
INNER JOIN orders o ON oi.order_id = o.order_id
WHERE o.status = 'cancelled'
AND o.order_date < DATE_SUB(NOW(), INTERVAL 30 DAY);
-- Delete orders
DELETE FROM orders
WHERE status = 'cancelled'
AND order_date < DATE_SUB(NOW(), INTERVAL 30 DAY);
SELECT ROW_COUNT() as deleted_orders;
COMMIT;
-- 6. Implement soft delete for customers
-- Add soft delete column
ALTER TABLE customers
ADD COLUMN deleted_at TIMESTAMP NULL,
ADD INDEX idx_deleted (deleted_at);
-- Soft delete a customer
UPDATE customers
SET
deleted_at = NOW(),
status = 'deleted'
WHERE customer_id = 999;
-- Create view for active customers
CREATE OR REPLACE VIEW active_customers AS
SELECT * FROM customers
WHERE deleted_at IS NULL;
-- Query only active customers
SELECT * FROM active_customers;
-- 7. Create upsert for product views
-- Create table if not exists
CREATE TABLE IF NOT EXISTS product_views (
product_id INT PRIMARY KEY,
view_count INT DEFAULT 1,
last_viewed TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
-- Upsert operation
INSERT INTO product_views (product_id, view_count, last_viewed)
VALUES (1, 1, NOW())
ON DUPLICATE KEY UPDATE
view_count = view_count + 1,
last_viewed = NOW();
-- Test multiple views
INSERT INTO product_views (product_id, view_count, last_viewed) VALUES
(1, 1, NOW()),
(2, 1, NOW()),
(1, 1, NOW()), -- Will increment product 1
(3, 1, NOW())
ON DUPLICATE KEY UPDATE
view_count = view_count + VALUES(view_count),
last_viewed = VALUES(last_viewed);
-- Check results
SELECT p.name, pv.view_count, pv.last_viewed
FROM products p
LEFT JOIN product_views pv ON p.product_id = pv.product_id
ORDER BY pv.view_count DESC;