Reader Stacks

Bulk Inserting Rows in Laravel Eloquent

A loop of individual ->save() calls means one query per row — insert() (or upsert() for a merge) does it in one, at the cost of skipping model events and timestamps unless handled manually.

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/created events, so anything listening for them (a queued job that sends a notification on creation, for instance) never runs.
  • No automatic timestampscreated_at and updated_at must be set explicitly in each row's array, as shown above, or they'll be left null.
  • No mass-assignment protection$fillable/$guarded on the model is irrelevant here, since insert() 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.

Topics: Database Queries & Eloquent