Reader Stacks

How to Insert Multiple Rows at Once in Laravel

insert() and Model::insert() both accept an array of rows and run a single SQL statement — considerably faster than looping create() calls, at the cost of skipping Eloquent events and timestamps.

How to Insert Multiple Rows at Once in Laravel

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.