{"id":305,"date":"2025-08-27T19:31:20","date_gmt":"2025-08-27T19:31:20","guid":{"rendered":"https:\/\/1v0.net\/blog\/?p=305"},"modified":"2025-08-27T19:31:22","modified_gmt":"2025-08-27T19:31:22","slug":"handling-large-data-sets-in-laravel-with-chunking-cursors","status":"publish","type":"post","link":"https:\/\/1v0.net\/blog\/handling-large-data-sets-in-laravel-with-chunking-cursors\/","title":{"rendered":"Handling Large Data Sets in Laravel with Chunking &amp; Cursors"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\"><strong>Handling Large Data Sets in Laravel with Chunking &amp; Cursors<\/strong><\/h2>\n\n\n\n<p>Loading tens or hundreds of thousands of rows into memory will crash your app or time out your requests. Laravel provides <code>chunk()<\/code>, <code>chunkById()<\/code>, <code>cursor()<\/code> \/ <code>lazy()<\/code>, and their <code>*ById()<\/code> variants to process large data sets in small, memory-friendly pieces. In this guide you\u2019ll learn when to use each, how to avoid classic pitfalls (like missing\/duplicated rows when ordering), and how to wire them into real features like CSV exports and background jobs.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>1 &#8211; When to Use chunk(), chunkById(), cursor(), lazy()<\/strong><\/h2>\n\n\n\n<p>Rule of thumb: <code>chunk()<\/code> pages using LIMIT\/OFFSET (simple but can skip\/duplicate if rows are inserted\/deleted mid-scan). <code>chunkById()<\/code> uses the primary key for stable windows (best for large mutable tables). <code>cursor()<\/code>\/<code>lazy()<\/code> stream one row at a time using a generator (lowest memory, slowest per-row). Use the <code>*ById()<\/code> variants when ordering by the primary key for safety.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>2 &#8211; Using chunk() for Medium Data (Static Windows)<\/strong><\/h2>\n\n\n\n<p>Process records in fixed pages. Works well for reporting tables that don\u2019t mutate during the scan.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>\\<span class=\"hljs-title\">Order<\/span>;\n\n<span class=\"hljs-comment\">\/\/ Process 2,000 records at a time<\/span>\nOrder::where(<span class=\"hljs-string\">'status'<\/span>, <span class=\"hljs-string\">'paid'<\/span>)\n    -&gt;orderBy(<span class=\"hljs-string\">'id'<\/span>) <span class=\"hljs-comment\">\/\/ always order deterministically<\/span>\n    -&gt;chunk(<span class=\"hljs-number\">2000<\/span>, <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">($orders)<\/span> <\/span>{\n        <span class=\"hljs-keyword\">foreach<\/span> ($orders <span class=\"hljs-keyword\">as<\/span> $order) {\n            <span class=\"hljs-comment\">\/\/ do work (aggregate, export, etc.)<\/span>\n        }\n    });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><code>chunk(2000, ...)<\/code> runs your callback for each page. Always add a deterministic <code>orderBy<\/code> (typically the PK). Avoid using this on hot tables that change rapidly; OFFSET can drift if rows are inserted\/deleted between pages.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>3 &#8211; Using chunkById() for Large, Mutable Tables<\/strong><\/h2>\n\n\n\n<p>Prefer <code>chunkById()<\/code> on big tables receiving writes while you scan. It uses the last seen ID instead of OFFSET, avoiding gaps\/duplication.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>\\<span class=\"hljs-title\">User<\/span>;\n\n<span class=\"hljs-comment\">\/\/ Process active users safely even if the table changes mid-scan<\/span>\nUser::where(<span class=\"hljs-string\">'is_active'<\/span>, <span class=\"hljs-keyword\">true<\/span>)\n    -&gt;chunkById(<span class=\"hljs-number\">3000<\/span>, <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">($users)<\/span> <\/span>{\n        <span class=\"hljs-keyword\">foreach<\/span> ($users <span class=\"hljs-keyword\">as<\/span> $user) {\n            <span class=\"hljs-comment\">\/\/ email sync, billing, etc.<\/span>\n        }\n    }, $column = <span class=\"hljs-string\">'id'<\/span>);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><code>chunkById(3000,...)<\/code> remembers the highest <code>id<\/code> from the last page and continues from there, so inserts\/deletes won\u2019t shift previously scanned windows. If your PK is not monotonic (e.g., UUIDs), use a sortable indexed column like an auto-increment surrogate.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>4 &#8211; Using cursor() \/ lazy() for Streaming (Ultra Low Memory)<\/strong><\/h2>\n\n\n\n<p><code>cursor()<\/code> (alias: <code>lazy()<\/code>) yields models one-by-one using generators\u2014minimal memory, at the cost of longer total runtime and sustained DB connection.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>\\<span class=\"hljs-title\">LogEntry<\/span>;\n\n<span class=\"hljs-keyword\">foreach<\/span> (LogEntry::where(<span class=\"hljs-string\">'level'<\/span>,<span class=\"hljs-string\">'error'<\/span>)-&gt;orderBy(<span class=\"hljs-string\">'id'<\/span>)-&gt;cursor() <span class=\"hljs-keyword\">as<\/span> $row) {\n    <span class=\"hljs-comment\">\/\/ Stream process each $row; memory footprint stays tiny<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><code>cursor()<\/code> is ideal for exports or ETL tasks. Because it keeps a DB cursor open, run it in CLI\/queue workers, not behind slow web requests. Combine with <code>set_time_limit<\/code> or queue timeouts appropriately.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>5 &#8211; Memory &amp; N+1: Eager Load Carefully<\/strong><\/h2>\n\n\n\n<p>When chunking or streaming, eager load only what you need. Avoid loading heavy relations per item; consider precomputing or joining.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ Keep the payload small when scanning<\/span>\nOrder::with(&#91;<span class=\"hljs-string\">'user:id,name'<\/span>, <span class=\"hljs-string\">'items:id,order_id,price'<\/span>])\n    -&gt;whereYear(<span class=\"hljs-string\">'created_at'<\/span>, now()-&gt;year)\n    -&gt;chunkById(<span class=\"hljs-number\">2000<\/span>, <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">($orders)<\/span> <\/span>{\n        <span class=\"hljs-keyword\">foreach<\/span> ($orders <span class=\"hljs-keyword\">as<\/span> $order) {\n            <span class=\"hljs-comment\">\/\/ $order-&gt;relation calls won't trigger extra queries<\/span>\n        }\n    });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Eager load only essential columns (select lists on relations) to control memory and query volume. Heavy relations can still cause memory spikes if the chunk size is too large\u2014tune chunk size down if needed (e.g., 500\u20131000).<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>6 &#8211; UI Feature: CSV Export with Streaming<\/strong><\/h2>\n\n\n\n<p>Let\u2019s build a safe CSV export that streams rows instead of loading them all. We\u2019ll use <code>cursor()<\/code> inside a streamed response so memory stays flat.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Http\/Controllers\/ExportController.php<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Controllers<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>\\<span class=\"hljs-title\">Order<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Symfony<\/span>\\<span class=\"hljs-title\">Component<\/span>\\<span class=\"hljs-title\">HttpFoundation<\/span>\\<span class=\"hljs-title\">StreamedResponse<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">ExportController<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">Controller<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">ordersCsv<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">StreamedResponse<\/span>\n    <\/span>{\n        $response = <span class=\"hljs-keyword\">new<\/span> StreamedResponse(<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">()<\/span> <\/span>{\n            $handle = fopen(<span class=\"hljs-string\">'php:\/\/output'<\/span>, <span class=\"hljs-string\">'w'<\/span>);\n            fputcsv($handle, &#91;<span class=\"hljs-string\">'ID'<\/span>,<span class=\"hljs-string\">'User'<\/span>,<span class=\"hljs-string\">'Total'<\/span>,<span class=\"hljs-string\">'Created At'<\/span>]);\n\n            <span class=\"hljs-keyword\">foreach<\/span> (\n                Order::with(<span class=\"hljs-string\">'user:id,name'<\/span>)\n                    -&gt;orderBy(<span class=\"hljs-string\">'id'<\/span>)\n                    -&gt;cursor() <span class=\"hljs-keyword\">as<\/span> $o\n            ) {\n                fputcsv($handle, &#91;\n                    $o-&gt;id,\n                    optional($o-&gt;user)-&gt;name,\n                    number_format($o-&gt;total \/ <span class=\"hljs-number\">100<\/span>, <span class=\"hljs-number\">2<\/span>),\n                    $o-&gt;created_at,\n                ]);\n            }\n\n            fclose($handle);\n        });\n\n        $response-&gt;headers-&gt;set(<span class=\"hljs-string\">'Content-Type'<\/span>, <span class=\"hljs-string\">'text\/csv'<\/span>);\n        $response-&gt;headers-&gt;set(<span class=\"hljs-string\">'Content-Disposition'<\/span>, <span class=\"hljs-string\">'attachment; filename=\"orders.csv\"'<\/span>);\n\n        <span class=\"hljs-keyword\">return<\/span> $response;\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This streams CSV lines as they are read from the database with <code>cursor()<\/code>. The browser starts downloading immediately and memory usage remains near-constant regardless of table size.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ routes\/web.php (snippet)<\/span>\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Controllers<\/span>\\<span class=\"hljs-title\">ExportController<\/span>;\n\nRoute::get(<span class=\"hljs-string\">'\/exports\/orders.csv'<\/span>, &#91;ExportController::class, <span class=\"hljs-string\">'ordersCsv'<\/span>])\n    -&gt;middleware(&#91;<span class=\"hljs-string\">'auth'<\/span>]);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Protect exports behind auth (and permissions if needed). For very long streams, prefer running the query in a queue and emailing a signed URL to the file once ready.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>7 &#8211; Background Jobs: Chunk Work into Batches<\/strong><\/h2>\n\n\n\n<p>Break the work into manageable pieces and dispatch jobs per chunk to parallelize safely without memory bloat.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Console\/Commands\/DispatchUserEmails.php<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Console<\/span>\\<span class=\"hljs-title\">Commands<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Jobs<\/span>\\<span class=\"hljs-title\">SendMonthlyEmail<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>\\<span class=\"hljs-title\">User<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Console<\/span>\\<span class=\"hljs-title\">Command<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">DispatchUserEmails<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">Command<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">protected<\/span> $signature = <span class=\"hljs-string\">'users:email-monthly'<\/span>;\n    <span class=\"hljs-keyword\">protected<\/span> $description = <span class=\"hljs-string\">'Dispatch monthly emails in chunks'<\/span>;\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">handle<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">int<\/span>\n    <\/span>{\n        User::where(<span class=\"hljs-string\">'is_active'<\/span>, <span class=\"hljs-keyword\">true<\/span>)\n            -&gt;select(<span class=\"hljs-string\">'id'<\/span>,<span class=\"hljs-string\">'email'<\/span>,<span class=\"hljs-string\">'name'<\/span>)\n            -&gt;chunkById(<span class=\"hljs-number\">500<\/span>, <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">($users)<\/span> <\/span>{\n                dispatch(<span class=\"hljs-keyword\">new<\/span> SendMonthlyEmail($users-&gt;pluck(<span class=\"hljs-string\">'id'<\/span>)-&gt;all()));\n            });\n\n        <span class=\"hljs-keyword\">$this<\/span>-&gt;info(<span class=\"hljs-string\">'Dispatched email jobs.'<\/span>);\n        <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">self<\/span>::SUCCESS;\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This command slices the user base into 500-row chunks and dispatches one job per slice. Each job can query those IDs again in isolation to send emails, avoiding giant payloads on the queue bus.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Jobs\/SendMonthlyEmail.php<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Jobs<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>\\<span class=\"hljs-title\">User<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Bus<\/span>\\<span class=\"hljs-title\">Queueable<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Contracts<\/span>\\<span class=\"hljs-title\">Queue<\/span>\\<span class=\"hljs-title\">ShouldQueue<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">SendMonthlyEmail<\/span> <span class=\"hljs-keyword\">implements<\/span> <span class=\"hljs-title\">ShouldQueue<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Queueable<\/span>;\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">__construct<\/span><span class=\"hljs-params\">(public array $userIds)<\/span> <\/span>{}\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">handle<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">void<\/span>\n    <\/span>{\n        User::whereIn(<span class=\"hljs-string\">'id'<\/span>, <span class=\"hljs-keyword\">$this<\/span>-&gt;userIds)\n            -&gt;select(<span class=\"hljs-string\">'id'<\/span>,<span class=\"hljs-string\">'email'<\/span>,<span class=\"hljs-string\">'name'<\/span>)\n            -&gt;chunkById(<span class=\"hljs-number\">100<\/span>, <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">($users)<\/span> <\/span>{\n                <span class=\"hljs-keyword\">foreach<\/span> ($users <span class=\"hljs-keyword\">as<\/span> $u) {\n                    <span class=\"hljs-comment\">\/\/ Mail::to($u-&gt;email)-&gt;queue(new MonthlyDigest($u));<\/span>\n                }\n            });\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Jobs receive lightweight ID lists, then process sub-chunks (100) within the job to keep memory flat and respect queue time limits. Use retry\/backoff for transient failures.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>8 &#8211; Avoiding Pitfalls (Ordering, Mutations, Transactions)<\/strong><\/h2>\n\n\n\n<p>Large scans can behave badly if ordering is unstable or if you wrap everything in one giant transaction.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ Bad: OFFSET can skip\/duplicate if rows change<\/span>\n<span class=\"hljs-comment\">\/\/ Good: use chunkById() with a stable increasing key<\/span>\nInvoice::where(<span class=\"hljs-string\">'status'<\/span>,<span class=\"hljs-string\">'unpaid'<\/span>)\n    -&gt;chunkById(<span class=\"hljs-number\">2000<\/span>, <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">($invoices)<\/span> <\/span>{\n        <span class=\"hljs-keyword\">foreach<\/span> ($invoices <span class=\"hljs-keyword\">as<\/span> $inv) {\n            <span class=\"hljs-comment\">\/\/ safely process<\/span>\n        }\n    });\n\n<span class=\"hljs-comment\">\/\/ Avoid one huge transaction around a whole scan<\/span>\n<span class=\"hljs-comment\">\/\/ Instead, commit per item or per small group<\/span>\nDB::transaction(<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">()<\/span> <span class=\"hljs-title\">use<\/span> <span class=\"hljs-params\">($orders)<\/span> <\/span>{\n    <span class=\"hljs-keyword\">foreach<\/span> ($orders <span class=\"hljs-keyword\">as<\/span> $order) {\n        <span class=\"hljs-comment\">\/\/ small, fast writes here<\/span>\n    }\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Use <code>chunkById()<\/code> for mutable tables. Keep transactions small to prevent lock contention and long-running locks. If you must guarantee a consistent snapshot, consider DB-level snapshot isolation or doing the scan from a read replica.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\">Wrapping Up<\/h2>\n\n\n\n<p>You learned how to pick the right tool for big reads: <code>chunk()<\/code> for simple paging on static data, <code>chunkById()<\/code> for safety on hot tables, and <code>cursor()<\/code>\/<code>lazy()<\/code> for true streaming. You also built a memory-safe CSV export and batched background jobs. With careful ordering, eager loading, and transaction strategy, you can process millions of rows reliably.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\">What\u2019s Next<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/blog\/how-to-use-eloquent-api-resources-for-clean-apis\">How to Use Eloquent API Resources for Clean APIs<\/a><\/li>\n<li><a href=\"\/blog\/how-to-use-eloquent-events-for-auditing-user-actions\">How to Use Eloquent Events for Auditing User Actions<\/a><\/li>\n<li><a href=\"\/blog\/eager-loading-vs-lazy-loading-in-laravel-best-practices\">Eager Loading vs Lazy Loading in Laravel: Best Practices<\/a><\/li>\n<\/ul>\n\n","protected":false},"excerpt":{"rendered":"<p>Handling Large Data Sets in Laravel with Chunking &amp; Cursors Loading tens or hundreds of thousands of rows into memory will crash your app or time out your requests. Laravel provides chunk(), chunkById(), cursor() \/ lazy(), and their *ById() variants to process large data sets in small, memory-friendly pieces. In this guide you\u2019ll learn when [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":309,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[45,36,44],"class_list":["post-305","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-laravel","tag-chunking","tag-eloquent","tag-performance"],"_links":{"self":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/305","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/comments?post=305"}],"version-history":[{"count":1,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/305\/revisions"}],"predecessor-version":[{"id":308,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/305\/revisions\/308"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/media\/309"}],"wp:attachment":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/media?parent=305"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/categories?post=305"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/tags?post=305"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}