<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Alexander Obregon's Substack: SQL]]></title><description><![CDATA[This is where all my SQL content goes. Related stuff like MySQL, PostgreSQL, and query tuning will be here too]]></description><link>https://alexanderobregon.substack.com/s/sql</link><image><url>https://substackcdn.com/image/fetch/$s_!4Mj9!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Falexanderobregon.substack.com%2Fimg%2Fsubstack.png</url><title>Alexander Obregon&apos;s Substack: SQL</title><link>https://alexanderobregon.substack.com/s/sql</link></image><generator>Substack</generator><lastBuildDate>Tue, 05 May 2026 00:04:26 GMT</lastBuildDate><atom:link href="https://alexanderobregon.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Alexander Obregon]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[alexanderobregon@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[alexanderobregon@substack.com]]></itunes:email><itunes:name><![CDATA[Alexander Obregon]]></itunes:name></itunes:owner><itunes:author><![CDATA[Alexander Obregon]]></itunes:author><googleplay:owner><![CDATA[alexanderobregon@substack.com]]></googleplay:owner><googleplay:email><![CDATA[alexanderobregon@substack.com]]></googleplay:email><googleplay:author><![CDATA[Alexander Obregon]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Expression Indexes in SQL Queries]]></title><description><![CDATA[Databases run into this problem whenever a query filters on lower(email), on a date pulled from a timestamp, or on something derived from two columns.]]></description><link>https://alexanderobregon.substack.com/p/expression-indexes-in-sql-queries</link><guid isPermaLink="false">https://alexanderobregon.substack.com/p/expression-indexes-in-sql-queries</guid><dc:creator><![CDATA[Alexander Obregon]]></dc:creator><pubDate>Tue, 05 May 2026 00:01:31 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ssy4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6ea5ff-228c-48c6-99b1-da29fecd76e8_306x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!aNOj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F378e3b8e-28fc-4019-b2dc-9c802a84a91f_800x373.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!aNOj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F378e3b8e-28fc-4019-b2dc-9c802a84a91f_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!aNOj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F378e3b8e-28fc-4019-b2dc-9c802a84a91f_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!aNOj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F378e3b8e-28fc-4019-b2dc-9c802a84a91f_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!aNOj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F378e3b8e-28fc-4019-b2dc-9c802a84a91f_800x373.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!aNOj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F378e3b8e-28fc-4019-b2dc-9c802a84a91f_800x373.png" width="800" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/378e3b8e-28fc-4019-b2dc-9c802a84a91f_800x373.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!aNOj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F378e3b8e-28fc-4019-b2dc-9c802a84a91f_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!aNOj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F378e3b8e-28fc-4019-b2dc-9c802a84a91f_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!aNOj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F378e3b8e-28fc-4019-b2dc-9c802a84a91f_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!aNOj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F378e3b8e-28fc-4019-b2dc-9c802a84a91f_800x373.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://commons.wikimedia.org/wiki/File:Sql_data_base_with_logo.png">Image Source</a></figcaption></figure></div><p>Databases run into this problem whenever a query filters on <code>lower(email)</code>, on a date pulled from a timestamp, or on something derived from two columns. If an index holds only the raw column data, the optimizer does not always have a direct way to search by that transformed result. An expression index fixes that by storing the result of the expression inside the index, which lets the search target the same transformed result named in the predicate. PostgreSQL calls these indexes indexes on expressions, Oracle calls them function-based indexes, MySQL supports functional index parts, SQL Server usually gets there through indexed computed columns, and SQLite supports indexes on expressions.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>What an Expression Index Really Is</h3><p>Most indexes store values taken straight from table columns and arrange those values so the database can search them quickly. An expression index changes that arrangement by storing the result of an expression instead. That stored value could be a concatenated name, a numeric calculation, or some other derived result built from one or more columns. Because the index holds the computed result rather than only the source column data, the optimizer can search for the transformed value named in the query instead of recalculating that value row by row.</p><p>Names differ across database engines, but the idea stays very close. PostgreSQL refers to indexes on expressions. Oracle uses the term function-based index. MySQL supports functional key parts. SQL Server usually gets there through computed columns that can be indexed. SQLite allows expressions inside <code>CREATE INDEX</code> in place of plain column names. Syntax changes from engine to engine, yet the storage idea remains the same. The index is built from the value produced by the expression, so a plain index on <code>first_name</code> is not the same as an index on <code>first_name || ' ' || last_name</code>, and an index on <code>amount</code> is not the same as an index on <code>abs(amount)</code>. Those indexes are ordered by different values, which means they support different searches.</p><h4>Same Idea Across Database Engines</h4><p>For major SQL products, expression indexing comes down to precomputing the value the query wants to search. PostgreSQL lets you place the expression directly inside the index definition. Oracle does the same through function-based indexes. MySQL also supports direct expression-based index parts, though its rules have their own wording and syntax details. SQL Server takes a nearby route by defining a computed column first and then indexing that computed column. SQLite allows expressions directly inside <code>CREATE INDEX</code> as well. Different spellings and DDL forms can make the feature look less related than it really is, yet the stored value is still a computed result rather than raw column data.</p><p>PostgreSQL makes that idea easy to see with a full-name lookup. Instead of indexing <code>first_name</code> and <code>last_name</code> separately, the index stores the concatenated result, which means the query can search the same value it asks for.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;6e2f5bd6-dbff-424f-bc1e-cc7668f5f339&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE INDEX people_full_name_idx
ON people ((first_name || ' ' || last_name));

SELECT *
FROM people
WHERE (first_name || ' ' || last_name) = 'Alex Carter';</code></pre></div><p>That index is ordered by the combined full-name value, not by <code>first_name</code> alone and not by <code>last_name</code> alone. If the query filters on that same concatenation, the optimizer has an indexed value that matches the predicate directly.</p><p>SQL Server reaches the same destination in a different form. Rather than placing the expression directly inside <code>CREATE INDEX</code>, you first add a computed column and then build the index on that column.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;c56af4b5-84d7-41c2-8468-5c017678f816&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">ALTER TABLE people
ADD full_name AS CONCAT(first_name, ' ', last_name) PERSISTED;

CREATE INDEX ix_people_full_name
ON people(full_name);

SELECT *
FROM people
WHERE full_name = 'Kaitlyn Brooks';</code></pre></div><p>What changes here is the syntax, not the idea. The database is still indexing a derived value rather than the raw source columns. The computed column simply gives that derived value a named place in the table definition before the index is built.</p><p>MySQL also supports direct expression-based indexing, and its syntax has its own small rules. Parentheses are part of the definition, and regular column parts can appear in the same index beside functional parts. That puts MySQL closer to PostgreSQL and SQLite in how the statement looks, while the internal storage route is tied to hidden virtual generated columns. All of these engine-specific details matter at DDL time, but the larger point stays steady. The index stores the result of a calculation, string expression, or function call so the search can target that computed value directly.</p><h4>The Query Must Match</h4><p>Matching the query text to the indexed expression is where this topic becomes more exact than people usually expect. SQLite is very strict about this. Its planner looks for the same expression form in the <code>WHERE</code> clause or <code>ORDER BY</code> clause that appears in the index definition, apart from very small syntax differences such as whitespace. That means mathematically equivalent expressions are not always treated as the same thing for index matching.</p><p>This example makes that easier to see:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;3c2bbf60-bdc8-48fb-aa61-c1d9fe246068&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE t2(x, y, z);
CREATE INDEX t2_xy_sum_idx ON t2(x + y);

SELECT *
FROM t2
WHERE y + x = 22;

SELECT *
FROM t2
WHERE x + y = 22;</code></pre></div><p>The two predicates return the same rows, but they are not written the same way. With SQLite, the index definition stores <code>x + y</code>, so the second query lines up with the indexed expression while the first query does not. That rule gives you a practical habit to keep in mind while writing queries. When an index is built on an expression, keep the predicate in the same form as the indexed expression instead of rewriting it into a nearby equivalent form.</p><p>MySQL has a similar idea with functional key parts. If an index is built on <code>SUBSTRING(col1, 1, 10)</code>, the predicate needs to ask for that same expression form to benefit from the index. Change the arguments or alter the expression, and the match can disappear. That is an important part of how expression indexes behave. The database is not promising fast access for every related transformation. It is storing a particular computed value, and the query has to ask for that same value in a compatible form.</p><p>PostgreSQL follows the same general logic. If the index is built on <code>(first_name || ' ' || last_name)</code>, then a predicate on that same concatenation is the natural fit for the index. If the index is built on <code>upper(last_name)</code>, then a query filtering on <code>upper(last_name)</code> lines up with the stored value. Small wording changes in documentation from one engine to the next can make this feel more abstract than it is. What helps most is remembering what the index actually contains. It contains the result of the indexed expression, so the search has to target that same result rather than a loosely related version of it.</p><h3>Good Fits for Expression Indexes</h3><p>Some queries do not search raw column values. They search transformed text, a calendar date taken from a timestamp, or a numeric result computed from other columns. That is where expression indexes tend to help most. The common thread is repetition. When the same derived value keeps appearing in filters, sorting, or lookup logic, storing that derived value in the index gives the optimizer a direct route to the value named in the predicate. That does not mean every expression belongs in an index, but a recurring query form can line up very well with this feature.</p><h4>Lowercased Text Searches</h4><p>Case-insensitive lookup is one of the most natural places for an expression index. Text may be stored exactly as entered, with uppercase and lowercase letters preserved, while the search condition folds both sides to a common form through <code>lower()</code> or <code>upper()</code>. If the only index is on the original text column, the database may have to apply that text function during filtering before it can decide which rows match. When the transformed value is stored in an index, the search can target that stored form directly.</p><p>PostgreSQL can express that directly:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;93e050a4-3a9a-4636-b935-ea45ce97d96f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE INDEX users_email_lower_idx
ON users (lower(email));

SELECT user_id, email
FROM users
WHERE lower(email) = 'alex@example.com';</code></pre></div><p>That index is ordered by the lowercased email value, so the predicate is searching the same value stored in the index. When a table is queried this way again and again, indexing the transformed text can be far more helpful than keeping only an index on the original <code>email</code> column. PostgreSQL also allows a <code>UNIQUE</code> expression index here, which means a system can block entries that differ only by case rather than treating them as separate values.</p><p>Oracle reaches the same goal with a function-based index:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;d27974fa-a6c6-436f-9f80-af18c4c33c3c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE INDEX emp_last_name_upper_idx
ON employees (UPPER(last_name));

SELECT employee_id, last_name
FROM employees
WHERE UPPER(last_name) = 'BROOKS';</code></pre></div><p>Searches like this are a strong fit because the expression is stable, repeated, and tied closely to the filter itself. A database can still search text without this type of index, but the engine has a far better chance of picking an indexed route when the transformed value already exists in index form. That becomes more helpful as the table grows and the same predicate keeps appearing in login checks, customer lookups, or administrative searches.</p><h4>Date Buckets</h4><p>Reporting queries frequently slice timestamps down to a day, month, or year. That sounds minor, yet it changes what the predicate is asking for. An index on <code>order_ts</code> is ordered by full timestamp values, while a filter on <code>CONVERT(date, order_ts)</code> or a related date-only expression is asking for a derived calendar value. If the same date bucket keeps appearing in filters, grouping, or joins, an expression index or an indexed computed column gives that derived value a place in indexed storage instead of forcing repeated recalculation during filtering.</p><p>SQL Server handles this through a computed column that can be indexed:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;d921e1c7-0c62-427d-8f61-835f3663e80a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">ALTER TABLE Sales
ADD order_day AS CONVERT(date, order_ts) PERSISTED;

CREATE INDEX ix_sales_order_day
ON Sales(order_day);

SELECT order_id, order_ts
FROM Sales
WHERE order_day = CONVERT(date, '20260501', 112);</code></pre></div><p>Two details help frame why this form is useful. First, SQL Server places rules on indexed computed columns. The expression has to be deterministic, and index-column expressions also have to be precise. Second, Microsoft recommends explicit date conversion with a deterministic style when date literals are involved. That avoids string-to-date ambiguity and keeps the computed value in a form the engine can treat consistently.</p><p>PostgreSQL, Oracle, and MySQL can support this broader idea as well, though the syntax changes from engine to engine. The reason date buckets fit so well is tied to the query itself. If the business question keeps landing on the date pulled from a timestamp instead of the full timestamp value, the indexed value that helps most is the extracted date rather than the raw column.</p><h4>Derived Numeric Values</h4><p>Arithmetic expressions are another strong match. Filters and sorts sometimes care less about a stored number than about a value derived from it, such as the magnitude of a balance change, a discounted total, or a formula built from several columns. Oracle allows function-based indexes on arithmetic expressions, and SQLite gives a practical example with <code>abs(amt)</code>. In both cases, the same idea applies. The query is not asking for the base value in its original form. It is asking for a computed numeric result, so indexing that result can be a better fit than indexing only the source columns.</p><p>SQLite&#8217;s <code>abs(amt)</code> example captures this well:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;14d1219e-fe4b-4ad6-8774-de89250ab774&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE INDEX acctchng_magnitude_idx
ON account_change(acct_no, abs(amt));

SELECT acct_no, amt
FROM account_change
WHERE acct_no = 17
ORDER BY abs(amt) DESC;</code></pre></div><p>That index does two jobs at the same time. It groups rows by account number and then orders them by the absolute value of the amount change. If the query keeps asking for rows in that form, indexing the derived numeric result gives the planner much better alignment with the requested order. The point is not limited to <code>abs()</code>. Similar thinking applies to recurring formulas such as <code>price * quantity</code>, <code>subtotal - discount</code>, or a rating score built from stored columns. What makes these expressions a good fit is repetition. The more the query logic returns to the same derived number, the more sense it makes to store that number in index form.</p><h4>Write Cost Matching Rules Engine Limits</h4><p>Read speed is only half of the decision. Every expression index has a write-side cost because the database has to compute the expression when relevant rows are inserted or updated. PostgreSQL states that expression indexes are relatively expensive to maintain because the derived value must be computed for each row insertion and for each non-HOT update. Oracle makes a similar point and states that function-based indexes on columns that are modified frequently are expensive for the database to maintain. That is why the best fit is usually a recurring expression that helps enough on reads to justify the added write overhead.</p><p>Engine rules can narrow what is allowed. SQL Server requires indexed computed-column expressions to be deterministic, and index-column expressions must also be precise. That rules out a range of expressions tied to <code>float</code> or <code>real</code> values, cross-row calculations, or nondeterministic behavior. It also places <code>SET</code> option requirements on sessions that create, change, and query these indexes. SQLite has a similar restriction in spirit. Functions inside expression indexes must be deterministic, and subqueries are not allowed. MySQL ties functional index parts to hidden virtual generated columns, which brings generated-column restrictions along with them. That means no subqueries, parameters, variables, stored functions, or loadable functions inside those indexed expressions. MySQL also does not allow primary keys to include these functional parts.</p><p>Oracle has a few limits worth keeping in view as well. A function-based index cannot contain <code>NULL</code>, so <code>NVL</code> may be needed when null-producing expressions are part of the search logic. Oracle also requires any user-defined PL/SQL function referenced by the index expression to be declared <code>DETERMINISTIC</code>, and if the function&#8217;s semantics change later, dependent function-based indexes need a rebuild so they stop reflecting the prior function behavior. Those rules are not just side notes. They affect daily query tuning, storage choices, and update cost. Expression indexes pay off best when the expression is stable, the predicate repeats, and the engine can legally store that derived result in a dependable form.</p><h3>Conclusion</h3><p>Expression indexes let a database store the result of a repeated calculation inside the index instead of relying only on raw column values. That changes how the optimizer can search, because predicates on lowercased text, date-derived values, or numeric formulas can match an indexed computed result rather than forcing extra row-by-row evaluation. The tradeoff is added index maintenance during inserts and updates, which is why they make the most sense when the same derived expression appears frequently enough to justify the extra storage and write cost.</p><ol><li><p><a href="https://www.postgresql.org/docs/current/indexes-expressional.html">PostgreSQL Documentation on Indexes on Expressions</a></p></li><li><p><a href="https://www.postgresql.org/docs/current/sql-createindex.html">PostgreSQL </a><code>CREATE INDEX</code><a href="https://www.postgresql.org/docs/current/sql-createindex.html"> Documentation</a></p></li><li><p><a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/adfns/indexes.html">Oracle Database Documentation on Function-Based Indexes</a></p></li><li><p><a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-INDEX.html">Oracle Database </a><code>CREATE INDEX</code><a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-INDEX.html"> Documentation</a></p></li><li><p><a href="https://dev.mysql.com/doc/refman/en/create-index.html">MySQL Documentation on </a><code>CREATE INDEX</code><a href="https://dev.mysql.com/doc/refman/en/create-index.html"> and Functional Key Parts</a></p></li><li><p><a href="https://learn.microsoft.com/en-us/sql/relational-databases/indexes/indexes-on-computed-columns?view=sql-server-ver17">SQL Server Documentation on Indexes on Computed Columns</a></p></li><li><p><a href="https://www.sqlite.org/expridx.html">SQLite Documentation on Indexes on Expressions</a></p></li></ol><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share Alexander Obregon's Substack&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share Alexander Obregon's Substack</span></a></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ssy4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6ea5ff-228c-48c6-99b1-da29fecd76e8_306x306.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ssy4!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6ea5ff-228c-48c6-99b1-da29fecd76e8_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!ssy4!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6ea5ff-228c-48c6-99b1-da29fecd76e8_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!ssy4!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6ea5ff-228c-48c6-99b1-da29fecd76e8_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!ssy4!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6ea5ff-228c-48c6-99b1-da29fecd76e8_306x306.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ssy4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6ea5ff-228c-48c6-99b1-da29fecd76e8_306x306.png" width="306" height="306" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/df6ea5ff-228c-48c6-99b1-da29fecd76e8_306x306.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:306,&quot;width&quot;:306,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ssy4!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6ea5ff-228c-48c6-99b1-da29fecd76e8_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!ssy4!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6ea5ff-228c-48c6-99b1-da29fecd76e8_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!ssy4!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6ea5ff-228c-48c6-99b1-da29fecd76e8_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!ssy4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6ea5ff-228c-48c6-99b1-da29fecd76e8_306x306.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://freesvg.org/mono-sql">Image Source</a></figcaption></figure></div>]]></content:encoded></item><item><title><![CDATA[Covering Indexes in SQL Queries]]></title><description><![CDATA[The idea of a covering index makes more sense when you tie it to what the database is actually doing during a query.]]></description><link>https://alexanderobregon.substack.com/p/covering-indexes-in-sql-queries</link><guid isPermaLink="false">https://alexanderobregon.substack.com/p/covering-indexes-in-sql-queries</guid><dc:creator><![CDATA[Alexander Obregon]]></dc:creator><pubDate>Mon, 27 Apr 2026 22:28:26 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!hfd5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F627d1b41-cbaa-4efb-a3ba-3e07ad2c2e5c_306x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!QoVD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65e40ed1-b0bd-4074-a848-c55080cd91b6_800x373.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QoVD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65e40ed1-b0bd-4074-a848-c55080cd91b6_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!QoVD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65e40ed1-b0bd-4074-a848-c55080cd91b6_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!QoVD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65e40ed1-b0bd-4074-a848-c55080cd91b6_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!QoVD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65e40ed1-b0bd-4074-a848-c55080cd91b6_800x373.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QoVD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65e40ed1-b0bd-4074-a848-c55080cd91b6_800x373.png" width="800" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/65e40ed1-b0bd-4074-a848-c55080cd91b6_800x373.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!QoVD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65e40ed1-b0bd-4074-a848-c55080cd91b6_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!QoVD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65e40ed1-b0bd-4074-a848-c55080cd91b6_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!QoVD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65e40ed1-b0bd-4074-a848-c55080cd91b6_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!QoVD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65e40ed1-b0bd-4074-a848-c55080cd91b6_800x373.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://commons.wikimedia.org/wiki/File:Sql_data_base_with_logo.png">Image Source</a></figcaption></figure></div><p>The idea of a covering index makes more sense when you tie it to what the database is actually doing during a query. It is not a separate index type with its own <code>CREATE INDEX</code> command. It is just a way to describe a case where one index already contains every column a query needs, so the database can return the result from the index itself instead of going back to the base table for extra column values. In SQL Server, that can happen with a nonclustered index. In PostgreSQL, it lines up with index-only scans when the needed columns are stored in the index. In MySQL, the same idea applies when an InnoDB query can be answered from index data alone.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>What Covering Means</h3><p>Coverage belongs to a query, not to an index name by itself. A given index becomes covering only when it already stores every column that a statement needs to filter rows, join rows, sort rows, group rows, or return values to the client. Change the statement, and the answer can change with it. Some queries can be fully satisfied from an index, while closely related queries against the same table still have to reach back to the base table for extra column values. That distinction removes a lot of confusion around the term. People sometimes treat covering index as though it names a separate structure, but the phrase is really pointing to a match between a query and the columns stored in an index entry. The database is not switching into a different family of indexes. It is simply finding that the chosen index already holds everything needed for that statement.</p><h4>Query First</h4><p>Coverage starts with the statement itself. Begin with the <code>SELECT</code> list, then move to the search condition, then any join columns, sort columns, or grouped columns. If any part of that statement asks for a column that is not stored in the index entry, the index can still help locate rows, but it does not fully cover the query.</p><p>Take a small table with customer account data. Say the table has <code>customer_id</code>, <code>email</code>, <code>phone_number</code>, and <code>created_at</code>. An index on <code>customer_id</code> is useful for locating a customer quickly, but that does not mean every query for that customer can be answered from the index alone.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;23a7bdcd-a8d7-428d-808e-d4e9508436b7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE INDEX idx_customer_accounts_customer_id
ON customer_accounts(customer_id);

SELECT email
FROM customer_accounts
WHERE customer_id = 1201;</code></pre></div><p>That index helps the database find the matching row for <code>customer_id = 1201</code>, yet the result still asks for <code>email</code>. If <code>email</code> is not stored in the index entry, the engine has to use the index to find the row and then visit the base table to fetch the missing value.</p><p>Now compare that with a second version:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;84815afb-0e70-4ef1-96e0-3dbaef8cb735&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE INDEX idx_customer_accounts_customer_id_email
ON customer_accounts(customer_id, email);

SELECT email
FROM customer_accounts
WHERE customer_id = 1201;</code></pre></div><p>This time, both the search column and the returned column are stored in the index. That gives the optimizer the option to satisfy the statement from the index itself without reading the table row for <code>email</code>.</p><p>Small changes to the query can remove that benefit right away. Watch what happens when the <code>SELECT</code> list grows:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;b3b0cf91-5804-4432-b0de-803dbf88e025&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT email, phone_number
FROM customer_accounts
WHERE customer_id = 1201;</code></pre></div><p>If the index stores only <code>customer_id</code> and <code>email</code>, the database still needs the base table to fetch <code>phone_number</code>. Nothing about the table changed, and nothing about the first two columns changed. The difference is that the query now asks for one more value. That is why the query comes first when you judge coverage.</p><p>Filtering rules follow the same idea. Returned columns are only part of the story. Search conditions count too, because the engine still has to evaluate them. Say a support table has <code>ticket_id</code>, <code>status</code>, <code>opened_at</code>, and <code>agent_id</code>. An index that stores <code>status</code> and <code>opened_at</code> can cover one statement but fall short on a very similar one.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;1b2e0888-72f2-4815-b5a5-914e715bfb74&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE INDEX idx_support_tickets_status_opened_at
ON support_tickets(status, opened_at);

SELECT opened_at
FROM support_tickets
WHERE status = 'OPEN';</code></pre></div><p>That query asks for <code>opened_at</code> and filters on <code>status</code>, so both needed columns are already present in the index entry. Now look at this version:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;7858f035-9dac-428a-a6fa-bc4d30b154e3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT opened_at
FROM support_tickets
WHERE status = 'OPEN'
  AND agent_id = 14;</code></pre></div><p>Adding the filter on <code>agent_id</code> changes the result. If <code>agent_id</code> is not stored in the index entry, the database may still use the index to narrow the search on <code>status</code>, but it cannot stay in the index for the whole statement. It has to read the table row to check <code>agent_id</code>.</p><p>Sorting and grouping can affect coverage too. A query that returns one column and orders by a different one is asking for both. Grouped queries ask the engine to read grouping columns and aggregate input values. If those referenced columns are missing from the index entry, the index is no longer enough by itself. Coverage, then, is tied to the full statement rather than just to the <code>SELECT</code> list.</p><h4>Why Index-Only Access Helps</h4><p>Most index-based lookups follow a two-step flow. First, the engine walks the index structure to locate matching entries. After that, it uses the row locator stored in the index entry to fetch the base table row if some needed column is missing. That second trip is the part a covering index can remove.</p><p>Think about what the database has to touch during execution. Index pages are usually smaller than full table rows, so more of them fit in memory and fewer bytes have to move around when the query stays inside the index. Once the engine starts jumping from index pages to table pages for every matching row, extra reads and extra page visits enter the plan. On a small result set, the gain can be modest. On a statement that matches a long run of rows, skipping those table lookups can change cost in a noticeable way.</p><p>Take this query against a shipment table:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;8097c01b-f733-4e01-8d9d-4c78a01e1ebe&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT shipment_status
FROM shipments
WHERE tracking_number = 'ZX-88491';</code></pre></div><p>If the chosen index stores <code>tracking_number</code> and <code>shipment_status</code>, the database already has the values it needs in the index entry. It can search for the tracking number and return the status without fetching the full shipment row. That keeps the amount of data touched tighter than a plan that has to leave the index and visit the table.</p><p>Now compare that with a broader request:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;eb9426b1-90c0-4a17-a037-b005b26d38d9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT *
FROM shipments
WHERE tracking_number = 'ZX-88491';</code></pre></div><p>A statement like that asks for every column in the row. Unless the index happens to contain the entire row, the engine cannot stay inside the index. It has to fetch the table row after finding the match. That is part of the reason narrow <code>SELECT</code> lists help so much. They give the optimizer a better chance to satisfy the statement from index data alone.</p><p>Coverage can also help with repeated lookups where the same narrow query runs again and again. Think of account checks, order status screens, or service calls that ask for a very small set of fields. If the statement requests only a few values and those values already live in the index entry, the engine can avoid extra table reads every time that query runs.</p><p>Engines do not all handle index-only access in exactly the same way internally, but the broad idea stays the same. The more fully the index can answer the statement by itself, the less the engine has to reach back to the base table. That does not mean a covered query will always be chosen by the optimizer, and it does not mean every engine can avoid every table touch in every storage condition. What stays true is the main point behind the term. Coverage helps by reducing or removing the need for that second trip from index entry to table row.</p><p>Read it as a storage question tied to a statement, and if the index entry already carries the values the query needs, the engine has fewer places to visit. If even one needed value is absent, the table visit returns. That is the whole reason coverage can have such a strong effect in day-to-day SQL tuning.</p><h3>Included Columns Within Composite Index Design</h3><p>Stored payload values and search-column order decide how wide an index becomes, how much data it carries, and what the optimizer can do with it. That is where included columns and composite indexes meet. Some engines let you store extra return values at the leaf level without making those values part of index navigation, while others reach the same result by placing extra columns directly in the composite index itself. The tradeoff stays the same. Wider indexes can satisfy more statements from index data, but they also take more space and add maintenance during inserts, updates, and deletes.</p><h4>SQL Server INCLUDE</h4><p>Within SQL Server, nonclustered indexes have a direct way to carry extra columns through <code>INCLUDE</code>. Those included columns live only at the leaf level, so they are stored with the final index rows rather than guiding navigation through upper B-tree levels. That separation is the whole point of the feature. Search columns belong in the indexed column list. Return-only columns usually fit better in <code>INCLUDE</code>, because they stay out of the ordered search sequence and out of the size limits that apply to indexed search columns.</p><p>Viewed from the storage side, the split is fairly practical. Search columns help the engine narrow the scan and preserve index order. Included columns are payload. SQL Server can read them from the leaf page after it reaches the matching slice of the index. SQL Server also allows included columns to hold some data types that are not valid as indexed search columns, though older large-object restrictions still apply in a few cases. That makes <code>INCLUDE</code> useful when the query returns columns that you do not want participating in index order.</p><p>Take an order history table like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;182a8423-7911-4338-ae16-560862404083&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE INDEX IX_order_history_customer_date
ON order_history (customer_id, order_date)
INCLUDE (order_total, payment_status);</code></pre></div><p>That definition puts <code>customer_id</code> and <code>order_date</code> in the searchable portion of the index. <code>order_total</code> and <code>payment_status</code> are carried as leaf-level payload. With that layout in place, a statement like this can benefit:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;caca09f7-e2db-40b6-804a-a351c5e07d04&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT order_date, order_total, payment_status
FROM order_history
WHERE customer_id = 8843
  AND order_date &gt;= '2026-01-01';</code></pre></div><p>With that index, the columns that narrow the row set are still at the front, while the returned values are carried without becoming part of index order. If <code>order_total</code> had been moved into the indexed column list even though the statement never filters or sorts on it, the index would become wider in ways that do not help navigation.</p><p>Clustered tables add a detail that changes how nonclustered indexes are read. On a table with a clustered index, SQL Server folds the clustered index columns into each nonunique nonclustered index as the row locator. That means some statements can read clustered columns from a nonclustered index even when those columns are not listed directly in the nonclustered definition.</p><p>A billing table makes that easier to see:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;69a1ae63-137c-4fb7-8c39-d31ac9da3304&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE INDEX IX_billing_account_due_date
ON billing_entries (account_id, due_date)
INCLUDE (balance_due);</code></pre></div><p>If the table is clustered on <code>invoice_id</code>, SQL Server can carry that clustered column set in the nonclustered row locator. A query that returns <code>invoice_id</code>, <code>due_date</code>, and <code>balance_due</code> may already have access to those values from the nonclustered index row, depending on the final plan and index properties. That is useful, but it is still worth keeping these indexes disciplined. Very wide nonclustered indexes take more space, fit fewer rows on a page, and add extra maintenance when data changes.</p><h4>PostgreSQL INCLUDE</h4><p>Inside PostgreSQL, <code>INCLUDE</code> serves a very similar purpose. Included values are payload columns stored in the index, but they are not part of the search columns that guide index scans. That gives you a practical split between columns that help find rows and columns that are merely returned to the client. For unique indexes, the uniqueness rule applies only to the indexed search columns, not to the payload columns named in <code>INCLUDE</code>.</p><p>That distinction helps keep the searchable portion of the index tighter. It also avoids dragging return-only columns into the uniqueness rule or into the ordered scan path. Read it as a way to keep the navigation portion lean while still carrying extra values in the leaf tuple.</p><p>Take a user session table like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;9e7e6a31-fa41-4ee0-b82d-2cce93a3b76b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE UNIQUE INDEX ux_user_sessions_token
ON user_sessions (session_token)
INCLUDE (user_id, expires_at);</code></pre></div><p>With that index, uniqueness applies to <code>session_token</code>. The columns <code>user_id</code> and <code>expires_at</code> are payload values carried with the leaf row. A query that looks up a session by token and returns those two extra values can benefit from that split without making them part of the unique indexed search sequence.</p><p>Older PostgreSQL habits help explain why this feature is useful. Before <code>INCLUDE</code> existed, people sometimes pushed return columns onto the end of the indexed column list to get a covering effect. That older style still functions, but it also turns those return columns into part of the indexed search definition. Compare these two forms:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;2b728afa-50ad-4924-93f1-397393daf6cf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE INDEX idx_product_lookup_old_style
ON product_lookup (sku, product_name, retail_price);</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;ca631fae-7ed4-4a43-a599-b395472980b0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE INDEX idx_product_lookup_sku
ON product_lookup (sku)
INCLUDE (product_name, retail_price);</code></pre></div><p>The newer form keeps <code>sku</code> as the searchable column while storing <code>product_name</code> and <code>retail_price</code> as payload. That usually makes the intent easier to read and keeps upper index levels smaller than the old habit of pushing every returned value into the indexed column list.</p><p>Width still needs attention here because large payload columns can bloat the index, and PostgreSQL has tuple-size limits for index entries. If the leaf tuple grows too large for the index method, inserts can fail. Payload columns also do not participate in navigation, so there is little value in packing wide data into <code>INCLUDE</code> unless queries really benefit from reading those values from the index. PostgreSQL also keeps some boundaries around the feature. Expressions cannot be placed in <code>INCLUDE</code>, and support is limited to B-tree, GiST, and SP-GiST.</p><h4>Column Order in Composite Indexes</h4><p>Order within a composite index changes how the engine can read it. The first indexed column is not interchangeable with the second or third. Databases read composite indexes from left to right, so the early columns in the definition do most of the narrowing. That is why composite indexes usually begin with columns that the query filters most directly, then continue with columns that support more filtering or requested sort order. Payload values belong later in the definition, or in <code>INCLUDE</code> when the engine offers that feature.</p><p>The left-to-right rule is easier to follow with this inventory example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;ccac8af0-590f-4e19-930f-0a5253f77560&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE INDEX idx_inventory_location_sku_status
ON inventory_snapshots (location_id, sku, stock_status);</code></pre></div><p>That index can support lookups on <code>location_id</code>, on <code>location_id</code> with <code>sku</code>, and on all three columns in that same left-to-right sequence. It is much less useful for a search on <code>sku</code> by itself, because the index is ordered first by <code>location_id</code>. Column order is doing real physical sorting inside the index, not just listing names.</p><p>Sorting is affected too. If a query filters on an early column and requests rows ordered by the next indexed column, the engine may be able to read the rows from the index in the requested order. Reverse those columns in the index, and that benefit can disappear. That does not mean every composite index should be built around <code>ORDER BY</code>, but it does mean the sequence should reflect the statement forms you care about most.</p><p>Trailing payload values deserve restraint relatively speaking. Columns at the front of a composite index should earn their place by helping the engine find or order rows. Return-only values belong later, or outside the indexed sequence when <code>INCLUDE</code> is available. Pushing payload values too far forward makes the navigation portion wider without giving the scan much back in return.</p><h4>MySQL Without INCLUDE</h4><p>In MySQL, ordinary secondary indexes do not have a general <code>INCLUDE</code> clause, so the layout decision is more direct. If a query needs extra columns from the index, those columns usually have to be part of the composite index definition itself. That puts column order right in the middle of the planning process, because the same column list is carrying both navigation columns and any extra values you want available from the index.</p><p>The leftmost-prefix rule is what keeps that planning grounded. With an index on <code>(store_id, item_code, price)</code>, MySQL can use the index for lookups on <code>store_id</code>, on <code>store_id</code> with <code>item_code</code>, and on all three columns in that order. A lookup that starts with <code>item_code</code> alone does not get the same benefit. That is why extra return columns usually belong near the tail end of the composite index, after the columns that truly narrow the row set.</p><p>Take this sales table example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;ddd317f4-1453-4790-bf59-895cb520081c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE INDEX idx_sales_region_day_total
ON sales_daily (region_id, sales_day, gross_total);</code></pre></div><p>That order gives MySQL a search path that starts with <code>region_id</code>, then narrows further by <code>sales_day</code>. The trailing <code>gross_total</code> value can still be read from the same index entry if the query returns it. Reverse the definition and start with <code>gross_total</code>, and the index becomes far less useful for the searches most people would run against that table.</p><p>InnoDB adds one more storage detail that changes how secondary indexes are read. Every secondary index record already carries the table&#8217;s primary identifier columns. That means a secondary index can sometimes satisfy a query that returns the primary identifier, even when you did not list that column in the secondary index definition. The tradeoff is storage. Long primary identifiers make every secondary index wider, because those values are copied into each secondary index record.</p><p>Read this example with that rule in mind:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;94fd5347-2e33-40e0-b111-0e46e0f99518&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE INDEX idx_catalog_category_sku
ON catalog_items (category_id, sku);</code></pre></div><p>If <code>catalog_items</code> has a primary identifier such as <code>item_id</code>, InnoDB stores that identifier in each secondary index row. Queries that search by <code>category_id</code> and <code>sku</code> can use this index, and a statement that also returns <code>item_id</code> may still stay within the secondary index data. That hidden carriage of the primary identifier is useful, but it also explains why a compact primary identifier is a good habit in InnoDB. Every extra byte there gets repeated across all secondary indexes on the table.</p><h3>Conclusion</h3><p>At the engine level, covering comes down to where the needed column values already live. If an index holds the search columns and the returned columns, the database can stay in that index for more of the statement instead of hopping back to the base table row. Included columns in engines that support them let extra return values ride at the leaf level without changing search order, while composite index order still controls how the engine narrows rows and reads them in sequence. Put those parts in the right place, and the index stops being only a shortcut to the row and starts carrying the data the statement needs.</p><ol><li><p><em><a href="https://learn.microsoft.com/en-us/sql/relational-databases/indexes/create-indexes-with-included-columns?view=sql-server-ver17">SQL Server Indexes with Included Columns</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/relational-databases/sql-server-index-design-guide?view=sql-server-ver17">SQL Server Index Architecture and Design Guide</a></em></p></li><li><p><em><a href="https://www.postgresql.org/docs/current/indexes-index-only-scans.html">PostgreSQL Index-Only Scans and Covering Indexes</a></em></p></li><li><p><em><a href="https://www.postgresql.org/docs/current/sql-createindex.html">PostgreSQL CREATE INDEX</a></em></p></li><li><p><em><a href="https://dev.mysql.com/doc/refman/8.4/en/multiple-column-indexes.html">MySQL Multiple-Column Indexes</a></em></p></li><li><p><em><a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-index-types.html">MySQL InnoDB Clustered and Secondary Indexes</a></em></p></li></ol><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share Alexander Obregon's Substack&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share Alexander Obregon's Substack</span></a></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!hfd5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F627d1b41-cbaa-4efb-a3ba-3e07ad2c2e5c_306x306.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!hfd5!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F627d1b41-cbaa-4efb-a3ba-3e07ad2c2e5c_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!hfd5!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F627d1b41-cbaa-4efb-a3ba-3e07ad2c2e5c_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!hfd5!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F627d1b41-cbaa-4efb-a3ba-3e07ad2c2e5c_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!hfd5!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F627d1b41-cbaa-4efb-a3ba-3e07ad2c2e5c_306x306.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!hfd5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F627d1b41-cbaa-4efb-a3ba-3e07ad2c2e5c_306x306.png" width="306" height="306" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/627d1b41-cbaa-4efb-a3ba-3e07ad2c2e5c_306x306.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:306,&quot;width&quot;:306,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!hfd5!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F627d1b41-cbaa-4efb-a3ba-3e07ad2c2e5c_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!hfd5!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F627d1b41-cbaa-4efb-a3ba-3e07ad2c2e5c_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!hfd5!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F627d1b41-cbaa-4efb-a3ba-3e07ad2c2e5c_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!hfd5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F627d1b41-cbaa-4efb-a3ba-3e07ad2c2e5c_306x306.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://freesvg.org/mono-sql">Image Source</a></figcaption></figure></div>]]></content:encoded></item><item><title><![CDATA[Plan Cache Behavior in SQL Server]]></title><description><![CDATA[Stored procedures can run fast on one call and then slow down on the next, even if the T-SQL text stays exactly the same.]]></description><link>https://alexanderobregon.substack.com/p/plan-cache-behavior-in-sql-server</link><guid isPermaLink="false">https://alexanderobregon.substack.com/p/plan-cache-behavior-in-sql-server</guid><dc:creator><![CDATA[Alexander Obregon]]></dc:creator><pubDate>Mon, 20 Apr 2026 17:47:06 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!EEOR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F931bd81e-a37e-43a3-a5ef-85cec38375b8_306x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3P0Q!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868df1a8-aebf-47e9-b79a-08f426ecc873_800x373.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3P0Q!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868df1a8-aebf-47e9-b79a-08f426ecc873_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!3P0Q!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868df1a8-aebf-47e9-b79a-08f426ecc873_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!3P0Q!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868df1a8-aebf-47e9-b79a-08f426ecc873_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!3P0Q!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868df1a8-aebf-47e9-b79a-08f426ecc873_800x373.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3P0Q!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868df1a8-aebf-47e9-b79a-08f426ecc873_800x373.png" width="800" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/868df1a8-aebf-47e9-b79a-08f426ecc873_800x373.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!3P0Q!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868df1a8-aebf-47e9-b79a-08f426ecc873_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!3P0Q!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868df1a8-aebf-47e9-b79a-08f426ecc873_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!3P0Q!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868df1a8-aebf-47e9-b79a-08f426ecc873_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!3P0Q!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868df1a8-aebf-47e9-b79a-08f426ecc873_800x373.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://commons.wikimedia.org/wiki/File:Sql_data_base_with_logo.png">Image Source</a></figcaption></figure></div><p>Stored procedures can run fast on one call and then slow down on the next, even if the T-SQL text stays exactly the same. SQL Server compiles an execution plan, keeps that plan in memory, and tries to reuse it so it does not have to compile the same statement again and again. Reuse helps save CPU and can keep request volume moving, yet it also opens the door to trouble when a cached plan was built for one parameter value and then reused for a very different one. That is the point where plan reuse, parameter sniffing, and uneven data distribution start affecting one another. Current SQL Server versions still follow those same basics, but newer engine features and Query Store options give you better ways to test bad cases and keep them from dragging down later executions.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>How SQL Server Reuses Plans</h3><p>After the first compilation, SQL Server tries to match later executions to a saved plan instead of building a fresh one every time. That match depends on more than procedure text alone. The engine checks the cached entry against the surrounding execution context, which includes things like database identity, session settings, and plan attributes tied to cache lookup. When those line up, SQL Server can run the saved plan again and avoid extra compile cost.</p><p>Memory is the second part of that story. For reuse to happen, the saved plan still needs to be present when the next execution arrives, and that depends on cache pressure and invalidation events such as schema or statistics changes. Reuse therefore comes down to two related checks. SQL Server needs a compatible cached plan, and that plan still needs to remain in memory when the next execution arrives.</p><h4>Compilation Reuse Cache Keys</h4><p>Matching starts differently for stored procedures and for ad hoc batches. A stored procedure gives SQL Server a stable object definition, which makes reuse more predictable. Ad hoc text is stricter. Small text changes, different qualification of object names, or different session settings can split cache entries. That is why a procedure call usually reuses plans more reliably than a batch sent with literals embedded directly in the text.</p><p>SQL Server also separates cached plans by plan store type. Plans tied to persisted objects such as stored procedures, functions, and triggers live in a different cache store from ad hoc or prepared statements. When you inspect cache DMVs, that split helps explain why two cached entries can look related while still landing in separate categories. Procedure-heavy workloads and literal-heavy ad hoc traffic usually leave different fingerprints in cache for that reason.</p><p>To watch reuse happen, start with a small procedure and run it twice:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;45b35402-4b54-4a24-9208-292ed7e64bab&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">USE tempdb;
GO

CREATE OR ALTER PROC dbo.usp_ObjectCount
AS
BEGIN
    SET NOCOUNT ON;

    SELECT COUNT(*) AS object_count
    FROM sys.objects
    WHERE type = 'U';
END;
GO

EXEC dbo.usp_ObjectCount;
EXEC dbo.usp_ObjectCount;
GO</code></pre></div><p>Run a cache query after those calls so you can inspect the saved entry and its reuse count:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;c2a2e9f7-518e-438d-89fe-8d74061aaf7e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT cp.usecounts,
       cp.objtype,
       cp.cacheobjtype,
       st.text
FROM sys.dm_exec_cached_plans AS cp
CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) AS st
WHERE st.text LIKE N'%usp_ObjectCount%';
GO</code></pre></div><p><code>usecounts</code> tells you how frequently SQL Server has reused that cached plan after it entered cache. <code>objtype</code> and <code>cacheobjtype</code> help identify what kind of entry you are looking at. If the plan stayed in cache between executions, the reuse count should rise instead of giving you separate one-use entries. That distinction is helpful early on because it separates true plan reuse from repeated compilation.</p><p>Cache lookup is not driven by T-SQL text alone. Plan attributes attached to the compiled plan tell SQL Server what has to match before reuse can happen. <code>sys.dm_exec_plan_attributes</code> exposes those attributes, and the <code>is_cache_key</code> column marks which ones participate in cache lookup. <code>dbid</code>, <code>objectid</code>, and <code>set_options</code> are familiar examples. <code>set_options</code> causes confusion in plenty of environments because two sessions can run the same procedure text while different plan-cache-affecting <code>SET</code> options lead SQL Server to keep separate entries.</p><p>This query exposes part of that attribute data for the cached procedure plan:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;6c39b9e9-3943-4167-91b2-4d9899cd596f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT st.text,
       pa.attribute,
       pa.value,
       pa.is_cache_key
FROM sys.dm_exec_cached_plans AS cp
CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) AS st
CROSS APPLY sys.dm_exec_plan_attributes(cp.plan_handle) AS pa
WHERE st.text LIKE N'%usp_ObjectCount%'
  AND pa.attribute IN (N'set_options', N'objectid', N'dbid');
GO</code></pre></div><p>Data from that DMV makes cache matching easier to follow because it shows that reuse depends on more than the procedure name. SQL Server also separates the compiled plan from the execution context tied to a specific call. The compiled plan can be shared across executions, while runtime state for a given call lives in a separate structure that is reinitialized before the next execution. Reuse of a compiled plan does not carry row-by-row runtime state from a prior call into the next one.</p><h4>Plan Lifetime Inside Memory</h4><p>Cached plans do not stay in memory forever. SQL Server keeps them there while they still have value and while memory pressure allows it. When memory pressure rises, the engine evaluates cached plans and removes entries whose current cost has dropped low enough to make eviction worthwhile. Frequently reused plans tend to last longer because SQL Server refreshes their standing when they are referenced again, while one-use entries are easier eviction targets.</p><p>Ad hoc entries get special treatment in that process. SQL Server starts an ad hoc plan with a current cost of zero, so a batch compiled and never reused can be removed quickly when memory pressure shows up. That behavior helps explain why literal-heavy ad hoc traffic can fill cache with entries that provide very little reuse benefit. On instances where one-time ad hoc text is a major source of cache churn, <code>OPTIMIZE_FOR_AD_HOC_WORKLOADS</code> can reduce wasted plan cache memory by storing a compiled plan stub on the first execution and saving the full compiled plan only after a later reuse.</p><p>Plan lifetime is affected by more than memory pressure. SQL Server also invalidates plans after changes that make the saved version unsafe or stale. Schema changes such as <code>ALTER TABLE</code>, <code>ALTER VIEW</code>, <code>ALTER PROCEDURE</code>, index creation or removal, and statistics updates can all lead to recompilation. Modern SQL Server versions handle recompilation at the statement level, so a single statement can be recompiled without forcing the entire batch through a full new compile every time.</p><p>For lab testing, a database-scoped cache flush is much safer than wiping the whole instance cache. That lets you watch first-execution behavior for a single database without disturbing unrelated cached plans elsewhere on the server:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;5c1792c5-e6a1-4279-8f3e-4bc3dcb0e179&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">USE tempdb;
GO

ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;
GO

EXEC dbo.usp_ObjectCount;
GO

SELECT cp.plan_handle,
       cp.usecounts,
       cp.cacheobjtype,
       st.text
FROM sys.dm_exec_cached_plans AS cp
CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) AS st
WHERE st.text LIKE N'%usp_ObjectCount%';
GO</code></pre></div><p>Running that sequence gives SQL Server a fresh starting point for the database, then places the procedure plan back into cache on the next execution. That is useful when you want to confirm that a later call is truly a reused execution instead of a leftover cached plan from earlier testing. Wider cache flushes still have their place, but they force unrelated queries to compile again later, which can create a burst of extra compile activity across the server.</p><p>Query Store helps fill a gap that the plan cache does not cover well. The cache is a live memory structure, so entries can disappear long before someone opens a DMV window. Query Store keeps query text, plans, and runtime stats over time, which makes it much more useful for historical review. Plan cache tells you what SQL Server has in memory right now. Query Store tells you what happened across a longer span, which is why the two features complement each other so well.</p><h3>Why One Parameter Can Hurt the Next Execution</h3><p>Trouble starts when SQL Server compiles a statement for the parameter value it sees at compile time and then reuses that same plan for a later call with very different selectivity. Stored procedures, queries sent through <code>sp_executesql</code>, and prepared queries all participate in that behavior. During compilation or recompilation, SQL Server sniffs the current parameter values and passes them to the optimizer so row estimates can be based on those values instead of on a generic average. Uneven value frequency is what turns that behavior into a performance problem. Data on one <code>CustomerID</code> can fill almost the whole table while most other <code>CustomerID</code> values return only a handful of rows. Statistics, density data, and histograms are what the optimizer reads to estimate row counts, and those estimates feed operator choice, memory grants, and join strategy. If the saved plan was compiled for a value from one end of that distribution, the next call can inherit estimates that are badly out of line with the rows it actually touches.</p><h4>What Parameter Sniffing Means</h4><p>Parameter sniffing is SQL Server reading the current parameter value during compilation or recompilation and handing that value to the optimizer before the plan is built. That behavior is normal and, in plenty of cases, beneficial. Procedures that always receive similarly selective values can get a better plan from a sniffed parameter than from a generic estimate. Trouble appears when row counts swing sharply between calls and the cached plan from the first compile is still the plan later executions inherit.</p><p>Take a lookup procedure that filters on <code>CustomerID</code>. If <code>CustomerID = 1</code> returns nearly the whole table, the optimizer can lean toward a scan. If <code>CustomerID = 199500</code> returns a single row, that same predicate can lean toward an index seek with a very different cost profile. SQL Server statistics hold value distribution data in histograms and densities, and the optimizer uses that data to estimate the row count a predicate is likely to return. That is why skewed data can pull the optimizer toward very different plans for different parameter values.</p><p>Local variables change the story a bit. SQL Server cannot sniff the runtime value of a local variable at compile time, so estimates fall back to average-density logic or heuristics instead of a specific incoming value. That can reduce the influence of the current parameter value, but it can also replace a targeted estimate with a broader average that is still not a great fit for the rows the query actually reads.</p><p>This rewrite makes that difference more easy to see:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;f5ef150e-5b97-43e6-b7bc-92f8961a2f2a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE OR ALTER PROC dbo.GetSalesByCustomer
    @CustomerID int
AS
BEGIN
    SET NOCOUNT ON;

    SELECT SalesID, CustomerID, OrderDate, Amount
    FROM dbo.SalesSkew
    WHERE CustomerID = @CustomerID;
END;
GO

CREATE OR ALTER PROC dbo.GetSalesByCustomerLocal
    @CustomerID int
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @LocalCustomerID int = @CustomerID;

    SELECT SalesID, CustomerID, OrderDate, Amount
    FROM dbo.SalesSkew
    WHERE CustomerID = @LocalCustomerID;
END;
GO</code></pre></div><p>The first procedure gives the optimizer the incoming parameter value during compilation. The second hides that value behind a local variable, so the estimate is based on average-density behavior instead of a sniffed parameter. Neither form is universally better. The value distribution in the table is what decides which choice looks good and which one turns costly.</p><h4>Lab Schema You Can Build</h4><p>Reproducing this issue in a lab is far less messy than trying to infer it from a busy production database. Small lab data with heavy skew, an index on the filter column, and a stored procedure with a single equality predicate is enough to make the difference visible. SQL Server 2022 ties Parameter Sensitive Plan optimization to compatibility level <code>160</code>, and Query Store is turned on below so you can inspect plan history after the tests. Query Store is on by default for new databases in SQL Server 2022, but an explicit setting keeps the lab self-contained.</p><p>Start with this setup:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;00690151-35d7-4421-99d6-b2350bfbb6ac&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">USE master;
GO

DROP DATABASE IF EXISTS PlanReuseLab;
GO

CREATE DATABASE PlanReuseLab;
GO

ALTER DATABASE PlanReuseLab SET COMPATIBILITY_LEVEL = 160;
ALTER DATABASE PlanReuseLab SET QUERY_STORE = ON;
GO

USE PlanReuseLab;
GO

CREATE TABLE dbo.SalesSkew
(
    SalesID     int IDENTITY(1,1) NOT NULL
        CONSTRAINT PK_SalesSkew PRIMARY KEY,
    CustomerID  int NOT NULL,
    OrderDate   date NOT NULL,
    Amount      money NOT NULL,
    filler      char(120) NOT NULL
        CONSTRAINT DF_SalesSkew_filler DEFAULT REPLICATE('X', 120)
);
GO

CREATE INDEX IX_SalesSkew_CustomerID
    ON dbo.SalesSkew(CustomerID);
GO

;WITH N AS
(
    SELECT TOP (200000)
           ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS n
    FROM sys.all_objects AS a
    CROSS JOIN sys.all_objects AS b
)
INSERT INTO dbo.SalesSkew (CustomerID, OrderDate, Amount)
SELECT CASE
           WHEN n &lt;= 199000 THEN 1
           ELSE n
       END,
       DATEADD(day, n % 365, '2024-01-01'),
       10.00
FROM N;
GO

UPDATE STATISTICS dbo.SalesSkew WITH FULLSCAN;
GO

CREATE OR ALTER PROC dbo.GetSalesByCustomer
    @CustomerID int
AS
BEGIN
    SET NOCOUNT ON;

    SELECT SalesID,
           CustomerID,
           OrderDate,
           Amount
    FROM dbo.SalesSkew
    WHERE CustomerID = @CustomerID;
END;
GO</code></pre></div><p>That data load packs almost the whole table into a single <code>CustomerID</code> and leaves the rest of the values sparse. The nonclustered index on <code>CustomerID</code> gives the optimizer a real choice between a seek-driven plan and a broader scan-driven plan. <code>UPDATE STATISTICS</code> with <code>FULLSCAN</code> removes first-pass sampling noise, which makes the skew much more visible in estimated row counts.</p><p>Run the procedure in two different orders so the first compile sees different values:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;277cb973-6318-4918-a2a3-922a3fa070cd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;
GO

SET STATISTICS IO, TIME ON;
GO

EXEC dbo.GetSalesByCustomer @CustomerID = 1;
EXEC dbo.GetSalesByCustomer @CustomerID = 199500;
GO

ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;
GO

EXEC dbo.GetSalesByCustomer @CustomerID = 199500;
EXEC dbo.GetSalesByCustomer @CustomerID = 1;
GO</code></pre></div><p>Execution order is the whole point of that test. If the first compile happens for the highly common value, the second call can inherit a plan that was priced for a large row count. If the first compile happens for the very selective value, the second call can inherit a plan that was priced for a tiny row count. Reads, CPU time, and the actual execution plan are what you want to compare between the two runs. Database-scoped cache removal is documented and is a safer lab reset than flushing the entire instance.</p><h4>Fixes You Can Test</h4><p>Targeted fixes are usually the best place to start. <code>OPTION (RECOMPILE)</code> tells SQL Server to compile a fresh temporary plan for that statement, use the current parameter values for compilation, and discard that plan after execution. For a query with highly uneven selectivity, that can turn a bad reused plan into a better per-call plan at the price of extra compile CPU.</p><p>This version applies that choice at the statement level inside the procedure:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;c754e316-0dd1-4f2f-88d2-5a334c6e94b9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE OR ALTER PROC dbo.GetSalesByCustomer
    @CustomerID int
AS
BEGIN
    SET NOCOUNT ON;

    SELECT SalesID,
           CustomerID,
           OrderDate,
           Amount
    FROM dbo.SalesSkew
    WHERE CustomerID = @CustomerID
    OPTION (RECOMPILE);
END;
GO</code></pre></div><p>Compile cost rises with that hint, but the optimizer no longer has to reuse a plan built for a very different parameter value. Statement-level <code>RECOMPILE</code> is also narrower than tagging the whole procedure with <code>WITH RECOMPILE</code>, which keeps the fix focused on the statement that is actually sensitive to skew.</p><p><code>OPTIMIZE FOR UNKNOWN</code> is a second test worth running. This hint tells the optimizer to compile against average-density information rather than the current runtime value. That can be helpful when neither the highly selective plan nor the highly nonselective plan is a good candidate for broad reuse. It does not turn parameter sniffing off, but it does bypass the current runtime value for plan selection.</p><p>Try it like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;a829b935-4e06-4bcb-8501-3b1bf78651a8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE OR ALTER PROC dbo.GetSalesByCustomer
    @CustomerID int
AS
BEGIN
    SET NOCOUNT ON;

    SELECT SalesID,
           CustomerID,
           OrderDate,
           Amount
    FROM dbo.SalesSkew
    WHERE CustomerID = @CustomerID
    OPTION (OPTIMIZE FOR UNKNOWN);
END;
GO</code></pre></div><p>Average-density estimates can be useful in the middle ground, but they are still just averages. If the table has very sharp skew, a generic estimate can still miss badly for both the hot value and the rare value. That is why this hint is worth testing rather than assuming. In some cases, picking a representative literal with <code>OPTIMIZE FOR (@CustomerID = 1)</code> or some other typical value can land closer to the traffic you actually see.</p><p>Cases where code cannot be edited have a different route. Query Store hints let you attach certain hints to a captured query without changing the procedure text, and supported hints include <code>RECOMPILE</code> and <code>OPTIMIZE FOR UNKNOWN</code>. Literal <code>OPTIMIZE FOR (@var = value)</code> is not supported as a Query Store hint, so that particular form still belongs in the T-SQL text itself. Query Store hints are available in SQL Server 2022 and later.</p><p>This sequence finds the <code>query_id</code> and then applies a Query Store hint to that captured value:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;849d4a2b-5221-40b1-902f-559eca3805e0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">DECLARE @query_id bigint;

SELECT TOP (1)
       @query_id = q.query_id
FROM sys.query_store_query_text AS qt
JOIN sys.query_store_query AS q
    ON qt.query_text_id = q.query_text_id
WHERE qt.query_sql_text LIKE N'%FROM dbo.SalesSkew%'
  AND qt.query_sql_text LIKE N'%WHERE CustomerID = @CustomerID%';
GO

EXEC sys.sp_query_store_set_hints
    @query_id = @query_id,
    @query_hints = N'OPTION(RECOMPILE)';
GO</code></pre></div><p>Broader controls exist too. <code>ALTER DATABASE SCOPED CONFIGURATION SET PARAMETER_SNIFFING = OFF</code> disables parameter sniffing at the database scope, and <code>USE HINT('DISABLE_PARAMETER_SNIFFING')</code> does the same at the query level. SQL Server also documents <code>OPTIMIZE FOR UNKNOWN</code> as a query-level way to compile from average-density estimates instead of the current runtime value. Those are much wider choices than statement-level <code>RECOMPILE</code>, so they usually belong later in testing after narrower fixes have had a fair look.</p><h4>What Newer SQL Server Versions Do</h4><p>SQL Server 2022 introduced Parameter Sensitive Plan optimization, usually shortened to PSP optimization. Under compatibility level <code>160</code>, that feature can keep more than one cached plan for a parameterized query when a single plan is not a good fit for all incoming values. Microsoft documents that PSP is turned on by default at compatibility level <code>160</code>, and the feature exists precisely for nonuniform data distributions where a single reused plan can drag badly for part of the traffic.</p><p>You can inspect the relevant database settings with a quick query:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;5ac576a2-afa6-4eff-98a8-34072bcef440&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT name, compatibility_level
FROM sys.databases
WHERE name = DB_NAME();

SELECT name, value, value_for_secondary
FROM sys.database_scoped_configurations
WHERE name IN
(
    'PARAMETER_SENSITIVE_PLAN_OPTIMIZATION',
    'PARAMETER_SNIFFING'
);
GO</code></pre></div><p>Multiple plan variants do not mean the old tuning choices vanish. <code>PSP</code> has limits on the number of variants that can be kept for a dispatcher, and Query Store is still very useful for reviewing plan history and applying Query Store hints when a query needs extra direction.</p><p>That relationship can also run in reverse. If parameter sniffing has been turned off by trace flag <code>4136</code>, by the database-scoped <code>PARAMETER_SNIFFING</code> option, or by <code>USE HINT('DISABLE_PARAMETER_SNIFFING')</code>, PSP optimization is disabled for that query traffic too. Compatibility level <code>160</code> by itself does not guarantee PSP engagement if parameter sniffing has already been disabled higher up in the stack.</p><h3>Conclusion</h3><p>SQL Server plan behavior comes down to compilation, cache reuse, memory pressure, and row estimates tied to parameter values. When the first compiled plan matches later calls, reuse saves compile cost. When data distribution changes the row counts the optimizer expects, that same reused plan can slow later executions. Looking at cache entries, statistics, execution order, Query Store, and Parameter Sensitive Plan optimization gives you a practical way to see why performance changes from call to call and how the engine decides what happens next.</p><ol><li><p><em><a href="https://learn.microsoft.com/en-us/sql/relational-databases/query-processing-architecture-guide?view=sql-server-ver17">SQL Server Query Processing Architecture Guide</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/relational-databases/performance/parameter-sensitive-plan-optimization?view=sql-server-ver17">Parameter Sensitive Plan Optimization</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-query?view=sql-server-ver17">Query Hints in Transact-SQL</a></em></p></li><li><p><em><a href="http://learn.microsoft.com/en-us/sql/t-sql/statements/alter-database-scoped-configuration-transact-sql?view=sql-server-ver17">ALTER DATABASE SCOPED CONFIGURATION</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/relational-databases/performance/query-store-hints?view=sql-server-ver17">Query Store Hints</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/relational-databases/statistics/statistics?view=sql-server-ver17">Statistics in SQL Server</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-exec-cached-plans-transact-sql?view=sql-server-ver17">sys.dm_exec_cached_plans</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-exec-plan-attributes-transact-sql?view=sql-server-ver17">sys.dm_exec_plan_attributes</a></em></p></li></ol><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share&quot;,&quot;text&quot;:&quot;Share Alexander Obregon's Substack&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share"><span>Share Alexander Obregon's Substack</span></a></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!EEOR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F931bd81e-a37e-43a3-a5ef-85cec38375b8_306x306.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!EEOR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F931bd81e-a37e-43a3-a5ef-85cec38375b8_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!EEOR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F931bd81e-a37e-43a3-a5ef-85cec38375b8_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!EEOR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F931bd81e-a37e-43a3-a5ef-85cec38375b8_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!EEOR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F931bd81e-a37e-43a3-a5ef-85cec38375b8_306x306.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!EEOR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F931bd81e-a37e-43a3-a5ef-85cec38375b8_306x306.png" width="306" height="306" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/931bd81e-a37e-43a3-a5ef-85cec38375b8_306x306.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:306,&quot;width&quot;:306,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!EEOR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F931bd81e-a37e-43a3-a5ef-85cec38375b8_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!EEOR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F931bd81e-a37e-43a3-a5ef-85cec38375b8_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!EEOR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F931bd81e-a37e-43a3-a5ef-85cec38375b8_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!EEOR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F931bd81e-a37e-43a3-a5ef-85cec38375b8_306x306.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://freesvg.org/mono-sql">Image Source</a></figcaption></figure></div>]]></content:encoded></item><item><title><![CDATA[Foreign Key Cascades in SQL]]></title><description><![CDATA[Relational databases use foreign keys to keep related rows consistent.]]></description><link>https://alexanderobregon.substack.com/p/foreign-key-cascades-in-sql</link><guid isPermaLink="false">https://alexanderobregon.substack.com/p/foreign-key-cascades-in-sql</guid><dc:creator><![CDATA[Alexander Obregon]]></dc:creator><pubDate>Mon, 13 Apr 2026 17:03:37 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!PCb_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a5b9b7c-1fb1-4236-b361-fd6baefc6083_306x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!B60D!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67e515e-2c79-47c6-b29e-2d4870633b8c_800x373.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!B60D!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67e515e-2c79-47c6-b29e-2d4870633b8c_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!B60D!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67e515e-2c79-47c6-b29e-2d4870633b8c_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!B60D!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67e515e-2c79-47c6-b29e-2d4870633b8c_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!B60D!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67e515e-2c79-47c6-b29e-2d4870633b8c_800x373.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!B60D!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67e515e-2c79-47c6-b29e-2d4870633b8c_800x373.png" width="800" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a67e515e-2c79-47c6-b29e-2d4870633b8c_800x373.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!B60D!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67e515e-2c79-47c6-b29e-2d4870633b8c_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!B60D!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67e515e-2c79-47c6-b29e-2d4870633b8c_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!B60D!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67e515e-2c79-47c6-b29e-2d4870633b8c_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!B60D!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67e515e-2c79-47c6-b29e-2d4870633b8c_800x373.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://commons.wikimedia.org/wiki/File:Sql_data_base_with_logo.png">Image Source</a></figcaption></figure></div><p>Relational databases use foreign keys to keep related rows consistent. A child row can reference a parent row, and the database checks that the link stays valid. Cascade actions tell the database what to do with matching child rows when the parent row is deleted or when a referenced value is updated, so the relationship does not end up pointing at something that no longer exists. Across major database engines, the usual action set includes <code>NO ACTION</code>, <code>RESTRICT</code>, <code>CASCADE</code>, and <code>SET NULL</code>, while <code>SET DEFAULT</code> is supported in some products but not all, so behavior can change between PostgreSQL, MySQL, SQL Server, and Oracle Database.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>How Cascade Rules Fit</h3><p>Relationships in a relational database depend on rules that keep parent and child rows connected in a valid way. The database checks those links during inserts, updates, and deletes so a child row does not end up pointing at a parent row that is gone. Cascade actions belong to that same group of rules. They do not create the link, they tell the database how to react after the link already exists and the parent side changes.</p><h4>Parent Rows</h4><p>Every <code>FOREIGN KEY</code> relationship has two sides. The parent table stores the referenced value, while the child table stores the column that points to it. The foreign key clause is written on the child table, not on the parent table. Parent columns must be backed by a <code>PRIMARY KEY</code> or another uniqueness rule so the database has a stable target for every child reference.</p><p>Publishing data gives a good starting point:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;83342192-b1ce-4217-8eaf-4334e0381cb0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE publishers (
    publisher_id INT PRIMARY KEY,
    publisher_name VARCHAR(100) NOT NULL
);

CREATE TABLE books (
    book_id INT PRIMARY KEY,
    publisher_id INT NOT NULL,
    title VARCHAR(200) NOT NULL,
    CONSTRAINT fk_books_publisher
        FOREIGN KEY (publisher_id)
        REFERENCES publishers (publisher_id)
);</code></pre></div><p>That layout places the parent value in <code>publishers.publisher_id</code> and the child reference in <code>books.publisher_id</code>. <code>books</code> depends on <code>publishers</code> in a very specific way. Every row in <code>books</code> must point to a publisher row that already exists, unless the child column is nullable and the schema allows it to be empty. The direction of the relationship does not change just because the child table has more columns or ends up with more rows. What defines the parent side is the fact that other rows point to it.</p><p>Data inserts make the relationship easier to read:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;7c01b8b4-8a32-4118-825b-ea71e1f4cec5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">INSERT INTO publishers (publisher_id, publisher_name)
VALUES (10, 'North Harbor Press');

INSERT INTO books (book_id, publisher_id, title)
VALUES (101, 10, 'The Long Query');

INSERT INTO books (book_id, publisher_id, title)
VALUES (102, 99, 'Broken Reference');</code></pre></div><p>The first <code>INSERT</code> into <code>books</code> succeeds because <code>publisher_id = 10</code> is already present in <code>publishers</code>. The second <code>INSERT</code> fails because <code>publisher_id = 99</code> has no matching row in the parent table. That check happens before any cascade rule becomes relevant. A cascade action does not change how the database validates a child row during insert time. Its job begins later, at the moment the referenced parent row is deleted or the referenced value changes.</p><p>Direction is what keeps this topic readable. Parent rows are not defined by age, size, or business importance. They are defined by reference flow. The parent table holds the value being referenced. The child table holds the column that refers to that value. Once that part is settled, every later rule becomes easier to read because <code>ON DELETE</code> and <code>ON UPDATE</code> are always reactions to changes on the parent side.</p><p>That parent child structure also explains why foreign keys are attached to relationship meaning rather than to row order. A row in <code>books</code> is not just placed near a row in <code>publishers</code>. It is tied to a specific publisher identity through <code>publisher_id</code>. If the publisher row is removed, the database has to decide what to do with the book row. If the publisher ID changes, the database has to decide what happens to child rows that still point to the old value. Those later decisions are what cascade clauses control, but they only make sense after the parent row and child row relationship is fully defined.</p><h4>Referential Actions</h4><p>Action clauses answer a narrow question. What should happen to matching child rows when a parent row is deleted, or when the referenced parent value is updated. The common family includes <code>NO ACTION</code>, <code>RESTRICT</code>, <code>CASCADE</code>, <code>SET NULL</code>, and in some database products <code>SET DEFAULT</code>. Support is not identical across engines, so the clause names may look familiar while product behavior still differs in important ways.</p><p>A school scheduling schema gives a good way to read those options:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;c63bc4ea-6a89-40ae-9a69-1418c48cbeb4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE instructors (
    instructor_id INT PRIMARY KEY,
    instructor_name VARCHAR(100) NOT NULL
);

CREATE TABLE office_assignments (
    office_id INT PRIMARY KEY,
    instructor_id INT UNIQUE,
    building_code VARCHAR(20) NOT NULL,
    CONSTRAINT fk_office_instructor
        FOREIGN KEY (instructor_id)
        REFERENCES instructors (instructor_id)
        ON DELETE CASCADE
);

CREATE TABLE class_sections (
    section_id INT PRIMARY KEY,
    instructor_id INT NULL,
    room_code VARCHAR(20) NOT NULL,
    CONSTRAINT fk_section_instructor
        FOREIGN KEY (instructor_id)
        REFERENCES instructors (instructor_id)
        ON DELETE SET NULL
);

CREATE TABLE payroll_profiles (
    profile_id INT PRIMARY KEY,
    instructor_id INT NOT NULL,
    tax_region VARCHAR(20) NOT NULL,
    CONSTRAINT fk_payroll_instructor
        FOREIGN KEY (instructor_id)
        REFERENCES instructors (instructor_id)
        ON DELETE RESTRICT
);</code></pre></div><p><code>ON DELETE CASCADE</code> removes child rows when the parent row is deleted. In that schema, an office assignment has no separate purpose after its instructor row is gone, so deleting the office assignment at the same time fits the relationship. <code>ON DELETE SET NULL</code> keeps the child row but removes the reference. A class section can stay in the table with <code>instructor_id</code> set to <code>NULL</code>, which leaves room for a later reassignment. <code>ON DELETE RESTRICT</code> blocks the parent delete while matching child rows still exist. Payroll data is treated more carefully there, so the instructor row cannot be removed until the child row is handled first.</p><p>Delete behavior gets the most attention, but update behavior belongs to the same family. <code>ON UPDATE CASCADE</code> tells the database to copy the new parent value into matching child rows. That can be useful in schemas where the referenced value is allowed to change. Not every major engine handles this the same way, which is why foreign key DDL is not fully portable across products. PostgreSQL, MySQL, and SQL Server support <code>ON UPDATE</code> actions on foreign keys. Oracle Database does not support an <code>ON UPDATE</code> clause in foreign key definitions, so a schema that depends on update cascades cannot be moved there unchanged.</p><p>This compact example brings the update side into view:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;dc48b0cd-a7f9-4270-b44b-ee3361a5d443&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE membership_tiers (
    tier_code VARCHAR(20) PRIMARY KEY,
    tier_name VARCHAR(50) NOT NULL
);

CREATE TABLE members (
    member_id INT PRIMARY KEY,
    tier_code VARCHAR(20) NOT NULL,
    full_name VARCHAR(100) NOT NULL,
    CONSTRAINT fk_members_tier
        FOREIGN KEY (tier_code)
        REFERENCES membership_tiers (tier_code)
        ON UPDATE CASCADE
);</code></pre></div><p>With that definition in place, changing a parent value such as <code>tier_code</code> from <code>SILVER</code> to <code>STANDARD</code> causes matching rows in <code>members</code> to move to the new code automatically. Without <code>ON UPDATE CASCADE</code>, that parent update would be blocked if child rows still referred to the old value.</p><p>Default behavior also deserves careful mention. If no explicit delete action is given, the database usually blocks the parent delete when matching child rows still exist. The names <code>NO ACTION</code> and <code>RESTRICT</code> sound close, but they are not treated exactly the same in every product. PostgreSQL separates them in cases that involve deferred constraint checking, while MySQL treats <code>NO ACTION</code> as the same as <code>RESTRICT</code>. That means the text of the clause cannot be read in isolation from the database product where it runs.</p><p><code>SET NULL</code> and <code>SET DEFAULT</code> carry their own limits too. <code>SET NULL</code> only makes sense if the child column is allowed to contain <code>NULL</code>. If that column is declared <code>NOT NULL</code>, the database cannot replace the parent reference with <code>NULL</code>, so the foreign key definition would not fit the column rule. <code>SET DEFAULT</code> has an extra requirement. The default value placed in the child column still has to satisfy the foreign key relationship after the change. In products where <code>SET DEFAULT</code> is unsupported or only partially supported, that clause is not a safe portable choice.</p><p>A good way to read referential actions is to tie each one to the meaning of the child row. If the child row should disappear with the parent, <code>CASCADE</code> fits. If the child row should stay but lose its reference, <code>SET NULL</code> can fit. If the parent row should be protected from deletion while children still exist, <code>RESTRICT</code> or <code>NO ACTION</code> is closer to that rule. The foreign key clause is where that decision is written down, and the database carries it out at the time the parent change happens.</p><h3>Cascades Across Larger Schemas</h3><p>Foreign-key actions stay fairly readable while a schema has only two related tables. Things get more serious when a delete or update can move through three, four, or ten links in a row. At that stage, a single statement can touch far more rows than it may suggest, and engine-specific limits start to affect what the database can do and how far a change can travel.</p><h4>Multi Level Chains</h4><p>Deletes can travel through parent, child, and grandchild tables in sequence. Remove a row at the top, and the database can keep moving downward through later references as long as each relationship uses <code>ON DELETE CASCADE</code>. That behavior is helpful when lower tables exist only because the row above them exists. After the parent goes away, child rows no longer have a valid reason to stay behind.</p><p>Catalog data makes that easier to read. Remove a catalog row, and related product rows can disappear. Remove those product rows, and related inventory event rows can disappear after them:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;2db7b116-534b-428e-8a87-583821e6fce2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE catalogs (
    catalog_id INT PRIMARY KEY,
    catalog_name VARCHAR(100) NOT NULL
);

CREATE TABLE products (
    product_id INT PRIMARY KEY,
    catalog_id INT NOT NULL,
    product_name VARCHAR(100) NOT NULL,
    CONSTRAINT fk_products_catalog
        FOREIGN KEY (catalog_id)
        REFERENCES catalogs (catalog_id)
        ON DELETE CASCADE
);

CREATE TABLE inventory_events (
    event_id INT PRIMARY KEY,
    product_id INT NOT NULL,
    event_type VARCHAR(40) NOT NULL,
    CONSTRAINT fk_inventory_events_product
        FOREIGN KEY (product_id)
        REFERENCES products (product_id)
        ON DELETE CASCADE
);</code></pre></div><p>With that schema in place, the top-level delete can trigger changes in both child tables:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;da4a6263-3c61-4c34-9feb-d83e2ee12e19&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">DELETE FROM catalogs
WHERE catalog_id = 300;</code></pre></div><p><code>catalogs</code> is the only table named in the <code>DELETE</code>, but matching rows in <code>products</code> and <code>inventory_events</code> can be removed as part of that same action. That is what makes cascades useful for rows that exist only as dependent parts of a larger row group.</p><p>Not every child row belongs in that category, though. Product rows may be tightly tied to a catalog, while payment rows, audit rows, archived records, or legal retention rows may need a different rule. Trouble starts when the same delete behavior is attached to every relationship just because it feels convenient. When a schema grows, that habit can turn a single delete into a far larger data change than intended.</p><p>Depth also becomes part of the conversation. Some database engines place limits on how deeply cascades can nest, while others block certain cascade graphs entirely if the same table can be reached by more than one route. That means cascade behavior is not just about table meaning. Graph structure and database product rules enter the discussion too.</p><h4>Update Cascades</h4><p>Parent identifiers usually stay fixed for the life of the row, which is why <code>ON UPDATE CASCADE</code> appears less frequently than <code>ON DELETE CASCADE</code>. Still, there are valid cases for it. Natural identifiers, externally assigned codes, or reference values imported from another source can change, and a foreign-key action can keep child rows aligned without requiring manual updates in every related table.</p><p>Regional reference data is a good fit for that idea because business-facing codes sometimes get renamed while the row itself stays in place:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;58327254-d6b2-4d7a-9575-54b042ef0d25&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE regions (
    region_code VARCHAR(20) PRIMARY KEY,
    region_name VARCHAR(100) NOT NULL
);

CREATE TABLE warehouses (
    warehouse_id INT PRIMARY KEY,
    region_code VARCHAR(20) NOT NULL,
    warehouse_name VARCHAR(100) NOT NULL,
    CONSTRAINT fk_warehouses_region
        FOREIGN KEY (region_code)
        REFERENCES regions (region_code)
        ON UPDATE CASCADE
);</code></pre></div><p>Parent code changes can then flow into matching child rows:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;177f2f0a-d5cd-474f-8f5c-2efc4354765a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">UPDATE regions
SET region_code = 'UPPER_MIDWEST'
WHERE region_code = 'MIDWEST';</code></pre></div><p>With <code>ON UPDATE CASCADE</code> in place, rows in <code>warehouses</code> that still point to <code>MIDWEST</code> move to <code>UPPER_MIDWEST</code> during that same statement. Without it, the update is usually blocked if child rows still refer to the old value.</p><p>That product gap is one reason referenced identifiers are frequently kept stable when portability is part of the schema plan.</p><p>Checking rules also differ from one engine to another. Some systems separate <code>NO ACTION</code> from <code>RESTRICT</code> in deferred constraint cases, while others treat them as the same or check foreign keys immediately row by row during larger statements. Those differences do not always show up in a tiny example, but they can affect larger updates in noticeable ways.</p><p>SQL Server adds one more guardrail, it blocks foreign-key definitions that would let the same table appear more than one time in the set of cascading actions started by a single delete or update. That rule keeps the engine from having to pick between competing routes to the same destination table.</p><h4>Indexing Foreign Keys</h4><p>Delete speed and update speed can change sharply when child reference columns have no supporting index. Removing a parent row or changing a referenced value means the database must go find every matching child row before it can delete, block, or modify anything further down the chain. If that child column has no index, the engine may be pushed into broad scans or heavier locking than expected.</p><p>Store-level sales tables make that easier to read:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;a8b983a4-51d1-4675-8005-8d1e564f0daf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE stores (
    store_id INT PRIMARY KEY,
    store_name VARCHAR(100) NOT NULL
);

CREATE TABLE sales_receipts (
    receipt_id BIGINT PRIMARY KEY,
    store_id INT NOT NULL,
    sale_total DECIMAL(10, 2) NOT NULL,
    CONSTRAINT fk_sales_receipts_store
        FOREIGN KEY (store_id)
        REFERENCES stores (store_id)
        ON DELETE CASCADE
);

CREATE INDEX idx_sales_receipts_store_id
    ON sales_receipts (store_id);</code></pre></div><p>That index on <code>sales_receipts.store_id</code> gives the database a faster route to the child rows tied to a store. Without it, deleting a single store row can force the engine to search through a much larger portion of the receipt table before it finishes the cascade.</p><p>Confusion shows up here because the foreign-key definition itself can look complete. Parent columns already have index backing through a <code>PRIMARY KEY</code> or unique constraint, so the relationship appears fully covered at first glance. Child columns are different. In several major databases, the child-side index is a separate schema choice rather than something created automatically by the foreign-key declaration.</p><p>Indexing becomes more valuable as child tables grow. Small development datasets can hide the difference for quite a while, then larger tables make parent deletes or updates feel unexpectedly heavy. What looked fine with a few rows can turn into long scans and lock pressure after the data set grows.</p><h4>Preventing Surprise Deletes</h4><p>Unexpected deletes usually begin with a schema choice, not with the <code>DELETE</code> statement itself. Rows that are truly owned by the parent are strong candidates for <code>CASCADE</code>. Rows that stand on their own usually belong with <code>RESTRICT</code>, <code>NO ACTION</code>, or in some cases <code>SET NULL</code>. The safest direction comes from asking what the child row means after the parent row is gone. If it still has a valid independent purpose, automatic deletion may be the wrong rule. Larger schemas benefit from a dependency map before cascade actions go live. Trace the foreign-key graph table by table and follow what a top-level delete can touch. That review gets more important as the schema adds branch points, optional relationships, and multiple routes between related tables. Without that review, a delete that looks narrow in SQL text can remove far more data than expected.</p><p>Transactional testing is a practical way to inspect delete behavior before making it part of normal operations. Start a transaction, delete the parent row, review the affected child tables, and roll everything back after the inspection:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;8af278e7-48d0-42c6-b1d7-0ccca6fa89df&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">BEGIN;

DELETE FROM stores
WHERE store_id = 15;

SELECT *
FROM sales_receipts
WHERE store_id = 15;

ROLLBACK;</code></pre></div><p>That kind of check gives a direct view of what the cascade touches without leaving the delete in place. It is also useful when several tables sit below the same parent and you want to verify that every child relationship behaves the way the schema author intended.</p><p>Keeping referenced identifiers stable avoids portability friction and keeps delete behavior as the main moving part in the foreign-key relationship.</p><h3>Conclusion</h3><p>Foreign key cascades give the database a built-in way to carry parent-child rules through deletes and updates without leaving related rows behind in an invalid state. After the relationship is defined, actions such as <code>CASCADE</code>, <code>RESTRICT</code>, <code>NO ACTION</code>, and <code>SET NULL</code> tell the engine how child rows should react, while schema depth, product differences, and child-side indexing affect how far those changes travel and how the database handles them. Read from that mechanical side, cascades are really about putting row behavior into the schema itself so related data reacts in a predictable way when parent data changes.</p><ol><li><p><em><a href="https://www.postgresql.org/docs/current/ddl-constraints.html">PostgreSQL Foreign Keys and Constraints</a></em></p></li><li><p><em><a href="https://www.postgresql.org/docs/current/tutorial-fk.html">PostgreSQL Foreign Key Tutorial</a></em></p></li><li><p><em><a href="https://dev.mysql.com/doc/refman/8.4/en/create-table-foreign-keys.html">MySQL Foreign Key Reference</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/relational-databases/tables/create-foreign-key-relationships">SQL Server Foreign Key Relationships</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/statements/alter-table-table-constraint-transact-sql">SQL Server </a></em><code>ALTER TABLE</code><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/statements/alter-table-table-constraint-transact-sql"> Constraint Reference</a></em></p></li><li><p><em><a href="https://docs.oracle.com/en/database/oracle/oracle-database/21/cncpt/data-integrity.html">Oracle Database Data Integrity Guide</a></em></p></li></ol><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share Alexander Obregon's Substack&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share Alexander Obregon's Substack</span></a></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PCb_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a5b9b7c-1fb1-4236-b361-fd6baefc6083_306x306.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PCb_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a5b9b7c-1fb1-4236-b361-fd6baefc6083_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!PCb_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a5b9b7c-1fb1-4236-b361-fd6baefc6083_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!PCb_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a5b9b7c-1fb1-4236-b361-fd6baefc6083_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!PCb_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a5b9b7c-1fb1-4236-b361-fd6baefc6083_306x306.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PCb_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a5b9b7c-1fb1-4236-b361-fd6baefc6083_306x306.png" width="306" height="306" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6a5b9b7c-1fb1-4236-b361-fd6baefc6083_306x306.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:306,&quot;width&quot;:306,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!PCb_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a5b9b7c-1fb1-4236-b361-fd6baefc6083_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!PCb_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a5b9b7c-1fb1-4236-b361-fd6baefc6083_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!PCb_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a5b9b7c-1fb1-4236-b361-fd6baefc6083_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!PCb_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a5b9b7c-1fb1-4236-b361-fd6baefc6083_306x306.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://freesvg.org/mono-sql">Image Source</a></figcaption></figure></div>]]></content:encoded></item><item><title><![CDATA[Generated Columns and Computed Columns in SQL]]></title><description><![CDATA[Derived values can cut down on repeated expressions in day-to-day queries.]]></description><link>https://alexanderobregon.substack.com/p/generated-columns-and-computed-columns</link><guid isPermaLink="false">https://alexanderobregon.substack.com/p/generated-columns-and-computed-columns</guid><dc:creator><![CDATA[Alexander Obregon]]></dc:creator><pubDate>Mon, 06 Apr 2026 19:44:04 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!2xws!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c5ba38f-9ffd-4250-90ab-15f4f37ab5b6_306x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!uBIw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2f770e-3965-4607-a401-3c9e5ab104b7_800x373.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!uBIw!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2f770e-3965-4607-a401-3c9e5ab104b7_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!uBIw!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2f770e-3965-4607-a401-3c9e5ab104b7_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!uBIw!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2f770e-3965-4607-a401-3c9e5ab104b7_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!uBIw!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2f770e-3965-4607-a401-3c9e5ab104b7_800x373.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!uBIw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2f770e-3965-4607-a401-3c9e5ab104b7_800x373.png" width="800" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ac2f770e-3965-4607-a401-3c9e5ab104b7_800x373.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!uBIw!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2f770e-3965-4607-a401-3c9e5ab104b7_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!uBIw!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2f770e-3965-4607-a401-3c9e5ab104b7_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!uBIw!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2f770e-3965-4607-a401-3c9e5ab104b7_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!uBIw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac2f770e-3965-4607-a401-3c9e5ab104b7_800x373.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://commons.wikimedia.org/wiki/File:Sql_data_base_with_logo.png">Image Source</a></figcaption></figure></div><p>Derived values can cut down on repeated expressions in day-to-day queries. In MySQL, this feature is called a <code>generated column</code>. For SQL Server, the comparable idea is called a <code>computed column</code>. Both let the database calculate a value from other columns in the same row, which keeps that logic attached to the table instead of repeating it across query text or application code. The main decisions come down to where that value lives, how the engine keeps it current, and what changes after an index is built on top of it. Those choices affect storage space, write overhead, and how much calculation still takes place during reads.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>Core Mechanics</h3><p>Table definitions can hold derived columns, not just values entered directly. With that arrangement, the database keeps a formula at the schema level and applies it row by row under the rules of the engine. Query text stays shorter because the same expression does not need to be written again for every statement that touches the table. That gives the schema a central place for row-level calculations and makes the column part of the table itself rather than something rebuilt in scattered query text.</p><h4>MySQL Generated Columns</h4><p>Within MySQL, a generated column is declared with <code>GENERATED ALWAYS AS (expression)</code>. The column still has a name and data type, but its value comes from the expression instead of direct input. MySQL supports two forms, <code>VIRTUAL</code> and <code>STORED</code>. A virtual generated column is calculated from other column values in the row, while a stored generated column is materialized as part of the row data. That choice is written directly into the column definition, so the table schema states how MySQL should treat the derived value from the start.</p><p>We can see this in action with this syntax example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;3cb6115a-dc2f-4362-8114-f0f7ab6b4af3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE employees (
    employee_id BIGINT PRIMARY KEY,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    full_name VARCHAR(101)
        GENERATED ALWAYS AS (CONCAT(first_name, ' ', last_name)) VIRTUAL
);</code></pre></div><p><code>full_name</code> is derived from <code>first_name</code> and <code>last_name</code>, so the application does not send a separate value for it. Every row gets the same formula, and every query that reads <code>full_name</code> is reading from that single schema rule rather than rebuilding the concatenation in ad hoc SQL. That keeps the table definition in charge of the expression and cuts down on repeated formula text in queries.</p><p>Generated columns are also helpful when part of a stored value needs to be pulled into its own column. <code>JSON</code> data is a common case because queries can become noisy when the same extraction expression is repeated again and again. Moving that extraction into a generated column keeps the table definition more readable and gives the derived value a normal column name.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;c7776d67-99a3-4b65-af88-118aec6b3e84&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE shipments (
    shipment_id BIGINT PRIMARY KEY,
    payload JSON NOT NULL,
    postal_code VARCHAR(20)
        GENERATED ALWAYS AS (
            JSON_UNQUOTE(JSON_EXTRACT(payload, '$.postalCode'))
        ) STORED
);</code></pre></div><p><code>postal_code</code> now comes from the <code>payload</code> document, but it behaves like a named column in the table definition. Queries can reference <code>postal_code</code> directly instead of carrying the full <code>JSON_EXTRACT</code> expression every time that value is needed. That is a practical reason generated columns show up in table schemas that carry semi-structured data.</p><p>MySQL treats generated columns as part of table structure, not as a loose display feature. That means they interact with schema rules such as indexing support, partitioning support, and foreign key restrictions in documented ways. At this stage, the main point is that a generated column is built into the table definition itself. It is not a query alias, not a view-only expression, and not a temporary value that exists only during a single <code>SELECT</code>.</p><p>MySQL also lets you add a generated column later through <code>ALTER TABLE</code>, which is useful when a table already exists and a repeated expression needs to move into the schema:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;195b4f47-292a-4e60-9c7e-7f8eb7ea6f67&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">ALTER TABLE employees
ADD email VARCHAR(255) NOT NULL,
ADD email_domain VARCHAR(255)
    GENERATED ALWAYS AS (SUBSTRING_INDEX(email, '@', -1)) VIRTUAL;</code></pre></div><p>That statement adds <code>email_domain</code> as a derived column without turning it into a value users type in directly. The table now carries the expression as part of its definition, so future queries can refer to <code>email_domain</code> as a column name instead of repeating the string function.</p><h4>SQL Server Computed Columns</h4><p>For SQL Server, the matching feature is a computed column. The table definition uses <code>AS (expression)</code> to tell SQL Server that the column value comes from an expression tied to other columns in the same row. By default, a computed column is virtual. SQL Server calculates it when the column is referenced instead of storing it physically in the table. If physical storage is wanted, <code>PERSISTED</code> can be added to the definition.</p><p>Let&#8217;s see how that looks:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;b0b56258-07b3-4dc1-9330-91d9f9a89a52&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE Sales.Payroll (
    PayrollId int NOT NULL PRIMARY KEY,
    HoursWorked decimal(9,2) NOT NULL,
    HourlyRate decimal(9,2) NOT NULL,
    GrossPay AS (HoursWorked * HourlyRate)
);</code></pre></div><p><code>GrossPay</code> is derived from <code>HoursWorked</code> and <code>HourlyRate</code>, so there is no separate insert value for it. SQL Server keeps the formula with the table definition and applies that formula whenever <code>GrossPay</code> is referenced. That keeps arithmetic out of repeated query text and gives the schema a fixed rule for that derived value.</p><p>Data type behavior is part of the mechanics too. SQL Server resolves the computed column data type through its normal type precedence rules. If an expression combines values in a way that does not convert cleanly, the definition can fail or produce a type you did not expect. In those cases, <code>CAST</code> or <code>CONVERT</code> is used to state the intended result type directly. That becomes very helpful with money values, formatted strings, or expressions that combine integers with decimals.</p><p>This example shows that type choice inside the expression can be stated directly:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;ea1b4902-7a3b-4bcf-a4a9-43aa066d70dc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE Sales.Invoices (
    InvoiceId int NOT NULL PRIMARY KEY,
    Subtotal decimal(10,2) NOT NULL,
    TaxRate decimal(5,4) NOT NULL,
    TaxAmount AS (CAST(Subtotal * TaxRate AS decimal(10,2)))
);</code></pre></div><p><code>TaxAmount</code> is still derived, but the expression now fixes the result type instead of leaving the final type entirely to implicit conversion rules. That keeps the schema aligned with the type you want the column to expose.</p><p>Physical storage is optional in SQL Server, and <code>PERSISTED</code> is the keyword that changes the column from virtual to stored in the table. A computed column marked that way is updated by SQL Server when the source columns change.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;66c8bd0e-1510-45a3-9c51-40a894e594ee&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">ALTER TABLE Sales.Payroll
ADD MonthlyPay AS (
    CAST(HoursWorked * HourlyRate * 4.33 AS decimal(12,2))
) PERSISTED;</code></pre></div><p><code>MonthlyPay</code> is now part of the stored row data rather than a value SQL Server recalculates only at read time. That storage choice becomes more important later when read cost and indexing come into view, but the mechanical point belongs here because <code>PERSISTED</code> is written right into the column definition.</p><p>Computed columns also follow stricter schema rules than regular columns. SQL Server does not treat them as ordinary input columns, so you do not insert or update them directly. They cannot be used as <code>DEFAULT</code> definitions, and they have documented rules around nullability, foreign keys, determinism, and indexing. Those rules make more sense once you remember what the column really is. Its value is tied to an expression owned by the table schema, not to direct user-supplied data.</p><h3>Persistence Indexing Performance Tradeoffs</h3><p>Storage choices change what the engine has to do during reads and writes. Virtual definitions leave the derived value out of the stored row, stored forms place it in row data, and indexes can place that value inside a search structure the optimizer can read directly. Read speed, row size, and update cost all move when that choice changes. That is where generated columns in MySQL and computed columns in SQL Server stop being only table-definition features and start affecting daily query behavior.</p><p>Read activity is only part of the story. Every derived value has to come from somewhere, and the database has to decide when that expression is evaluated, where the result is kept, and what extra maintenance follows when base columns change. Storage stays lower when the value is left virtual, but reads still have to evaluate the expression. Storing the result removes that repeated calculation for the column value itself, though row growth and update overhead rise in exchange. When an index is added, the trade becomes even more noticeable because the engine can search the derived value directly instead of recalculating it during a scan.</p><h4>Leaving the Value Virtual</h4><p>Virtual definitions leave the derived value outside the base row. MySQL handles that with <code>VIRTUAL</code>, while SQL Server leaves a computed column virtual until <code>PERSISTED</code> is added. In both products, the formula stays in the schema and the result is calculated when the column is referenced. That keeps row storage lower, which can be useful when the expression is short and the derived value is not pulled into every query.</p><p>Let&#8217;s look at a lightweight MySQL case:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;73f8e9da-ec5d-4599-bf24-c8ca4dbf4b3f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE page_views (
    view_id BIGINT PRIMARY KEY,
    viewed_at DATETIME NOT NULL,
    viewed_month CHAR(7)
        GENERATED ALWAYS AS (DATE_FORMAT(viewed_at, '%Y-%m')) VIRTUAL
);</code></pre></div><p><code>viewed_month</code> does not take space in the row as stored data. MySQL derives it from <code>viewed_at</code> whenever the column is needed. For a short date-format expression like this, that can be a reasonable trade because the source value already exists and the derived value is just a small transformation of it.</p><p>This form also helps keep queries from repeating the same expression again and again. Instead of writing <code>DATE_FORMAT(viewed_at, '%Y-%m')</code> in every report query, the table exposes <code>viewed_month</code> as a named column. That keeps query text shorter and reduces the chance that two queries end up with slightly different formulas for what should be the same value.</p><p>Read cost still exists, though. If a query touches a large number of rows and needs the virtual column for each row, the engine has to evaluate that expression across the read set. For light expressions that may be perfectly fine. For heavy expressions or very busy read paths, that repeated calculation can become more noticeable. Virtual form, then, fits best when lower row storage is more valuable than pre-saving the result.</p><p>SQL Server follows the same broad idea with different terminology. A computed column stays virtual by default, so SQL Server calculates it when the column is referenced instead of storing it physically in the row. That makes virtual computed columns a reasonable fit for columns that tidy up query logic but are not central to the busiest filters and result sets.</p><h4>Storing the Value in the Row</h4><p>Stored forms place the derived result inside the row itself. MySQL does that with <code>STORED</code>. SQL Server does it with <code>PERSISTED</code>. Both products then refresh the saved value when dependent columns change. Reads no longer need to recalculate the column value just to return it, but inserts and updates now carry extra maintenance and extra storage.</p><p>We can look at a short SQL Server definition that shows that change directly:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;cda7d265-2392-456f-af09-cae7bd67e31b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE Sales.SubscriptionBilling (
    BillingId int NOT NULL PRIMARY KEY,
    MonthlyRate decimal(10,2) NOT NULL,
    BillableMonths int NOT NULL,
    ContractTotal AS (CAST(MonthlyRate * BillableMonths AS decimal(12,2))) PERSISTED
);</code></pre></div><p><code>ContractTotal</code> is stored with the row, so SQL Server updates it when <code>MonthlyRate</code> or <code>BillableMonths</code> changes. That can fit repeated reads of the same amount, mainly when the value appears in result sets again and again and there is little reason to recalculate it every time a query returns it.</p><p>MySQL presents the same trade with <code>STORED</code> generated columns. The derived value becomes part of row data, which means a read can fetch it directly rather than evaluating the expression for that column at that moment. That can be useful for values such as extended prices, tax totals, extracted <code>JSON</code> fields referenced constantly, or formatted values that appear across a broad slice of read traffic. Write activity becomes more expensive at the same time. If the dependent columns change, the saved derived value has to be refreshed. That refresh is automatic, but it still means more row maintenance during <code>INSERT</code> and <code>UPDATE</code>. Row size also grows because the result is now part of stored data rather than something built only when needed.</p><p>Stored form is really a trade between repeating CPU calculation during reads and carrying extra bytes and maintenance during writes. Tables that are read far more than they are changed may benefit from that trade. Tables with heavy write traffic and only occasional reference to the derived value may not gain much from storing it.</p><h4>Indexing the Derived Column</h4><p>Indexes can turn a derived column from a convenience feature into a search-friendly value the optimizer can use for filtering, ordering, and in selected cases index-only retrieval. MySQL allows indexes on stored generated columns, and <code>InnoDB</code> also supports secondary indexes on virtual generated columns. SQL Server allows indexes on computed columns too, but the expression must satisfy documented rules around determinism and precision, or be deterministic and persisted.</p><p>Let&#8217;s look at a MySQL table where a derived value comes out of <code>JSON</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;39f97d62-8317-4542-bb13-ff41305fc158&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE customer_events (
    event_id BIGINT PRIMARY KEY,
    payload JSON NOT NULL,
    region_code VARCHAR(8)
        GENERATED ALWAYS AS (
            JSON_UNQUOTE(JSON_EXTRACT(payload, '$.region'))
        ) VIRTUAL,
    INDEX ix_region_code (region_code)
);</code></pre></div><p>The base row does not store <code>region_code</code>, yet the secondary index does store the generated value in its index records. That gives MySQL something searchable without requiring the table itself to carry the derived value as stored row data. Queries that filter by <code>region_code</code> can then use the index instead of recalculating the <code>JSON_EXTRACT</code> expression across a scan of the table. Also, if a query can be satisfied from the secondary index alone, MySQL can read the generated value from the index record rather than recalculating it from the base row. That makes indexed virtual generated columns more useful than they first appear, because the value may still live in searchable storage even though it is not part of the stored row itself.</p><p>SQL Server handles indexed computed columns with stricter rules. Determinism is part of that rule set, which means the expression must always return the same result for the same input values. Precision also matters. If a computed expression is deterministic and precise, indexing can be allowed without storage in some cases. If the expression is deterministic but not precise, <code>PERSISTED</code> becomes part of the route to indexing.</p><p>This definition shows a common SQL Server case:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;6cfabf3d-a434-4b73-b327-f70a99df87f4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE Sales.InvoiceLines (
    InvoiceLineId int NOT NULL PRIMARY KEY,
    Quantity int NOT NULL,
    UnitPrice decimal(10,2) NOT NULL,
    ExtendedAmount AS (CAST(Quantity * UnitPrice AS decimal(12,2))) PERSISTED
);

CREATE INDEX IX_InvoiceLines_ExtendedAmount
    ON Sales.InvoiceLines (ExtendedAmount);</code></pre></div><p><code>ExtendedAmount</code> is now both stored and indexed, which gives SQL Server a searchable derived amount instead of forcing a fresh multiplication across rows during every relevant filter or sort. That can help range predicates, ordered retrieval, and some join cases where the computed value is central to the query.</p><p>Constraints can follow those same indexing rules in SQL Server. A computed column may participate in <code>PRIMARY KEY</code> or <code>UNIQUE</code> constraints if the expression satisfies the documented requirements. That matters because it shows the engine is treating the derived value as more than a display convenience. Once the expression meets the rule set, the column can become part of schema-level uniqueness and search structures.</p><p>Write overhead rises here too. Each index on a derived column has to be maintained when the dependent values change. In MySQL that means generated values are materialized into secondary index records during row changes. In SQL Server the indexed computed value must also stay in sync with updates to the underlying columns. Reads can get much faster, but that speed comes with added write cost and extra storage in index structures.</p><h4>Picking the Right Form</h4><p>Choice comes down to query behavior, row size, expression cost, and indexing rules in the database engine in front of you. Virtual or nonpersisted form fits best when the formula is light, the derived value mainly keeps query text shorter, and storing the result inside every row would add little value. Stored or persisted form fits better when the same derived value is returned constantly or when storage in the row lines up better with the indexing rules needed for that column.</p><p>In MySQL, a table that keeps base rows lean while still exposing a searchable derived value shows that balance well:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;138cf87a-d63c-4921-ad13-d3b8f83992d9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE support_requests (
    request_id BIGINT PRIMARY KEY,
    payload JSON NOT NULL,
    priority_code VARCHAR(10)
        GENERATED ALWAYS AS (
            JSON_UNQUOTE(JSON_EXTRACT(payload, '$.priority'))
        ) VIRTUAL,
    INDEX ix_priority_code (priority_code)
);</code></pre></div><p><code>priority_code</code> stays out of the base row, but the secondary index still carries the derived value for search use. That can be a good fit when preserving row space is more important than storing the extracted value inside every table record, yet fast filtering on that derived field is still needed.</p><p>SQL Server makes the choice a little tighter because indexing rules are more restrictive. If an expression is deterministic and precise, an indexed computed column may be allowed without persistence. If the expression is deterministic but not precise, <code>PERSISTED</code> can become necessary before the column is indexed. That means the storage decision is tied not only to read frequency, but also to the rule set attached to computed-column indexing.</p><p>Read-heavy reporting, repeated range filters, and frequent ordering by the derived value all push the decision toward indexed forms. Tables with heavy write traffic and only occasional reference to the derived value usually fit better with leaner definitions. Good schema choices come from reading the query load first, then matching that load to row growth, update overhead, and the indexing limits of the engine involved.</p><h3>Conclusion</h3><p>Generated columns in MySQL and computed columns in SQL Server give the table a built-in way to derive values from other columns, which moves repeated formulas out of day-to-day queries and into the schema itself. From there, the mechanics come down to when the value is calculated, where it is kept, and how that choice affects reads, writes, and indexing. Virtual definitions save row space but keep calculation on the read side, while stored or persisted definitions add row maintenance in exchange for direct access to the derived value itself. After indexing enters the discussion, those derived values can also become searchable parts of the table structure, which is why the best fit comes from how the expression is read, updated, and filtered in the database.</p><ol><li><p><em><a href="https://dev.mysql.com/doc/refman/8.4/en/create-table-generated-columns.html">MySQL Generated Columns</a></em></p></li><li><p><em><a href="https://dev.mysql.com/doc/refman/8.4/en/create-table-secondary-indexes.html">MySQL Secondary Indexes on Generated Columns</a></em></p></li><li><p><em><a href="https://dev.mysql.com/doc/refman/8.4/en/json-function-reference.html">MySQL JSON Function Reference</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/relational-databases/tables/specify-computed-columns-in-a-table">SQL Server Computed Columns</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/statements/create-index-transact-sql">SQL Server CREATE INDEX</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/statements/alter-table-computed-column-definition-transact-sql">SQL Server ALTER TABLE computed_column_definition</a></em></p></li></ol><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share Alexander Obregon's Substack&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share Alexander Obregon's Substack</span></a></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2xws!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c5ba38f-9ffd-4250-90ab-15f4f37ab5b6_306x306.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2xws!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c5ba38f-9ffd-4250-90ab-15f4f37ab5b6_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!2xws!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c5ba38f-9ffd-4250-90ab-15f4f37ab5b6_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!2xws!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c5ba38f-9ffd-4250-90ab-15f4f37ab5b6_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!2xws!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c5ba38f-9ffd-4250-90ab-15f4f37ab5b6_306x306.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2xws!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c5ba38f-9ffd-4250-90ab-15f4f37ab5b6_306x306.png" width="306" height="306" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4c5ba38f-9ffd-4250-90ab-15f4f37ab5b6_306x306.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:306,&quot;width&quot;:306,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!2xws!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c5ba38f-9ffd-4250-90ab-15f4f37ab5b6_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!2xws!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c5ba38f-9ffd-4250-90ab-15f4f37ab5b6_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!2xws!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c5ba38f-9ffd-4250-90ab-15f4f37ab5b6_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!2xws!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c5ba38f-9ffd-4250-90ab-15f4f37ab5b6_306x306.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://freesvg.org/mono-sql">Image Source</a></figcaption></figure></div>]]></content:encoded></item><item><title><![CDATA[Partition Pruning with SQL Filters]]></title><description><![CDATA[Good partition pruning begins during query planning, when the database decides which partitions it can skip before row reads start.]]></description><link>https://alexanderobregon.substack.com/p/partition-pruning-with-sql-filters</link><guid isPermaLink="false">https://alexanderobregon.substack.com/p/partition-pruning-with-sql-filters</guid><dc:creator><![CDATA[Alexander Obregon]]></dc:creator><pubDate>Wed, 01 Apr 2026 17:15:48 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!dktO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbbbc8a4-6c25-4cb7-8fe9-63dbb9dddd72_306x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9SnP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F888cf162-e374-4a6e-9545-96f9a8f7581f_800x373.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9SnP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F888cf162-e374-4a6e-9545-96f9a8f7581f_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!9SnP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F888cf162-e374-4a6e-9545-96f9a8f7581f_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!9SnP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F888cf162-e374-4a6e-9545-96f9a8f7581f_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!9SnP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F888cf162-e374-4a6e-9545-96f9a8f7581f_800x373.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9SnP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F888cf162-e374-4a6e-9545-96f9a8f7581f_800x373.png" width="800" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/888cf162-e374-4a6e-9545-96f9a8f7581f_800x373.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!9SnP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F888cf162-e374-4a6e-9545-96f9a8f7581f_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!9SnP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F888cf162-e374-4a6e-9545-96f9a8f7581f_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!9SnP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F888cf162-e374-4a6e-9545-96f9a8f7581f_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!9SnP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F888cf162-e374-4a6e-9545-96f9a8f7581f_800x373.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://commons.wikimedia.org/wiki/File:Sql_data_base_with_logo.png">Image Source</a></figcaption></figure></div><p>Good partition pruning begins during query planning, when the database decides which partitions it can skip before row reads start. That only happens when the optimizer can connect the <code>WHERE</code> clause to the stored partition boundaries tightly enough to rule partitions out ahead of time or while the statement is running. Direct comparisons on the partition column usually give it that proof. Put the column inside a function, compare it through the wrong data type, or write the filter in a form the engine cannot fold back to those boundaries, and a query that should touch a small slice of data may end up scanning every partition. Across PostgreSQL, MySQL, BigQuery, Oracle Database, and SQL Server, that broad rule stays the same, but the exact pruning behavior changes from one product to the next.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>How Partition Pruning Works</h3><p>Before row reads begin, the optimizer tries to narrow the table down to the partitions that could still hold matching data. That step happens by comparing the filter in the query with the partition map stored for the table. If the filter lines up with those boundaries, the engine can skip partitions before it spends time reading them. PostgreSQL explains pruning as proving that a partition cannot contain rows that satisfy the <code>WHERE</code> clause, while Oracle talks about building a partition access list from the <code>FROM</code> and <code>WHERE</code> clauses.</p><h4>Reading Partition Boundaries</h4><p>Beneath a partitioned table, the rows live in separate physical pieces even though SQL still treats the object as a single table. Range partitioning splits values into intervals such as one month per partition. List partitioning assigns named values to specific partitions. Hash partitioning routes rows into buckets from a hash result. That layout gives the optimizer a map it can reason from before the scan begins.</p><p>Monthly date partitioning helps make that easier to see. January rows can live in one partition, February rows in the next, and March rows in the next after that. With that layout in place, a query asking only for March data does not need to open January or February. The planner can compare the filter to the stored bounds and narrow the scan to the partitions whose ranges still overlap the requested dates.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;dcbb6378-db12-48f9-a94b-e89f10295124&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE meter_readings (
    account_id BIGINT NOT NULL,
    reading_date DATE NOT NULL,
    kilowatt_hours NUMERIC(10,2) NOT NULL
) PARTITION BY RANGE (reading_date);

CREATE TABLE meter_readings_2026_01
    PARTITION OF meter_readings
    FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');

CREATE TABLE meter_readings_2026_02
    PARTITION OF meter_readings
    FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');

CREATE TABLE meter_readings_2026_03
    PARTITION OF meter_readings
    FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');</code></pre></div><p>That definition stores three separate date intervals for <code>reading_date</code>. When the optimizer sees a filter that falls fully inside March, only the March partition overlaps that request. January and February can be ruled out before row filtering begins. The same general idea applies to list partitions and hash partitions, but the stored map looks different. List partitions store named values or value groups. Hash partitions store bucket assignments rather than human-readable ranges. Pruning also needs to be separated from indexing. An index can help locate rows inside a chosen partition, but partition pruning happens earlier. The pruning step asks which partitions still belong in the scan at all. After that decision is made, indexes inside those remaining partitions can still affect how rows are read. That distinction helps explain why a table can prune partitions even when the partition column itself has no index.</p><h4>Filters That Map to Boundaries</h4><p>Most pruning-friendly filters leave the partition column visible and compare it to values that line up with the partition map. Equality predicates do that well. <code>IN</code> lists can do it well too when the listed values map to a small set of partitions. Range predicates are also strong candidates when the start and end values match the stored boundaries closely enough for the optimizer to narrow the scan early.</p><p>Half-open date ranges are a common form because they line up neatly with range partition boundaries and avoid overlap between adjacent time windows:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;adc12aff-4449-448f-a3c1-8f391e611ddc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT account_id, kilowatt_hours
FROM meter_readings
WHERE reading_date &gt;= DATE '2026-03-01'
  AND reading_date &lt; DATE '2026-04-01';</code></pre></div><p>That filter leaves <code>reading_date</code> alone and compares it directly to typed boundary values. March rows can match. January, February, and April rows cannot. With monthly partitions, the optimizer can tie that predicate to the March partition and avoid scanning the others. Date filtering written this way also avoids boundary confusion that can happen when people try to express month filters with a single ending date that still includes time values.</p><p>Equality predicates are even tighter when the partition layout matches them. List partitioning is a good example. If a table is partitioned by a status column, the optimizer can look at the requested values and narrow the scan to the partitions that own those statuses:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;21940bd5-ef50-4456-8edb-8ba7737a841c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT ticket_id, opened_at
FROM support_tickets
WHERE ticket_status IN ('OPEN', 'PENDING');</code></pre></div><p>With list partitioning, those values are already part of the partition map. If both values belong to one partition, the scan can stay there. If they belong to two partitions, the engine only needs those two. Nothing outside those mapped values needs to be touched.</p><p>BigQuery follows the same broad rule, but ingestion-time partitioned tables use the <code>_PARTITIONTIME</code> pseudocolumn for pruning, and daily ingestion-time tables also expose <code>_PARTITIONDATE</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;db0d4a04-0992-422d-99b9-5895a89eaf41&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT order_id, total_amount
FROM dataset.orders
WHERE _PARTITIONDATE BETWEEN DATE '2026-03-01' AND DATE '2026-03-07';</code></pre></div><p>That filter narrows the scan to the daily partitions covering those dates. The pruning idea is still the same. The optimizer needs a direct relationship between the filter and the partition map. When that relationship is visible in the predicate, the engine can reduce how much storage it reads before row filtering moves forward.</p><p>Not every partitioning method uses calendar-style ranges, but the same planning rule still applies. Integer-range partitioning narrows scans from numeric boundaries. List partitioning narrows scans from stored value groups. Hash partitioning is less intuitive to read by eye, yet it still rests on the same principle. The optimizer has to map the filter back to the partition definitions closely enough to tell which partitions can still contain matching rows.</p><p>PostgreSQL adds a useful characteristic to that broad idea, partition pruning does not have to happen only at initial plan creation. It can also happen during execution, which means partition selection can still narrow later in the statement lifecycle when the database has the needed values. The basic requirement stays the same throughout. The filter has to give the database enough information to connect the request back to partition boundaries.</p><h3>Why Some Filters Miss Pruning</h3><p>Pruning depends on how much the optimizer can prove from a predicate before or during partition selection. That proof gets harder when the partition column is wrapped in extra logic, compared through an implicit conversion, or fed by values that arrive later than the planner can reason about. Row filtering can still return the right result, but the database may lose its chance to cut away large parts of the table early, which pulls more partitions into the scan.</p><h4>Functions Over the Partition Column</h4><p>Putting a function around the partition column is a common way pruning gets lost. Oracle calls this out for transformations such as <code>CAST</code> and <code>TRUNC</code>, and BigQuery says function-based filters over an integer-range partition column do not prune partitions. After the column is wrapped, the optimizer no longer sees the stored boundary compared against a value in its original form. It first has to reason through the added expression before it can map the predicate back to partition metadata, and some engines stop there.</p><p>Take a month filter written like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;4312574a-0e38-4549-a0aa-f86c4d782fb6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT order_id, order_total
FROM sales_orders
WHERE DATE_TRUNC('month', order_date) = DATE '2026-06-01';</code></pre></div><p>That query can return the right rows, but the month calculation is applied to <code>order_date</code> instead of leaving <code>order_date</code> exposed for boundary matching. A range written directly on the column gives the optimizer a shorter route back to the stored bounds:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;6cf2bd9e-f664-4b28-a35b-4220212a33c3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT order_id, order_total
FROM sales_orders
WHERE order_date &gt;= DATE '2026-06-01'
  AND order_date &lt; DATE '2026-07-01';</code></pre></div><p>Same month window, very different pruning odds. Predicates written directly on the column match stored bounds more closely, so the planner has a better shot at cutting away unneeded partitions before scan time.</p><p>BigQuery calls out the same issue for integer-range partitions with arithmetic on the partition column:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;7e08bf04-9398-4ec0-89c3-492fb1c394f8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT *
FROM dataset.customer_events
WHERE customer_id + 1 BETWEEN 30 AND 50;</code></pre></div><p>That arithmetic changes the pruning picture before the scan starts. A rewritten filter such as <code>customer_id BETWEEN 29 AND 49</code> puts the partition column back in direct view. MySQL is less rigid in a few date partition layouts. If the partition expression itself is based on <code>YEAR()</code>, <code>TO_DAYS()</code>, or <code>TO_SECONDS()</code>, pruning can still happen because MySQL knows how to map date predicates back through those specific expressions. Blanket rules about functions hide that product-level detail, so the real question is narrower. Can the engine translate this predicate into partition-boundary choices for this table definition.</p><h4>Casts That Force Conversions</h4><p>Type conversions can block pruning without looking dramatic in the SQL text. Oracle separates static pruning from dynamic pruning and warns that data type conversions usually move a statement away from static pruning. With <code>DATE</code> columns, it also notes that implicit conversion depends on session <code>NLS</code> settings, while a proper <code>TO_DATE</code> call lets the database identify the intended date value more precisely.</p><p>Take an Oracle-style filter written against a <code>DATE</code> partition column:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;74cf42ce-2ac4-4ddb-8570-161ea51d97f4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT shipment_id, delivered_on
FROM shipments
WHERE delivered_on &gt;= TO_DATE('2026-06-15', 'YYYY-MM-DD')
  AND delivered_on &lt; TO_DATE('2026-06-16', 'YYYY-MM-DD');</code></pre></div><p>The conversion stays on the constant side there, which keeps the partition column in its native type. On date-partitioned tables, that small text choice can separate compile-time pruning from a later pruning decision that has less room to narrow the scan early. Trouble starts when the partition column itself is cast or when a string literal is left for the database to interpret through session formatting rules. Both forms can return valid rows, yet they make boundary matching less direct.</p><h4>Runtime Values Across Engines</h4><p>Database products do not all treat runtime values the same way. PostgreSQL can prune during execution, not just during planning, and its docs name prepared statement parameters, subquery values, and parameterized nested loop values as inputs that can still remove partitions after planning has started. Oracle has the same static-versus-dynamic split and lists bind variables, subqueries, and nested loop joins as dynamic pruning cases.</p><p>BigQuery takes a stricter route for partition filters that are meant to limit scanned partitions. Seeing a filter tied to a subquery result is a good example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;4dc543a7-e3b4-436b-b7c0-33ceaa0c3530&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT *
FROM dataset.events
WHERE _PARTITIONTIME = (
    SELECT MAX(bookmark_ts)
    FROM dataset.event_bookmarks
);</code></pre></div><p>That statement ties <code>_PARTITIONTIME</code> to a subquery result instead of a constant boundary. In BigQuery, filters written that way do not limit scanned partitions the same way a constant partition filter does. Differences like that become important when the same application SQL is expected to behave similarly across products. Prepared or derived values that still prune in PostgreSQL or Oracle can leave BigQuery reading far more data.</p><h4>What to Check in Execution Plans</h4><p>Execution plans are where pruning has to prove itself. Reading the <code>WHERE</code> clause alone is not enough. PostgreSQL gives two different clues depending on timing. During initialization, <code>EXPLAIN</code> can report <code>Subplans Removed</code>. During execution, <code>EXPLAIN ANALYZE</code> can reveal partition subplans with different <code>loops</code> counts, and some partitions can appear as <code>(never executed)</code> when runtime pruning removes them every time.</p><p>Take this PostgreSQL plan check:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;d3aa431e-be45-4396-a3e7-132997834a2f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">EXPLAIN ANALYZE
SELECT invoice_id
FROM monthly_invoices
WHERE invoice_date &gt;= DATE '2026-06-01'
  AND invoice_date &lt; DATE '2026-07-01';</code></pre></div><p>Plan output tells the real story. If pruning happened during initialization, <code>Subplans Removed</code> reports partitions taken out before execution moved forward. If pruning happened later, partition subplans can carry different <code>loops</code> counts, and some can appear as <code>(never executed)</code>.</p><p>Oracle exposes pruning very directly through <code>PSTART</code> and <code>PSTOP</code>. When the plan reports <code>PARTITION RANGE SINGLE</code>, those columns identify the accessed partition span. When it reports <code>PARTITION RANGE ALL</code>, pruning did not narrow the scan. Dynamic pruning can show <code>KEY</code> markers in <code>PSTART</code> and <code>PSTOP</code> rather than fixed partition numbers resolved at parse time.</p><p>SQL Server and BigQuery surface the same question in different ways. SQL Server focuses on partition elimination in its plan tools, while BigQuery exposes the result through bytes scanned and bytes processed estimates, including dry runs. If a filter was written to touch a narrow date slice but the plan or estimate still reflects the full table, pruning did not happen in the way the query text implied.</p><h3>Conclusion</h3><p>Partition pruning comes down to how directly a filter points the optimizer to partition boundaries. Keep the partition column visible, compare it in its native type, and write predicates in forms the engine can map back to stored ranges or values. If that link gets blurred by functions, conversions, or late-bound expressions, the database has less room to cut the scan down early, which is why two queries that return the same rows can read very different amounts of data.</p><ol><li><p><em><a href="https://www.postgresql.org/docs/current/ddl-partitioning.html">PostgreSQL Declarative Partitioning</a></em></p></li><li><p><em><a href="https://www.postgresql.org/docs/current/using-explain.html">PostgreSQL Using EXPLAIN</a></em></p></li><li><p><em><a href="https://dev.mysql.com/doc/refman/8.4/en/partitioning-pruning.html">MySQL Partition Pruning</a></em></p></li><li><p><em><a href="https://cloud.google.com/bigquery/docs/querying-partitioned-tables">BigQuery Query Partitioned Tables</a></em></p></li><li><p><em><a href="https://docs.oracle.com/en/database/oracle/oracle-database/21/vldbg/partition-pruning.html">Oracle Database Partition Pruning</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/relational-databases/partitions/partitioned-tables-and-indexes">SQL Server Partitioned Tables and Indexes</a></em></p></li></ol><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share Alexander Obregon's Substack&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share Alexander Obregon's Substack</span></a></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dktO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbbbc8a4-6c25-4cb7-8fe9-63dbb9dddd72_306x306.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dktO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbbbc8a4-6c25-4cb7-8fe9-63dbb9dddd72_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!dktO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbbbc8a4-6c25-4cb7-8fe9-63dbb9dddd72_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!dktO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbbbc8a4-6c25-4cb7-8fe9-63dbb9dddd72_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!dktO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbbbc8a4-6c25-4cb7-8fe9-63dbb9dddd72_306x306.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dktO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbbbc8a4-6c25-4cb7-8fe9-63dbb9dddd72_306x306.png" width="306" height="306" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dbbbc8a4-6c25-4cb7-8fe9-63dbb9dddd72_306x306.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:306,&quot;width&quot;:306,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!dktO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbbbc8a4-6c25-4cb7-8fe9-63dbb9dddd72_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!dktO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbbbc8a4-6c25-4cb7-8fe9-63dbb9dddd72_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!dktO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbbbc8a4-6c25-4cb7-8fe9-63dbb9dddd72_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!dktO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbbbc8a4-6c25-4cb7-8fe9-63dbb9dddd72_306x306.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://freesvg.org/mono-sql">Image Source</a></figcaption></figure></div>]]></content:encoded></item><item><title><![CDATA[Set Based Updates in SQL Without Cursors]]></title><description><![CDATA[Learn how set-based SQL updates replace cursors with joins, CTEs, grouped source rows, and preview queries before writing data changes safely.]]></description><link>https://alexanderobregon.substack.com/p/set-based-updates-in-sql-without</link><guid isPermaLink="false">https://alexanderobregon.substack.com/p/set-based-updates-in-sql-without</guid><dc:creator><![CDATA[Alexander Obregon]]></dc:creator><pubDate>Mon, 23 Mar 2026 17:05:42 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!VY4j!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbce89d5-e2c8-41cb-9c73-4cdc8a954285_306x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5Ari!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1a32fdd-ea7c-4460-abda-8d39aa945afb_800x373.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5Ari!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1a32fdd-ea7c-4460-abda-8d39aa945afb_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!5Ari!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1a32fdd-ea7c-4460-abda-8d39aa945afb_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!5Ari!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1a32fdd-ea7c-4460-abda-8d39aa945afb_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!5Ari!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1a32fdd-ea7c-4460-abda-8d39aa945afb_800x373.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5Ari!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1a32fdd-ea7c-4460-abda-8d39aa945afb_800x373.png" width="800" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d1a32fdd-ea7c-4460-abda-8d39aa945afb_800x373.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!5Ari!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1a32fdd-ea7c-4460-abda-8d39aa945afb_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!5Ari!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1a32fdd-ea7c-4460-abda-8d39aa945afb_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!5Ari!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1a32fdd-ea7c-4460-abda-8d39aa945afb_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!5Ari!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1a32fdd-ea7c-4460-abda-8d39aa945afb_800x373.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://commons.wikimedia.org/wiki/File:Sql_data_base_with_logo.png">Image Source</a></figcaption></figure></div><p>Bulk updates usually start with a business rule, not a cursor. Maybe a customer tier needs to change, a balance needs a refresh, a summary column needs values from detail rows, or a shipment flag has to flip after related data arrives. That is where row by row logic creeps in, with one row read, one result calculated, and one row updated again and again. SQL gives you a better route. PostgreSQL, SQL Server, and MySQL all support joined updates and CTE-based updates, so the database can evaluate the target rows, source rows, and filter rules in one statement instead of repeating the same update row by row.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>Why Set Based Updates Fit SQL</h3><p>Set based thinking lines up well with how an <code>UPDATE</code> statement is built. SQL starts from a group of rows that match a rule, then assigns new values across that full row set in a single statement. PostgreSQL supports <code>UPDATE</code> with a <code>FROM</code> clause and <code>RETURNING</code>, SQL Server supports <code>UPDATE</code> with <code>FROM</code>, CTEs, and <code>OUTPUT</code>, and MySQL 8.4 supports <code>UPDATE</code> statements that can begin with <code>WITH</code> along with multi-table updates. Those features let the database read the target rows, source rows, and filter rules as one unit instead of repeating the same write row by row.</p><h4>SQL Wants the Full Change Set</h4><p>Set based SQL begins with the rule that defines the rows to change. The statement is not focused on which row comes next. Its focus stays on which rows belong in the update and what value each of those rows should receive. That lines up well with SQL because the full row set is visible from the start.</p><p>Reading an update through that lens makes the structure much easier to follow. Start with the <code>WHERE</code> clause. That is where the target row set is defined. Then move to the <code>SET</code> clause. That is where the new value is defined for every row that passed the filter. Row by row code breaks those ideas apart and repeats them again and again. Set based SQL keeps the rule in one place, which makes it much easier to read against the business change you are trying to make.</p><p>Take a small status change as a first example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;3f6108d6-f680-4d29-86f6-65b618961f73&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">UPDATE support_ticket
SET priority = 'HIGH'
WHERE status = 'OPEN'
  AND days_open &gt;= 30;</code></pre></div><p>Nothing in that statement depends on processing rows one at a time. The filter picks every qualifying row, and the assignment applies the same new value across that full set.</p><p>That same idea still holds when the new value depends on data already stored in the row. SQL can evaluate the rule for every qualifying row inside the statement itself, so there is no need to pull rows into a loop just to classify them and send them back.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;9bc6dbf4-4a80-4935-927d-192609a20901&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">UPDATE support_ticket
SET priority =
    CASE
        WHEN days_open &gt;= 30 THEN 'HIGH'
        WHEN days_open &gt;= 7 THEN 'MEDIUM'
        ELSE 'LOW'
    END
WHERE status = 'OPEN';</code></pre></div><p><code>CASE</code> keeps the decision logic inside the update, which lets the database evaluate the full row set under one statement. That is a better fit for SQL than reading a row, calculating a label outside the database, writing it back, and repeating the cycle until the table is done.</p><p>CTEs fit naturally into that same idea because sometimes the row set needs a name before the write happens. Sometimes the filter logic is long enough that it reads better when it is separated from the final assignment. A CTE gives you a statement-scoped result set that can feed the update without turning the operation into row by row code.</p><p>Take a look at this form:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;f23f382b-a7a7-448f-b741-4fccdce1c69f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">WITH eligible_ticket AS (
    SELECT ticket_id, days_open
    FROM support_ticket
    WHERE status = 'OPEN'
)
UPDATE support_ticket t
SET priority =
    CASE
        WHEN e.days_open &gt;= 30 THEN 'HIGH'
        WHEN e.days_open &gt;= 7 THEN 'MEDIUM'
        ELSE 'LOW'
    END
FROM eligible_ticket e
WHERE t.ticket_id = e.ticket_id;</code></pre></div><p>The update still happens as a set. The CTE just gives the qualifying rows a name before the write takes place. That keeps the rule readable without changing the set based nature of the statement.</p><h4>One Source Row for Each Target Row</h4><p>Joined updates need one rule kept firmly in view. Each target row should line up with a single source row for the value being assigned. When that does not happen, the statement can produce results you did not intend because more than one source row is feeding the same target row. That idea comes into better focus with a table that stores summary values. Say an <code>inventory</code> table stores the latest sale time for each item. A <code>sale_history</code> table can hold several sales for the same item. Joining <code>inventory</code> straight to <code>sale_history</code> leaves one inventory row matching several source rows, which is not a good match for a joined update.</p><p>The safer route is to reduce the source side first so that only one row remains for each target row:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;17d5f984-f959-4116-a40a-9f148f04cf5f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">WITH latest_sale AS (
    SELECT
        item_id,
        MAX(sale_time) AS latest_sale_time
    FROM sale_history
    GROUP BY item_id
)
UPDATE inventory i
SET last_sale_at = ls.latest_sale_time
FROM latest_sale ls
WHERE i.item_id = ls.item_id;</code></pre></div><p>That CTE changes the source side from several sale rows per item to a single row per item. After that reduction, each inventory row has one source row feeding the assigned value, which is exactly what a joined update needs.</p><p>The same rule shows up in smaller cases too. Think about an account table that stores the most recent payment date. Payment history can contain several rows for one account. Jumping straight into a joined update would leave the account row matching several payment rows, so the source side needs to be narrowed first.</p><p>This version keeps the join aligned with one source row for each account:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;d0e41daa-99ee-434e-bc30-a2ba83412f59&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">WITH latest_payment AS (
    SELECT
        account_id,
        MAX(payment_date) AS last_payment_date
    FROM payment_history
    GROUP BY account_id
)
UPDATE account a
SET last_payment_date = lp.last_payment_date
FROM latest_payment lp
WHERE a.account_id = lp.account_id;</code></pre></div><p>Grouping, ranking, or some other row reduction step is what makes joined updates dependable in cases like these. After the source side has been narrowed to one row per target row, the final update reads much more naturally. You can look at the statement and see the target table, the source result, the join condition, and the assigned value without having to trace a loop outside SQL.</p><p>That difference is one of the biggest reasons set based updates fit SQL so well. Row by row code hides the matching rule inside repeated reads and writes. Keeping the full row relationship inside the statement makes it much simpler to spot when the source side is too wide and when it needs to be reduced before the write happens.</p><h3>Rewriting Common Row by Row Updates</h3><p>Row by row update logic usually repeats the same cycle. A row is read, a value is derived, that value is written back, and the database moves on to the next row. SQL can fold that repeated cycle into a single <code>UPDATE</code> when the new value comes from a lookup row, a grouped detail result, or a rule written as an expression inside the statement. PostgreSQL, SQL Server, and MySQL all support forms of that rewrite through joined updates and CTE-fed updates, and those same joins or CTEs can be turned into a preview query before data changes are written.</p><p>Moving from row by row logic to a set based statement starts with a small change in how the update is read. Instead of thinking about the next row to fetch, focus on the full target row set, the source of the new value, and the condition that limits the write. That keeps the update tied to the data rule itself rather than to a loop wrapped around it.</p><h4>Copying Values From a Lookup Table</h4><p>Lookup-driven updates are one of the first places where cursor logic starts to feel unnecessary. Code that fetches a target row, reads a related lookup row, copies a value, and repeats is really expressing a join. After that becomes visible, the rewrite is usually very natural. Put the target table in the <code>UPDATE</code>, join to the lookup source, filter the rows that need the change, and assign the lookup value in the <code>SET</code> clause. PostgreSQL and SQL Server support this through <code>UPDATE</code> with <code>FROM</code>, while MySQL supports the joined form through multi-table <code>UPDATE</code> syntax.</p><p>Take a postal-rate update where each open shipment should copy the current zone from a reference table:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;6e444f68-64fd-4b25-9e8d-453cf2847bb1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">UPDATE shipment s
SET zone_code = z.zone_code
FROM postal_zone z
WHERE s.postal_code = z.postal_code
  AND s.status = 'OPEN';</code></pre></div><p>Nothing in that statement needs a cursor. <code>shipment</code> is the target, <code>postal_zone</code> is the lookup source, the join ties the two tables together, and the filter limits the write to open shipments. Putting that logic in a loop outside SQL would just repeat the same lookup again and again.</p><p>SQL Server reads in much the same way, with the target alias named in the <code>UPDATE</code> and the join written in the <code>FROM</code> clause:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;812df7ea-a9dd-4697-b1b4-fc52868668b3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">UPDATE pr
SET tax_rate = tr.rate
FROM purchase_record AS pr
JOIN tax_rate AS tr
  ON pr.state_code = tr.state_code
WHERE pr.invoice_status = 'PENDING';</code></pre></div><p>That rewrite fits any case where the new value already lives in a related table. Postal zones, tax rates, office codes, district names, supervisor IDs, and billing categories all fall into that group. What looked like row by row logic in application code usually turns out to be a joined update waiting to be written.</p><h4>Filling Summary Columns From Detail Rows</h4><p>Summary columns need an extra step before the update happens. Detail rows have to be reduced to a single row per parent first. Without that reduction, a joined update can match the same target row against several detail rows, which is not a good fit when the target table stores a single summary value. Aggregates such as <code>SUM</code>, <code>MAX</code>, <code>MIN</code>, and <code>COUNT</code> turn the detail side into a row set that can feed the update safely.</p><p>Take a shipping example where <code>manifest.total_weight</code> should reflect the sum of all package weights tied to that manifest. Cursor logic would read one manifest, sum its package rows, update the weight, and continue to the next manifest. SQL can do that in one statement by grouping the detail rows first and then joining the grouped result back to the target table:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;f2990742-e6ed-4bf8-87cb-1bf9cb9c1df0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">WITH manifest_weight AS (
    SELECT
        package.manifest_id,
        SUM(package.weight_lbs) AS total_weight
    FROM package
    GROUP BY package.manifest_id
)
UPDATE manifest m
SET total_weight = mw.total_weight
FROM manifest_weight mw
WHERE m.manifest_id = mw.manifest_id;</code></pre></div><p>That grouped CTE changes the source side before the update starts. Instead of several package rows for the same manifest, the update sees a single grouped row per manifest with the final weight already calculated. That lines up with the target column being filled. A single <code>total_weight</code> value belongs to each manifest row, so the source side should also present a single row for each manifest.</p><p>The same rewrite fits balances, totals, last activity timestamps, line counts, and similar parent-level values that come from detail tables. Repeated loop logic was never really about processing each parent in isolation. The actual rule was to reduce the detail rows to one result per parent and then write that result back.</p><h4>Applying Rule Based Changes With a CTE</h4><p>Rule-driven updates do not always need a second base table. Some loops exist only because the decision logic feels easier to write outside SQL. The loop reads a row, checks several columns, assigns a label or flag, and then updates that row. SQL can keep that rule inside the statement through <code>CASE</code>, and a CTE helps when the target row set or the intermediate values deserve a name before the final write.</p><p>Let&#8217;s see a subscription table where active rows need a renewal status based on days left and unpaid balance:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;c2416a47-f39f-4414-901b-ce9bc236a6de&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">WITH renewal_target AS (
    SELECT
        subscription_id,
        days_to_expiry,
        unpaid_balance
    FROM subscription
    WHERE account_status = 'ACTIVE'
)
UPDATE subscription s
SET renewal_status =
    CASE
        WHEN r.unpaid_balance &gt; 0 THEN 'HOLD'
        WHEN r.days_to_expiry &lt;= 7 THEN 'RENEW_NOW'
        WHEN r.days_to_expiry &lt;= 30 THEN 'REVIEW'
        ELSE 'OK'
    END
FROM renewal_target r
WHERE s.subscription_id = r.subscription_id;</code></pre></div><p>That CTE does not change the fact that the update is still set based. Its job is to name the qualifying row set and keep the final <code>UPDATE</code> focused on assigning the new value. That can help when the filter logic is longer, when intermediate calculations belong in the source row set, or when the statement reads better after the row selection has been separated from the final assignment.</p><p>Keeping the rule inside SQL also keeps the logic near the data it depends on. That makes the rewrite from row loop to set update much easier to follow, because the filtering logic, the decision logic, and the write all stay in the same statement.</p><h4>Previewing Rows Before the Write</h4><p>Before any update runs, the safest habit is to turn the same join or CTE into a <code>SELECT</code> first. That preview tells you which rows qualify, what value is stored now, and what value is about to be written. Nothing about that step is separate from the final update. It is the same row logic with the write removed, which makes it a very useful checkpoint before data changes happen.</p><p>For example, say we are working with a membership-fee update where each plan fee should be refreshed from a rate table. Before the write, run the row-finding logic as a <code>SELECT</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;d823f59d-9480-4337-a10b-8e877104d697&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">WITH target_fee AS (
    SELECT
        m.member_id,
        m.monthly_fee AS current_fee,
        r.monthly_fee AS new_fee
    FROM member_plan m
    JOIN fee_rate r
      ON m.plan_code = r.plan_code
    WHERE m.monthly_fee &lt;&gt; r.monthly_fee
)
SELECT
    member_id,
    current_fee,
    new_fee
FROM target_fee
ORDER BY member_id;</code></pre></div><p>That preview makes the pending change visible before the update touches the table. You can inspect the candidate rows, check that the join is matching the right rate, and verify that the new value is what you expected. Turning an update into a preview <code>SELECT</code> like this is one of the best habits to build around set based writes.</p><p>After the write, PostgreSQL can return the affected rows directly from the <code>UPDATE</code> statement:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;5bc855f3-696f-4877-a210-0dffc2862470&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">UPDATE member_plan m
SET monthly_fee = r.monthly_fee
FROM fee_rate r
WHERE m.plan_code = r.plan_code
  AND m.monthly_fee &lt;&gt; r.monthly_fee
RETURNING
    m.member_id,
    m.monthly_fee AS updated_fee;</code></pre></div><p>SQL Server has a similar review step with <code>OUTPUT</code>, which can return the old row image from <code>deleted</code> and the new row image from <code>inserted</code> for each affected row. That is useful when you want a record of what changed right after the statement runs, but it does not replace the preview query. The preview still answers the question that comes first, which rows are about to change and what values they are about to receive.</p><h3>Conclusion</h3><p>Set based updates keep the row filter, value calculation, joins, grouping, and final write inside the same SQL statement, which is what makes them such a good replacement for cursors in jobs like lookup copies, summary refreshes, and rule-based status changes. After the source rows have been narrowed to the right grain and the update logic has been previewed with a matching <code>SELECT</code>, the final write becomes more natural to reason through, more reliable to check before it runs, and more closely aligned with the data rule being applied.</p><ol><li><p><em><a href="https://www.postgresql.org/docs/current/sql-update.html">PostgreSQL UPDATE</a></em></p></li><li><p><em><a href="https://www.postgresql.org/docs/current/queries-with.html">PostgreSQL WITH Queries</a></em></p></li><li><p><em><a href="https://www.postgresql.org/docs/current/dml-returning.html">PostgreSQL Returning Data From Modified Rows</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/queries/update-transact-sql?view=sql-server-ver17">SQL Server UPDATE Transact-SQL</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql?view=sql-server-ver17">SQL Server OUTPUT Clause</a></em></p></li><li><p><em><a href="https://dev.mysql.com/doc/refman/8.4/en/update.html">MySQL UPDATE Statement</a></em></p></li><li><p><em><a href="https://dev.mysql.com/doc/refman/8.4/en/with.html">MySQL WITH Common Table Expressions</a></em></p></li></ol><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share Alexander Obregon's Substack&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share Alexander Obregon's Substack</span></a></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!VY4j!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbce89d5-e2c8-41cb-9c73-4cdc8a954285_306x306.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!VY4j!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbce89d5-e2c8-41cb-9c73-4cdc8a954285_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!VY4j!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbce89d5-e2c8-41cb-9c73-4cdc8a954285_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!VY4j!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbce89d5-e2c8-41cb-9c73-4cdc8a954285_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!VY4j!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbce89d5-e2c8-41cb-9c73-4cdc8a954285_306x306.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!VY4j!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbce89d5-e2c8-41cb-9c73-4cdc8a954285_306x306.png" width="306" height="306" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dbce89d5-e2c8-41cb-9c73-4cdc8a954285_306x306.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:306,&quot;width&quot;:306,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!VY4j!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbce89d5-e2c8-41cb-9c73-4cdc8a954285_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!VY4j!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbce89d5-e2c8-41cb-9c73-4cdc8a954285_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!VY4j!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbce89d5-e2c8-41cb-9c73-4cdc8a954285_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!VY4j!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbce89d5-e2c8-41cb-9c73-4cdc8a954285_306x306.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://freesvg.org/mono-sql">Image Source</a></figcaption></figure></div>]]></content:encoded></item><item><title><![CDATA[Grouping Sets and Rollups in SQL]]></title><description><![CDATA[Summarizing large result sets is a normal part of reporting, dashboards, exports, finance work, and ad hoc query work.]]></description><link>https://alexanderobregon.substack.com/p/grouping-sets-and-rollups-in-sql</link><guid isPermaLink="false">https://alexanderobregon.substack.com/p/grouping-sets-and-rollups-in-sql</guid><dc:creator><![CDATA[Alexander Obregon]]></dc:creator><pubDate>Mon, 16 Mar 2026 15:43:35 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!UL82!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc004ab0-519e-4bc7-9a5e-0c288773a1a7_306x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9B6f!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5262d75c-e803-40ab-81a0-624d0248bdbd_800x373.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9B6f!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5262d75c-e803-40ab-81a0-624d0248bdbd_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!9B6f!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5262d75c-e803-40ab-81a0-624d0248bdbd_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!9B6f!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5262d75c-e803-40ab-81a0-624d0248bdbd_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!9B6f!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5262d75c-e803-40ab-81a0-624d0248bdbd_800x373.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9B6f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5262d75c-e803-40ab-81a0-624d0248bdbd_800x373.png" width="800" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5262d75c-e803-40ab-81a0-624d0248bdbd_800x373.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!9B6f!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5262d75c-e803-40ab-81a0-624d0248bdbd_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!9B6f!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5262d75c-e803-40ab-81a0-624d0248bdbd_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!9B6f!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5262d75c-e803-40ab-81a0-624d0248bdbd_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!9B6f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5262d75c-e803-40ab-81a0-624d0248bdbd_800x373.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://commons.wikimedia.org/wiki/File:Sql_data_base_with_logo.png">Image Source</a></figcaption></figure></div><p>Summarizing large result sets is a normal part of reporting, dashboards, exports, finance work, and ad hoc query work. The hard part starts when one report needs detailed rows, subtotal rows at more than one level, and a grand total in the same result. Writing a separate <code>GROUP BY</code> query for every total level can get repetitive fast, and it also makes the SQL harder to read. SQL solves that with grouping extensions that let one query return several grouped levels at the same time. That is where <code>GROUPING SETS</code>, <code>ROLLUP</code>, and <code>CUBE</code> come in. They all return grouped output, but they do not produce the same structure. <code>ROLLUP</code> follows a hierarchy, <code>CUBE</code> returns every possible grouping combination, and <code>GROUPING SETS</code> lets you state the exact grouped levels you want. Major database engines support these features a little differently, so those differences matter when you want one query to work well across more than one system.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>How Multi Level Grouping Works in One Query</h3><p>Grouped extensions build on the same foundation as plain <code>GROUP BY</code>, but they remove the limit of returning only one grouping level from a query block. Regular grouping divides rows by the selected columns and applies aggregate functions such as <code>SUM</code>, <code>COUNT</code>, or <code>AVG</code> to each group. <code>GROUPING SETS</code>, <code>ROLLUP</code>, and <code>CUBE</code> keep that same aggregate process, but they let the database return multiple grouped levels in one result. That change is what makes multi-level totals possible without stacking separate grouped queries.</p><h4>Why Plain GROUP BY Stops Short</h4><p>Plain <code>GROUP BY</code> works nicely when the report asks for a single grouping level and nothing more. Group order rows by <code>sales_year</code> and <code>region</code>, and the result gives a row per year and region pair. That answers a reporting question well, but it does not also return yearly totals by themselves or a full grand total.</p><p>This query gives a good starting point:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;0d959f60-35cb-4fee-a8c5-b491c983d9d4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    sales_year,
    region,
    SUM(net_amount) AS total_net_amount
FROM order_summary
GROUP BY sales_year, region
ORDER BY sales_year, region;</code></pre></div><p>In that code, the query returns totals for each <code>sales_year</code> and <code>region</code> pair. If the report also needs a row per year across all regions, plain <code>GROUP BY</code> cannot add that second grouped level automatically. You would need a separate grouped query.</p><p>Repetition starts to grow at that point. The first grouped query answers the detailed level. The second answers the higher total. The third answers the grand total. The output can still be built, but the SQL now has to repeat the source table, repeat the filter logic, and repeat the aggregate expression. As subtotal levels grow, that repetition spreads across more of the statement.</p><p>In this kind of query we can see what that looks like:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;f5788850-3d61-4de2-b925-78b7c7497ee9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    sales_year,
    region,
    SUM(net_amount) AS total_net_amount
FROM order_summary
GROUP BY sales_year, region

UNION ALL

SELECT
    sales_year,
    NULL AS region,
    SUM(net_amount) AS total_net_amount
FROM order_summary
GROUP BY sales_year

UNION ALL

SELECT
    NULL AS sales_year,
    NULL AS region,
    SUM(net_amount) AS total_net_amount
FROM order_summary;</code></pre></div><p>That works because each <code>SELECT</code> returns a single grouping level, and <code>UNION ALL</code> stacks those results into a single output. Still, the statement gets harder to scan because the grouping intent is spread across three query blocks instead of living in a single grouped clause. It also raises the chance of drift. If a filter is added to some branch but missed in another, the totals change.</p><p>Grouped extensions solve that by keeping the grouped levels in a single <code>GROUP BY</code> clause. The same source rows are still being summarized, but the grouping logic becomes more direct. Instead of saying run three different grouped queries and stack them, you say return these grouped levels from this grouped statement.</p><p>Something else worth mentioning is that plain <code>GROUP BY</code> always returns rows at a single granularity for that query block. It does not carry a built-in idea of subtotal rows. That is why subtotal features had to be added as separate syntax rather than folded into normal grouping behavior.</p><h4>GROUPING SETS, ROLLUP and CUBE</h4><p>The general idea behind all three features is the same, a grouped statement can return more than a single grouping level. The difference is in how those levels are chosen. <code>GROUPING SETS</code> is the most direct form. You name the grouping levels you want, and the database returns those levels and no others. That makes it a good fit when the report has a fixed subtotal layout.</p><p>This version asks for exactly three grouped levels:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;4388d356-119c-434f-8275-b7ff495fbb6f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    department,
    job_title,
    SUM(base_salary) AS total_salary
FROM employee_payroll
GROUP BY GROUPING SETS
(
    (department, job_title),
    (department),
    ()
)
ORDER BY department, job_title;</code></pre></div><p>Here, the query asks for three grouped levels. The first grouping set returns totals by <code>department</code> and <code>job_title</code>. The second returns totals by <code>department</code> alone. The empty grouping set <code>()</code> returns the grand total across the whole input. That empty set matters because it tells the database to aggregate all remaining rows into a final summary row.</p><p><code>ROLLUP</code> is shorter when the grouped levels follow a hierarchy. Hierarchy means the grouped columns naturally move from more detailed to less detailed in left to right order. If a report groups by year, quarter, and month, that order already forms a ladder from detailed rows up to broader totals. <code>ROLLUP</code> follows that ladder by removing grouping columns from right to left.</p><p>Take a query like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;dbc10a72-20de-42a9-8065-3285b9003b30&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    sales_year,
    sales_quarter,
    sales_month,
    SUM(net_amount) AS total_net_amount
FROM monthly_sales
GROUP BY ROLLUP(sales_year, sales_quarter, sales_month)
ORDER BY sales_year, sales_quarter, sales_month;</code></pre></div><p>The single <code>ROLLUP</code> clause expands into grouped levels that match the column order. It returns totals by year, quarter, and month, then year and quarter, then year, then the full grand total. Put plainly, the placement of columns inside <code>ROLLUP</code> matters. Swap them, and you change the subtotal path.</p><p>Left to right order deserves a little extra attention because it explains a lot of potential confusion. <code>ROLLUP(region, store)</code> produces store totals inside each region, then region totals, then the grand total. <code>ROLLUP(store, region)</code> produces region totals inside each store, which is a very different report. The function is not reading business meaning from the names. It is following column order. <code>CUBE</code> goes wider than <code>ROLLUP</code>. Instead of following a single hierarchy, it returns every possible grouping combination from the listed expressions. With two grouped columns, that means four grouped levels. With three grouped columns, that means eight grouped levels. Row counts can grow fast, which is why <code>CUBE</code> fits best when every subtotal combination is actually useful to the report.</p><p>We can see that wider grouping style here:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;f9bac477-0d72-4f3b-a932-ac7d8a140811&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    warehouse_region,
    shipping_method,
    SUM(package_count) AS total_packages
FROM shipment_facts
GROUP BY CUBE(warehouse_region, shipping_method)
ORDER BY warehouse_region, shipping_method;</code></pre></div><p>That query returns totals for <code>warehouse_region</code> and <code>shipping_method</code> pairs, totals by <code>warehouse_region</code>, totals by <code>shipping_method</code>, and a grand total. Notice what makes <code>CUBE</code> different from <code>ROLLUP</code>. <code>ROLLUP(warehouse_region, shipping_method)</code> would not return totals by <code>shipping_method</code> alone. <code>CUBE</code> does, because it includes every grouping combination across the listed columns.</p><p>Small side by side comparisons can also make the distinction easier to see. These two queries look close, but they do not ask for the same grouped result:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;498c4068-de8f-4017-9f5c-3fb905a2096b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    campus,
    course_level,
    COUNT(*) AS course_count
FROM class_schedule
GROUP BY ROLLUP(campus, course_level);</code></pre></div><p>That <code>ROLLUP</code> returns grouped rows for <code>campus</code> and <code>course_level</code>, then <code>campus</code>, then the grand total.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;10961993-4ba9-47a9-a0b4-8f8052e43422&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    campus,
    course_level,
    COUNT(*) AS course_count
FROM class_schedule
GROUP BY CUBE(campus, course_level);</code></pre></div><p>And this <code>CUBE</code> returns the same grouped rows as the rollup query, but it also adds totals by <code>course_level</code> alone across all campuses.</p><p><code>GROUPING SETS</code> can match either form when you want explicit control. If a report needs the detailed rows, totals by campus, and a grand total, but does not need totals by course level alone, then <code>GROUPING SETS</code> can ask for just that set of grouped levels and skip the rest.</p><p>A report like that can be written like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;3333f1e1-9106-4416-a7e7-8d56bea4bf50&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    campus,
    course_level,
    COUNT(*) AS course_count
FROM class_schedule
GROUP BY GROUPING SETS
(
    (campus, course_level),
    (campus),
    ()
);</code></pre></div><p>This is part of why <code>GROUPING SETS</code> reads well in reporting SQL. The grouped levels are written directly in the query, almost like a compact report outline.</p><p>All three forms still depend on regular aggregate logic. You still choose grouped columns. You still choose aggregate expressions such as <code>SUM</code> or <code>COUNT</code>. The change is that the grouped clause can now return multi level summaries in a single result set instead of stopping at a single granularity.</p><h3>Writing Better Subtotal Queries</h3><p>Getting multi-level totals into a result set is only part of the work. Reports also need to stay readable after subtotal rows appear. Query-writing choices matter more at that stage. Results can be mathematically right and still feel awkward to read if subtotal rows blur into detail rows, if the grouping form adds levels nobody asked for, or if the SQL relies on syntax that does not travel well between engines. Good subtotal queries keep the output readable while still matching the report layout the query is trying to produce.</p><h4>Labeling Subtotal Rows</h4><p>Subtotal rows usually place <code>NULL</code> into grouped columns that are no longer active at that subtotal level. That creates a practical problem right away. Source data can already contain <code>NULL</code>, so subtotal rows with <code>NULL</code> in columns such as <code>region</code> or <code>product_line</code> do not explain themselves. Without a helper function, the output can leave readers unsure which <code>NULL</code> came from missing data and which <code>NULL</code> marks an all-values subtotal.</p><p>Readable reports normally start by turning those subtotal markers into text labels people can scan quickly. <code>CASE</code> works well with <code>GROUPING()</code> for that job:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;8b12a5f3-9143-4ffa-ab62-532f6f1911d4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    CASE
        WHEN GROUPING(region) = 1 THEN 'All Regions'
        ELSE region
    END AS region_label,
    CASE
        WHEN GROUPING(product_line) = 1 THEN 'All Product Lines'
        ELSE product_line
    END AS product_line_label,
    SUM(revenue) AS total_revenue
FROM sales_fact
GROUP BY ROLLUP(region, product_line)
ORDER BY region_label, product_line_label;</code></pre></div><p>For that, the result reads much better than a report full of unlabeled <code>NULL</code> values. It also keeps actual <code>NULL</code> data separate from subtotal placeholders, which matters when results are exported or passed to charts, spreadsheets, or later review. Row order matters too. Subtotal rows can end up mixed into detail rows in ways that make a report feel scrambled. In engines that support <code>GROUPING_ID()</code>, that function can give you a numeric grouping level that helps keep row order stable:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;06a6a2aa-2133-4410-95ed-012c22aa42aa&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    region,
    product_line,
    SUM(revenue) AS total_revenue,
    GROUPING_ID(region, product_line) AS grouping_level
FROM sales_fact
GROUP BY ROLLUP(region, product_line)
ORDER BY grouping_level, region, product_line;</code></pre></div><p>That ordering tends to place detail rows first, then subtotal rows, then the grand total, which is usually easier to scan than relying on whatever order falls out of the grouped result. Engines without <code>GROUPING_ID()</code> can still produce readable output with <code>GROUPING()</code> inside <code>ORDER BY</code>, though the query gets a little longer.</p><p>Label text itself should stay plain. Grand Total, All Regions, All Departments, or similar wording usually works better than labels that force the reader to stop and decode the row. Reports stay easier to follow when subtotal labels match the business language already used in the rest of the output.</p><h4>Picking the Right Form</h4><p>Choosing between <code>ROLLUP</code>, <code>CUBE</code>, and <code>GROUPING SETS</code> comes down to the report layout you need. <code>ROLLUP</code> fits best when grouped columns follow a hierarchy. Time-based reporting is a common case because grouped levels usually move from detailed parts up to broader totals. Year, quarter, and month already form that ladder, so <code>ROLLUP</code> reads naturally there.</p><p>This kind of query is a good fit for hierarchical totals:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;71336dcf-7173-4a29-8424-dba9e0caf79d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    fiscal_year,
    fiscal_quarter,
    fiscal_month,
    SUM(expense_amount) AS total_expense
FROM expense_summary
GROUP BY ROLLUP(fiscal_year, fiscal_quarter, fiscal_month);</code></pre></div><p>In that query, it asks for month-level detail, then quarter totals, then year totals, then the grand total. SQL stays fairly short because the subtotal path is implied by the column order.</p><p><code>CUBE</code> fits a different kind of report. Rather than following a single hierarchy, it returns every grouping combination across the listed columns. That works well when each grouped column can stand on its own as a reporting dimension. Regional totals, channel totals, and product-family totals may all be needed separately, along with the detailed combinations.</p><p>That same strength can also produce more output than the report needs. Each added grouped expression raises the number of subtotal combinations, so <code>CUBE</code> is best saved for reports where those combinations are actually wanted. If the report only needs selected subtotal levels, <code>GROUPING SETS</code> is usually the better choice.</p><p>Take a query like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;b683187e-8235-4ccf-8f6b-8fa71a1c3d3e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    channel,
    subscription_tier,
    billing_cycle,
    COUNT(*) AS account_count
FROM subscription_accounts
GROUP BY GROUPING SETS
(
    (channel, subscription_tier, billing_cycle),
    (channel, subscription_tier),
    (channel),
    ()
);</code></pre></div><p>That query asks for exactly the subtotal levels needed and nothing extra. That makes <code>GROUPING SETS</code> a strong choice when the report layout is fixed ahead of time and only certain subtotal levels belong in the output.</p><p>In short, <code>ROLLUP</code> fits ladder-like totals, <code>CUBE</code> fits every-combination summaries, and <code>GROUPING SETS</code> fits hand-picked subtotal layouts. Picking the form that matches the report keeps the output smaller and keeps the SQL easier to follow later.</p><h4>Portability Across Major Engines</h4><p>Support across database engines is close in some places and uneven in others. PostgreSQL, SQL Server, and Oracle support <code>GROUPING SETS</code>, <code>ROLLUP</code>, and <code>CUBE</code>. SQL Server also supports helper functions such as <code>GROUPING()</code> and <code>GROUPING_ID()</code>. Oracle supports both <code>GROUPING()</code> and <code>GROUPING_ID()</code>, and PostgreSQL supports <code>GROUPING()</code>. That gives those three engines fairly similar coverage for subtotal query work, though the helper function details are not identical across them.</p><p>MySQL is where portability questions show up faster. Standard MySQL centers more on <code>WITH ROLLUP</code>, so queries written with <code>GROUPING SETS</code> or <code>CUBE</code> do not travel across plain MySQL deployments the same way they do across PostgreSQL, SQL Server, and Oracle. That matters when the same reporting query has to run on more than one backend.</p><p>That difference changes how portable SQL should be written. Codebases targeting PostgreSQL, SQL Server, and Oracle can use the full family more freely. When MySQL is part of the target set, the safe subset gets smaller, and subtotal queries may need to rely more on <code>ROLLUP</code> or fall back to <code>UNION ALL</code> for wider compatibility.</p><p>Helper functions matter too. SQL Server has both <code>GROUPING()</code> and <code>GROUPING_ID()</code>, which makes labeling and sorting subtotal rows easier. PostgreSQL and Oracle cover the basic grouping marker function, but not every helper is shared the same way across engines. Query text that feels tidy in one engine can need small rewrites in another.</p><p>Portability also goes past syntax. Large generated reports can push engine-specific grouping limits or produce more subtotal rows than expected. That is worth keeping in mind when query builders or report layers build grouped SQL automatically instead of by hand.</p><h4>Direct Alternatives</h4><p>Grouped extensions are not the only way to write subtotal queries. The most direct fallback is still multiple grouped <code>SELECT</code> statements combined with <code>UNION ALL</code>. That form is longer, but it is familiar SQL and travels well because it only depends on regular grouping and set operations.</p><p>This version shows the manual form:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;49fc9994-0bc1-49af-9d93-5342cfa6e222&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    division,
    team_name,
    SUM(ticket_count) AS total_tickets
FROM support_volume
GROUP BY division, team_name

UNION ALL

SELECT
    division,
    NULL AS team_name,
    SUM(ticket_count) AS total_tickets
FROM support_volume
GROUP BY division

UNION ALL

SELECT
    NULL AS division,
    NULL AS team_name,
    SUM(ticket_count) AS total_tickets
FROM support_volume;</code></pre></div><p>In that, the query can produce the same kind of result you would ask from <code>GROUPING SETS</code>. The tradeoff is repetition. Filters, joins, computed expressions, and business rules have to stay aligned across every branch, which leaves more room for drift when somebody edits the query later.</p><p>Window functions are another nearby option, but they solve a different problem. Window aggregates attach totals to detail rows without collapsing the result into grouped subtotal rows. That is useful when a report needs every detail row to stay visible while still displaying summary values beside it.</p><p>We can see this kind of query in action:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;14384699-87c9-4739-82cb-14090d296153&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    division,
    team_name,
    ticket_id,
    opened_at,
    COUNT(*) OVER (PARTITION BY division) AS division_ticket_total
FROM support_tickets;</code></pre></div><p>That query keeps every ticket row and adds a division total next to it. Useful output, but not the same thing as subtotal rows returned by <code>ROLLUP</code> or <code>GROUPING SETS</code>.</p><p>Summary tables and materialized views also belong in the larger discussion. Those options fit recurring reporting workloads where the same subtotal-heavy queries are read repeatedly. That choice is less about subtotal syntax and more about where the summary work should happen, at query time or ahead of time.</p><h3>Conclusion</h3><p><code>GROUPING SETS</code>, <code>ROLLUP</code>, and <code>CUBE</code> all build on normal <code>GROUP BY</code>, but they change what a grouped query can return by letting one statement produce more than one aggregation level. That lets the database generate detail totals, subtotal rows, and a grand total from the same grouped operation instead of forcing separate queries to be stacked by hand. When viewed mechanically, the main difference comes down to how grouping levels are selected, how subtotal rows are marked, and how far the syntax travels across database engines.</p><ol><li><p><em><a href="https://www.postgresql.org/docs/current/queries-table-expressions.html#QUERIES-GROUPING-SETS">PostgreSQL </a></em><code>GROUP BY</code><em><a href="https://www.postgresql.org/docs/current/queries-table-expressions.html#QUERIES-GROUPING-SETS"> and Grouping Sets Documentation</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/queries/select-group-by-transact-sql?view=sql-server-ver17">SQL Server </a></em><code>SELECT GROUP BY</code><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/queries/select-group-by-transact-sql?view=sql-server-ver17"> Documentation</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/functions/grouping-transact-sql?view=sql-server-ver17">SQL Server </a></em><code>GROUPING</code><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/functions/grouping-transact-sql?view=sql-server-ver17"> Documentation</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/functions/grouping-id-transact-sql?view=sql-server-ver17">SQL Server </a></em><code>GROUPING_ID</code><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/functions/grouping-id-transact-sql?view=sql-server-ver17"> Documentation</a></em></p></li><li><p><em><a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/sqlrf/SELECT.html">Oracle Database </a></em><code>SELECT</code><em><a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/sqlrf/SELECT.html"> Documentation</a></em></p></li><li><p><em><a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/sqlrf/GROUPING.html">Oracle Database </a></em><code>GROUPING</code><em><a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/sqlrf/GROUPING.html"> Documentation</a></em></p></li><li><p><em><a href="https://dev.mysql.com/doc/refman/8.4/en/group-by-modifiers.html">MySQL </a></em><code>GROUP BY</code><em><a href="https://dev.mysql.com/doc/refman/8.4/en/group-by-modifiers.html"> Modifiers and </a></em><code>WITH ROLLUP</code><em><a href="https://dev.mysql.com/doc/refman/8.4/en/group-by-modifiers.html"> Documentation</a></em></p></li></ol><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share&quot;,&quot;text&quot;:&quot;Share Alexander Obregon's Substack&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share"><span>Share Alexander Obregon's Substack</span></a></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UL82!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc004ab0-519e-4bc7-9a5e-0c288773a1a7_306x306.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UL82!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc004ab0-519e-4bc7-9a5e-0c288773a1a7_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!UL82!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc004ab0-519e-4bc7-9a5e-0c288773a1a7_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!UL82!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc004ab0-519e-4bc7-9a5e-0c288773a1a7_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!UL82!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc004ab0-519e-4bc7-9a5e-0c288773a1a7_306x306.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UL82!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc004ab0-519e-4bc7-9a5e-0c288773a1a7_306x306.png" width="306" height="306" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fc004ab0-519e-4bc7-9a5e-0c288773a1a7_306x306.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:306,&quot;width&quot;:306,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!UL82!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc004ab0-519e-4bc7-9a5e-0c288773a1a7_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!UL82!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc004ab0-519e-4bc7-9a5e-0c288773a1a7_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!UL82!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc004ab0-519e-4bc7-9a5e-0c288773a1a7_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!UL82!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc004ab0-519e-4bc7-9a5e-0c288773a1a7_306x306.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://freesvg.org/mono-sql">Image Source</a></figcaption></figure></div>]]></content:encoded></item><item><title><![CDATA[Lateral Joins in SQL for Per-Row Lookups]]></title><description><![CDATA[Lots of SQL queries need one related row for each parent row instead of every related row.]]></description><link>https://alexanderobregon.substack.com/p/lateral-joins-in-sql-for-per-row</link><guid isPermaLink="false">https://alexanderobregon.substack.com/p/lateral-joins-in-sql-for-per-row</guid><dc:creator><![CDATA[Alexander Obregon]]></dc:creator><pubDate>Fri, 13 Mar 2026 19:03:35 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!kVvv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d50be90-5f4c-44f8-81b9-c6e72fb11772_306x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!VBmQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd65b21a4-f859-4c05-8fa2-85053d819677_800x373.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!VBmQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd65b21a4-f859-4c05-8fa2-85053d819677_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!VBmQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd65b21a4-f859-4c05-8fa2-85053d819677_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!VBmQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd65b21a4-f859-4c05-8fa2-85053d819677_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!VBmQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd65b21a4-f859-4c05-8fa2-85053d819677_800x373.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!VBmQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd65b21a4-f859-4c05-8fa2-85053d819677_800x373.png" width="800" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d65b21a4-f859-4c05-8fa2-85053d819677_800x373.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!VBmQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd65b21a4-f859-4c05-8fa2-85053d819677_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!VBmQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd65b21a4-f859-4c05-8fa2-85053d819677_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!VBmQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd65b21a4-f859-4c05-8fa2-85053d819677_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!VBmQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd65b21a4-f859-4c05-8fa2-85053d819677_800x373.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://commons.wikimedia.org/wiki/File:Sql_data_base_with_logo.png">Image Source</a></figcaption></figure></div><p>Lots of SQL queries need one related row for each parent row instead of every related row. One customer may need the latest order, one product may need the newest price change, and one account may need the highest payment. Lateral join features fit that job nicely. PostgreSQL gives you <code>LATERAL</code>, SQL Server gives you <code>CROSS APPLY</code> and <code>OUTER APPLY</code>, Oracle supports <code>LATERAL</code> inline views along with <code>APPLY</code>, and current MySQL versions support lateral derived tables. Across those systems, the same idea is in play. The table expression on the right can read values from the current row on the left, then return a small result set for that one row. That makes top-one related row queries much easier to write than piling the whole problem into nested correlated subqueries or large ranking logic right away.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>How Per-Row Lookup Joins Work</h3><p>Per-row lookup joins solve a very specific SQL problem. One row on the left needs a related lookup before the database moves to the next row. That gives you a natural way to ask for the latest order for one customer, the newest status row for one ticket, or the first matching shipment for one invoice. Instead of building the whole query around a large join and trimming it down later, the lookup can happen as part of the row flow itself. PostgreSQL does that with <code>LATERAL</code>, while SQL Server does it with <code>CROSS APPLY</code> and <code>OUTER APPLY</code>. In both forms, the right side can read columns from the left side row currently being processed.</p><h4>What LATERAL Changes in PostgreSQL</h4><p>Regular subqueries in the <code>FROM</code> clause are independent. They act like self-contained table expressions, so they cannot reach across and read columns from neighboring <code>FROM</code> items. PostgreSQL changes that rule with <code>LATERAL</code>. With <code>LATERAL</code> in place, a subquery can reference columns from <code>FROM</code> items that appear before it, and that evaluation happens for each row or row set coming from those earlier items.</p><p>That changes how lookup logic can be written. Instead of joining every related row and then working out which one you wanted, you can ask PostgreSQL to take one left row, run a small right-side query tied to that row, and join back only what that small query returns. That is why <code>LATERAL</code> fits per-row lookups so well. The database is not treating the right side like a fixed mini-table. It is treating it as a row-dependent table expression.</p><p>Seeing a customer-and-order example can make that flow easy to see:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;be7f5d79-2b4a-4af7-b0f0-39b10d016be1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    c.customer_id,
    c.customer_name,
    last_order.order_id,
    last_order.placed_at
FROM customers AS c
LEFT JOIN LATERAL (
    SELECT
        o.order_id,
        o.placed_at
    FROM orders AS o
    WHERE o.customer_id = c.customer_id
    ORDER BY o.placed_at DESC, o.order_id DESC
    LIMIT 1
) AS last_order ON true;</code></pre></div><p><code>customers</code> is the driving table in that query. For each customer row, the lateral subquery filters <code>orders</code> to that customer, sorts the related rows so the newest one comes first, and keeps only one row with <code>LIMIT 1</code>. <code>LEFT JOIN</code> keeps customers that have no matching orders, so the order columns become null for those rows. The <code>ON true</code> part is common in PostgreSQL lateral join syntax when the row-matching logic already lives inside the subquery.</p><p>PostgreSQL also allows <code>LATERAL</code> with table functions in <code>FROM</code>. For row lookup work, though, the common case is still a lateral subquery that filters, sorts, and trims related rows.</p><p>Status history is another good fit for the same idea:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;88c1c750-9611-4ac1-8c43-7cd3ac8658ac&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    t.ticket_id,
    t.subject,
    s.status_code,
    s.changed_at
FROM support_tickets AS t
LEFT JOIN LATERAL (
    SELECT
        h.status_code,
        h.changed_at
    FROM ticket_status_history AS h
    WHERE h.ticket_id = t.ticket_id
    ORDER BY h.changed_at DESC, h.history_id DESC
    LIMIT 1
) AS s ON true;</code></pre></div><p>The flow stays the same. One ticket row goes in, one small history lookup runs, then zero or one chosen history row comes back out.</p><h4>CROSS APPLY and OUTER APPLY in SQL Server</h4><p>SQL Server uses <code>APPLY</code> for this family of joins. <code>CROSS APPLY</code> and <code>OUTER APPLY</code> let the right table expression run against each row from the left side. That makes <code>APPLY</code> row-aware in a way that a regular derived table is not. The right side can read values from the current left row and return a rowset tied to that row. If the right side finds one row, that row is attached. If it finds no rows, the final result depends on which <code>APPLY</code> form you picked.</p><p><code>OUTER APPLY</code> keeps the left row and fills the right-side columns with nulls when there is no match. That makes it the SQL Server counterpart to a left-style lateral join.</p><p>Take a look at this version:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;afeda67c-9b51-437b-92a0-f55c66190a64&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    c.customer_id,
    c.customer_name,
    x.order_id,
    x.placed_at
FROM customers AS c
OUTER APPLY (
    SELECT TOP (1)
        o.order_id,
        o.placed_at
    FROM orders AS o
    WHERE o.customer_id = c.customer_id
    ORDER BY o.placed_at DESC, o.order_id DESC
) AS x;</code></pre></div><p>That query is very close in spirit to the PostgreSQL version, but the SQL Server syntax changes a bit. <code>TOP (1)</code> takes the place of <code>LIMIT 1</code>, and <code>OUTER APPLY</code> keeps customers with no order rows. The sorting logic still decides which related row counts as the chosen one.</p><p>This example helps separate <code>CROSS APPLY</code> from <code>OUTER APPLY</code> visually:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;49efeb53-0202-4e19-8ecc-98f7e9748a44&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    p.project_id,
    p.project_name,
    latest_note.note_text
FROM projects AS p
CROSS APPLY (
    SELECT TOP (1)
        n.note_text
    FROM project_notes AS n
    WHERE n.project_id = p.project_id
    ORDER BY n.created_at DESC, n.note_id DESC
) AS latest_note;</code></pre></div><p>That version returns only projects that have at least one note, because <code>CROSS APPLY</code> drops left rows when the right side returns nothing. Changing only the word <code>CROSS</code> to <code>OUTER</code> changes the row-preserving behavior without changing the lookup logic itself.</p><p>For a beginner, that is one of the nicest parts of <code>APPLY</code>. The lookup reads like a small query block attached to each left row, so the SQL stays close to the question being answered.</p><h4>Why This Is Better Than Forcing Everything Into a Scalar Subquery</h4><p>Scalar subqueries still have a place in SQL, they work well when one left row needs one value and the logic is short. Trouble starts when one chosen related row has several columns you want to return. Scalar subqueries return one value at a time, not a full row from the child table. Lateral joins and <code>APPLY</code> queries return a table expression instead, so several columns can come back from the chosen row without repeating the same lookup over and over.</p><p>Let&#8217;s imagine we are working with a request for the latest payment per invoice where you need <code>payment_id</code>, <code>paid_at</code>, and <code>amount</code>. With scalar subqueries, one version for each selected value is a common first attempt:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;8bd69773-9291-4c7f-bfb7-386b02ac78ce&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    i.invoice_id,
    (
        SELECT p.payment_id
        FROM payments AS p
        WHERE p.invoice_id = i.invoice_id
        ORDER BY p.paid_at DESC, p.payment_id DESC
        LIMIT 1
    ) AS latest_payment_id,
    (
        SELECT p.paid_at
        FROM payments AS p
        WHERE p.invoice_id = i.invoice_id
        ORDER BY p.paid_at DESC, p.payment_id DESC
        LIMIT 1
    ) AS latest_paid_at,
    (
        SELECT p.amount
        FROM payments AS p
        WHERE p.invoice_id = i.invoice_id
        ORDER BY p.paid_at DESC, p.payment_id DESC
        LIMIT 1
    ) AS latest_amount
FROM invoices AS i;</code></pre></div><p>That style repeats the same filtering and sorting logic three times. It also puts more pressure on the reader, who has to scan each subquery to confirm they all pick the same row. One later edit can break that alignment if a tie-breaker changes in one place but not in the others.</p><p>A row-producing form reads much better for that case:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;941d99c7-1c4f-43cd-a083-dfac9161ca15&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    i.invoice_id,
    p.payment_id,
    p.paid_at,
    p.amount
FROM invoices AS i
LEFT JOIN LATERAL (
    SELECT
        p.payment_id,
        p.paid_at,
        p.amount
    FROM payments AS p
    WHERE p.invoice_id = i.invoice_id
    ORDER BY p.paid_at DESC, p.payment_id DESC
    LIMIT 1
) AS p ON true;</code></pre></div><p>The row-picking logic appears one time, and all three columns come from the same chosen payment row. That is the big readability gain. SQL Server gets the same benefit with <code>APPLY</code>, and the same idea holds there as well. Scalar subqueries also hide the fact that the query is really asking for a row, not just an isolated value. Per-row lookup joins put that row-level intent in the <code>FROM</code> clause, which is a much better home for row-producing logic.</p><h4>Inner-Style Behavior</h4><p>Join behavior matters just as much as the lookup itself. Inner-style behavior means the left row disappears when the right-side lookup returns nothing. PostgreSQL gets that with <code>JOIN LATERAL</code> or <code>CROSS JOIN LATERAL</code>, and SQL Server gets it with <code>CROSS APPLY</code>.</p><p>That behavior fits queries where the related row is required. For example, say you want stores that have at least one inventory count entry, and you only care about the latest count for stores that do have one:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;86fa79d5-87d0-4905-9c3d-4e95d7f854ed&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    s.store_id,
    s.store_name,
    inv.counted_at,
    inv.quantity_on_hand
FROM stores AS s
JOIN LATERAL (
    SELECT
        i.counted_at,
        i.quantity_on_hand
    FROM inventory_counts AS i
    WHERE i.store_id = s.store_id
    ORDER BY i.counted_at DESC, i.count_id DESC
    LIMIT 1
) AS inv ON true;</code></pre></div><p>That query drops stores with no inventory count rows. The join itself is saying that a missing lookup result means there should be no final row.</p><h4>Left-Style Behavior</h4><p>Left-style behavior keeps the left row even when the lookup comes back empty. PostgreSQL gets that with <code>LEFT JOIN LATERAL</code>, while SQL Server gets it with <code>OUTER APPLY</code>. That behavior is a better fit for reports where the parent row still matters without a child match. A department list with the latest review row is one example. Departments with no review yet still belong in the result.</p><p>This SQL Server version shows that form:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;93bdc200-ee5d-4070-879a-0d16f503ecfc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    d.department_id,
    d.department_name,
    r.reviewed_at,
    r.score
FROM departments AS d
OUTER APPLY (
    SELECT TOP (1)
        rv.reviewed_at,
        rv.score
    FROM department_reviews AS rv
    WHERE rv.department_id = d.department_id
    ORDER BY rv.reviewed_at DESC, rv.review_id DESC
) AS r;</code></pre></div><p>Departments with no review rows still appear, but <code>reviewed_at</code> and <code>score</code> come back null. That makes the join behavior match the reporting need. The lookup logic on the right did not change much from the earlier examples. What changed was the decision about what to do when no related row exists.</p><p>That distinction between inner-style and left-style behavior is one of the first things to settle before writing the query. If the business question says every parent row should still appear, the join form has to reflect that. If the business question says only parents with at least one matching child row belong in the result, the inner-style form is the better fit.</p><h3>Writing Good Per-Row Lookup Queries Across Systems</h3><p>Good per-row lookup queries come down to a few practical choices. You need a rule for which related row wins, you need syntax that matches the database product in front of you, and you need supporting indexes that let the database find that row without extra work. The join feature itself is only part of the story. Most mistakes happen earlier, at the point where the query writer has not fully decided what counts as the latest row, the highest row, or the first matching row. After that rule makes sense, the SQL becomes much easier to read and much easier to trust.</p><h4>Picking the Related Row in a Stable Way</h4><p>Per-row lookups work best when the winning row is chosen by a rule that stays stable from run to run. Sorting only by a timestamp can leave ties. Sorting only by a score can do the same. If two child rows are tied on the main sort column, the database still needs a consistent way to place one ahead of the other. That is why a second sort column such as an identity value, sequence value, or primary key is usually part of the lookup. Without that extra ordering rule, the query can still run, but the chosen row is not as firmly defined.</p><p>A latest subscription event query shows the idea visually:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;2a5be39d-5fc7-417e-b2b9-e3a62a5aa690&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    s.subscription_id,
    s.account_id,
    e.event_type,
    e.recorded_at
FROM subscriptions AS s
LEFT JOIN LATERAL (
    SELECT
        se.event_type,
        se.recorded_at
    FROM subscription_events AS se
    WHERE se.subscription_id = s.subscription_id
    ORDER BY se.recorded_at DESC, se.event_id DESC
    LIMIT 1
) AS e ON true;</code></pre></div><p><code>recorded_at DESC</code> says newer rows come first. <code>event_id DESC</code> breaks ties when two rows land on the same timestamp. That extra sort column may look minor, but it changes the query from roughly newest row to a fully defined newest row. PostgreSQL processes the lateral subquery per left row, so that ordering rule is applied separately for each subscription.</p><p>Some lookups are not about time at all. Highest bid per auction, most expensive invoice line per invoice, or earliest appointment per client all follow the same general structure. What changes is the ordering rule inside the right-side query. SQL Server uses <code>TOP (1)</code> in that spot, while the row ranking logic still depends on the <code>ORDER BY</code> list you provide.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;fe844535-7c19-448c-a7c7-024918b8d22e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    a.auction_id,
    a.title,
    b.bid_id,
    b.bid_amount
FROM auctions AS a
OUTER APPLY (
    SELECT TOP (1)
        ab.bid_id,
        ab.bid_amount
    FROM auction_bids AS ab
    WHERE ab.auction_id = a.auction_id
    ORDER BY ab.bid_amount DESC, ab.bid_id DESC
) AS b;</code></pre></div><p>That query asks for the highest bid for each auction. The primary sort is <code>bid_amount DESC</code>. The secondary sort is <code>bid_id DESC</code>. If two bids tie on amount, the larger bid id wins. Without that second part, tied rows are left to a less explicit rule than most people intend.</p><p>Stable row picking also matters when filters appear inside the lookup. A request for the latest paid invoice is different from the latest invoice of any type. That filter belongs inside the right-side query so the database picks from the correct subset first and only then trims to one row. Moving that condition outside can change the result in a way that is easy to miss during a quick read.</p><h4>PostgreSQL, SQL Server, Oracle, and MySQL Compared</h4><p>PostgreSQL uses <code>LATERAL</code> for row-dependent subqueries in the <code>FROM</code> clause. Its docs state that a lateral subquery can reference columns from <code>FROM</code> items that appear before it. That is why a row from the left side can feed values into the right-side query during execution. PostgreSQL also allows lateral behavior with table functions in <code>FROM</code>, where the keyword can be optional for functions.</p><p>SQL Server expresses the same broad idea with <code>CROSS APPLY</code> and <code>OUTER APPLY</code>. Microsoft&#8217;s docs say the right table source is evaluated against each row from the left table source. <code>CROSS APPLY</code> keeps only rows where the right side returns at least one row, while <code>OUTER APPLY</code> keeps the left row and fills the right-side columns with nulls when the lookup returns nothing. That makes <code>APPLY</code> the SQL Server form of a per-row table expression.</p><p>Oracle supports both <code>LATERAL</code> inline views and <code>CROSS APPLY</code> and <code>OUTER APPLY</code>. Oracle also has restrictions around how <code>LATERAL</code> can be used, and <code>APPLY</code> syntax should be treated as its own form rather than stacked with <code>LATERAL</code> in the same right-side clause. Put simply, Oracle gives you both syntax families, but they are not meant to be combined in one right-side table expression.</p><p>MySQL supports lateral derived tables with the <code>LATERAL</code> keyword in the <code>FROM</code> clause. The MySQL manual states that a lateral derived table can appear only in <code>FROM</code>, and it lists join-side restrictions tied to left and right operands. The manual also notes that <code>JSON_TABLE()</code> is treated as though <code>LATERAL</code> were present, so the keyword is implicit there rather than written explicitly before that function.</p><p>These two versions make the syntax contrast easy to see. PostgreSQL looks like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;9a419ed7-e68d-43d0-8acb-d837b4b83aaf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    p.product_id,
    p.product_name,
    ph.changed_at,
    ph.new_price
FROM products AS p
LEFT JOIN LATERAL (
    SELECT
        h.changed_at,
        h.new_price
    FROM price_history AS h
    WHERE h.product_id = p.product_id
    ORDER BY h.changed_at DESC, h.price_history_id DESC
    LIMIT 1
) AS ph ON true;</code></pre></div><p>SQL Server expresses the same lookup like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;13e2a21e-03d0-457f-9040-3f9012119936&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    p.product_id,
    p.product_name,
    ph.changed_at,
    ph.new_price
FROM products AS p
OUTER APPLY (
    SELECT TOP (1)
        h.changed_at,
        h.new_price
    FROM price_history AS h
    WHERE h.product_id = p.product_id
    ORDER BY h.changed_at DESC, h.price_history_id DESC
) AS ph;</code></pre></div><p>The SQL text is different, but the row flow is closely related. One product row is read from the left, a child-row lookup runs for that product, and zero or one selected child row comes back. PostgreSQL and MySQL place that logic under <code>LATERAL</code>. SQL Server places it under <code>APPLY</code>. Oracle accepts both families, subject to its own syntax rules.</p><p>Porting these queries from one database to another usually means changing the syntax around the right-side table expression and the row limiting clause. PostgreSQL uses <code>LIMIT 1</code>. SQL Server uses <code>TOP (1)</code>. Oracle commonly uses its row limiting clause within the subquery, and MySQL uses <code>LIMIT</code>. The bigger lesson is that the query idea survives the port. Product-specific syntax changes, but the per-row lookup rule stays the same.</p><h4>Indexing and Performance Habits That Fit This Pattern</h4><p>Per-row lookup joins tend to work best when the child table has an index that matches both the join filter and the row-picking order. If the query filters child rows by <code>customer_id</code> and then sorts by <code>order_date DESC, order_id DESC</code>, the child-side index usually starts with <code>customer_id</code> and then continues with the sort columns. That gives the database a much better chance to find matching rows quickly and stop after the first qualifying row near the top of the ordered range. Query optimizers differ, but that general index direction lines up well with the lookup itself. Table hints and forced plans are not the starting point here. Good indexing and a well-defined sort rule come first.</p><p>Take a current-status lookup for devices:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;de0920a5-b288-491a-b34b-94468d201d21&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
    d.device_id,
    d.serial_number,
    st.status_code,
    st.logged_at
FROM devices AS d
LEFT JOIN LATERAL (
    SELECT
        ds.status_code,
        ds.logged_at
    FROM device_status_log AS ds
    WHERE ds.device_id = d.device_id
    ORDER BY ds.logged_at DESC, ds.status_log_id DESC
    LIMIT 1
) AS st ON true;</code></pre></div><p>An index on the child table that starts with <code>device_id</code> and then includes <code>logged_at</code> and <code>status_log_id</code> fits that query far better than an index that starts only with <code>logged_at</code>. The lookup starts from the parent row, so the index should follow that same direction. The database still makes the final plan choice, but the lookup has a much better starting point when the access path follows the same columns used for filtering and ordering.</p><p>Just as important, keep the right-side query focused on picking the row instead of carrying every later report calculation inside it. When the lookup subquery starts doing too much, the row-picking rule gets harder to read. Smaller lookup blocks are easier to verify because the filter and ordering rules stay in one place. If more joins or calculations are needed after the child row is chosen, it is usually better to add them outside the lookup rather than crowding the lookup itself.</p><p>One last habit helps keep these queries stable. Decide first what row wins, write that ordering rule in full, make the child-side index support that lookup, and then pick the vendor syntax that matches the database in front of you. That sequence keeps the SQL focused on the result you want instead of drifting into syntax-first writing.</p><h3>Conclusion</h3><p>Per-row lookup joins work by letting one row from the left side feed values into a right-side query, which then returns the related row that matches the lookup rule you wrote. That is why <code>LATERAL</code>, <code>CROSS APPLY</code>, and <code>OUTER APPLY</code> fit latest-row, highest-row, and first-match queries so well across different SQL engines. After the row choice is fully defined, the rest comes down to matching the database syntax and giving the child table an index that lines up with the filter and sort order.</p><ol><li><p><em><a href="https://www.postgresql.org/docs/current/queries-table-expressions.html">PostgreSQL Table Expressions and </a></em><code>LATERAL</code></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/queries/from-transact-sql?view=sql-server-ver17">SQL Server </a></em><code>FROM</code><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/queries/from-transact-sql?view=sql-server-ver17"> Clause with </a></em><code>APPLY</code></p></li><li><p><em><a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/sqlrf/SELECT.html">Oracle Database </a></em><code>SELECT</code><em><a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/sqlrf/SELECT.html"> Statement Reference</a></em></p></li><li><p><em><a href="https://dev.mysql.com/doc/refman/8.4/en/lateral-derived-tables.html">MySQL Lateral Derived Tables</a></em></p></li><li><p><em><a href="https://www.postgresql.org/docs/current/sql-select.html">PostgreSQL </a></em><code>SELECT</code><em><a href="https://www.postgresql.org/docs/current/sql-select.html"> Reference</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-table?view=sql-server-ver17">SQL Server Query Hints and Table Hints</a></em></p></li></ol><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share Alexander Obregon's Substack&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share Alexander Obregon's Substack</span></a></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!kVvv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d50be90-5f4c-44f8-81b9-c6e72fb11772_306x306.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!kVvv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d50be90-5f4c-44f8-81b9-c6e72fb11772_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!kVvv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d50be90-5f4c-44f8-81b9-c6e72fb11772_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!kVvv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d50be90-5f4c-44f8-81b9-c6e72fb11772_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!kVvv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d50be90-5f4c-44f8-81b9-c6e72fb11772_306x306.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!kVvv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d50be90-5f4c-44f8-81b9-c6e72fb11772_306x306.png" width="306" height="306" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9d50be90-5f4c-44f8-81b9-c6e72fb11772_306x306.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:306,&quot;width&quot;:306,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!kVvv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d50be90-5f4c-44f8-81b9-c6e72fb11772_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!kVvv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d50be90-5f4c-44f8-81b9-c6e72fb11772_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!kVvv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d50be90-5f4c-44f8-81b9-c6e72fb11772_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!kVvv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d50be90-5f4c-44f8-81b9-c6e72fb11772_306x306.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://freesvg.org/mono-sql">Image Source</a></figcaption></figure></div>]]></content:encoded></item><item><title><![CDATA[Table Normalization Basics in SQL]]></title><description><![CDATA[Normalization structures tables so one fact lives in one place, which helps inserts, updates, and deletes avoid conflicting copies of the same data.]]></description><link>https://alexanderobregon.substack.com/p/table-normalization-basics-in-sql</link><guid isPermaLink="false">https://alexanderobregon.substack.com/p/table-normalization-basics-in-sql</guid><dc:creator><![CDATA[Alexander Obregon]]></dc:creator><pubDate>Sat, 07 Mar 2026 18:18:38 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!4J_J!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1548ddd1-8094-4e8d-bca5-6aad2a2aac17_306x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2iJf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b624084-0e17-44d2-8907-2bed1dc1d877_800x373.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2iJf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b624084-0e17-44d2-8907-2bed1dc1d877_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!2iJf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b624084-0e17-44d2-8907-2bed1dc1d877_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!2iJf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b624084-0e17-44d2-8907-2bed1dc1d877_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!2iJf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b624084-0e17-44d2-8907-2bed1dc1d877_800x373.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2iJf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b624084-0e17-44d2-8907-2bed1dc1d877_800x373.png" width="800" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9b624084-0e17-44d2-8907-2bed1dc1d877_800x373.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!2iJf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b624084-0e17-44d2-8907-2bed1dc1d877_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!2iJf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b624084-0e17-44d2-8907-2bed1dc1d877_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!2iJf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b624084-0e17-44d2-8907-2bed1dc1d877_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!2iJf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b624084-0e17-44d2-8907-2bed1dc1d877_800x373.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://commons.wikimedia.org/wiki/File:Sql_data_base_with_logo.png">Image Source</a></figcaption></figure></div><p>Normalization structures tables so one fact lives in one place, which helps inserts, updates, and deletes avoid conflicting copies of the same data. It starts with spotting repeated groups and relationships that are hiding inside wide rows, then splitting that data into tables that match how the rows relate to each other, so changes stay consistent without side effects.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>Why Normalization Exists</h3><p>In short, normalization keeps facts in one place so the database does not store conflicting copies of the same information. Tables start drifting when one row tries to represent more than one thing at the same time, such as an order, the items on that order, and customer details. That drift usually appears in two ways repeated groups that grow sideways into extra columns, and anomalies where routine edits create mismatched data.</p><h4>Repeated Groups Show Up as Extra Columns or Mixed Lists</h4><p>Wide rows can hide repeated groups when the same set of fields repeats side by side, like <code>item1_*</code>, <code>item2_*</code>, and so on. Another form shows up when a table stores a list inside one column, like SKUs joined by commas. Both styles hide the same relationship. One order can contain multiple items, yet the table is laid out like an order can only hold a fixed number of items.</p><p>An early draft order table looks like this because it&#8217;s normal to keep everything together in one record:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;d0e4bf6d-08af-4328-bff4-83109eac1271&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE order_sheet (
  order_id        BIGINT PRIMARY KEY,
  order_date      DATE NOT NULL,
  customer_id     BIGINT NOT NULL,
  customer_name   VARCHAR(255) NOT NULL,
  customer_phone  VARCHAR(32),
  item1_sku       VARCHAR(50),
  item1_name      VARCHAR(255),
  item1_price     NUMERIC(10,2),
  item1_qty       INTEGER,
  item2_sku       VARCHAR(50),
  item2_name      VARCHAR(255),
  item2_price     NUMERIC(10,2),
  item2_qty       INTEGER
);</code></pre></div><p>Pressure shows up quickly. First, a hard ceiling appears on how much one order can hold, so extra items push you toward adding <code>item3_*</code> columns or splitting one order into multiple rows, both of which blur what <code>order_id</code> is supposed to mean. Second, the layout stops matching the questions you want to ask. Searching for orders that contain a product turns into a scan across multiple columns, and the query gets longer every time more item columns are added:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;dockerfile&quot;,&quot;nodeId&quot;:&quot;bad09040-b1a2-4a97-bdc0-cd095f930cf3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-dockerfile">SELECT order_id, order_date
FROM order_sheet
WHERE item1_sku = 'SKU-1007'
   OR item2_sku = 'SKU-1007';</code></pre></div><p>Total calculations run into similar trouble. Once line counts vary, totals turn into repeated expressions with lots of null handling, which is a sign the table is carrying a repeating structure that wants rows instead of columns.</p><p>Lists inside one column cause the same problems in a different disguise. One column that holds values like <code>SKU-1007,SKU-2040</code> cannot be validated with a normal foreign key, cannot be joined safely to a product table, and turns searches into string work. That pushes business rules into ad hoc parsing logic, and different queries start parsing the same column in different ways.</p><h4>Anomalies Signal Structural Trouble</h4><p>Problems show up fast when routine changes force edits across more rows than expected, when recording one fact requires inventing unrelated facts, or when deleting one row removes data you did not mean to remove.</p><p>Update anomalies appear when one fact is stored in multiple places. In the <code>order_sheet</code> table, customer data is copied into each order row. If a customer changes their phone number, the edit needs to touch every row that repeated that value. Missing one row leaves the database claiming two phone numbers for the same customer.</p><p>Two updates that do not hit the same set of rows can create a mismatch fast:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;9d6defad-40c0-4ec9-a322-69bf7fcc93cf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">UPDATE order_sheet
SET customer_phone = '715-555-0199'
WHERE customer_id = 501;

UPDATE order_sheet
SET customer_phone = '715-555-0188'
WHERE customer_id = 501
  AND order_date &gt;= DATE '2026-02-01';</code></pre></div><p>After those statements, <code>customer_id</code> 501 has two phone numbers stored in the same table. Nothing in the table structure prevents it, because the layout treats customer details as if they were order details.</p><p>Insert anomalies show up when the table layout blocks you from recording something that should stand on its own. If product details only exist inside <code>item1_name</code>, <code>item1_price</code>, and similar columns, adding a new product to a catalog becomes impossible without also creating an order row, because the table has no separate home for product facts. People work around that by inserting placeholder orders or placeholder customers, which pollutes data quality.</p><p>Delete anomalies show up when removing one row also removes facts that were not part of the deletion decision. If the only place a product name exists is inside old order rows, deleting old orders can erase the last record of what that product was called. That is a side effect of storing multiple concepts in one row, where each concept should have its own table.</p><h3>First Normal Form Through Third Normal Form With One Running Example</h3><p>This part takes the order story and reshapes it step by step so the tables match the relationships in the data. The same order can contain multiple line items, product details belong to a product catalog, and customer details belong to the customer record. Normal forms give names to those separations so you can reason about them and spot trouble early.</p><h4>First Normal Form Make Rows Atomic</h4><p>Row structure comes first. Values stay atomic, meaning one value per column, and repeating groups stop living as extra column sets like <code>item1_*</code> and <code>item2_*</code>. Instead of widening the table each time an order has more items, line items become rows in their own table, tied back to the order. The table split below keeps order level facts in <code>orders</code> and item level facts in <code>order_item</code>. <code>line_no</code> gives each item a stable identity inside its order, which matters when the same SKU appears twice for separate lines or when line order matters on invoices.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;197fa13e-562e-4ee0-9995-592c12385b15&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE orders (
  order_id     BIGINT PRIMARY KEY,
  order_date   DATE NOT NULL,
  customer_id  BIGINT NOT NULL
);

CREATE TABLE order_item (
  order_id    BIGINT NOT NULL,
  line_no     INTEGER NOT NULL,
  sku         VARCHAR(50) NOT NULL,
  qty         INTEGER NOT NULL,
  unit_price  NUMERIC(10,2) NOT NULL,
  PRIMARY KEY (order_id, line_no),
  FOREIGN KEY (order_id) REFERENCES orders(order_id),
  CHECK (qty &gt; 0),
  CHECK (unit_price &gt;= 0)
);</code></pre></div><p>After that split, any order can carry as many items as needed without altering the schema. Searches also get easier because the SKU lives in one column across all line rows, rather than spread across <code>item1_sku</code>, <code>item2_sku</code>, and future columns.</p><p>Data entry also starts to be a bit more natural. Insert the order once, then insert one row per line item:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;d2d3cec8-d667-427c-be3a-c96d7c7606b6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">INSERT INTO orders (order_id, order_date, customer_id)
VALUES (90001, DATE '2026-02-10', 501);

INSERT INTO order_item (order_id, line_no, sku, qty, unit_price)
VALUES
  (90001, 1, 'SKU-1007', 2, 19.99),
  (90001, 2, 'SKU-2040', 1, 8.50),
  (90001, 3, 'SKU-1007', 1, 17.99);</code></pre></div><p>That last line item repeats the same SKU on a separate line with a different <code>unit_price</code>. That can happen with discounts, price changes, or partial returns. A wide table with one SKU slot per column set struggles with that case because the repeated group is trying to represent a list, not a fixed set of columns.</p><h4>Second Normal Form Remove Partial Dependencies From Composite Identifiers</h4><p>Composite identifiers deserve extra attention. <code>order_item</code> identifies a row by <code>(order_id, line_no)</code>, which means any non identifying column in that table should depend on the full pair, not only part of it. Quantities and invoice prices belong to that line within that order, so they fit naturally.</p><p>Order level values like <code>order_date</code> and <code>customer_id</code> depend on <code>order_id</code> alone, so they stay in <code>orders</code> instead of being repeated on every <code>order_item</code> row identified by (<code>order_id</code>, <code>line_no</code>). That keeps one edit from turning into many edits, and it prevents line rows from disagreeing about the same order.</p><p>After that, you can split catalog facts into <code>product</code> so the name and current <code>list_price</code> live with the <code>sku</code>, while <code>order_item</code> keeps the invoice <code>unit_price</code> that reflects what was charged on that order.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;82695cf0-b1c8-434f-ba21-9b53d048d5ac&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE product (
  sku         VARCHAR(50) PRIMARY KEY,
  name        VARCHAR(255) NOT NULL,
  list_price  NUMERIC(10,2) NOT NULL,
  CHECK (list_price &gt;= 0)
);

ALTER TABLE order_item
  ADD CONSTRAINT order_item_product_fk
  FOREIGN KEY (sku) REFERENCES product(sku);</code></pre></div><p><code>order_item</code> can keep <code>sku</code>, <code>qty</code>, and the invoice <code>unit_price</code>, while the current catalog <code>name</code> comes from joining to <code>product</code> when needed. Put a foreign key on <code>order_item.sku</code> so the database can validate that every line item references a real catalog row. That division also supports a common business rule. <code>list_price</code> can change over time, while <code>unit_price</code> on the order line reflects what was charged on that date.</p><h4>Third Normal Form Remove Transitive Dependencies</h4><p>Transitive dependencies show up when a non identifying column depends on another non identifying column. Customer attributes in an order table are the classic case. When <code>orders</code> stores <code>customer_id</code>, any customer name or phone stored in <code>orders</code> depends on <code>customer_id</code>, not on <code>order_id</code>. That creates duplicated customer facts across order rows.</p><p>Customer attributes belong in a <code>customer</code> table, with <code>orders</code> referencing it:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;293ffaf0-3c45-401d-b679-f238103d6bca&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE customer (
  customer_id  BIGINT PRIMARY KEY,
  name         VARCHAR(255) NOT NULL,
  phone        VARCHAR(32)
);

ALTER TABLE orders
  ADD CONSTRAINT orders_customer_fk
  FOREIGN KEY (customer_id) REFERENCES customer(customer_id);</code></pre></div><p>With that structure, changing a phone number becomes one update in one place, and all orders that reference the customer still point at the current contact data. Joins in reports pull the related rows back into one result set so you can view an order with its customer details in a single query:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;e0c15cbd-d7e3-4426-91cc-2153ba74d666&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT
  o.order_id,
  o.order_date,
  c.name AS customer_name,
  c.phone AS customer_phone
FROM orders o
JOIN customer c ON c.customer_id = o.customer_id
WHERE o.order_date &gt;= DATE '2026-01-01'
ORDER BY o.order_date, o.order_id;</code></pre></div><p>Some data looks like customer data but is actually order data. Shipping address is a good example. Customers can have multiple addresses, orders can ship to a one time destination, and historical invoices usually need the exact address that was used at the time. Normalization does not force one choice there. It pushes you to decide what the column represents. If the value represents the customer profile, store it with the customer. If the value represents what happened on the order, store it with the order.</p><h4>Spotting Junction Table Needs</h4><p>Junction tables come up when two entity tables relate in a many to many way. Promotions and orders fit that structure well, because an order can have multiple promotions applied, and a promotion can apply to multiple orders. Repeated columns like <code>promo_code1</code> and <code>promo_code2</code> run into the same repeated group problem as item columns, and a comma separated list runs into the same validation and query problems as any list stored in a single column.</p><p>With a junction table, the relationship is stored as rows instead of being packed into columns or text:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;4e412f52-cdc6-4df7-9235-b876820c9af9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE promotion (
  promo_code  VARCHAR(50) PRIMARY KEY,
  description VARCHAR(255) NOT NULL
);

CREATE TABLE order_promotion (
  order_id    BIGINT NOT NULL,
  promo_code  VARCHAR(50) NOT NULL,
  PRIMARY KEY (order_id, promo_code),
  FOREIGN KEY (order_id) REFERENCES orders(order_id),
  FOREIGN KEY (promo_code) REFERENCES promotion(promo_code)
);</code></pre></div><p>That structure supports questions that are awkward in a wide column layout, such as finding all orders that used a given promotion or counting promotion usage over a date range. It also supports validation. Foreign keys can confirm that every <code>promo_code</code> recorded on an order exists in the promotion table, and the composite primary identifier prevents duplicate application of the same promotion to the same order unless you intentionally model repeated application as its own concept.</p><p>Data entry follows the same rhythm as line items. Insert promotions into <code>promotion</code>, then record pairings in <code>order_promotion</code> as they happen on orders:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;962c1c0d-5c11-4dfb-b94a-b954bf052125&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">INSERT INTO promotion (promo_code, description)
VALUES
  ('WELCOME10', 'New customer discount'),
  ('FREESHIP', 'Free shipping promotion');

INSERT INTO order_promotion (order_id, promo_code)
VALUES
  (90001, 'WELCOME10'),
  (90001, 'FREESHIP');</code></pre></div><p>After the relationship is stored as rows, the database can answer relationship questions through joins and filtering, rather than through column scanning or string parsing.</p><h3>Conclusion</h3><p>Normalization works by turning hidden repetition into explicit relationships that the database can store and join reliably. First normal form moves repeating groups out of wide rows so line items become their own rows tied back to an order. Second normal form keeps order level facts off line item rows when the line item table is identified by a multi column identifier, so values like <code>order_date</code> and <code>customer_id</code> stay on the single order row in <code>orders</code> instead of being repeated across lines. Third normal form separates customer facts from order facts, so customer attributes change in one place while <code>orders</code> keep the history of order facts. If you need historical customer name or phone as it was at purchase time, store a snapshot on the order or in a separate history table. When the relationship is many to many, the junction table keeps each pairing in its own row, so constraints can validate the data and queries stay join based instead of scanning extra columns or splitting strings.</p><ol><li><p><em><a href="https://www.postgresql.org/docs/current/sql-createtable.html">PostgreSQL Documentation on </a></em><code>CREATE TABLE</code></p></li><li><p><em><a href="https://www.postgresql.org/docs/current/ddl-constraints.html">PostgreSQL Documentation on Constraints</a></em></p></li><li><p><em><a href="https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-FK">PostgreSQL Documentation on Foreign Keys</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-transact-sql">SQL Server Documentation on </a></em><code>CREATE TABLE</code></p></li><li><p><em><a href="https://dev.mysql.com/doc/refman/8.0/en/create-table-foreign-keys.html">MySQL Documentation on InnoDB Foreign Keys</a></em></p></li><li><p><em><a href="https://www.sqlite.org/foreignkeys.html">SQLite Documentation on Foreign Key Support</a></em></p></li></ol><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share&quot;,&quot;text&quot;:&quot;Share Alexander Obregon's Substack&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share"><span>Share Alexander Obregon's Substack</span></a></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4J_J!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1548ddd1-8094-4e8d-bca5-6aad2a2aac17_306x306.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4J_J!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1548ddd1-8094-4e8d-bca5-6aad2a2aac17_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!4J_J!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1548ddd1-8094-4e8d-bca5-6aad2a2aac17_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!4J_J!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1548ddd1-8094-4e8d-bca5-6aad2a2aac17_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!4J_J!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1548ddd1-8094-4e8d-bca5-6aad2a2aac17_306x306.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4J_J!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1548ddd1-8094-4e8d-bca5-6aad2a2aac17_306x306.png" width="306" height="306" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1548ddd1-8094-4e8d-bca5-6aad2a2aac17_306x306.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:306,&quot;width&quot;:306,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!4J_J!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1548ddd1-8094-4e8d-bca5-6aad2a2aac17_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!4J_J!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1548ddd1-8094-4e8d-bca5-6aad2a2aac17_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!4J_J!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1548ddd1-8094-4e8d-bca5-6aad2a2aac17_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!4J_J!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1548ddd1-8094-4e8d-bca5-6aad2a2aac17_306x306.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://freesvg.org/mono-sql">Image Source</a></figcaption></figure></div>]]></content:encoded></item><item><title><![CDATA[SQL Cursors for Row by Row Work]]></title><description><![CDATA[In general, SQL runs best when you tell the database which rows you want and what result you want back, then let the optimizer choose a plan to read and transform that set.]]></description><link>https://alexanderobregon.substack.com/p/sql-cursors-for-row-by-row-work</link><guid isPermaLink="false">https://alexanderobregon.substack.com/p/sql-cursors-for-row-by-row-work</guid><dc:creator><![CDATA[Alexander Obregon]]></dc:creator><pubDate>Wed, 04 Mar 2026 18:28:15 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!nVXc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7508b660-0148-4efb-a882-79901c32c5e4_306x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!INpb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde98b191-36af-4573-b170-fca5f050adbc_800x373.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!INpb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde98b191-36af-4573-b170-fca5f050adbc_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!INpb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde98b191-36af-4573-b170-fca5f050adbc_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!INpb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde98b191-36af-4573-b170-fca5f050adbc_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!INpb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde98b191-36af-4573-b170-fca5f050adbc_800x373.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!INpb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde98b191-36af-4573-b170-fca5f050adbc_800x373.png" width="800" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/de98b191-36af-4573-b170-fca5f050adbc_800x373.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!INpb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde98b191-36af-4573-b170-fca5f050adbc_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!INpb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde98b191-36af-4573-b170-fca5f050adbc_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!INpb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde98b191-36af-4573-b170-fca5f050adbc_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!INpb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde98b191-36af-4573-b170-fca5f050adbc_800x373.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://commons.wikimedia.org/wiki/File:Sql_data_base_with_logo.png">Image Source</a></figcaption></figure></div><p>In general, SQL runs best when you tell the database which rows you want and what result you want back, then let the optimizer choose a plan to read and transform that set. That set based style is the default for good reasons, but stored routines still sometimes need row by row work for stateful steps, external calls, or ordered processing that you cannot express as one statement.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>Set First Thinking Before Reaching for a Cursor</h3><p>Cursor work starts making sense only after you have already done the set work. Think of it as two phases that stay separate. Phase one is a query that narrows the work down to the rows that matter and returns only the columns you need. Phase two is the procedural step that runs per row, usually because it has state, side effects, or per row error handling that a single statement cannot express well.</p><p>Keeping that split upfront keeps the cursor small and keeps the database doing what it is built to do, which is filter, join, and project sets of rows efficiently. It also helps you test the input set on its own before any procedural logic touches it.</p><h4>What Set Based SQL Is Built to Do</h4><p>Set based SQL focuses on describing results, not dictating step order. You express a relationship between tables, filters, and projections, then the optimizer decides how to reach that result. That freedom is what allows join reordering, predicate pushdown, index selection, streaming operators, and parallel execution where the engine supports it. One practical way to see the difference is to compare a single statement that updates a set of rows against a loop that updates one row at a time.</p><p>This is a typical set based update in SQL Server. It touches every matching row in one statement, and the engine chooses a plan that matches the table sizes and indexes:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;01cbcd6d-0dd4-4404-a420-5ec2fcf12cf8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">UPDATE a
SET a.StatusId = w.NewStatus,
    a.UpdatedAt = SYSUTCDATETIME()
FROM dbo.Account AS a
JOIN dbo.AccountStatusWork AS w
  ON w.AccountId = a.AccountId
WHERE w.ProcessedAt IS NULL;</code></pre></div><p>That statement does not lock you into one join method or one access path. It still has a defined result, but the path to that result is chosen by the optimizer based on statistics, indexes, and the current environment. That is the main reason set based SQL tends to outperform procedural loops for pure data movement.</p><p>Ordering fits the same idea, a query only has an ordered output when you ask for it with <code>ORDER BY</code>. The database can read data in whatever internal order is efficient, then sort or otherwise order the final output stream when required.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;cdfb84c9-6bda-4ee3-8b36-4d56f3fc5c59&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT TOP (50) w.AccountId, w.NewStatus
FROM dbo.AccountStatusWork AS w
WHERE w.ProcessedAt IS NULL
ORDER BY w.AccountId;</code></pre></div><p>That query gives you predictable ordering in the result, while still letting the engine pick the best way to locate the rows.</p><h4>When Row by Row Work Still Makes Sense</h4><p>Row by row work earns its place when each row triggers a procedural action, when the next step depends on state that changes as you go, or when you need per row logging and error capture. That is where cursor style processing fits, because it gives you a controlled sequence over a query result set.</p><p>Practical work goes better when the per row stage is fed by a tight set producing query. Work tables are common for this, you populate them in a set based statement, then process the queued rows one at a time.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;4aab3271-20dc-4c82-b4cf-3658c06e9c5f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">INSERT INTO dbo.AccountStatusWork (AccountId, NewStatus, CreatedAt)
SELECT a.AccountId,
       CASE WHEN a.IsLocked = 1 THEN 5 ELSE 2 END,
       SYSUTCDATETIME()
FROM dbo.Account AS a
WHERE a.UpdatedAt &lt; DATEADD(day, -7, SYSUTCDATETIME());</code></pre></div><p>After the input set is defined, the per row loop can focus on the procedural action and progress tracking rather than searching for the next row.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;3869eaa7-51b2-40fa-aff3-bb80774c3e6a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">DECLARE @AccountId int;
DECLARE @NewStatus int;

DECLARE work_cursor CURSOR LOCAL FAST_FORWARD FOR
SELECT w.AccountId, w.NewStatus
FROM dbo.AccountStatusWork AS w
WHERE w.ProcessedAt IS NULL
ORDER BY w.AccountId;

OPEN work_cursor;

FETCH NEXT FROM work_cursor INTO @AccountId, @NewStatus;

WHILE @@FETCH_STATUS = 0
BEGIN
    UPDATE dbo.Account
    SET StatusId = @NewStatus,
        UpdatedAt = SYSUTCDATETIME()
    WHERE AccountId = @AccountId;

    UPDATE dbo.AccountStatusWork
    SET ProcessedAt = SYSUTCDATETIME()
    WHERE AccountId = @AccountId;

    FETCH NEXT FROM work_cursor INTO @AccountId, @NewStatus;
END</code></pre></div><p>That is still a cursor loop, but it is a cursor loop over a pre filtered set, which keeps the procedural part focused on the row by row requirement rather than doing set work repeatedly inside the loop.</p><h3>Cursor Lifecycle From Declare to Deallocate</h3><p>Row by row work through a cursor follows a lifecycle with defined stages, and each stage has a specific meaning. Declaration ties a name to a query and options. Opening activates the cursor so rows can be read. Fetching steps through rows in a controlled order. Closing releases the active result set. Deallocation releases cursor resources so the cursor name is no longer valid for further operations.</p><h4>Declaration Then Open</h4><p>At the <code>DECLARE</code> step, you define what the cursor will read and how it will behave. In SQL Server, <code>DECLARE CURSOR</code> binds the cursor name to a <code>SELECT</code> statement and cursor options. Options matter because they affect behavior and overhead. For row by row processing that only needs forward movement and no updates through the cursor, <code>FAST_FORWARD</code> is a common choice because it implies a <code>FORWARD_ONLY</code>, <code>READ_ONLY</code> cursor.</p><p>Code tends to read well when the cursor name reflects the row source and the variables match the projected columns:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;56d1f8f0-570f-43f3-a2eb-ebd62d18c259&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">DECLARE @WorkId int;
DECLARE @AccountId int;

DECLARE work_cursor CURSOR LOCAL FAST_FORWARD FOR
SELECT w.WorkId, w.AccountId
FROM dbo.AccountWorkQueue AS w
WHERE w.ProcessedAt IS NULL
ORDER BY w.WorkId;

OPEN work_cursor;</code></pre></div><p><code>OPEN</code> activates the cursor and positions it before the first row in the result set. Data does not come back at <code>OPEN</code> time. Rows arrive when you <code>FETCH</code>.</p><p>PostgreSQL has a similar separation. <code>DECLARE cursor_name CURSOR FOR</code> defines the cursor and its query, and cursor lifetime is tied to a transaction unless <code>WITH HOLD</code> is specified. That transaction tie is the reason you commonly start a transaction block before declaring the cursor, then keep the cursor work inside that same transaction scope.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;8838acc9-0eb2-42b6-849f-57201e80a8f9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">BEGIN;

DECLARE work_cursor NO SCROLL CURSOR FOR
SELECT work_id, account_id
FROM account_work_queue
WHERE processed_at IS NULL
ORDER BY work_id;</code></pre></div><p>Cursor options in PostgreSQL also matter. Forward only work fits well with <code>NO SCROLL</code>. If you need backward movement, PostgreSQL expects the cursor to be declared <code>SCROLL</code>. Relying on backward fetch behavior without <code>SCROLL</code> is not a good idea, and staying explicit keeps behavior consistent.</p><p>MySQL has a different constraint that shapes the lifecycle. MySQL cursor support is tied to stored programs, so cursor declaration and fetch loops live inside stored procedures or functions rather than ad hoc SQL.</p><h4>Fetch Loop Mechanics</h4><p>Row retrieval happens during <code>FETCH</code>. Each fetch copies the current row into variables and moves the cursor position forward based on the direction you requested. In SQL Server, <code>FETCH NEXT</code> is the common forward movement option. The first <code>FETCH NEXT</code> after <code>OPEN</code> returns the first row. Loop control usually checks <code>@@FETCH_STATUS</code> after each fetch, and a status of <code>0</code> means the fetch succeeded.</p><p>In practice, the loop pulls one row into variables, runs the per row work, then pulls the next row:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;333cdbcf-f10f-47c5-bf96-70fb2764b689&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">FETCH NEXT FROM work_cursor INTO @WorkId, @AccountId;

WHILE @@FETCH_STATUS = 0
BEGIN
    UPDATE dbo.AccountWorkQueue
    SET PickedAt = SYSUTCDATETIME()
    WHERE WorkId = @WorkId;

    EXEC dbo.ProcessAccountWork @AccountId = @AccountId;

    UPDATE dbo.AccountWorkQueue
    SET ProcessedAt = SYSUTCDATETIME()
    WHERE WorkId = @WorkId;

    FETCH NEXT FROM work_cursor INTO @WorkId, @AccountId;
END</code></pre></div><p>PostgreSQL fetch behavior is expressed through <code>FETCH</code> against the cursor name. In PL pgSQL, routines frequently rely on <code>FOUND</code> or <code>NOT FOUND</code> after a <code>FETCH</code> to decide when to exit a loop. PostgreSQL also supports <code>MOVE</code>, which is like <code>FETCH</code> in terms of cursor movement but does not return a row, so it is useful when you need to reposition the cursor without pulling data into variables.</p><p>Something with PostgreSQL to keep in mind is that PL pgSQL cursor docs mark bare count expressions without an explicit direction keyword as deprecated, so stick with forms like <code>FETCH NEXT</code> or <code>FETCH FORWARD n</code>.</p><h4>Close Deallocate Scope</h4><p>Closing and deallocation are separate steps, and treating them as separate steps keeps intent easy to follow.</p><p>SQL Server <code>CLOSE</code> releases the current result set associated with the cursor. The cursor still exists after <code>CLOSE</code>, and it can be opened again if needed. <code>DEALLOCATE</code> releases the cursor resources and removes the cursor definition, so reopening is no longer possible without a new <code>DECLARE CURSOR</code>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;ec04b849-5b72-429a-937c-f1503a80651a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CLOSE work_cursor;
DEALLOCATE work_cursor;</code></pre></div><p>Scope rules matter in SQL Server. <code>LOCAL</code> limits the cursor to the current scope. <code>GLOBAL</code> makes it visible beyond the current scope. Picking the smallest scope that fits reduces surprises when code is refactored into nested procedures.</p><p>PostgreSQL has a different scope anchor. Cursors typically live within the current transaction. <code>CLOSE</code> frees resources associated with an open cursor, and after closing, further operations on that cursor are not allowed. If the cursor is not declared <code>WITH HOLD</code>, committing the transaction ends the cursor lifetime as well.</p><h4>Error Handling Batch Work Without Bottlenecks</h4><p>Row by row routines fail in a different way than set based statements. Failures can happen on one item while the next item is fine, and that usually calls for per row logging and per row decisions about retry, skip, or stop.</p><p>SQL Server <code>TRY</code> and <code>CATCH</code> blocks make per row error capture easy. One common pattern logs the error message alongside the work item id, then continues to the next fetch. Progress tracking stays separate from the business work so you can see what was picked, what finished, and what failed.</p><p>One way to keep the flow readable is to update the work row in both branches so the queue table tells the full story:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;873e8763-d733-43da-a41e-e30b712b1309&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">DECLARE @WorkId int;
DECLARE @AccountId int;

FETCH NEXT FROM work_cursor INTO @WorkId, @AccountId;

WHILE @@FETCH_STATUS = 0
BEGIN
    BEGIN TRY
        EXEC dbo.ProcessAccountWork @AccountId = @AccountId;

        UPDATE dbo.AccountWorkQueue
        SET ProcessedAt = SYSUTCDATETIME(),
            ErrorText = NULL
        WHERE WorkId = @WorkId;
    END TRY
    BEGIN CATCH
        UPDATE dbo.AccountWorkQueue
        SET ProcessedAt = SYSUTCDATETIME(),
            ErrorText = ERROR_MESSAGE()
        WHERE WorkId = @WorkId;
    END CATCH;

    FETCH NEXT FROM work_cursor INTO @WorkId, @AccountId;
END</code></pre></div><p>Batching keeps cursor work from becoming a long running transaction that holds locks too long. Practical batch flow marks a limited number of rows as picked, processes only those rows, then commits and repeats. That also helps when a routine is interrupted, because the work table can track picked time, processed time, and error text.</p><p>PostgreSQL batch patterns commonly rely on a work table plus row locking so multiple workers do not pick the same items. <code>FOR UPDATE SKIP LOCKED</code> is frequently used to claim rows without waiting on locks, then the routine processes the claimed set. Cursor based processing still fits that style, but transaction boundaries matter more because the cursor and locks live inside the transaction.</p><p>Batching is also where cursor options and cursor scope connect back to resource usage. Smaller batches limit the active cursor window, reduce lock duration, and reduce the chance that one slow row blocks progress for everything behind it.</p><h3>Conclusion</h3><p>Cursors add a procedural layer on top of SQL&#8217;s set based engine, so the mechanics matter. Start with a query that defines the input rows, then walk through the lifecycle in order <code>DECLARE</code> to bind the cursor to that query, <code>OPEN</code> to activate it, <code>FETCH</code> to pull rows into variables while moving the cursor forward, <code>CLOSE</code> to release the active result set, and <code>DEALLOCATE</code> to release cursor resources. Keeping the set producing query tight, keeping the per row work focused, and batching when needed keeps the cursor stage predictable while the database still does the heavy lifting where it performs best.</p><ol><li><p><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/language-elements/declare-cursor-transact-sql?view=sql-server-ver17">SQL Server </a></em><code>DECLARE CURSOR</code><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/language-elements/declare-cursor-transact-sql?view=sql-server-ver17"> Transact SQL</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/language-elements/fetch-transact-sql?view=sql-server-ver17">SQL Server </a></em><code>FETCH</code><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/language-elements/fetch-transact-sql?view=sql-server-ver17"> Transact SQL</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/language-elements/close-transact-sql?view=sql-server-ver17">SQL Server </a></em><code>CLOSE</code><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/language-elements/close-transact-sql?view=sql-server-ver17"> Cursor Transact SQL</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/language-elements/deallocate-transact-sql?view=sql-server-ver17">SQL Server </a></em><code>DEALLOCATE</code><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/language-elements/deallocate-transact-sql?view=sql-server-ver17"> Cursor Transact SQL</a></em></p></li><li><p><em><a href="https://www.postgresql.org/docs/current/sql-declare.html">PostgreSQL </a></em><code>DECLARE</code></p></li><li><p><em><a href="https://www.postgresql.org/docs/current/sql-fetch.html">PostgreSQL </a></em><code>FETCH</code></p></li><li><p><em><a href="https://www.postgresql.org/docs/current/sql-close.html">PostgreSQL </a></em><code>CLOSE</code></p></li><li><p><em><a href="https://www.postgresql.org/docs/current/plpgsql-cursors.html">PL pgSQL Cursors</a></em></p></li><li><p><em><a href="https://dev.mysql.com/doc/refman/8.4/en/cursors.html">MySQL Cursors</a></em></p></li></ol><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share Alexander Obregon's Substack&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share Alexander Obregon's Substack</span></a></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nVXc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7508b660-0148-4efb-a882-79901c32c5e4_306x306.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nVXc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7508b660-0148-4efb-a882-79901c32c5e4_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!nVXc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7508b660-0148-4efb-a882-79901c32c5e4_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!nVXc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7508b660-0148-4efb-a882-79901c32c5e4_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!nVXc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7508b660-0148-4efb-a882-79901c32c5e4_306x306.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nVXc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7508b660-0148-4efb-a882-79901c32c5e4_306x306.png" width="306" height="306" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7508b660-0148-4efb-a882-79901c32c5e4_306x306.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:306,&quot;width&quot;:306,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!nVXc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7508b660-0148-4efb-a882-79901c32c5e4_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!nVXc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7508b660-0148-4efb-a882-79901c32c5e4_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!nVXc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7508b660-0148-4efb-a882-79901c32c5e4_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!nVXc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7508b660-0148-4efb-a882-79901c32c5e4_306x306.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://freesvg.org/mono-sql">Image Source</a></figcaption></figure></div>]]></content:encoded></item><item><title><![CDATA[Parameterized Queries in SQL for Safer Apps]]></title><description><![CDATA[Sending SQL as a template and the user values as separate inputs lets the database treat the SQL as code while keeping the values as data.]]></description><link>https://alexanderobregon.substack.com/p/parameterized-queries-in-sql-for</link><guid isPermaLink="false">https://alexanderobregon.substack.com/p/parameterized-queries-in-sql-for</guid><dc:creator><![CDATA[Alexander Obregon]]></dc:creator><pubDate>Fri, 27 Feb 2026 18:34:15 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!N7YU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b4a4ff7-8d1e-4b9a-8dd2-6d894180aa57_306x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vUnp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc39c7388-9a6f-481c-a5ee-4c6d43c8be3c_800x373.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vUnp!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc39c7388-9a6f-481c-a5ee-4c6d43c8be3c_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!vUnp!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc39c7388-9a6f-481c-a5ee-4c6d43c8be3c_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!vUnp!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc39c7388-9a6f-481c-a5ee-4c6d43c8be3c_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!vUnp!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc39c7388-9a6f-481c-a5ee-4c6d43c8be3c_800x373.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vUnp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc39c7388-9a6f-481c-a5ee-4c6d43c8be3c_800x373.png" width="800" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c39c7388-9a6f-481c-a5ee-4c6d43c8be3c_800x373.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vUnp!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc39c7388-9a6f-481c-a5ee-4c6d43c8be3c_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!vUnp!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc39c7388-9a6f-481c-a5ee-4c6d43c8be3c_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!vUnp!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc39c7388-9a6f-481c-a5ee-4c6d43c8be3c_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!vUnp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc39c7388-9a6f-481c-a5ee-4c6d43c8be3c_800x373.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://commons.wikimedia.org/wiki/File:Sql_data_base_with_logo.png">Image Source</a></figcaption></figure></div><p>Sending SQL as a template and the user values as separate inputs lets the database treat the SQL as code while keeping the values as data. That division changes the whole trip from client to server because the driver sends structured parameter values instead of pasting text into a string, then the database parses and compiles the statement without letting those values alter the grammar. SQL injection attempts fall apart for the same reason, and plan caching behavior shifts too, which matters in engines like SQL Server and PostgreSQL.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>Parameter Binding Mechanics From Client Code</h3><p>Most application database calls follow the same basic plan. You write one SQL statement that stays stable over time, then you pass values separately at runtime. The database receives both pieces in a structured form and connects them at execution time, which changes what the server parses, what the driver sends, and what you can safely allow user input to do.</p><h4>Placeholders and Typed Values</h4><p>Different databases and drivers spell placeholders differently, but the intent is consistent. SQL Server commonly uses named parameters like <code>@customerId</code> inside the SQL text. PostgreSQL&#8217;s server protocol uses positional parameters like <code>$1</code>, <code>$2</code>. Client libraries can expose different placeholder spellings in application code, like psycopg&#8217;s <code>%s</code> or node-postgres <code>$1</code>, but the binding is still positional and the values still travel separately from the SQL text.</p><p>Typed values matter more than people expect at first. The driver is not just shipping a string of characters. It&#8217;s sending a value along with a type, or at least a strong hint about the type. That type affects how the database parses the statement, how it checks conversions, and how it compares values inside operators and predicates.</p><p>With SQL Server client code, you&#8217;ll usually see a parameter created with a declared <code>SqlDbType</code>. It changes what goes over the connection and how SQL Server reads it.</p>
      <p>
          <a href="https://alexanderobregon.substack.com/p/parameterized-queries-in-sql-for">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Triggers in SQL for Auditing Data Changes]]></title><description><![CDATA[Sooner or later, every database ends up answering the same set of questions after data changes.]]></description><link>https://alexanderobregon.substack.com/p/triggers-in-sql-for-auditing-data</link><guid isPermaLink="false">https://alexanderobregon.substack.com/p/triggers-in-sql-for-auditing-data</guid><dc:creator><![CDATA[Alexander Obregon]]></dc:creator><pubDate>Tue, 24 Feb 2026 18:35:22 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!HQc2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66f4584d-5f82-4443-b57b-1c57a553a5c1_306x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3ck7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5130a571-6371-4564-9bf1-7dbbd1359479_800x373.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3ck7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5130a571-6371-4564-9bf1-7dbbd1359479_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!3ck7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5130a571-6371-4564-9bf1-7dbbd1359479_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!3ck7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5130a571-6371-4564-9bf1-7dbbd1359479_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!3ck7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5130a571-6371-4564-9bf1-7dbbd1359479_800x373.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3ck7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5130a571-6371-4564-9bf1-7dbbd1359479_800x373.png" width="800" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5130a571-6371-4564-9bf1-7dbbd1359479_800x373.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!3ck7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5130a571-6371-4564-9bf1-7dbbd1359479_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!3ck7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5130a571-6371-4564-9bf1-7dbbd1359479_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!3ck7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5130a571-6371-4564-9bf1-7dbbd1359479_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!3ck7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5130a571-6371-4564-9bf1-7dbbd1359479_800x373.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://commons.wikimedia.org/wiki/File:Sql_data_base_with_logo.png">Image Source</a></figcaption></figure></div><p>Sooner or later, every database ends up answering the same set of questions after data changes. Which row changed, who made the change, and what the row looked like right before it happened. Triggers give you a place inside the database to capture that record at write time, right when an insert, update, or delete runs, so the audit data stays tied to the same transaction as the change. Trigger based auditing then comes down to writing the old values, new values, a timestamp, and whatever user identity the engine can expose into an audit table you can query later.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>How Trigger Auditing Works</h3><p>Trigger auditing runs alongside the write path inside the database, so the work happens at the same moment the change happens. When a row change statement runs, the engine builds the before and after row images it already needs for the operation, and trigger code can read those images and write a matching record into a separate table.</p><h4>Trigger Timing and Row Images</h4><p>Timing decides when trigger code runs relative to the row change. Many engines support BEFORE and AFTER timing, and the choice affects what trigger code can safely assume about the final state. AFTER timing is a common fit for auditing because the engine has already accepted the change when the trigger body runs, so the audit insert records what the table ended up with. BEFORE timing still has a place, but it can invite extra logic that starts to look like business rules rather than audit capture. Row images are the other half of the picture. Auditing needs access to values from the row before the change and values after the change, and each database surfaces that data in its own way.</p><p>PostgreSQL row level triggers expose special record values named <code>OLD</code> and <code>NEW</code>, and the trigger function can also read variables like <code>TG_OP</code> and <code>TG_TABLE_NAME</code> to learn what fired it.</p><p>MySQL triggers also expose <code>OLD</code> and <code>NEW</code> inside the trigger body, where <code>OLD.col_name</code> refers to the prior row value for updates and deletes, and <code>NEW.col_name</code> refers to the inserted or updated row value.</p><p>SQL Server exposes row images through two logical tables inside the trigger, <code>inserted</code> and <code>deleted</code>. An <code>INSERT</code> statement places rows in <code>inserted</code>, a <code>DELETE</code> statement places rows in <code>deleted</code>, and an <code>UPDATE</code> statement places old rows in <code>deleted</code> and new rows in <code>inserted</code>.</p><p>Let&#8217;s see how trigger timing changes what the database hands your trigger during an update, and how <code>OLD</code> and <code>NEW</code> show up inside a PostgreSQL trigger function:</p>
      <p>
          <a href="https://alexanderobregon.substack.com/p/triggers-in-sql-for-auditing-data">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[SQL Transactions for Multi Step Updates]]></title><description><![CDATA[Almost all database write work takes more than one statement.]]></description><link>https://alexanderobregon.substack.com/p/sql-transactions-for-multi-step-updates</link><guid isPermaLink="false">https://alexanderobregon.substack.com/p/sql-transactions-for-multi-step-updates</guid><dc:creator><![CDATA[Alexander Obregon]]></dc:creator><pubDate>Mon, 23 Feb 2026 18:37:18 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!gwRq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe71c287d-565c-47d7-b1b7-48db5750ccbb_306x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!jKYD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64f23023-8fbc-4571-9fe5-309feab10b47_800x373.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!jKYD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64f23023-8fbc-4571-9fe5-309feab10b47_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!jKYD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64f23023-8fbc-4571-9fe5-309feab10b47_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!jKYD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64f23023-8fbc-4571-9fe5-309feab10b47_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!jKYD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64f23023-8fbc-4571-9fe5-309feab10b47_800x373.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!jKYD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64f23023-8fbc-4571-9fe5-309feab10b47_800x373.png" width="800" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/64f23023-8fbc-4571-9fe5-309feab10b47_800x373.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!jKYD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64f23023-8fbc-4571-9fe5-309feab10b47_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!jKYD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64f23023-8fbc-4571-9fe5-309feab10b47_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!jKYD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64f23023-8fbc-4571-9fe5-309feab10b47_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!jKYD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64f23023-8fbc-4571-9fe5-309feab10b47_800x373.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://commons.wikimedia.org/wiki/File:Sql_data_base_with_logo.png">Image Source</a></figcaption></figure></div><p>Almost all database write work takes more than one statement. Features usually touch several rows, and sometimes more than one table, so the changes need to land as one unit or not land at all. Transactions handle that. You wrap a group of writes, then either <code>COMMIT</code> to make them permanent or <code>ROLLBACK</code> to undo the row changes, though some databases still consume generated id or sequence values even if you roll back. That&#8217;s what keeps a money transfer from ending with money removed from one account but never added to the other after a mid way failure.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>Transaction Basics</h3><p>Multi step writes need a boundary that treats several statements as one unit. That boundary is the transaction. Inside it, your session can stage changes, read them back, and decide to make them permanent or throw them away. Getting comfortable with that mental model makes the rest of the topic feel far less slippery.</p><h4>What a Transaction Really Means</h4><p>Think of a transaction as a temporary workspace tied to your current database session. Statements run the same way they normally do, but their effects stay private to your session until you decide what to do at the end. You can run several <code>INSERT</code>, <code>UPDATE</code>, and <code>DELETE</code> statements, then finish with <code>COMMIT</code> to publish the result. If something goes wrong, <code>ROLLBACK</code> throws away everything done after the transaction began.</p><p>One practical detail can help a lot early on is that your session can see its own uncommitted changes, and other sessions usually cannot unless they are running a mode that allows dirty reads. So after an <code>UPDATE</code>, a <code>SELECT</code> in the same transaction will usually read back the new value, even though nobody else can see it yet. Transactions also provide an all or nothing result. If step three fails, steps one and two do not get left behind when you roll back.</p><p>Let&#8217;s say we are working with two sessions. Session 1 starts a transaction and updates a row. Session 2 reads at the same time. Session 2 keeps seeing the older committed value until Session 1 commits.</p><p>Run this in Session 1:</p>
      <p>
          <a href="https://alexanderobregon.substack.com/p/sql-transactions-for-multi-step-updates">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Merge Statement in SQL for Upsert Jobs]]></title><description><![CDATA[Upsert work comes up when changes keep arriving and you need one table to stay current.]]></description><link>https://alexanderobregon.substack.com/p/merge-statement-in-sql-for-upsert</link><guid isPermaLink="false">https://alexanderobregon.substack.com/p/merge-statement-in-sql-for-upsert</guid><dc:creator><![CDATA[Alexander Obregon]]></dc:creator><pubDate>Sat, 21 Feb 2026 16:06:18 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!3Ndl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26227d93-3601-4502-8b12-66bbc2021217_306x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lJcv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ba961e9-80fc-46ff-bb8a-b9b8c041f291_800x373.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lJcv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ba961e9-80fc-46ff-bb8a-b9b8c041f291_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!lJcv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ba961e9-80fc-46ff-bb8a-b9b8c041f291_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!lJcv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ba961e9-80fc-46ff-bb8a-b9b8c041f291_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!lJcv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ba961e9-80fc-46ff-bb8a-b9b8c041f291_800x373.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lJcv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ba961e9-80fc-46ff-bb8a-b9b8c041f291_800x373.png" width="800" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2ba961e9-80fc-46ff-bb8a-b9b8c041f291_800x373.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!lJcv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ba961e9-80fc-46ff-bb8a-b9b8c041f291_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!lJcv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ba961e9-80fc-46ff-bb8a-b9b8c041f291_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!lJcv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ba961e9-80fc-46ff-bb8a-b9b8c041f291_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!lJcv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ba961e9-80fc-46ff-bb8a-b9b8c041f291_800x373.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://commons.wikimedia.org/wiki/File:Sql_data_base_with_logo.png">Image Source</a></figcaption></figure></div><p>Upsert work comes up when changes keep arriving and you need one table to stay current. That could be a nightly file load, a CDC feed, or a staging table you refresh each run. SQL&#8217;s <code>MERGE</code> statement does that job well because you write the match rule one time, then outline what to do for rows that match and rows that don&#8217;t.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>How the T SQL MERGE Statement Syncs a Target</h3><p><code>MERGE</code> gives you one place to express how rows line up between a source set and a target table, then what to do with matches and non matches. It reads like a data sync plan, but it still runs as a join driven DML operation, so the match rule and the source set matter a lot.</p><h4>Join Shape for Matching Rows</h4><p>Thinking of <code>MERGE</code> as a join helps. SQL Server builds a row set out of the target and the source by applying the <code>ON</code> predicate, then it decides which action to take for each source row based on match status. The <code>ON</code> predicate is the definition of what counts as the same business row, so it should line up with a <code>PRIMARY KEY</code> or <code>UNIQUE</code> constraint on the target whenever that is possible. When the match rule is too broad, one target row can end up paired with more than one source row. When the source contains duplicates for the same identifier, the source can try to update or insert the same logical row more than once in a single statement, and <code>MERGE</code> can fail or behave in ways that are hard to reason about during a load review.</p><p>Before writing <code>MERGE</code>, it helps to sanity check the source set in the same way you would check input to a bulk load. Look for duplicates on the identifier you plan to match on, then decide how to collapse them. In staging pipelines, the most common rule is to keep the newest row per identifier based on a timestamp or a sequence value. That gives the <code>ON</code> predicate a stable match on one row per identity and keeps the update path deterministic.</p><p>One common collapse method is a <code>ROW_NUMBER</code> filter that keeps the newest row per business identifier.</p>
      <p>
          <a href="https://alexanderobregon.substack.com/p/merge-statement-in-sql-for-upsert">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Stored Procedures in SQL for Business Logic]]></title><description><![CDATA[Database servers still rely on stored procedures to group SQL statements, keep business rules close to the data, and present stable entry points to application code in the database catalog that accept parameters, run one or more statements, and return result sets or single value outputs.]]></description><link>https://alexanderobregon.substack.com/p/stored-procedures-in-sql-for-business</link><guid isPermaLink="false">https://alexanderobregon.substack.com/p/stored-procedures-in-sql-for-business</guid><dc:creator><![CDATA[Alexander Obregon]]></dc:creator><pubDate>Sun, 15 Feb 2026 18:08:23 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!mhLf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5da5d7c0-637b-4306-8bb5-29b54c8baf10_306x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qUlT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99b482bf-e873-4360-bad1-9aa756f3436e_800x373.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qUlT!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99b482bf-e873-4360-bad1-9aa756f3436e_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!qUlT!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99b482bf-e873-4360-bad1-9aa756f3436e_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!qUlT!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99b482bf-e873-4360-bad1-9aa756f3436e_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!qUlT!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99b482bf-e873-4360-bad1-9aa756f3436e_800x373.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qUlT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99b482bf-e873-4360-bad1-9aa756f3436e_800x373.png" width="800" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/99b482bf-e873-4360-bad1-9aa756f3436e_800x373.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!qUlT!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99b482bf-e873-4360-bad1-9aa756f3436e_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!qUlT!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99b482bf-e873-4360-bad1-9aa756f3436e_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!qUlT!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99b482bf-e873-4360-bad1-9aa756f3436e_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!qUlT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99b482bf-e873-4360-bad1-9aa756f3436e_800x373.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://commons.wikimedia.org/wiki/File:Sql_data_base_with_logo.png">Image Source</a></figcaption></figure></div><p>Database servers still rely on stored procedures to group SQL statements, keep business rules close to the data, and present stable entry points to application code in the database catalog that accept parameters, run one or more statements, and return result sets or single value outputs. This arrangement keeps repeated queries in one place, supports performance for large batches, and lets database developers treat procedures as a data API between application code and the physical tables.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>What Stored Procedures Do In Databases</h3><p>Stored procedures give a database its own library of reusable queries and data operations. They live next to the tables and indexes, have names, accept parameters, and can return rows or single values. Instead of sending a long SQL string from the application every time, a client can call a stored procedure by name and hand in the values that change, while the database keeps the actual logic in one place. That setup keeps query text consistent, helps with plan reuse, and gives database staff a stable place to express business rules that must live close to the data.</p><p>Relational engines treat stored procedures as objects in the catalog. SQL Server places them in schemas such as <code>dbo</code>, PostgreSQL tracks them in its system catalogs, MySQL stores them in its data dictionary, and Oracle manages them in its data dictionary views. The general flow is similar across engines. A procedure is created with a DDL statement, compiled into an internal representation, and then called many times with different arguments. The caller does not need to send the full text again, only the procedure name and parameter values.</p><p>Stored procedures can do far more than a single <code>SELECT</code>. They can chain multiple statements, such as an insert followed by an update, and they can branch on conditions with <code>IF</code> or <code>CASE</code>. Many teams use them for write heavy operations that must run in a controlled sequence, for read heavy logic that performs several related queries, and for routines that sit at the boundary between an application and its core data.</p><h4>Stored Procedure Definition</h4><p>Definitions start by giving the procedure a name and a parameter list, then a block of statements that run when the name is called. Many real databases use schemas to group procedures that relate to a given area of the application, such as <code>billing</code> or <code>inventory</code>, which helps keep long catalogs organized.</p><p>Here is a stored procedure in SQL Server that returns recent orders for a single customer:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-juy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45b22197-c290-4164-a5cc-34c50c580190_1855x837.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-juy!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45b22197-c290-4164-a5cc-34c50c580190_1855x837.png 424w, https://substackcdn.com/image/fetch/$s_!-juy!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45b22197-c290-4164-a5cc-34c50c580190_1855x837.png 848w, https://substackcdn.com/image/fetch/$s_!-juy!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45b22197-c290-4164-a5cc-34c50c580190_1855x837.png 1272w, https://substackcdn.com/image/fetch/$s_!-juy!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45b22197-c290-4164-a5cc-34c50c580190_1855x837.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-juy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45b22197-c290-4164-a5cc-34c50c580190_1855x837.png" width="1456" height="657" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/45b22197-c290-4164-a5cc-34c50c580190_1855x837.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:657,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:155735,&quot;alt&quot;:&quot;CREATE PROCEDURE dbo.GetRecentOrders     @CustomerId INT,     @DaysBack   INT AS BEGIN     SET NOCOUNT ON;      SELECT o.OrderId,            o.OrderDate,            o.TotalAmount     FROM   dbo.Orders AS o     WHERE  o.CustomerId = @CustomerId       AND  o.OrderDate >= DATEADD(DAY, -@DaysBack, SYSDATETIME())     ORDER BY o.OrderDate DESC; END;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187144501?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45b22197-c290-4164-a5cc-34c50c580190_1855x837.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE PROCEDURE dbo.GetRecentOrders     @CustomerId INT,     @DaysBack   INT AS BEGIN     SET NOCOUNT ON;      SELECT o.OrderId,            o.OrderDate,            o.TotalAmount     FROM   dbo.Orders AS o     WHERE  o.CustomerId = @CustomerId       AND  o.OrderDate >= DATEADD(DAY, -@DaysBack, SYSDATETIME())     ORDER BY o.OrderDate DESC; END;" title="CREATE PROCEDURE dbo.GetRecentOrders     @CustomerId INT,     @DaysBack   INT AS BEGIN     SET NOCOUNT ON;      SELECT o.OrderId,            o.OrderDate,            o.TotalAmount     FROM   dbo.Orders AS o     WHERE  o.CustomerId = @CustomerId       AND  o.OrderDate >= DATEADD(DAY, -@DaysBack, SYSDATETIME())     ORDER BY o.OrderDate DESC; END;" srcset="https://substackcdn.com/image/fetch/$s_!-juy!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45b22197-c290-4164-a5cc-34c50c580190_1855x837.png 424w, https://substackcdn.com/image/fetch/$s_!-juy!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45b22197-c290-4164-a5cc-34c50c580190_1855x837.png 848w, https://substackcdn.com/image/fetch/$s_!-juy!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45b22197-c290-4164-a5cc-34c50c580190_1855x837.png 1272w, https://substackcdn.com/image/fetch/$s_!-juy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45b22197-c290-4164-a5cc-34c50c580190_1855x837.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This definition registers <code>dbo.GetRecentOrders</code> as a stored procedure in the database. The two parameters describe the values the caller must supply. <code>SET NOCOUNT ON</code> asks SQL Server not to send row count messages after each statement, which keeps result traffic focused on the data. The <code>SELECT</code> filters rows based on those parameters and returns the columns that callers care about. When client code needs recent orders, it can call <code>dbo.GetRecentOrders</code> instead of embedding the full <code>SELECT</code> each time.</p><p>Executions of this procedure reuse the same compiled plan in SQL Server unless something important changes, such as table statistics or certain schema details. Plan reuse helps with performance because the server does not reparse and replan the statement text on every call. Parameter values still influence row estimates and index paths, but the overall skeleton of the query remains stable across calls.</p><p>PostgreSQL supports functions that return result sets, which many teams use the same way they use stored procedures in SQL Server:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!A7Tb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca9ec069-ac7a-492e-9e85-f5b47f48b3d9_1861x795.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!A7Tb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca9ec069-ac7a-492e-9e85-f5b47f48b3d9_1861x795.png 424w, https://substackcdn.com/image/fetch/$s_!A7Tb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca9ec069-ac7a-492e-9e85-f5b47f48b3d9_1861x795.png 848w, https://substackcdn.com/image/fetch/$s_!A7Tb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca9ec069-ac7a-492e-9e85-f5b47f48b3d9_1861x795.png 1272w, https://substackcdn.com/image/fetch/$s_!A7Tb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca9ec069-ac7a-492e-9e85-f5b47f48b3d9_1861x795.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!A7Tb!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca9ec069-ac7a-492e-9e85-f5b47f48b3d9_1861x795.png" width="948" height="404.9835164835165" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ca9ec069-ac7a-492e-9e85-f5b47f48b3d9_1861x795.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:622,&quot;width&quot;:1456,&quot;resizeWidth&quot;:948,&quot;bytes&quot;:137699,&quot;alt&quot;:&quot;CREATE OR REPLACE FUNCTION get_recent_orders(     p_customer_id INT,     p_days_back   INT ) RETURNS TABLE(     order_id     INT,     order_date   TIMESTAMPTZ,     total_amount NUMERIC ) LANGUAGE sql AS $$     SELECT o.order_id,            o.order_date,            o.total_amount     FROM   orders AS o     WHERE  o.customer_id = p_customer_id       AND  o.order_date >= NOW() - (p_days_back || ' days')::INTERVAL     ORDER BY o.order_date DESC; $$;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187144501?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca9ec069-ac7a-492e-9e85-f5b47f48b3d9_1861x795.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="CREATE OR REPLACE FUNCTION get_recent_orders(     p_customer_id INT,     p_days_back   INT ) RETURNS TABLE(     order_id     INT,     order_date   TIMESTAMPTZ,     total_amount NUMERIC ) LANGUAGE sql AS $$     SELECT o.order_id,            o.order_date,            o.total_amount     FROM   orders AS o     WHERE  o.customer_id = p_customer_id       AND  o.order_date >= NOW() - (p_days_back || ' days')::INTERVAL     ORDER BY o.order_date DESC; $$;" title="CREATE OR REPLACE FUNCTION get_recent_orders(     p_customer_id INT,     p_days_back   INT ) RETURNS TABLE(     order_id     INT,     order_date   TIMESTAMPTZ,     total_amount NUMERIC ) LANGUAGE sql AS $$     SELECT o.order_id,            o.order_date,            o.total_amount     FROM   orders AS o     WHERE  o.customer_id = p_customer_id       AND  o.order_date >= NOW() - (p_days_back || ' days')::INTERVAL     ORDER BY o.order_date DESC; $$;" srcset="https://substackcdn.com/image/fetch/$s_!A7Tb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca9ec069-ac7a-492e-9e85-f5b47f48b3d9_1861x795.png 424w, https://substackcdn.com/image/fetch/$s_!A7Tb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca9ec069-ac7a-492e-9e85-f5b47f48b3d9_1861x795.png 848w, https://substackcdn.com/image/fetch/$s_!A7Tb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca9ec069-ac7a-492e-9e85-f5b47f48b3d9_1861x795.png 1272w, https://substackcdn.com/image/fetch/$s_!A7Tb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fca9ec069-ac7a-492e-9e85-f5b47f48b3d9_1861x795.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And for this function, it lives in the database catalog and can be called from SQL with <code>SELECT * FROM get_recent_orders(7, 30);</code>. The <code>RETURNS TABLE</code> clause tells PostgreSQL what columns to expect, which supports result type checking for callers. For <code>LANGUAGE sql</code> functions, the planner can inline the function into the calling query when it is safe to do so, so planning happens as part of the caller. For <code>plpgsql</code>, SQL statements written directly in the function body are planned and cached for reuse, and they get replanned when invalidated. Dynamic SQL run with <code>EXECUTE</code> is planned each time it runs.</p><p>MySQL uses a slightly different syntax, but the idea is the same. Client tools usually change the statement delimiter so the body can contain semicolons without ending the entire <code>CREATE PROCEDURE</code> command:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nAf2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01607294-4bf9-4bf0-a9a3-a629c1691b93_1856x561.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nAf2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01607294-4bf9-4bf0-a9a3-a629c1691b93_1856x561.png 424w, https://substackcdn.com/image/fetch/$s_!nAf2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01607294-4bf9-4bf0-a9a3-a629c1691b93_1856x561.png 848w, https://substackcdn.com/image/fetch/$s_!nAf2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01607294-4bf9-4bf0-a9a3-a629c1691b93_1856x561.png 1272w, https://substackcdn.com/image/fetch/$s_!nAf2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01607294-4bf9-4bf0-a9a3-a629c1691b93_1856x561.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nAf2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01607294-4bf9-4bf0-a9a3-a629c1691b93_1856x561.png" width="1456" height="440" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/01607294-4bf9-4bf0-a9a3-a629c1691b93_1856x561.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:440,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:83328,&quot;alt&quot;:&quot;DELIMITER //  CREATE PROCEDURE GetCustomerBalance(IN p_customer_id INT) BEGIN     SELECT SUM(amount) AS balance     FROM   payments     WHERE  customer_id = p_customer_id; END//  DELIMITER ;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187144501?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01607294-4bf9-4bf0-a9a3-a629c1691b93_1856x561.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="DELIMITER //  CREATE PROCEDURE GetCustomerBalance(IN p_customer_id INT) BEGIN     SELECT SUM(amount) AS balance     FROM   payments     WHERE  customer_id = p_customer_id; END//  DELIMITER ;" title="DELIMITER //  CREATE PROCEDURE GetCustomerBalance(IN p_customer_id INT) BEGIN     SELECT SUM(amount) AS balance     FROM   payments     WHERE  customer_id = p_customer_id; END//  DELIMITER ;" srcset="https://substackcdn.com/image/fetch/$s_!nAf2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01607294-4bf9-4bf0-a9a3-a629c1691b93_1856x561.png 424w, https://substackcdn.com/image/fetch/$s_!nAf2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01607294-4bf9-4bf0-a9a3-a629c1691b93_1856x561.png 848w, https://substackcdn.com/image/fetch/$s_!nAf2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01607294-4bf9-4bf0-a9a3-a629c1691b93_1856x561.png 1272w, https://substackcdn.com/image/fetch/$s_!nAf2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01607294-4bf9-4bf0-a9a3-a629c1691b93_1856x561.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This definition gives MySQL a procedure named <code>GetCustomerBalance</code> that accepts one parameter and returns a single row with a <code>balance</code> column. The database stores this definition and makes it callable through <code>CALL GetCustomerBalance(7);</code>. That call reads more like a function call than a raw query, which suits code that wants a named operation rather than a text string.</p><p>Across different engines, stored procedure definitions usually support control flow as well. It is common to see <code>IF</code> branches that choose among several queries, loops that walk through cursor results when set based statements are not practical, and <code>RETURN</code> or <code>RAISE</code> statements that exit with status codes. Those features make procedures closer to small programs that live inside the database, rather than single ad hoc queries.</p><h4>Parameters With Output Values</h4><p>Business logic rarely stops at a fixed query. Procedures tend to accept parameters to filter data, control date ranges, or select among modes of operation. Input parameters act as placeholders in the stored code and receive their values from the caller. That arrangement lets the database reuse one definition while still handling many different cases.</p><p>Output parameters provide a second channel for returning values beside result sets. A procedure can stream rows in a normal result set and still set one or more output parameters for totals, counts, or status codes. Callers then read those output values the same way they read any other parameter, without parsing a special result row.</p><p>Here is a SQL Server procedure that accepts a customer id as input and exposes the total order amount as an output value:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!uKVC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3939ce0-a3f8-4e85-b3f4-6ca14698ccad_1846x800.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!uKVC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3939ce0-a3f8-4e85-b3f4-6ca14698ccad_1846x800.png 424w, https://substackcdn.com/image/fetch/$s_!uKVC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3939ce0-a3f8-4e85-b3f4-6ca14698ccad_1846x800.png 848w, https://substackcdn.com/image/fetch/$s_!uKVC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3939ce0-a3f8-4e85-b3f4-6ca14698ccad_1846x800.png 1272w, https://substackcdn.com/image/fetch/$s_!uKVC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3939ce0-a3f8-4e85-b3f4-6ca14698ccad_1846x800.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!uKVC!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3939ce0-a3f8-4e85-b3f4-6ca14698ccad_1846x800.png" width="916" height="396.97527472527474" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b3939ce0-a3f8-4e85-b3f4-6ca14698ccad_1846x800.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:631,&quot;width&quot;:1456,&quot;resizeWidth&quot;:916,&quot;bytes&quot;:137062,&quot;alt&quot;:&quot;CREATE PROCEDURE dbo.GetCustomerOrderSummary     @CustomerId   INT,     @TotalAmount  DECIMAL(18,2) OUTPUT AS BEGIN     SET NOCOUNT ON;      SELECT o.OrderId,            o.OrderDate,            o.TotalAmount     FROM   dbo.Orders AS o     WHERE  o.CustomerId = @CustomerId     ORDER BY o.OrderDate DESC;      SELECT @TotalAmount =         SUM(o.TotalAmount)     FROM dbo.Orders AS o     WHERE o.CustomerId = @CustomerId; END;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187144501?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3939ce0-a3f8-4e85-b3f4-6ca14698ccad_1846x800.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="CREATE PROCEDURE dbo.GetCustomerOrderSummary     @CustomerId   INT,     @TotalAmount  DECIMAL(18,2) OUTPUT AS BEGIN     SET NOCOUNT ON;      SELECT o.OrderId,            o.OrderDate,            o.TotalAmount     FROM   dbo.Orders AS o     WHERE  o.CustomerId = @CustomerId     ORDER BY o.OrderDate DESC;      SELECT @TotalAmount =         SUM(o.TotalAmount)     FROM dbo.Orders AS o     WHERE o.CustomerId = @CustomerId; END;" title="CREATE PROCEDURE dbo.GetCustomerOrderSummary     @CustomerId   INT,     @TotalAmount  DECIMAL(18,2) OUTPUT AS BEGIN     SET NOCOUNT ON;      SELECT o.OrderId,            o.OrderDate,            o.TotalAmount     FROM   dbo.Orders AS o     WHERE  o.CustomerId = @CustomerId     ORDER BY o.OrderDate DESC;      SELECT @TotalAmount =         SUM(o.TotalAmount)     FROM dbo.Orders AS o     WHERE o.CustomerId = @CustomerId; END;" srcset="https://substackcdn.com/image/fetch/$s_!uKVC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3939ce0-a3f8-4e85-b3f4-6ca14698ccad_1846x800.png 424w, https://substackcdn.com/image/fetch/$s_!uKVC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3939ce0-a3f8-4e85-b3f4-6ca14698ccad_1846x800.png 848w, https://substackcdn.com/image/fetch/$s_!uKVC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3939ce0-a3f8-4e85-b3f4-6ca14698ccad_1846x800.png 1272w, https://substackcdn.com/image/fetch/$s_!uKVC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3939ce0-a3f8-4e85-b3f4-6ca14698ccad_1846x800.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The first <code>SELECT</code> returns detailed rows to the caller, one per order. The second query computes the total and assigns it into <code>@TotalAmount</code>, which then leaves the procedure as an output parameter. Client code receives two pieces of information in one call, both the list and the aggregate number. This pattern avoids an extra round trip just to compute the total.</p><p>Return codes often sit next to output parameters in SQL Server. Many teams add an <code>INT</code> return value that represents success or specific error conditions, while still raising errors for severe problems. That return value can be used in calling T-SQL or in client code to make decisions about retries or alternate flows.</p><p>PostgreSQL procedures and functions can also use input and output parameters. Functions that use <code>OUT</code> parameters return those values as columns in the result. This example computes a count and a total amount for a customer in a single call:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cZNT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4f4c40c-33dd-4bc4-995e-3891b10aa9d4_1870x587.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cZNT!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4f4c40c-33dd-4bc4-995e-3891b10aa9d4_1870x587.png 424w, https://substackcdn.com/image/fetch/$s_!cZNT!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4f4c40c-33dd-4bc4-995e-3891b10aa9d4_1870x587.png 848w, https://substackcdn.com/image/fetch/$s_!cZNT!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4f4c40c-33dd-4bc4-995e-3891b10aa9d4_1870x587.png 1272w, https://substackcdn.com/image/fetch/$s_!cZNT!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4f4c40c-33dd-4bc4-995e-3891b10aa9d4_1870x587.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cZNT!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4f4c40c-33dd-4bc4-995e-3891b10aa9d4_1870x587.png" width="1002" height="314.5013736263736" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e4f4c40c-33dd-4bc4-995e-3891b10aa9d4_1870x587.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:457,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1002,&quot;bytes&quot;:103558,&quot;alt&quot;:&quot;CREATE OR REPLACE FUNCTION get_customer_order_stats(     p_customer_id INT,     OUT order_count INT,     OUT total_amount NUMERIC ) LANGUAGE plpgsql AS $$ BEGIN     SELECT COUNT(*), COALESCE(SUM(total_amount), 0)     INTO   order_count, total_amount     FROM   orders     WHERE  customer_id = p_customer_id; END; $$;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187144501?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4f4c40c-33dd-4bc4-995e-3891b10aa9d4_1870x587.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="CREATE OR REPLACE FUNCTION get_customer_order_stats(     p_customer_id INT,     OUT order_count INT,     OUT total_amount NUMERIC ) LANGUAGE plpgsql AS $$ BEGIN     SELECT COUNT(*), COALESCE(SUM(total_amount), 0)     INTO   order_count, total_amount     FROM   orders     WHERE  customer_id = p_customer_id; END; $$;" title="CREATE OR REPLACE FUNCTION get_customer_order_stats(     p_customer_id INT,     OUT order_count INT,     OUT total_amount NUMERIC ) LANGUAGE plpgsql AS $$ BEGIN     SELECT COUNT(*), COALESCE(SUM(total_amount), 0)     INTO   order_count, total_amount     FROM   orders     WHERE  customer_id = p_customer_id; END; $$;" srcset="https://substackcdn.com/image/fetch/$s_!cZNT!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4f4c40c-33dd-4bc4-995e-3891b10aa9d4_1870x587.png 424w, https://substackcdn.com/image/fetch/$s_!cZNT!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4f4c40c-33dd-4bc4-995e-3891b10aa9d4_1870x587.png 848w, https://substackcdn.com/image/fetch/$s_!cZNT!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4f4c40c-33dd-4bc4-995e-3891b10aa9d4_1870x587.png 1272w, https://substackcdn.com/image/fetch/$s_!cZNT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4f4c40c-33dd-4bc4-995e-3891b10aa9d4_1870x587.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Calling <code>SELECT * FROM get_customer_order_stats(7);</code> returns a single row with <code>order_count</code> and <code>total_amount</code>. Input and output values tie directly to the parameter list, which keeps the function signature self documenting. Client code that calls this function does not need to know the internal query, only the name and the output column names.</p><p>Default parameter values give procedures extra flexibility. Databases such as SQL Server and PostgreSQL support defaults, which let callers omit certain arguments when they want common behavior. Procedures that usually look back thirty days can define <code>@DaysBack INT = 30</code> or <code>p_days_back INT DEFAULT 30</code>. Callers that want the default window do not have to pass a value, while callers with special needs can pass a different number.</p><p>Write focused stored procedures rely heavily on parameters and outputs. In one routine that creates an order, the procedure may accept customer data, product ids, quantities, and discount codes as inputs, then hand back a new order id as an output parameter or a single row result. Journal posting procedures can return a transcript id through an output parameter so later processes can refer back to it. These patterns keep the work anchored inside the database while still giving applications clear ways to send in values and read back results.</p><h3>Working With Stored Procedures In Applications</h3><p>Client code that talks to a database does not need to send raw SQL text for every operation. Stored procedures give that code a named entry point that represents a unit of work inside the database, such as loading a summary view, posting a payment, or archiving a set of records. The application passes parameters, the procedure runs its logic next to the data, and the result comes back as rows, output values, or both. That arrangement fits well with the idea of a data API, where the procedure name and its parameters feel similar to a service call while table layouts and join details stay inside the database.</p><p>Many groups treat stored procedures as a boundary. Everything on the application side works with procedure names and parameter objects. Everything on the database side works with tables, indexes, constraints, and the procedure bodies that coordinate them. When that boundary stays stable, schema changes inside the database can evolve without constant edits to application SQL, because the procedure contract remains the same even as its internals change.</p><h4>Calling Procedures From Application Code</h4><p>Client libraries give Java code a way to call stored procedures through JDBC without building SQL strings by hand. JDBC offers <code>CallableStatement</code> for this purpose, which lets a developer prepare a call, bind input parameters, register output parameters, and then execute the stored routine. The driver turns that call into the actual <code>CALL</code> statement or vendor specific syntax that the database expects.</p><p>Let&#8217;s Imagine a SQL Server procedure named <code>dbo.GetCustomerOrderSummary</code> that returns recent orders for a customer and sets an output parameter for the total amount. Java code that calls it through JDBC can look like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yIIL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0f02559-916a-43af-a29c-432b5c345e5d_1794x900.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yIIL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0f02559-916a-43af-a29c-432b5c345e5d_1794x900.png 424w, https://substackcdn.com/image/fetch/$s_!yIIL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0f02559-916a-43af-a29c-432b5c345e5d_1794x900.png 848w, https://substackcdn.com/image/fetch/$s_!yIIL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0f02559-916a-43af-a29c-432b5c345e5d_1794x900.png 1272w, https://substackcdn.com/image/fetch/$s_!yIIL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0f02559-916a-43af-a29c-432b5c345e5d_1794x900.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yIIL!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0f02559-916a-43af-a29c-432b5c345e5d_1794x900.png" width="1200" height="601.6483516483516" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b0f02559-916a-43af-a29c-432b5c345e5d_1794x900.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:730,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:173052,&quot;alt&quot;:&quot;import java.math.BigDecimal; import java.sql.*;  public class OrderRepository {      private final String connectionString;      public OrderRepository(String connectionString) {         this.connectionString = connectionString;     }      public void loadCustomerSummary(int customerId) throws SQLException {         try (Connection conn = DriverManager.getConnection(connectionString);              CallableStatement stmt = conn.prepareCall(\&quot;{call dbo.GetCustomerOrderSummary(?, ?)}\&quot;)) {              stmt.setInt(1, customerId);             stmt.registerOutParameter(2, Types.DECIMAL);              boolean hasResultSet = stmt.execute();             if (hasResultSet) {                 try (ResultSet rs = stmt.getResultSet()) {                     while (rs.next()) {                         // map each row into an order object or DTO here                     }                 }             }              BigDecimal total = stmt.getBigDecimal(2);             // use total and mapped rows here         }     } }&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187144501?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0f02559-916a-43af-a29c-432b5c345e5d_1794x900.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="import java.math.BigDecimal; import java.sql.*;  public class OrderRepository {      private final String connectionString;      public OrderRepository(String connectionString) {         this.connectionString = connectionString;     }      public void loadCustomerSummary(int customerId) throws SQLException {         try (Connection conn = DriverManager.getConnection(connectionString);              CallableStatement stmt = conn.prepareCall(&quot;{call dbo.GetCustomerOrderSummary(?, ?)}&quot;)) {              stmt.setInt(1, customerId);             stmt.registerOutParameter(2, Types.DECIMAL);              boolean hasResultSet = stmt.execute();             if (hasResultSet) {                 try (ResultSet rs = stmt.getResultSet()) {                     while (rs.next()) {                         // map each row into an order object or DTO here                     }                 }             }              BigDecimal total = stmt.getBigDecimal(2);             // use total and mapped rows here         }     } }" title="import java.math.BigDecimal; import java.sql.*;  public class OrderRepository {      private final String connectionString;      public OrderRepository(String connectionString) {         this.connectionString = connectionString;     }      public void loadCustomerSummary(int customerId) throws SQLException {         try (Connection conn = DriverManager.getConnection(connectionString);              CallableStatement stmt = conn.prepareCall(&quot;{call dbo.GetCustomerOrderSummary(?, ?)}&quot;)) {              stmt.setInt(1, customerId);             stmt.registerOutParameter(2, Types.DECIMAL);              boolean hasResultSet = stmt.execute();             if (hasResultSet) {                 try (ResultSet rs = stmt.getResultSet()) {                     while (rs.next()) {                         // map each row into an order object or DTO here                     }                 }             }              BigDecimal total = stmt.getBigDecimal(2);             // use total and mapped rows here         }     } }" srcset="https://substackcdn.com/image/fetch/$s_!yIIL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0f02559-916a-43af-a29c-432b5c345e5d_1794x900.png 424w, https://substackcdn.com/image/fetch/$s_!yIIL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0f02559-916a-43af-a29c-432b5c345e5d_1794x900.png 848w, https://substackcdn.com/image/fetch/$s_!yIIL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0f02559-916a-43af-a29c-432b5c345e5d_1794x900.png 1272w, https://substackcdn.com/image/fetch/$s_!yIIL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0f02559-916a-43af-a29c-432b5c345e5d_1794x900.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The repository method treats the stored procedure as a named operation, with <code>customerId</code> going in and data for that customer coming back. The call string <code>"{call dbo.GetCustomerOrderSummary(?, ?)}"</code> declares two parameters that match the procedure signature. <code>setInt</code> binds the input value, and <code>registerOutParameter</code> asks for the total amount back from the database. Result rows move through the <code>ResultSet</code> loop, while the aggregate total arrives through the output parameter.</p><p>Many codebases wrap this pattern in a thinner adapter layer so that business code never touches JDBC types directly. That adapter maps procedure calls into higher level methods such as <code>loadCustomerSummary</code>, <code>placeOrder</code>, or <code>archiveOldSessions</code>. In that setup the data layer becomes a wrapper around a small set of procedures, and Java code sees those methods as its interface, while the stored routines carry all the SQL details.</p><p>PostgreSQL and MySQL work with Java in similar fashion. PostgreSQL often favors functions that return sets, so a call can go through a <code>PreparedStatement</code> with SQL such as <code>SELECT * FROM get_recent_orders(?)</code>, while MySQL drivers frequently handle <code>CALL GetRecentOrders(?)</code> through <code>CallableStatement</code>. The application side keeps the same pattern of preparing a call, binding parameters, and reading rows or output values, even though the exact SQL string sent to the server varies between engines.</p><h4>Performance For Bulk Workloads</h4><p>Heavy workloads benefit when the database can perform large set operations in a single stored procedure call rather than a long stream of row by row commands from the application layer. Network latency, round trip counts, and repeated parsing all add up when a client sends many small commands. When a single procedure processes a large batch in one call, that overhead goes down and the optimizer can choose plans that handle the whole set in one go.</p><p>Billing runs make a good example. Suppose a data model holds accounts and usage records, and every month the system needs to generate invoices for every active account based on that usage. Java code could loop through every account and send an insert per invoice, but that burns network time and ties business throughput to a loop in application code. Stored procedures move that work closer to the data in a more set oriented way.</p><p>This T-SQL procedure posts monthly invoices for accounts within a given billing period:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!aHAD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70e69a1c-d5c6-45c2-8b44-420fc7af3484_1862x839.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!aHAD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70e69a1c-d5c6-45c2-8b44-420fc7af3484_1862x839.png 424w, https://substackcdn.com/image/fetch/$s_!aHAD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70e69a1c-d5c6-45c2-8b44-420fc7af3484_1862x839.png 848w, https://substackcdn.com/image/fetch/$s_!aHAD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70e69a1c-d5c6-45c2-8b44-420fc7af3484_1862x839.png 1272w, https://substackcdn.com/image/fetch/$s_!aHAD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70e69a1c-d5c6-45c2-8b44-420fc7af3484_1862x839.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!aHAD!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70e69a1c-d5c6-45c2-8b44-420fc7af3484_1862x839.png" width="1002" height="451.45054945054943" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/70e69a1c-d5c6-45c2-8b44-420fc7af3484_1862x839.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:656,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1002,&quot;bytes&quot;:166190,&quot;alt&quot;:&quot;import java.math.BigDecimal; import java.sql.*;  public class OrderRepository {      private final String connectionString;      public OrderRepository(String connectionString) {         this.connectionString = connectionString;     }      public void loadCustomerSummary(int customerId) throws SQLException {         try (Connection conn = DriverManager.getConnection(connectionString);              CallableStatement stmt = conn.prepareCall(\&quot;{call dbo.GetCustomerOrderSummary(?, ?)}\&quot;)) {              stmt.setInt(1, customerId);             stmt.registerOutParameter(2, Types.DECIMAL);              boolean hasResultSet = stmt.execute();             if (hasResultSet) {                 try (ResultSet rs = stmt.getResultSet()) {                     while (rs.next()) {                         // map each row into an order object or DTO here                     }                 }             }              BigDecimal total = stmt.getBigDecimal(2);             // use total and mapped rows here         }     } }&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187144501?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70e69a1c-d5c6-45c2-8b44-420fc7af3484_1862x839.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="import java.math.BigDecimal; import java.sql.*;  public class OrderRepository {      private final String connectionString;      public OrderRepository(String connectionString) {         this.connectionString = connectionString;     }      public void loadCustomerSummary(int customerId) throws SQLException {         try (Connection conn = DriverManager.getConnection(connectionString);              CallableStatement stmt = conn.prepareCall(&quot;{call dbo.GetCustomerOrderSummary(?, ?)}&quot;)) {              stmt.setInt(1, customerId);             stmt.registerOutParameter(2, Types.DECIMAL);              boolean hasResultSet = stmt.execute();             if (hasResultSet) {                 try (ResultSet rs = stmt.getResultSet()) {                     while (rs.next()) {                         // map each row into an order object or DTO here                     }                 }             }              BigDecimal total = stmt.getBigDecimal(2);             // use total and mapped rows here         }     } }" title="import java.math.BigDecimal; import java.sql.*;  public class OrderRepository {      private final String connectionString;      public OrderRepository(String connectionString) {         this.connectionString = connectionString;     }      public void loadCustomerSummary(int customerId) throws SQLException {         try (Connection conn = DriverManager.getConnection(connectionString);              CallableStatement stmt = conn.prepareCall(&quot;{call dbo.GetCustomerOrderSummary(?, ?)}&quot;)) {              stmt.setInt(1, customerId);             stmt.registerOutParameter(2, Types.DECIMAL);              boolean hasResultSet = stmt.execute();             if (hasResultSet) {                 try (ResultSet rs = stmt.getResultSet()) {                     while (rs.next()) {                         // map each row into an order object or DTO here                     }                 }             }              BigDecimal total = stmt.getBigDecimal(2);             // use total and mapped rows here         }     } }" srcset="https://substackcdn.com/image/fetch/$s_!aHAD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70e69a1c-d5c6-45c2-8b44-420fc7af3484_1862x839.png 424w, https://substackcdn.com/image/fetch/$s_!aHAD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70e69a1c-d5c6-45c2-8b44-420fc7af3484_1862x839.png 848w, https://substackcdn.com/image/fetch/$s_!aHAD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70e69a1c-d5c6-45c2-8b44-420fc7af3484_1862x839.png 1272w, https://substackcdn.com/image/fetch/$s_!aHAD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70e69a1c-d5c6-45c2-8b44-420fc7af3484_1862x839.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This single procedure call scans the relevant usage rows, joins to the accounts table, groups by account, and inserts invoice rows in one set. Application code calls <code>billing.PostMonthlyInvoices</code> for a given billing period, passing the dates, instead of iterating through thousands of accounts on its own. The database engine can choose an execution plan that fits the entire set of usage rows, which tends to lower overhead compared to repeating smaller queries.</p><p>Java can treat this bulk operation as a single method call in its data layer. Take this for example that runs the billing procedure through JDBC:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9f6w!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90afdcb9-02e3-4475-aed5-3ea7f44e18f3_1779x631.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9f6w!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90afdcb9-02e3-4475-aed5-3ea7f44e18f3_1779x631.png 424w, https://substackcdn.com/image/fetch/$s_!9f6w!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90afdcb9-02e3-4475-aed5-3ea7f44e18f3_1779x631.png 848w, https://substackcdn.com/image/fetch/$s_!9f6w!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90afdcb9-02e3-4475-aed5-3ea7f44e18f3_1779x631.png 1272w, https://substackcdn.com/image/fetch/$s_!9f6w!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90afdcb9-02e3-4475-aed5-3ea7f44e18f3_1779x631.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9f6w!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90afdcb9-02e3-4475-aed5-3ea7f44e18f3_1779x631.png" width="1104" height="391.25274725274727" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/90afdcb9-02e3-4475-aed5-3ea7f44e18f3_1779x631.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:516,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1104,&quot;bytes&quot;:150610,&quot;alt&quot;:&quot;public class BillingRunner {      private final String connectionString;      public BillingRunner(String connectionString) {         this.connectionString = connectionString;     }      public void runMonthlyBilling(LocalDate periodStart, LocalDate periodEnd) throws SQLException {         try (Connection conn = DriverManager.getConnection(connectionString);              CallableStatement stmt = conn.prepareCall(\&quot;{call billing.PostMonthlyInvoices(?, ?)}\&quot;)) {              stmt.setDate(1, Date.valueOf(periodStart));             stmt.setDate(2, Date.valueOf(periodEnd));             stmt.execute();         }     } }&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187144501?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90afdcb9-02e3-4475-aed5-3ea7f44e18f3_1779x631.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="public class BillingRunner {      private final String connectionString;      public BillingRunner(String connectionString) {         this.connectionString = connectionString;     }      public void runMonthlyBilling(LocalDate periodStart, LocalDate periodEnd) throws SQLException {         try (Connection conn = DriverManager.getConnection(connectionString);              CallableStatement stmt = conn.prepareCall(&quot;{call billing.PostMonthlyInvoices(?, ?)}&quot;)) {              stmt.setDate(1, Date.valueOf(periodStart));             stmt.setDate(2, Date.valueOf(periodEnd));             stmt.execute();         }     } }" title="public class BillingRunner {      private final String connectionString;      public BillingRunner(String connectionString) {         this.connectionString = connectionString;     }      public void runMonthlyBilling(LocalDate periodStart, LocalDate periodEnd) throws SQLException {         try (Connection conn = DriverManager.getConnection(connectionString);              CallableStatement stmt = conn.prepareCall(&quot;{call billing.PostMonthlyInvoices(?, ?)}&quot;)) {              stmt.setDate(1, Date.valueOf(periodStart));             stmt.setDate(2, Date.valueOf(periodEnd));             stmt.execute();         }     } }" srcset="https://substackcdn.com/image/fetch/$s_!9f6w!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90afdcb9-02e3-4475-aed5-3ea7f44e18f3_1779x631.png 424w, https://substackcdn.com/image/fetch/$s_!9f6w!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90afdcb9-02e3-4475-aed5-3ea7f44e18f3_1779x631.png 848w, https://substackcdn.com/image/fetch/$s_!9f6w!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90afdcb9-02e3-4475-aed5-3ea7f44e18f3_1779x631.png 1272w, https://substackcdn.com/image/fetch/$s_!9f6w!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90afdcb9-02e3-4475-aed5-3ea7f44e18f3_1779x631.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The Java side now treats billing as one database operation that takes a period start and end date. All details about scanning usage, filtering active accounts, and inserting invoices live in the procedure body. Performance tuning around indexes, joins, and grouping logic stays in T-SQL, where database specialists can adjust it without changing the shape of the Java call.</p><p>Stored procedures also work well for other bulk cases, such as archiving old session rows into history tables, recalculating reward points across a customer base, or rebuilding summary tables for reports. In each of these cases, the client code sends a small parameter set while the database performs the heavy work in one or a few statements.</p><h4>Security With Least Privilege</h4><p>Relational databases give fine grained control over what each login can do, and stored procedures fit naturally with that model. Rather than granting an application user direct rights on tables, a security plan can grant only <code>EXECUTE</code> permission on a small group of stored procedures. Those procedures then act as gates that enforce business rules and log activity whenever data changes.</p><p>SQL Server makes this pattern fairly direct. An application login maps to a database user, and that user can receive <code>EXECUTE</code> rights on individual procedures while tables remain locked down. This setup is common to see:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!1QCH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefa515ff-1516-41d2-8bf0-f2b3e813ae2d_1847x279.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1QCH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefa515ff-1516-41d2-8bf0-f2b3e813ae2d_1847x279.png 424w, https://substackcdn.com/image/fetch/$s_!1QCH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefa515ff-1516-41d2-8bf0-f2b3e813ae2d_1847x279.png 848w, https://substackcdn.com/image/fetch/$s_!1QCH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefa515ff-1516-41d2-8bf0-f2b3e813ae2d_1847x279.png 1272w, https://substackcdn.com/image/fetch/$s_!1QCH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefa515ff-1516-41d2-8bf0-f2b3e813ae2d_1847x279.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!1QCH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefa515ff-1516-41d2-8bf0-f2b3e813ae2d_1847x279.png" width="1456" height="220" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/efa515ff-1516-41d2-8bf0-f2b3e813ae2d_1847x279.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:220,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:100612,&quot;alt&quot;:&quot;CREATE LOGIN app_login WITH PASSWORD = 'strong_password_here'; CREATE USER app_user FOR LOGIN app_login;  GRANT EXECUTE ON dbo.GetCustomerOrderSummary TO app_user; GRANT EXECUTE ON billing.PostMonthlyInvoices TO app_user;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187144501?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefa515ff-1516-41d2-8bf0-f2b3e813ae2d_1847x279.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE LOGIN app_login WITH PASSWORD = 'strong_password_here'; CREATE USER app_user FOR LOGIN app_login;  GRANT EXECUTE ON dbo.GetCustomerOrderSummary TO app_user; GRANT EXECUTE ON billing.PostMonthlyInvoices TO app_user;" title="CREATE LOGIN app_login WITH PASSWORD = 'strong_password_here'; CREATE USER app_user FOR LOGIN app_login;  GRANT EXECUTE ON dbo.GetCustomerOrderSummary TO app_user; GRANT EXECUTE ON billing.PostMonthlyInvoices TO app_user;" srcset="https://substackcdn.com/image/fetch/$s_!1QCH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefa515ff-1516-41d2-8bf0-f2b3e813ae2d_1847x279.png 424w, https://substackcdn.com/image/fetch/$s_!1QCH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefa515ff-1516-41d2-8bf0-f2b3e813ae2d_1847x279.png 848w, https://substackcdn.com/image/fetch/$s_!1QCH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefa515ff-1516-41d2-8bf0-f2b3e813ae2d_1847x279.png 1272w, https://substackcdn.com/image/fetch/$s_!1QCH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefa515ff-1516-41d2-8bf0-f2b3e813ae2d_1847x279.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>With this configuration the <code>app_user</code> account can run <code>dbo.GetCustomerOrderSummary</code> and <code>billing.PostMonthlyInvoices</code>, but cannot select from <code>dbo.Orders</code>, <code>billing.Usage</code>, or <code>billing.Invoices</code> directly unless more rights are granted later. In SQL Server, ownership chaining can skip permission checks on underlying objects when the procedure and the referenced objects share the same owner. In that case, the caller needs <code>EXECUTE</code> permission on the procedure, not direct permissions on the tables.</p><p>For PostgreSQL, you can create a database account with limited rights, then grant execution on the functions and procedures that form the data API:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!D338!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5719f91e-fd19-4ffe-8d52-e548ea2d89d2_1863x168.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!D338!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5719f91e-fd19-4ffe-8d52-e548ea2d89d2_1863x168.png 424w, https://substackcdn.com/image/fetch/$s_!D338!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5719f91e-fd19-4ffe-8d52-e548ea2d89d2_1863x168.png 848w, https://substackcdn.com/image/fetch/$s_!D338!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5719f91e-fd19-4ffe-8d52-e548ea2d89d2_1863x168.png 1272w, https://substackcdn.com/image/fetch/$s_!D338!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5719f91e-fd19-4ffe-8d52-e548ea2d89d2_1863x168.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!D338!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5719f91e-fd19-4ffe-8d52-e548ea2d89d2_1863x168.png" width="974" height="87.63324175824175" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5719f91e-fd19-4ffe-8d52-e548ea2d89d2_1863x168.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:131,&quot;width&quot;:1456,&quot;resizeWidth&quot;:974,&quot;bytes&quot;:66524,&quot;alt&quot;:&quot;CREATE ROLE app_role LOGIN PASSWORD 'strong_password_here';  GRANT EXECUTE ON FUNCTION get_customer_order_stats(INT) TO app_role; GRANT EXECUTE ON PROCEDURE billing_post_monthly_invoices(DATE, DATE) TO app_role;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187144501?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5719f91e-fd19-4ffe-8d52-e548ea2d89d2_1863x168.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="CREATE ROLE app_role LOGIN PASSWORD 'strong_password_here';  GRANT EXECUTE ON FUNCTION get_customer_order_stats(INT) TO app_role; GRANT EXECUTE ON PROCEDURE billing_post_monthly_invoices(DATE, DATE) TO app_role;" title="CREATE ROLE app_role LOGIN PASSWORD 'strong_password_here';  GRANT EXECUTE ON FUNCTION get_customer_order_stats(INT) TO app_role; GRANT EXECUTE ON PROCEDURE billing_post_monthly_invoices(DATE, DATE) TO app_role;" srcset="https://substackcdn.com/image/fetch/$s_!D338!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5719f91e-fd19-4ffe-8d52-e548ea2d89d2_1863x168.png 424w, https://substackcdn.com/image/fetch/$s_!D338!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5719f91e-fd19-4ffe-8d52-e548ea2d89d2_1863x168.png 848w, https://substackcdn.com/image/fetch/$s_!D338!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5719f91e-fd19-4ffe-8d52-e548ea2d89d2_1863x168.png 1272w, https://substackcdn.com/image/fetch/$s_!D338!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5719f91e-fd19-4ffe-8d52-e548ea2d89d2_1863x168.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Granting <code>EXECUTE</code> on routines limits what can be invoked, but it does not by itself bypass table privileges in PostgreSQL. By default, functions run as <code>SECURITY INVOKER</code>, so the caller still needs the required privileges on referenced tables. If you want a low privilege role to run table access through a controlled routine, define the routine as <code>SECURITY DEFINER</code> under a trusted owner and lock down its <code>search_path</code>, or explicitly grant the needed table privileges.</p><p>This model also reduces the impact of SQL injection bugs in higher layers. Parameter binding with <code>CallableStatement</code> or prepared statements sends values separately from the SQL or procedure text. An attacker who finds a way to manipulate strings in application code still has to get through the stored procedure contract. If the only option is to call something like <code>billing.PostMonthlyInvoices</code> with different dates, there is far less room to smuggle in arbitrary data access.</p><h4>Version Control For Stored Procedures</h4><p>Database code benefits from the same discipline as application code, and stored procedures sit right at the center of that. Treating procedure bodies as first class source files brings them into code review, pull requests, testing, and release pipelines. Rather than editing procedure text directly in a management console, many shops keep a folder of <code>.sql</code> files in a Git repository and apply them through database tooling.</p><p>SQL Server Data Tools integrates database schema management with Visual Studio. Stored procedures, tables, and views live as files inside a database project. A developer edits <code>dbo.GetCustomerOrderSummary.sql</code>, commits the change, and shares it through normal source control. SSDT can compare the project state to a target database and generate upgrade scripts, which include <code>ALTER PROCEDURE</code> statements that bring the server in line with the project. That workflow treats stored procedures as part of the same lifecycle as the rest of the codebase, with history and review for every change.</p><p>Migration tools such as Flyway and Liquibase take a slightly different route. They focus on ordered migration scripts that describe how a database moves from one version to the next. Stored procedure changes become part of these migrations. In a Flyway setup, a new procedure or change to an existing one goes into a migration file with a versioned name such as <code>V005__update_customer_summary_proc.sql</code>:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vAlR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F478d31aa-c4dc-430d-baa6-018815230d1f_1876x734.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vAlR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F478d31aa-c4dc-430d-baa6-018815230d1f_1876x734.png 424w, https://substackcdn.com/image/fetch/$s_!vAlR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F478d31aa-c4dc-430d-baa6-018815230d1f_1876x734.png 848w, https://substackcdn.com/image/fetch/$s_!vAlR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F478d31aa-c4dc-430d-baa6-018815230d1f_1876x734.png 1272w, https://substackcdn.com/image/fetch/$s_!vAlR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F478d31aa-c4dc-430d-baa6-018815230d1f_1876x734.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vAlR!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F478d31aa-c4dc-430d-baa6-018815230d1f_1876x734.png" width="1200" height="469.7802197802198" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/478d31aa-c4dc-430d-baa6-018815230d1f_1876x734.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:570,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:132628,&quot;alt&quot;:&quot;-- V005__update_customer_summary_proc.sql ALTER PROCEDURE dbo.GetCustomerOrderSummary     @CustomerId   INT,     @TotalAmount  DECIMAL(18,2) OUTPUT AS BEGIN     SET NOCOUNT ON;      SELECT o.OrderId,            o.OrderDate,            o.TotalAmount,            o.CurrencyCode     FROM   dbo.Orders AS o     WHERE  o.CustomerId = @CustomerId     ORDER BY o.OrderDate DESC;      SELECT @TotalAmount =         SUM(o.TotalAmount)     FROM dbo.Orders AS o     WHERE o.CustomerId = @CustomerId; END;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187144501?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F478d31aa-c4dc-430d-baa6-018815230d1f_1876x734.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="-- V005__update_customer_summary_proc.sql ALTER PROCEDURE dbo.GetCustomerOrderSummary     @CustomerId   INT,     @TotalAmount  DECIMAL(18,2) OUTPUT AS BEGIN     SET NOCOUNT ON;      SELECT o.OrderId,            o.OrderDate,            o.TotalAmount,            o.CurrencyCode     FROM   dbo.Orders AS o     WHERE  o.CustomerId = @CustomerId     ORDER BY o.OrderDate DESC;      SELECT @TotalAmount =         SUM(o.TotalAmount)     FROM dbo.Orders AS o     WHERE o.CustomerId = @CustomerId; END;" title="-- V005__update_customer_summary_proc.sql ALTER PROCEDURE dbo.GetCustomerOrderSummary     @CustomerId   INT,     @TotalAmount  DECIMAL(18,2) OUTPUT AS BEGIN     SET NOCOUNT ON;      SELECT o.OrderId,            o.OrderDate,            o.TotalAmount,            o.CurrencyCode     FROM   dbo.Orders AS o     WHERE  o.CustomerId = @CustomerId     ORDER BY o.OrderDate DESC;      SELECT @TotalAmount =         SUM(o.TotalAmount)     FROM dbo.Orders AS o     WHERE o.CustomerId = @CustomerId; END;" srcset="https://substackcdn.com/image/fetch/$s_!vAlR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F478d31aa-c4dc-430d-baa6-018815230d1f_1876x734.png 424w, https://substackcdn.com/image/fetch/$s_!vAlR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F478d31aa-c4dc-430d-baa6-018815230d1f_1876x734.png 848w, https://substackcdn.com/image/fetch/$s_!vAlR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F478d31aa-c4dc-430d-baa6-018815230d1f_1876x734.png 1272w, https://substackcdn.com/image/fetch/$s_!vAlR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F478d31aa-c4dc-430d-baa6-018815230d1f_1876x734.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Flyway records that this script has run on a given database by writing into its metadata table. When a new environment comes online, Flyway walks through the migration directory in order and applies any scripts that have not run yet. Stored procedure changes travel with the rest of the schema changes, which keeps development, test, and production databases in sync.</p><p>Liquibase supports a similar story through change logs that can be written in XML, YAML, JSON, or raw SQL. Change sets in Liquibase can include an <code>sqlFile</code> entry that points at a stored procedure definition, and Liquibase applies that file as part of a numbered change. Either way the important point is that the text of the procedure lives in source control, with a recorded history, rather than only inside the database catalog.</p><p>Treating stored procedures as versioned artifacts brings stability to the data API idea. The procedure signature and behavior are documented by the source file and by tests, changes pass through review like any other code, and rollbacks stay possible by reapplying an older migration or reverting in Git and redeploying. That mindset keeps the interface between Java code and the database predictable, even as schema and performance tuning evolve inside the stored procedures.</p><h3>Conclusion</h3><p>Stored procedures anchor business logic in the database, tying together parameter handling, compiled plans, and result structures in one named unit. They give application code stable entry points that behave like a data API while the database handles joins, filtering, and set based updates near the tables. Performance gains show up when heavy batches move into set oriented procedures, and security improves when application accounts receive only EXECUTE rights instead of direct table access. Version controlled procedure definitions and scripted migrations round out the idea by keeping that data API reproducible across environments and traceable as it changes.</p><ol><li><p><em><a href="https://learn.microsoft.com/en-us/sql/relational-databases/stored-procedures/stored-procedures-database-engine?view=sql-server-ver17">SQL Server Stored Procedures Documentation</a></em></p></li><li><p><em><a href="https://www.postgresql.org/docs/current/sql-createfunction.html">PostgreSQL CREATE FUNCTION Reference</a></em></p></li><li><p><em><a href="https://www.postgresql.org/docs/current/sql-createprocedure.html">PostgreSQL CREATE PROCEDURE Reference</a></em></p></li><li><p><em><a href="https://dev.mysql.com/doc/refman/8.0/en/create-procedure.html">MySQL CREATE PROCEDURE Reference</a></em></p></li><li><p><em><a href="https://docs.oracle.com/javase/8/docs/api/java/sql/CallableStatement.html">JDBC CallableStatement Javadoc</a></em></p></li><li><p><em><a href="https://documentation.red-gate.com/fd">Flyway Database Migrations Documentation</a></em></p></li><li><p><em><a href="https://docs.liquibase.com/">Liquibase Documentation</a></em></p></li></ol><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share&quot;,&quot;text&quot;:&quot;Share Alexander Obregon's Substack&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share"><span>Share Alexander Obregon's Substack</span></a></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mhLf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5da5d7c0-637b-4306-8bb5-29b54c8baf10_306x306.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mhLf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5da5d7c0-637b-4306-8bb5-29b54c8baf10_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!mhLf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5da5d7c0-637b-4306-8bb5-29b54c8baf10_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!mhLf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5da5d7c0-637b-4306-8bb5-29b54c8baf10_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!mhLf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5da5d7c0-637b-4306-8bb5-29b54c8baf10_306x306.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mhLf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5da5d7c0-637b-4306-8bb5-29b54c8baf10_306x306.png" width="306" height="306" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5da5d7c0-637b-4306-8bb5-29b54c8baf10_306x306.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:306,&quot;width&quot;:306,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!mhLf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5da5d7c0-637b-4306-8bb5-29b54c8baf10_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!mhLf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5da5d7c0-637b-4306-8bb5-29b54c8baf10_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!mhLf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5da5d7c0-637b-4306-8bb5-29b54c8baf10_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!mhLf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5da5d7c0-637b-4306-8bb5-29b54c8baf10_306x306.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://freesvg.org/mono-sql">Image Source</a></figcaption></figure></div>]]></content:encoded></item><item><title><![CDATA[Temporary Tables in SQL Explained]]></title><description><![CDATA[Developers use temporary tables in SQL to store intermediate results without touching permanent schema, which helps break large queries into stages and keeps report logic readable.]]></description><link>https://alexanderobregon.substack.com/p/temporary-tables-in-sql-explained</link><guid isPermaLink="false">https://alexanderobregon.substack.com/p/temporary-tables-in-sql-explained</guid><dc:creator><![CDATA[Alexander Obregon]]></dc:creator><pubDate>Fri, 13 Feb 2026 18:59:14 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!qTg9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff34ca62-618f-4bc5-961e-c07f881ca478_306x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nRLu!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8bddbd5-0eb5-49e0-b685-dfec028e60a9_800x373.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nRLu!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8bddbd5-0eb5-49e0-b685-dfec028e60a9_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!nRLu!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8bddbd5-0eb5-49e0-b685-dfec028e60a9_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!nRLu!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8bddbd5-0eb5-49e0-b685-dfec028e60a9_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!nRLu!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8bddbd5-0eb5-49e0-b685-dfec028e60a9_800x373.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nRLu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8bddbd5-0eb5-49e0-b685-dfec028e60a9_800x373.png" width="800" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f8bddbd5-0eb5-49e0-b685-dfec028e60a9_800x373.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!nRLu!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8bddbd5-0eb5-49e0-b685-dfec028e60a9_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!nRLu!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8bddbd5-0eb5-49e0-b685-dfec028e60a9_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!nRLu!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8bddbd5-0eb5-49e0-b685-dfec028e60a9_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!nRLu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8bddbd5-0eb5-49e0-b685-dfec028e60a9_800x373.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://commons.wikimedia.org/wiki/File:Sql_data_base_with_logo.png">Image Source</a></figcaption></figure></div><p>Developers use temporary tables in SQL to store intermediate results without touching permanent schema, which helps break large queries into stages and keeps report logic readable. Database engines provide their own flavors of temporary storage, yet they share common ideas such as session scope, automatic cleanup, and basic <code>CREATE</code> and <code>DROP</code> syntax. That view of how temporary tables behave in current SQL engines, how to create and remove them, and how to pass partial results through them for multi step reports helps explain how they keep large queries from turning into a maze.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>How Temporary Tables Behave In Databases</h3><p>Temporary tables sit between short lived query results and permanent tables stored in the catalog. They use normal SQL syntax, yet the database keeps them separate from regular objects and ties them to a session or connection. That combination of familiar commands and special handling affects scope, lifetime, storage location, and performance, so it helps to walk through how the main engines treat them before building workflows on top.</p><h4>Scope, Lifetime, Storage</h4><p>Session scope is the first thing to keep in mind. MySQL, PostgreSQL, SQL Server, and Oracle all keep temporary data private in some way, but they do it with slightly different rules and naming schemes.</p><p>MySQL attaches a temporary table to the session that created it. No other connection can see it, and the name can be reused by other sessions with no conflict. The table vanishes automatically when the session ends, even if no <code>DROP</code> statement runs. The basic form looks like this:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dUex!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22d4374e-90a2-4d66-9a81-1620a97b99ef_1681x280.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dUex!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22d4374e-90a2-4d66-9a81-1620a97b99ef_1681x280.png 424w, https://substackcdn.com/image/fetch/$s_!dUex!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22d4374e-90a2-4d66-9a81-1620a97b99ef_1681x280.png 848w, https://substackcdn.com/image/fetch/$s_!dUex!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22d4374e-90a2-4d66-9a81-1620a97b99ef_1681x280.png 1272w, https://substackcdn.com/image/fetch/$s_!dUex!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22d4374e-90a2-4d66-9a81-1620a97b99ef_1681x280.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dUex!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22d4374e-90a2-4d66-9a81-1620a97b99ef_1681x280.png" width="1456" height="243" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/22d4374e-90a2-4d66-9a81-1620a97b99ef_1681x280.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:243,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:49310,&quot;alt&quot;:&quot;CREATE TEMPORARY TABLE temp_daily_totals (     customer_id INT,     sale_date   DATE,     amount      DECIMAL(12, 2) );&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187139427?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22d4374e-90a2-4d66-9a81-1620a97b99ef_1681x280.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE TEMPORARY TABLE temp_daily_totals (     customer_id INT,     sale_date   DATE,     amount      DECIMAL(12, 2) );" title="CREATE TEMPORARY TABLE temp_daily_totals (     customer_id INT,     sale_date   DATE,     amount      DECIMAL(12, 2) );" srcset="https://substackcdn.com/image/fetch/$s_!dUex!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22d4374e-90a2-4d66-9a81-1620a97b99ef_1681x280.png 424w, https://substackcdn.com/image/fetch/$s_!dUex!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22d4374e-90a2-4d66-9a81-1620a97b99ef_1681x280.png 848w, https://substackcdn.com/image/fetch/$s_!dUex!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22d4374e-90a2-4d66-9a81-1620a97b99ef_1681x280.png 1272w, https://substackcdn.com/image/fetch/$s_!dUex!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22d4374e-90a2-4d66-9a81-1620a97b99ef_1681x280.png 1456w" sizes="100vw"></picture><div></div></div></a></figure></div><p>That structure exists only for the current connection, and any attempt from another connection to query <code>temp_daily_totals</code> raises an error, even if the same schema and user name are involved.</p><p>PostgreSQL also ties temporary tables to sessions, but adds more detailed control around when rows disappear. The default behavior drops temporary tables at session end. With <code>ON COMMIT</code> clauses, the table can be kept while rows are removed, or the table can be removed entirely at transaction boundaries. One common form looks like this:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!rlNz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4e0d761-c380-48bd-8c0d-3b9a6c5a9e9f_1674x281.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rlNz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4e0d761-c380-48bd-8c0d-3b9a6c5a9e9f_1674x281.png 424w, https://substackcdn.com/image/fetch/$s_!rlNz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4e0d761-c380-48bd-8c0d-3b9a6c5a9e9f_1674x281.png 848w, https://substackcdn.com/image/fetch/$s_!rlNz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4e0d761-c380-48bd-8c0d-3b9a6c5a9e9f_1674x281.png 1272w, https://substackcdn.com/image/fetch/$s_!rlNz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4e0d761-c380-48bd-8c0d-3b9a6c5a9e9f_1674x281.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rlNz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4e0d761-c380-48bd-8c0d-3b9a6c5a9e9f_1674x281.png" width="1456" height="244" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a4e0d761-c380-48bd-8c0d-3b9a6c5a9e9f_1674x281.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:244,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:56536,&quot;alt&quot;:&quot;CREATE TEMP TABLE temp_recent_orders (     order_id   BIGINT,     order_date DATE,     total      NUMERIC(12, 2) ) ON COMMIT DELETE ROWS;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187139427?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4e0d761-c380-48bd-8c0d-3b9a6c5a9e9f_1674x281.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE TEMP TABLE temp_recent_orders (     order_id   BIGINT,     order_date DATE,     total      NUMERIC(12, 2) ) ON COMMIT DELETE ROWS;" title="CREATE TEMP TABLE temp_recent_orders (     order_id   BIGINT,     order_date DATE,     total      NUMERIC(12, 2) ) ON COMMIT DELETE ROWS;" srcset="https://substackcdn.com/image/fetch/$s_!rlNz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4e0d761-c380-48bd-8c0d-3b9a6c5a9e9f_1674x281.png 424w, https://substackcdn.com/image/fetch/$s_!rlNz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4e0d761-c380-48bd-8c0d-3b9a6c5a9e9f_1674x281.png 848w, https://substackcdn.com/image/fetch/$s_!rlNz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4e0d761-c380-48bd-8c0d-3b9a6c5a9e9f_1674x281.png 1272w, https://substackcdn.com/image/fetch/$s_!rlNz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4e0d761-c380-48bd-8c0d-3b9a6c5a9e9f_1674x281.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>That definition keeps the table name and columns around for the whole session, but clears all rows whenever a transaction commits. Short lived working sets benefit from this because a new transaction starts with an empty table, while code does not need to recreate the structure each time.</p><p>SQL Server takes a different route and uses naming rules to flag temporary tables. Names that start with a single hash mark such as <code>#recent_orders</code> create a local temporary table visible only inside that connection and any nested scopes spawned from it. Names that start with two hash marks such as <code>##scratchpad</code> create tables that can be shared by multiple connections while at least one connection still references them. Both live in the <code>tempdb</code> database behind the scenes. Common usage in code looks like this:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!jB4l!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62567925-7479-4fa0-a11f-5addf832fef9_1681x281.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!jB4l!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62567925-7479-4fa0-a11f-5addf832fef9_1681x281.png 424w, https://substackcdn.com/image/fetch/$s_!jB4l!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62567925-7479-4fa0-a11f-5addf832fef9_1681x281.png 848w, https://substackcdn.com/image/fetch/$s_!jB4l!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62567925-7479-4fa0-a11f-5addf832fef9_1681x281.png 1272w, https://substackcdn.com/image/fetch/$s_!jB4l!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62567925-7479-4fa0-a11f-5addf832fef9_1681x281.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!jB4l!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62567925-7479-4fa0-a11f-5addf832fef9_1681x281.png" width="1456" height="243" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/62567925-7479-4fa0-a11f-5addf832fef9_1681x281.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:243,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:40981,&quot;alt&quot;:&quot;CREATE TABLE #recent_orders (     order_id   INT,     order_date DATE,     total      DECIMAL(12, 2) );&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187139427?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62567925-7479-4fa0-a11f-5addf832fef9_1681x281.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE TABLE #recent_orders (     order_id   INT,     order_date DATE,     total      DECIMAL(12, 2) );" title="CREATE TABLE #recent_orders (     order_id   INT,     order_date DATE,     total      DECIMAL(12, 2) );" srcset="https://substackcdn.com/image/fetch/$s_!jB4l!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62567925-7479-4fa0-a11f-5addf832fef9_1681x281.png 424w, https://substackcdn.com/image/fetch/$s_!jB4l!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62567925-7479-4fa0-a11f-5addf832fef9_1681x281.png 848w, https://substackcdn.com/image/fetch/$s_!jB4l!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62567925-7479-4fa0-a11f-5addf832fef9_1681x281.png 1272w, https://substackcdn.com/image/fetch/$s_!jB4l!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62567925-7479-4fa0-a11f-5addf832fef9_1681x281.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>That table disappears when the connection that created it closes, so long running application processes that reuse connections tend to drop it explicitly once they no longer need it.</p><p>Oracle exposes global temporary tables, which work a bit differently. The table definition is permanent and stored in the data dictionary, so any session can reference the table by name. Data itself is still temporary, and each session sees only its own rows. The <code>ON COMMIT</code> clause controls when those rows go away. Typical usage looks like this:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!TNWs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F736842de-390f-4a42-8706-d39ce23d9bb0_1688x282.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!TNWs!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F736842de-390f-4a42-8706-d39ce23d9bb0_1688x282.png 424w, https://substackcdn.com/image/fetch/$s_!TNWs!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F736842de-390f-4a42-8706-d39ce23d9bb0_1688x282.png 848w, https://substackcdn.com/image/fetch/$s_!TNWs!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F736842de-390f-4a42-8706-d39ce23d9bb0_1688x282.png 1272w, https://substackcdn.com/image/fetch/$s_!TNWs!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F736842de-390f-4a42-8706-d39ce23d9bb0_1688x282.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!TNWs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F736842de-390f-4a42-8706-d39ce23d9bb0_1688x282.png" width="1456" height="243" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/736842de-390f-4a42-8706-d39ce23d9bb0_1688x282.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:243,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:64406,&quot;alt&quot;:&quot;CREATE GLOBAL TEMPORARY TABLE temp_order_buffer (     order_id   NUMBER,     order_date DATE,     total      NUMBER(12, 2) ) ON COMMIT DELETE ROWS;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187139427?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F736842de-390f-4a42-8706-d39ce23d9bb0_1688x282.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE GLOBAL TEMPORARY TABLE temp_order_buffer (     order_id   NUMBER,     order_date DATE,     total      NUMBER(12, 2) ) ON COMMIT DELETE ROWS;" title="CREATE GLOBAL TEMPORARY TABLE temp_order_buffer (     order_id   NUMBER,     order_date DATE,     total      NUMBER(12, 2) ) ON COMMIT DELETE ROWS;" srcset="https://substackcdn.com/image/fetch/$s_!TNWs!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F736842de-390f-4a42-8706-d39ce23d9bb0_1688x282.png 424w, https://substackcdn.com/image/fetch/$s_!TNWs!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F736842de-390f-4a42-8706-d39ce23d9bb0_1688x282.png 848w, https://substackcdn.com/image/fetch/$s_!TNWs!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F736842de-390f-4a42-8706-d39ce23d9bb0_1688x282.png 1272w, https://substackcdn.com/image/fetch/$s_!TNWs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F736842de-390f-4a42-8706-d39ce23d9bb0_1688x282.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Rows inserted into <code>temp_order_buffer</code> vanish on commit, while the table definition stays in place for reuse. Oracle stores the data in a temporary tablespace and isolates it from permanent tables in system catalogs.</p><p>Storage layout also matters. SQL Server places all temporary tables and internal work tables inside <code>tempdb</code>, along with temporary indexes and other supporting objects. Heavy use of temporary tables places load on that database, so administrators watch its size and I/O behavior. MySQL maintains internal temporary tables that may stay in memory or spill to disk depending on size and query features, with in memory internal temporary tables using <code>TempTable</code> by default (or <code>MEMORY</code> if configured) and on disk internal temporary tables using <code>InnoDB</code>. User created temporary tables use the engine given in the <code>CREATE TEMPORARY TABLE</code> statement, or the server <code>default_tmp_storage_engine</code> setting when no engine is specified. PostgreSQL keeps temporary tables in a per session schema named <code>pg_temp</code> and manages data in its shared buffer pool and temporary files. Oracle uses temporary tablespaces that can live on separate storage from permanent datafiles, which helps balance workloads.</p><p>Lifetime then depends both on scope rules and on the storage engine. A temporary table can vanish at commit time, session end, or connection close. Some engines drop structure and data in one step, while others keep the structure in the catalog and only purge data. Applications that create many temporary tables benefit from knowing when their databases reclaim these objects so they do not accidentally keep long lived connections full of leftover scratch tables.</p><h4>Creation Syntax Across Popular Engines</h4><p>Creation syntax stays close to normal <code>CREATE TABLE</code> commands, with small twists in each engine. The main difference is how to mark a table as temporary and how much control the command grants over commit behavior.</p><p>MySQL uses the <code>TEMPORARY</code> keyword. The table definition can be written from scratch, or a query can feed the definition through <code>CREATE TEMPORARY TABLE AS SELECT</code>. Both styles appear in production code, and the choice comes down to how much control is needed over column types and indexes. Direct table definitions often look like this:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mKBH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5257e976-6024-48ef-897b-c378b5c1ae5f_1700x390.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mKBH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5257e976-6024-48ef-897b-c378b5c1ae5f_1700x390.png 424w, https://substackcdn.com/image/fetch/$s_!mKBH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5257e976-6024-48ef-897b-c378b5c1ae5f_1700x390.png 848w, https://substackcdn.com/image/fetch/$s_!mKBH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5257e976-6024-48ef-897b-c378b5c1ae5f_1700x390.png 1272w, https://substackcdn.com/image/fetch/$s_!mKBH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5257e976-6024-48ef-897b-c378b5c1ae5f_1700x390.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mKBH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5257e976-6024-48ef-897b-c378b5c1ae5f_1700x390.png" width="1456" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5257e976-6024-48ef-897b-c378b5c1ae5f_1700x390.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:79148,&quot;alt&quot;:&quot;CREATE TEMPORARY TABLE temp_customer_rollup (     customer_id   INT NOT NULL,     first_order   DATE,     last_order    DATE,     total_spent   DECIMAL(14, 2),     PRIMARY KEY (customer_id) );&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187139427?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5257e976-6024-48ef-897b-c378b5c1ae5f_1700x390.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE TEMPORARY TABLE temp_customer_rollup (     customer_id   INT NOT NULL,     first_order   DATE,     last_order    DATE,     total_spent   DECIMAL(14, 2),     PRIMARY KEY (customer_id) );" title="CREATE TEMPORARY TABLE temp_customer_rollup (     customer_id   INT NOT NULL,     first_order   DATE,     last_order    DATE,     total_spent   DECIMAL(14, 2),     PRIMARY KEY (customer_id) );" srcset="https://substackcdn.com/image/fetch/$s_!mKBH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5257e976-6024-48ef-897b-c378b5c1ae5f_1700x390.png 424w, https://substackcdn.com/image/fetch/$s_!mKBH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5257e976-6024-48ef-897b-c378b5c1ae5f_1700x390.png 848w, https://substackcdn.com/image/fetch/$s_!mKBH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5257e976-6024-48ef-897b-c378b5c1ae5f_1700x390.png 1272w, https://substackcdn.com/image/fetch/$s_!mKBH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5257e976-6024-48ef-897b-c378b5c1ae5f_1700x390.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>And a later statement can load data into that structure through an <code>INSERT</code> from a query. When column types do not need adjustments, <code>CREATE TEMPORARY TABLE</code> can be driven directly from a <code>SELECT</code>:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!v0D5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b3dccf6-2ce8-4f92-a6be-eb2672c3ac7b_1698x389.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!v0D5!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b3dccf6-2ce8-4f92-a6be-eb2672c3ac7b_1698x389.png 424w, https://substackcdn.com/image/fetch/$s_!v0D5!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b3dccf6-2ce8-4f92-a6be-eb2672c3ac7b_1698x389.png 848w, https://substackcdn.com/image/fetch/$s_!v0D5!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b3dccf6-2ce8-4f92-a6be-eb2672c3ac7b_1698x389.png 1272w, https://substackcdn.com/image/fetch/$s_!v0D5!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b3dccf6-2ce8-4f92-a6be-eb2672c3ac7b_1698x389.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!v0D5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b3dccf6-2ce8-4f92-a6be-eb2672c3ac7b_1698x389.png" width="1456" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8b3dccf6-2ce8-4f92-a6be-eb2672c3ac7b_1698x389.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:73183,&quot;alt&quot;:&quot;CREATE TEMPORARY TABLE temp_large_orders AS SELECT o.id          AS order_id,        o.customer_id,        o.order_date,        o.total FROM   orders o WHERE  o.total >= 500.00;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187139427?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b3dccf6-2ce8-4f92-a6be-eb2672c3ac7b_1698x389.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE TEMPORARY TABLE temp_large_orders AS SELECT o.id          AS order_id,        o.customer_id,        o.order_date,        o.total FROM   orders o WHERE  o.total >= 500.00;" title="CREATE TEMPORARY TABLE temp_large_orders AS SELECT o.id          AS order_id,        o.customer_id,        o.order_date,        o.total FROM   orders o WHERE  o.total >= 500.00;" srcset="https://substackcdn.com/image/fetch/$s_!v0D5!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b3dccf6-2ce8-4f92-a6be-eb2672c3ac7b_1698x389.png 424w, https://substackcdn.com/image/fetch/$s_!v0D5!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b3dccf6-2ce8-4f92-a6be-eb2672c3ac7b_1698x389.png 848w, https://substackcdn.com/image/fetch/$s_!v0D5!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b3dccf6-2ce8-4f92-a6be-eb2672c3ac7b_1698x389.png 1272w, https://substackcdn.com/image/fetch/$s_!v0D5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b3dccf6-2ce8-4f92-a6be-eb2672c3ac7b_1698x389.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>PostgreSQL uses <code>TEMP</code> or <code>TEMPORARY</code> keywords and supports the same two styles. It also adds the <code>ON COMMIT</code> options that were mentioned earlier, so structure and data lifetime are tuned in the definition. Query driven forms often look like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7Ran!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5700657c-c7f6-477e-97c0-da9a8df91f54_1653x506.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7Ran!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5700657c-c7f6-477e-97c0-da9a8df91f54_1653x506.png 424w, https://substackcdn.com/image/fetch/$s_!7Ran!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5700657c-c7f6-477e-97c0-da9a8df91f54_1653x506.png 848w, https://substackcdn.com/image/fetch/$s_!7Ran!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5700657c-c7f6-477e-97c0-da9a8df91f54_1653x506.png 1272w, https://substackcdn.com/image/fetch/$s_!7Ran!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5700657c-c7f6-477e-97c0-da9a8df91f54_1653x506.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7Ran!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5700657c-c7f6-477e-97c0-da9a8df91f54_1653x506.png" width="1456" height="446" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5700657c-c7f6-477e-97c0-da9a8df91f54_1653x506.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:446,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:73155,&quot;alt&quot;:&quot;CREATE TEMP TABLE temp_unpaid_invoices ON COMMIT DROP AS SELECT invoice_id,        customer_id,        due_date,        amount FROM   invoices WHERE  paid_at IS NULL;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187139427?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5700657c-c7f6-477e-97c0-da9a8df91f54_1653x506.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE TEMP TABLE temp_unpaid_invoices ON COMMIT DROP AS SELECT invoice_id,        customer_id,        due_date,        amount FROM   invoices WHERE  paid_at IS NULL;" title="CREATE TEMP TABLE temp_unpaid_invoices ON COMMIT DROP AS SELECT invoice_id,        customer_id,        due_date,        amount FROM   invoices WHERE  paid_at IS NULL;" srcset="https://substackcdn.com/image/fetch/$s_!7Ran!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5700657c-c7f6-477e-97c0-da9a8df91f54_1653x506.png 424w, https://substackcdn.com/image/fetch/$s_!7Ran!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5700657c-c7f6-477e-97c0-da9a8df91f54_1653x506.png 848w, https://substackcdn.com/image/fetch/$s_!7Ran!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5700657c-c7f6-477e-97c0-da9a8df91f54_1653x506.png 1272w, https://substackcdn.com/image/fetch/$s_!7Ran!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5700657c-c7f6-477e-97c0-da9a8df91f54_1653x506.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That statement creates the table, copies in the current unpaid invoices, and arranges for the table itself to disappear when the transaction commits. For longer lived scratch tables, <code>ON COMMIT PRESERVE ROWS</code> keeps data until session end. Indexes and constraints are allowed as well:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!XWK7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff073fd84-5627-4467-9698-2a44688a1f80_1697x336.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!XWK7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff073fd84-5627-4467-9698-2a44688a1f80_1697x336.png 424w, https://substackcdn.com/image/fetch/$s_!XWK7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff073fd84-5627-4467-9698-2a44688a1f80_1697x336.png 848w, https://substackcdn.com/image/fetch/$s_!XWK7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff073fd84-5627-4467-9698-2a44688a1f80_1697x336.png 1272w, https://substackcdn.com/image/fetch/$s_!XWK7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff073fd84-5627-4467-9698-2a44688a1f80_1697x336.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!XWK7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff073fd84-5627-4467-9698-2a44688a1f80_1697x336.png" width="1456" height="288" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f073fd84-5627-4467-9698-2a44688a1f80_1697x336.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:288,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:79377,&quot;alt&quot;:&quot;CREATE TEMP TABLE temp_daily_pageviews (     view_date  DATE,     page_slug  TEXT,     views      BIGINT,     PRIMARY KEY (view_date, page_slug) ) ON COMMIT PRESERVE ROWS;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187139427?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff073fd84-5627-4467-9698-2a44688a1f80_1697x336.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE TEMP TABLE temp_daily_pageviews (     view_date  DATE,     page_slug  TEXT,     views      BIGINT,     PRIMARY KEY (view_date, page_slug) ) ON COMMIT PRESERVE ROWS;" title="CREATE TEMP TABLE temp_daily_pageviews (     view_date  DATE,     page_slug  TEXT,     views      BIGINT,     PRIMARY KEY (view_date, page_slug) ) ON COMMIT PRESERVE ROWS;" srcset="https://substackcdn.com/image/fetch/$s_!XWK7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff073fd84-5627-4467-9698-2a44688a1f80_1697x336.png 424w, https://substackcdn.com/image/fetch/$s_!XWK7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff073fd84-5627-4467-9698-2a44688a1f80_1697x336.png 848w, https://substackcdn.com/image/fetch/$s_!XWK7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff073fd84-5627-4467-9698-2a44688a1f80_1697x336.png 1272w, https://substackcdn.com/image/fetch/$s_!XWK7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff073fd84-5627-4467-9698-2a44688a1f80_1697x336.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>SQL Server uses regular <code>CREATE TABLE</code> syntax, with the table name carrying all the temporary behavior through the hash prefix. Small differences still appear, such as the database automatically placing the object in <code>tempdb</code> and adding an internal suffix to guarantee uniqueness. Many scripts start with a definition and follow with an insert:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PZDI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdff341c-7ddb-4888-8790-9d07332e2f74_1666x728.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PZDI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdff341c-7ddb-4888-8790-9d07332e2f74_1666x728.png 424w, https://substackcdn.com/image/fetch/$s_!PZDI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdff341c-7ddb-4888-8790-9d07332e2f74_1666x728.png 848w, https://substackcdn.com/image/fetch/$s_!PZDI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdff341c-7ddb-4888-8790-9d07332e2f74_1666x728.png 1272w, https://substackcdn.com/image/fetch/$s_!PZDI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdff341c-7ddb-4888-8790-9d07332e2f74_1666x728.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PZDI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdff341c-7ddb-4888-8790-9d07332e2f74_1666x728.png" width="1456" height="636" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bdff341c-7ddb-4888-8790-9d07332e2f74_1666x728.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:636,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:146051,&quot;alt&quot;:&quot;CREATE TABLE #session_events (     event_id     INT IDENTITY(1, 1) PRIMARY KEY,     created_at   DATETIME2,     user_id      INT,     event_type   NVARCHAR(50) );  INSERT INTO #session_events (created_at, user_id, event_type) SELECT created_at,        user_id,        event_type FROM   dbo.AuditLog WHERE  created_at >= DATEADD(hour, -1, SYSUTCDATETIME());&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187139427?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdff341c-7ddb-4888-8790-9d07332e2f74_1666x728.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE TABLE #session_events (     event_id     INT IDENTITY(1, 1) PRIMARY KEY,     created_at   DATETIME2,     user_id      INT,     event_type   NVARCHAR(50) );  INSERT INTO #session_events (created_at, user_id, event_type) SELECT created_at,        user_id,        event_type FROM   dbo.AuditLog WHERE  created_at >= DATEADD(hour, -1, SYSUTCDATETIME());" title="CREATE TABLE #session_events (     event_id     INT IDENTITY(1, 1) PRIMARY KEY,     created_at   DATETIME2,     user_id      INT,     event_type   NVARCHAR(50) );  INSERT INTO #session_events (created_at, user_id, event_type) SELECT created_at,        user_id,        event_type FROM   dbo.AuditLog WHERE  created_at >= DATEADD(hour, -1, SYSUTCDATETIME());" srcset="https://substackcdn.com/image/fetch/$s_!PZDI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdff341c-7ddb-4888-8790-9d07332e2f74_1666x728.png 424w, https://substackcdn.com/image/fetch/$s_!PZDI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdff341c-7ddb-4888-8790-9d07332e2f74_1666x728.png 848w, https://substackcdn.com/image/fetch/$s_!PZDI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdff341c-7ddb-4888-8790-9d07332e2f74_1666x728.png 1272w, https://substackcdn.com/image/fetch/$s_!PZDI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbdff341c-7ddb-4888-8790-9d07332e2f74_1666x728.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The <code>#session_events</code> table is session scoped. Statements run through <code>sp_executesql</code> in the same session can read it when it was created in the outer batch. If a <code>#</code> table is created inside a dynamic SQL string, later outer statements usually won&#8217;t see it, so scripts normally create the table first and then fill it from dynamic SQL when needed. A similar form with <code>##session_events</code> builds a global temporary table that any connection can read while it exists.</p><p>Oracle treats global temporary tables more like regular tables that just happen to store transient data. Definitions are created once and kept in the schema. Sessions reuse them in the same way they reuse permanent tables, but Oracle quietly isolates rows per session. This definition is common to see in practice:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!uK9J!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9b05d80-d5db-4494-be3a-1aa9445c8bd3_1691x336.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!uK9J!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9b05d80-d5db-4494-be3a-1aa9445c8bd3_1691x336.png 424w, https://substackcdn.com/image/fetch/$s_!uK9J!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9b05d80-d5db-4494-be3a-1aa9445c8bd3_1691x336.png 848w, https://substackcdn.com/image/fetch/$s_!uK9J!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9b05d80-d5db-4494-be3a-1aa9445c8bd3_1691x336.png 1272w, https://substackcdn.com/image/fetch/$s_!uK9J!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9b05d80-d5db-4494-be3a-1aa9445c8bd3_1691x336.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!uK9J!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9b05d80-d5db-4494-be3a-1aa9445c8bd3_1691x336.png" width="1456" height="289" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d9b05d80-d5db-4494-be3a-1aa9445c8bd3_1691x336.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:289,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:80179,&quot;alt&quot;:&quot;CREATE GLOBAL TEMPORARY TABLE temp_session_payments (     payment_id   NUMBER,     customer_id  NUMBER,     paid_at      DATE,     amount       NUMBER(12, 2) ) ON COMMIT PRESERVE ROWS;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187139427?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9b05d80-d5db-4494-be3a-1aa9445c8bd3_1691x336.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE GLOBAL TEMPORARY TABLE temp_session_payments (     payment_id   NUMBER,     customer_id  NUMBER,     paid_at      DATE,     amount       NUMBER(12, 2) ) ON COMMIT PRESERVE ROWS;" title="CREATE GLOBAL TEMPORARY TABLE temp_session_payments (     payment_id   NUMBER,     customer_id  NUMBER,     paid_at      DATE,     amount       NUMBER(12, 2) ) ON COMMIT PRESERVE ROWS;" srcset="https://substackcdn.com/image/fetch/$s_!uK9J!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9b05d80-d5db-4494-be3a-1aa9445c8bd3_1691x336.png 424w, https://substackcdn.com/image/fetch/$s_!uK9J!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9b05d80-d5db-4494-be3a-1aa9445c8bd3_1691x336.png 848w, https://substackcdn.com/image/fetch/$s_!uK9J!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9b05d80-d5db-4494-be3a-1aa9445c8bd3_1691x336.png 1272w, https://substackcdn.com/image/fetch/$s_!uK9J!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9b05d80-d5db-4494-be3a-1aa9445c8bd3_1691x336.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Later sessions run <code>INSERT</code> and <code>SELECT</code> against <code>temp_session_payments</code> as needed, and the database handles lifecycle of the rows based on the <code>ON COMMIT</code> clause and session end. Data dictionaries will list this table alongside permanent tables, although the underlying storage is managed in temporary tablespaces.</p><p>Throughout these engines, the <code>CREATE</code> syntax for temporary tables stays close to permanent table definitions, which keeps the learning curve gentle. The main differences lie in keywords, naming conventions, and commit options, while the mechanics of column definitions, constraints, indexes, and insert statements follow familiar rules.</p><h3>Practical Workflows With Temporary Tables</h3><p>Temporary tables turn long, tangled queries into a series of shorter steps that are easier to reason about. Instead of cramming every join, filter, and aggregation into one statement, the work can be broken into phases that pass data through temporary tables. That same idea applies to reports, stored procedures, and ad hoc analysis in query tools, with each engine bringing its own syntax but following the same broad workflow.</p><h4>Breaking Large Reports Into Stages</h4><p>Large reporting queries tend to do several things within a single statement. Monthly revenue reports, for instance, may filter by date, apply currency conversion, join to customer data, and aggregate by region and product category. When all of that lives in one <code>SELECT</code>, small changes become hard to manage. Temporary tables provide a scratch area where each stage can store its output before the next stage picks it up.</p><p>One way to handle this in PostgreSQL is to create a temporary table for the filtered and normalized base set, then another for enriched data, then run the final aggregation. The first step can filter sales and handle currency conversion:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ICcA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ddf07ac-d44b-43cf-941b-253fc0a2cd7c_1600x462.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ICcA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ddf07ac-d44b-43cf-941b-253fc0a2cd7c_1600x462.png 424w, https://substackcdn.com/image/fetch/$s_!ICcA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ddf07ac-d44b-43cf-941b-253fc0a2cd7c_1600x462.png 848w, https://substackcdn.com/image/fetch/$s_!ICcA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ddf07ac-d44b-43cf-941b-253fc0a2cd7c_1600x462.png 1272w, https://substackcdn.com/image/fetch/$s_!ICcA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ddf07ac-d44b-43cf-941b-253fc0a2cd7c_1600x462.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ICcA!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ddf07ac-d44b-43cf-941b-253fc0a2cd7c_1600x462.png" width="990" height="285.5769230769231" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1ddf07ac-d44b-43cf-941b-253fc0a2cd7c_1600x462.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:420,&quot;width&quot;:1456,&quot;resizeWidth&quot;:990,&quot;bytes&quot;:93747,&quot;alt&quot;:&quot;CREATE TEMP TABLE temp_filtered_sales AS SELECT s.id,        s.customer_id,        s.product_id,        s.sale_date,        s.amount * fx.rate AS amount_usd FROM   sales s JOIN   fx_rates fx   ON   fx.currency = s.currency  AND   fx.rate_date = s.sale_date WHERE  s.sale_date BETWEEN DATE '2026-01-01' AND DATE '2026-01-31';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187139427?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ddf07ac-d44b-43cf-941b-253fc0a2cd7c_1600x462.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="CREATE TEMP TABLE temp_filtered_sales AS SELECT s.id,        s.customer_id,        s.product_id,        s.sale_date,        s.amount * fx.rate AS amount_usd FROM   sales s JOIN   fx_rates fx   ON   fx.currency = s.currency  AND   fx.rate_date = s.sale_date WHERE  s.sale_date BETWEEN DATE '2026-01-01' AND DATE '2026-01-31';" title="CREATE TEMP TABLE temp_filtered_sales AS SELECT s.id,        s.customer_id,        s.product_id,        s.sale_date,        s.amount * fx.rate AS amount_usd FROM   sales s JOIN   fx_rates fx   ON   fx.currency = s.currency  AND   fx.rate_date = s.sale_date WHERE  s.sale_date BETWEEN DATE '2026-01-01' AND DATE '2026-01-31';" srcset="https://substackcdn.com/image/fetch/$s_!ICcA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ddf07ac-d44b-43cf-941b-253fc0a2cd7c_1600x462.png 424w, https://substackcdn.com/image/fetch/$s_!ICcA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ddf07ac-d44b-43cf-941b-253fc0a2cd7c_1600x462.png 848w, https://substackcdn.com/image/fetch/$s_!ICcA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ddf07ac-d44b-43cf-941b-253fc0a2cd7c_1600x462.png 1272w, https://substackcdn.com/image/fetch/$s_!ICcA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ddf07ac-d44b-43cf-941b-253fc0a2cd7c_1600x462.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This temporary table now holds only the rows and columns needed for the report, with values already converted to a single currency. Later steps no longer need to repeat conversion logic or scan dates outside that window.</p><p>The next step can enrich those rows with dimension data, such as customer region and product line:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!HDSO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F333959d7-0801-4b4e-b5bc-7bcd453edca8_1654x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HDSO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F333959d7-0801-4b4e-b5bc-7bcd453edca8_1654x559.png 424w, https://substackcdn.com/image/fetch/$s_!HDSO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F333959d7-0801-4b4e-b5bc-7bcd453edca8_1654x559.png 848w, https://substackcdn.com/image/fetch/$s_!HDSO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F333959d7-0801-4b4e-b5bc-7bcd453edca8_1654x559.png 1272w, https://substackcdn.com/image/fetch/$s_!HDSO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F333959d7-0801-4b4e-b5bc-7bcd453edca8_1654x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!HDSO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F333959d7-0801-4b4e-b5bc-7bcd453edca8_1654x559.png" width="1456" height="492" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/333959d7-0801-4b4e-b5bc-7bcd453edca8_1654x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:492,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:101030,&quot;alt&quot;:&quot;CREATE TEMP TABLE temp_enriched_sales AS SELECT f.sale_date,        c.region,        p.product_line,        f.amount_usd FROM   temp_filtered_sales f JOIN   customers c   ON   c.id = f.customer_id JOIN   products p   ON   p.id = f.product_id;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187139427?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F333959d7-0801-4b4e-b5bc-7bcd453edca8_1654x559.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE TEMP TABLE temp_enriched_sales AS SELECT f.sale_date,        c.region,        p.product_line,        f.amount_usd FROM   temp_filtered_sales f JOIN   customers c   ON   c.id = f.customer_id JOIN   products p   ON   p.id = f.product_id;" title="CREATE TEMP TABLE temp_enriched_sales AS SELECT f.sale_date,        c.region,        p.product_line,        f.amount_usd FROM   temp_filtered_sales f JOIN   customers c   ON   c.id = f.customer_id JOIN   products p   ON   p.id = f.product_id;" srcset="https://substackcdn.com/image/fetch/$s_!HDSO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F333959d7-0801-4b4e-b5bc-7bcd453edca8_1654x559.png 424w, https://substackcdn.com/image/fetch/$s_!HDSO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F333959d7-0801-4b4e-b5bc-7bcd453edca8_1654x559.png 848w, https://substackcdn.com/image/fetch/$s_!HDSO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F333959d7-0801-4b4e-b5bc-7bcd453edca8_1654x559.png 1272w, https://substackcdn.com/image/fetch/$s_!HDSO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F333959d7-0801-4b4e-b5bc-7bcd453edca8_1654x559.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>At this point, the <code>temp_enriched_sales</code> table contains data that is ready to aggregate in different ways. One query can group by region and product line for a high level view:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Q6lV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F947b644e-7edd-4066-bf12-423cd4d11b9c_1668x336.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Q6lV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F947b644e-7edd-4066-bf12-423cd4d11b9c_1668x336.png 424w, https://substackcdn.com/image/fetch/$s_!Q6lV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F947b644e-7edd-4066-bf12-423cd4d11b9c_1668x336.png 848w, https://substackcdn.com/image/fetch/$s_!Q6lV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F947b644e-7edd-4066-bf12-423cd4d11b9c_1668x336.png 1272w, https://substackcdn.com/image/fetch/$s_!Q6lV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F947b644e-7edd-4066-bf12-423cd4d11b9c_1668x336.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Q6lV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F947b644e-7edd-4066-bf12-423cd4d11b9c_1668x336.png" width="1456" height="293" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/947b644e-7edd-4066-bf12-423cd4d11b9c_1668x336.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:293,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:76061,&quot;alt&quot;:&quot;SELECT region,        product_line,        SUM(amount_usd) AS total_amount_usd FROM   temp_enriched_sales GROUP  BY region, product_line ORDER  BY region, product_line;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187139427?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F947b644e-7edd-4066-bf12-423cd4d11b9c_1668x336.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT region,        product_line,        SUM(amount_usd) AS total_amount_usd FROM   temp_enriched_sales GROUP  BY region, product_line ORDER  BY region, product_line;" title="SELECT region,        product_line,        SUM(amount_usd) AS total_amount_usd FROM   temp_enriched_sales GROUP  BY region, product_line ORDER  BY region, product_line;" srcset="https://substackcdn.com/image/fetch/$s_!Q6lV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F947b644e-7edd-4066-bf12-423cd4d11b9c_1668x336.png 424w, https://substackcdn.com/image/fetch/$s_!Q6lV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F947b644e-7edd-4066-bf12-423cd4d11b9c_1668x336.png 848w, https://substackcdn.com/image/fetch/$s_!Q6lV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F947b644e-7edd-4066-bf12-423cd4d11b9c_1668x336.png 1272w, https://substackcdn.com/image/fetch/$s_!Q6lV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F947b644e-7edd-4066-bf12-423cd4d11b9c_1668x336.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Different queries in the same session can reuse <code>temp_enriched_sales</code> to focus on a single region or compare a few product lines without rejoining everything. Temporary tables give the report a middle layer that can support several final queries that share the same prepared data.</p><p>The same idea applies in MySQL with <code>CREATE TEMPORARY TABLE</code> and a query driven form. One filtering and conversion step can look like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!T1Hk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1458ecae-0d02-4811-b580-b6d84757869c_1662x673.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!T1Hk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1458ecae-0d02-4811-b580-b6d84757869c_1662x673.png 424w, https://substackcdn.com/image/fetch/$s_!T1Hk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1458ecae-0d02-4811-b580-b6d84757869c_1662x673.png 848w, https://substackcdn.com/image/fetch/$s_!T1Hk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1458ecae-0d02-4811-b580-b6d84757869c_1662x673.png 1272w, https://substackcdn.com/image/fetch/$s_!T1Hk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1458ecae-0d02-4811-b580-b6d84757869c_1662x673.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!T1Hk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1458ecae-0d02-4811-b580-b6d84757869c_1662x673.png" width="1456" height="590" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1458ecae-0d02-4811-b580-b6d84757869c_1662x673.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:590,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:136940,&quot;alt&quot;:&quot;CREATE TEMPORARY TABLE temp_january_sales AS SELECT s.id,        s.customer_id,        s.product_id,        s.sale_date,        s.amount * fx.rate AS amount_usd FROM   sales s JOIN   fx_rates fx   ON   fx.currency = s.currency  AND   fx.rate_date = s.sale_date WHERE  s.sale_date >= '2026-01-01'   AND  s.sale_date <  '2026-02-01';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187139427?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1458ecae-0d02-4811-b580-b6d84757869c_1662x673.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE TEMPORARY TABLE temp_january_sales AS SELECT s.id,        s.customer_id,        s.product_id,        s.sale_date,        s.amount * fx.rate AS amount_usd FROM   sales s JOIN   fx_rates fx   ON   fx.currency = s.currency  AND   fx.rate_date = s.sale_date WHERE  s.sale_date >= '2026-01-01'   AND  s.sale_date <  '2026-02-01';" title="CREATE TEMPORARY TABLE temp_january_sales AS SELECT s.id,        s.customer_id,        s.product_id,        s.sale_date,        s.amount * fx.rate AS amount_usd FROM   sales s JOIN   fx_rates fx   ON   fx.currency = s.currency  AND   fx.rate_date = s.sale_date WHERE  s.sale_date >= '2026-01-01'   AND  s.sale_date <  '2026-02-01';" srcset="https://substackcdn.com/image/fetch/$s_!T1Hk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1458ecae-0d02-4811-b580-b6d84757869c_1662x673.png 424w, https://substackcdn.com/image/fetch/$s_!T1Hk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1458ecae-0d02-4811-b580-b6d84757869c_1662x673.png 848w, https://substackcdn.com/image/fetch/$s_!T1Hk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1458ecae-0d02-4811-b580-b6d84757869c_1662x673.png 1272w, https://substackcdn.com/image/fetch/$s_!T1Hk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1458ecae-0d02-4811-b580-b6d84757869c_1662x673.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Later statements in that session can join <code>temp_january_sales</code> to customer and product tables, or aggregate by different groupings, without scanning unrelated months again. Each engine handles the storage of the temporary data in its own layer, but the workflow of breaking the report into stages looks similar across platforms.</p><h4>Passing Intermediate Results Between Steps</h4><p>Workflows that involve several statements need a way to hand results from one statement to another without materializing permanent tables. Temporary tables are well suited for that handoff because they survive across statements in the same session and then vanish when the work is done.</p><p>Stored procedures in SQL Server use this frequently when several queries share the same filtered base set. Suppose a reporting procedure needs to filter a large audit log down to one customer for a date range, then use that same subset to produce multiple summaries. In that case, a temporary table fits right in:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UQnB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c01335a-72d1-495f-a310-234ae405eccd_1602x919.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UQnB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c01335a-72d1-495f-a310-234ae405eccd_1602x919.png 424w, https://substackcdn.com/image/fetch/$s_!UQnB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c01335a-72d1-495f-a310-234ae405eccd_1602x919.png 848w, https://substackcdn.com/image/fetch/$s_!UQnB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c01335a-72d1-495f-a310-234ae405eccd_1602x919.png 1272w, https://substackcdn.com/image/fetch/$s_!UQnB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c01335a-72d1-495f-a310-234ae405eccd_1602x919.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UQnB!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c01335a-72d1-495f-a310-234ae405eccd_1602x919.png" width="910" height="521.875" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8c01335a-72d1-495f-a310-234ae405eccd_1602x919.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:835,&quot;width&quot;:1456,&quot;resizeWidth&quot;:910,&quot;bytes&quot;:182850,&quot;alt&quot;:&quot;CREATE OR ALTER PROCEDURE dbo.ReportCustomerActivity     @customer_id  INT,     @start_date   DATE,     @end_date     DATE AS BEGIN     SET NOCOUNT ON;      CREATE TABLE #base_events (         event_id     INT IDENTITY(1, 1) PRIMARY KEY,         event_time   DATETIME2,         event_type   NVARCHAR(50),         metadata     NVARCHAR(4000)     );      INSERT INTO #base_events (event_time, event_type, metadata)     SELECT event_time,            event_type,            metadata     FROM   dbo.AuditLog     WHERE  customer_id = @customer_id       AND  event_time  >= @start_date       AND  event_time  <  DATEADD(day, 1, @end_date);&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187139427?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c01335a-72d1-495f-a310-234ae405eccd_1602x919.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="CREATE OR ALTER PROCEDURE dbo.ReportCustomerActivity     @customer_id  INT,     @start_date   DATE,     @end_date     DATE AS BEGIN     SET NOCOUNT ON;      CREATE TABLE #base_events (         event_id     INT IDENTITY(1, 1) PRIMARY KEY,         event_time   DATETIME2,         event_type   NVARCHAR(50),         metadata     NVARCHAR(4000)     );      INSERT INTO #base_events (event_time, event_type, metadata)     SELECT event_time,            event_type,            metadata     FROM   dbo.AuditLog     WHERE  customer_id = @customer_id       AND  event_time  >= @start_date       AND  event_time  <  DATEADD(day, 1, @end_date);" title="CREATE OR ALTER PROCEDURE dbo.ReportCustomerActivity     @customer_id  INT,     @start_date   DATE,     @end_date     DATE AS BEGIN     SET NOCOUNT ON;      CREATE TABLE #base_events (         event_id     INT IDENTITY(1, 1) PRIMARY KEY,         event_time   DATETIME2,         event_type   NVARCHAR(50),         metadata     NVARCHAR(4000)     );      INSERT INTO #base_events (event_time, event_type, metadata)     SELECT event_time,            event_type,            metadata     FROM   dbo.AuditLog     WHERE  customer_id = @customer_id       AND  event_time  >= @start_date       AND  event_time  <  DATEADD(day, 1, @end_date);" srcset="https://substackcdn.com/image/fetch/$s_!UQnB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c01335a-72d1-495f-a310-234ae405eccd_1602x919.png 424w, https://substackcdn.com/image/fetch/$s_!UQnB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c01335a-72d1-495f-a310-234ae405eccd_1602x919.png 848w, https://substackcdn.com/image/fetch/$s_!UQnB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c01335a-72d1-495f-a310-234ae405eccd_1602x919.png 1272w, https://substackcdn.com/image/fetch/$s_!UQnB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c01335a-72d1-495f-a310-234ae405eccd_1602x919.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The temporary table <code>#base_events</code> now carries only the rows relevant to that customer and date window. Any later query inside the procedure can reference it without repeating the filters on <code>AuditLog</code>.</p><p>Later, a step can summarize activity by day:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PP0p!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F795b30f3-324f-4e9d-9921-275ed65bda5b_1684x279.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PP0p!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F795b30f3-324f-4e9d-9921-275ed65bda5b_1684x279.png 424w, https://substackcdn.com/image/fetch/$s_!PP0p!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F795b30f3-324f-4e9d-9921-275ed65bda5b_1684x279.png 848w, https://substackcdn.com/image/fetch/$s_!PP0p!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F795b30f3-324f-4e9d-9921-275ed65bda5b_1684x279.png 1272w, https://substackcdn.com/image/fetch/$s_!PP0p!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F795b30f3-324f-4e9d-9921-275ed65bda5b_1684x279.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PP0p!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F795b30f3-324f-4e9d-9921-275ed65bda5b_1684x279.png" width="1456" height="241" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/795b30f3-324f-4e9d-9921-275ed65bda5b_1684x279.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:241,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:75288,&quot;alt&quot;:&quot;SELECT CAST(event_time AS DATE) AS event_date,            COUNT(*)                 AS event_count     FROM   #base_events     GROUP  BY CAST(event_time AS DATE)     ORDER  BY event_date;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187139427?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F795b30f3-324f-4e9d-9921-275ed65bda5b_1684x279.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT CAST(event_time AS DATE) AS event_date,            COUNT(*)                 AS event_count     FROM   #base_events     GROUP  BY CAST(event_time AS DATE)     ORDER  BY event_date;" title="SELECT CAST(event_time AS DATE) AS event_date,            COUNT(*)                 AS event_count     FROM   #base_events     GROUP  BY CAST(event_time AS DATE)     ORDER  BY event_date;" srcset="https://substackcdn.com/image/fetch/$s_!PP0p!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F795b30f3-324f-4e9d-9921-275ed65bda5b_1684x279.png 424w, https://substackcdn.com/image/fetch/$s_!PP0p!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F795b30f3-324f-4e9d-9921-275ed65bda5b_1684x279.png 848w, https://substackcdn.com/image/fetch/$s_!PP0p!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F795b30f3-324f-4e9d-9921-275ed65bda5b_1684x279.png 1272w, https://substackcdn.com/image/fetch/$s_!PP0p!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F795b30f3-324f-4e9d-9921-275ed65bda5b_1684x279.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The next query in the same procedure can focus on event types:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PTr_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e6890a7-9ca2-4509-a809-e431a5d35b88_1688x282.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PTr_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e6890a7-9ca2-4509-a809-e431a5d35b88_1688x282.png 424w, https://substackcdn.com/image/fetch/$s_!PTr_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e6890a7-9ca2-4509-a809-e431a5d35b88_1688x282.png 848w, https://substackcdn.com/image/fetch/$s_!PTr_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e6890a7-9ca2-4509-a809-e431a5d35b88_1688x282.png 1272w, https://substackcdn.com/image/fetch/$s_!PTr_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e6890a7-9ca2-4509-a809-e431a5d35b88_1688x282.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PTr_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e6890a7-9ca2-4509-a809-e431a5d35b88_1688x282.png" width="1456" height="243" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1e6890a7-9ca2-4509-a809-e431a5d35b88_1688x282.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:243,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:60155,&quot;alt&quot;:&quot;SELECT event_type,            COUNT(*) AS event_count     FROM   #base_events     GROUP  BY event_type     ORDER  BY event_count DESC;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187139427?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e6890a7-9ca2-4509-a809-e431a5d35b88_1688x282.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT event_type,            COUNT(*) AS event_count     FROM   #base_events     GROUP  BY event_type     ORDER  BY event_count DESC;" title="SELECT event_type,            COUNT(*) AS event_count     FROM   #base_events     GROUP  BY event_type     ORDER  BY event_count DESC;" srcset="https://substackcdn.com/image/fetch/$s_!PTr_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e6890a7-9ca2-4509-a809-e431a5d35b88_1688x282.png 424w, https://substackcdn.com/image/fetch/$s_!PTr_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e6890a7-9ca2-4509-a809-e431a5d35b88_1688x282.png 848w, https://substackcdn.com/image/fetch/$s_!PTr_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e6890a7-9ca2-4509-a809-e431a5d35b88_1688x282.png 1272w, https://substackcdn.com/image/fetch/$s_!PTr_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e6890a7-9ca2-4509-a809-e431a5d35b88_1688x282.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Both summaries reuse the same filtered base set, which cuts down on repeated work and keeps the procedure logic easier to read. At the end of the procedure the temporary table can be dropped to keep the connection free of leftover objects:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!E5MW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3cbc536-bb82-474f-98a5-f784513e6ddb_1695x110.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!E5MW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3cbc536-bb82-474f-98a5-f784513e6ddb_1695x110.png 424w, https://substackcdn.com/image/fetch/$s_!E5MW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3cbc536-bb82-474f-98a5-f784513e6ddb_1695x110.png 848w, https://substackcdn.com/image/fetch/$s_!E5MW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3cbc536-bb82-474f-98a5-f784513e6ddb_1695x110.png 1272w, https://substackcdn.com/image/fetch/$s_!E5MW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3cbc536-bb82-474f-98a5-f784513e6ddb_1695x110.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!E5MW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3cbc536-bb82-474f-98a5-f784513e6ddb_1695x110.png" width="1456" height="94" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d3cbc536-bb82-474f-98a5-f784513e6ddb_1695x110.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:94,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:15963,&quot;alt&quot;:&quot;DROP TABLE #base_events; END;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/187139427?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3cbc536-bb82-474f-98a5-f784513e6ddb_1695x110.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="DROP TABLE #base_events; END;" title="DROP TABLE #base_events; END;" srcset="https://substackcdn.com/image/fetch/$s_!E5MW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3cbc536-bb82-474f-98a5-f784513e6ddb_1695x110.png 424w, https://substackcdn.com/image/fetch/$s_!E5MW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3cbc536-bb82-474f-98a5-f784513e6ddb_1695x110.png 848w, https://substackcdn.com/image/fetch/$s_!E5MW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3cbc536-bb82-474f-98a5-f784513e6ddb_1695x110.png 1272w, https://substackcdn.com/image/fetch/$s_!E5MW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3cbc536-bb82-474f-98a5-f784513e6ddb_1695x110.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>MySQL routines can adopt a similar practice with <code>CREATE TEMPORARY TABLE</code>. The session can call a procedure that creates a temporary table at the start, populates it, then runs several queries against it. That table stays private to the connection that owns the procedure call, and MySQL removes it when the session finishes, so the procedure body does not need permanent staging tables for intermediate steps.</p><p>PostgreSQL functions can also lean on temporary tables, although many workflows there prefer common table expressions or materialized views. When a function needs to write and read the same intermediate data across multiple statements, a temporary table in the same session still provides that bridge. After creation, the table sits in the <code>pg_temp</code> schema for that session, and later queries in the function can refer to it directly until the function drops it or the session ends.</p><h4>Dropping Temporary Tables At The End</h4><p>Automatic cleanup rules for temporary tables handle many cases, but explicit drops still help keep long lived sessions tidy and avoid surprises in shared environments. Connection pools in application servers are a good example. One pooled connection can serve many different requests over time, and each request may create its own temporary tables. Dropping those tables at the end of a workflow frees internal resources and reduces the risk of name clashes when another request runs later on the same connection.</p><p>Development and query tools benefit from explicit cleanup as well. When a script is run repeatedly in a tool like psql, sqlcmd, or a graphical client, leftover temporary tables from a previous run can cause errors if the script tries to create a table with the same name again. Adding <code>DROP</code> statements near the end of the script, or guarded drops before the <code>CREATE</code> statements, keeps the session in a known state and removes the need for manual cleanup between runs.</p><p>Different engines offer their own syntax for drop operations. MySQL supports <code>DROP TEMPORARY TABLE</code> and an <code>IF EXISTS</code> option so that scripts can remove tables without raising errors if those tables have already gone away. PostgreSQL uses standard <code>DROP TABLE</code>, also with <code>IF EXISTS</code>, and applies it to temporary tables in the same session in the same way as permanent ones. SQL Server supports <code>DROP TABLE</code> for <code>#</code> and <code>##</code> tables and lets scripts guard the operation with <code>DROP TABLE IF EXISTS</code>. Oracle global temporary tables can be cleared with <code>TRUNCATE TABLE</code> when the structure should remain for reuse, or removed with <code>DROP TABLE</code> when the definition should go away as well.</p><p>Global temporary tables in SQL Server deserve special attention because they are visible across connections while any connection still references them. If <code>DROP TABLE ##scratchpad</code> is forgotten, shared staging data can remain in place and confuse later sessions that read from that name. Dropping global temporary tables as soon as shared work finishes prevents that bleed over and keeps the shared namespace predictable.</p><p>Temporary tables give SQL workflows a flexible middle ground between one shot result sets and permanent schema, but they share resources with other parts of the database engine. Creating them thoughtfully and dropping them when their job is done keeps those workflows efficient and avoids side effects in pooled or long running sessions.</p><h3>Conclusion</h3><p>Temporary tables give SQL developers controlled, short lived storage between single query results and permanent schema, with scope and lifetime governed by engine specific rules. MySQL and PostgreSQL attach temporary tables to sessions, SQL Server binds local and global temporary tables to connections through hash prefixed names in tempdb, and Oracle treats global temporary tables as permanent definitions with transient per session rows. In all of these engines, temporary tables rely on standard <code>CREATE</code> and <code>DROP</code> statements while acting as a staging layer where intermediate rows are written, queried by later statements in the same session, and then removed through automatic cleanup or explicit drop commands.</p><ol><li><p><em><a href="https://dev.mysql.com/doc/refman/8.4/en/create-temporary-table.html">MySQL CREATE TEMPORARY TABLE Documentation</a></em></p></li><li><p><em><a href="https://www.postgresql.org/docs/current/sql-createtable.html">PostgreSQL CREATE TABLE Temporary Relations</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-transact-sql">SQL Server CREATE TABLE And Temporary Tables</a></em></p></li><li><p><em><a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-TABLE.html">Oracle Global Temporary Tables Reference</a></em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share&quot;,&quot;text&quot;:&quot;Share Alexander Obregon's Substack&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share"><span>Share Alexander Obregon's Substack</span></a></p></li></ol><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qTg9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff34ca62-618f-4bc5-961e-c07f881ca478_306x306.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qTg9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff34ca62-618f-4bc5-961e-c07f881ca478_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!qTg9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff34ca62-618f-4bc5-961e-c07f881ca478_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!qTg9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff34ca62-618f-4bc5-961e-c07f881ca478_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!qTg9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff34ca62-618f-4bc5-961e-c07f881ca478_306x306.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qTg9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff34ca62-618f-4bc5-961e-c07f881ca478_306x306.png" width="306" height="306" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ff34ca62-618f-4bc5-961e-c07f881ca478_306x306.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:306,&quot;width&quot;:306,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!qTg9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff34ca62-618f-4bc5-961e-c07f881ca478_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!qTg9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff34ca62-618f-4bc5-961e-c07f881ca478_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!qTg9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff34ca62-618f-4bc5-961e-c07f881ca478_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!qTg9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff34ca62-618f-4bc5-961e-c07f881ca478_306x306.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://freesvg.org/mono-sql">Image Source</a></figcaption></figure></div>]]></content:encoded></item><item><title><![CDATA[Row Level Security Rules In SQL]]></title><description><![CDATA[RLS, or Row level security, lets a database keep many users or tenants in the same tables while still restricting which rows each session can read or change.]]></description><link>https://alexanderobregon.substack.com/p/row-level-security-rules-in-sql</link><guid isPermaLink="false">https://alexanderobregon.substack.com/p/row-level-security-rules-in-sql</guid><dc:creator><![CDATA[Alexander Obregon]]></dc:creator><pubDate>Mon, 09 Feb 2026 18:19:04 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!mqhx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe704a91f-8bb4-4989-8019-87b41b4b83a0_306x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Bfq6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70ef5bf4-fa3b-4c0f-85fd-ab4a3138e030_800x373.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Bfq6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70ef5bf4-fa3b-4c0f-85fd-ab4a3138e030_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!Bfq6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70ef5bf4-fa3b-4c0f-85fd-ab4a3138e030_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!Bfq6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70ef5bf4-fa3b-4c0f-85fd-ab4a3138e030_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!Bfq6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70ef5bf4-fa3b-4c0f-85fd-ab4a3138e030_800x373.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Bfq6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70ef5bf4-fa3b-4c0f-85fd-ab4a3138e030_800x373.png" width="800" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/70ef5bf4-fa3b-4c0f-85fd-ab4a3138e030_800x373.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Bfq6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70ef5bf4-fa3b-4c0f-85fd-ab4a3138e030_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!Bfq6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70ef5bf4-fa3b-4c0f-85fd-ab4a3138e030_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!Bfq6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70ef5bf4-fa3b-4c0f-85fd-ab4a3138e030_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!Bfq6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70ef5bf4-fa3b-4c0f-85fd-ab4a3138e030_800x373.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://commons.wikimedia.org/wiki/File:Sql_data_base_with_logo.png">Image Source</a></figcaption></figure></div><p>RLS, or Row level security, lets a database keep many users or tenants in the same tables while still restricting which rows each session can read or change. Instead of putting every access rule in application code, the database applies extra predicates whenever a query touches a protected table. That arrangement creates a single place for row visibility rules, which matters for multi tenant products, compliance requirements, and shared reporting environments.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>Row Level Security Basics</h3><p>Modern database servers started with permissions that act at the level of whole tables, views, and schemas. Row level security adds an extra check so the server can decide not only which table a session may query but also which individual rows stay visible for that session. RLS can keep several tenants or groups inside shared tables while still keeping their data separated from one another, even when every query flows through the same application logic. Traditional privilege systems answer questions like &#8220;can this login run SELECT on table orders&#8221;. Row level security adds a second question about row visibility that runs after normal privileges pass. Any session that has table level SELECT still only sees rows that satisfy the row security predicate. That extra filter comes from policy rules that reference both columns on the table and information about the current user or tenant.</p><h4>What Row Level Security Does</h4><p>Many developers first meet row level security while working on a multi tenant product where every tenant has its own users, orders, and reports, but all of that data lives in shared tables. Granting plain SELECT on those tables would expose every row to anyone with that permission, so the database needs a rule that trims the result set for each login without asking every query author to remember the right WHERE clause.</p><p>Standard SQL permissions treat table access as all or nothing. Either the session has SELECT on a table or it does not, and the server does not inspect which specific rows belong to that caller. With row level security turned on for a table, the server evaluates a policy whenever a statement touches that table. The policy behaves like an extra WHERE expression that must be true for a row to participate in SELECT, UPDATE, or DELETE for that user.</p><p>One way to think about this that can help beginners including me when I was learning this, is to think of the database rewriting plain queries so they always include a filter that reflects the current user. Take this example with a shared order:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!jpP8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17a27f3e-110d-469e-b7d1-99cc7038e930_1655x112.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!jpP8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17a27f3e-110d-469e-b7d1-99cc7038e930_1655x112.png 424w, https://substackcdn.com/image/fetch/$s_!jpP8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17a27f3e-110d-469e-b7d1-99cc7038e930_1655x112.png 848w, https://substackcdn.com/image/fetch/$s_!jpP8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17a27f3e-110d-469e-b7d1-99cc7038e930_1655x112.png 1272w, https://substackcdn.com/image/fetch/$s_!jpP8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17a27f3e-110d-469e-b7d1-99cc7038e930_1655x112.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!jpP8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17a27f3e-110d-469e-b7d1-99cc7038e930_1655x112.png" width="1456" height="99" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/17a27f3e-110d-469e-b7d1-99cc7038e930_1655x112.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:99,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:12335,&quot;alt&quot;:&quot;SELECT * FROM orders;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186664272?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17a27f3e-110d-469e-b7d1-99cc7038e930_1655x112.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT * FROM orders;" title="SELECT * FROM orders;" srcset="https://substackcdn.com/image/fetch/$s_!jpP8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17a27f3e-110d-469e-b7d1-99cc7038e930_1655x112.png 424w, https://substackcdn.com/image/fetch/$s_!jpP8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17a27f3e-110d-469e-b7d1-99cc7038e930_1655x112.png 848w, https://substackcdn.com/image/fetch/$s_!jpP8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17a27f3e-110d-469e-b7d1-99cc7038e930_1655x112.png 1272w, https://substackcdn.com/image/fetch/$s_!jpP8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17a27f3e-110d-469e-b7d1-99cc7038e930_1655x112.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Ordinary privileges say whether that statement is allowed at all. Row level security then trims rows down to only the ones that match a user or tenant rule. The effect resembles running a second query with a filter that references some identity column.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Q6rh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0520e23-68cb-4152-b14e-3965dccf933a_1690x170.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Q6rh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0520e23-68cb-4152-b14e-3965dccf933a_1690x170.png 424w, https://substackcdn.com/image/fetch/$s_!Q6rh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0520e23-68cb-4152-b14e-3965dccf933a_1690x170.png 848w, https://substackcdn.com/image/fetch/$s_!Q6rh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0520e23-68cb-4152-b14e-3965dccf933a_1690x170.png 1272w, https://substackcdn.com/image/fetch/$s_!Q6rh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0520e23-68cb-4152-b14e-3965dccf933a_1690x170.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Q6rh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0520e23-68cb-4152-b14e-3965dccf933a_1690x170.png" width="1456" height="146" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d0520e23-68cb-4152-b14e-3965dccf933a_1690x170.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:146,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:30139,&quot;alt&quot;:&quot;SELECT * FROM orders WHERE customer_id = current_app_user_id();&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186664272?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0520e23-68cb-4152-b14e-3965dccf933a_1690x170.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT * FROM orders WHERE customer_id = current_app_user_id();" title="SELECT * FROM orders WHERE customer_id = current_app_user_id();" srcset="https://substackcdn.com/image/fetch/$s_!Q6rh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0520e23-68cb-4152-b14e-3965dccf933a_1690x170.png 424w, https://substackcdn.com/image/fetch/$s_!Q6rh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0520e23-68cb-4152-b14e-3965dccf933a_1690x170.png 848w, https://substackcdn.com/image/fetch/$s_!Q6rh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0520e23-68cb-4152-b14e-3965dccf933a_1690x170.png 1272w, https://substackcdn.com/image/fetch/$s_!Q6rh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0520e23-68cb-4152-b14e-3965dccf933a_1690x170.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>No real system literally rewrites the text of the query in that way, but the final result set tells the same story. Rows that do not satisfy the policy condition vanish for that session, and aggregates such as counts and sums naturally reflect only the visible subset.</p><p>Row level security also applies to write operations in engines that support it. Updates and deletes normally see only rows that pass their policies, so attempts to update someone else&#8217;s row in a shared table simply affect zero rows instead of crossing tenant boundaries. Some platforms also enforce checks on inserted rows so new data must satisfy a policy before it enters the table.</p><p>Any table can have row level security active or inactive as a feature flag. Databases with built in RLS usually expose a DDL switch that turns it on. A family of systems uses a statement in the same idea:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!E8pA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8ada2cd-68d3-47b3-9ef5-ae5891f4fa86_1685x108.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!E8pA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8ada2cd-68d3-47b3-9ef5-ae5891f4fa86_1685x108.png 424w, https://substackcdn.com/image/fetch/$s_!E8pA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8ada2cd-68d3-47b3-9ef5-ae5891f4fa86_1685x108.png 848w, https://substackcdn.com/image/fetch/$s_!E8pA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8ada2cd-68d3-47b3-9ef5-ae5891f4fa86_1685x108.png 1272w, https://substackcdn.com/image/fetch/$s_!E8pA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8ada2cd-68d3-47b3-9ef5-ae5891f4fa86_1685x108.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!E8pA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8ada2cd-68d3-47b3-9ef5-ae5891f4fa86_1685x108.png" width="1456" height="93" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d8ada2cd-68d3-47b3-9ef5-ae5891f4fa86_1685x108.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:93,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:21626,&quot;alt&quot;:&quot;ALTER TABLE employee     ENABLE ROW LEVEL SECURITY;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186664272?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8ada2cd-68d3-47b3-9ef5-ae5891f4fa86_1685x108.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="ALTER TABLE employee     ENABLE ROW LEVEL SECURITY;" title="ALTER TABLE employee     ENABLE ROW LEVEL SECURITY;" srcset="https://substackcdn.com/image/fetch/$s_!E8pA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8ada2cd-68d3-47b3-9ef5-ae5891f4fa86_1685x108.png 424w, https://substackcdn.com/image/fetch/$s_!E8pA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8ada2cd-68d3-47b3-9ef5-ae5891f4fa86_1685x108.png 848w, https://substackcdn.com/image/fetch/$s_!E8pA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8ada2cd-68d3-47b3-9ef5-ae5891f4fa86_1685x108.png 1272w, https://substackcdn.com/image/fetch/$s_!E8pA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8ada2cd-68d3-47b3-9ef5-ae5891f4fa86_1685x108.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>After that change, every ordinary query that references the employee table flows through the row security machinery, and the table behaves as protected data rather than freely readable data.</p><h4>Row Level Security Rules</h4><p>Rules that implement row level security include several moving parts that work in combination during query execution. Treating those parts separately helps explain how databases answer questions about which rows belong to which caller.</p><p>Every rule starts with a table that needs protection. Some platforms mark this on the table itself, such as a property that says row level security is active. Others attach the rule through a security object that names both the table and a predicate. In all of those cases, the database records that row level decisions must run for that table in addition to the normal privilege checks.</p><p>Predicates carry most of the logic. The predicate is an expression that returns true or false for a row. It may read columns on the row, like <code>tenant_id</code> or <code>owner_id</code>, and values supplied by the current session. Engines that support RLS usually store this predicate as part of a policy definition, which keeps the rule close to the table it guards.</p><p>Many systems phrase a predicate directly in SQL. Policies in those systems frequently look like short WHERE clauses that reference both a column and some function call that reflects who is connected.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!D41x!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3e524d4-818f-46d7-adea-9e2015ec2935_1676x56.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!D41x!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3e524d4-818f-46d7-adea-9e2015ec2935_1676x56.png 424w, https://substackcdn.com/image/fetch/$s_!D41x!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3e524d4-818f-46d7-adea-9e2015ec2935_1676x56.png 848w, https://substackcdn.com/image/fetch/$s_!D41x!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3e524d4-818f-46d7-adea-9e2015ec2935_1676x56.png 1272w, https://substackcdn.com/image/fetch/$s_!D41x!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3e524d4-818f-46d7-adea-9e2015ec2935_1676x56.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!D41x!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3e524d4-818f-46d7-adea-9e2015ec2935_1676x56.png" width="1456" height="49" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b3e524d4-818f-46d7-adea-9e2015ec2935_1676x56.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:49,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:10960,&quot;alt&quot;:&quot;tenant_id = current_tenant_id()&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186664272?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3e524d4-818f-46d7-adea-9e2015ec2935_1676x56.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="tenant_id = current_tenant_id()" title="tenant_id = current_tenant_id()" srcset="https://substackcdn.com/image/fetch/$s_!D41x!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3e524d4-818f-46d7-adea-9e2015ec2935_1676x56.png 424w, https://substackcdn.com/image/fetch/$s_!D41x!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3e524d4-818f-46d7-adea-9e2015ec2935_1676x56.png 848w, https://substackcdn.com/image/fetch/$s_!D41x!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3e524d4-818f-46d7-adea-9e2015ec2935_1676x56.png 1272w, https://substackcdn.com/image/fetch/$s_!D41x!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3e524d4-818f-46d7-adea-9e2015ec2935_1676x56.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>That single expression can sit inside a policy, a view, or a stored function, yet the core idea stays the same, only rows with a matching tenant id remain visible.</p><p>Other vendors let administrators write policy functions in a procedural language like PL/pgSQL, T-SQL, or PL/SQL that return a boolean value or a text predicate. Those functions still end up controlling a WHERE condition, but the extra language features make it easier to handle complex cases, such as administrators who should see multiple tenants or audit robots that should see every row.</p><p>Rules also need a scope so the server knows which statement types they apply to. Some policies only apply to reads and leave writes to application code, while others apply to both reads and writes so those queries stay inside the same boundaries. Typical configurations can permit users to read only their own rows yet allow administrators with a specific flag to bypass RLS or rely on a broader predicate.</p><p>Tenant isolation offers a good starting point when talking about rule pieces. In a shared billing table, every row carries a tenant identifier, and the predicate compares that column to a tenant id stored in session state. Take this example in SQL that focuses on the table structure first:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!aS3C!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70a9ddee-cf75-40cc-bea1-525d4e14b1aa_1679x337.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!aS3C!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70a9ddee-cf75-40cc-bea1-525d4e14b1aa_1679x337.png 424w, https://substackcdn.com/image/fetch/$s_!aS3C!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70a9ddee-cf75-40cc-bea1-525d4e14b1aa_1679x337.png 848w, https://substackcdn.com/image/fetch/$s_!aS3C!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70a9ddee-cf75-40cc-bea1-525d4e14b1aa_1679x337.png 1272w, https://substackcdn.com/image/fetch/$s_!aS3C!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70a9ddee-cf75-40cc-bea1-525d4e14b1aa_1679x337.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!aS3C!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70a9ddee-cf75-40cc-bea1-525d4e14b1aa_1679x337.png" width="1456" height="292" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/70a9ddee-cf75-40cc-bea1-525d4e14b1aa_1679x337.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:292,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:64809,&quot;alt&quot;:&quot;CREATE TABLE invoices (     invoice_id     bigserial,     tenant_id      uuid        NOT NULL,     amount_cents   integer     NOT NULL,     created_at     timestamptz NOT NULL DEFAULT now() );&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186664272?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70a9ddee-cf75-40cc-bea1-525d4e14b1aa_1679x337.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE TABLE invoices (     invoice_id     bigserial,     tenant_id      uuid        NOT NULL,     amount_cents   integer     NOT NULL,     created_at     timestamptz NOT NULL DEFAULT now() );" title="CREATE TABLE invoices (     invoice_id     bigserial,     tenant_id      uuid        NOT NULL,     amount_cents   integer     NOT NULL,     created_at     timestamptz NOT NULL DEFAULT now() );" srcset="https://substackcdn.com/image/fetch/$s_!aS3C!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70a9ddee-cf75-40cc-bea1-525d4e14b1aa_1679x337.png 424w, https://substackcdn.com/image/fetch/$s_!aS3C!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70a9ddee-cf75-40cc-bea1-525d4e14b1aa_1679x337.png 848w, https://substackcdn.com/image/fetch/$s_!aS3C!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70a9ddee-cf75-40cc-bea1-525d4e14b1aa_1679x337.png 1272w, https://substackcdn.com/image/fetch/$s_!aS3C!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70a9ddee-cf75-40cc-bea1-525d4e14b1aa_1679x337.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Row level security never requires a special primary identifier, but many real tables that participate in RLS still have one so updates and joins stay fast and precise.</p><p>With a table like that in place, a policy only has to express a comparison between <code>tenant_id</code> and some tenant marker attached to the current session. Different engines spell that policy in their own syntax, yet the logical core is always a predicate along the same lines as the earlier <code>tenant_id</code> expression.</p><h4>User Context In Session Data</h4><p>Row level security always needs some notion of who is asking. That identity can come from the database login, a mapped application user id, a tenant id, or a blend of all three. Whatever form it takes, that data has to be available inside the predicate so the database can compare it to the values stored in rows.</p><p>Database servers expose several ways to represent identity. Most have a base concept of current user, which refers to the database account behind the session. Many also offer configuration parameters or session context slots that an application can populate just after opening a connection. Those values stay tied to the session and can be read back from inside SQL or policy functions.</p><p>One common arrangement for multi tenant web applications ties HTTP authentication to database context. Middleware code decodes a session token, looks up a tenant id and application user id, and then records those values through small statements at the start of every transaction.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cJxJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdde955a3-8f4a-43a5-abd5-46e4eff28ae4_1691x113.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cJxJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdde955a3-8f4a-43a5-abd5-46e4eff28ae4_1691x113.png 424w, https://substackcdn.com/image/fetch/$s_!cJxJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdde955a3-8f4a-43a5-abd5-46e4eff28ae4_1691x113.png 848w, https://substackcdn.com/image/fetch/$s_!cJxJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdde955a3-8f4a-43a5-abd5-46e4eff28ae4_1691x113.png 1272w, https://substackcdn.com/image/fetch/$s_!cJxJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdde955a3-8f4a-43a5-abd5-46e4eff28ae4_1691x113.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cJxJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdde955a3-8f4a-43a5-abd5-46e4eff28ae4_1691x113.png" width="1456" height="97" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dde955a3-8f4a-43a5-abd5-46e4eff28ae4_1691x113.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:97,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:41285,&quot;alt&quot;:&quot;SET app.current_tenant = '9b4b7b6b-1e9f-4e7a-9d8a-71f94ac90111'; SET app.current_user_id = 2307;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186664272?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdde955a3-8f4a-43a5-abd5-46e4eff28ae4_1691x113.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SET app.current_tenant = '9b4b7b6b-1e9f-4e7a-9d8a-71f94ac90111'; SET app.current_user_id = 2307;" title="SET app.current_tenant = '9b4b7b6b-1e9f-4e7a-9d8a-71f94ac90111'; SET app.current_user_id = 2307;" srcset="https://substackcdn.com/image/fetch/$s_!cJxJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdde955a3-8f4a-43a5-abd5-46e4eff28ae4_1691x113.png 424w, https://substackcdn.com/image/fetch/$s_!cJxJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdde955a3-8f4a-43a5-abd5-46e4eff28ae4_1691x113.png 848w, https://substackcdn.com/image/fetch/$s_!cJxJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdde955a3-8f4a-43a5-abd5-46e4eff28ae4_1691x113.png 1272w, https://substackcdn.com/image/fetch/$s_!cJxJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdde955a3-8f4a-43a5-abd5-46e4eff28ae4_1691x113.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>With that information stored as session state, predicates on protected tables can call functions that read those settings. An expression such as <code>tenant_id = current_setting('app.current_tenant')</code> will only pass rows that belong to the current tenant, no matter which query text the application sends later in the transaction.</p><p>Vendors ship different tools for this idea, but the template stays similar. Some platforms use special context functions that read from a namespace of session variables, while others rely on explicit session context features that store values under string names and expose them to predicate logic. Regardless of the exact feature name, row level security depends on that bridge between user or tenant identity in the session and matching columns in protected tables.</p><p>Systems that lack built in row level security features still rely on user context in a similar way. Views and stored procedures can read <code>CURRENT_USER</code> or other identity markers and use those values inside WHERE clauses that restrict rows. The main difference is that the enforcement lives in views and procedures rather than in a dedicated policy system tied directly to each table.</p><h3>Row Level Security In Practice</h3><p>Projects rarely work with row level security as an abstract feature. Real value shows up when a real world database engine takes the general idea of policies and user context and turns that into specific DDL statements, functions, and execution rules. Different vendors picked their own mechanics, but they all revolve around three ingredients. There is a way to mark which tables should be protected, there are predicate rules tied to those tables, and there is some channel that feeds user or tenant identity into those rules at query time.</p><p>PostgreSQL, SQL Server, Oracle Database, and MySQL all sit in this picture, although they do not offer the same feature set. PostgreSQL and SQL Server ship built in row level security features. Oracle Database provides Virtual Private Database policies that behave in a closely related way.</p><h4>PostgreSQL Policies</h4><p>PostgreSQL keeps row level security turned off for a table until the owner makes an explicit change. That switch prevents surprises on existing schemas. When RLS is off, queries only pay attention to normal privileges such as <code>SELECT</code> or <code>UPDATE</code> on the table. After RLS is turned on, the server evaluates policies for every row that a statement touches, and only rows that pass those policies participate in the result or write operation.</p><p>The feature starts with a table. Let&#8217;s say we are working with a billing application in Eau Claire that tracks customer accounts in a single shared table with one tenant identifier per row:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Z_9h!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40c8cf62-99f7-44a9-8764-628bda07e685_1687x391.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Z_9h!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40c8cf62-99f7-44a9-8764-628bda07e685_1687x391.png 424w, https://substackcdn.com/image/fetch/$s_!Z_9h!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40c8cf62-99f7-44a9-8764-628bda07e685_1687x391.png 848w, https://substackcdn.com/image/fetch/$s_!Z_9h!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40c8cf62-99f7-44a9-8764-628bda07e685_1687x391.png 1272w, https://substackcdn.com/image/fetch/$s_!Z_9h!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40c8cf62-99f7-44a9-8764-628bda07e685_1687x391.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Z_9h!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40c8cf62-99f7-44a9-8764-628bda07e685_1687x391.png" width="1456" height="337" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/40c8cf62-99f7-44a9-8764-628bda07e685_1687x391.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:337,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:80103,&quot;alt&quot;:&quot;CREATE TABLE accounts (     id           uuid primary key,     tenant_id    uuid        NOT NULL,     account_name text        NOT NULL,     balance_cents integer    NOT NULL,     created_at   timestamptz NOT NULL DEFAULT now() );&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186664272?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40c8cf62-99f7-44a9-8764-628bda07e685_1687x391.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE TABLE accounts (     id           uuid primary key,     tenant_id    uuid        NOT NULL,     account_name text        NOT NULL,     balance_cents integer    NOT NULL,     created_at   timestamptz NOT NULL DEFAULT now() );" title="CREATE TABLE accounts (     id           uuid primary key,     tenant_id    uuid        NOT NULL,     account_name text        NOT NULL,     balance_cents integer    NOT NULL,     created_at   timestamptz NOT NULL DEFAULT now() );" srcset="https://substackcdn.com/image/fetch/$s_!Z_9h!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40c8cf62-99f7-44a9-8764-628bda07e685_1687x391.png 424w, https://substackcdn.com/image/fetch/$s_!Z_9h!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40c8cf62-99f7-44a9-8764-628bda07e685_1687x391.png 848w, https://substackcdn.com/image/fetch/$s_!Z_9h!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40c8cf62-99f7-44a9-8764-628bda07e685_1687x391.png 1272w, https://substackcdn.com/image/fetch/$s_!Z_9h!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40c8cf62-99f7-44a9-8764-628bda07e685_1687x391.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>PostgreSQL does not treat that table as row protected yet. Turning on RLS takes one DDL statement.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3bFw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F806f02ef-c841-46c8-8f34-51bd53d5a31a_1674x110.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3bFw!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F806f02ef-c841-46c8-8f34-51bd53d5a31a_1674x110.png 424w, https://substackcdn.com/image/fetch/$s_!3bFw!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F806f02ef-c841-46c8-8f34-51bd53d5a31a_1674x110.png 848w, https://substackcdn.com/image/fetch/$s_!3bFw!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F806f02ef-c841-46c8-8f34-51bd53d5a31a_1674x110.png 1272w, https://substackcdn.com/image/fetch/$s_!3bFw!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F806f02ef-c841-46c8-8f34-51bd53d5a31a_1674x110.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3bFw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F806f02ef-c841-46c8-8f34-51bd53d5a31a_1674x110.png" width="1456" height="96" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/806f02ef-c841-46c8-8f34-51bd53d5a31a_1674x110.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:96,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:20763,&quot;alt&quot;:&quot;ALTER TABLE accounts     ENABLE ROW LEVEL SECURITY;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186664272?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F806f02ef-c841-46c8-8f34-51bd53d5a31a_1674x110.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="ALTER TABLE accounts     ENABLE ROW LEVEL SECURITY;" title="ALTER TABLE accounts     ENABLE ROW LEVEL SECURITY;" srcset="https://substackcdn.com/image/fetch/$s_!3bFw!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F806f02ef-c841-46c8-8f34-51bd53d5a31a_1674x110.png 424w, https://substackcdn.com/image/fetch/$s_!3bFw!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F806f02ef-c841-46c8-8f34-51bd53d5a31a_1674x110.png 848w, https://substackcdn.com/image/fetch/$s_!3bFw!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F806f02ef-c841-46c8-8f34-51bd53d5a31a_1674x110.png 1272w, https://substackcdn.com/image/fetch/$s_!3bFw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F806f02ef-c841-46c8-8f34-51bd53d5a31a_1674x110.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>After that change, queries from ordinary roles only see rows that pass at least one policy. When no policy matches, PostgreSQL treats the table as if it had zero visible rows for that user. Superusers and roles with the <code>BYPASSRLS</code> attribute are exempt, and the table owner can bypass RLS as well by default, which helps administrators inspect data when they need to debug policies.</p><p>Policies carry the row filter logic. PostgreSQL stores them as named objects tied to a specific table. Policies can apply to all commands or to just one of <code>SELECT</code>, <code>INSERT</code>, <code>UPDATE</code>, or <code>DELETE</code>, and they can be restricted to certain roles.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!GOin!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6f3c007-5738-4b3b-b0a8-287168260226_1664x449.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!GOin!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6f3c007-5738-4b3b-b0a8-287168260226_1664x449.png 424w, https://substackcdn.com/image/fetch/$s_!GOin!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6f3c007-5738-4b3b-b0a8-287168260226_1664x449.png 848w, https://substackcdn.com/image/fetch/$s_!GOin!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6f3c007-5738-4b3b-b0a8-287168260226_1664x449.png 1272w, https://substackcdn.com/image/fetch/$s_!GOin!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6f3c007-5738-4b3b-b0a8-287168260226_1664x449.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!GOin!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6f3c007-5738-4b3b-b0a8-287168260226_1664x449.png" width="1456" height="393" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f6f3c007-5738-4b3b-b0a8-287168260226_1664x449.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:393,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:68546,&quot;alt&quot;:&quot;CREATE POLICY accounts_tenant_policy ON accounts FOR SELECT USING (     tenant_id = CAST(         current_setting('app.current_tenant', true) AS uuid     ) );&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186664272?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6f3c007-5738-4b3b-b0a8-287168260226_1664x449.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE POLICY accounts_tenant_policy ON accounts FOR SELECT USING (     tenant_id = CAST(         current_setting('app.current_tenant', true) AS uuid     ) );" title="CREATE POLICY accounts_tenant_policy ON accounts FOR SELECT USING (     tenant_id = CAST(         current_setting('app.current_tenant', true) AS uuid     ) );" srcset="https://substackcdn.com/image/fetch/$s_!GOin!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6f3c007-5738-4b3b-b0a8-287168260226_1664x449.png 424w, https://substackcdn.com/image/fetch/$s_!GOin!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6f3c007-5738-4b3b-b0a8-287168260226_1664x449.png 848w, https://substackcdn.com/image/fetch/$s_!GOin!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6f3c007-5738-4b3b-b0a8-287168260226_1664x449.png 1272w, https://substackcdn.com/image/fetch/$s_!GOin!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6f3c007-5738-4b3b-b0a8-287168260226_1664x449.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That policy instructs PostgreSQL to keep rows where <code>tenant_id</code> matches whatever value the application put into the <code>app.current_tenant</code> setting for that session. Any other row in the accounts table behaves as if it does not exist when a <code>SELECT</code> runs through that connection.</p><p>Policies can also check new rows that an <code>INSERT</code> or <code>UPDATE</code> tries to write. The <code>WITH CHECK</code> expression covers this case. If it is absent, PostgreSQL reuses the <code>USING</code> expression, which keeps read and write rules consistent.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lPVy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa4e11e7-ab1f-4de0-b96b-40290aa6117f_1712x920.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lPVy!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa4e11e7-ab1f-4de0-b96b-40290aa6117f_1712x920.png 424w, https://substackcdn.com/image/fetch/$s_!lPVy!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa4e11e7-ab1f-4de0-b96b-40290aa6117f_1712x920.png 848w, https://substackcdn.com/image/fetch/$s_!lPVy!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa4e11e7-ab1f-4de0-b96b-40290aa6117f_1712x920.png 1272w, https://substackcdn.com/image/fetch/$s_!lPVy!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa4e11e7-ab1f-4de0-b96b-40290aa6117f_1712x920.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lPVy!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa4e11e7-ab1f-4de0-b96b-40290aa6117f_1712x920.png" width="922" height="495.19505494505495" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fa4e11e7-ab1f-4de0-b96b-40290aa6117f_1712x920.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:782,&quot;width&quot;:1456,&quot;resizeWidth&quot;:922,&quot;bytes&quot;:134131,&quot;alt&quot;:&quot;CREATE POLICY accounts_tenant_insert_policy ON accounts FOR INSERT WITH CHECK (     tenant_id = CAST(         current_setting('app.current_tenant', true) AS uuid     ) );  CREATE POLICY accounts_tenant_update_policy ON accounts FOR UPDATE USING (     tenant_id = CAST(         current_setting('app.current_tenant', true) AS uuid     ) ) WITH CHECK (     tenant_id = CAST(         current_setting('app.current_tenant', true) AS uuid     ) );&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186664272?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa4e11e7-ab1f-4de0-b96b-40290aa6117f_1712x920.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="CREATE POLICY accounts_tenant_insert_policy ON accounts FOR INSERT WITH CHECK (     tenant_id = CAST(         current_setting('app.current_tenant', true) AS uuid     ) );  CREATE POLICY accounts_tenant_update_policy ON accounts FOR UPDATE USING (     tenant_id = CAST(         current_setting('app.current_tenant', true) AS uuid     ) ) WITH CHECK (     tenant_id = CAST(         current_setting('app.current_tenant', true) AS uuid     ) );" title="CREATE POLICY accounts_tenant_insert_policy ON accounts FOR INSERT WITH CHECK (     tenant_id = CAST(         current_setting('app.current_tenant', true) AS uuid     ) );  CREATE POLICY accounts_tenant_update_policy ON accounts FOR UPDATE USING (     tenant_id = CAST(         current_setting('app.current_tenant', true) AS uuid     ) ) WITH CHECK (     tenant_id = CAST(         current_setting('app.current_tenant', true) AS uuid     ) );" srcset="https://substackcdn.com/image/fetch/$s_!lPVy!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa4e11e7-ab1f-4de0-b96b-40290aa6117f_1712x920.png 424w, https://substackcdn.com/image/fetch/$s_!lPVy!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa4e11e7-ab1f-4de0-b96b-40290aa6117f_1712x920.png 848w, https://substackcdn.com/image/fetch/$s_!lPVy!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa4e11e7-ab1f-4de0-b96b-40290aa6117f_1712x920.png 1272w, https://substackcdn.com/image/fetch/$s_!lPVy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa4e11e7-ab1f-4de0-b96b-40290aa6117f_1712x920.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>With those two policies active, tenants read only their own rows and can only insert or update rows that match the tenant id stored in their session setting. Any attempt to sneak in a row with a different tenant id fails at the database level, even if application code forgets to enforce the rule.</p><p>PostgreSQL also distinguishes between permissive and restrictive policies. Permissive policies broaden access, and the server combines their predicates with logical OR. Restrictive policies narrow access, and their predicates combine with logical AND. Each row must satisfy at least one permissive policy and all restrictive policies to appear in query results. This split makes it possible to have a general tenant wide policy, then an extra restrictive policy that hides rows with <code>deleted_at</code> set or that limit certain roles to older data while administrators see more.</p><h4>SQL Server Security Policy</h4><p>Microsoft SQL Server introduced row level security through a feature that centers on predicate functions and security policies. Instead of embedding the predicate directly into the table definition, SQL Server expects an inline table valued function that checks one row at a time, then a security policy that attaches that function to a table.</p><p>Predicate functions live in a schema, just like tables and views. They must use the <code>SCHEMABINDING</code> option so that the engine can reason about dependencies. This next predicate ties customers to their own rows by tenant id:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!WWtH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8616984-4e1f-4566-93a5-33870da49c23_1706x329.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WWtH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8616984-4e1f-4566-93a5-33870da49c23_1706x329.png 424w, https://substackcdn.com/image/fetch/$s_!WWtH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8616984-4e1f-4566-93a5-33870da49c23_1706x329.png 848w, https://substackcdn.com/image/fetch/$s_!WWtH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8616984-4e1f-4566-93a5-33870da49c23_1706x329.png 1272w, https://substackcdn.com/image/fetch/$s_!WWtH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8616984-4e1f-4566-93a5-33870da49c23_1706x329.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!WWtH!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8616984-4e1f-4566-93a5-33870da49c23_1706x329.png" width="894" height="172.5370879120879" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e8616984-4e1f-4566-93a5-33870da49c23_1706x329.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:281,&quot;width&quot;:1456,&quot;resizeWidth&quot;:894,&quot;bytes&quot;:71015,&quot;alt&quot;:&quot;CREATE FUNCTION Security.fn_customer_tenant_predicate(@TenantId int) RETURNS TABLE WITH SCHEMABINDING AS     RETURN     SELECT 1 AS fn_customer_tenant_predicate_result     WHERE @TenantId = CAST(SESSION_CONTEXT(N'TenantId') AS int); GO&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186664272?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8616984-4e1f-4566-93a5-33870da49c23_1706x329.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="CREATE FUNCTION Security.fn_customer_tenant_predicate(@TenantId int) RETURNS TABLE WITH SCHEMABINDING AS     RETURN     SELECT 1 AS fn_customer_tenant_predicate_result     WHERE @TenantId = CAST(SESSION_CONTEXT(N'TenantId') AS int); GO" title="CREATE FUNCTION Security.fn_customer_tenant_predicate(@TenantId int) RETURNS TABLE WITH SCHEMABINDING AS     RETURN     SELECT 1 AS fn_customer_tenant_predicate_result     WHERE @TenantId = CAST(SESSION_CONTEXT(N'TenantId') AS int); GO" srcset="https://substackcdn.com/image/fetch/$s_!WWtH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8616984-4e1f-4566-93a5-33870da49c23_1706x329.png 424w, https://substackcdn.com/image/fetch/$s_!WWtH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8616984-4e1f-4566-93a5-33870da49c23_1706x329.png 848w, https://substackcdn.com/image/fetch/$s_!WWtH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8616984-4e1f-4566-93a5-33870da49c23_1706x329.png 1272w, https://substackcdn.com/image/fetch/$s_!WWtH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8616984-4e1f-4566-93a5-33870da49c23_1706x329.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>That function reads from <code>SESSION_CONTEXT</code>, which acts as a key value store bound to the current session. Application code sets the value with <code>sp_set_session_context</code> when a connection opens or when a request starts, and the predicate function compares that stored value to the row&#8217;s tenant identifier.</p><p>Tables that need row filtering can then reference this function in a security policy:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!wgbK!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffba4a2fa-8502-4e8b-b680-01e6e79494bc_1721x542.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!wgbK!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffba4a2fa-8502-4e8b-b680-01e6e79494bc_1721x542.png 424w, https://substackcdn.com/image/fetch/$s_!wgbK!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffba4a2fa-8502-4e8b-b680-01e6e79494bc_1721x542.png 848w, https://substackcdn.com/image/fetch/$s_!wgbK!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffba4a2fa-8502-4e8b-b680-01e6e79494bc_1721x542.png 1272w, https://substackcdn.com/image/fetch/$s_!wgbK!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffba4a2fa-8502-4e8b-b680-01e6e79494bc_1721x542.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!wgbK!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffba4a2fa-8502-4e8b-b680-01e6e79494bc_1721x542.png" width="900" height="283.7225274725275" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fba4a2fa-8502-4e8b-b680-01e6e79494bc_1721x542.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:459,&quot;width&quot;:1456,&quot;resizeWidth&quot;:900,&quot;bytes&quot;:107338,&quot;alt&quot;:&quot;CREATE TABLE Sales.CustomerOrders (     OrderId    int           primary key,     TenantId   int           not null,     Amount     decimal(10,2) not null,     Status     nvarchar(20)  not null ); GO  CREATE SECURITY POLICY Security.CustomerOrderPolicy ADD FILTER PREDICATE Security.fn_customer_tenant_predicate(TenantId) ON Sales.CustomerOrders WITH (STATE = ON); GO&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186664272?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffba4a2fa-8502-4e8b-b680-01e6e79494bc_1721x542.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="CREATE TABLE Sales.CustomerOrders (     OrderId    int           primary key,     TenantId   int           not null,     Amount     decimal(10,2) not null,     Status     nvarchar(20)  not null ); GO  CREATE SECURITY POLICY Security.CustomerOrderPolicy ADD FILTER PREDICATE Security.fn_customer_tenant_predicate(TenantId) ON Sales.CustomerOrders WITH (STATE = ON); GO" title="CREATE TABLE Sales.CustomerOrders (     OrderId    int           primary key,     TenantId   int           not null,     Amount     decimal(10,2) not null,     Status     nvarchar(20)  not null ); GO  CREATE SECURITY POLICY Security.CustomerOrderPolicy ADD FILTER PREDICATE Security.fn_customer_tenant_predicate(TenantId) ON Sales.CustomerOrders WITH (STATE = ON); GO" srcset="https://substackcdn.com/image/fetch/$s_!wgbK!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffba4a2fa-8502-4e8b-b680-01e6e79494bc_1721x542.png 424w, https://substackcdn.com/image/fetch/$s_!wgbK!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffba4a2fa-8502-4e8b-b680-01e6e79494bc_1721x542.png 848w, https://substackcdn.com/image/fetch/$s_!wgbK!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffba4a2fa-8502-4e8b-b680-01e6e79494bc_1721x542.png 1272w, https://substackcdn.com/image/fetch/$s_!wgbK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffba4a2fa-8502-4e8b-b680-01e6e79494bc_1721x542.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>After that policy is in place, SQL Server calls the predicate for rows touched by <code>SELECT</code>, <code>UPDATE</code>, and <code>DELETE</code> statements. Rows that fail the predicate do not appear in result sets and do not take part in those write operations. The calling code does not need to mention tenant restrictions at all, the policy applies to the table regardless of the query text.</p><p>Block predicates extend the model to prevent disallowed writes. A block predicate that targets <code>INSERT</code>, <code>UPDATE</code>, or <code>DELETE</code> can stop operations that would have introduced out of tenant rows or modified them in ways that break security rules. Both filter and block predicates share the same table and rely on predicate functions, but they attach to different statement types through the security policy.</p><p>SQL Server enforces row filtering through security policies, and it restricts predicates per table by operation. If a table already has a predicate defined for a given operation, trying to add another predicate for that same operation results in an error. Those limits reduce the risk of conflicting logic where one predicate tries to hide rows that another predicate expects to see. Developers working on row level security in SQL Server usually end up with one or two small predicate functions that all related policies share across multiple tables.</p><h4>Oracle Virtual Private Database</h4><p>Oracle Database takes a slightly different route with its Virtual Private Database feature, or VPD. Instead of inline functions that return a boolean column, VPD policy functions return a text string that represents a predicate. The engine then appends that predicate to user queries as if they contained that condition from the start.</p><p>Administration begins with a normal table. Let&#8217;s say we are working with a human resources schema named <code>HR</code> that keeps employee data in a single table that serves multiple departments:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2Pkz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20664077-aa25-4e43-9510-c86ec5ea62c7_1691x392.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2Pkz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20664077-aa25-4e43-9510-c86ec5ea62c7_1691x392.png 424w, https://substackcdn.com/image/fetch/$s_!2Pkz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20664077-aa25-4e43-9510-c86ec5ea62c7_1691x392.png 848w, https://substackcdn.com/image/fetch/$s_!2Pkz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20664077-aa25-4e43-9510-c86ec5ea62c7_1691x392.png 1272w, https://substackcdn.com/image/fetch/$s_!2Pkz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20664077-aa25-4e43-9510-c86ec5ea62c7_1691x392.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2Pkz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20664077-aa25-4e43-9510-c86ec5ea62c7_1691x392.png" width="1456" height="338" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/20664077-aa25-4e43-9510-c86ec5ea62c7_1691x392.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:338,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:76306,&quot;alt&quot;:&quot;CREATE TABLE hr_employees (     emp_id      number       primary key,     dept_code   varchar2(10) not null,     full_name   varchar2(100),     salary      number,     hire_date   date );&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186664272?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20664077-aa25-4e43-9510-c86ec5ea62c7_1691x392.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE TABLE hr_employees (     emp_id      number       primary key,     dept_code   varchar2(10) not null,     full_name   varchar2(100),     salary      number,     hire_date   date );" title="CREATE TABLE hr_employees (     emp_id      number       primary key,     dept_code   varchar2(10) not null,     full_name   varchar2(100),     salary      number,     hire_date   date );" srcset="https://substackcdn.com/image/fetch/$s_!2Pkz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20664077-aa25-4e43-9510-c86ec5ea62c7_1691x392.png 424w, https://substackcdn.com/image/fetch/$s_!2Pkz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20664077-aa25-4e43-9510-c86ec5ea62c7_1691x392.png 848w, https://substackcdn.com/image/fetch/$s_!2Pkz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20664077-aa25-4e43-9510-c86ec5ea62c7_1691x392.png 1272w, https://substackcdn.com/image/fetch/$s_!2Pkz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20664077-aa25-4e43-9510-c86ec5ea62c7_1691x392.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Virtual Private Database relies on the <code>DBMS_RLS</code> package to attach policies. Before that call, a policy function has to exist. That function reads context values, then assembles a predicate string such as <code>dept_code = 'SALES'</code>:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qTzE!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3da4fd19-d69e-44d2-9d83-a88b45d2c950_1655x667.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qTzE!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3da4fd19-d69e-44d2-9d83-a88b45d2c950_1655x667.png 424w, https://substackcdn.com/image/fetch/$s_!qTzE!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3da4fd19-d69e-44d2-9d83-a88b45d2c950_1655x667.png 848w, https://substackcdn.com/image/fetch/$s_!qTzE!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3da4fd19-d69e-44d2-9d83-a88b45d2c950_1655x667.png 1272w, https://substackcdn.com/image/fetch/$s_!qTzE!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3da4fd19-d69e-44d2-9d83-a88b45d2c950_1655x667.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qTzE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3da4fd19-d69e-44d2-9d83-a88b45d2c950_1655x667.png" width="1456" height="587" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3da4fd19-d69e-44d2-9d83-a88b45d2c950_1655x667.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:587,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:113840,&quot;alt&quot;:&quot;CREATE OR REPLACE FUNCTION hr_dept_policy(     object_schema IN varchar2,     object_name   IN varchar2 ) RETURN varchar2 AS     v_dept  varchar2(10); BEGIN     v_dept := SYS_CONTEXT('HR_APP_CTX', 'DEPT_CODE');     RETURN 'dept_code = ''' || v_dept || ''''; END; /&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186664272?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3da4fd19-d69e-44d2-9d83-a88b45d2c950_1655x667.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE OR REPLACE FUNCTION hr_dept_policy(     object_schema IN varchar2,     object_name   IN varchar2 ) RETURN varchar2 AS     v_dept  varchar2(10); BEGIN     v_dept := SYS_CONTEXT('HR_APP_CTX', 'DEPT_CODE');     RETURN 'dept_code = ''' || v_dept || ''''; END; /" title="CREATE OR REPLACE FUNCTION hr_dept_policy(     object_schema IN varchar2,     object_name   IN varchar2 ) RETURN varchar2 AS     v_dept  varchar2(10); BEGIN     v_dept := SYS_CONTEXT('HR_APP_CTX', 'DEPT_CODE');     RETURN 'dept_code = ''' || v_dept || ''''; END; /" srcset="https://substackcdn.com/image/fetch/$s_!qTzE!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3da4fd19-d69e-44d2-9d83-a88b45d2c950_1655x667.png 424w, https://substackcdn.com/image/fetch/$s_!qTzE!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3da4fd19-d69e-44d2-9d83-a88b45d2c950_1655x667.png 848w, https://substackcdn.com/image/fetch/$s_!qTzE!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3da4fd19-d69e-44d2-9d83-a88b45d2c950_1655x667.png 1272w, https://substackcdn.com/image/fetch/$s_!qTzE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3da4fd19-d69e-44d2-9d83-a88b45d2c950_1655x667.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The call to <code>DBMS_RLS.ADD_POLICY</code> links this function to the table:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7vhh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396bd582-5f8c-4c45-a299-fb9ab5985dc0_1663x611.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7vhh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396bd582-5f8c-4c45-a299-fb9ab5985dc0_1663x611.png 424w, https://substackcdn.com/image/fetch/$s_!7vhh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396bd582-5f8c-4c45-a299-fb9ab5985dc0_1663x611.png 848w, https://substackcdn.com/image/fetch/$s_!7vhh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396bd582-5f8c-4c45-a299-fb9ab5985dc0_1663x611.png 1272w, https://substackcdn.com/image/fetch/$s_!7vhh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396bd582-5f8c-4c45-a299-fb9ab5985dc0_1663x611.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7vhh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396bd582-5f8c-4c45-a299-fb9ab5985dc0_1663x611.png" width="1456" height="535" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/396bd582-5f8c-4c45-a299-fb9ab5985dc0_1663x611.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:535,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:104129,&quot;alt&quot;:&quot;BEGIN     DBMS_RLS.ADD_POLICY(         object_schema   => 'HR',         object_name     => 'HR_EMPLOYEES',         policy_name     => 'HR_EMP_POLICY',         function_schema => 'HR',         policy_function => 'HR_DEPT_POLICY',         statement_types => 'SELECT,UPDATE,DELETE'     ); END; /&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186664272?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396bd582-5f8c-4c45-a299-fb9ab5985dc0_1663x611.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="BEGIN     DBMS_RLS.ADD_POLICY(         object_schema   => 'HR',         object_name     => 'HR_EMPLOYEES',         policy_name     => 'HR_EMP_POLICY',         function_schema => 'HR',         policy_function => 'HR_DEPT_POLICY',         statement_types => 'SELECT,UPDATE,DELETE'     ); END; /" title="BEGIN     DBMS_RLS.ADD_POLICY(         object_schema   => 'HR',         object_name     => 'HR_EMPLOYEES',         policy_name     => 'HR_EMP_POLICY',         function_schema => 'HR',         policy_function => 'HR_DEPT_POLICY',         statement_types => 'SELECT,UPDATE,DELETE'     ); END; /" srcset="https://substackcdn.com/image/fetch/$s_!7vhh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396bd582-5f8c-4c45-a299-fb9ab5985dc0_1663x611.png 424w, https://substackcdn.com/image/fetch/$s_!7vhh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396bd582-5f8c-4c45-a299-fb9ab5985dc0_1663x611.png 848w, https://substackcdn.com/image/fetch/$s_!7vhh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396bd582-5f8c-4c45-a299-fb9ab5985dc0_1663x611.png 1272w, https://substackcdn.com/image/fetch/$s_!7vhh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396bd582-5f8c-4c45-a299-fb9ab5985dc0_1663x611.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>With that policy in place, Oracle Database consults the <code>HR_APP_CTX</code> context for a <code>DEPT_CODE</code> value during query parsing. The returned predicate string attaches to user queries that reference <code>HR.HR_EMPLOYEES</code> for the listed statement types. Queries that do not mention <code>dept_code</code> in their text still receive that filter at runtime, so users only see rows from their own department. VPD also supports column level policies. Policy rules can apply only when certain columns appear in the query, which helps when columns such as <code>salary</code> need tighter access while columns such as <code>full_name</code> are fine to expose more broadly. In all cases, the engine enforces access by attaching a dynamic predicate to the statement issued against the protected object.</p><p>Context values for VPD policies usually come from <code>SYS_CONTEXT</code>. Namespaces such as <code>USERENV</code> expose generic information like session user or client identifier, and custom namespaces created by the application hold values such as department or tenant identifiers. Policy functions stay relatively small because they mainly translate those context values into predicate strings that Oracle attaches to SQL statements.</p><h4>MySQL Views With Triggers</h4><p>Standard MySQL editions do not ship a dedicated row level security feature at the server level. Access control still revolves around object privileges, so a user with <code>SELECT</code> on a table can see all rows in that table. Many installations still need finer grained control, so administrators build row restricted access paths with views and grants, sometimes helped by triggers and stored routines.</p><p>One common arrangement relies on a view that filters rows based on the current MySQL user. The base table may carry an owner column that matches the MySQL account string returned by <code>USER()</code>:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!u4H_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa9df04f-a589-4cc2-9147-8e6c534e5e35_1683x338.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!u4H_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa9df04f-a589-4cc2-9147-8e6c534e5e35_1683x338.png 424w, https://substackcdn.com/image/fetch/$s_!u4H_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa9df04f-a589-4cc2-9147-8e6c534e5e35_1683x338.png 848w, https://substackcdn.com/image/fetch/$s_!u4H_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa9df04f-a589-4cc2-9147-8e6c534e5e35_1683x338.png 1272w, https://substackcdn.com/image/fetch/$s_!u4H_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa9df04f-a589-4cc2-9147-8e6c534e5e35_1683x338.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!u4H_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa9df04f-a589-4cc2-9147-8e6c534e5e35_1683x338.png" width="1456" height="292" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fa9df04f-a589-4cc2-9147-8e6c534e5e35_1683x338.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:292,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:74177,&quot;alt&quot;:&quot;CREATE TABLE project_notes (     note_id     int           primary key,     owner_name  varchar(288)  not null,     project     varchar(64)   not null,     note_body   text          not null );&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186664272?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa9df04f-a589-4cc2-9147-8e6c534e5e35_1683x338.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE TABLE project_notes (     note_id     int           primary key,     owner_name  varchar(288)  not null,     project     varchar(64)   not null,     note_body   text          not null );" title="CREATE TABLE project_notes (     note_id     int           primary key,     owner_name  varchar(288)  not null,     project     varchar(64)   not null,     note_body   text          not null );" srcset="https://substackcdn.com/image/fetch/$s_!u4H_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa9df04f-a589-4cc2-9147-8e6c534e5e35_1683x338.png 424w, https://substackcdn.com/image/fetch/$s_!u4H_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa9df04f-a589-4cc2-9147-8e6c534e5e35_1683x338.png 848w, https://substackcdn.com/image/fetch/$s_!u4H_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa9df04f-a589-4cc2-9147-8e6c534e5e35_1683x338.png 1272w, https://substackcdn.com/image/fetch/$s_!u4H_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa9df04f-a589-4cc2-9147-8e6c534e5e35_1683x338.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The filtered view then narrows access:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!SHcV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd88bc28-f335-4530-a73c-e01f332dd1f6_1667x393.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!SHcV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd88bc28-f335-4530-a73c-e01f332dd1f6_1667x393.png 424w, https://substackcdn.com/image/fetch/$s_!SHcV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd88bc28-f335-4530-a73c-e01f332dd1f6_1667x393.png 848w, https://substackcdn.com/image/fetch/$s_!SHcV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd88bc28-f335-4530-a73c-e01f332dd1f6_1667x393.png 1272w, https://substackcdn.com/image/fetch/$s_!SHcV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd88bc28-f335-4530-a73c-e01f332dd1f6_1667x393.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!SHcV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd88bc28-f335-4530-a73c-e01f332dd1f6_1667x393.png" width="1456" height="343" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fd88bc28-f335-4530-a73c-e01f332dd1f6_1667x393.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:343,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:79602,&quot;alt&quot;:&quot;CREATE DEFINER = 'rls_admin'@'%' SQL SECURITY DEFINER VIEW current_user_notes AS SELECT note_id, owner_name, project, note_body FROM project_notes WHERE owner_name = USER();&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186664272?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd88bc28-f335-4530-a73c-e01f332dd1f6_1667x393.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE DEFINER = 'rls_admin'@'%' SQL SECURITY DEFINER VIEW current_user_notes AS SELECT note_id, owner_name, project, note_body FROM project_notes WHERE owner_name = USER();" title="CREATE DEFINER = 'rls_admin'@'%' SQL SECURITY DEFINER VIEW current_user_notes AS SELECT note_id, owner_name, project, note_body FROM project_notes WHERE owner_name = USER();" srcset="https://substackcdn.com/image/fetch/$s_!SHcV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd88bc28-f335-4530-a73c-e01f332dd1f6_1667x393.png 424w, https://substackcdn.com/image/fetch/$s_!SHcV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd88bc28-f335-4530-a73c-e01f332dd1f6_1667x393.png 848w, https://substackcdn.com/image/fetch/$s_!SHcV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd88bc28-f335-4530-a73c-e01f332dd1f6_1667x393.png 1272w, https://substackcdn.com/image/fetch/$s_!SHcV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd88bc28-f335-4530-a73c-e01f332dd1f6_1667x393.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Grants direct users toward the view and away from the base table:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ufcY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e263d9-95a8-425d-a8d1-8dae2f93710f_1681x391.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ufcY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e263d9-95a8-425d-a8d1-8dae2f93710f_1681x391.png 424w, https://substackcdn.com/image/fetch/$s_!ufcY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e263d9-95a8-425d-a8d1-8dae2f93710f_1681x391.png 848w, https://substackcdn.com/image/fetch/$s_!ufcY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e263d9-95a8-425d-a8d1-8dae2f93710f_1681x391.png 1272w, https://substackcdn.com/image/fetch/$s_!ufcY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e263d9-95a8-425d-a8d1-8dae2f93710f_1681x391.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ufcY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e263d9-95a8-425d-a8d1-8dae2f93710f_1681x391.png" width="1456" height="339" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/18e263d9-95a8-425d-a8d1-8dae2f93710f_1681x391.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:339,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:70529,&quot;alt&quot;:&quot;GRANT SELECT, INSERT, UPDATE, DELETE ON your_db.current_user_notes TO 'alex'@'%';  REVOKE ALL PRIVILEGES ON your_db.project_notes FROM 'alex'@'%';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186664272?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e263d9-95a8-425d-a8d1-8dae2f93710f_1681x391.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="GRANT SELECT, INSERT, UPDATE, DELETE ON your_db.current_user_notes TO 'alex'@'%';  REVOKE ALL PRIVILEGES ON your_db.project_notes FROM 'alex'@'%';" title="GRANT SELECT, INSERT, UPDATE, DELETE ON your_db.current_user_notes TO 'alex'@'%';  REVOKE ALL PRIVILEGES ON your_db.project_notes FROM 'alex'@'%';" srcset="https://substackcdn.com/image/fetch/$s_!ufcY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e263d9-95a8-425d-a8d1-8dae2f93710f_1681x391.png 424w, https://substackcdn.com/image/fetch/$s_!ufcY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e263d9-95a8-425d-a8d1-8dae2f93710f_1681x391.png 848w, https://substackcdn.com/image/fetch/$s_!ufcY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e263d9-95a8-425d-a8d1-8dae2f93710f_1681x391.png 1272w, https://substackcdn.com/image/fetch/$s_!ufcY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e263d9-95a8-425d-a8d1-8dae2f93710f_1681x391.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>With that structure, the user <code>alex</code> interacts only with <code>current_user_notes</code>. Any query that user sends against the view inherits the <code>WHERE owner_name = USER()</code> filter, and the absence of privileges on <code>project_notes</code> prevents bypassing the filter through direct table access.</p><p>Triggers and stored procedures can reinforce that structure for write operations. An <code>INSERT</code> trigger on <code>project_notes</code> can force <code>owner_name</code> to match <code>USER()</code> or <code>SESSION_USER()</code> regardless of what value the client supplied, and an <code>UPDATE</code> trigger can reject attempts to change <code>owner_name</code> to someone else. Some MySQL based systems also centralize writes through stored procedures that check <code>USER()</code> against ownership rules before allowing inserts or updates to proceed.</p><p>Cloud platforms built on MySQL, such as Amazon Aurora MySQL and managed RDS for MySQL, document similar setups. Application code connects with accounts that only see filtered views and that rely on triggers or stored routines for data changes. Even though there is no single statement that turns on row level security for a table, the combined effect of views, grants, triggers, and session user checks gives row based isolation that behaves much like built in RLS in day to day work.</p><h3>Conclusion</h3><p>Row level security pushes row filtering into the database engine by tying policy predicates to tables and driving those predicates with user or tenant context on every query. PostgreSQL does this with table policies and <code>current_setting</code>, SQL Server with predicate functions and security policies, Oracle Database with VPD functions and <code>DBMS_RLS</code>, and MySQL through views, grants, and triggers around <code>CURRENT_USER</code>. These mechanics define which rows a session can read or modify and keep shared tables isolated while application code continues to send ordinary queries without tenant specific filters in every statement.</p><ol><li><p><em><a href="https://www.postgresql.org/docs/current/ddl-rowsecurity.html">PostgreSQL Row Level Security Documentation</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/en-us/sql/relational-databases/security/row-level-security">SQL Server Row Level Security Guide</a></em></p></li><li><p><em><a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/using-oracle-vpd-to-control-data-access.html">Oracle Virtual Private Database Guide</a></em></p></li><li><p><em><a href="https://dev.mysql.com/doc/refman/8.0/en/create-view.html">MySQL Views and Privileges Reference</a></em></p></li><li><p><em><a href="https://aws.amazon.com/blogs/database/implement-row-level-security-in-amazon-aurora-mysql-and-amazon-rds-for-mysql/">Aurora MySQL Row Level Security Example</a></em></p></li></ol><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share Alexander Obregon's Substack&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share Alexander Obregon's Substack</span></a></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mqhx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe704a91f-8bb4-4989-8019-87b41b4b83a0_306x306.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mqhx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe704a91f-8bb4-4989-8019-87b41b4b83a0_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!mqhx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe704a91f-8bb4-4989-8019-87b41b4b83a0_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!mqhx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe704a91f-8bb4-4989-8019-87b41b4b83a0_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!mqhx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe704a91f-8bb4-4989-8019-87b41b4b83a0_306x306.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mqhx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe704a91f-8bb4-4989-8019-87b41b4b83a0_306x306.png" width="306" height="306" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e704a91f-8bb4-4989-8019-87b41b4b83a0_306x306.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:306,&quot;width&quot;:306,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!mqhx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe704a91f-8bb4-4989-8019-87b41b4b83a0_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!mqhx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe704a91f-8bb4-4989-8019-87b41b4b83a0_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!mqhx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe704a91f-8bb4-4989-8019-87b41b4b83a0_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!mqhx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe704a91f-8bb4-4989-8019-87b41b4b83a0_306x306.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://freesvg.org/mono-sql">Image Source</a></figcaption></figure></div>]]></content:encoded></item><item><title><![CDATA[Index Hints For Slow SQL Queries]]></title><description><![CDATA[Production databases tend to carry queries that feel slow even on strong hardware, and index hints give database engineers a direct way to override the optimizer when its choices do not match the actual data distribution or traffic behavior, so engineers can steer specific statements toward index plans that trim I O and response time.]]></description><link>https://alexanderobregon.substack.com/p/index-hints-for-slow-sql-queries</link><guid isPermaLink="false">https://alexanderobregon.substack.com/p/index-hints-for-slow-sql-queries</guid><dc:creator><![CDATA[Alexander Obregon]]></dc:creator><pubDate>Sat, 07 Feb 2026 19:13:10 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!52ox!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b198e3-a634-4a57-83ad-0cf19f86a5c9_306x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ZSZl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc0f6006-e94d-46f7-83e7-3b9e4d8c3772_800x373.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ZSZl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc0f6006-e94d-46f7-83e7-3b9e4d8c3772_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!ZSZl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc0f6006-e94d-46f7-83e7-3b9e4d8c3772_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!ZSZl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc0f6006-e94d-46f7-83e7-3b9e4d8c3772_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!ZSZl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc0f6006-e94d-46f7-83e7-3b9e4d8c3772_800x373.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ZSZl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc0f6006-e94d-46f7-83e7-3b9e4d8c3772_800x373.png" width="800" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bc0f6006-e94d-46f7-83e7-3b9e4d8c3772_800x373.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ZSZl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc0f6006-e94d-46f7-83e7-3b9e4d8c3772_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!ZSZl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc0f6006-e94d-46f7-83e7-3b9e4d8c3772_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!ZSZl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc0f6006-e94d-46f7-83e7-3b9e4d8c3772_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!ZSZl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc0f6006-e94d-46f7-83e7-3b9e4d8c3772_800x373.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://commons.wikimedia.org/wiki/File:Sql_data_base_with_logo.png">Image Source</a></figcaption></figure></div><p>Production databases tend to carry queries that feel slow even on strong hardware, and index hints give database engineers a direct way to override the optimizer when its choices do not match the actual data distribution or traffic behavior, so engineers can steer specific statements toward index plans that trim I O and response time.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>How Index Hints Affect Query Plans</h3><p>Index hints sit inside query text and tell the optimizer how to treat specific indexes. Without hints, the engine reads statistics, builds several candidate plans, and picks the one that looks cheapest in terms of I O and CPU cost. With hints, that search space narrows, because certain indexes are pushed to the front or removed from the options entirely. MySQL and SQL Server both follow this broad idea, even though their hint syntax and details differ.</p><p>These hints do not turn off the optimizer. The engine still has to decide join order, join algorithms, sort strategies, and many other details. Hints mainly control which access routes are allowed for a table, such as a full scan, a seek on an index, or a scan of a covering index. That makes them a precise tool for queries where the planner repeatedly picks an index that works poorly with real data statistics.</p><h4>What Query Optimizers Do</h4><p>Database engines treat a text query as a starting point for building an execution plan. The first step turns the string into a parsed tree, which records tables, columns, predicates, and join conditions in a structured form. That tree may expand views and inlineable subqueries so that the optimizer can reason about a single, unified plan instead of many smaller fragments. After parsing, the optimizer reads metadata. It checks which indexes exist on each table, what columns those indexes contain, and what kind of statistics are available on base tables and indexes. Histograms and density values stored with statistics give the planner a way to estimate how many rows match a predicate such as <code>status = 'ACTIVE'</code> or <code>order_date &gt;= '2026-01-01'</code>. Those row count estimates drive almost every later decision, because they determine how much work each part of the plan must do.</p><p>When metadata and statistics are in hand, the planner starts building alternative plans. One candidate may scan an entire table and apply a filter to every row, while other candidates seek into selective indexes and touch only a small slice of the data. Further variation appears around join order and join type. The engine can join <code>customers</code> to <code>orders</code> in more than one sequence, and it can choose nested loops, hash joins, or merge joins based on estimated row counts and sort properties.</p><p>Indexes enter this process as access options for a table. One table that has three secondary indexes and a clustered index presents several possible access patterns, including a full scan of the clustered index and different seek or scan patterns on each secondary index. Without hints, the optimizer weighs all of them and keeps the ones that look most promising. With hints, that list shrinks, which guides the final plan toward specific indexes.</p><p>MySQL offers hints such as <code>USE INDEX</code>, <code>FORCE INDEX</code>, and <code>IGNORE INDEX</code> inside the <code>FROM</code> clause. These apply to <code>SELECT</code> and <code>UPDATE</code> statements and to multi table <code>DELETE</code> statements, and they can also be scoped to phases like join planning, ordering, or grouping. Newer MySQL releases also document index level optimizer hints meant to replace these, with <code>USE INDEX</code>, <code>FORCE INDEX</code>, and <code>IGNORE INDEX</code> expected to be deprecated in a future release. Here is a small example with <code>USE INDEX</code>:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dPU1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc27918c7-6649-4d09-8e14-77c14cdde42c_1684x170.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dPU1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc27918c7-6649-4d09-8e14-77c14cdde42c_1684x170.png 424w, https://substackcdn.com/image/fetch/$s_!dPU1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc27918c7-6649-4d09-8e14-77c14cdde42c_1684x170.png 848w, https://substackcdn.com/image/fetch/$s_!dPU1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc27918c7-6649-4d09-8e14-77c14cdde42c_1684x170.png 1272w, https://substackcdn.com/image/fetch/$s_!dPU1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc27918c7-6649-4d09-8e14-77c14cdde42c_1684x170.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dPU1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc27918c7-6649-4d09-8e14-77c14cdde42c_1684x170.png" width="1456" height="147" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c27918c7-6649-4d09-8e14-77c14cdde42c_1684x170.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:147,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:50964,&quot;alt&quot;:&quot;SELECT order_id, order_date, total_amount FROM shop_orders USE INDEX (idx_shop_orders_customer) WHERE customer_id = 1001;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186669901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc27918c7-6649-4d09-8e14-77c14cdde42c_1684x170.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT order_id, order_date, total_amount FROM shop_orders USE INDEX (idx_shop_orders_customer) WHERE customer_id = 1001;" title="SELECT order_id, order_date, total_amount FROM shop_orders USE INDEX (idx_shop_orders_customer) WHERE customer_id = 1001;" srcset="https://substackcdn.com/image/fetch/$s_!dPU1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc27918c7-6649-4d09-8e14-77c14cdde42c_1684x170.png 424w, https://substackcdn.com/image/fetch/$s_!dPU1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc27918c7-6649-4d09-8e14-77c14cdde42c_1684x170.png 848w, https://substackcdn.com/image/fetch/$s_!dPU1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc27918c7-6649-4d09-8e14-77c14cdde42c_1684x170.png 1272w, https://substackcdn.com/image/fetch/$s_!dPU1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc27918c7-6649-4d09-8e14-77c14cdde42c_1684x170.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>This query still goes through normal planning, but the hint tells MySQL to favor <code>idx_shop_orders_customer</code> for access to <code>shop_orders</code> and to push other indexes to the side for this statement.</p><p>SQL Server applies table hints by placing <code>WITH (hint options)</code> immediately after the table name. One common form is the <code>INDEX</code> hint that directs planning to a specific index identifier or name:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!OFxP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60da9559-5c35-44ec-a4d6-d6dc51494fbf_1676x163.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OFxP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60da9559-5c35-44ec-a4d6-d6dc51494fbf_1676x163.png 424w, https://substackcdn.com/image/fetch/$s_!OFxP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60da9559-5c35-44ec-a4d6-d6dc51494fbf_1676x163.png 848w, https://substackcdn.com/image/fetch/$s_!OFxP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60da9559-5c35-44ec-a4d6-d6dc51494fbf_1676x163.png 1272w, https://substackcdn.com/image/fetch/$s_!OFxP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60da9559-5c35-44ec-a4d6-d6dc51494fbf_1676x163.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!OFxP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60da9559-5c35-44ec-a4d6-d6dc51494fbf_1676x163.png" width="1456" height="142" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/60da9559-5c35-44ec-a4d6-d6dc51494fbf_1676x163.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:142,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:53609,&quot;alt&quot;:&quot;SELECT OrderId, OrderDate, TotalAmount FROM Sales.Orders WITH (INDEX(IX_Orders_CustomerId)) WHERE CustomerId = 1001;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186669901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60da9559-5c35-44ec-a4d6-d6dc51494fbf_1676x163.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT OrderId, OrderDate, TotalAmount FROM Sales.Orders WITH (INDEX(IX_Orders_CustomerId)) WHERE CustomerId = 1001;" title="SELECT OrderId, OrderDate, TotalAmount FROM Sales.Orders WITH (INDEX(IX_Orders_CustomerId)) WHERE CustomerId = 1001;" srcset="https://substackcdn.com/image/fetch/$s_!OFxP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60da9559-5c35-44ec-a4d6-d6dc51494fbf_1676x163.png 424w, https://substackcdn.com/image/fetch/$s_!OFxP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60da9559-5c35-44ec-a4d6-d6dc51494fbf_1676x163.png 848w, https://substackcdn.com/image/fetch/$s_!OFxP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60da9559-5c35-44ec-a4d6-d6dc51494fbf_1676x163.png 1272w, https://substackcdn.com/image/fetch/$s_!OFxP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60da9559-5c35-44ec-a4d6-d6dc51494fbf_1676x163.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>That hint does not fully lock down the plan, yet it steers the optimizer toward <code>IX_Orders_CustomerId</code> when it decides how to read <code>Sales.Orders</code>. Other parts of the plan, such as join choices and sort operations, still come from the usual cost based process.</p><h4>Index Selection With Statistics</h4><p>Statistics sit at the core of index choice. Each index and base table can have statistics objects that record how values are spread across the column domain. Histograms on columns such as <code>status</code> can reveal that only a small fraction of rows use a value like <code>'PENDING_REVIEW'</code>. Density values for a composite index on <code>(customer_id, order_date)</code> can show how many different combinations exist in the table. Planners rely heavily on these numbers when they decide which index to use.</p><p>When a predicate references columns with up to date statistics, the engine can estimate selectivity for each index that covers those columns. If a filter on <code>customer_id</code> and <code>order_date</code> is expected to match only a small band of rows, a composite index on those columns looks attractive. If statistics indicate that almost every row matches a predicate, the planner may prefer a scan, because seeking into an index would just add overhead without much benefit.</p><p>Problems appear when statistics no longer match reality. Data can change dramatically after a bulk load, seasonal spike, or archival process. Skewed distributions can also confuse the planner. Think about a flag column with values that are almost always zero, apart from a tiny active slice. Queries that focus on the rare slice can perform well with a selective index, while queries that touch the common slice behave badly if the engine misjudges how many rows will match.</p><p>Let&#8217;s take a look at a MySQL example of this, say we are working with a logging table that holds web events:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Sdsq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d02e143-acb2-45f9-9e6b-4b0e97081da2_1666x449.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Sdsq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d02e143-acb2-45f9-9e6b-4b0e97081da2_1666x449.png 424w, https://substackcdn.com/image/fetch/$s_!Sdsq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d02e143-acb2-45f9-9e6b-4b0e97081da2_1666x449.png 848w, https://substackcdn.com/image/fetch/$s_!Sdsq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d02e143-acb2-45f9-9e6b-4b0e97081da2_1666x449.png 1272w, https://substackcdn.com/image/fetch/$s_!Sdsq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d02e143-acb2-45f9-9e6b-4b0e97081da2_1666x449.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Sdsq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d02e143-acb2-45f9-9e6b-4b0e97081da2_1666x449.png" width="1456" height="392" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5d02e143-acb2-45f9-9e6b-4b0e97081da2_1666x449.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:392,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:112974,&quot;alt&quot;:&quot;CREATE TABLE page_events (     event_id     BIGINT PRIMARY KEY,     user_id      BIGINT NOT NULL,     event_type   VARCHAR(50) NOT NULL,     event_time   DATETIME NOT NULL,     KEY idx_page_events_user_time (user_id, event_time),     KEY idx_page_events_type_time (event_type, event_time) );&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186669901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d02e143-acb2-45f9-9e6b-4b0e97081da2_1666x449.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE TABLE page_events (     event_id     BIGINT PRIMARY KEY,     user_id      BIGINT NOT NULL,     event_type   VARCHAR(50) NOT NULL,     event_time   DATETIME NOT NULL,     KEY idx_page_events_user_time (user_id, event_time),     KEY idx_page_events_type_time (event_type, event_time) );" title="CREATE TABLE page_events (     event_id     BIGINT PRIMARY KEY,     user_id      BIGINT NOT NULL,     event_type   VARCHAR(50) NOT NULL,     event_time   DATETIME NOT NULL,     KEY idx_page_events_user_time (user_id, event_time),     KEY idx_page_events_type_time (event_type, event_time) );" srcset="https://substackcdn.com/image/fetch/$s_!Sdsq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d02e143-acb2-45f9-9e6b-4b0e97081da2_1666x449.png 424w, https://substackcdn.com/image/fetch/$s_!Sdsq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d02e143-acb2-45f9-9e6b-4b0e97081da2_1666x449.png 848w, https://substackcdn.com/image/fetch/$s_!Sdsq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d02e143-acb2-45f9-9e6b-4b0e97081da2_1666x449.png 1272w, https://substackcdn.com/image/fetch/$s_!Sdsq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d02e143-acb2-45f9-9e6b-4b0e97081da2_1666x449.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Suppose most rows have <code>event_type = 'PAGE_VIEW'</code>, while rare events such as <code>event_type = 'PURCHASE'</code> form a tiny minority. One query that hunts for recent purchase events by a single user can be:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nml1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0affd63-3ef6-491c-b822-775bcc8e17fe_1696x281.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nml1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0affd63-3ef6-491c-b822-775bcc8e17fe_1696x281.png 424w, https://substackcdn.com/image/fetch/$s_!nml1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0affd63-3ef6-491c-b822-775bcc8e17fe_1696x281.png 848w, https://substackcdn.com/image/fetch/$s_!nml1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0affd63-3ef6-491c-b822-775bcc8e17fe_1696x281.png 1272w, https://substackcdn.com/image/fetch/$s_!nml1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0affd63-3ef6-491c-b822-775bcc8e17fe_1696x281.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nml1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0affd63-3ef6-491c-b822-775bcc8e17fe_1696x281.png" width="1456" height="241" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b0affd63-3ef6-491c-b822-775bcc8e17fe_1696x281.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:241,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:64582,&quot;alt&quot;:&quot;SELECT event_id, event_time, event_type FROM page_events WHERE user_id = 733   AND event_type = 'PURCHASE'   AND event_time >= '2026-01-01';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186669901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0affd63-3ef6-491c-b822-775bcc8e17fe_1696x281.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT event_id, event_time, event_type FROM page_events WHERE user_id = 733   AND event_type = 'PURCHASE'   AND event_time >= '2026-01-01';" title="SELECT event_id, event_time, event_type FROM page_events WHERE user_id = 733   AND event_type = 'PURCHASE'   AND event_time >= '2026-01-01';" srcset="https://substackcdn.com/image/fetch/$s_!nml1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0affd63-3ef6-491c-b822-775bcc8e17fe_1696x281.png 424w, https://substackcdn.com/image/fetch/$s_!nml1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0affd63-3ef6-491c-b822-775bcc8e17fe_1696x281.png 848w, https://substackcdn.com/image/fetch/$s_!nml1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0affd63-3ef6-491c-b822-775bcc8e17fe_1696x281.png 1272w, https://substackcdn.com/image/fetch/$s_!nml1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0affd63-3ef6-491c-b822-775bcc8e17fe_1696x281.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>If statistics on <code>event_type</code> and <code>user_id</code> do not reflect the true distribution, MySQL may misjudge which index will touch fewer rows. The planner may pick <code>idx_page_events_type_time</code> when that index leads to many more reads than a targeted seek on <code>idx_page_events_user_time</code> for this query shape. Index hint use can nudge the engine toward the composite index on <code>user_id</code> and <code>event_time</code>.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!VWIA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0637193e-59c8-4f3e-842a-b09758600d48_1690x282.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!VWIA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0637193e-59c8-4f3e-842a-b09758600d48_1690x282.png 424w, https://substackcdn.com/image/fetch/$s_!VWIA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0637193e-59c8-4f3e-842a-b09758600d48_1690x282.png 848w, https://substackcdn.com/image/fetch/$s_!VWIA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0637193e-59c8-4f3e-842a-b09758600d48_1690x282.png 1272w, https://substackcdn.com/image/fetch/$s_!VWIA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0637193e-59c8-4f3e-842a-b09758600d48_1690x282.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!VWIA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0637193e-59c8-4f3e-842a-b09758600d48_1690x282.png" width="1456" height="243" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0637193e-59c8-4f3e-842a-b09758600d48_1690x282.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:243,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:80625,&quot;alt&quot;:&quot;SELECT event_id, event_time, event_type FROM page_events FORCE INDEX (idx_page_events_user_time) WHERE user_id = 733   AND event_type = 'PURCHASE'   AND event_time >= '2026-01-01';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186669901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0637193e-59c8-4f3e-842a-b09758600d48_1690x282.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT event_id, event_time, event_type FROM page_events FORCE INDEX (idx_page_events_user_time) WHERE user_id = 733   AND event_type = 'PURCHASE'   AND event_time >= '2026-01-01';" title="SELECT event_id, event_time, event_type FROM page_events FORCE INDEX (idx_page_events_user_time) WHERE user_id = 733   AND event_type = 'PURCHASE'   AND event_time >= '2026-01-01';" srcset="https://substackcdn.com/image/fetch/$s_!VWIA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0637193e-59c8-4f3e-842a-b09758600d48_1690x282.png 424w, https://substackcdn.com/image/fetch/$s_!VWIA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0637193e-59c8-4f3e-842a-b09758600d48_1690x282.png 848w, https://substackcdn.com/image/fetch/$s_!VWIA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0637193e-59c8-4f3e-842a-b09758600d48_1690x282.png 1272w, https://substackcdn.com/image/fetch/$s_!VWIA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0637193e-59c8-4f3e-842a-b09758600d48_1690x282.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>That hint tells MySQL to center its plan on <code>idx_page_events_user_time</code> for access to <code>page_events</code>. The predicate on <code>event_type</code> still applies, but now it filters rows that already passed the <code>user_id</code> and <code>event_time</code> filter driven by the composite index.</p><p>SQL Server runs into the same category of problem. Say there is a table for support tickets:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!huk2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef12beb-6a84-46aa-aee0-c0ea1a0b4afe_1711x624.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!huk2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef12beb-6a84-46aa-aee0-c0ea1a0b4afe_1711x624.png 424w, https://substackcdn.com/image/fetch/$s_!huk2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef12beb-6a84-46aa-aee0-c0ea1a0b4afe_1711x624.png 848w, https://substackcdn.com/image/fetch/$s_!huk2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef12beb-6a84-46aa-aee0-c0ea1a0b4afe_1711x624.png 1272w, https://substackcdn.com/image/fetch/$s_!huk2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef12beb-6a84-46aa-aee0-c0ea1a0b4afe_1711x624.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!huk2!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef12beb-6a84-46aa-aee0-c0ea1a0b4afe_1711x624.png" width="914" height="333.3337912087912" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3ef12beb-6a84-46aa-aee0-c0ea1a0b4afe_1711x624.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:531,&quot;width&quot;:1456,&quot;resizeWidth&quot;:914,&quot;bytes&quot;:121620,&quot;alt&quot;:&quot;CREATE TABLE Support.Tickets (     TicketId      BIGINT       NOT NULL PRIMARY KEY,     AccountId     BIGINT       NOT NULL,     CreatedAt     DATETIME2    NOT NULL,     Status        NVARCHAR(20) NOT NULL,     Priority      INT          NOT NULL ); GO  CREATE INDEX IX_Tickets_Account_CreatedAt     ON Support.Tickets (AccountId, CreatedAt);  CREATE INDEX IX_Tickets_Status_Priority     ON Support.Tickets (Status, Priority); GO&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186669901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef12beb-6a84-46aa-aee0-c0ea1a0b4afe_1711x624.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="CREATE TABLE Support.Tickets (     TicketId      BIGINT       NOT NULL PRIMARY KEY,     AccountId     BIGINT       NOT NULL,     CreatedAt     DATETIME2    NOT NULL,     Status        NVARCHAR(20) NOT NULL,     Priority      INT          NOT NULL ); GO  CREATE INDEX IX_Tickets_Account_CreatedAt     ON Support.Tickets (AccountId, CreatedAt);  CREATE INDEX IX_Tickets_Status_Priority     ON Support.Tickets (Status, Priority); GO" title="CREATE TABLE Support.Tickets (     TicketId      BIGINT       NOT NULL PRIMARY KEY,     AccountId     BIGINT       NOT NULL,     CreatedAt     DATETIME2    NOT NULL,     Status        NVARCHAR(20) NOT NULL,     Priority      INT          NOT NULL ); GO  CREATE INDEX IX_Tickets_Account_CreatedAt     ON Support.Tickets (AccountId, CreatedAt);  CREATE INDEX IX_Tickets_Status_Priority     ON Support.Tickets (Status, Priority); GO" srcset="https://substackcdn.com/image/fetch/$s_!huk2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef12beb-6a84-46aa-aee0-c0ea1a0b4afe_1711x624.png 424w, https://substackcdn.com/image/fetch/$s_!huk2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef12beb-6a84-46aa-aee0-c0ea1a0b4afe_1711x624.png 848w, https://substackcdn.com/image/fetch/$s_!huk2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef12beb-6a84-46aa-aee0-c0ea1a0b4afe_1711x624.png 1272w, https://substackcdn.com/image/fetch/$s_!huk2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef12beb-6a84-46aa-aee0-c0ea1a0b4afe_1711x624.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Think about a report that focuses on open tickets for a single account in a recent date range:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3_19!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8351425-4959-437f-b55f-42aad2f8c8a9_1688x280.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3_19!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8351425-4959-437f-b55f-42aad2f8c8a9_1688x280.png 424w, https://substackcdn.com/image/fetch/$s_!3_19!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8351425-4959-437f-b55f-42aad2f8c8a9_1688x280.png 848w, https://substackcdn.com/image/fetch/$s_!3_19!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8351425-4959-437f-b55f-42aad2f8c8a9_1688x280.png 1272w, https://substackcdn.com/image/fetch/$s_!3_19!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8351425-4959-437f-b55f-42aad2f8c8a9_1688x280.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3_19!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8351425-4959-437f-b55f-42aad2f8c8a9_1688x280.png" width="1456" height="242" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a8351425-4959-437f-b55f-42aad2f8c8a9_1688x280.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:242,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:67085,&quot;alt&quot;:&quot;SELECT TicketId, CreatedAt, Status, Priority FROM Support.Tickets WHERE AccountId = 555   AND Status = N'Open'   AND CreatedAt >= '2026-01-01';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186669901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8351425-4959-437f-b55f-42aad2f8c8a9_1688x280.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT TicketId, CreatedAt, Status, Priority FROM Support.Tickets WHERE AccountId = 555   AND Status = N'Open'   AND CreatedAt >= '2026-01-01';" title="SELECT TicketId, CreatedAt, Status, Priority FROM Support.Tickets WHERE AccountId = 555   AND Status = N'Open'   AND CreatedAt >= '2026-01-01';" srcset="https://substackcdn.com/image/fetch/$s_!3_19!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8351425-4959-437f-b55f-42aad2f8c8a9_1688x280.png 424w, https://substackcdn.com/image/fetch/$s_!3_19!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8351425-4959-437f-b55f-42aad2f8c8a9_1688x280.png 848w, https://substackcdn.com/image/fetch/$s_!3_19!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8351425-4959-437f-b55f-42aad2f8c8a9_1688x280.png 1272w, https://substackcdn.com/image/fetch/$s_!3_19!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8351425-4959-437f-b55f-42aad2f8c8a9_1688x280.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Depending on statistics, SQL Server may gravitate toward <code>IX_Tickets_Status_Priority</code> and then apply the <code>AccountId</code> filter as a residual predicate. If that index leads to a large scan, a table hint can direct the engine back toward the composite index centered on <code>AccountId</code>:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NpsD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e42469b-8919-4e36-be94-c6ccb627ab8e_1690x277.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NpsD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e42469b-8919-4e36-be94-c6ccb627ab8e_1690x277.png 424w, https://substackcdn.com/image/fetch/$s_!NpsD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e42469b-8919-4e36-be94-c6ccb627ab8e_1690x277.png 848w, https://substackcdn.com/image/fetch/$s_!NpsD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e42469b-8919-4e36-be94-c6ccb627ab8e_1690x277.png 1272w, https://substackcdn.com/image/fetch/$s_!NpsD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e42469b-8919-4e36-be94-c6ccb627ab8e_1690x277.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NpsD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e42469b-8919-4e36-be94-c6ccb627ab8e_1690x277.png" width="1456" height="239" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3e42469b-8919-4e36-be94-c6ccb627ab8e_1690x277.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:239,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:82307,&quot;alt&quot;:&quot;SELECT TicketId, CreatedAt, Status, Priority FROM Support.Tickets WITH (INDEX(IX_Tickets_Account_CreatedAt)) WHERE AccountId = 555   AND Status = N'Open'   AND CreatedAt >= '2026-01-01';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186669901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e42469b-8919-4e36-be94-c6ccb627ab8e_1690x277.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT TicketId, CreatedAt, Status, Priority FROM Support.Tickets WITH (INDEX(IX_Tickets_Account_CreatedAt)) WHERE AccountId = 555   AND Status = N'Open'   AND CreatedAt >= '2026-01-01';" title="SELECT TicketId, CreatedAt, Status, Priority FROM Support.Tickets WITH (INDEX(IX_Tickets_Account_CreatedAt)) WHERE AccountId = 555   AND Status = N'Open'   AND CreatedAt >= '2026-01-01';" srcset="https://substackcdn.com/image/fetch/$s_!NpsD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e42469b-8919-4e36-be94-c6ccb627ab8e_1690x277.png 424w, https://substackcdn.com/image/fetch/$s_!NpsD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e42469b-8919-4e36-be94-c6ccb627ab8e_1690x277.png 848w, https://substackcdn.com/image/fetch/$s_!NpsD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e42469b-8919-4e36-be94-c6ccb627ab8e_1690x277.png 1272w, https://substackcdn.com/image/fetch/$s_!NpsD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e42469b-8919-4e36-be94-c6ccb627ab8e_1690x277.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>This version tells the optimizer to base access on <code>IX_Tickets_Account_CreatedAt</code>. The statistics on that index still matter, but the hint removes other indexes from the competition for this table inside this query.</p><h4>Risks Of Forcing Index Choices</h4><p>Hints that steer index choice help with stubborn slow queries, yet they bring tradeoffs that are easy to overlook. Every hint hard codes an assumption about data distribution, workload habits, and index layout. As months pass, tables grow, data skews change, and administrators add or drop indexes. That hint can later turn into a liability when those assumptions no longer hold. Limited flexibility is one of the biggest side effects. Without hints, the optimizer can generate different plans for different parameter values, storage layouts, or statistics states. It can pick a selective index for rare values and a scan for common ones. With a forced index, that freedom shrinks. The planner must keep the hinted index in the plan, even for parameter values that would benefit from a different route through the table.</p><p>Schema changes get more awkward when index hints spread through application code, because dropping or renaming a hinted index affects not only the optimizer&#8217;s choices but can also cause queries to fail at parse time, forcing database engineers to comb through code bases and stored procedures to update or remove those hints before they can safely adjust index definitions.</p><p>Say we work with a customer table in MySQL that once relied on a narrow index on <code>last_name</code> to support specific reports:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Td9a!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68dc1c2-5bb4-473d-832d-cd7807d3ac63_1647x394.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Td9a!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68dc1c2-5bb4-473d-832d-cd7807d3ac63_1647x394.png 424w, https://substackcdn.com/image/fetch/$s_!Td9a!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68dc1c2-5bb4-473d-832d-cd7807d3ac63_1647x394.png 848w, https://substackcdn.com/image/fetch/$s_!Td9a!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68dc1c2-5bb4-473d-832d-cd7807d3ac63_1647x394.png 1272w, https://substackcdn.com/image/fetch/$s_!Td9a!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68dc1c2-5bb4-473d-832d-cd7807d3ac63_1647x394.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Td9a!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68dc1c2-5bb4-473d-832d-cd7807d3ac63_1647x394.png" width="1456" height="348" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c68dc1c2-5bb4-473d-832d-cd7807d3ac63_1647x394.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:348,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:93777,&quot;alt&quot;:&quot;CREATE TABLE customers (     customer_id BIGINT PRIMARY KEY,     last_name   VARCHAR(100) NOT NULL,     city        VARCHAR(100) NOT NULL,     email       VARCHAR(255) NOT NULL,     KEY idx_customers_last_name (last_name) );&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186669901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68dc1c2-5bb4-473d-832d-cd7807d3ac63_1647x394.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE TABLE customers (     customer_id BIGINT PRIMARY KEY,     last_name   VARCHAR(100) NOT NULL,     city        VARCHAR(100) NOT NULL,     email       VARCHAR(255) NOT NULL,     KEY idx_customers_last_name (last_name) );" title="CREATE TABLE customers (     customer_id BIGINT PRIMARY KEY,     last_name   VARCHAR(100) NOT NULL,     city        VARCHAR(100) NOT NULL,     email       VARCHAR(255) NOT NULL,     KEY idx_customers_last_name (last_name) );" srcset="https://substackcdn.com/image/fetch/$s_!Td9a!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68dc1c2-5bb4-473d-832d-cd7807d3ac63_1647x394.png 424w, https://substackcdn.com/image/fetch/$s_!Td9a!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68dc1c2-5bb4-473d-832d-cd7807d3ac63_1647x394.png 848w, https://substackcdn.com/image/fetch/$s_!Td9a!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68dc1c2-5bb4-473d-832d-cd7807d3ac63_1647x394.png 1272w, https://substackcdn.com/image/fetch/$s_!Td9a!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68dc1c2-5bb4-473d-832d-cd7807d3ac63_1647x394.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>At some point new reporting needs lead to a wider composite index.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dFv-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd03d15d7-eae9-4e33-8a88-a8db98fc43d9_1688x112.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dFv-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd03d15d7-eae9-4e33-8a88-a8db98fc43d9_1688x112.png 424w, https://substackcdn.com/image/fetch/$s_!dFv-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd03d15d7-eae9-4e33-8a88-a8db98fc43d9_1688x112.png 848w, https://substackcdn.com/image/fetch/$s_!dFv-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd03d15d7-eae9-4e33-8a88-a8db98fc43d9_1688x112.png 1272w, https://substackcdn.com/image/fetch/$s_!dFv-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd03d15d7-eae9-4e33-8a88-a8db98fc43d9_1688x112.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dFv-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd03d15d7-eae9-4e33-8a88-a8db98fc43d9_1688x112.png" width="1456" height="97" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d03d15d7-eae9-4e33-8a88-a8db98fc43d9_1688x112.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:97,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:33789,&quot;alt&quot;:&quot;CREATE INDEX idx_customers_last_name_city     ON customers (last_name, city);&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186669901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd03d15d7-eae9-4e33-8a88-a8db98fc43d9_1688x112.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE INDEX idx_customers_last_name_city     ON customers (last_name, city);" title="CREATE INDEX idx_customers_last_name_city     ON customers (last_name, city);" srcset="https://substackcdn.com/image/fetch/$s_!dFv-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd03d15d7-eae9-4e33-8a88-a8db98fc43d9_1688x112.png 424w, https://substackcdn.com/image/fetch/$s_!dFv-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd03d15d7-eae9-4e33-8a88-a8db98fc43d9_1688x112.png 848w, https://substackcdn.com/image/fetch/$s_!dFv-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd03d15d7-eae9-4e33-8a88-a8db98fc43d9_1688x112.png 1272w, https://substackcdn.com/image/fetch/$s_!dFv-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd03d15d7-eae9-4e33-8a88-a8db98fc43d9_1688x112.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>If an early version of the reporting query had a hint like this:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!iW0x!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fbbb140-0a18-4a98-bdab-939033908996_1697x169.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!iW0x!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fbbb140-0a18-4a98-bdab-939033908996_1697x169.png 424w, https://substackcdn.com/image/fetch/$s_!iW0x!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fbbb140-0a18-4a98-bdab-939033908996_1697x169.png 848w, https://substackcdn.com/image/fetch/$s_!iW0x!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fbbb140-0a18-4a98-bdab-939033908996_1697x169.png 1272w, https://substackcdn.com/image/fetch/$s_!iW0x!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fbbb140-0a18-4a98-bdab-939033908996_1697x169.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!iW0x!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fbbb140-0a18-4a98-bdab-939033908996_1697x169.png" width="1456" height="145" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1fbbb140-0a18-4a98-bdab-939033908996_1697x169.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:145,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:50799,&quot;alt&quot;:&quot;SELECT customer_id, last_name, city FROM customers FORCE INDEX (idx_customers_last_name) WHERE last_name = 'Nguyen';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186669901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fbbb140-0a18-4a98-bdab-939033908996_1697x169.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT customer_id, last_name, city FROM customers FORCE INDEX (idx_customers_last_name) WHERE last_name = 'Nguyen';" title="SELECT customer_id, last_name, city FROM customers FORCE INDEX (idx_customers_last_name) WHERE last_name = 'Nguyen';" srcset="https://substackcdn.com/image/fetch/$s_!iW0x!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fbbb140-0a18-4a98-bdab-939033908996_1697x169.png 424w, https://substackcdn.com/image/fetch/$s_!iW0x!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fbbb140-0a18-4a98-bdab-939033908996_1697x169.png 848w, https://substackcdn.com/image/fetch/$s_!iW0x!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fbbb140-0a18-4a98-bdab-939033908996_1697x169.png 1272w, https://substackcdn.com/image/fetch/$s_!iW0x!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fbbb140-0a18-4a98-bdab-939033908996_1697x169.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>And later the engineering group decides to drop <code>idx_customers_last_name</code> because <code>idx_customers_last_name_city</code> serves modern reporting better, that hint turns into a problem. Dropping the index without touching the query causes MySQL to raise an error, because the hint now references an index that no longer exists.</p><p>SQL Server faces similar issues with long lived table hints. Index hint use that names a specific index id or name needs a code change whenever index definitions move. Hint heavy code bases tend to slow down index tuning work, because every change requires coordination between database administrators and application developers.</p><p>For these reasons many engineering groups reserve index hints for specific pain points, document them carefully, and revisit them during major schema or workload changes. Better index choices, fresher statistics, and query rewrites usually carry less long term risk than a broad wave of forced index selection across an entire application.</p><h3>Hands On Index Hint Examples</h3><p>Query tuning makes more sense when you see the shape of an actual table, an actual query, and the plan that the optimizer picks. Index hints come into play when those plans stay slow even after you create reasonable indexes and let statistics update. The examples here stay focused on read queries, because those are common in reporting and web backends, and they give you a direct way to see how hints tie into <code>EXPLAIN</code> output and execution plans.</p><h4>MySQL Index Hint On A Read Query</h4><p>You will often see MySQL applications track customer purchases in a table that grows every day. Indexes help keep recent history queries fast, but the optimizer still has choices that do not always line up with how the data is distributed. As one case, take this table that records orders from an online store:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pLoD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc439d010-7e9e-45fe-8c22-56cd31ad584b_1665x507.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pLoD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc439d010-7e9e-45fe-8c22-56cd31ad584b_1665x507.png 424w, https://substackcdn.com/image/fetch/$s_!pLoD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc439d010-7e9e-45fe-8c22-56cd31ad584b_1665x507.png 848w, https://substackcdn.com/image/fetch/$s_!pLoD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc439d010-7e9e-45fe-8c22-56cd31ad584b_1665x507.png 1272w, https://substackcdn.com/image/fetch/$s_!pLoD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc439d010-7e9e-45fe-8c22-56cd31ad584b_1665x507.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!pLoD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc439d010-7e9e-45fe-8c22-56cd31ad584b_1665x507.png" width="1456" height="443" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c439d010-7e9e-45fe-8c22-56cd31ad584b_1665x507.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:443,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:116346,&quot;alt&quot;:&quot;CREATE TABLE orders (     id           BIGINT PRIMARY KEY,     customer_id  BIGINT NOT NULL,     order_date   DATETIME NOT NULL,     status       VARCHAR(20) NOT NULL,     total_amount DECIMAL(10,2) NOT NULL,     KEY idx_orders_customer_date (customer_id, order_date),     KEY idx_orders_status (status) );&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186669901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc439d010-7e9e-45fe-8c22-56cd31ad584b_1665x507.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE TABLE orders (     id           BIGINT PRIMARY KEY,     customer_id  BIGINT NOT NULL,     order_date   DATETIME NOT NULL,     status       VARCHAR(20) NOT NULL,     total_amount DECIMAL(10,2) NOT NULL,     KEY idx_orders_customer_date (customer_id, order_date),     KEY idx_orders_status (status) );" title="CREATE TABLE orders (     id           BIGINT PRIMARY KEY,     customer_id  BIGINT NOT NULL,     order_date   DATETIME NOT NULL,     status       VARCHAR(20) NOT NULL,     total_amount DECIMAL(10,2) NOT NULL,     KEY idx_orders_customer_date (customer_id, order_date),     KEY idx_orders_status (status) );" srcset="https://substackcdn.com/image/fetch/$s_!pLoD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc439d010-7e9e-45fe-8c22-56cd31ad584b_1665x507.png 424w, https://substackcdn.com/image/fetch/$s_!pLoD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc439d010-7e9e-45fe-8c22-56cd31ad584b_1665x507.png 848w, https://substackcdn.com/image/fetch/$s_!pLoD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc439d010-7e9e-45fe-8c22-56cd31ad584b_1665x507.png 1272w, https://substackcdn.com/image/fetch/$s_!pLoD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc439d010-7e9e-45fe-8c22-56cd31ad584b_1665x507.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Assume most traffic comes from screens that show recent orders for one customer at a time. A read query can look like this:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!JzY3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff87901a6-2397-4a76-8cdb-e65553350335_1664x337.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JzY3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff87901a6-2397-4a76-8cdb-e65553350335_1664x337.png 424w, https://substackcdn.com/image/fetch/$s_!JzY3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff87901a6-2397-4a76-8cdb-e65553350335_1664x337.png 848w, https://substackcdn.com/image/fetch/$s_!JzY3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff87901a6-2397-4a76-8cdb-e65553350335_1664x337.png 1272w, https://substackcdn.com/image/fetch/$s_!JzY3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff87901a6-2397-4a76-8cdb-e65553350335_1664x337.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JzY3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff87901a6-2397-4a76-8cdb-e65553350335_1664x337.png" width="1456" height="295" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f87901a6-2397-4a76-8cdb-e65553350335_1664x337.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:295,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:68295,&quot;alt&quot;:&quot;SELECT id, order_date, status, total_amount FROM orders WHERE customer_id = 1001   AND order_date >= '2026-01-01' ORDER BY order_date DESC LIMIT 50;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186669901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff87901a6-2397-4a76-8cdb-e65553350335_1664x337.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT id, order_date, status, total_amount FROM orders WHERE customer_id = 1001   AND order_date >= '2026-01-01' ORDER BY order_date DESC LIMIT 50;" title="SELECT id, order_date, status, total_amount FROM orders WHERE customer_id = 1001   AND order_date >= '2026-01-01' ORDER BY order_date DESC LIMIT 50;" srcset="https://substackcdn.com/image/fetch/$s_!JzY3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff87901a6-2397-4a76-8cdb-e65553350335_1664x337.png 424w, https://substackcdn.com/image/fetch/$s_!JzY3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff87901a6-2397-4a76-8cdb-e65553350335_1664x337.png 848w, https://substackcdn.com/image/fetch/$s_!JzY3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff87901a6-2397-4a76-8cdb-e65553350335_1664x337.png 1272w, https://substackcdn.com/image/fetch/$s_!JzY3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff87901a6-2397-4a76-8cdb-e65553350335_1664x337.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>On a large table with a good composite index, a plan that seeks into <code>idx_orders_customer_date</code>, walks back through recent dates for that customer, and stops after 50 rows keeps I O relatively low. That path lines up with the filter and the sort order. On some data sets, though, MySQL estimates that many rows will match and quietly chooses a full table scan.</p><p>Running <code>EXPLAIN</code> on the same text makes that choice easier to see:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6XwV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3086cddb-7ece-4a70-8e85-a843dfb5a48f_1669x390.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6XwV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3086cddb-7ece-4a70-8e85-a843dfb5a48f_1669x390.png 424w, https://substackcdn.com/image/fetch/$s_!6XwV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3086cddb-7ece-4a70-8e85-a843dfb5a48f_1669x390.png 848w, https://substackcdn.com/image/fetch/$s_!6XwV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3086cddb-7ece-4a70-8e85-a843dfb5a48f_1669x390.png 1272w, https://substackcdn.com/image/fetch/$s_!6XwV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3086cddb-7ece-4a70-8e85-a843dfb5a48f_1669x390.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6XwV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3086cddb-7ece-4a70-8e85-a843dfb5a48f_1669x390.png" width="1456" height="340" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3086cddb-7ece-4a70-8e85-a843dfb5a48f_1669x390.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:340,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:72882,&quot;alt&quot;:&quot;EXPLAIN SELECT id, order_date, status, total_amount FROM orders WHERE customer_id = 1001   AND order_date >= '2026-01-01' ORDER BY order_date DESC LIMIT 50;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186669901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3086cddb-7ece-4a70-8e85-a843dfb5a48f_1669x390.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="EXPLAIN SELECT id, order_date, status, total_amount FROM orders WHERE customer_id = 1001   AND order_date >= '2026-01-01' ORDER BY order_date DESC LIMIT 50;" title="EXPLAIN SELECT id, order_date, status, total_amount FROM orders WHERE customer_id = 1001   AND order_date >= '2026-01-01' ORDER BY order_date DESC LIMIT 50;" srcset="https://substackcdn.com/image/fetch/$s_!6XwV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3086cddb-7ece-4a70-8e85-a843dfb5a48f_1669x390.png 424w, https://substackcdn.com/image/fetch/$s_!6XwV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3086cddb-7ece-4a70-8e85-a843dfb5a48f_1669x390.png 848w, https://substackcdn.com/image/fetch/$s_!6XwV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3086cddb-7ece-4a70-8e85-a843dfb5a48f_1669x390.png 1272w, https://substackcdn.com/image/fetch/$s_!6XwV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3086cddb-7ece-4a70-8e85-a843dfb5a48f_1669x390.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>One run against a table with about a million rows can produce a row that reports <code>type = ALL</code>, <code>rows = 1000000</code>, and an <code>Extra</code> field that mentions a <code>filesort</code>. That combination means the engine walks the whole table, applies the filter to every row, sorts the survivors on <code>order_date</code>, and finally discards all but the first 50. On modest hardware that kind of plan can sit in the few hundred millisecond range whenever the buffer pool does not already hold the needed pages.</p><p>Index hints give you a way to push the optimizer toward the composite index. The hint becomes part of the query text and stays under version control with the rest of the application.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8UXR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99a1286b-6697-432c-84e5-1eb07a116d8a_1668x450.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8UXR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99a1286b-6697-432c-84e5-1eb07a116d8a_1668x450.png 424w, https://substackcdn.com/image/fetch/$s_!8UXR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99a1286b-6697-432c-84e5-1eb07a116d8a_1668x450.png 848w, https://substackcdn.com/image/fetch/$s_!8UXR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99a1286b-6697-432c-84e5-1eb07a116d8a_1668x450.png 1272w, https://substackcdn.com/image/fetch/$s_!8UXR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99a1286b-6697-432c-84e5-1eb07a116d8a_1668x450.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8UXR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99a1286b-6697-432c-84e5-1eb07a116d8a_1668x450.png" width="1456" height="393" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/99a1286b-6697-432c-84e5-1eb07a116d8a_1668x450.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:393,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:91134,&quot;alt&quot;:&quot;EXPLAIN SELECT id, order_date, status, total_amount FROM orders FORCE INDEX (idx_orders_customer_date) WHERE customer_id = 1001   AND order_date >= '2026-01-01' ORDER BY order_date DESC LIMIT 50;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186669901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99a1286b-6697-432c-84e5-1eb07a116d8a_1668x450.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="EXPLAIN SELECT id, order_date, status, total_amount FROM orders FORCE INDEX (idx_orders_customer_date) WHERE customer_id = 1001   AND order_date >= '2026-01-01' ORDER BY order_date DESC LIMIT 50;" title="EXPLAIN SELECT id, order_date, status, total_amount FROM orders FORCE INDEX (idx_orders_customer_date) WHERE customer_id = 1001   AND order_date >= '2026-01-01' ORDER BY order_date DESC LIMIT 50;" srcset="https://substackcdn.com/image/fetch/$s_!8UXR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99a1286b-6697-432c-84e5-1eb07a116d8a_1668x450.png 424w, https://substackcdn.com/image/fetch/$s_!8UXR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99a1286b-6697-432c-84e5-1eb07a116d8a_1668x450.png 848w, https://substackcdn.com/image/fetch/$s_!8UXR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99a1286b-6697-432c-84e5-1eb07a116d8a_1668x450.png 1272w, https://substackcdn.com/image/fetch/$s_!8UXR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99a1286b-6697-432c-84e5-1eb07a116d8a_1668x450.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>With that hint in place, <code>EXPLAIN</code> tends to report <code>type = range</code> or <code>ref</code>, the <code>key</code> column moves to <code>idx_orders_customer_date</code>, and the <code>rows</code> estimate drops sharply, sometimes into the low hundreds on realistic data. That change reflects a new access route where MySQL seeks directly into the slice of the index that matches <code>customer_id = 1001</code> and the date range. The engine still has to read base table pages for the selected rows, yet it avoids touching the entire table and also avoids sorting all matching rows in memory or on disk.</p><p>On a basic test instance with data in that range, queries that follow the hinted plan can land in the tens of milliseconds, because they only read the portion of the index that satisfies the filter and sort requirements. Timings always depend on hardware and load, but the mechanical reason stays the same. The hint makes the optimizer treat the composite index as the primary route into <code>orders</code> and narrows the number of candidate plans that involve full scans.</p><p>MySQL also supports the reverse move, where you tell the optimizer to ignore one or more indexes that tend to drag plans in the wrong direction. Queries that suffer from an unhelpful status index can be written like this:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8vxd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32710236-3425-40dd-ba65-bf2ede100a9b_1679x334.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8vxd!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32710236-3425-40dd-ba65-bf2ede100a9b_1679x334.png 424w, https://substackcdn.com/image/fetch/$s_!8vxd!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32710236-3425-40dd-ba65-bf2ede100a9b_1679x334.png 848w, https://substackcdn.com/image/fetch/$s_!8vxd!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32710236-3425-40dd-ba65-bf2ede100a9b_1679x334.png 1272w, https://substackcdn.com/image/fetch/$s_!8vxd!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32710236-3425-40dd-ba65-bf2ede100a9b_1679x334.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8vxd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32710236-3425-40dd-ba65-bf2ede100a9b_1679x334.png" width="1456" height="290" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/32710236-3425-40dd-ba65-bf2ede100a9b_1679x334.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:290,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:81805,&quot;alt&quot;:&quot;SELECT id, order_date, status, total_amount FROM orders IGNORE INDEX (idx_orders_status) WHERE customer_id = 1001   AND order_date >= '2026-01-01' ORDER BY order_date DESC LIMIT 50;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186669901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32710236-3425-40dd-ba65-bf2ede100a9b_1679x334.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT id, order_date, status, total_amount FROM orders IGNORE INDEX (idx_orders_status) WHERE customer_id = 1001   AND order_date >= '2026-01-01' ORDER BY order_date DESC LIMIT 50;" title="SELECT id, order_date, status, total_amount FROM orders IGNORE INDEX (idx_orders_status) WHERE customer_id = 1001   AND order_date >= '2026-01-01' ORDER BY order_date DESC LIMIT 50;" srcset="https://substackcdn.com/image/fetch/$s_!8vxd!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32710236-3425-40dd-ba65-bf2ede100a9b_1679x334.png 424w, https://substackcdn.com/image/fetch/$s_!8vxd!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32710236-3425-40dd-ba65-bf2ede100a9b_1679x334.png 848w, https://substackcdn.com/image/fetch/$s_!8vxd!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32710236-3425-40dd-ba65-bf2ede100a9b_1679x334.png 1272w, https://substackcdn.com/image/fetch/$s_!8vxd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32710236-3425-40dd-ba65-bf2ede100a9b_1679x334.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>That change forces the planner to treat <code>idx_orders_status</code> as unavailable for the duration of that statement. The remaining choices still go through cost based planning, but the hint removes one distracting option from the search space, which can guide MySQL toward access paths built on <code>idx_orders_customer_date</code> without fully locking it into a single index name.</p><h4>SQL Server Index Hint Through Table Hints</h4><p>SQL Server tends to appear in environments that lean on reporting queries and mixed workloads, so table hints interact with a wider range of query plans. Index hints come through the <code>WITH (hint options)</code> clause after a table reference and can nudge the optimizer toward indexes that track business filters more closely than its default choice.</p><p>Take this <code>Sales</code> table in a transactional database that feeds dashboards and nightly reports:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LQ5r!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53ffc74e-4402-4726-9b17-63e9a9bdabd8_1666x832.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LQ5r!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53ffc74e-4402-4726-9b17-63e9a9bdabd8_1666x832.png 424w, https://substackcdn.com/image/fetch/$s_!LQ5r!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53ffc74e-4402-4726-9b17-63e9a9bdabd8_1666x832.png 848w, https://substackcdn.com/image/fetch/$s_!LQ5r!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53ffc74e-4402-4726-9b17-63e9a9bdabd8_1666x832.png 1272w, https://substackcdn.com/image/fetch/$s_!LQ5r!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53ffc74e-4402-4726-9b17-63e9a9bdabd8_1666x832.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LQ5r!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53ffc74e-4402-4726-9b17-63e9a9bdabd8_1666x832.png" width="1456" height="727" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/53ffc74e-4402-4726-9b17-63e9a9bdabd8_1666x832.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:727,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:164028,&quot;alt&quot;:&quot;CREATE TABLE Sales (     Id           BIGINT       NOT NULL PRIMARY KEY,     CustomerId   BIGINT       NOT NULL,     OrderDate    DATETIME2    NOT NULL,     Status       NVARCHAR(20) NOT NULL,     TotalAmount  DECIMAL(10,2) NOT NULL ); GO  CREATE INDEX IX_Sales_Customer_OrderDate     ON Sales (CustomerId, OrderDate);  CREATE INDEX IX_Sales_Status_OrderDate     ON Sales (Status, OrderDate); GO&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186669901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53ffc74e-4402-4726-9b17-63e9a9bdabd8_1666x832.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE TABLE Sales (     Id           BIGINT       NOT NULL PRIMARY KEY,     CustomerId   BIGINT       NOT NULL,     OrderDate    DATETIME2    NOT NULL,     Status       NVARCHAR(20) NOT NULL,     TotalAmount  DECIMAL(10,2) NOT NULL ); GO  CREATE INDEX IX_Sales_Customer_OrderDate     ON Sales (CustomerId, OrderDate);  CREATE INDEX IX_Sales_Status_OrderDate     ON Sales (Status, OrderDate); GO" title="CREATE TABLE Sales (     Id           BIGINT       NOT NULL PRIMARY KEY,     CustomerId   BIGINT       NOT NULL,     OrderDate    DATETIME2    NOT NULL,     Status       NVARCHAR(20) NOT NULL,     TotalAmount  DECIMAL(10,2) NOT NULL ); GO  CREATE INDEX IX_Sales_Customer_OrderDate     ON Sales (CustomerId, OrderDate);  CREATE INDEX IX_Sales_Status_OrderDate     ON Sales (Status, OrderDate); GO" srcset="https://substackcdn.com/image/fetch/$s_!LQ5r!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53ffc74e-4402-4726-9b17-63e9a9bdabd8_1666x832.png 424w, https://substackcdn.com/image/fetch/$s_!LQ5r!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53ffc74e-4402-4726-9b17-63e9a9bdabd8_1666x832.png 848w, https://substackcdn.com/image/fetch/$s_!LQ5r!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53ffc74e-4402-4726-9b17-63e9a9bdabd8_1666x832.png 1272w, https://substackcdn.com/image/fetch/$s_!LQ5r!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53ffc74e-4402-4726-9b17-63e9a9bdabd8_1666x832.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Reporting screens that show recent completed orders for one customer pull from this table with a query similar to the MySQL example but written in T SQL:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!CJGo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cd7a69c-18b8-4bff-b761-6ae76ea6f282_1652x333.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!CJGo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cd7a69c-18b8-4bff-b761-6ae76ea6f282_1652x333.png 424w, https://substackcdn.com/image/fetch/$s_!CJGo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cd7a69c-18b8-4bff-b761-6ae76ea6f282_1652x333.png 848w, https://substackcdn.com/image/fetch/$s_!CJGo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cd7a69c-18b8-4bff-b761-6ae76ea6f282_1652x333.png 1272w, https://substackcdn.com/image/fetch/$s_!CJGo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cd7a69c-18b8-4bff-b761-6ae76ea6f282_1652x333.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!CJGo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cd7a69c-18b8-4bff-b761-6ae76ea6f282_1652x333.png" width="1456" height="293" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9cd7a69c-18b8-4bff-b761-6ae76ea6f282_1652x333.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:293,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:82143,&quot;alt&quot;:&quot;SELECT TOP 100 Id, OrderDate, Status, TotalAmount FROM Sales WHERE CustomerId = 1001   AND Status = N'Completed'   AND OrderDate >= '2026-01-01' ORDER BY OrderDate DESC;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186669901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cd7a69c-18b8-4bff-b761-6ae76ea6f282_1652x333.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT TOP 100 Id, OrderDate, Status, TotalAmount FROM Sales WHERE CustomerId = 1001   AND Status = N'Completed'   AND OrderDate >= '2026-01-01' ORDER BY OrderDate DESC;" title="SELECT TOP 100 Id, OrderDate, Status, TotalAmount FROM Sales WHERE CustomerId = 1001   AND Status = N'Completed'   AND OrderDate >= '2026-01-01' ORDER BY OrderDate DESC;" srcset="https://substackcdn.com/image/fetch/$s_!CJGo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cd7a69c-18b8-4bff-b761-6ae76ea6f282_1652x333.png 424w, https://substackcdn.com/image/fetch/$s_!CJGo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cd7a69c-18b8-4bff-b761-6ae76ea6f282_1652x333.png 848w, https://substackcdn.com/image/fetch/$s_!CJGo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cd7a69c-18b8-4bff-b761-6ae76ea6f282_1652x333.png 1272w, https://substackcdn.com/image/fetch/$s_!CJGo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cd7a69c-18b8-4bff-b761-6ae76ea6f282_1652x333.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>An estimated execution plan in SQL Server Management Studio can reveal that the optimizer chooses an index seek on <code>IX_Sales_Status_OrderDate</code>, applies a residual predicate on <code>CustomerId</code>, and performs key lookups into the clustered index to fetch <code>TotalAmount</code>. When nearly every row has <code>Status = 'Completed'</code>, this plan ends up touching a large portion of the table, because the first predicate in the composite index is not selective for this workload.</p><p>A table hint can steer the engine in a direction that fits the actual filter better. Placing an index hint right after the table name changes the planning step:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!v0zg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F852ef129-2a7a-444e-87f1-9eb35ade1d9a_1680x334.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!v0zg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F852ef129-2a7a-444e-87f1-9eb35ade1d9a_1680x334.png 424w, https://substackcdn.com/image/fetch/$s_!v0zg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F852ef129-2a7a-444e-87f1-9eb35ade1d9a_1680x334.png 848w, https://substackcdn.com/image/fetch/$s_!v0zg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F852ef129-2a7a-444e-87f1-9eb35ade1d9a_1680x334.png 1272w, https://substackcdn.com/image/fetch/$s_!v0zg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F852ef129-2a7a-444e-87f1-9eb35ade1d9a_1680x334.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!v0zg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F852ef129-2a7a-444e-87f1-9eb35ade1d9a_1680x334.png" width="1456" height="289" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/852ef129-2a7a-444e-87f1-9eb35ade1d9a_1680x334.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:289,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:99089,&quot;alt&quot;:&quot;SELECT TOP 100 Id, OrderDate, Status, TotalAmount FROM Sales WITH (INDEX(IX_Sales_Customer_OrderDate)) WHERE CustomerId = 1001   AND Status = N'Completed'   AND OrderDate >= '2026-01-01' ORDER BY OrderDate DESC;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186669901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F852ef129-2a7a-444e-87f1-9eb35ade1d9a_1680x334.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT TOP 100 Id, OrderDate, Status, TotalAmount FROM Sales WITH (INDEX(IX_Sales_Customer_OrderDate)) WHERE CustomerId = 1001   AND Status = N'Completed'   AND OrderDate >= '2026-01-01' ORDER BY OrderDate DESC;" title="SELECT TOP 100 Id, OrderDate, Status, TotalAmount FROM Sales WITH (INDEX(IX_Sales_Customer_OrderDate)) WHERE CustomerId = 1001   AND Status = N'Completed'   AND OrderDate >= '2026-01-01' ORDER BY OrderDate DESC;" srcset="https://substackcdn.com/image/fetch/$s_!v0zg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F852ef129-2a7a-444e-87f1-9eb35ade1d9a_1680x334.png 424w, https://substackcdn.com/image/fetch/$s_!v0zg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F852ef129-2a7a-444e-87f1-9eb35ade1d9a_1680x334.png 848w, https://substackcdn.com/image/fetch/$s_!v0zg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F852ef129-2a7a-444e-87f1-9eb35ade1d9a_1680x334.png 1272w, https://substackcdn.com/image/fetch/$s_!v0zg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F852ef129-2a7a-444e-87f1-9eb35ade1d9a_1680x334.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Execution plans generated from this text typically show an index seek on <code>IX_Sales_Customer_OrderDate</code> with a range on <code>CustomerId</code> and <code>OrderDate</code>, and then a predicate on <code>Status</code> applied to the index rows that pass the first filter. If the nonclustered index includes <code>Status</code> and <code>TotalAmount</code> as included columns, the engine can satisfy the entire query from the nonclustered index alone, without key lookups back to the clustered index.</p><p>On a medium sized table that holds millions of rows, that change can make a visible dent in query time, because the seek on a customer and date range greatly reduces the number of rows that participate in the plan. I O drops, and memory pressure from sorting or buffering rows falls as well, which benefits other queries that share the same instance.</p><p>SQL Server also exposes a <code>FORCESEEK</code> hint, which gives a finer tool for encouraging index seeks instead of scans. When a table has a wide index that the optimizer occasionally scans in full, even though a seek would work, <code>FORCESEEK</code> can channel the engine toward the seek operator:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!BBKu!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90725ef6-bdec-4051-ad50-bcae50748f76_1693x280.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!BBKu!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90725ef6-bdec-4051-ad50-bcae50748f76_1693x280.png 424w, https://substackcdn.com/image/fetch/$s_!BBKu!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90725ef6-bdec-4051-ad50-bcae50748f76_1693x280.png 848w, https://substackcdn.com/image/fetch/$s_!BBKu!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90725ef6-bdec-4051-ad50-bcae50748f76_1693x280.png 1272w, https://substackcdn.com/image/fetch/$s_!BBKu!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90725ef6-bdec-4051-ad50-bcae50748f76_1693x280.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!BBKu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90725ef6-bdec-4051-ad50-bcae50748f76_1693x280.png" width="1456" height="241" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/90725ef6-bdec-4051-ad50-bcae50748f76_1693x280.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:241,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:90094,&quot;alt&quot;:&quot;SELECT TOP 100 Id, OrderDate, Status, TotalAmount FROM Sales WITH (INDEX(IX_Sales_Customer_OrderDate), FORCESEEK) WHERE CustomerId = 1001   AND OrderDate >= '2026-01-01' ORDER BY OrderDate DESC;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186669901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90725ef6-bdec-4051-ad50-bcae50748f76_1693x280.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT TOP 100 Id, OrderDate, Status, TotalAmount FROM Sales WITH (INDEX(IX_Sales_Customer_OrderDate), FORCESEEK) WHERE CustomerId = 1001   AND OrderDate >= '2026-01-01' ORDER BY OrderDate DESC;" title="SELECT TOP 100 Id, OrderDate, Status, TotalAmount FROM Sales WITH (INDEX(IX_Sales_Customer_OrderDate), FORCESEEK) WHERE CustomerId = 1001   AND OrderDate >= '2026-01-01' ORDER BY OrderDate DESC;" srcset="https://substackcdn.com/image/fetch/$s_!BBKu!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90725ef6-bdec-4051-ad50-bcae50748f76_1693x280.png 424w, https://substackcdn.com/image/fetch/$s_!BBKu!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90725ef6-bdec-4051-ad50-bcae50748f76_1693x280.png 848w, https://substackcdn.com/image/fetch/$s_!BBKu!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90725ef6-bdec-4051-ad50-bcae50748f76_1693x280.png 1272w, https://substackcdn.com/image/fetch/$s_!BBKu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90725ef6-bdec-4051-ad50-bcae50748f76_1693x280.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>That form keeps the access path centered on <code>IX_Sales_Customer_OrderDate</code> and makes it far less likely that SQL Server will choose a scan on that index for this query. Combined with included columns that cover the select list, the plan can stay small and predictable, which helps when the same query text runs across many tenants or accounts with different data volumes but similar data shapes.</p><h3>Conclusion</h3><p>Index hints give you a direct way to influence how MySQL and SQL Server read tables by specifying which indexes the optimizer treats as entry points and how those choices interact with statistics and predicates. With the mechanics and examples in this article, you can map a slow query to its plan, see when the planner leans on a scan or a weak index, and then apply targeted hints or indexing and statistics changes that keep execution plans aligned with the filters and sort orders that matter for real workloads.</p><ol><li><p><em><a href="https://dev.mysql.com/doc/refman/8.0/en/index-hints.html">MySQL 8.0 Reference Manual Index Hints</a></em></p></li><li><p><em><a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-performance-optimizer-statistics.html">MySQL 8.0 Optimizer Statistics For InnoDB</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/sql/t-sql/queries/hints-transact-sql">SQL Server Query Hints Documentation</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/sql/relational-databases/statistics/statistics">SQL Server Statistics Documentation</a></em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share Alexander Obregon's Substack&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share Alexander Obregon's Substack</span></a></p></li></ol><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!52ox!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b198e3-a634-4a57-83ad-0cf19f86a5c9_306x306.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!52ox!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b198e3-a634-4a57-83ad-0cf19f86a5c9_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!52ox!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b198e3-a634-4a57-83ad-0cf19f86a5c9_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!52ox!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b198e3-a634-4a57-83ad-0cf19f86a5c9_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!52ox!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b198e3-a634-4a57-83ad-0cf19f86a5c9_306x306.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!52ox!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b198e3-a634-4a57-83ad-0cf19f86a5c9_306x306.png" width="306" height="306" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c7b198e3-a634-4a57-83ad-0cf19f86a5c9_306x306.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:306,&quot;width&quot;:306,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!52ox!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b198e3-a634-4a57-83ad-0cf19f86a5c9_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!52ox!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b198e3-a634-4a57-83ad-0cf19f86a5c9_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!52ox!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b198e3-a634-4a57-83ad-0cf19f86a5c9_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!52ox!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b198e3-a634-4a57-83ad-0cf19f86a5c9_306x306.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://freesvg.org/mono-sql">Image Source</a></figcaption></figure></div>]]></content:encoded></item><item><title><![CDATA[Partial Text Search In SQL With Indexes]]></title><description><![CDATA[Text search in relational databases usually starts from a basic need to filter rows by a name, title, or description when a user types only part of a word or phrase.]]></description><link>https://alexanderobregon.substack.com/p/partial-text-search-in-sql-with-indexes</link><guid isPermaLink="false">https://alexanderobregon.substack.com/p/partial-text-search-in-sql-with-indexes</guid><dc:creator><![CDATA[Alexander Obregon]]></dc:creator><pubDate>Tue, 03 Feb 2026 18:37:24 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!MV4U!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3519289-9d65-4c44-85fb-67a6a6b8545b_306x306.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!OR12!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b7180cd-b8df-4a7d-acf8-7bb40bb92351_800x373.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OR12!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b7180cd-b8df-4a7d-acf8-7bb40bb92351_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!OR12!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b7180cd-b8df-4a7d-acf8-7bb40bb92351_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!OR12!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b7180cd-b8df-4a7d-acf8-7bb40bb92351_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!OR12!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b7180cd-b8df-4a7d-acf8-7bb40bb92351_800x373.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!OR12!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b7180cd-b8df-4a7d-acf8-7bb40bb92351_800x373.png" width="800" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3b7180cd-b8df-4a7d-acf8-7bb40bb92351_800x373.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!OR12!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b7180cd-b8df-4a7d-acf8-7bb40bb92351_800x373.png 424w, https://substackcdn.com/image/fetch/$s_!OR12!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b7180cd-b8df-4a7d-acf8-7bb40bb92351_800x373.png 848w, https://substackcdn.com/image/fetch/$s_!OR12!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b7180cd-b8df-4a7d-acf8-7bb40bb92351_800x373.png 1272w, https://substackcdn.com/image/fetch/$s_!OR12!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b7180cd-b8df-4a7d-acf8-7bb40bb92351_800x373.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://commons.wikimedia.org/wiki/File:Sql_data_base_with_logo.png">Image Source</a></figcaption></figure></div><p>Text search in relational databases usually starts from a basic need to filter rows by a name, title, or description when a user types only part of a word or phrase. At a small scale, a filter with the <code>LIKE</code> operator can feel fast enough as queries read only a modest number of rows. As tables grow to millions of records, the same filter can force long scans across indexes or even the whole table if the search expression does not line up with how data is stored. Partial matches stay fast when the database pairs the right search operators, such as <code>LIKE</code>, full text search features, and trigram based extensions, with index structures that match the way text values are accessed.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>How Partial Text Search Works With Indexes</h3><p>Partial text search sits where human friendly filters meet storage rules inside a database engine. Users sometimes type part of a product name, a piece of an email address, or a single word from a long description, and expect the application to bring back the right rows with only that fragment to work with.</p><p>Index structures decide how much data the engine has to read to answer that request. B tree indexes on text columns hold values in sorted order and let the engine jump to a position that matches a given value or a range of values. When conditions with <code>LIKE</code> match the way values are laid out in that index, searches stay close to index range scans. When a condition hides the prefix or wraps the column in a function, the planner loses that benefit and drifts toward scans that touch a large part of the table.</p><h4>Basic Pattern Matching With LIKE</h4><p>Many SQL queries that search text start off with the <code>LIKE</code> operator. This operator treats text as a sequence of characters and compares it to a template that can include wildcard symbols along with normal letters or digits. <code>LIKE</code> understands two wildcard characters. The percent sign <code>%</code> stands for any sequence of characters, including an empty string. The underscore <code>_</code> stands for a single character. All other characters in the match string have to line up exactly, subject to collation and case sensitivity rules in the database.</p><p>Let&#8217;s look at a filter that looks up products by prefix and fits this model:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cUEO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd6956494-78ad-4ff1-8503-b4294d637917_1624x210.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cUEO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd6956494-78ad-4ff1-8503-b4294d637917_1624x210.png 424w, https://substackcdn.com/image/fetch/$s_!cUEO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd6956494-78ad-4ff1-8503-b4294d637917_1624x210.png 848w, https://substackcdn.com/image/fetch/$s_!cUEO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd6956494-78ad-4ff1-8503-b4294d637917_1624x210.png 1272w, https://substackcdn.com/image/fetch/$s_!cUEO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd6956494-78ad-4ff1-8503-b4294d637917_1624x210.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cUEO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd6956494-78ad-4ff1-8503-b4294d637917_1624x210.png" width="1456" height="188" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d6956494-78ad-4ff1-8503-b4294d637917_1624x210.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:188,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:37676,&quot;alt&quot;:&quot;SELECT product_id, name FROM products WHERE name LIKE 'Post%';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd6956494-78ad-4ff1-8503-b4294d637917_1624x210.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT product_id, name FROM products WHERE name LIKE 'Post%';" title="SELECT product_id, name FROM products WHERE name LIKE 'Post%';" srcset="https://substackcdn.com/image/fetch/$s_!cUEO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd6956494-78ad-4ff1-8503-b4294d637917_1624x210.png 424w, https://substackcdn.com/image/fetch/$s_!cUEO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd6956494-78ad-4ff1-8503-b4294d637917_1624x210.png 848w, https://substackcdn.com/image/fetch/$s_!cUEO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd6956494-78ad-4ff1-8503-b4294d637917_1624x210.png 1272w, https://substackcdn.com/image/fetch/$s_!cUEO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd6956494-78ad-4ff1-8503-b4294d637917_1624x210.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>This condition has a constant prefix <code>Post</code> followed by a wildcard. On a B tree index built on <code>products(name)</code>, many engines can treat this as a range that starts at <code>Post</code> and ends just before the next possible string after that prefix. That lets the engine scan only a slice of the index, not the entire table.</p><p>Suffix matches keep the wildcard at the front and fix the tail of the string instead:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Oo3h!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aa90b5b-3e45-4c2c-933d-ccf8d744642a_1623x208.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Oo3h!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aa90b5b-3e45-4c2c-933d-ccf8d744642a_1623x208.png 424w, https://substackcdn.com/image/fetch/$s_!Oo3h!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aa90b5b-3e45-4c2c-933d-ccf8d744642a_1623x208.png 848w, https://substackcdn.com/image/fetch/$s_!Oo3h!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aa90b5b-3e45-4c2c-933d-ccf8d744642a_1623x208.png 1272w, https://substackcdn.com/image/fetch/$s_!Oo3h!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aa90b5b-3e45-4c2c-933d-ccf8d744642a_1623x208.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Oo3h!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aa90b5b-3e45-4c2c-933d-ccf8d744642a_1623x208.png" width="1456" height="187" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5aa90b5b-3e45-4c2c-933d-ccf8d744642a_1623x208.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:187,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:37919,&quot;alt&quot;:&quot;SELECT product_id, name FROM products WHERE name LIKE '%Guide';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aa90b5b-3e45-4c2c-933d-ccf8d744642a_1623x208.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT product_id, name FROM products WHERE name LIKE '%Guide';" title="SELECT product_id, name FROM products WHERE name LIKE '%Guide';" srcset="https://substackcdn.com/image/fetch/$s_!Oo3h!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aa90b5b-3e45-4c2c-933d-ccf8d744642a_1623x208.png 424w, https://substackcdn.com/image/fetch/$s_!Oo3h!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aa90b5b-3e45-4c2c-933d-ccf8d744642a_1623x208.png 848w, https://substackcdn.com/image/fetch/$s_!Oo3h!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aa90b5b-3e45-4c2c-933d-ccf8d744642a_1623x208.png 1272w, https://substackcdn.com/image/fetch/$s_!Oo3h!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aa90b5b-3e45-4c2c-933d-ccf8d744642a_1623x208.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>This kind of match has no fixed beginning, so the engine cannot map it to a contiguous range in a B tree index on <code>name</code>. A common plan in that case is a full scan of the table or a scan of every entry in the index, with the <code>LIKE</code> comparison applied row by row.</p><p>Substring matches fall into the same category:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!eEbV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eaeec21-0ab1-4a93-b2e8-b2a37e680663_1626x210.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!eEbV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eaeec21-0ab1-4a93-b2e8-b2a37e680663_1626x210.png 424w, https://substackcdn.com/image/fetch/$s_!eEbV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eaeec21-0ab1-4a93-b2e8-b2a37e680663_1626x210.png 848w, https://substackcdn.com/image/fetch/$s_!eEbV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eaeec21-0ab1-4a93-b2e8-b2a37e680663_1626x210.png 1272w, https://substackcdn.com/image/fetch/$s_!eEbV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eaeec21-0ab1-4a93-b2e8-b2a37e680663_1626x210.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!eEbV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eaeec21-0ab1-4a93-b2e8-b2a37e680663_1626x210.png" width="1456" height="188" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2eaeec21-0ab1-4a93-b2e8-b2a37e680663_1626x210.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:188,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:40077,&quot;alt&quot;:&quot;SELECT product_id, name FROM products WHERE name LIKE '%search%';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eaeec21-0ab1-4a93-b2e8-b2a37e680663_1626x210.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT product_id, name FROM products WHERE name LIKE '%search%';" title="SELECT product_id, name FROM products WHERE name LIKE '%search%';" srcset="https://substackcdn.com/image/fetch/$s_!eEbV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eaeec21-0ab1-4a93-b2e8-b2a37e680663_1626x210.png 424w, https://substackcdn.com/image/fetch/$s_!eEbV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eaeec21-0ab1-4a93-b2e8-b2a37e680663_1626x210.png 848w, https://substackcdn.com/image/fetch/$s_!eEbV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eaeec21-0ab1-4a93-b2e8-b2a37e680663_1626x210.png 1272w, https://substackcdn.com/image/fetch/$s_!eEbV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eaeec21-0ab1-4a93-b2e8-b2a37e680663_1626x210.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>With wildcards on both sides, the database must examine many values to see which ones contain that middle fragment. A normal B tree index on <code>name</code> does not help much with that predicate.</p><p>The underscore wildcard matches a single character position and keeps the rest fixed:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!fT-a!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe1692ec-ab84-4d15-a3a6-138d1e9dfc0c_1608x216.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!fT-a!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe1692ec-ab84-4d15-a3a6-138d1e9dfc0c_1608x216.png 424w, https://substackcdn.com/image/fetch/$s_!fT-a!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe1692ec-ab84-4d15-a3a6-138d1e9dfc0c_1608x216.png 848w, https://substackcdn.com/image/fetch/$s_!fT-a!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe1692ec-ab84-4d15-a3a6-138d1e9dfc0c_1608x216.png 1272w, https://substackcdn.com/image/fetch/$s_!fT-a!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe1692ec-ab84-4d15-a3a6-138d1e9dfc0c_1608x216.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!fT-a!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe1692ec-ab84-4d15-a3a6-138d1e9dfc0c_1608x216.png" width="1456" height="196" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fe1692ec-ab84-4d15-a3a6-138d1e9dfc0c_1608x216.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:196,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:37674,&quot;alt&quot;:&quot;SELECT product_id, code FROM products WHERE code LIKE 'AB_123';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe1692ec-ab84-4d15-a3a6-138d1e9dfc0c_1608x216.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT product_id, code FROM products WHERE code LIKE 'AB_123';" title="SELECT product_id, code FROM products WHERE code LIKE 'AB_123';" srcset="https://substackcdn.com/image/fetch/$s_!fT-a!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe1692ec-ab84-4d15-a3a6-138d1e9dfc0c_1608x216.png 424w, https://substackcdn.com/image/fetch/$s_!fT-a!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe1692ec-ab84-4d15-a3a6-138d1e9dfc0c_1608x216.png 848w, https://substackcdn.com/image/fetch/$s_!fT-a!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe1692ec-ab84-4d15-a3a6-138d1e9dfc0c_1608x216.png 1272w, https://substackcdn.com/image/fetch/$s_!fT-a!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe1692ec-ab84-4d15-a3a6-138d1e9dfc0c_1608x216.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>This condition says that the first two characters must be <code>A</code> and <code>B</code>, the fourth through sixth characters must be <code>1</code>, <code>2</code>, and <code>3</code>, and the third character can be anything. Index support here depends on the database engine and collation rules. Some engines can still use the fixed prefix <code>AB</code> to find a starting point in the index and then apply the full match expression as a filter on rows that share that prefix.</p><p>Real applications frequently combine <code>LIKE</code> with other predicates. Many customer search pages only allow queries inside one city or one status flag:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!hfWX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39912c1f-02b3-4cfd-a98c-a0c47f4c8ed0_1631x278.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!hfWX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39912c1f-02b3-4cfd-a98c-a0c47f4c8ed0_1631x278.png 424w, https://substackcdn.com/image/fetch/$s_!hfWX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39912c1f-02b3-4cfd-a98c-a0c47f4c8ed0_1631x278.png 848w, https://substackcdn.com/image/fetch/$s_!hfWX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39912c1f-02b3-4cfd-a98c-a0c47f4c8ed0_1631x278.png 1272w, https://substackcdn.com/image/fetch/$s_!hfWX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39912c1f-02b3-4cfd-a98c-a0c47f4c8ed0_1631x278.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!hfWX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39912c1f-02b3-4cfd-a98c-a0c47f4c8ed0_1631x278.png" width="1456" height="248" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/39912c1f-02b3-4cfd-a98c-a0c47f4c8ed0_1631x278.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:248,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:54178,&quot;alt&quot;:&quot;SELECT customer_id, last_name, city FROM customers WHERE city = 'Eau Claire'   AND last_name LIKE 'Sm%';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39912c1f-02b3-4cfd-a98c-a0c47f4c8ed0_1631x278.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT customer_id, last_name, city FROM customers WHERE city = 'Eau Claire'   AND last_name LIKE 'Sm%';" title="SELECT customer_id, last_name, city FROM customers WHERE city = 'Eau Claire'   AND last_name LIKE 'Sm%';" srcset="https://substackcdn.com/image/fetch/$s_!hfWX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39912c1f-02b3-4cfd-a98c-a0c47f4c8ed0_1631x278.png 424w, https://substackcdn.com/image/fetch/$s_!hfWX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39912c1f-02b3-4cfd-a98c-a0c47f4c8ed0_1631x278.png 848w, https://substackcdn.com/image/fetch/$s_!hfWX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39912c1f-02b3-4cfd-a98c-a0c47f4c8ed0_1631x278.png 1272w, https://substackcdn.com/image/fetch/$s_!hfWX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39912c1f-02b3-4cfd-a98c-a0c47f4c8ed0_1631x278.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The equality on <code>city</code> narrows the set of rows that need to be checked, and the prefix <code>Sm%</code> can align with an index on <code>(city, last_name)</code> in that order. Index choices on multiple columns become important in those cases, because the leftmost column or columns in the index define how the B tree is laid out and what range of <code>LIKE</code> prefixes keep the scan narrow.</p><h4>Limits Of Plain LIKE On Large Tables</h4><p>Large tables change how <code>LIKE</code> behaves in practice. Filters that feel acceptable on a few thousand rows can turn into heavy operations on tens of millions of rows when the index cannot help.</p><p>Search strings that begin with <code>%</code> cause trouble first. Take this for an example:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4KTW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc453ffb8-da57-4c68-8919-ff2c6cb9a3c7_1616x209.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4KTW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc453ffb8-da57-4c68-8919-ff2c6cb9a3c7_1616x209.png 424w, https://substackcdn.com/image/fetch/$s_!4KTW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc453ffb8-da57-4c68-8919-ff2c6cb9a3c7_1616x209.png 848w, https://substackcdn.com/image/fetch/$s_!4KTW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc453ffb8-da57-4c68-8919-ff2c6cb9a3c7_1616x209.png 1272w, https://substackcdn.com/image/fetch/$s_!4KTW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc453ffb8-da57-4c68-8919-ff2c6cb9a3c7_1616x209.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4KTW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc453ffb8-da57-4c68-8919-ff2c6cb9a3c7_1616x209.png" width="1456" height="188" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c453ffb8-da57-4c68-8919-ff2c6cb9a3c7_1616x209.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:188,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:36729,&quot;alt&quot;:&quot;SELECT product_id, name FROM products WHERE name LIKE '%Pro';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc453ffb8-da57-4c68-8919-ff2c6cb9a3c7_1616x209.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT product_id, name FROM products WHERE name LIKE '%Pro';" title="SELECT product_id, name FROM products WHERE name LIKE '%Pro';" srcset="https://substackcdn.com/image/fetch/$s_!4KTW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc453ffb8-da57-4c68-8919-ff2c6cb9a3c7_1616x209.png 424w, https://substackcdn.com/image/fetch/$s_!4KTW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc453ffb8-da57-4c68-8919-ff2c6cb9a3c7_1616x209.png 848w, https://substackcdn.com/image/fetch/$s_!4KTW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc453ffb8-da57-4c68-8919-ff2c6cb9a3c7_1616x209.png 1272w, https://substackcdn.com/image/fetch/$s_!4KTW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc453ffb8-da57-4c68-8919-ff2c6cb9a3c7_1616x209.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Here, the code runs against a large catalog, the engine has no way to guess where names that end in <code>Pro</code> sit inside a sorted index. That usually leads to a plan that touches the full table or the full index and applies the <code>LIKE</code> check to every candidate value. With enough rows, that plan pushes latency up and increases load on the storage layer.</p><p>Computed expressions inside the predicate cause similar trouble. Many codebases want case insensitive search and reach for <code>LOWER</code> or <code>UPPER</code> calls:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9duq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d3448ee-4f96-4cec-8a91-076afadcc45f_1631x208.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9duq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d3448ee-4f96-4cec-8a91-076afadcc45f_1631x208.png 424w, https://substackcdn.com/image/fetch/$s_!9duq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d3448ee-4f96-4cec-8a91-076afadcc45f_1631x208.png 848w, https://substackcdn.com/image/fetch/$s_!9duq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d3448ee-4f96-4cec-8a91-076afadcc45f_1631x208.png 1272w, https://substackcdn.com/image/fetch/$s_!9duq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d3448ee-4f96-4cec-8a91-076afadcc45f_1631x208.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9duq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d3448ee-4f96-4cec-8a91-076afadcc45f_1631x208.png" width="1456" height="186" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1d3448ee-4f96-4cec-8a91-076afadcc45f_1631x208.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:186,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:43720,&quot;alt&quot;:&quot;SELECT product_id, name FROM products WHERE LOWER(name) LIKE 'post%';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d3448ee-4f96-4cec-8a91-076afadcc45f_1631x208.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT product_id, name FROM products WHERE LOWER(name) LIKE 'post%';" title="SELECT product_id, name FROM products WHERE LOWER(name) LIKE 'post%';" srcset="https://substackcdn.com/image/fetch/$s_!9duq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d3448ee-4f96-4cec-8a91-076afadcc45f_1631x208.png 424w, https://substackcdn.com/image/fetch/$s_!9duq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d3448ee-4f96-4cec-8a91-076afadcc45f_1631x208.png 848w, https://substackcdn.com/image/fetch/$s_!9duq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d3448ee-4f96-4cec-8a91-076afadcc45f_1631x208.png 1272w, https://substackcdn.com/image/fetch/$s_!9duq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d3448ee-4f96-4cec-8a91-076afadcc45f_1631x208.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Plain indexes on <code>name</code> no longer match that condition, because the stored index entries hold the original text while the predicate runs against lowercased text. Several engines, including PostgreSQL and Oracle, support indexes built on expressions such as <code>LOWER(name)</code> so that queries can use the transformed text directly in the index. When that index is not present, the engine falls back to a scan.</p><p>Function calls that trim whitespace or remove accents have the same effect. Any expression that changes the value the index sees makes a normal index on the base column less helpful unless a matching index on the expression exists.</p><p>Multiple wildcards in the middle of a search string increase the cost as well.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!k1pI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02df2ad-48ad-4008-9eba-e176a88d0813_1635x211.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!k1pI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02df2ad-48ad-4008-9eba-e176a88d0813_1635x211.png 424w, https://substackcdn.com/image/fetch/$s_!k1pI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02df2ad-48ad-4008-9eba-e176a88d0813_1635x211.png 848w, https://substackcdn.com/image/fetch/$s_!k1pI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02df2ad-48ad-4008-9eba-e176a88d0813_1635x211.png 1272w, https://substackcdn.com/image/fetch/$s_!k1pI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02df2ad-48ad-4008-9eba-e176a88d0813_1635x211.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!k1pI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02df2ad-48ad-4008-9eba-e176a88d0813_1635x211.png" width="1456" height="188" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d02df2ad-48ad-4008-9eba-e176a88d0813_1635x211.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:188,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:44451,&quot;alt&quot;:&quot;SELECT product_id, name FROM products WHERE name LIKE '%pro%search%guide%';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02df2ad-48ad-4008-9eba-e176a88d0813_1635x211.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT product_id, name FROM products WHERE name LIKE '%pro%search%guide%';" title="SELECT product_id, name FROM products WHERE name LIKE '%pro%search%guide%';" srcset="https://substackcdn.com/image/fetch/$s_!k1pI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02df2ad-48ad-4008-9eba-e176a88d0813_1635x211.png 424w, https://substackcdn.com/image/fetch/$s_!k1pI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02df2ad-48ad-4008-9eba-e176a88d0813_1635x211.png 848w, https://substackcdn.com/image/fetch/$s_!k1pI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02df2ad-48ad-4008-9eba-e176a88d0813_1635x211.png 1272w, https://substackcdn.com/image/fetch/$s_!k1pI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd02df2ad-48ad-4008-9eba-e176a88d0813_1635x211.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>This asks the engine to find rows containing <code>pro</code>, then later <code>search</code>, then later <code>guide</code>, all in that order, without any help from an index on the original column. For long strings and large tables, that type of search scans many bytes per row.</p><p>Parameter driven search interfaces bring an extra challenge. One field can accept a prefix search, while another allows a contains search, depending on user input:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!n9GL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20e3990e-5c8c-4e5f-9d48-d591c8c610ec_1609x169.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!n9GL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20e3990e-5c8c-4e5f-9d48-d591c8c610ec_1609x169.png 424w, https://substackcdn.com/image/fetch/$s_!n9GL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20e3990e-5c8c-4e5f-9d48-d591c8c610ec_1609x169.png 848w, https://substackcdn.com/image/fetch/$s_!n9GL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20e3990e-5c8c-4e5f-9d48-d591c8c610ec_1609x169.png 1272w, https://substackcdn.com/image/fetch/$s_!n9GL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20e3990e-5c8c-4e5f-9d48-d591c8c610ec_1609x169.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!n9GL!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20e3990e-5c8c-4e5f-9d48-d591c8c610ec_1609x169.png" width="1080" height="113.48901098901099" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/20e3990e-5c8c-4e5f-9d48-d591c8c610ec_1609x169.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:153,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1080,&quot;bytes&quot;:46196,&quot;alt&quot;:&quot;SELECT product_id, name FROM products WHERE (:prefix IS NOT NULL AND name LIKE :prefix || '%')    OR (:contains IS NOT NULL AND name LIKE '%' || :contains || '%');&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20e3990e-5c8c-4e5f-9d48-d591c8c610ec_1609x169.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="SELECT product_id, name FROM products WHERE (:prefix IS NOT NULL AND name LIKE :prefix || '%')    OR (:contains IS NOT NULL AND name LIKE '%' || :contains || '%');" title="SELECT product_id, name FROM products WHERE (:prefix IS NOT NULL AND name LIKE :prefix || '%')    OR (:contains IS NOT NULL AND name LIKE '%' || :contains || '%');" srcset="https://substackcdn.com/image/fetch/$s_!n9GL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20e3990e-5c8c-4e5f-9d48-d591c8c610ec_1609x169.png 424w, https://substackcdn.com/image/fetch/$s_!n9GL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20e3990e-5c8c-4e5f-9d48-d591c8c610ec_1609x169.png 848w, https://substackcdn.com/image/fetch/$s_!n9GL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20e3990e-5c8c-4e5f-9d48-d591c8c610ec_1609x169.png 1272w, https://substackcdn.com/image/fetch/$s_!n9GL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20e3990e-5c8c-4e5f-9d48-d591c8c610ec_1609x169.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Query forms like this lead to different access patterns at runtime. Prefix search can work with an index, while contains search does not. Query planners inspect the constants or bound parameters at execution time and decide how much of the index can help, but parts of the filter that begin with a wildcard tend to fall back to scanning.</p><p>These limits pushed database vendors to add features geared toward indexed text search such as full text search and trigram style matching. Those features build on different index structures and search strategies and address cases where <code>LIKE</code> alone cannot keep partial text search fast on very large data sets.</p><h3>Indexing Strategies For Partial Matches</h3><p>When the index structure lines up with how the query engine reads text, partial text search becomes much more practical. Queries that only need prefixes can lean on ordinary B tree indexes, while word based search benefits from inverted indexes that map terms to rows. Substring search and fuzzy matches depend on structures that store short character fragments such as trigrams. All of these live side by side and address different types of text filters that appear in real applications.</p><p>Databases bring these strategies to life through specific index types and operators. B tree indexes back up <code>LIKE 'prefix%'</code> filters. Full text indexes support boolean and ranked search against large bodies of text. Trigram indexes and related features in extensions push partial and typo tolerant search toward index backed plans instead of table wide scans. Getting comfortable with how each one works makes it easier to pick the right option for a given search screen or API endpoint.</p><h4>Prefix Search With B Tree Indexes</h4><p>Many user interfaces ask for the beginning of a value rather than the full string. Autocomplete on customer last names, suggestions for city names, or product codes typed into a search box all fit this kind of prefix search. B tree indexes handle these cases well when queries keep the leading characters fixed and place wildcard symbols only at the end.</p><p>B trees store values in sorted order and arrange index entries in pages that connect through a tree of pointers. When a query looks for a range of values, the engine can follow the tree down to the first matching value and walk through pages in order until the range ends. Prefix searches match that behavior closely, because strings that share a prefix sit next to one another in the index.</p><p>This PostgreSQL example makes this a bit easier to see:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!uTRF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb9ad78a-32e0-4892-b185-77c9e584a6ad_1600x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!uTRF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb9ad78a-32e0-4892-b185-77c9e584a6ad_1600x559.png 424w, https://substackcdn.com/image/fetch/$s_!uTRF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb9ad78a-32e0-4892-b185-77c9e584a6ad_1600x559.png 848w, https://substackcdn.com/image/fetch/$s_!uTRF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb9ad78a-32e0-4892-b185-77c9e584a6ad_1600x559.png 1272w, https://substackcdn.com/image/fetch/$s_!uTRF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb9ad78a-32e0-4892-b185-77c9e584a6ad_1600x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!uTRF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb9ad78a-32e0-4892-b185-77c9e584a6ad_1600x559.png" width="1456" height="509" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cb9ad78a-32e0-4892-b185-77c9e584a6ad_1600x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:509,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:85431,&quot;alt&quot;:&quot;CREATE TABLE customers (     id        bigserial PRIMARY KEY,     last_name text      NOT NULL,     city      text      NOT NULL );  CREATE INDEX idx_customers_last_name     ON customers (last_name);&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb9ad78a-32e0-4892-b185-77c9e584a6ad_1600x559.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE TABLE customers (     id        bigserial PRIMARY KEY,     last_name text      NOT NULL,     city      text      NOT NULL );  CREATE INDEX idx_customers_last_name     ON customers (last_name);" title="CREATE TABLE customers (     id        bigserial PRIMARY KEY,     last_name text      NOT NULL,     city      text      NOT NULL );  CREATE INDEX idx_customers_last_name     ON customers (last_name);" srcset="https://substackcdn.com/image/fetch/$s_!uTRF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb9ad78a-32e0-4892-b185-77c9e584a6ad_1600x559.png 424w, https://substackcdn.com/image/fetch/$s_!uTRF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb9ad78a-32e0-4892-b185-77c9e584a6ad_1600x559.png 848w, https://substackcdn.com/image/fetch/$s_!uTRF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb9ad78a-32e0-4892-b185-77c9e584a6ad_1600x559.png 1272w, https://substackcdn.com/image/fetch/$s_!uTRF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb9ad78a-32e0-4892-b185-77c9e584a6ad_1600x559.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>With that index in place, a query that matches a prefix has a good chance of using it:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qeMX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff289a990-8ae6-4802-bc4e-ff51672e84f1_1642x282.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qeMX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff289a990-8ae6-4802-bc4e-ff51672e84f1_1642x282.png 424w, https://substackcdn.com/image/fetch/$s_!qeMX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff289a990-8ae6-4802-bc4e-ff51672e84f1_1642x282.png 848w, https://substackcdn.com/image/fetch/$s_!qeMX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff289a990-8ae6-4802-bc4e-ff51672e84f1_1642x282.png 1272w, https://substackcdn.com/image/fetch/$s_!qeMX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff289a990-8ae6-4802-bc4e-ff51672e84f1_1642x282.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qeMX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff289a990-8ae6-4802-bc4e-ff51672e84f1_1642x282.png" width="1456" height="250" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f289a990-8ae6-4802-bc4e-ff51672e84f1_1642x282.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:250,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:43400,&quot;alt&quot;:&quot;EXPLAIN ANALYZE SELECT id, last_name FROM customers WHERE last_name LIKE 'Sm%';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff289a990-8ae6-4802-bc4e-ff51672e84f1_1642x282.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="EXPLAIN ANALYZE SELECT id, last_name FROM customers WHERE last_name LIKE 'Sm%';" title="EXPLAIN ANALYZE SELECT id, last_name FROM customers WHERE last_name LIKE 'Sm%';" srcset="https://substackcdn.com/image/fetch/$s_!qeMX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff289a990-8ae6-4802-bc4e-ff51672e84f1_1642x282.png 424w, https://substackcdn.com/image/fetch/$s_!qeMX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff289a990-8ae6-4802-bc4e-ff51672e84f1_1642x282.png 848w, https://substackcdn.com/image/fetch/$s_!qeMX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff289a990-8ae6-4802-bc4e-ff51672e84f1_1642x282.png 1272w, https://substackcdn.com/image/fetch/$s_!qeMX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff289a990-8ae6-4802-bc4e-ff51672e84f1_1642x282.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The planner can translate <code>LIKE 'Sm%'</code> into a range that starts just before <code>Sm</code> and ends just before the next possible prefix after <code>Sm</code>. That plan scans only the range of index entries whose <code>last_name</code> values begin with <code>Sm</code> and then retrieves the matching rows from the table.</p><p>MySQL and SQL Server apply the same basic idea to <code>LIKE 'prefix%'</code> on indexed <code>CHAR</code> or <code>VARCHAR</code> columns with compatible collations. A <code>WHERE last_name LIKE 'Sm%'</code> condition on a plain index <code>INDEX(last_name)</code> typically turns into an index range scan rather than a full read of the table. Case sensitivity plays a part here. Columns stored with case insensitive collations can match <code>Sm%</code> for both <code>Smith</code> and <code>smith</code>, while case sensitive collations treat those as separate values.</p><p>Queries that blend equality and prefix search across multiple columns can still benefit from B tree indexes when the index column order aligns with the filter. Let&#8217;s say we are working with a support tool that keeps customer records with status flags and names:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!DHn2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38528482-df8b-404e-9f6b-3f15a72baba9_1601x560.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!DHn2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38528482-df8b-404e-9f6b-3f15a72baba9_1601x560.png 424w, https://substackcdn.com/image/fetch/$s_!DHn2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38528482-df8b-404e-9f6b-3f15a72baba9_1601x560.png 848w, https://substackcdn.com/image/fetch/$s_!DHn2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38528482-df8b-404e-9f6b-3f15a72baba9_1601x560.png 1272w, https://substackcdn.com/image/fetch/$s_!DHn2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38528482-df8b-404e-9f6b-3f15a72baba9_1601x560.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!DHn2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38528482-df8b-404e-9f6b-3f15a72baba9_1601x560.png" width="1456" height="509" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/38528482-df8b-404e-9f6b-3f15a72baba9_1601x560.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:509,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:90616,&quot;alt&quot;:&quot;CREATE TABLE support_customers (     id        bigserial PRIMARY KEY,     status    text      NOT NULL,     last_name text      NOT NULL );  CREATE INDEX idx_support_status_last     ON support_customers (status, last_name);&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38528482-df8b-404e-9f6b-3f15a72baba9_1601x560.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE TABLE support_customers (     id        bigserial PRIMARY KEY,     status    text      NOT NULL,     last_name text      NOT NULL );  CREATE INDEX idx_support_status_last     ON support_customers (status, last_name);" title="CREATE TABLE support_customers (     id        bigserial PRIMARY KEY,     status    text      NOT NULL,     last_name text      NOT NULL );  CREATE INDEX idx_support_status_last     ON support_customers (status, last_name);" srcset="https://substackcdn.com/image/fetch/$s_!DHn2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38528482-df8b-404e-9f6b-3f15a72baba9_1601x560.png 424w, https://substackcdn.com/image/fetch/$s_!DHn2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38528482-df8b-404e-9f6b-3f15a72baba9_1601x560.png 848w, https://substackcdn.com/image/fetch/$s_!DHn2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38528482-df8b-404e-9f6b-3f15a72baba9_1601x560.png 1272w, https://substackcdn.com/image/fetch/$s_!DHn2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38528482-df8b-404e-9f6b-3f15a72baba9_1601x560.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Searches against active customers whose last names start with <code>Sm</code> fit this index:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mZT1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74710920-faa9-46a2-b208-e6b6e6f11e31_1641x283.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mZT1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74710920-faa9-46a2-b208-e6b6e6f11e31_1641x283.png 424w, https://substackcdn.com/image/fetch/$s_!mZT1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74710920-faa9-46a2-b208-e6b6e6f11e31_1641x283.png 848w, https://substackcdn.com/image/fetch/$s_!mZT1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74710920-faa9-46a2-b208-e6b6e6f11e31_1641x283.png 1272w, https://substackcdn.com/image/fetch/$s_!mZT1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74710920-faa9-46a2-b208-e6b6e6f11e31_1641x283.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mZT1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74710920-faa9-46a2-b208-e6b6e6f11e31_1641x283.png" width="1456" height="251" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/74710920-faa9-46a2-b208-e6b6e6f11e31_1641x283.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:251,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:53937,&quot;alt&quot;:&quot;SELECT id, status, last_name FROM support_customers WHERE status = 'active'   AND last_name LIKE 'Sm%';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74710920-faa9-46a2-b208-e6b6e6f11e31_1641x283.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT id, status, last_name FROM support_customers WHERE status = 'active'   AND last_name LIKE 'Sm%';" title="SELECT id, status, last_name FROM support_customers WHERE status = 'active'   AND last_name LIKE 'Sm%';" srcset="https://substackcdn.com/image/fetch/$s_!mZT1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74710920-faa9-46a2-b208-e6b6e6f11e31_1641x283.png 424w, https://substackcdn.com/image/fetch/$s_!mZT1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74710920-faa9-46a2-b208-e6b6e6f11e31_1641x283.png 848w, https://substackcdn.com/image/fetch/$s_!mZT1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74710920-faa9-46a2-b208-e6b6e6f11e31_1641x283.png 1272w, https://substackcdn.com/image/fetch/$s_!mZT1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74710920-faa9-46a2-b208-e6b6e6f11e31_1641x283.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The index is sorted first by <code>status</code>, then by <code>last_name</code>. With <code>status = 'active'</code> fixed and <code>last_name LIKE 'Sm%'</code> expressing a prefix, the engine can walk a compact section of the index that covers exactly those rows instead of scanning inactive records.</p><p>Case insensitive prefix search benefits from functional indexes in engines that support them. PostgreSQL, for example, can store lowercased names directly in the index:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YQBX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c8daec4-96c7-484e-9e0c-3d493d7d2d01_1625x141.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YQBX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c8daec4-96c7-484e-9e0c-3d493d7d2d01_1625x141.png 424w, https://substackcdn.com/image/fetch/$s_!YQBX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c8daec4-96c7-484e-9e0c-3d493d7d2d01_1625x141.png 848w, https://substackcdn.com/image/fetch/$s_!YQBX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c8daec4-96c7-484e-9e0c-3d493d7d2d01_1625x141.png 1272w, https://substackcdn.com/image/fetch/$s_!YQBX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c8daec4-96c7-484e-9e0c-3d493d7d2d01_1625x141.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YQBX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c8daec4-96c7-484e-9e0c-3d493d7d2d01_1625x141.png" width="1456" height="126" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3c8daec4-96c7-484e-9e0c-3d493d7d2d01_1625x141.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:126,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:37734,&quot;alt&quot;:&quot;CREATE INDEX idx_customers_last_lower     ON customers (LOWER(last_name));&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c8daec4-96c7-484e-9e0c-3d493d7d2d01_1625x141.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE INDEX idx_customers_last_lower     ON customers (LOWER(last_name));" title="CREATE INDEX idx_customers_last_lower     ON customers (LOWER(last_name));" srcset="https://substackcdn.com/image/fetch/$s_!YQBX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c8daec4-96c7-484e-9e0c-3d493d7d2d01_1625x141.png 424w, https://substackcdn.com/image/fetch/$s_!YQBX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c8daec4-96c7-484e-9e0c-3d493d7d2d01_1625x141.png 848w, https://substackcdn.com/image/fetch/$s_!YQBX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c8daec4-96c7-484e-9e0c-3d493d7d2d01_1625x141.png 1272w, https://substackcdn.com/image/fetch/$s_!YQBX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c8daec4-96c7-484e-9e0c-3d493d7d2d01_1625x141.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Queries then match against the transformed value:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!rJXf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2be71f6f-b000-47ac-867c-461864d2f2de_1633x210.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rJXf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2be71f6f-b000-47ac-867c-461864d2f2de_1633x210.png 424w, https://substackcdn.com/image/fetch/$s_!rJXf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2be71f6f-b000-47ac-867c-461864d2f2de_1633x210.png 848w, https://substackcdn.com/image/fetch/$s_!rJXf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2be71f6f-b000-47ac-867c-461864d2f2de_1633x210.png 1272w, https://substackcdn.com/image/fetch/$s_!rJXf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2be71f6f-b000-47ac-867c-461864d2f2de_1633x210.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rJXf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2be71f6f-b000-47ac-867c-461864d2f2de_1633x210.png" width="1456" height="187" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2be71f6f-b000-47ac-867c-461864d2f2de_1633x210.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:187,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:42245,&quot;alt&quot;:&quot;SELECT id, last_name FROM customers WHERE LOWER(last_name) LIKE 'sm%';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2be71f6f-b000-47ac-867c-461864d2f2de_1633x210.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT id, last_name FROM customers WHERE LOWER(last_name) LIKE 'sm%';" title="SELECT id, last_name FROM customers WHERE LOWER(last_name) LIKE 'sm%';" srcset="https://substackcdn.com/image/fetch/$s_!rJXf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2be71f6f-b000-47ac-867c-461864d2f2de_1633x210.png 424w, https://substackcdn.com/image/fetch/$s_!rJXf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2be71f6f-b000-47ac-867c-461864d2f2de_1633x210.png 848w, https://substackcdn.com/image/fetch/$s_!rJXf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2be71f6f-b000-47ac-867c-461864d2f2de_1633x210.png 1272w, https://substackcdn.com/image/fetch/$s_!rJXf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2be71f6f-b000-47ac-867c-461864d2f2de_1633x210.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>This query form avoids lowercasing every row during the scan and lets the engine use the index entries that already hold <code>LOWER(last_name)</code> values. Similar ideas appear in Oracle and in other systems that support indexes on expressions, with syntax differences but the same basic goal of lining up the index with the expression in the filter.</p><p>Prefix search with B trees works best when queries avoid wildcards at the front of the string and limit function calls on the indexed column. As long as filters keep that prefix intact and match the expression stored in the index, the engine has a good chance of treating the request as a range scan instead of a full scan.</p><h4>Full Text Search For Word Based Matching</h4><p>Applications that deal with article bodies, support tickets, product descriptions, or chat transcripts need more than prefixes. Users expect to type a few words and see matching content with some ranking applied. B tree indexes on plain text columns do not have enough structure for that style of search, so engines rely on full text indexes that act more like term dictionaries. Full text indexes usually work by breaking text into tokens and building an inverted index. Instead of mapping each row to its values, the system maps each term to the list of rows that contain it, sometimes with extra details such as term frequency or positions. This layout mirrors what search engines use and makes it possible to fetch candidate rows quickly for a given set of terms.</p><p>PostgreSQL supports full text search through <code>tsvector</code> and <code>tsquery</code> types combined with GIN or GiST indexes. A typical table can look like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!89Bo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36832639-1c93-46c8-85bb-aa98092f697b_1663x508.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!89Bo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36832639-1c93-46c8-85bb-aa98092f697b_1663x508.png 424w, https://substackcdn.com/image/fetch/$s_!89Bo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36832639-1c93-46c8-85bb-aa98092f697b_1663x508.png 848w, https://substackcdn.com/image/fetch/$s_!89Bo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36832639-1c93-46c8-85bb-aa98092f697b_1663x508.png 1272w, https://substackcdn.com/image/fetch/$s_!89Bo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36832639-1c93-46c8-85bb-aa98092f697b_1663x508.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!89Bo!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36832639-1c93-46c8-85bb-aa98092f697b_1663x508.png" width="834" height="254.89697802197801" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/36832639-1c93-46c8-85bb-aa98092f697b_1663x508.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:445,&quot;width&quot;:1456,&quot;resizeWidth&quot;:834,&quot;bytes&quot;:89382,&quot;alt&quot;:&quot;CREATE TABLE articles (     id      bigserial PRIMARY KEY,     title   text      NOT NULL,     body    text      NOT NULL );  CREATE INDEX idx_articles_search     ON articles     USING GIN (to_tsvector('english', title || ' ' || body));&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36832639-1c93-46c8-85bb-aa98092f697b_1663x508.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="CREATE TABLE articles (     id      bigserial PRIMARY KEY,     title   text      NOT NULL,     body    text      NOT NULL );  CREATE INDEX idx_articles_search     ON articles     USING GIN (to_tsvector('english', title || ' ' || body));" title="CREATE TABLE articles (     id      bigserial PRIMARY KEY,     title   text      NOT NULL,     body    text      NOT NULL );  CREATE INDEX idx_articles_search     ON articles     USING GIN (to_tsvector('english', title || ' ' || body));" srcset="https://substackcdn.com/image/fetch/$s_!89Bo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36832639-1c93-46c8-85bb-aa98092f697b_1663x508.png 424w, https://substackcdn.com/image/fetch/$s_!89Bo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36832639-1c93-46c8-85bb-aa98092f697b_1663x508.png 848w, https://substackcdn.com/image/fetch/$s_!89Bo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36832639-1c93-46c8-85bb-aa98092f697b_1663x508.png 1272w, https://substackcdn.com/image/fetch/$s_!89Bo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36832639-1c93-46c8-85bb-aa98092f697b_1663x508.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The index stores the <code>tsvector</code> representation of <code>title || ' ' || body</code>. That representation contains normalized tokens for words in the text, usually lowercased and stripped of suffixes according to the selected configuration such as <code>english</code>. Queries build a <code>tsquery</code> value and apply the <code>@@</code> operator to match those tokens:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!OlWg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f0adf8-3966-4ce8-8f61-8b4d469be4cc_1691x224.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OlWg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f0adf8-3966-4ce8-8f61-8b4d469be4cc_1691x224.png 424w, https://substackcdn.com/image/fetch/$s_!OlWg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f0adf8-3966-4ce8-8f61-8b4d469be4cc_1691x224.png 848w, https://substackcdn.com/image/fetch/$s_!OlWg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f0adf8-3966-4ce8-8f61-8b4d469be4cc_1691x224.png 1272w, https://substackcdn.com/image/fetch/$s_!OlWg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f0adf8-3966-4ce8-8f61-8b4d469be4cc_1691x224.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!OlWg!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f0adf8-3966-4ce8-8f61-8b4d469be4cc_1691x224.png" width="858" height="113.73214285714286" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b2f0adf8-3966-4ce8-8f61-8b4d469be4cc_1691x224.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:193,&quot;width&quot;:1456,&quot;resizeWidth&quot;:858,&quot;bytes&quot;:61279,&quot;alt&quot;:&quot;SELECT id, title FROM articles WHERE to_tsvector('english', title || ' ' || body)       @@ plainto_tsquery('english', 'partial text search');&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f0adf8-3966-4ce8-8f61-8b4d469be4cc_1691x224.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="SELECT id, title FROM articles WHERE to_tsvector('english', title || ' ' || body)       @@ plainto_tsquery('english', 'partial text search');" title="SELECT id, title FROM articles WHERE to_tsvector('english', title || ' ' || body)       @@ plainto_tsquery('english', 'partial text search');" srcset="https://substackcdn.com/image/fetch/$s_!OlWg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f0adf8-3966-4ce8-8f61-8b4d469be4cc_1691x224.png 424w, https://substackcdn.com/image/fetch/$s_!OlWg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f0adf8-3966-4ce8-8f61-8b4d469be4cc_1691x224.png 848w, https://substackcdn.com/image/fetch/$s_!OlWg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f0adf8-3966-4ce8-8f61-8b4d469be4cc_1691x224.png 1272w, https://substackcdn.com/image/fetch/$s_!OlWg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f0adf8-3966-4ce8-8f61-8b4d469be4cc_1691x224.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>In this code, the planner uses the GIN index to find rows whose token sets satisfy the <code>tsquery</code>. Binary operators in the query string such as <code>&amp;</code>, <code>|</code>, and <code>!</code> express logical conditions between terms, and weights or ranking functions can adjust ordering in result sets.</p><p>MySQL follows a similar concept with <code>FULLTEXT</code> indexes on <code>CHAR</code>, <code>VARCHAR</code>, and <code>TEXT</code> columns. Developers can declare an index like this on an InnoDB table:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!voHY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cb93401-d470-480b-9e1e-fdf085377930_1687x338.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!voHY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cb93401-d470-480b-9e1e-fdf085377930_1687x338.png 424w, https://substackcdn.com/image/fetch/$s_!voHY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cb93401-d470-480b-9e1e-fdf085377930_1687x338.png 848w, https://substackcdn.com/image/fetch/$s_!voHY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cb93401-d470-480b-9e1e-fdf085377930_1687x338.png 1272w, https://substackcdn.com/image/fetch/$s_!voHY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cb93401-d470-480b-9e1e-fdf085377930_1687x338.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!voHY!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cb93401-d470-480b-9e1e-fdf085377930_1687x338.png" width="842" height="168.86263736263737" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1cb93401-d470-480b-9e1e-fdf085377930_1687x338.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:292,&quot;width&quot;:1456,&quot;resizeWidth&quot;:842,&quot;bytes&quot;:80227,&quot;alt&quot;:&quot;CREATE TABLE product_docs (     id          BIGINT PRIMARY KEY AUTO_INCREMENT,     title       VARCHAR(255) NOT NULL,     description TEXT         NOT NULL,     FULLTEXT INDEX ft_title_desc (title, description) ) ENGINE = InnoDB;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cb93401-d470-480b-9e1e-fdf085377930_1687x338.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="CREATE TABLE product_docs (     id          BIGINT PRIMARY KEY AUTO_INCREMENT,     title       VARCHAR(255) NOT NULL,     description TEXT         NOT NULL,     FULLTEXT INDEX ft_title_desc (title, description) ) ENGINE = InnoDB;" title="CREATE TABLE product_docs (     id          BIGINT PRIMARY KEY AUTO_INCREMENT,     title       VARCHAR(255) NOT NULL,     description TEXT         NOT NULL,     FULLTEXT INDEX ft_title_desc (title, description) ) ENGINE = InnoDB;" srcset="https://substackcdn.com/image/fetch/$s_!voHY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cb93401-d470-480b-9e1e-fdf085377930_1687x338.png 424w, https://substackcdn.com/image/fetch/$s_!voHY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cb93401-d470-480b-9e1e-fdf085377930_1687x338.png 848w, https://substackcdn.com/image/fetch/$s_!voHY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cb93401-d470-480b-9e1e-fdf085377930_1687x338.png 1272w, https://substackcdn.com/image/fetch/$s_!voHY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cb93401-d470-480b-9e1e-fdf085377930_1687x338.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Queries then use <code>MATCH</code> and <code>AGAINST</code>:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Z_nH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab11c4c6-56be-45a3-9420-5955a048cf82_1693x224.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Z_nH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab11c4c6-56be-45a3-9420-5955a048cf82_1693x224.png 424w, https://substackcdn.com/image/fetch/$s_!Z_nH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab11c4c6-56be-45a3-9420-5955a048cf82_1693x224.png 848w, https://substackcdn.com/image/fetch/$s_!Z_nH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab11c4c6-56be-45a3-9420-5955a048cf82_1693x224.png 1272w, https://substackcdn.com/image/fetch/$s_!Z_nH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab11c4c6-56be-45a3-9420-5955a048cf82_1693x224.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Z_nH!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab11c4c6-56be-45a3-9420-5955a048cf82_1693x224.png" width="852" height="112.93681318681318" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ab11c4c6-56be-45a3-9420-5955a048cf82_1693x224.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:193,&quot;width&quot;:1456,&quot;resizeWidth&quot;:852,&quot;bytes&quot;:56481,&quot;alt&quot;:&quot;SELECT id, title FROM product_docs WHERE MATCH(title, description)       AGAINST ('partial text search' IN NATURAL LANGUAGE MODE);&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab11c4c6-56be-45a3-9420-5955a048cf82_1693x224.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="SELECT id, title FROM product_docs WHERE MATCH(title, description)       AGAINST ('partial text search' IN NATURAL LANGUAGE MODE);" title="SELECT id, title FROM product_docs WHERE MATCH(title, description)       AGAINST ('partial text search' IN NATURAL LANGUAGE MODE);" srcset="https://substackcdn.com/image/fetch/$s_!Z_nH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab11c4c6-56be-45a3-9420-5955a048cf82_1693x224.png 424w, https://substackcdn.com/image/fetch/$s_!Z_nH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab11c4c6-56be-45a3-9420-5955a048cf82_1693x224.png 848w, https://substackcdn.com/image/fetch/$s_!Z_nH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab11c4c6-56be-45a3-9420-5955a048cf82_1693x224.png 1272w, https://substackcdn.com/image/fetch/$s_!Z_nH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab11c4c6-56be-45a3-9420-5955a048cf82_1693x224.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The <code>FULLTEXT</code> index organizes terms and document references so that searches over large text columns can avoid row by row scans. Boolean mode queries add operators such as <code>+</code> and <code>-</code> to require or exclude terms.</p><p>SQLite handles full text search through FTS virtual tables like FTS5. Rather than adding an index to an existing table, the database stores content inside a special table that maintains its own full text index:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Fjdn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ce20054-4859-46f0-9fd0-fa0253153a8f_1641x139.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Fjdn!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ce20054-4859-46f0-9fd0-fa0253153a8f_1641x139.png 424w, https://substackcdn.com/image/fetch/$s_!Fjdn!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ce20054-4859-46f0-9fd0-fa0253153a8f_1641x139.png 848w, https://substackcdn.com/image/fetch/$s_!Fjdn!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ce20054-4859-46f0-9fd0-fa0253153a8f_1641x139.png 1272w, https://substackcdn.com/image/fetch/$s_!Fjdn!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ce20054-4859-46f0-9fd0-fa0253153a8f_1641x139.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Fjdn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ce20054-4859-46f0-9fd0-fa0253153a8f_1641x139.png" width="728" height="61.5" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6ce20054-4859-46f0-9fd0-fa0253153a8f_1641x139.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:123,&quot;width&quot;:1456,&quot;resizeWidth&quot;:728,&quot;bytes&quot;:30835,&quot;alt&quot;:&quot;CREATE VIRTUAL TABLE notes_fts USING fts5(title, body);&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ce20054-4859-46f0-9fd0-fa0253153a8f_1641x139.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE VIRTUAL TABLE notes_fts USING fts5(title, body);" title="CREATE VIRTUAL TABLE notes_fts USING fts5(title, body);" srcset="https://substackcdn.com/image/fetch/$s_!Fjdn!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ce20054-4859-46f0-9fd0-fa0253153a8f_1641x139.png 424w, https://substackcdn.com/image/fetch/$s_!Fjdn!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ce20054-4859-46f0-9fd0-fa0253153a8f_1641x139.png 848w, https://substackcdn.com/image/fetch/$s_!Fjdn!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ce20054-4859-46f0-9fd0-fa0253153a8f_1641x139.png 1272w, https://substackcdn.com/image/fetch/$s_!Fjdn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ce20054-4859-46f0-9fd0-fa0253153a8f_1641x139.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Inserts and updates into <code>notes_fts</code> update the full text index automatically. Queries use a <code>MATCH</code> operator with a search string:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5c5V!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d186362-1389-47d4-b60b-6c2333bf8da7_1633x208.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5c5V!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d186362-1389-47d4-b60b-6c2333bf8da7_1633x208.png 424w, https://substackcdn.com/image/fetch/$s_!5c5V!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d186362-1389-47d4-b60b-6c2333bf8da7_1633x208.png 848w, https://substackcdn.com/image/fetch/$s_!5c5V!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d186362-1389-47d4-b60b-6c2333bf8da7_1633x208.png 1272w, https://substackcdn.com/image/fetch/$s_!5c5V!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d186362-1389-47d4-b60b-6c2333bf8da7_1633x208.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5c5V!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d186362-1389-47d4-b60b-6c2333bf8da7_1633x208.png" width="1456" height="185" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6d186362-1389-47d4-b60b-6c2333bf8da7_1633x208.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:185,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:47435,&quot;alt&quot;:&quot;SELECT rowid, title FROM notes_fts WHERE notes_fts MATCH 'NEAR(partial search, 2)';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d186362-1389-47d4-b60b-6c2333bf8da7_1633x208.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT rowid, title FROM notes_fts WHERE notes_fts MATCH 'NEAR(partial search, 2)';" title="SELECT rowid, title FROM notes_fts WHERE notes_fts MATCH 'NEAR(partial search, 2)';" srcset="https://substackcdn.com/image/fetch/$s_!5c5V!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d186362-1389-47d4-b60b-6c2333bf8da7_1633x208.png 424w, https://substackcdn.com/image/fetch/$s_!5c5V!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d186362-1389-47d4-b60b-6c2333bf8da7_1633x208.png 848w, https://substackcdn.com/image/fetch/$s_!5c5V!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d186362-1389-47d4-b60b-6c2333bf8da7_1633x208.png 1272w, https://substackcdn.com/image/fetch/$s_!5c5V!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d186362-1389-47d4-b60b-6c2333bf8da7_1633x208.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>That search looks for rows where <code>partial</code> and <code>search</code> occur within two tokens of each other. Ranking functions such as <code>bm25</code> can then order results by relevance.</p><p>All of these systems trade more complex index maintenance at write time for fast reads on multi word queries. Tokenization, stopword lists, stemming, and language configurations vary between engines, but the shared goal is to treat text as a bag of terms and store those terms in a structure that makes term based search fast.</p><h4>Trigram Indexes For Fuzzy Partial Search</h4><p>Substring search and typo tolerance call for a different strategy. Queries that look for fragments buried inside a word or short string, or that need some tolerance for misspellings, do not map neatly onto plain full text indexes. Many engines address that niche with trigram techniques and related extensions.</p><p>Trigram methods break strings into overlapping chunks of three characters. For the text <code>search</code>, one common set of trigrams would be <code>sea</code>, <code>ear</code>, <code>arc</code>, <code>rch</code>, sometimes with extra boundary markers added at the start and end. When an index stores these trigrams, the engine can compare trigram sets between the search term and stored values, then treat higher overlap as a sign of similarity or as a way to filter candidates for a substring match.</p><p>PostgreSQL brings trigram support through the <code>pg_trgm</code> extension. After enabling the extension, indexes can store trigram data and speed up <code>LIKE</code>, <code>ILIKE</code>, regular expression matches, and similarity operations.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zLfX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb87785d1-a8b1-4acd-a3b0-4c50ff2dbf1c_1598x775.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zLfX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb87785d1-a8b1-4acd-a3b0-4c50ff2dbf1c_1598x775.png 424w, https://substackcdn.com/image/fetch/$s_!zLfX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb87785d1-a8b1-4acd-a3b0-4c50ff2dbf1c_1598x775.png 848w, https://substackcdn.com/image/fetch/$s_!zLfX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb87785d1-a8b1-4acd-a3b0-4c50ff2dbf1c_1598x775.png 1272w, https://substackcdn.com/image/fetch/$s_!zLfX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb87785d1-a8b1-4acd-a3b0-4c50ff2dbf1c_1598x775.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zLfX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb87785d1-a8b1-4acd-a3b0-4c50ff2dbf1c_1598x775.png" width="1456" height="706" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b87785d1-a8b1-4acd-a3b0-4c50ff2dbf1c_1598x775.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:706,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:127248,&quot;alt&quot;:&quot;CREATE EXTENSION IF NOT EXISTS pg_trgm;  CREATE TABLE products_trgm (     id          bigserial PRIMARY KEY,     name        text      NOT NULL,     description text      NOT NULL );  CREATE INDEX idx_products_trgm_name     ON products_trgm     USING GIN (name gin_trgm_ops);&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb87785d1-a8b1-4acd-a3b0-4c50ff2dbf1c_1598x775.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CREATE EXTENSION IF NOT EXISTS pg_trgm;  CREATE TABLE products_trgm (     id          bigserial PRIMARY KEY,     name        text      NOT NULL,     description text      NOT NULL );  CREATE INDEX idx_products_trgm_name     ON products_trgm     USING GIN (name gin_trgm_ops);" title="CREATE EXTENSION IF NOT EXISTS pg_trgm;  CREATE TABLE products_trgm (     id          bigserial PRIMARY KEY,     name        text      NOT NULL,     description text      NOT NULL );  CREATE INDEX idx_products_trgm_name     ON products_trgm     USING GIN (name gin_trgm_ops);" srcset="https://substackcdn.com/image/fetch/$s_!zLfX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb87785d1-a8b1-4acd-a3b0-4c50ff2dbf1c_1598x775.png 424w, https://substackcdn.com/image/fetch/$s_!zLfX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb87785d1-a8b1-4acd-a3b0-4c50ff2dbf1c_1598x775.png 848w, https://substackcdn.com/image/fetch/$s_!zLfX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb87785d1-a8b1-4acd-a3b0-4c50ff2dbf1c_1598x775.png 1272w, https://substackcdn.com/image/fetch/$s_!zLfX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb87785d1-a8b1-4acd-a3b0-4c50ff2dbf1c_1598x775.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>With that GIN index on <code>name</code>, substring search across a large product catalog becomes more practical:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7nrG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4df2370b-7cf4-4453-bf1d-83bb085355b6_1633x280.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7nrG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4df2370b-7cf4-4453-bf1d-83bb085355b6_1633x280.png 424w, https://substackcdn.com/image/fetch/$s_!7nrG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4df2370b-7cf4-4453-bf1d-83bb085355b6_1633x280.png 848w, https://substackcdn.com/image/fetch/$s_!7nrG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4df2370b-7cf4-4453-bf1d-83bb085355b6_1633x280.png 1272w, https://substackcdn.com/image/fetch/$s_!7nrG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4df2370b-7cf4-4453-bf1d-83bb085355b6_1633x280.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7nrG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4df2370b-7cf4-4453-bf1d-83bb085355b6_1633x280.png" width="1456" height="250" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4df2370b-7cf4-4453-bf1d-83bb085355b6_1633x280.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:250,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:44304,&quot;alt&quot;:&quot;EXPLAIN ANALYZE SELECT id, name FROM products_trgm WHERE name ILIKE '%graph%';&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4df2370b-7cf4-4453-bf1d-83bb085355b6_1633x280.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="EXPLAIN ANALYZE SELECT id, name FROM products_trgm WHERE name ILIKE '%graph%';" title="EXPLAIN ANALYZE SELECT id, name FROM products_trgm WHERE name ILIKE '%graph%';" srcset="https://substackcdn.com/image/fetch/$s_!7nrG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4df2370b-7cf4-4453-bf1d-83bb085355b6_1633x280.png 424w, https://substackcdn.com/image/fetch/$s_!7nrG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4df2370b-7cf4-4453-bf1d-83bb085355b6_1633x280.png 848w, https://substackcdn.com/image/fetch/$s_!7nrG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4df2370b-7cf4-4453-bf1d-83bb085355b6_1633x280.png 1272w, https://substackcdn.com/image/fetch/$s_!7nrG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4df2370b-7cf4-4453-bf1d-83bb085355b6_1633x280.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The planner can rely on trigram overlap between <code>graph</code> and stored names to pick a reduced set of candidates. Only those rows whose trigram sets share enough three character chunks with <code>graph</code> need to pass through the full <code>ILIKE</code> comparison. This narrows the search space even though the wildcard sits at the beginning of the <code>LIKE</code> pattern.</p><p>The <code>pg_trgm</code> extension also supplies similarity operators and functions, which support nearest neighbor style queries. One search for names close to a user supplied term uses one of these operators:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nkF8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1864160-ff6e-4bd2-9c59-efe5413074e4_1622x348.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nkF8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1864160-ff6e-4bd2-9c59-efe5413074e4_1622x348.png 424w, https://substackcdn.com/image/fetch/$s_!nkF8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1864160-ff6e-4bd2-9c59-efe5413074e4_1622x348.png 848w, https://substackcdn.com/image/fetch/$s_!nkF8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1864160-ff6e-4bd2-9c59-efe5413074e4_1622x348.png 1272w, https://substackcdn.com/image/fetch/$s_!nkF8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1864160-ff6e-4bd2-9c59-efe5413074e4_1622x348.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nkF8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1864160-ff6e-4bd2-9c59-efe5413074e4_1622x348.png" width="1456" height="312" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a1864160-ff6e-4bd2-9c59-efe5413074e4_1622x348.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:312,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:63441,&quot;alt&quot;:&quot;SELECT id, name FROM products_trgm WHERE name % 'elastc' ORDER BY similarity(name, 'elastc') DESC LIMIT 5;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://alexanderobregon.substack.com/i/186013030?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1864160-ff6e-4bd2-9c59-efe5413074e4_1622x348.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SELECT id, name FROM products_trgm WHERE name % 'elastc' ORDER BY similarity(name, 'elastc') DESC LIMIT 5;" title="SELECT id, name FROM products_trgm WHERE name % 'elastc' ORDER BY similarity(name, 'elastc') DESC LIMIT 5;" srcset="https://substackcdn.com/image/fetch/$s_!nkF8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1864160-ff6e-4bd2-9c59-efe5413074e4_1622x348.png 424w, https://substackcdn.com/image/fetch/$s_!nkF8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1864160-ff6e-4bd2-9c59-efe5413074e4_1622x348.png 848w, https://substackcdn.com/image/fetch/$s_!nkF8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1864160-ff6e-4bd2-9c59-efe5413074e4_1622x348.png 1272w, https://substackcdn.com/image/fetch/$s_!nkF8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1864160-ff6e-4bd2-9c59-efe5413074e4_1622x348.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Typos such as <code>elastc</code> instead of <code>elastic</code> still return reasonable matches, ordered by their trigram similarity scores. A GIN or GiST index on the trigram representation keeps such searches responsive on large tables.</p><p>Other ecosystems adopt related ideas. MySQL offers an ngram full text parser that splits text into fixed length fragments and feeds them into a full text index, which works well for languages without clear word boundaries and also helps with substring matches in some workloads. Third party extensions and libraries for SQLite and other databases implement trigram or ngram search in various ways, always with the same basic goal of storing short character fragments in a way that supports partial and fuzzy matching on large data sets.</p><h3>Conclusion</h3><p>In practice, partial text search performance comes down to how string operators interact with index structures. <code>LIKE</code> with anchored prefixes rides on B tree ranges, full text indexes route word based queries through token lists, and trigram indexes narrow substring and fuzzy matches by comparing short character sequences. Matched mechanics let databases scan far less data, keep latency low, and still support flexible search behavior as tables grow.</p><ol><li><p><em><a href="https://www.postgresql.org/docs/current/textsearch.html">PostgreSQL Text Search Documentation</a></em></p></li><li><p><em><a href="https://www.postgresql.org/docs/current/pgtrgm.html">PostgreSQL pg_trgm Extension Reference</a></em></p></li><li><p><em><a href="https://dev.mysql.com/doc/refman/8.0/en/fulltext-search.html">MySQL InnoDB Full Text Search Guide</a></em></p></li><li><p><em><a href="https://dev.mysql.com/doc/refman/8.0/en/fulltext-search-ngram.html">MySQL Ngram Full Text Parser</a></em></p></li><li><p><em><a href="https://www.sqlite.org/fts5.html">SQLite FTS5 Extension Overview</a></em></p></li><li><p><em><a href="https://learn.microsoft.com/sql/relational-databases/search/full-text-search">SQL Server Full Text Search Overview</a></em></p></li></ol><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share Alexander Obregon's Substack&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://alexanderobregon.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share Alexander Obregon's Substack</span></a></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!MV4U!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3519289-9d65-4c44-85fb-67a6a6b8545b_306x306.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MV4U!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3519289-9d65-4c44-85fb-67a6a6b8545b_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!MV4U!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3519289-9d65-4c44-85fb-67a6a6b8545b_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!MV4U!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3519289-9d65-4c44-85fb-67a6a6b8545b_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!MV4U!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3519289-9d65-4c44-85fb-67a6a6b8545b_306x306.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!MV4U!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3519289-9d65-4c44-85fb-67a6a6b8545b_306x306.png" width="306" height="306" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d3519289-9d65-4c44-85fb-67a6a6b8545b_306x306.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:306,&quot;width&quot;:306,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!MV4U!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3519289-9d65-4c44-85fb-67a6a6b8545b_306x306.png 424w, https://substackcdn.com/image/fetch/$s_!MV4U!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3519289-9d65-4c44-85fb-67a6a6b8545b_306x306.png 848w, https://substackcdn.com/image/fetch/$s_!MV4U!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3519289-9d65-4c44-85fb-67a6a6b8545b_306x306.png 1272w, https://substackcdn.com/image/fetch/$s_!MV4U!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3519289-9d65-4c44-85fb-67a6a6b8545b_306x306.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://freesvg.org/mono-sql">Image Source</a></figcaption></figure></div>]]></content:encoded></item></channel></rss>