SQL INSERT INTO
The INSERT INTO statement is how you add new rows of data into an existing table. Every time an application creates a user account, records an order, or logs an event, there is very likely an INSERT INTO statement running behind the scenes. It looks simple at first glance, but understanding exactly how the database engine validates, orders, and commits new rows will save you from subtle bugs later on.
Overview: How INSERT INTO Works
INSERT INTO is a Data Manipulation Language (DML) statement, not a Data Definition Language (DDL) one — it adds rows to a table that must already exist, it does not create or alter the table itself. When you run an INSERT, the engine performs several steps for every row you supply:
First, it resolves the target table and, if you gave a column list, maps each value to the correct column by position. Second, for each row, it checks every constraint defined on the table: NOT NULL constraints (does every required column have a value?), PRIMARY KEY and UNIQUE constraints (does this value already exist elsewhere in the table or index?), CHECK constraints (does the value satisfy the rule?), and FOREIGN KEY constraints (does the referenced row exist in the parent table?). Third, once a row passes all checks, the engine writes it into the table’s underlying storage and updates every index defined on that table so future lookups, joins, and sorts can find the new row immediately.
In SQLite specifically, every table has a hidden rowid unless it was created with WITHOUT ROWID. If you declare a column as INTEGER PRIMARY KEY, that column becomes an alias for the rowid, and SQLite will auto-generate the next integer for you if you omit it or pass NULL — this is the SQLite equivalent of MySQL’s AUTO_INCREMENT or SQL Server’s IDENTITY.
An INSERT statement is atomic: if any single row in a multi-row insert violates a constraint, the entire statement fails and none of the rows are added (unless you explicitly use conflict-handling clauses, discussed below). This all happens inside an implicit transaction if you have not started one explicitly, so a successful INSERT is immediately visible to subsequent queries.
Syntax
There are a few common forms of INSERT INTO:
-- 1. Explicit column list (recommended)
INSERT INTO table_name (column1, column2, column3)
VALUES (value1, value2, value3);
-- 2. All columns, in table order (fragile, not recommended)
INSERT INTO table_name
VALUES (value1, value2, value3);
-- 3. Multiple rows in one statement
INSERT INTO table_name (column1, column2)
VALUES
(value1a, value2a),
(value1b, value2b);
-- 4. Insert rows produced by a query
INSERT INTO table_name (column1, column2)
SELECT columnA, columnB FROM other_table WHERE condition;
| Clause | Purpose |
|---|---|
INSERT INTO table_name |
Names the target table, which must already exist. |
(column1, column2, ...) |
Optional but strongly recommended explicit column list, in the order you will supply values. |
VALUES (...) |
One or more comma-separated value tuples; each tuple must match the column list in count and compatible type. |
SELECT ... |
An alternative to VALUES — inserts the rows returned by a query instead of literal values. |
Examples
Example 1: A single-row insert with an explicit column list
INSERT INTO employees (id, name, department, salary, hire_date)
VALUES (5, 'Dana Whitfield', 'Engineering', 95000, '2024-03-01');
SELECT * FROM employees WHERE id = 5;
Result:
| id | name | department | salary | hire_date |
|---|---|---|---|---|
| 5 | Dana Whitfield | Engineering | 95000.0 | 2024-03-01 |
The column list tells the engine exactly which value goes into which column, so the order of the VALUES tuple must match the order of the column list — not necessarily the table’s physical column order. The follow-up SELECT simply confirms the row landed correctly.
Example 2: Inserting multiple rows in one statement
INSERT INTO products (product_id, product_name, price, category)
VALUES
(10, 'USB Hub', 19.99, 'Hardware'),
(11, 'Wireless Mouse', 29.99, 'Hardware'),
(12, '4K Monitor', 349.99, 'Electronics');
SELECT * FROM products WHERE product_id IN (10, 11, 12) ORDER BY product_id;
Result:
| product_id | product_name | price | category |
|---|---|---|---|
| 10 | USB Hub | 19.99 | Hardware |
| 11 | Wireless Mouse | 29.99 | Hardware |
| 12 | 4K Monitor | 349.99 | Electronics |
Multi-row inserts are far more efficient than issuing a separate INSERT per row, because the engine parses the statement once and can batch the underlying writes and index updates, and (depending on your driver) it also means one network round trip instead of many.
Example 3: Omitted columns and DEFAULT values
CREATE TABLE books (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
author TEXT,
in_stock INTEGER DEFAULT 1
);
INSERT INTO books (id, title, author) VALUES (1, 'The Pragmatic Programmer', 'David Thomas');
INSERT INTO books (id, title, author, in_stock) VALUES (2, 'Clean Code', 'Robert C. Martin', 0);
SELECT * FROM books;
Result:
| id | title | author | in_stock |
|---|---|---|---|
| 1 | The Pragmatic Programmer | David Thomas | 1 |
| 2 | Clean Code | Robert C. Martin | 0 |
Any column you leave out of the column list gets its DEFAULT value (or NULL if no default is defined and the column allows nulls). The first insert omits in_stock, so it falls back to the table’s default of 1; the second insert supplies it explicitly as 0.
Example 4: Inserting rows produced by a SELECT query
CREATE TABLE staging_orders (
order_id INTEGER,
customer_id INTEGER,
amount REAL,
status TEXT
);
INSERT INTO staging_orders (order_id, customer_id, amount, status)
VALUES
(1, 1, 250.00, 'approved'),
(2, 2, 80.00, 'pending'),
(3, 1, 120.00, 'approved');
CREATE TABLE approved_orders (
order_id INTEGER,
customer_id INTEGER,
amount REAL
);
INSERT INTO approved_orders (order_id, customer_id, amount)
SELECT order_id, customer_id, amount
FROM staging_orders
WHERE status = 'approved';
SELECT * FROM approved_orders ORDER BY order_id;
Result:
| order_id | customer_id | amount |
|---|---|---|
| 1 | 1 | 250.0 |
| 3 | 1 | 120.0 |
This is INSERT INTO ... SELECT, commonly used to copy, filter, or archive data between tables without pulling it into application code first. Here only the rows with status = 'approved' are copied into approved_orders.
How It Works Step by Step (Under the Hood)
For a plain INSERT INTO ... VALUES statement, the engine works through each value tuple in order: validate column count matches, coerce/check types against column affinity, check constraints, write the row, update indexes, move to the next tuple. If any tuple fails, SQLite aborts the whole statement (no partial insert survives) unless you have overridden this with a conflict-resolution clause such as INSERT OR IGNORE or INSERT OR REPLACE (SQLite-specific; the portable equivalents are MySQL’s INSERT IGNORE / ON DUPLICATE KEY UPDATE and PostgreSQL’s ON CONFLICT ... DO UPDATE, both of which implement "upsert" behavior).
For INSERT INTO ... SELECT, the engine first fully evaluates the SELECT using its own logical processing order — FROM/JOIN, then WHERE, then GROUP BY, then HAVING, then the SELECT column list, then ORDER BY, then any LIMIT — to produce a result set. Only once that result set is determined does the engine begin feeding those rows, one at a time, through the same constraint-checking and index-updating pipeline used for a literal VALUES insert. This means the source query can reference the target table too, as long as it does not create infinite recursion, and it means any WHERE filtering on the source happens before a single row is written to the destination.
Common Mistakes
Mistake 1: Column list and value count don’t match
INSERT INTO products (product_id, product_name, price)
VALUES (13, 'Keyboard');
This fails because the column list has three columns but the VALUES tuple only supplies two values. Every value tuple must have exactly as many entries as the column list (or as many as the table has, if you omitted the column list).
INSERT INTO products (product_id, product_name, price, category)
VALUES (13, 'Keyboard', 49.99, 'Hardware');
The corrected version supplies a value for every named column, so it inserts cleanly.
Mistake 2: Forgetting to quote string literals
INSERT INTO customers (customer_id, customer_name, country)
VALUES (4, Acme Corp, USA);
Without quotes, Acme Corp is parsed as two bare identifiers rather than a string, which is a syntax error the moment there’s a space in an unquoted value. String and date literals must always be wrapped in single quotes.
INSERT INTO customers (customer_id, customer_name, country)
VALUES (4, 'Acme Corp', 'USA');
Quoting 'Acme Corp' and 'USA' fixes the syntax and inserts the row correctly.
Best Practices
- Always specify an explicit column list, even when inserting into every column — it protects your statement from breaking silently if someone later adds, removes, or reorders a column.
- Prefer a single multi-row
INSERTover many single-row statements when loading bulk data; it is significantly faster because parsing and index maintenance overhead is amortized across rows. - Wrap large batch inserts in an explicit transaction (
BEGIN ... COMMIT) so you get one atomic commit and one disk sync instead of one per row. - Use parameterized queries (bound parameters) from application code instead of concatenating strings into SQL — this prevents SQL injection and handles quoting/escaping for you automatically.
- Let the database generate surrogate primary keys (
INTEGER PRIMARY KEYin SQLite,AUTO_INCREMENTin MySQL,IDENTITYin SQL Server,SERIAL/GENERATED ALWAYS AS IDENTITYin PostgreSQL) rather than computing IDs yourself. - When copying data with
INSERT INTO ... SELECT, run theSELECTalone first to confirm it returns exactly the rows you expect before attaching theINSERT INTO. - Define
NOT NULL,CHECK, and foreign key constraints on your tables so bad data is rejected by the engine at insert time rather than discovered later.
Practice Exercises
Exercise 1: Write an INSERT INTO statement that adds a new employee named ‘Priya Nair’ to the Marketing department with a salary of 72000 and a hire date of ‘2024-06-15’. Use an explicit column list.
Exercise 2: Using a single statement, insert two new customers at once: one from ‘Germany’ and one from ‘Japan’. Pick your own customer_id and customer_name values, and verify the rows with a follow-up SELECT.
Exercise 3: Create a table named high_value_orders with columns order_id, customer_id, and amount. Then use INSERT INTO ... SELECT to copy every row from the orders table where amount is greater than 100 into your new table. What rows do you expect to see?
Summary
INSERT INTOadds new rows to an existing table; it does not create the table.- Always prefer an explicit column list so values map to the correct columns regardless of table structure changes.
- You can insert one row, many rows in one statement, or rows produced by a
SELECTquery (INSERT INTO ... SELECT). - Columns left out of the column list receive their
DEFAULTvalue orNULL. - The engine checks
NOT NULL,PRIMARY KEY/UNIQUE,CHECK, and foreign key constraints per row, and by default the whole statement fails atomically if any row is invalid. - For
INSERT INTO ... SELECT, the source query is fully evaluated through its own logical order before any row is written to the target table. - Batch multi-row inserts and wrap bulk loads in a transaction for performance.
