SQL - INSERT INTO Statement
• The INSERT INTO statement adds new rows to database tables using either explicit column lists or positional values, with explicit lists being safer and more maintainable in production code.
Key Insights
• The INSERT INTO statement adds new rows to database tables using either explicit column lists or positional values, with explicit lists being safer and more maintainable in production code. • Modern SQL supports batch inserts, INSERT…SELECT operations, and RETURNING clauses that significantly improve performance and reduce round-trips to the database. • Understanding transaction behavior, constraint handling, and default value mechanics is critical for building reliable data insertion logic in application architectures.
Basic INSERT Syntax
The INSERT INTO statement follows two primary patterns. The first explicitly names columns:
INSERT INTO users (username, email, created_at)
VALUES ('jsmith', 'john@example.com', CURRENT_TIMESTAMP);
The second omits column names, requiring values in table definition order:
INSERT INTO users
VALUES (1, 'jsmith', 'john@example.com', CURRENT_TIMESTAMP, NULL);
Always use explicit column lists in production code. Table schema changes break positional inserts, and explicit columns make code self-documenting. The performance difference is negligible while the maintenance cost of positional inserts is substantial.
Multiple Row Inserts
Batch inserts dramatically reduce database round-trips. Instead of executing individual INSERT statements:
-- Inefficient: 3 database calls
INSERT INTO products (sku, name, price) VALUES ('WIDGET-1', 'Widget A', 19.99);
INSERT INTO products (sku, name, price) VALUES ('WIDGET-2', 'Widget B', 24.99);
INSERT INTO products (sku, name, price) VALUES ('WIDGET-3', 'Widget C', 29.99);
Use a single statement with multiple value sets:
-- Efficient: 1 database call
INSERT INTO products (sku, name, price)
VALUES
('WIDGET-1', 'Widget A', 19.99),
('WIDGET-2', 'Widget B', 24.99),
('WIDGET-3', 'Widget C', 29.99);
Most databases limit the number of rows per batch. PostgreSQL handles thousands efficiently, while MySQL’s max_allowed_packet setting constrains total statement size. For large datasets, chunk inserts into batches of 500-1000 rows.
INSERT INTO SELECT
Copying data between tables or inserting query results uses INSERT INTO…SELECT:
-- Archive old orders
INSERT INTO orders_archive (order_id, customer_id, order_date, total)
SELECT order_id, customer_id, order_date, total
FROM orders
WHERE order_date < '2023-01-01';
This pattern is essential for ETL operations, data migrations, and denormalization strategies:
-- Populate summary table
INSERT INTO daily_sales_summary (sale_date, total_revenue, order_count)
SELECT
DATE(order_date) as sale_date,
SUM(total) as total_revenue,
COUNT(*) as order_count
FROM orders
WHERE order_date >= CURRENT_DATE - INTERVAL '1 day'
GROUP BY DATE(order_date);
The SELECT clause can include joins, aggregations, and complex transformations. The column count and types must match between the INSERT column list and SELECT output.
Default Values and NULL Handling
Columns with DEFAULT constraints or nullable definitions don’t require explicit values:
CREATE TABLE sessions (
session_id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP DEFAULT (CURRENT_TIMESTAMP + INTERVAL '24 hours'),
metadata JSONB DEFAULT '{}'
);
-- Minimal insert using defaults
INSERT INTO sessions (user_id)
VALUES (42);
Explicitly insert DEFAULT to use the column’s default value:
INSERT INTO sessions (user_id, created_at, metadata)
VALUES (42, DEFAULT, DEFAULT);
For nullable columns without defaults, omitted columns receive NULL:
CREATE TABLE profiles (
user_id INTEGER PRIMARY KEY,
bio TEXT,
avatar_url TEXT
);
-- bio and avatar_url will be NULL
INSERT INTO profiles (user_id)
VALUES (42);
RETURNING Clause
PostgreSQL, Oracle, and SQL Server support RETURNING (or OUTPUT) to retrieve inserted data in a single operation:
-- PostgreSQL
INSERT INTO orders (customer_id, total)
VALUES (123, 99.99)
RETURNING order_id, created_at;
This eliminates the need for separate SELECT queries to retrieve auto-generated IDs:
-- SQL Server syntax
INSERT INTO orders (customer_id, total)
OUTPUT INSERTED.order_id, INSERTED.created_at
VALUES (123, 99.99);
For batch inserts, RETURNING provides all inserted rows:
INSERT INTO inventory_transactions (product_id, quantity, transaction_type)
VALUES
(101, 50, 'RESTOCK'),
(102, 30, 'RESTOCK'),
(103, 75, 'RESTOCK')
RETURNING transaction_id, created_at;
Handling Conflicts and Duplicates
Modern SQL provides UPSERT capabilities through database-specific syntax. PostgreSQL uses ON CONFLICT:
INSERT INTO user_preferences (user_id, theme, language)
VALUES (42, 'dark', 'en')
ON CONFLICT (user_id)
DO UPDATE SET
theme = EXCLUDED.theme,
language = EXCLUDED.language,
updated_at = CURRENT_TIMESTAMP;
MySQL and MariaDB use ON DUPLICATE KEY UPDATE:
INSERT INTO user_preferences (user_id, theme, language)
VALUES (42, 'dark', 'en')
ON DUPLICATE KEY UPDATE
theme = VALUES(theme),
language = VALUES(language),
updated_at = CURRENT_TIMESTAMP;
SQLite uses ON CONFLICT with similar syntax to PostgreSQL:
INSERT INTO cache (key, value, expires_at)
VALUES ('user:42', '{"name":"John"}', CURRENT_TIMESTAMP + 3600)
ON CONFLICT (key)
DO UPDATE SET
value = EXCLUDED.value,
expires_at = EXCLUDED.expires_at;
Transaction Considerations
INSERT statements participate in transaction boundaries. Explicit transactions ensure atomicity for related inserts:
BEGIN TRANSACTION;
INSERT INTO orders (customer_id, total)
VALUES (123, 99.99)
RETURNING order_id INTO @order_id;
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES
(@order_id, 501, 2, 29.99),
(@order_id, 502, 1, 39.99);
COMMIT;
Without explicit transactions, each INSERT commits immediately (in autocommit mode). For multi-table inserts maintaining referential integrity, transactions are mandatory.
Constraint Violations
INSERT statements fail when violating constraints. Primary key and unique constraints prevent duplicates:
-- This will fail if email already exists
INSERT INTO users (email, username)
VALUES ('existing@example.com', 'newuser');
-- ERROR: duplicate key value violates unique constraint
Foreign key constraints require referenced values to exist:
-- Fails if customer_id 999 doesn't exist in customers table
INSERT INTO orders (customer_id, total)
VALUES (999, 99.99);
-- ERROR: insert or update on table "orders" violates foreign key constraint
Check constraints validate data before insertion:
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
price DECIMAL(10,2) CHECK (price > 0),
stock INTEGER CHECK (stock >= 0)
);
-- This fails the CHECK constraint
INSERT INTO products (price, stock)
VALUES (-10.00, 100);
-- ERROR: new row violates check constraint
Performance Optimization
For bulk inserts, disable indexes and constraints temporarily:
-- PostgreSQL approach for large data loads
BEGIN;
ALTER TABLE large_table DROP CONSTRAINT fk_constraint;
DROP INDEX idx_large_table_column;
-- Perform bulk insert
COPY large_table FROM '/data/import.csv' CSV;
CREATE INDEX idx_large_table_column ON large_table(column);
ALTER TABLE large_table ADD CONSTRAINT fk_constraint
FOREIGN KEY (ref_id) REFERENCES other_table(id);
COMMIT;
Use prepared statements for repeated inserts with different values:
-- Application code pattern (pseudo-code)
PREPARE insert_user AS
INSERT INTO users (username, email) VALUES ($1, $2);
EXECUTE insert_user('user1', 'user1@example.com');
EXECUTE insert_user('user2', 'user2@example.com');
This reduces parsing overhead and improves throughput for high-volume insert operations.
Common Patterns in Application Architecture
Application-level insert patterns should handle failures gracefully:
-- Idempotent insert pattern
INSERT INTO event_log (event_id, event_type, payload)
VALUES ('evt_123', 'user.created', '{"user_id": 42}')
ON CONFLICT (event_id) DO NOTHING;
For distributed systems, generate IDs client-side using UUIDs:
INSERT INTO distributed_entities (entity_id, data)
VALUES (gen_random_uuid(), '{"status": "active"}');
This avoids coordination overhead from auto-incrementing sequences across multiple application instances.