<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	xmlns:series="https://publishpress.com/"
	>

<channel>
	<title>Laravel Archives - Tomoshare</title>
	<atom:link href="https://blog.tomosia.com.vn/danh-muc/laravel/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.tomosia.com.vn/danh-muc/laravel/</link>
	<description>Kênh chia sẻ kiến thức Tomosia Việt Nam</description>
	<lastBuildDate>Fri, 17 Apr 2026 07:08:02 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://blog.tomosia.com.vn/wp-content/uploads/2023/09/cropped-icon-32x32.png</url>
	<title>Laravel Archives - Tomoshare</title>
	<link>https://blog.tomosia.com.vn/danh-muc/laravel/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Tối ưu Slow Query PostgreSQL: Khi một dòng code cứu cả hệ thống</title>
		<link>https://blog.tomosia.com.vn/toi-uu-slow-query-postgresql-khi-mot-dong-code-cuu-ca-he-thong/</link>
					<comments>https://blog.tomosia.com.vn/toi-uu-slow-query-postgresql-khi-mot-dong-code-cuu-ca-he-thong/#respond</comments>
		
		<dc:creator><![CDATA[hoa nguyen]]></dc:creator>
		<pubDate>Fri, 17 Apr 2026 07:07:57 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[database]]></category>
		<category><![CDATA[Laravel]]></category>
		<guid isPermaLink="false">https://blog.tomosia.com.vn/?p=4257</guid>

					<description><![CDATA[<p>Tối ưu Slow Query PostgreSQL: Khi một dòng code cứu cả hệ thống Bối cảnh Trong một dự&#8230;</p>
<p>The post <a href="https://blog.tomosia.com.vn/toi-uu-slow-query-postgresql-khi-mot-dong-code-cuu-ca-he-thong/">Tối ưu Slow Query PostgreSQL: Khi một dòng code cứu cả hệ thống</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 id="toi-uu-slow-query-postgresql-khi-mot-dong-code-cuu-ca-he-thong" class="wp-block-heading">Tối ưu Slow Query PostgreSQL: Khi một dòng code cứu cả hệ thống</h1>



<h2 id="boi-canh" class="wp-block-heading">Bối cảnh</h2>



<p>Trong một dự án e-commerce gần đây, tôi gặp một bài toán rất quen thuộc:<br>trên trang chi tiết đơn hàng, cần có nút:</p>



<ul class="wp-block-list">
<li>“Đơn tiếp theo” (Next)</li>



<li>“Đơn trước đó” (Previous)</li>
</ul>



<p>Yêu cầu tưởng đơn giản, nhưng có một constraint quan trọng:<br><strong>không được load toàn bộ danh sách lên RAM</strong>.</p>



<p>Giải pháp hiển nhiên là dùng <strong>Keyset Pagination</strong> (cursor-based), thay vì offset pagination.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="cach-trien-khai-ban-dau" class="wp-block-heading">Cách triển khai ban đầu</h2>



<p>Dữ liệu được sort theo:</p>



<ul class="wp-block-list">
<li><code>paid_at</code> (thời điểm thanh toán)</li>



<li><code>id</code> (để break tie)</li>
</ul>



<p>Code Laravel Eloquent ban đầu của tôi cho “Next Order” trông như sau:</p>



<pre class="wp-block-preformatted">-&gt;where(function (Builder $q) use ($order) {<br>    $q-&gt;where('paid_at', '&gt;', $order-&gt;paid_at)<br>        -&gt;orWhere(function (Builder $q1) use ($order) {<br>            $q1-&gt;where('paid_at', $order-&gt;paid_at)<br>                -&gt;where('id', '&gt;', $order-&gt;id);<br>        });<br>})<br>-&gt;orderBy('paid_at')<br>-&gt;orderBy('id')</pre>



<p>Nếu đọc bằng “tiếng người”, logic này hoàn toàn đúng:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Lấy các đơn có <code>paid_at</code> lớn hơn,<br>hoặc nếu cùng thời điểm thì <code>id</code> phải lớn hơn.</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="van-de-thuc-te-xay-ra" class="wp-block-heading">Vấn đề thực tế xảy ra</h2>



<p>Khi dữ liệu tăng lên vài triệu bản ghi, hệ thống bắt đầu:</p>



<ul class="wp-block-list">
<li>xuất hiện <strong>slow query</strong></li>



<li>query plan chuyển sang:
<ul class="wp-block-list">
<li><code>Bitmap Heap Scan</code></li>



<li>hoặc tệ hơn là <code>Seq Scan</code></li>
</ul>
</li>
</ul>



<p>Trong khi đó, database đã có <strong>composite index</strong>:</p>



<pre class="wp-block-preformatted">(paid_at, id)</pre>



<p>=&gt; Về lý thuyết phải chạy rất nhanh.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="nguyen-nhan-cot-loi" class="wp-block-heading">Nguyên nhân cốt lõi</h2>



<p>Thủ phạm nằm ở <strong>mệnh đề <code>OR</code></strong>.</p>



<p>PostgreSQL Query Planner khi gặp <code>OR</code> thường:</p>



<ul class="wp-block-list">
<li>tách điều kiện thành nhiều nhánh</li>



<li>không tận dụng được composite index một cách hiệu quả</li>



<li>dẫn tới scan nhiều hơn cần thiết</li>
</ul>



<p>Nói đơn giản:<br><strong>Index có, nhưng không được dùng đúng cách.</strong></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="cach-giai-quyet-va-la-diem-aha-moment" class="wp-block-heading">Cách giải quyết (và là điểm “aha moment”)</h2>



<p>Thay vì viết điều kiện dạng boolean phức tạp,<br>ta sử dụng <strong>Tuple Comparison (Row Value Syntax)</strong> của PostgreSQL.</p>



<h3 id="thay-doi-duy-nhat" class="wp-block-heading">Thay đổi duy nhất:</h3>



<pre class="wp-block-preformatted">-&gt;whereRaw('(paid_at, id) &gt; (?, ?)', [$order-&gt;paid_at, $order-&gt;id])</pre>



<p>Với “Previous”:</p>



<pre class="wp-block-preformatted">-&gt;whereRaw('(paid_at, id) &lt; (?, ?)', [$order-&gt;paid_at, $order-&gt;id])</pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="vi-sao-cach-nay-hieu-qua" class="wp-block-heading">Vì sao cách này hiệu quả?</h2>



<p>Composite index <code>(paid_at, id)</code> trong PostgreSQL được sắp xếp theo kiểu:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>từ điển (lexicographical order)</p>
</blockquote>



<p>Tức là:</p>



<ol class="wp-block-list">
<li>So sánh <code>paid_at</code></li>



<li>Nếu bằng nhau → so sánh <code>id</code></li>
</ol>



<p>Khi bạn viết:</p>



<pre class="wp-block-preformatted">(paid_at, id) &gt; (x, y)</pre>



<p>PostgreSQL có thể:</p>



<ul class="wp-block-list">
<li><strong>map trực tiếp vào B-Tree index</strong></li>



<li>nhảy đúng vị trí <code>(x, y)</code></li>



<li>thực hiện <strong>Index Scan tuyến tính</strong></li>
</ul>



<p>=&gt; Không cần phân nhánh logic như <code>OR</code> nữa.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="ket-qua" class="wp-block-heading">Kết quả</h2>



<p>Sau khi thay đổi:</p>



<ul class="wp-block-list">
<li>Query Plan chuyển thành <strong>Index Scan</strong></li>



<li>Slow query biến mất</li>



<li>Thời gian response ổn định lại</li>
</ul>



<p>Ngoài ra còn một lợi ích “không ngờ”:</p>



<h3 id="code-gon-hon-rat-nhieu" class="wp-block-heading">Code gọn hơn rất nhiều</h3>



<p>Từ:</p>



<ul class="wp-block-list">
<li>nhiều closure lồng nhau</li>



<li>logic khó đọc</li>
</ul>



<p>=&gt; còn:</p>



<pre class="wp-block-preformatted">-&gt;whereRaw('(paid_at, id) &gt; (?, ?)', [...])</pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="bai-hoc-rut-ra" class="wp-block-heading">Bài học rút ra</h2>



<h3 id="1-tranh-or-khi-lam-keyset-pagination" class="wp-block-heading">1. Tránh <code>OR</code> khi làm keyset pagination</h3>



<p>Đặc biệt khi:</p>



<ul class="wp-block-list">
<li>sort theo nhiều cột</li>



<li>đã có composite index</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 id="2-hieu-cach-database-to-chuc-index-quan-trong-hon-orm" class="wp-block-heading">2. Hiểu cách database tổ chức index quan trọng hơn ORM</h3>



<p>ORM giúp code “đẹp”, nhưng:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>không đảm bảo query tối ưu</p>
</blockquote>



<p>Đôi khi, quay về <strong>RAW SQL đúng chỗ</strong> lại là lựa chọn tốt hơn.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 id="3-tan-dung-tuple-comparison-trong-postgresql" class="wp-block-heading">3. Tận dụng Tuple Comparison trong PostgreSQL</h3>



<p>Đây là một feature rất mạnh nhưng ít được dùng:</p>



<pre class="wp-block-preformatted">(col1, col2) &gt; (val1, val2)</pre>



<p>Rất phù hợp cho:</p>



<ul class="wp-block-list">
<li>keyset pagination</li>



<li>multi-column sorting</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="ket-luan" class="wp-block-heading">Kết luận</h2>



<p>Một thay đổi nhỏ:</p>



<ul class="wp-block-list">
<li>bỏ <code>OR</code></li>



<li>dùng tuple comparison</li>
</ul>



<p>=&gt; có thể:</p>



<ul class="wp-block-list">
<li>giảm load database</li>



<li>tránh slow query</li>



<li>đơn giản hoá code</li>
</ul>



<p>Đây là một ví dụ điển hình cho việc:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>tối ưu performance không phải lúc nào cũng cần rewrite lớn — đôi khi chỉ cần hiểu đúng cách database hoạt động.</strong></p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p></p>
<p>The post <a href="https://blog.tomosia.com.vn/toi-uu-slow-query-postgresql-khi-mot-dong-code-cuu-ca-he-thong/">Tối ưu Slow Query PostgreSQL: Khi một dòng code cứu cả hệ thống</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.tomosia.com.vn/toi-uu-slow-query-postgresql-khi-mot-dong-code-cuu-ca-he-thong/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
