Drizzle ORM provides a type-safe and intuitive API for inserting data into your database tables.
Basic Insert
Insert a single row into a table:
import { db } from './db' ;
import { users } from './schema' ;
const result = await db . insert ( users ). values ({
name: 'John Doe' ,
email: '[email protected] ' ,
age: 30 ,
});
Bulk Insert
Insert multiple rows in a single query:
const result = await db . insert ( users ). values ([
{
name: 'John Doe' ,
email: '[email protected] ' ,
age: 30 ,
},
{
name: 'Jane Smith' ,
email: '[email protected] ' ,
age: 28 ,
},
{
name: 'Bob Johnson' ,
email: '[email protected] ' ,
age: 35 ,
},
]);
Bulk inserts are more efficient than multiple single inserts as they execute in a single database round-trip.
Insert with Returning
Get the inserted row(s) back from the database:
Return All Columns
Return Specific Columns
Bulk Insert with Returning
const [ newUser ] = await db . insert ( users )
. values ({
name: 'John Doe' ,
email: '[email protected] ' ,
})
. returning ();
// newUser contains all columns including auto-generated id
console . log ( newUser . id );
.returning() is supported in PostgreSQL, SQLite, and MySQL 8.0+. It’s not available in older MySQL versions.
On Conflict Do Nothing
Ignore inserts that would violate constraints:
import { db } from './db' ;
// Ignore conflicts on email column
await db . insert ( users )
. values ({ name: 'John' , email: '[email protected] ' })
. onConflictDoNothing ();
// Ignore conflicts on specific columns
await db . insert ( users )
. values ({ name: 'John' , email: '[email protected] ' })
. onConflictDoNothing ({ target: users . email });
On Conflict Do Update (Upsert)
Update existing rows when conflicts occur:
Basic Upsert
Use Excluded Values
Conditional Update
Multiple Columns
import { db } from './db' ;
// Update name if email already exists
await db . insert ( users )
. values ({ email: '[email protected] ' , name: 'John Doe' })
. onConflictDoUpdate ({
target: users . email ,
set: { name: 'John Doe Updated' },
});
Insert from Select
Insert data from another query:
import { db } from './db' ;
import { users , archivedUsers } from './schema' ;
// Copy inactive users to archive
await db . insert ( archivedUsers )
. select (
db . select (). from ( users ). where ( eq ( users . active , false ))
);
The selected columns must match the target table’s column structure.
Insert with SQL Expressions
Use SQL expressions in insert values:
import { sql } from 'drizzle-orm' ;
await db . insert ( users ). values ({
name: 'John Doe' ,
email: '[email protected] ' ,
createdAt: sql `now()` ,
metadata: sql `jsonb_build_object('source', 'web')` ,
});
Insert with Default Values
Omit columns with default values:
// If createdAt has a default value in schema
await db . insert ( users ). values ({
name: 'John Doe' ,
email: '[email protected] ' ,
// createdAt will use database default
});
PostgreSQL: Override System Values
Insert into generated columns:
// PostgreSQL only: override GENERATED ALWAYS columns
await db . insert ( users )
. overridingSystemValue ()
. values ({
id: 100 , // Override auto-increment
name: 'John' ,
email: '[email protected] ' ,
});
Type Safety
Drizzle provides complete type safety for inserts:
await db . insert ( users ). values ({
name: 'John Doe' ,
email: '[email protected] ' ,
age: 30 ,
// TypeScript error: 'invalid' is not a valid column
// invalid: 'value'
// TypeScript error: age must be a number
// age: 'thirty'
});
With Clause (CTE)
Use Common Table Expressions with inserts:
const newUserIds = db . $with ( 'new_users' ). as (
db . insert ( users )
. values ([{ name: 'John' }, { name: 'Jane' }])
. returning ({ id: users . id })
);
const result = await db . with ( newUserIds )
. select ()
. from ( newUserIds );
Batch Inserts
For very large datasets, use batch processing:
const BATCH_SIZE = 1000 ;
const largeDataset = [ ... ]; // Array of 10,000 items
for ( let i = 0 ; i < largeDataset . length ; i += BATCH_SIZE ) {
const batch = largeDataset . slice ( i , i + BATCH_SIZE );
await db . insert ( users ). values ( batch );
}
Insert Examples by Database
// PostgreSQL specific features
await db . insert ( users )
. values ({ name: 'John' , email: '[email protected] ' })
. onConflictDoUpdate ({
target: users . email ,
set: { name: sql `excluded.name` },
where: sql ` ${ users . updatedAt } < excluded.updated_at` ,
})
. returning ();
// MySQL upsert
await db . insert ( users )
. values ({ name: 'John' , email: '[email protected] ' })
. onDuplicateKeyUpdate ({
set: { name: sql `VALUES(name)` },
});
// SQLite conflict handling
await db . insert ( users )
. values ({ name: 'John' , email: '[email protected] ' })
. onConflictDoUpdate ({
target: users . email ,
set: { name: sql `excluded.name` },
})
. returning ();
Use Bulk Inserts
Insert multiple rows in a single query instead of multiple queries
Batch Large Datasets
Split very large datasets (10k+ rows) into batches of 1000-5000 rows
Use Transactions
Wrap multiple inserts in a transaction for consistency and performance
Avoid Returning When Unnecessary
Only use .returning() when you need the inserted data back
Next Steps
Update Queries Learn how to update existing data
Select Queries Query and filter your data
Transactions Use transactions for data consistency