Inserting many rows at once — a CSV import, a batch of API-sourced records — is considerably more efficient as one bulk insert statement than as a loop of individual create() calls, though the trade-off is skipping Eloquent's model events and automatic timestamps.
Bulk inserting with the query builder
DB::table('products')->insert([
['name' => 'Widget', 'price' => 19.99],
['name' => 'Gadget', 'price' => 29.99],
['name' => 'Gizmo', 'price' => 39.99],
]);
Bulk inserting via an Eloquent model
Product::insert([
['name' => 'Widget', 'price' => 19.99, 'created_at' => now(), 'updated_at' => now()],
['name' => 'Gadget', 'price' => 29.99, 'created_at' => now(), 'updated_at' => now()],
]);
Unlike create(), insert() doesn't automatically populate created_at/updated_at — these need to be included explicitly in each row's array if the table has timestamp columns.
Why insert() is dramatically faster than a loop of create() calls
// slow: one SQL INSERT statement per row, 1000 separate queries
foreach ($rows as $row) {
Product::create($row);
}
// fast: one SQL statement handling all 1000 rows at once
Product::insert($rows);
Each create() call is a completely separate round-trip to the database — for a large batch, this overhead adds up to a meaningful, measurable difference in total execution time compared to a single bulk insert() statement.
What insert() skips that create() doesn't
insert() bypasses Eloquent model events (creating, created), mutators, and any model-level validation logic that a creating event listener might enforce — this is the real trade-off for the performance gain, and it means insert() isn't a safe drop-in replacement for create() if the model relies on any of that event-driven behavior.
Chunking a very large insert to avoid hitting database limits
collect($veryLargeDataset)->chunk(500)->each(function ($chunk) {
DB::table('products')->insert($chunk->toArray());
});
Most databases have a practical limit on how many rows (or how much total data) a single INSERT statement can handle — chunking a very large dataset into batches of a few hundred rows each, inserted in a loop of much larger bulk statements, avoids hitting that limit while still getting nearly all of the performance benefit over row-by-row inserts.
Handling duplicate keys during a bulk insert
DB::table('products')->upsert(
$rows,
['sku'], // unique key(s) to check for conflicts
['price', 'updated_at'] // columns to update if a conflict is found
);
upsert() is the bulk equivalent of "insert, or update if it already exists" — genuinely useful for a sync job that repeatedly imports the same dataset and needs to update existing rows rather than fail or create duplicates on every re-run.