Running a single UPDATE or DELETE over millions of rows can hold database locks for minutes, leave replicas lagging, or time out during deployments. Queue-SQL, a Laravel package by Kamran Atayev, addresses this by splitting large writes into parallel queued jobs: it determines the minimum and maximum primary keys for a query, cuts the ID range into bounded slices, and dispatches an Illuminate\Bus\Batch to process each slice independently.
The package registers a queue() macro on both the Query Builder and Eloquent, exposing update, delete, insert, and upsert. Constraints are compiled into SQL fragments and bindings, so nested closures, whereHas, whereExists, and sub-selects survive the trip to workers without closure serialization problems. Job sizing works via chunk (rows per job) or maxJobs (a target total across the key range); the two are mutually exclusive and passing both throws an InvalidArgumentException. dispatch() returns a native Illuminate\Bus\Batch supporting then, catch, finally, and allowFailures.
Usage chains queue() onto a query before the write method and ends with dispatch() — nothing is queued until then. Example: Order::where('status', 'complete')->queue(chunk: 25000, tries: 2, onQueue: 'maintenance', throttle: 4)->update(['status' => 'completed'])->dispatch(). The throttle option caps jobs per second, protecting database connections and replicas during big backfills. Deletes use identical syntax, while bulk inserts skip key-range planning and split the record array across jobs instead.
Because each update or delete job targets a fixed key slice, retries are idempotent. Retried insert jobs are not, and can create duplicates — the docs recommend a unique index or upsert() where retry safety matters. Compared to Laravel's Prunable trait and model:prune command, which delete sequentially in one process, Queue-SQL runs concurrently, tracks progress as a batch, and also handles arbitrary updates and bulk inserts.
Replacing dispatch() with dryRun() previews the batching plan (operation, table, job count, ranges, estimated rows) without queueing anything. Key-range chunking uses min/max primary key values rather than exact row counts, so tables with large ID gaps produce uneven job sizes while the total job count stays fixed.
The CLI monitors batches directly against the framework's batch table: queue-sql:status lists or inspects batches, queue-sql:cancel stops one, --watch refreshes every two seconds in interactive terminals (printing once in non-interactive contexts), and --json formats output. Status output shows total, pending and failed jobs, progress percentages, and batch state. Horizon users get automatic tags with queue-sql, the operation type, and target tables. Global defaults for chunk size, retries, backoff, throttling, delays, and queue names live in config/queue-sql.php.
Requirements are PHP 8.1+ and Laravel 10 through 13, with SQLite, MySQL, and PostgreSQL supported. Installation is composer require kamranata/queue-sql, followed by queue:batches-table and migrate, since the package relies on native job batching. Two caveats: slicing needs an incrementing integer primary key (other tables fall back to a single queued job), and key boundaries are fixed at dispatch time, so rows inserted afterward with higher IDs are not covered. A benchmark script (benchmarks/lock_duration.php) ships with the repo; in a 20,000-row SQLite delete test, an un-batched DELETE held a lock 3.6 times longer than Queue-SQL's longest recorded lock.
Comments
No comments yet — be the first.
Open the discussion
No account or password needed — just enter your e-mail and we’ll send you a one-time sign-in link. First time here? You’re set up automatically.
Your rating will be applied automatically after you sign in.
Check your inbox
We’ve sent a sign-in link to …. Open it on this device — this tab will sign you in automatically.
Waiting for your click …
·