Creating many rows in a loop, one Model::create() call at a time, means one database round-trip per row — for a handful of rows that's fine, but for hundreds or thousands it's measurably slower than sending the same data as a single bulk INSERT statement, which Eloquent supports directly.
1. The slow way
foreach ($rows as $row) {
Product::create($row); // one INSERT query per iteration
}
2. Bulk insert with insert()
Product::insert([
['name' => 'Widget A', 'price' => 9.99, 'created_at' => now(), 'updated_at' => now()],
['name' => 'Widget B', 'price' => 14.99, 'created_at' => now(), 'updated_at' => now()],
['name' => 'Widget C', 'price' => 19.99, 'created_at' => now(), 'updated_at' => now()],
]);
insert() sends every row in the array as a single INSERT statement — dramatically fewer round-trips to the database than the equivalent loop, especially noticeable once the row count reaches the hundreds or thousands.
3. The trade-offs of insert()
insert() is a lower-level, static query builder method — it bypasses several things a normal create() call handles automatically:
- No model events fire — no
creating/createdevents, so anything listening for them (a queued job that sends a notification on creation, for instance) never runs. - No automatic timestamps —
created_atandupdated_atmust be set explicitly in each row's array, as shown above, or they'll be leftnull. - No mass-assignment protection —
$fillable/$guardedon the model is irrelevant here, sinceinsert()doesn't instantiate model instances at all.
4. Chunking very large datasets
A single insert() call with tens of thousands of rows can hit database limits (max packet size, query length) or use a large amount of memory building the array — chunk it instead:
foreach (array_chunk($rows, 500) as $chunk) {
Product::insert($chunk);
}
5. upsert() — insert or update on a match
Product::upsert(
[
['sku' => 'WID-A', 'name' => 'Widget A', 'price' => 9.99],
['sku' => 'WID-B', 'name' => 'Widget B', 'price' => 14.99],
],
['sku'], // unique column(s) to detect an existing row
['name', 'price'] // columns to update if a match is found
);
upsert() is the right tool for syncing external data (an import, an API sync) where some rows are genuinely new and others should update an existing row matched by a unique key — one call handles both cases instead of needing to check for existence and branch between create() and update() per row.
6. When to still use create() in a loop
If the app genuinely relies on model events firing for each created row (queuing a welcome email per new user, for example), or the dataset is small enough that raw query performance isn't a concern, plain create() in a loop remains the simpler, more correct choice — bulk insert() is a targeted optimization for larger, event-independent datasets, not a universal replacement.