Write one INSERT and put a parenthesised list per row after VALUES, separated by commas: INSERT INTO users (id, name) VALUES (1, 'Ada'), (2, 'Grace'), (3, 'Linus');. It works in SQLite, PostgreSQL, MySQL and SQL Server, and the statement either inserts every row or none of them.
Running one INSERT per row works, but it is the slowest way to load data and the noisiest to read. Standard SQL lets a single INSERT carry many rows: name the columns once, then give one list of values per row. The sections below show that form, how columns you leave out get their defaults, how to see the ids the database assigned, how to speed up row-by-row inserts with a transaction, and the older UNION ALL form you will still find in legacy code. If the rows already live in another table, you don't need VALUES at all: see insert from a SELECT. The examples run on SQLite; the notes say where PostgreSQL, MySQL and SQL Server spell something differently. Hit Run on any of them, then edit the rows and run again.
1VALUES (…), (…), (…): many rows in one INSERTRecommended
After the column list, write VALUES once and then one parenthesised row per line, with a comma between rows and a semicolon only at the very end. Each row lists its values in the same order as the column list. SELECT changes() is SQLite's count of the rows the last statement touched, which confirms that one statement added all three.
Output
rows_added is 3, and the table holds Ada, Grace and Linus. The same statement runs unchanged on PostgreSQL, MySQL and SQL Server (2008 and later). SQLite has accepted it since 3.7.11 (2012); this page runs 3.53.4. In MySQL, ROW_COUNT() plays the part of changes(), and SQL Server prints (3 rows affected) by itself. Oracle accepts multi-row VALUES only from 23ai; older versions use INSERT ALL or the UNION ALL form in section 5.
2Name the columns, let the rest take their defaults
You only have to list the columns you are filling. Every column you leave out gets its DEFAULT, or NULL if it has none, and an INTEGER PRIMARY KEY gets the next id. The catch with a multi-row insert is that every row fills the same columns. When one row needs a value the others don't, either add the column and write the default out for the other rows, or split the rows into two statements, as here.
Output
The first three tasks got ids 1 to 3, the default status todo and a NULL due date. In the second statement, Review PR repeats 'todo' by hand because its row has to fill status too. PostgreSQL, MySQL and SQL Server let you write the keyword DEFAULT in that slot instead, as in ('Alan', DEFAULT); all three ran it. SQLite doesn't: it rejects the statement with near "DEFAULT": syntax error. Every row must also have the same number of values as the column list, or SQLite refuses the whole statement with 3 values for 2 columns or all VALUES must have the same number of terms.
3RETURNING: see the ids of the rows you inserted
When the database assigns the ids, you usually need them back, for example to insert child rows that point at the new parents. Add RETURNING and a column list to the end of the INSERT, and it gives back one row per inserted row, like a SELECT. SQLite has had it since 3.35. The table already holds one tag, so the new rows start at id 2.
Output
The insert returns ids 2, 3 and 4, each next to its name. PostgreSQL uses the same RETURNING clause. SQL Server puts an OUTPUT clause between the column list and VALUES: INSERT INTO users (name) OUTPUT INSERTED.id, INSERTED.name VALUES (…), (…). MySQL has no RETURNING (8.4 rejects it with a syntax error); after a multi-row insert, LAST_INSERT_ID() gives the id of the first new row and ROW_COUNT() the number of rows. More on single rows in get the id of an inserted row.
4Many single INSERTs inside one transaction
Sometimes rows reach you one at a time: a loop in your application, a CSV read line by line, a script of generated statements. Run each INSERT on its own and the database treats each one as a separate transaction, committing (and on a file database, syncing to disk) after every row. That per-row commit is usually what makes bulk loads slow. Wrap the batch in BEGIN … COMMIT and the database commits once for the whole lot. As a bonus, the batch becomes all-or-nothing: ROLLBACK undoes every row since BEGIN.
Output
The three committed events are there; the two test rows are gone because their transaction was rolled back. This page uses an in-memory database, so it can't show the speed difference, but on a real file or server database the gap between autocommit and one transaction grows with every row. In application code, combine this with a prepared statement that you run once per row, or send the rows in multi-row VALUES chunks inside the transaction. PostgreSQL and MySQL accept BEGIN as written (MySQL also has START TRANSACTION), and SQL Server uses BEGIN TRANSACTION … COMMIT; all three ran it.
5The old form: INSERT … SELECT … UNION ALL SELECT …
Before multi-row VALUES reached every database (SQLite before 3.7.11, SQL Server before 2008, Oracle before 23ai), the usual trick was to build the rows as a query. Each row is a SELECT of literal values, the rows are joined with UNION ALL, and INSERT INTO … SELECT stores the result. It still works everywhere, so you will meet it in older scripts and answers. Use UNION ALL, not plain UNION: the second statement shows why.
Output
The UNION ALL insert stores the same three rows as section 1. The UNION insert offered Alan twice but stored him once (id 4), because UNION removes duplicate rows before the insert sees them; that silently loses data when two real rows happen to be equal. This form has a size limit in SQLite: a compound SELECT of 500 terms ran, but 501 failed with too many terms in compound SELECT. A 5,000-row VALUES list ran without complaint, which is one more reason to prefer section 1. In Oracle, each SELECT needs FROM dual.
6Which should you use?
| Method | Works in | Best for |
|---|---|---|
| INSERT … VALUES (…), (…), (…) | SQLite, PostgreSQL, MySQL, SQL Server | Rows you write out or generate: the default choice |
| INSERT … VALUES … RETURNING id | SQLite 3.35+, PostgreSQL | Getting the new ids back (OUTPUT INSERTED.id on SQL Server) |
| BEGIN; INSERT …; INSERT …; COMMIT; | Everywhere | Rows that arrive one at a time from application code |
| INSERT … SELECT … UNION ALL SELECT … | Everywhere, including old versions | SQLite before 3.7.11, SQL Server 2005, older Oracle |
| INSERT … SELECT … FROM other_table | Everywhere | Rows that already live in another table |
7When one row fails, the whole statement is rolled back
A multi-row INSERT is a single statement, and a statement either succeeds completely or changes nothing. If any row breaks a constraint (a duplicate key, a NULL in a NOT NULL column, a failed CHECK), the rows before it in the same list are undone too. Here the third row repeats an email that is already in the table.
Output
SQLite reports UNIQUE constraint failed: users.email, and the table still holds only [email protected]: grace and linus were valid, but they went down with the statement. PostgreSQL, MySQL and SQL Server behave the same way; on each of them the table kept only its first row after the same failing insert. To keep the good rows and skip the duplicates, add ON CONFLICT DO NOTHING (SQLite 3.24+ and PostgreSQL) or write INSERT IGNORE in MySQL; both stored grace and linus and skipped ada. For updating the existing row instead, see insert or update if it exists.
Frequently asked questions
How do I insert multiple rows in a single SQL query?
List the rows after one VALUES, each in its own parentheses and separated by commas: INSERT INTO users (id, name) VALUES (1, 'Ada'), (2, 'Grace'), (3, 'Linus');. The same statement works in SQLite (3.7.11+), PostgreSQL, MySQL and SQL Server (2008+). On older databases, use INSERT INTO users (id, name) SELECT 1, 'Ada' UNION ALL SELECT 2, 'Grace'.
How many rows can one INSERT … VALUES statement insert?
It depends on the database. In SQLite a 5,000-row VALUES list ran fine here, while the old UNION ALL form stopped at 500 terms with too many terms in compound SELECT. MySQL limits the size of the statement text through max_allowed_packet rather than the number of rows. Microsoft documents a limit of 1,000 rows per VALUES list for SQL Server; Babelfish, which this site uses to check SQL Server syntax, accepted 1,001 rows, so that number is not verified here. For very large loads, send the rows in chunks inside one transaction.
Is one multi-row INSERT faster than many single-row INSERTs?
Usually, yes: the database parses one statement, and your application makes one round trip instead of one per row. The bigger win, though, is the transaction. Outside an explicit transaction each INSERT commits on its own, so wrapping the inserts in BEGIN … COMMIT lets the database commit once for the whole batch, whether you send single-row or multi-row statements.
What happens if one row in a multi-row INSERT fails?
The whole statement fails and none of its rows are kept, including the valid rows listed before the bad one. In SQLite, a duplicate in the third row printed UNIQUE constraint failed: users.email and the table still had only its original row; PostgreSQL, MySQL and SQL Server kept nothing from the statement either. To skip conflicting rows instead, use ON CONFLICT DO NOTHING (SQLite, PostgreSQL) or INSERT IGNORE (MySQL).
Why do I get "3 values for 2 columns" or "all VALUES must have the same number of terms"?
Every row in the VALUES list must have exactly as many values as the column list has columns. SQLite reports 3 values for 2 columns when the rows are all too long or too short, and all VALUES must have the same number of terms when the rows have different lengths. Count the commas in each row, and add the missing column to the column list if one row really needs it.
How do I get the ids of all the rows I just inserted?
Add RETURNING id to the end of the INSERT in SQLite 3.35+ or PostgreSQL; it returns one row per inserted row. In SQL Server, put OUTPUT INSERTED.id between the column list and VALUES. MySQL has no RETURNING: LAST_INSERT_ID() returns the id of the first row the statement inserted, and ROW_COUNT() how many rows it inserted.
Run it yourself
Open any of these in the SQLite playground to change the rows, run and share.
SQLite playground