Back to Blog
PHPJuly 10, 20264 min read

Optimizing Laravel Eloquent: upsert() vs updateOrCreate

Struggling with slow database updates? Learn when to use Eloquent upsert vs updateOrCreate to optimize Laravel bulk updates and prevent performance issues.

LaravelEloquentDatabaseOptimizationPerformancePHP

I remember sitting on an on-call rotation a few months back when a background job started choking our production database. We were processing an incoming CSV file with about 50,000 product updates, and the developer had used updateOrCreate() inside a foreach loop. It was a classic "silent killer"—the page load times spiked, the database CPU hit 98%, and we ended up with a massive backlog of failed jobs.

If you’re dealing with anything more than a few dozen records, updateOrCreate() is usually the wrong tool for the job. Let’s break down why that is and how to use upsert() to keep your application responsive.

The Problem with updateOrCreate

When you call Model::updateOrCreate(['id' => $id], $data), Eloquent does exactly what the name implies: it fires a SELECT query to check if the record exists, then executes either an INSERT or an UPDATE.

If you run this inside a loop for 1,000 records, you’re hitting your database at least 1,000 times—often 2,000 times if the records exist. You're incurring the overhead of:

  • Network latency for every single round-trip.
  • Transaction management overhead.
  • The "N+1" equivalent for database writes.

I’ve seen this pattern drag simple imports from taking five seconds to over ten minutes. If you’re trying to scale, you have to move away from individual row processing. You need to leverage database indexing strategies to make those lookups faster, but even with perfect indexes, the sheer number of queries is the bottleneck.

When to use Eloquent upsert

The upsert() method was built specifically for high-volume data synchronization. Instead of checking every row individually, it sends the entire batch to the database engine in one go.

PHP
#6A9955">// The efficient way to handle bulk updates
Product::upsert([
    ['id' => 1, 'price' => 10.99, 'stock' => 50],
    ['id' => 2, 'price' => 15.50, 'stock' => 20],
    #6A9955">// ... up to thousands of records
], ['id'], ['price', 'stock']);

The first argument is your data. The second argument is the unique column (or columns) that the database uses to determine if a record should be updated. The third argument is the list of columns that should be updated if a match is found.

Performance Comparison

When you compare these two, the difference isn't just incremental—it's structural.

StrategyDatabase QueriesBest For
updateOrCreate2 per recordSingle row changes, triggers/events
upsert()1 per batchBulk syncs, imports, high-volume updates

The "Gotchas" You Need to Know

While upsert() is significantly faster, it has a few trade-offs you need to be aware of:

  1. Eloquent Events: upsert() is a database-level operation. It does not fire creating, updating, or saved events. If your application relies on those events to clear caches or trigger notifications, upsert() will bypass them entirely.
  2. Mass Assignment: You still need to ensure your model's $fillable array is set correctly. Even though it's a bulk operation, Laravel still respects these security boundaries.
  3. Database Support: Ensure your database engine supports the ON DUPLICATE KEY UPDATE syntax (MySQL) or ON CONFLICT (PostgreSQL). Fortunately, Laravel handles the abstraction for most modern versions.

My Advice for Large-Scale Updates

If you're dealing with massive datasets, don't just swap the method and hope for the best. I usually combine upsert() with chunkById to keep memory usage low. Even if you aren't doing a 1M row import, processing in chunks of 500 or 1,000 provides a great balance between speed and memory stability.

If you find yourself stuck with complex database bottlenecks or need help refactoring legacy loops into high-performance bulk operations, I offer Laravel Bug Fixes, Maintenance & Optimization services to help get your queries back under control.

Next time, I’d probably look into using dedicated job batches via the new Bus::bulk() feature if I was dealing with really high-latency external data, but for standard database synchronization, upsert() remains my go-to. Just watch out for those missing model events—they've caught me off guard more than once.

FAQ

Does upsert() fire Eloquent model events? No. It executes a single SQL query directly against the database to maximize performance. If you need to trigger logic on update, you'll need to handle that manually after the batch completes.

How many records can I pass to upsert() at once? It depends on your database's max_allowed_packet (in MySQL) and the number of placeholders allowed. Usually, chunks of 500–1,000 records are the "sweet spot" for performance without hitting database limits.

Can I use upsert() with composite keys? Yes. Pass an array of column names to the second argument, like ['user_id', 'product_id'], and Eloquent will use both as the unique identifier for the conflict check.

Similar Posts