<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nishant Gaurav </title>
    <description>The latest articles on DEV Community by Nishant Gaurav  (@im_nishant).</description>
    <link>https://dev.to/im_nishant</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3755207%2Fccfc2018-a401-48be-ac00-4b0aed7027c9.jpeg</url>
      <title>DEV Community: Nishant Gaurav </title>
      <link>https://dev.to/im_nishant</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/im_nishant"/>
    <language>en</language>
    <item>
      <title>Longest Common Subsequence: A New Pattern, the Same Thinking</title>
      <dc:creator>Nishant Gaurav </dc:creator>
      <pubDate>Sun, 01 Nov 2026 15:30:00 +0000</pubDate>
      <link>https://dev.to/im_nishant/longest-common-subsequence-a-new-pattern-the-same-thinking-2g0g</link>
      <guid>https://dev.to/im_nishant/longest-common-subsequence-a-new-pattern-the-same-thinking-2g0g</guid>
      <description>&lt;p&gt;This chapter covers the first two steps: recursion and memoization. Tabulation comes next.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is a Subsequence?
&lt;/h2&gt;

&lt;p&gt;Before anything else, this needs to be clear.&lt;/p&gt;

&lt;p&gt;A subsequence is formed by deleting zero or more characters from a string without changing the order of the remaining characters. The selected characters don't have to be adjacent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;String: A B C D E

Valid subsequences:
  ACE    (positions 1, 3, 5)
  ABD    (positions 1, 2, 4)
  BDE    (positions 2, 4, 5)
  ABCDE  (the whole string)
  A      (just the first character)
  ""     (empty — valid)

Not a valid subsequence:
  CA     (order is reversed)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key point: gaps are allowed, but order is not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm3r3qblz07nkog4g5wl8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm3r3qblz07nkog4g5wl8.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is the Longest Common Subsequence?
&lt;/h2&gt;

&lt;p&gt;Given two strings, find the longest subsequence that appears in both.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X = ABCDGH
Y = ABEDFW
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some common subsequences: &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;AB&lt;/code&gt;, &lt;code&gt;ABD&lt;/code&gt;, &lt;code&gt;ABDH&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The longest one is &lt;code&gt;ABDH&lt;/code&gt; with length 4. That's the LCS.&lt;/p&gt;

&lt;p&gt;The characters must appear in the same relative order in both strings, but they don't need to be adjacent in either.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Is a DP Problem
&lt;/h2&gt;

&lt;p&gt;Look at the last characters of both strings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X = A B C D G H   → last character: H
Y = A B E D F W   → last character: W
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two cases arise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 1: Last characters match.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;X[m-1] == Y[n-1]&lt;/code&gt;, that character is part of the LCS. Include it and reduce both strings by one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LCS(X, Y, m, n) = 1 + LCS(X, Y, m-1, n-1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Case 2: Last characters don't match.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;X[m-1] != Y[n-1]&lt;/code&gt;, you don't know which one to drop. So you try both and take the better result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nc"&gt;LCS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;LCS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;    &lt;span class="c1"&gt;# drop last char of X
&lt;/span&gt;    &lt;span class="nc"&gt;LCS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# drop last char of Y
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fas83ggcxefmx84q1n11a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fas83ggcxefmx84q1n11a.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These two cases completely define the recursive structure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Base Case
&lt;/h2&gt;

&lt;p&gt;The smallest valid input is when either string runs out of characters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X = ""         or        Y = ""
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An empty string has no characters to share. The LCS is zero.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always design the base case first. Ask: what is the smallest input where the answer is obvious?&lt;/p&gt;




&lt;h2&gt;
  
  
  The Recursive Solution
&lt;/h2&gt;

&lt;p&gt;Base case plus choice diagram gives you the full recursion directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Solution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lcs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

        &lt;span class="c1"&gt;# Base case: one string is empty, no common subsequence
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

        &lt;span class="c1"&gt;# Last characters match — include and shrink both
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lcs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Last characters don't match — try dropping from either side
&lt;/span&gt;        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lcs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lcs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;longestCommonSubsequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lcs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code is a direct translation of the choice diagram. Nothing is added beyond base case and two cases.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the Recursive Solution Gets Slow
&lt;/h2&gt;

&lt;p&gt;When characters don't match, two recursive calls are made. Each of those makes two more. The tree grows exponentially.&lt;/p&gt;

&lt;p&gt;Worse, the same state appears multiple times through different paths.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LCS(m, n)
├── LCS(m-1, n)
│   ├── LCS(m-2, n)
│   └── LCS(m-1, n-1)  ← appears here
└── LCS(m, n-1)
    ├── LCS(m-1, n-1)  ← and here again
    └── LCS(m, n-2)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;LCS(m-1, n-1)&lt;/code&gt; is calculated twice from the top level alone. Deeper in the tree, the overlap gets worse. For long strings, this becomes too slow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg5fdlh9eqvspb7c76yjt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg5fdlh9eqvspb7c76yjt.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix is the same as it was in the Knapsack series: store the answer the first time, reuse it every time after.&lt;/p&gt;




&lt;h2&gt;
  
  
  Memoization
&lt;/h2&gt;

&lt;p&gt;The state is defined by two values: &lt;code&gt;m&lt;/code&gt; and &lt;code&gt;n&lt;/code&gt;. So the cache is a 2D table of size &lt;code&gt;(m+1) × (n+1)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;+1&lt;/code&gt; on each dimension is because &lt;code&gt;m&lt;/code&gt; and &lt;code&gt;n&lt;/code&gt; range from &lt;code&gt;0&lt;/code&gt; to their respective lengths. Zero is a valid state (it's the base case), so it needs a slot in the table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why initialise with &lt;code&gt;-1&lt;/code&gt; and not &lt;code&gt;0&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;0&lt;/code&gt; is a valid LCS answer. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X = ABC
Y = XYZ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These strings share no characters. LCS = 0. If the table is initialised to zero, you can't tell whether a cell holds a computed answer of zero or an unvisited state. So we use &lt;code&gt;-1&lt;/code&gt; as the sentinel.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-1 → not computed yet
 0 → computed, answer is 0
&amp;gt;0 → computed, answer is this value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same reasoning as using &lt;code&gt;None&lt;/code&gt; in Count of Subsets (Chapter 4), where &lt;code&gt;False&lt;/code&gt; was a valid answer and couldn't serve as the placeholder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common mistake:&lt;/strong&gt; initialising with 0 and then checking &lt;code&gt;if T[m][n] != 0&lt;/code&gt;. This skips recomputing incorrectly when the real answer is zero.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# WRONG
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# CORRECT
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Memoized Solution
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Solution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lcs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

        &lt;span class="c1"&gt;# Base case
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

        &lt;span class="c1"&gt;# Return stored answer if already computed
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="c1"&gt;# Match: include character, shrink both strings
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lcs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# No match: try both options, take the better one
&lt;/span&gt;        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lcs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lcs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;longestCommonSubsequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# -1 means not yet computed
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lcs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recursive logic is unchanged. Two lines added: the cache check at the top, the store before returning. That's memoization.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmwco4fxpr5eb88nnp0n0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmwco4fxpr5eb88nnp0n0.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The LCS Pattern to Remember
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Base case:
  m == 0 or n == 0 → return 0

Choice:
  X[m-1] == Y[n-1] → 1 + LCS(m-1, n-1)
  X[m-1] != Y[n-1] → max(LCS(m-1, n), LCS(m, n-1))

Memoization:
  Table size: (m+1) × (n+1)
  Initialise with -1
  Check before computing, store after
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What You Now Understand
&lt;/h2&gt;

&lt;p&gt;LCS is not Knapsack. The inputs are two strings, not an array and a capacity. The table is indexed by string lengths, not items and weight. The choice is match-or-no-match, not take-or-skip.&lt;/p&gt;

&lt;p&gt;But the process is the same. Base case first. Choices second. Recursion from those two. Repeated states observed. Memoization added with two lines.&lt;/p&gt;

&lt;p&gt;The next chapter converts this recursion into a bottom-up tabulation. Once you see how &lt;code&gt;m-1&lt;/code&gt; and &lt;code&gt;n-1&lt;/code&gt; in the recursion map to &lt;code&gt;i-1&lt;/code&gt; and &lt;code&gt;j-1&lt;/code&gt; in the table, the entire LCS family of problems becomes much easier to build.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>datastructures</category>
      <category>algorithms</category>
      <category>dynamicprogramming</category>
    </item>
    <item>
      <title>GROUP BY In PostgreSQL</title>
      <dc:creator>Nishant Gaurav </dc:creator>
      <pubDate>Thu, 22 Oct 2026 17:30:00 +0000</pubDate>
      <link>https://dev.to/im_nishant/group-by-in-postgresql-n61</link>
      <guid>https://dev.to/im_nishant/group-by-in-postgresql-n61</guid>
      <description>&lt;p&gt;For a while, &lt;code&gt;GROUP BY&lt;/code&gt; was basically the ceiling of what I knew how to do with aggregation. Total per city, count per venue, that kind of thing. Then I ran into a task — rank venues within their own city by latitude, but keep every single venue row in the result, not collapse them into one row per city — and &lt;code&gt;GROUP BY&lt;/code&gt; just couldn't do it. That's the gap window functions fill, and once it clicked, a bunch of queries that used to feel awkward got a lot simpler.&lt;/p&gt;

&lt;h2&gt;
  
  
  What GROUP BY can't do
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;GROUP BY&lt;/code&gt; collapses rows. If you group venues by city and count them, you get one row per city — the individual venues are gone from the result, folded into that count. That's exactly what you want sometimes. It's exactly the problem other times, like when you want to know each venue's rank within its city, but still want every venue listed individually, not just a per-city summary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Window functions keep every row
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RANK&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;latitude&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rank_in_city&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns one row per venue — nothing gets collapsed. Each row also gets a computed value attached to it, &lt;code&gt;rank_in_city&lt;/code&gt;, based on how that venue compares to others in the same city. &lt;code&gt;PARTITION BY city&lt;/code&gt; is doing something similar to what &lt;code&gt;GROUP BY&lt;/code&gt; does conceptually, splitting the data into per-city buckets, but instead of collapsing each bucket into a single summary row, the window function computes something within each bucket and attaches it back onto every original row.&lt;/p&gt;

&lt;p&gt;That's really the core difference worth holding onto: &lt;code&gt;GROUP BY&lt;/code&gt; reduces row count, window functions don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ones that come up most
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ROW_NUMBER()&lt;/code&gt;, &lt;code&gt;RANK()&lt;/code&gt;, and &lt;code&gt;DENSE_RANK()&lt;/code&gt; all number rows within a partition, but they handle ties differently. &lt;code&gt;ROW_NUMBER()&lt;/code&gt; gives every row a distinct number regardless of ties, arbitrarily breaking them. &lt;code&gt;RANK()&lt;/code&gt; gives tied rows the same number, then skips ahead — two venues tied for rank 1 means the next one is rank 3, not 2. &lt;code&gt;DENSE_RANK()&lt;/code&gt; also gives tied rows the same number, but doesn't skip — the next distinct value after a tie at 1 is 2, not 3.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LAG()&lt;/code&gt; and &lt;code&gt;LEAD()&lt;/code&gt; compare a row to the row before or after it, within the same ordering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;venue_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;closed_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;closed_on&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;venue_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;closed_on&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;previous_closure&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;closures&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pulls, for each closure record, the date of that same venue's previous closure, sitting right there in the same row. Useful for things like "days since last closure" style calculations, where you need to compare a row to the one before it without writing a self join to do it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SUM() OVER (...)&lt;/code&gt; gives you running totals — the same idea as a regular &lt;code&gt;SUM&lt;/code&gt;, but computed cumulatively per row instead of collapsed into a single number.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ynmmpa2fkc4b6cs137g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ynmmpa2fkc4b6cs137g.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this actually gets used
&lt;/h2&gt;

&lt;p&gt;A common real request is "top N per category" — for this project, maybe "the two highest-latitude venues in each city." &lt;code&gt;GROUP BY&lt;/code&gt; genuinely can't answer this on its own, because it would need to keep multiple rows per city, which defeats the whole point of grouping. The window function version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latitude&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;latitude&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The inner query attaches a row number to every venue, restarting at 1 for each new city, ordered by latitude descending. The outer query then just filters down to &lt;code&gt;rn &amp;lt;= 2&lt;/code&gt;. This pattern — window function inside, filter outside — is worth recognizing on sight, because "top N per group" comes up often enough that it's basically a template once you've seen it once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;Window functions solve a specific gap that &lt;code&gt;GROUP BY&lt;/code&gt; leaves open: computing something per group while still keeping every individual row in the result, rather than collapsing down to one row per group. If a query needs a ranking, a comparison to a neighboring row, or a running total, and you catch yourself reaching for &lt;code&gt;GROUP BY&lt;/code&gt; and getting frustrated that it's throwing away rows you actually needed, that's usually the signal a window function is what you actually want.&lt;/p&gt;

&lt;p&gt;Next up: JSONB in practice — querying and indexing semi-structured data, which comes up constantly once you're syncing data from external sources.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>postgressql</category>
      <category>sql</category>
      <category>database</category>
    </item>
    <item>
      <title>Views vs Materialized Views In PostgreSQL</title>
      <dc:creator>Nishant Gaurav </dc:creator>
      <pubDate>Wed, 21 Oct 2026 17:30:00 +0000</pubDate>
      <link>https://dev.to/im_nishant/views-vs-materialized-views-in-postgresql-589p</link>
      <guid>https://dev.to/im_nishant/views-vs-materialized-views-in-postgresql-589p</guid>
      <description>&lt;p&gt;This one's short compared to some of the earlier topics in this series, but it's also one of those things that's easy to get backwards if you only skim the definitions once. A view and a materialized view sound like variations on the same idea, and syntactically they're not far apart, but they behave in genuinely different ways once you actually use them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the confusion comes from
&lt;/h2&gt;

&lt;p&gt;Say you've got a dashboard summary showing total venues per city, and it's built on a &lt;code&gt;GROUP BY&lt;/code&gt; query that's a bit expensive — maybe it's joining against &lt;code&gt;closures&lt;/code&gt; too, computing a few aggregates at once. Someone suggests "just make it a view," and that's reasonable advice, except it doesn't actually solve the performance problem people usually mean when they say that. A regular view doesn't cache anything. It just saves you from retyping the query.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two, side by side
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;active_venues&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_closed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;venue_stats&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;REFRESH&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;venue_stats&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A regular &lt;code&gt;VIEW&lt;/code&gt; is really just a stored query, nothing more. Every time you &lt;code&gt;SELECT * FROM active_venues&lt;/code&gt;, Postgres runs the underlying query against &lt;code&gt;venues&lt;/code&gt; again, live, right then. It's always current, because it's not storing anything — it's just a name attached to a query definition that gets substituted in and executed fresh each time.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;MATERIALIZED VIEW&lt;/code&gt; actually stores the result physically, like a real table. Reading from &lt;code&gt;venue_stats&lt;/code&gt; is fast, because it's just reading stored rows, not recomputing anything. The trade-off is that it goes stale the moment the underlying data changes, and it stays stale until you explicitly run &lt;code&gt;REFRESH MATERIALIZED VIEW&lt;/code&gt; to recompute and re-store the result.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1evc3hpbvo77fe5j5o81.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1evc3hpbvo77fe5j5o81.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking one
&lt;/h2&gt;

&lt;p&gt;Use a regular &lt;code&gt;VIEW&lt;/code&gt; when the underlying query is cheap enough that running it live isn't a real cost, and when the data behind it absolutely needs to be current every time someone reads it. &lt;code&gt;active_venues&lt;/code&gt; above is a good fit — filtering on a boolean is trivial for Postgres, there's no real performance reason to cache it, and you'd generally want to know immediately if a venue's status changed.&lt;/p&gt;

&lt;p&gt;Use a &lt;code&gt;MATERIALIZED VIEW&lt;/code&gt; when the query is genuinely expensive — heavy aggregation, several joins, something that takes real time to compute — and when the consumers of that data can tolerate it being slightly stale in exchange for speed. A dashboard's summary stats are the classic case: nobody's checking &lt;code&gt;venue_stats&lt;/code&gt; expecting it to reflect a venue that closed thirty seconds ago, and recomputing that aggregation on every single page load for every user would be wasteful when refreshing it once every few minutes, or on some schedule, is perfectly acceptable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;The names sound similar, but the behavior isn't — a &lt;code&gt;VIEW&lt;/code&gt; is a saved query that always runs live, a &lt;code&gt;MATERIALIZED VIEW&lt;/code&gt; is a saved result that needs an explicit &lt;code&gt;REFRESH&lt;/code&gt; to stay current. Picking between them comes down to one question: does this need to be live every time, or is a periodically refreshed snapshot good enough for what it's being used for.&lt;/p&gt;

&lt;p&gt;Next up: window functions — a step past &lt;code&gt;GROUP BY&lt;/code&gt; that lets you compute things like running totals and per-group rankings without collapsing your rows down to one per group.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>postgressql</category>
      <category>database</category>
      <category>sql</category>
    </item>
    <item>
      <title>Transactions, ACID, and MVCC In PostgreSQL</title>
      <dc:creator>Nishant Gaurav </dc:creator>
      <pubDate>Tue, 20 Oct 2026 17:30:00 +0000</pubDate>
      <link>https://dev.to/im_nishant/transactions-acid-and-mvcc-in-postgresql-56mi</link>
      <guid>https://dev.to/im_nishant/transactions-acid-and-mvcc-in-postgresql-56mi</guid>
      <description>&lt;p&gt;This is the chapter that finally connected a bunch of things I'd been treating as separate facts — why &lt;code&gt;UPDATE&lt;/code&gt; doesn't edit rows in place, why table files keep growing even after you delete data, why &lt;code&gt;VACUUM&lt;/code&gt; exists at all. It turns out all of it comes back to one mechanism, MVCC, and once that clicked, a handful of previously mysterious Postgres behaviors stopped being mysterious.&lt;/p&gt;

&lt;h2&gt;
  
  
  What transactions actually give you
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;is_closed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;closures&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;closed_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;venue_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- or ROLLBACK;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything between &lt;code&gt;BEGIN&lt;/code&gt; and &lt;code&gt;COMMIT&lt;/code&gt; either all happens, or none of it does. If something goes wrong partway through — an error, a constraint violation, or you just decide to &lt;code&gt;ROLLBACK&lt;/code&gt; yourself — neither update takes effect. This matters here specifically because marking a venue closed and logging when it closed are really two halves of one action. Without a transaction wrapping both, you could end up with a venue marked closed but no closure record, or a closure record for a venue that's still marked open, if something failed between the two statements.&lt;/p&gt;

&lt;h2&gt;
  
  
  ACID, without the textbook phrasing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Atomicity&lt;/strong&gt; — the whole transaction is one unit. No partial, half-applied state where some of it went through and some didn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt; — the database moves from one valid state to another. Constraints never get violated in the committed result, even if intermediate steps inside the transaction technically would have violated them on their own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolation&lt;/strong&gt; — concurrent transactions don't see each other's uncommitted changes. What "see" actually means here depends on the isolation level, which is worth its own section.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durability&lt;/strong&gt; — once committed, it survives a crash. It's written to disk, specifically to something called the write-ahead log (WAL), before Postgres considers the commit successful.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These four letters get recited a lot without much explanation of what they're actually protecting against. Atomicity is what stops the venue/closure scenario above from going half-done. Durability is what means a &lt;code&gt;COMMIT&lt;/code&gt; isn't a lie — if the server crashes one second after it returns success, that data is still there when it comes back up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isolation levels
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Prevents&lt;/th&gt;
&lt;th&gt;Postgres default?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read Uncommitted&lt;/td&gt;
&lt;td&gt;(Postgres treats this the same as Read Committed — it doesn't actually implement dirty reads)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read Committed&lt;/td&gt;
&lt;td&gt;dirty reads&lt;/td&gt;
&lt;td&gt;yes, this is the default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeatable Read&lt;/td&gt;
&lt;td&gt;dirty + non-repeatable reads&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serializable&lt;/td&gt;
&lt;td&gt;all anomalies, including phantom reads&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read Committed being the default means each individual statement inside a transaction sees whatever was committed at the moment that specific statement started — not the moment the whole transaction began. That distinction matters more than it sounds like it should: two statements in the same transaction, run a few seconds apart, could see different data if something else committed in between, because each statement takes its own fresh look at what's currently committed.&lt;/p&gt;

&lt;p&gt;Serializable is the strongest guarantee — it behaves as though transactions ran one at a time, in some order, with no interleaving at all. The trade-off is that Postgres has to detect conflicts between concurrent Serializable transactions and abort one of them if it can't guarantee that "as if run one at a time" property, which means application code using Serializable transactions generally needs retry logic built in for when that happens.&lt;/p&gt;

&lt;p&gt;For anything genuinely financial-grade, Serializable is the safer default despite that extra complexity. For most everyday application logic, Read Committed is fine, and it's what you get unless you explicitly ask for something stronger.&lt;/p&gt;

&lt;h2&gt;
  
  
  MVCC — the actual mechanism underneath all of this
&lt;/h2&gt;

&lt;p&gt;Here's the part that reframed a bunch of things for me. Postgres doesn't lock rows for reads the way you might picture from a simpler mental model of "one person edits, everyone else waits." Instead, it keeps multiple &lt;strong&gt;versions&lt;/strong&gt; of a row.&lt;/p&gt;

&lt;p&gt;When you run an &lt;code&gt;UPDATE&lt;/code&gt;, Postgres doesn't modify the existing row in place. It writes an entirely new row version with the updated values, and marks the old version as expired — not deleted yet, just no longer the current version. Readers who started their query before the update still see the old version, consistently, without being blocked by the write happening concurrently. Writers, likewise, aren't blocked by ongoing reads. This is why Postgres handles a mix of concurrent reads and writes well without needing to lock everything constantly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkn22t7xfe7odswelid6f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkn22t7xfe7odswelid6f.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This also connects directly back to something from the CRUD article earlier in this series — every &lt;code&gt;INSERT&lt;/code&gt; creates a new row version, and now the reason that phrasing matters is clearer: Postgres genuinely never edits a row's storage in place. It's always writing new versions and marking old ones as no longer current.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why VACUUM exists
&lt;/h2&gt;

&lt;p&gt;Those old, expired row versions don't just disappear the moment they stop being current — they sit there, taking up space, until something cleans them up. That's what &lt;code&gt;VACUUM&lt;/code&gt; does.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;VACUUM&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've ever deleted a bunch of rows from a table and noticed the file on disk didn't shrink the way you'd expect, this is the explanation — the dead row versions are still physically there until &lt;code&gt;VACUUM&lt;/code&gt; reclaims that space. &lt;code&gt;AUTOVACUUM&lt;/code&gt; runs by default in the background specifically so you don't normally have to think about this, but it's worth knowing it exists and understanding why, rather than just trusting it silently.&lt;/p&gt;

&lt;p&gt;This is a genuinely good question to know the answer to, mostly because a lot of people who've used Postgres casually have never actually encountered it — they've never had a table grow large enough, or gone deep enough into how updates work, to run into it. Knowing why &lt;code&gt;VACUUM&lt;/code&gt; exists is less about the command itself and more a decent signal that you've thought about what's actually happening underneath an &lt;code&gt;UPDATE&lt;/code&gt;, not just what syntax makes one run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together with the venue/closure example
&lt;/h2&gt;

&lt;p&gt;Back to the transaction from the start of this article:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;is_closed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;closures&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;closed_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;venue_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, both &lt;code&gt;UPDATE&lt;/code&gt;s create new row versions rather than editing in place. Until &lt;code&gt;COMMIT&lt;/code&gt; runs, another transaction reading &lt;code&gt;venues&lt;/code&gt; at Read Committed isolation still sees the old, pre-update version of that row — the change genuinely isn't visible yet. Once &lt;code&gt;COMMIT&lt;/code&gt; happens, the new versions become the current ones, and the old versions become eligible for cleanup, eventually, by &lt;code&gt;VACUUM&lt;/code&gt;. If something failed between the two &lt;code&gt;UPDATE&lt;/code&gt; statements and the transaction rolled back instead, both new row versions would just be discarded, and the original rows would remain the current version, as if none of this had happened at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;MVCC is the reason Postgres handles concurrent reads and writes gracefully without heavy locking, and it's also the reason a handful of things that might otherwise seem unrelated — why updates don't happen in place, why deleted data doesn't immediately shrink a table, why &lt;code&gt;VACUUM&lt;/code&gt; exists — actually connect to one underlying idea. Transactions and isolation levels sit on top of this, controlling exactly what a given piece of code is allowed to see while other things are happening concurrently.&lt;/p&gt;

&lt;p&gt;Next up: views and materialized views — the difference between a query that re-runs live every time versus one that's actually stored and needs to be refreshed.&lt;/p&gt;

</description>
      <category>postgressql</category>
      <category>sql</category>
      <category>postgres</category>
      <category>database</category>
    </item>
    <item>
      <title>Indexes in PostgreSQL</title>
      <dc:creator>Nishant Gaurav </dc:creator>
      <pubDate>Mon, 19 Oct 2026 17:30:00 +0000</pubDate>
      <link>https://dev.to/im_nishant/indexes-in-postgresql-ea5</link>
      <guid>https://dev.to/im_nishant/indexes-in-postgresql-ea5</guid>
      <description>&lt;p&gt;I added my first index thinking it was basically a free speedup — like flipping a switch. Nobody had told me indexes cost anything, so it didn't occur to me they might not always be worth it. It took looking at a genuinely slow write path, and eventually tracing it back to a table with five indexes on columns nobody actually queried by, to understand that an index is a trade-off, not a strict upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem indexes actually solve
&lt;/h2&gt;

&lt;p&gt;Without an index, finding rows that match a &lt;code&gt;WHERE&lt;/code&gt; condition means Postgres reads every single row in the table and checks each one — a sequential scan. On a table with a few hundred rows, that's basically instant, not worth worrying about. On a table with millions of rows, it's the difference between a query that returns in milliseconds and one that takes seconds, every single time it runs.&lt;/p&gt;

&lt;p&gt;That's the entire motivation. An index gives Postgres a shortcut — a separate structure it can search instead of scanning the whole table — for columns you filter, join, or sort by often enough that the sequential scan actually becomes a problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating one
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_venues_city&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_venues_name&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing exotic in the syntax itself. The &lt;code&gt;UNIQUE&lt;/code&gt; version does double duty — it speeds up lookups on &lt;code&gt;name&lt;/code&gt; and enforces that no two rows can share the same value, functioning as a constraint as well as a performance tool.&lt;/p&gt;

&lt;p&gt;The columns worth indexing are the ones that actually show up in your &lt;code&gt;WHERE&lt;/code&gt; clauses, your join conditions, or your &lt;code&gt;ORDER BY&lt;/code&gt; — not every column "just in case it's useful someday." That instinct, indexing defensively, is exactly what leads to the situation I mentioned above.&lt;/p&gt;

&lt;h2&gt;
  
  
  When an index doesn't actually help
&lt;/h2&gt;

&lt;p&gt;This is the part that gets skipped in a lot of explanations, and it's arguably more useful to know than when &lt;em&gt;to&lt;/em&gt; index, since the "when to" case is fairly intuitive once you understand sequential scans.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small tables.&lt;/strong&gt; A sequential scan on a few hundred rows is genuinely faster than using an index, once you account for the overhead of looking the index up and then jumping back to the table to fetch the actual row. The index doesn't help; it's just extra bookkeeping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Columns that are rarely queried.&lt;/strong&gt; An index that never gets used in a &lt;code&gt;WHERE&lt;/code&gt; or join condition is pure cost with no corresponding benefit — it still has to be maintained on every write, for nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Columns updated extremely often.&lt;/strong&gt; Every &lt;code&gt;INSERT&lt;/code&gt; or &lt;code&gt;UPDATE&lt;/code&gt; that touches an indexed column also has to update the index itself, not just the table row. On a write-heavy table, having several indexes on frequently updated columns adds up to real overhead, and it's the kind of thing that's easy to not notice until write latency creeps up over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low-cardinality columns.&lt;/strong&gt; Something like a boolean &lt;code&gt;is_closed&lt;/code&gt; column only has two possible values. A plain B-tree index on it doesn't narrow things down much, since roughly half the table matches either value — the index isn't selective enough to be worth using over a sequential scan in most cases.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvp2vbd18vuzjmjob3y8p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvp2vbd18vuzjmjob3y8p.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The index types worth being able to name
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;B-tree (default)&lt;/td&gt;
&lt;td&gt;equality and range queries — the default 90% of the time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GIN&lt;/td&gt;
&lt;td&gt;JSONB, arrays, full-text search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GiST&lt;/td&gt;
&lt;td&gt;geospatial data (what PostGIS relies on), also full-text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BRIN&lt;/td&gt;
&lt;td&gt;huge tables with naturally sequential data, like timestamps in an append-only log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hash&lt;/td&gt;
&lt;td&gt;pure equality lookups only, rarely used over B-tree in practice&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You'll mostly use B-tree without ever thinking about it, since it's the default &lt;code&gt;CREATE INDEX&lt;/code&gt; gives you. The others matter once you're dealing with specific data shapes. For a geospatial project, this is worth knowing specifically: PostGIS relies on GiST indexes, which support things like "nearest neighbor" and bounding-box style queries efficiently. A plain B-tree fundamentally can't do that kind of spatial lookup — it's built for ordering along a single dimension, not for two-dimensional proximity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Actually checking if your query is using an index
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Bhopal'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;EXPLAIN&lt;/code&gt; on its own shows you the &lt;em&gt;planned&lt;/em&gt; execution — what Postgres intends to do, without actually running the query. &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; actually executes it and shows real timing alongside the plan. The thing to look for in the output is whether it says &lt;code&gt;Seq Scan&lt;/code&gt; or &lt;code&gt;Index Scan&lt;/code&gt;. A sequential scan on a large, filtered table is usually a sign something's missing — either the index doesn't exist yet, or it exists but isn't being used for some reason, which is its own separate rabbit hole worth investigating if it happens.&lt;/p&gt;

&lt;p&gt;This is genuinely one of the highest-value habits to build early. Before assuming a slow query is "just how Postgres is" or reaching for some other fix, checking &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; first tells you whether the problem is actually about indexing or something else entirely — a bad join order, an unnecessary sort, or something else the plan reveals if you know what to look for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OFFSET gets slow, and what to use instead
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks harmless — you're only asking for 20 rows. But Postgres still has to scan through and discard the first 10,000 rows before it can return the 20 you actually wanted. As the offset grows, the query gets slower, even though the result set size never changes. On a table that only ever has a few pages, nobody notices. On a table with deep pagination — page 500 of search results, say — it becomes a real problem.&lt;/p&gt;

&lt;p&gt;The alternative is keyset pagination:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of counting through rows to skip, this jumps directly to where the last page left off, using the last seen &lt;code&gt;id&lt;/code&gt; as the starting point. With an index on &lt;code&gt;id&lt;/code&gt; — which a primary key already has by default — this is fast regardless of how deep into the results you are, because it's not scanning and discarding anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deciding what to index on a real table
&lt;/h2&gt;

&lt;p&gt;Say the &lt;code&gt;venues&lt;/code&gt; table from earlier in this series is now getting queried in a few different ways: filtering by &lt;code&gt;city&lt;/code&gt;, checking &lt;code&gt;is_closed&lt;/code&gt;, and occasionally sorting by &lt;code&gt;created_at&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;city&lt;/code&gt; is a solid candidate — it's a genuine filter condition, likely used often, and has enough distinct values to actually be selective. &lt;code&gt;is_closed&lt;/code&gt; is the low-cardinality case from above — a boolean with two values, and indexing it alone probably doesn't help much on its own. &lt;code&gt;created_at&lt;/code&gt;, if the table grows into millions of rows and queries often sort by recency, is a reasonable candidate too, potentially even a BRIN index specifically, since timestamps in a table like this tend to be naturally close to sequential as new rows get inserted.&lt;/p&gt;

&lt;p&gt;The way I'd actually verify any of this, rather than guessing, is running the real queries through &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; before and after adding a candidate index, and comparing the plan and the timing directly instead of assuming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;An index isn't a free performance upgrade — it's a genuine trade: faster reads for slower writes and more storage, and it's only worth making on columns that are actually queried often enough for the sequential scan to be a real cost. &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; is the tool that turns this from guesswork into something you can actually verify, and it's worth reaching for before adding or removing any index, not just when something's already obviously slow.&lt;/p&gt;

&lt;p&gt;Next up: transactions, ACID, and MVCC — the part of Postgres that explains why concurrent reads and writes mostly don't get in each other's way, and why &lt;code&gt;VACUUM&lt;/code&gt; exists at all.&lt;/p&gt;

</description>
      <category>postgressql</category>
      <category>postgres</category>
      <category>sql</category>
      <category>database</category>
    </item>
    <item>
      <title>GROUP BY, HAVING, and Subqueries in PostgreSQL</title>
      <dc:creator>Nishant Gaurav </dc:creator>
      <pubDate>Sun, 18 Oct 2026 17:30:00 +0000</pubDate>
      <link>https://dev.to/im_nishant/group-by-having-and-subqueries-in-postgresql-4mp2</link>
      <guid>https://dev.to/im_nishant/group-by-having-and-subqueries-in-postgresql-4mp2</guid>
      <description>&lt;p&gt;There's a point in almost every project where you stop just retrieving rows and start actually asking questions — how many venues per city, which cities have more than a handful, which venues are above average latitude. That's the shift this article is about. The syntax for aggregation isn't complicated on its own. What takes a bit longer to get comfortable with is the order things actually happen in, especially once &lt;code&gt;WHERE&lt;/code&gt; and &lt;code&gt;HAVING&lt;/code&gt; are both in the same query and doing different jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it gets confusing
&lt;/h2&gt;

&lt;p&gt;Say you want cities with more than three venues. The instinct, especially early on, is to write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;venue_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fails. Not silently, at least — Postgres will give you an error about aggregate functions not being allowed in &lt;code&gt;WHERE&lt;/code&gt;. But the reason it fails is worth actually understanding rather than just memorizing "use HAVING instead." &lt;code&gt;WHERE&lt;/code&gt; filters individual rows, and it runs before any grouping happens. At the point &lt;code&gt;WHERE&lt;/code&gt; is being evaluated, there's no such thing as "the count for this city" yet, because the rows haven't been grouped into cities at all — they're still just individual venue rows. You can't filter on something that doesn't exist yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aggregate functions and GROUP BY
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;venue_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GROUP BY city&lt;/code&gt; collapses all the rows sharing the same city value into a single row per city, and the aggregate functions — &lt;code&gt;COUNT&lt;/code&gt;, &lt;code&gt;AVG&lt;/code&gt;, and similar — operate on each group rather than the whole table. Fairly intuitive once you've written a few of these.&lt;/p&gt;

&lt;h2&gt;
  
  
  HAVING, and why it exists at all
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;venue_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;HAVING&lt;/code&gt; is what &lt;code&gt;WHERE&lt;/code&gt; would be if it ran after grouping instead of before. It filters groups, not individual rows, and by the time it runs, the counts and averages actually exist as real values you can compare against. That's the whole reason it's a separate keyword rather than Postgres just letting you use aggregates inside &lt;code&gt;WHERE&lt;/code&gt; — the two clauses run at genuinely different stages of query execution, filtering different things.&lt;/p&gt;

&lt;p&gt;A decent way to remember which is which: &lt;code&gt;WHERE&lt;/code&gt; happens before grouping, so it can't see aggregate results yet. &lt;code&gt;HAVING&lt;/code&gt; happens after, so it can.&lt;/p&gt;

&lt;h2&gt;
  
  
  Subqueries — a query inside a query
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- scalar subquery&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;latitude&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here the inner query runs first, computes a single number — the average latitude across all venues — and the outer query uses that number as a comparison value. This is called a scalar subquery because it returns exactly one value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- subquery in FROM (a derived table)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;avg_lat&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_lat&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;city_stats&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;avg_lat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one's a bit different — the subquery in the &lt;code&gt;FROM&lt;/code&gt; clause produces an entire table-like result (city-level averages), which the outer query then filters and selects from, as though it were a real table. It needs an alias (&lt;code&gt;city_stats&lt;/code&gt; here) because Postgres requires every table-like thing in a &lt;code&gt;FROM&lt;/code&gt; clause to have a name, even a temporary computed one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- correlated subquery — references the outer query, runs per row&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;closures&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;venue_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This last one is genuinely different from the first two. It's called "correlated" because the inner query references &lt;code&gt;v.id&lt;/code&gt; from the outer query — it's not a standalone query you could run on its own, it depends on which outer row is currently being evaluated. Conceptually, Postgres runs this subquery once per row of the outer query, checking "does at least one closure exist for this specific venue." Worth being careful with these on large tables, since running a subquery per row can get expensive if it's not written efficiently.&lt;/p&gt;

&lt;h3&gt;
  
  
  EXISTS vs IN
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;EXISTS&lt;/code&gt; stops as soon as it finds a single matching row — it doesn't need to know how many matches there are, just whether at least one exists, which also means it handles nulls in the subquery result more predictably. &lt;code&gt;IN&lt;/code&gt; builds out the entire subquery result first and then checks membership against it. For "does at least one related row exist" type questions, &lt;code&gt;EXISTS&lt;/code&gt; is usually the better fit, both for performance and for correctness around nulls.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhmfjuft29adpgztbo7bp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhmfjuft29adpgztbo7bp.png" alt=" " width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  CTEs: naming your intermediate steps
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;closed_venues&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;venue_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;closures&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;venue_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;closed_venues&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A CTE — the &lt;code&gt;WITH&lt;/code&gt; clause — lets you name a piece of a query and reference it later in the same statement, instead of nesting subqueries inside each other until the query becomes hard to read top to bottom. This particular example is simple enough that it wouldn't really need a CTE, but the value becomes obvious once a query has three or four logical steps chained together — each one gets a name, and the final query reads almost like a short paragraph instead of a wall of nested parentheses.&lt;/p&gt;

&lt;p&gt;CTEs are also how you handle recursive queries, using &lt;code&gt;WITH RECURSIVE&lt;/code&gt;, which is the standard way to deal with hierarchical data — something like a category tree, where categories can have subcategories that have their own subcategories. That's a bigger topic on its own and probably deserves its own explanation somewhere down the line rather than a paragraph here.&lt;/p&gt;

&lt;p&gt;One historical detail worth knowing if it comes up: older versions of Postgres treated CTEs as what's called an "optimization fence" — meaning the planner would essentially compute the whole CTE first, in isolation, before using it in the rest of the query, even if that wasn't the most efficient approach. Since Postgres 12, CTEs are generally optimized more like subqueries, folded into the overall query plan rather than treated as a hard boundary. If someone asks why you'd use a CTE instead of a subquery, readability and reusability within the same query are the main reasons now — the performance difference that used to matter mostly doesn't anymore, on current versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting a few of these together
&lt;/h2&gt;

&lt;p&gt;Say you want cities where the average venue latitude is unusually high, but only among cities that actually have a meaningful number of venues — filtering out cities with just one or two entries that could skew the average:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;city_stats&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;venue_count&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;avg_lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;venue_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;city_stats&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;venue_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;avg_lat&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CTE computes both the average and the count per city in one pass, and the outer query then filters on &lt;code&gt;venue_count&lt;/code&gt;, which at this point is just a regular column value, not an aggregate anymore — so it goes in &lt;code&gt;WHERE&lt;/code&gt;, not &lt;code&gt;HAVING&lt;/code&gt;, since the grouping already happened inside the CTE. This is a good example of how naming an intermediate result makes the logic of a query clearer than trying to cram the same thing into one query with &lt;code&gt;HAVING&lt;/code&gt; doing double duty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;WHERE&lt;/code&gt; filters rows, &lt;code&gt;HAVING&lt;/code&gt; filters groups, and the reason for the split comes down to when each one actually runs relative to &lt;code&gt;GROUP BY&lt;/code&gt;. Subqueries let you use the result of one query inside another, and CTEs let you name those intermediate steps instead of nesting them — mostly for readability at this point, less so for performance on modern Postgres versions.&lt;/p&gt;

&lt;p&gt;Next up: indexes — how to actually make these queries fast, and just as importantly, when adding an index doesn't help at all.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>postgressql</category>
      <category>sql</category>
      <category>database</category>
    </item>
    <item>
      <title>Joins In PostgreSQL</title>
      <dc:creator>Nishant Gaurav </dc:creator>
      <pubDate>Sat, 17 Oct 2026 17:30:00 +0000</pubDate>
      <link>https://dev.to/im_nishant/joins-in-postgresql-5g0f</link>
      <guid>https://dev.to/im_nishant/joins-in-postgresql-5g0f</guid>
      <description>&lt;p&gt;Joins are one of those topics where everyone nods along when you explain them, then freezes up the moment they have to write one on the spot, in front of someone, without being able to run it first and check. I think that's mostly because the four main types genuinely blur together if you've only half-memorized their definitions instead of actually understanding what each one is doing to the result set. This article is my attempt at explaining them the way I eventually understood them, not the way they're usually first presented.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is the part people freeze on
&lt;/h2&gt;

&lt;p&gt;The syntax for a join isn't hard. &lt;code&gt;SELECT ... FROM a JOIN b ON a.x = b.y&lt;/code&gt; — that's most of it. What actually trips people up is reasoning about what rows come back, especially when the two tables don't match up perfectly. A venue with no closures. A closure somehow pointing at a venue that doesn't exist. These edge cases are exactly what separates the four join types, and if you've only memorized "LEFT JOIN keeps everything on the left," without really internalizing why, you'll get it backwards the first time you're asked to write one under any pressure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working through all four, using the same two tables
&lt;/h2&gt;

&lt;p&gt;Assume &lt;code&gt;venues&lt;/code&gt; and &lt;code&gt;closures&lt;/code&gt;, where &lt;code&gt;closures.venue_id&lt;/code&gt; references &lt;code&gt;venues.id&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- INNER JOIN: only rows that match in both tables&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;closed_on&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;closures&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;venue_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you venues that actually have at least one closure recorded. A venue with zero closures just doesn't show up at all — not as a row with nulls, it's simply absent from the result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- LEFT JOIN: all venues, even ones with no closures&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;closed_on&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;closures&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;venue_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every venue shows up here, whether or not it has closures. For venues with no matching closure row, &lt;code&gt;c.closed_on&lt;/code&gt; just comes back as null. This is probably the join you'll reach for most often in practice, since "show me all of X, along with whatever related Y exists" is an extremely common shape of question.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- RIGHT JOIN: all closures, even orphaned ones&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;closed_on&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="k"&gt;RIGHT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;closures&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;venue_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mirror image of &lt;code&gt;LEFT JOIN&lt;/code&gt; — every closure shows up, even if somehow its &lt;code&gt;venue_id&lt;/code&gt; doesn't match any real venue. In practice this is rare to actually use, since you can just swap the table order and use &lt;code&gt;LEFT JOIN&lt;/code&gt; instead, which is why most people default to always writing &lt;code&gt;LEFT JOIN&lt;/code&gt; and almost never reach for &lt;code&gt;RIGHT JOIN&lt;/code&gt; even though it exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- FULL OUTER JOIN: everything from both sides&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;closed_on&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="k"&gt;FULL&lt;/span&gt; &lt;span class="k"&gt;OUTER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;closures&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;venue_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything from both tables, matched where possible, with nulls filling in on whichever side doesn't have a match. Less common day to day, but genuinely useful when you need to audit both directions at once — venues with no closures, and closures pointing at venues that no longer exist.&lt;/p&gt;

&lt;p&gt;A simpler way I ended up explaining these to myself: &lt;code&gt;INNER&lt;/code&gt; only wants pairs that actually match. &lt;code&gt;LEFT&lt;/code&gt; wants everything from the left table no matter what, filling in gaps with null. &lt;code&gt;RIGHT&lt;/code&gt; is the same idea flipped. &lt;code&gt;FULL OUTER&lt;/code&gt; wants everything from both, period.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuyoeaiaazt6og5rtrybr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuyoeaiaazt6og5rtrybr.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Self joins, and the thing that goes wrong without an ON clause
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- venues in the same city, paired against each other&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a self join — the same table joined against itself, using two aliases (&lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt;) so Postgres can tell which "copy" of the table a given column reference belongs to. The classic real-world case is an employee-manager table, where each row references another row in the same table. Here it's a more contrived example — pairing up venues that share a city — but the mechanics are identical. The &lt;code&gt;a.id &amp;lt; b.id&lt;/code&gt; part matters too: without it, you'd get every pair twice, once in each order, plus every venue paired with itself.&lt;/p&gt;

&lt;p&gt;The mistake that catches almost everyone at least once: forgetting the &lt;code&gt;ON&lt;/code&gt; condition entirely, or getting it wrong in a way that matches everything. That gives you a CROSS JOIN by accident — every row from one table matched with every row from the other, a full Cartesian product. If a join query comes back with a suspiciously huge row count, way more than you'd expect, this is almost always why. It's usually pretty obvious once you see it, but it's an easy thing to miss for a minute if you're staring at a query that looks syntactically fine.&lt;/p&gt;

&lt;p&gt;A real &lt;code&gt;CROSS JOIN&lt;/code&gt; does have legitimate uses — generating every combination of two sets, like all dates crossed with all venues for a reporting calendar that needs a row for every date-venue pair regardless of whether anything happened. It's rare, but it's worth knowing it's an intentional tool sometimes, not just a mistake that happens to have a name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working through a real query
&lt;/h2&gt;

&lt;p&gt;Say you want a report: every venue, and how many times it's been closed, including venues that have never been closed at all, showing zero rather than being left out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;closure_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;closures&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;venue_id&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;closure_count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;LEFT JOIN&lt;/code&gt; is doing the actual work here — it's what makes sure venues with zero closures still show up, instead of silently vanishing the way they would with an &lt;code&gt;INNER JOIN&lt;/code&gt;. &lt;code&gt;COUNT(c.id)&lt;/code&gt; matters more than it looks like it should: for a venue with no closures, the joined &lt;code&gt;c.id&lt;/code&gt; value is null, and &lt;code&gt;COUNT()&lt;/code&gt; doesn't count nulls, so it correctly comes back as zero. If you'd written &lt;code&gt;COUNT(*)&lt;/code&gt; instead, every row would count as one regardless of whether &lt;code&gt;c.id&lt;/code&gt; was null, and a venue with no closures would incorrectly show a count of one instead of zero, because the &lt;code&gt;LEFT JOIN&lt;/code&gt; still produces exactly one row for it, just with null values in it.&lt;/p&gt;

&lt;p&gt;That's a small detail, but it's exactly the kind of thing that produces a report with numbers that are subtly wrong in a way that's easy to miss during a quick review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;The four join types aren't really four different things to memorize — they're one idea (match rows between two tables based on a condition) with different rules about what happens when a match doesn't exist on one side or the other. If you can explain, out loud, what happens to an unmatched row in each of the four cases, you understand joins. If you're just recalling which keyword goes with which definition, that's worth practicing out loud a few times before it comes up somewhere that actually matters.&lt;/p&gt;

&lt;p&gt;Next up: aggregations — &lt;code&gt;GROUP BY&lt;/code&gt;, &lt;code&gt;HAVING&lt;/code&gt;, and the subquery patterns that come up constantly once you start asking more complex questions of your data.&lt;/p&gt;

</description>
      <category>postgressql</category>
      <category>postgres</category>
      <category>sql</category>
      <category>database</category>
    </item>
    <item>
      <title>Where Clause In PostgreSQL</title>
      <dc:creator>Nishant Gaurav </dc:creator>
      <pubDate>Fri, 16 Oct 2026 17:30:00 +0000</pubDate>
      <link>https://dev.to/im_nishant/where-clause-in-postgresql-33db</link>
      <guid>https://dev.to/im_nishant/where-clause-in-postgresql-33db</guid>
      <description>&lt;p&gt;&lt;code&gt;WHERE&lt;/code&gt; is one of the first things you learn in SQL, and it's also one of the things people stop paying attention to too early. It feels solved — you know the syntax, you've written a hundred of these, why would there be more to say about it. But there are a few Postgres-specific details in here that genuinely surprised me, and a habit around combining conditions that I only picked up after seeing it go wrong once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this quietly causes bugs
&lt;/h2&gt;

&lt;p&gt;Say you've got a query filtering venues that are open, and either in Bhopal or have no city set yet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_closed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Bhopal'&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks reasonable at a glance. It's wrong. Without parentheses, &lt;code&gt;AND&lt;/code&gt; binds tighter than &lt;code&gt;OR&lt;/code&gt;, so this actually reads as "give me rows where (closed is false and city is Bhopal), or (city is null) — regardless of whether they're closed." You'd get back closed venues with no city set, which almost certainly isn't what you meant.&lt;/p&gt;

&lt;p&gt;This is the kind of bug that doesn't announce itself. The query runs, it returns rows, nothing errors — it's just quietly including data it shouldn't. You usually catch it because a number looks off in a report, not because anything crashed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basics, and the Postgres-specific one
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;latitude&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DB Mall'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Upper Lake'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'Van%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;-- case-sensitive pattern match&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;ILIKE&lt;/span&gt; &lt;span class="s1"&gt;'van%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;-- case-insensitive&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="s1"&gt;'2026-01-01'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="s1"&gt;'2026-12-31'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most of this is standard SQL and probably familiar already. &lt;code&gt;ILIKE&lt;/code&gt; is the one worth calling out specifically, because it's Postgres-specific — MySQL's &lt;code&gt;LIKE&lt;/code&gt; happens to behave case-insensitively by default depending on the collation setting, which is its own small gotcha if you're used to that behavior and then move to Postgres and suddenly your pattern matches stop working the way you expect. In Postgres, &lt;code&gt;LIKE&lt;/code&gt; is case-sensitive, full stop, and &lt;code&gt;ILIKE&lt;/code&gt; is the explicit case-insensitive version. If someone asks about this in an interview, it's usually less about the syntax and more about whether you've actually worked with Postgres specifically rather than SQL in the abstract.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;IN&lt;/code&gt; versus a chain of &lt;code&gt;OR&lt;/code&gt; conditions is mostly a readability choice at this scale — &lt;code&gt;WHERE name IN ('DB Mall', 'Upper Lake')&lt;/code&gt; says the same thing as &lt;code&gt;WHERE name = 'DB Mall' OR name = 'Upper Lake'&lt;/code&gt;, just more compactly once you're checking against more than two or three values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combining conditions without shooting yourself
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_closed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Bhopal'&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix for the earlier bug is just parentheses, grouping the &lt;code&gt;OR&lt;/code&gt; explicitly. The bigger point isn't the specific bug — it's that relying on memorized operator precedence rules, and getting them right every time under pressure, isn't a great strategy. Parenthesizing mixed &lt;code&gt;AND&lt;/code&gt;/&lt;code&gt;OR&lt;/code&gt; conditions costs nothing and means the query reads correctly on its own, without anyone needing to recall precedence rules to verify it during a code review.&lt;/p&gt;

&lt;p&gt;I started doing this by default after the bug above, not because someone told me to as a rule. It's one of those habits that feels unnecessary right up until it isn't.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8012io632b6m3pmctxxg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8012io632b6m3pmctxxg.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Sorting and limiting
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not much complexity here on its own — &lt;code&gt;ORDER BY&lt;/code&gt; sorts, &lt;code&gt;LIMIT&lt;/code&gt; caps how many rows come back. The thing worth knowing now, even though it's really a topic for the indexing article later in this series, is that &lt;code&gt;ORDER BY&lt;/code&gt; on a large table without a supporting index means Postgres has to sort the entire result set before it can hand back even a &lt;code&gt;LIMIT 5&lt;/code&gt;. It works fine on a few thousand rows. It gets noticeably slower as a table grows, which is part of why indexing decisions and query patterns end up tied together more than they look at first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting a few of these together
&lt;/h2&gt;

&lt;p&gt;Say you're building the "nearby open venues in my city, or venues where we don't know the city yet" view for the dashboard — a genuinely realistic filter, since city data synced from an external API is exactly the kind of field that sometimes comes back empty:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_closed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="k"&gt;ILIKE&lt;/span&gt; &lt;span class="s1"&gt;'bhopal'&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This uses &lt;code&gt;ILIKE&lt;/code&gt; instead of &lt;code&gt;=&lt;/code&gt; for the city comparison, since city names synced from an external source aren't always consistently capitalized — &lt;code&gt;Bhopal&lt;/code&gt; versus &lt;code&gt;bhopal&lt;/code&gt; versus &lt;code&gt;BHOPAL&lt;/code&gt; depending on which upstream feed the data came from. The parentheses around the &lt;code&gt;OR&lt;/code&gt; keep the null-handling correct. And &lt;code&gt;LIMIT&lt;/code&gt; caps it to a reasonable page size instead of pulling back everything at once. None of these individually is complicated, but stacking them correctly, in the right order and with the right grouping, is really what filtering well in practice comes down to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;WHERE&lt;/code&gt; clauses feel basic enough that it's easy to stop thinking carefully about them once you know the syntax. The two things actually worth carrying forward: parenthesize mixed &lt;code&gt;AND&lt;/code&gt;/&lt;code&gt;OR&lt;/code&gt; conditions every time, even when you're fairly sure you have precedence right, and remember that &lt;code&gt;ILIKE&lt;/code&gt; is a Postgres-specific tool, not something you can assume exists everywhere.&lt;/p&gt;

&lt;p&gt;Next up: joins — which is where a lot of the real anxiety around SQL interviews tends to live, and honestly, for good reason, since it's easy to get subtly wrong in a way that still returns results.&lt;/p&gt;

</description>
      <category>postgressql</category>
      <category>sql</category>
      <category>database</category>
      <category>mysql</category>
    </item>
    <item>
      <title>Constraints in PostgreSQL</title>
      <dc:creator>Nishant Gaurav </dc:creator>
      <pubDate>Thu, 15 Oct 2026 17:30:00 +0000</pubDate>
      <link>https://dev.to/im_nishant/constraints-in-postgresql-23om</link>
      <guid>https://dev.to/im_nishant/constraints-in-postgresql-23om</guid>
      <description>&lt;p&gt;There's a difference between "we validate this in the application" and "the database won't let this happen." I used to assume application-level validation was enough — check the input in Flask, reject anything weird, move on. Then I had a bug where a background job inserted a closure record for a venue that had already been deleted, because the validation logic lived in one code path and the background job used a different one that skipped it. The database happily accepted it, because nothing at the database level said it couldn't.&lt;/p&gt;

&lt;p&gt;Constraints are how you stop trusting your application code to always remember the rules, and instead make the rules structurally true no matter what code is writing to the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens without them
&lt;/h2&gt;

&lt;p&gt;Picture a &lt;code&gt;closures&lt;/code&gt; table that references &lt;code&gt;venues&lt;/code&gt; by ID, but with no foreign key set up — just a plain integer column that's supposed to match an existing venue. For a while, everything looks fine, because your application code is careful about it. Then someone writes a one-off script to backfill some historical data, or a bug in some unrelated feature deletes a venue without cleaning up its closures first, and now you've got closure records pointing at venues that don't exist. Nothing errors. Nothing crashes. The data is just quietly wrong, and you don't find out until a report starts showing null venue names where there shouldn't be any.&lt;/p&gt;

&lt;p&gt;This is the actual argument for constraints — not that they're a best practice you're supposed to follow, but that they close off entire categories of bugs from ever happening in the first place, instead of relying on every single piece of code that touches the table to remember the rules correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraints worth knowing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constraint&lt;/th&gt;
&lt;th&gt;What it enforces&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PRIMARY KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;uniqueness + not null, one per table&lt;/td&gt;
&lt;td&gt;&lt;code&gt;id SERIAL PRIMARY KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FOREIGN KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;referential integrity between tables&lt;/td&gt;
&lt;td&gt;&lt;code&gt;venue_id INTEGER REFERENCES venues(id)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UNIQUE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no duplicate values in a column (or column combo)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UNIQUE(name, city)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NOT NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;column can't be empty&lt;/td&gt;
&lt;td&gt;&lt;code&gt;name VARCHAR(100) NOT NULL&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CHECK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;custom rule on a value&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CHECK (latitude BETWEEN -90 AND 90)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DEFAULT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;value used when none is provided&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DEFAULT NOW()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;closures&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;venue_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;closed_on&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;risk_score&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;risk_score&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few of these are pretty self-explanatory — &lt;code&gt;NOT NULL&lt;/code&gt; and &lt;code&gt;DEFAULT&lt;/code&gt; don't need much unpacking. &lt;code&gt;CHECK&lt;/code&gt; is worth pointing out specifically because it's underused. Most people know about primary and foreign keys, fewer people think to add something like &lt;code&gt;CHECK (latitude BETWEEN -90 AND 90)&lt;/code&gt; even when the column obviously has a valid range. It's a small thing, but it means a typo that puts in &lt;code&gt;230&lt;/code&gt; instead of &lt;code&gt;23.0&lt;/code&gt; gets rejected immediately instead of silently breaking a map rendering somewhere downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deciding what happens when a parent row disappears
&lt;/h2&gt;

&lt;p&gt;This is the part of foreign keys that actually requires some thought, rather than just copying &lt;code&gt;REFERENCES table(column)&lt;/code&gt; and moving on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;venue_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;CASCADE&lt;/code&gt;&lt;/strong&gt; — deleting a venue deletes its closures too. Makes sense when the child data genuinely has no meaning without the parent. A closure record for a venue that no longer exists isn't useful information, it's just orphaned data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;RESTRICT&lt;/code&gt;&lt;/strong&gt; (the default-ish behavior if you don't specify anything) — blocks deleting a venue if closures still reference it. This forces whoever's doing the deleting to think about what should happen to the related data first, instead of it disappearing silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;SET NULL&lt;/code&gt;&lt;/strong&gt; — keeps the closure row, but sets &lt;code&gt;venue_id&lt;/code&gt; to null. Makes sense when the historical record matters even after the venue itself is gone — say, for reporting purposes, where you still want to count "there was a closure event" even if you can no longer say which venue it was.&lt;/p&gt;

&lt;p&gt;None of these is universally correct. It depends on whether the child data is meaningless without the parent, needs explicit cleanup, or should survive on its own. I've seen people default to &lt;code&gt;CASCADE&lt;/code&gt; everywhere because it's the path of least resistance, and then lose data they actually needed later because a parent row got deleted for an unrelated reason.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpneq3ssnlz9mf6h42k48.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpneq3ssnlz9mf6h42k48.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where constraints get uncomfortable in practice
&lt;/h2&gt;

&lt;p&gt;Constraints are great until you're trying to add one to a table that already has bad data in it. Adding &lt;code&gt;NOT NULL&lt;/code&gt; to an existing column, or a &lt;code&gt;CHECK&lt;/code&gt; constraint, fails outright if even one existing row violates it — Postgres won't just apply it going forward, it validates the whole table. On a small table that's a non-issue. On a large one, you sometimes need to clean up the offending rows first, or add the constraint as &lt;code&gt;NOT VALID&lt;/code&gt; and validate it separately later, so you're not holding a lock on the table while every existing row gets checked.&lt;/p&gt;

&lt;p&gt;The other thing that catches people off guard: constraints add real overhead on writes, not just reads. Every &lt;code&gt;INSERT&lt;/code&gt; or &lt;code&gt;UPDATE&lt;/code&gt; against a table with a foreign key means Postgres also has to check that the referenced row actually exists. It's usually not noticeable, but on a very high-throughput table, it's worth being aware that constraints aren't free — they're a trade, correctness for a small amount of write cost, and it's almost always worth it, but it's not nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing the venues/closures relationship from scratch
&lt;/h2&gt;

&lt;p&gt;Say you're setting this schema up for the first time, deciding what constraints actually belong on it, not just what the syntax looks like.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;venues.id&lt;/code&gt; is the primary key — straightforward, every table needs a way to uniquely identify a row. &lt;code&gt;venues.name&lt;/code&gt; gets &lt;code&gt;NOT NULL&lt;/code&gt;, because a venue with no name isn't meaningful data, it's a placeholder that shouldn't exist. If venues need to be unique per city — no duplicate "DB Mall" entries within the same city — that's &lt;code&gt;UNIQUE(name, city)&lt;/code&gt;, which catches accidental duplicate entries from a sync job without blocking two venues with the same name in different cities.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;closures.venue_id&lt;/code&gt; references &lt;code&gt;venues.id&lt;/code&gt;. The decision that actually takes thought is the &lt;code&gt;ON DELETE&lt;/code&gt; behavior. If venues in this system are only ever marked &lt;code&gt;is_closed = TRUE&lt;/code&gt; rather than actually deleted — which is the more common pattern in practice, since you usually want the history — then this decision barely matters, because venues rarely get hard-deleted in the first place. But if hard deletes are a real possibility, &lt;code&gt;RESTRICT&lt;/code&gt; is probably the safer default here, since it forces a conscious decision about what happens to the closure history instead of losing it silently through a &lt;code&gt;CASCADE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;closures.risk_score&lt;/code&gt;, if it's meant to represent a probability, gets &lt;code&gt;CHECK (risk_score BETWEEN 0 AND 1)&lt;/code&gt;. This is exactly the kind of constraint that's easy to skip because "the application already validates this," and exactly the kind that saves you when some other code path — a script, a migration, an admin tool — doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;Constraints move the responsibility for correctness out of application code, where it has to be remembered and re-implemented correctly everywhere data gets written, and into the database itself, where it's enforced no matter what's doing the writing. The &lt;code&gt;ON DELETE&lt;/code&gt; decision in particular is worth actually thinking through per relationship instead of defaulting to whatever's fastest to type.&lt;/p&gt;

&lt;p&gt;A good exercise, and one that comes up constantly in interviews, is being asked to design a small schema with two or three tables and their constraints out loud. It's worth practicing that out loud, not just reading through examples like this one, because explaining &lt;em&gt;why&lt;/em&gt; you chose &lt;code&gt;CASCADE&lt;/code&gt; over &lt;code&gt;RESTRICT&lt;/code&gt; is a different skill than recognizing the syntax.&lt;/p&gt;

&lt;p&gt;Next up: filtering, sorting, and pattern matching — the &lt;code&gt;WHERE&lt;/code&gt; clause in more depth, and a few things that are genuinely Postgres-specific.&lt;/p&gt;

</description>
      <category>postgressql</category>
      <category>sql</category>
      <category>mysql</category>
      <category>database</category>
    </item>
    <item>
      <title>INSERT, SELECT, UPDATE, DELETE: PostgreSQL</title>
      <dc:creator>Nishant Gaurav </dc:creator>
      <pubDate>Wed, 14 Oct 2026 17:30:00 +0000</pubDate>
      <link>https://dev.to/im_nishant/insert-select-update-delete-postgresql-1l8f</link>
      <guid>https://dev.to/im_nishant/insert-select-update-delete-postgresql-1l8f</guid>
      <description>&lt;p&gt;At some point you stop thinking about SQL as "a language" and it just becomes muscle memory — you know what an &lt;code&gt;UPDATE&lt;/code&gt; without a &lt;code&gt;WHERE&lt;/code&gt; clause does before you even finish typing it, because you've either done it once by accident or heard someone else's horror story about it. That's basically what this article is about. Not anything exotic, just the four commands that make up most of what you actually do day to day, and the handful of ways each one bites people early on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is worth slowing down for
&lt;/h2&gt;

&lt;p&gt;It's tempting to skim past CRUD because it feels like the "easy" part of SQL — everyone's written a &lt;code&gt;SELECT&lt;/code&gt; before. But most of the actual production incidents I've read about, and a couple I've caused myself in test databases on purpose just to see what happens, come from these four commands specifically. Not from window functions or CTEs. From a missing &lt;code&gt;WHERE&lt;/code&gt; clause, or a &lt;code&gt;SELECT *&lt;/code&gt; that broke when someone renamed a column six months later.&lt;/p&gt;

&lt;p&gt;The syntax is simple. The discipline around using it correctly is the part that takes longer to build.&lt;/p&gt;

&lt;h2&gt;
  
  
  INSERT: getting data in
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Van Vihar'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2237&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;77&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3910&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- multiple rows in one call — fewer round trips, faster&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Upper Lake'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2415&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;77&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3550&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DB Mall'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;77&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4340&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The multi-row version is worth using whenever you're inserting more than one row at a time, even for something small. It's not just a style preference — one round trip to the database instead of several actually adds up, especially if you're populating a table with anything more than a handful of rows.&lt;/p&gt;

&lt;p&gt;Here's a detail that connects back to how Postgres works internally: every &lt;code&gt;INSERT&lt;/code&gt; creates a brand new row version. Postgres doesn't have a concept of "editing in place" the way you might picture it. That's tied directly to MVCC, which is a much bigger topic I'll get into properly later in this series — for now it's enough to know that even &lt;code&gt;INSERT&lt;/code&gt; isn't as simple under the hood as "write these bytes to a table."&lt;/p&gt;

&lt;h2&gt;
  
  
  SELECT: the one you'll use more than everything else combined
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;longitude&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_closed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;WHERE&lt;/code&gt; filters rows before anything gets grouped or aggregated. &lt;code&gt;ORDER BY&lt;/code&gt; sorts whatever's left. &lt;code&gt;LIMIT&lt;/code&gt;/&lt;code&gt;OFFSET&lt;/code&gt; handle pagination, though &lt;code&gt;OFFSET&lt;/code&gt; gets slow once a table grows large — that's a topic for the indexing article later on, but it's worth knowing it exists as a limitation now.&lt;/p&gt;

&lt;p&gt;The thing that trips people up early, and that I did plenty myself when I was starting out, is reaching for &lt;code&gt;SELECT *&lt;/code&gt; out of habit. It's fine while you're exploring data in &lt;code&gt;psql&lt;/code&gt;. It's a problem in application code, for a few reasons that aren't obvious until they bite you: it breaks silently if someone adds or renames a column later, it pulls back data you don't actually need, and it can block certain optimizations — like an index-only scan, where Postgres could answer a query using just the index without touching the table at all, but only if it knows exactly which columns you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  UPDATE: changing what's already there
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;is_closed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one rule here that actually matters: never run an &lt;code&gt;UPDATE&lt;/code&gt; without a &lt;code&gt;WHERE&lt;/code&gt; clause unless you genuinely mean to update every row in the table. This is one of those things that sounds obvious written down, and then someone runs a query against production instead of the staging environment they meant to be in, and every row silently gets the same value. It happens more than you'd think, usually from copy-pasting a query and forgetting to re-add the &lt;code&gt;WHERE&lt;/code&gt; at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  DELETE: same warning, different verb
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_closed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same logic as &lt;code&gt;UPDATE&lt;/code&gt; — no &lt;code&gt;WHERE&lt;/code&gt; means "delete everything," and Postgres will do exactly that without complaining, because as far as it's concerned that's a perfectly valid instruction. If you're ever testing a &lt;code&gt;DELETE&lt;/code&gt; on something you're not 100% sure about, wrapping it in a transaction first (&lt;code&gt;BEGIN&lt;/code&gt;, run the delete, check the result, then &lt;code&gt;COMMIT&lt;/code&gt; or &lt;code&gt;ROLLBACK&lt;/code&gt;) is a cheap habit that saves you from this exact situation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ukizwfetyauv0ua9xqk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ukizwfetyauv0ua9xqk.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it actually gets interesting: UPSERT
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Van Vihar National Park'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2237&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;77&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3910&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is Postgres's answer to "insert this row, but if it already exists, update it instead" — a pattern that comes up constantly in real projects. Syncing data from an external API is the most common case: you're pulling venue data from some third-party source on a schedule, and you don't know ahead of time whether a given venue is already in your table or not. Instead of writing a &lt;code&gt;SELECT&lt;/code&gt; to check first and then branching into either an &lt;code&gt;INSERT&lt;/code&gt; or an &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;ON CONFLICT&lt;/code&gt; handles both in one statement.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EXCLUDED&lt;/code&gt; refers to the row that would have been inserted if there hadn't been a conflict — so &lt;code&gt;EXCLUDED.name&lt;/code&gt; means "the name value from the row I was trying to insert." If you just want to skip duplicates entirely without updating anything, &lt;code&gt;ON CONFLICT DO NOTHING&lt;/code&gt; does that.&lt;/p&gt;

&lt;p&gt;If you get asked how to handle duplicate key inserts gracefully, this is the expected answer, and it's worth mentioning &lt;code&gt;DO NOTHING&lt;/code&gt; alongside &lt;code&gt;DO UPDATE&lt;/code&gt; since they solve slightly different problems — one is deduplication, the other is a real upsert.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together: syncing venue data from an external source
&lt;/h2&gt;

&lt;p&gt;Say your dashboard pulls venue data from a city API once a day, and venues can be added, renamed, or marked closed on their end. Here's roughly how that sync would actually look, combining a few of these commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_closed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Central Park Cafe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2510&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;77&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4020&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;is_closed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_closed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run once per venue returned from the API, this single statement handles both new venues and updates to existing ones, without you needing to check which case you're in first. Compare that to the naive version — a &lt;code&gt;SELECT&lt;/code&gt; to check existence, then an &lt;code&gt;if/else&lt;/code&gt; in application code choosing between &lt;code&gt;INSERT&lt;/code&gt; and &lt;code&gt;UPDATE&lt;/code&gt; — and it's not just shorter, it also avoids a race condition where two sync jobs running close together could both decide the row doesn't exist yet and both try to insert it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;These four commands, plus &lt;code&gt;ON CONFLICT&lt;/code&gt;, cover most of what you'll actually type against a database in a normal week. The syntax isn't the hard part — the discipline of always including &lt;code&gt;WHERE&lt;/code&gt; on anything destructive, and reaching for &lt;code&gt;ON CONFLICT&lt;/code&gt; instead of a manual check-then-branch pattern, is what actually separates code that survives contact with real data from code that doesn't.&lt;/p&gt;

&lt;p&gt;Next up: constraints — how to make bad data structurally impossible to insert in the first place, instead of relying on catching it after the fact.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>PostgreSQL Data Types:</title>
      <dc:creator>Nishant Gaurav </dc:creator>
      <pubDate>Tue, 13 Oct 2026 17:30:00 +0000</pubDate>
      <link>https://dev.to/im_nishant/postgresql-data-types-36em</link>
      <guid>https://dev.to/im_nishant/postgresql-data-types-36em</guid>
      <description>&lt;p&gt;There's a specific kind of bug that doesn't show up right away. A &lt;code&gt;latitude&lt;/code&gt; column stored as &lt;code&gt;FLOAT&lt;/code&gt;, and months later your distance calculations are just slightly off in a way that's hard to pin down. Or a &lt;code&gt;price&lt;/code&gt; column using the same type, and a customer notices their total is a cent wrong, in a way that somehow compounds across enough transactions to matter. Nobody wrote a bug in the usual sense. The data type was the bug, and it had been sitting there quietly since the table was created.&lt;/p&gt;

&lt;p&gt;Choosing a column's type feels like the most mechanical part of designing a schema — pick something that looks roughly right and move on. I used to treat it that way too. It's actually one of the few decisions that's genuinely annoying to undo later, because by the time you notice it's wrong, there's real data sitting in that column.&lt;/p&gt;

&lt;h2&gt;
  
  
  A table that works fine until it doesn't
&lt;/h2&gt;

&lt;p&gt;Say you're building a &lt;code&gt;venues&lt;/code&gt; table for a dashboard that tracks locations and closures. It's tempting to just reach for whatever feels familiar — &lt;code&gt;VARCHAR&lt;/code&gt; for text, &lt;code&gt;FLOAT&lt;/code&gt; for anything with a decimal, &lt;code&gt;INTEGER&lt;/code&gt; for IDs without thinking twice.&lt;/p&gt;

&lt;p&gt;That table inserts rows fine. It passes whatever tests you write in the first week. Then a risk score computed from &lt;code&gt;FLOAT&lt;/code&gt; values starts drifting in a way that doesn't reproduce consistently, or the ID column runs into its ceiling because nobody considered &lt;code&gt;BIGINT&lt;/code&gt;, or a &lt;code&gt;TIMESTAMP&lt;/code&gt; column with no timezone awareness makes an event look like it happened in a different hour depending on which server handled it. None of these are exotic problems. They're just what happens when types don't get real thought up front.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going through the types one at a time
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The table itself
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;latitude&lt;/span&gt; &lt;span class="nb"&gt;DOUBLE&lt;/span&gt; &lt;span class="nb"&gt;PRECISION&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;longitude&lt;/span&gt; &lt;span class="nb"&gt;DOUBLE&lt;/span&gt; &lt;span class="nb"&gt;PRECISION&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;is_closed&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a fairly realistic starting point for a venue-tracking dashboard. Every type choice here is worth understanding on its own, not just copying because it looked right in a tutorial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;INTEGER&lt;/code&gt; / &lt;code&gt;BIGINT&lt;/code&gt;&lt;/strong&gt; — good for counts and IDs. The distinction matters once you think about scale: &lt;code&gt;INTEGER&lt;/code&gt; tops out around 2.1 billion. If there's any real chance a table could grow past that — a high-volume events table, say — starting with &lt;code&gt;BIGINT&lt;/code&gt; costs nothing now and avoids a painful migration later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;SERIAL&lt;/code&gt; / &lt;code&gt;BIGSERIAL&lt;/code&gt;&lt;/strong&gt; — the usual auto-incrementing ID setup. It's worth knowing this isn't actually a separate type. It's an &lt;code&gt;INTEGER&lt;/code&gt; (or &lt;code&gt;BIGINT&lt;/code&gt;) column backed by a sequence that hands out the next value automatically. That distinction matters later if you ever need to work with the sequence directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;VARCHAR(n)&lt;/code&gt; vs &lt;code&gt;TEXT&lt;/code&gt;&lt;/strong&gt; — this one surprised me coming from MySQL. In Postgres these perform almost identically. &lt;code&gt;TEXT&lt;/code&gt; is usually the better default unless you actually need to enforce a maximum length — &lt;code&gt;VARCHAR(n)&lt;/code&gt; is really just &lt;code&gt;TEXT&lt;/code&gt; with a length constraint bolted on, not a different storage format underneath.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;NUMERIC(p,s)&lt;/code&gt; vs &lt;code&gt;DOUBLE PRECISION&lt;/code&gt;&lt;/strong&gt; — this is the one that actually causes real incidents. &lt;code&gt;NUMERIC&lt;/code&gt; stores exact decimal values. &lt;code&gt;DOUBLE PRECISION&lt;/code&gt; is a floating-point approximation. Don't use &lt;code&gt;FLOAT&lt;/code&gt;/&lt;code&gt;DOUBLE PRECISION&lt;/code&gt; for money — the rounding errors aren't a hypothetical, they show up given enough transactions. &lt;code&gt;DOUBLE PRECISION&lt;/code&gt; is fine for something like geospatial coordinates, where the imprecision at that scale doesn't really change the result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;BOOLEAN&lt;/code&gt;&lt;/strong&gt; — flags. Not much to say here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;TIMESTAMP&lt;/code&gt; vs &lt;code&gt;TIMESTAMPTZ&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;TIMESTAMPTZ&lt;/code&gt; stores the value normalized to UTC and converts it on display based on the client's timezone. &lt;code&gt;TIMESTAMP&lt;/code&gt; just stores whatever you gave it, with no timezone awareness at all. For pretty much any real app — anything with users spread across timezones, or a backend and frontend that might not agree on local time — &lt;code&gt;TIMESTAMPTZ&lt;/code&gt; is the safer default, even though &lt;code&gt;TIMESTAMP&lt;/code&gt; looks simpler at first glance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;JSONB&lt;/code&gt;&lt;/strong&gt; — binary-stored, indexable, faster to query than &lt;code&gt;JSON&lt;/code&gt;, which is just stored as raw text. Anything semi-structured, like a payload synced from a third-party API, belongs in &lt;code&gt;JSONB&lt;/code&gt; by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;UUID&lt;/code&gt;&lt;/strong&gt; — useful when IDs need to be generated somewhere other than the database itself, or across multiple systems, without collision risk. Needs either &lt;code&gt;gen_random_uuid()&lt;/code&gt; or an extension like &lt;code&gt;pgcrypto&lt;/code&gt; or &lt;code&gt;uuid-ossp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjty6ob1h84uormqt8r4w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjty6ob1h84uormqt8r4w.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The two questions that always come up
&lt;/h2&gt;

&lt;p&gt;"Difference between &lt;code&gt;VARCHAR&lt;/code&gt; and &lt;code&gt;TEXT&lt;/code&gt;?" — functionally almost identical in Postgres, unlike MySQL. &lt;code&gt;VARCHAR(n)&lt;/code&gt; just adds a length constraint on top of the same underlying storage.&lt;/p&gt;

&lt;p&gt;"Difference between &lt;code&gt;JSON&lt;/code&gt; and &lt;code&gt;JSONB&lt;/code&gt;?" — &lt;code&gt;JSON&lt;/code&gt; stores the exact text, preserving whitespace and key order, but it's slower to query and doesn't support much indexing. &lt;code&gt;JSONB&lt;/code&gt; stores a parsed binary format, which loses the exact original formatting but is faster and indexable through GIN indexes. Default to &lt;code&gt;JSONB&lt;/code&gt; most of the time, and only reach for plain &lt;code&gt;JSON&lt;/code&gt; if you specifically need to preserve the text exactly as it came in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Altering a table isn't always as harmless as it looks
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;venue_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These look simple, and on a small table they are. The problem shows up at scale — altering a table with millions of rows during peak traffic can lock it, blocking reads and writes for however long the operation takes. &lt;code&gt;ADD COLUMN&lt;/code&gt; with a default value used to force a full table rewrite in older Postgres versions. That got a lot cheaper starting in Postgres 11 for constant defaults, but it's worth knowing that history exists, since not every production system runs the latest version, and assuming modern behavior on an older cluster is a reasonable way to cause an outage.&lt;/p&gt;

&lt;h3&gt;
  
  
  DROP vs TRUNCATE vs DELETE
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;-- removes the table and its structure entirely&lt;/span&gt;
&lt;span class="k"&gt;TRUNCATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;-- empties all rows, keeps the structure&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;-- removes rows, can be filtered, slower, undoable within a transaction&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DELETE&lt;/code&gt; works row by row, gets logged, supports &lt;code&gt;WHERE&lt;/code&gt;, and is fully transaction-safe — you can roll it back. &lt;code&gt;TRUNCATE&lt;/code&gt; is close to instant because it deallocates whole pages instead of scanning rows one at a time. It can't use &lt;code&gt;WHERE&lt;/code&gt;, but it's still transactional in Postgres, so it can be rolled back too, as long as you're inside a transaction that hasn't committed. &lt;code&gt;DROP&lt;/code&gt; removes the entire table, structure included, not just what's inside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing a type before it becomes a real migration
&lt;/h2&gt;

&lt;p&gt;Say the &lt;code&gt;venues&lt;/code&gt; table above shipped with &lt;code&gt;risk_score DOUBLE PRECISION&lt;/code&gt;, because someone figured "it's just a number between 0 and 1, a float is fine." A few weeks later, a report comparing risk scores across venues starts showing inconsistent sort order for values that should be identical — classic floating-point comparison drift.&lt;/p&gt;

&lt;p&gt;Caught early, the fix is a simple alteration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;venues&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;risk_score&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caught after the table has millions of rows, this becomes a much more careful operation — possibly needing a maintenance window, or a safer pattern like adding a new column, backfilling it, and swapping it in. The gap between fixing this on day one and fixing it six months later isn't small. It's the difference between a one-line change and a planned migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;Types in Postgres aren't just about what values a column technically accepts — they carry real assumptions about precision, timezone handling, and scale. Those assumptions are cheap to get right when you're creating the table and expensive to fix once there's data in it. Next time you write a &lt;code&gt;CREATE TABLE&lt;/code&gt;, it's worth treating each type as a real decision, especially &lt;code&gt;NUMERIC&lt;/code&gt; vs &lt;code&gt;DOUBLE PRECISION&lt;/code&gt; for anything money-related, and &lt;code&gt;TIMESTAMPTZ&lt;/code&gt; vs &lt;code&gt;TIMESTAMP&lt;/code&gt; for anything tied to a user.&lt;/p&gt;

&lt;p&gt;Next up: actually working with the data — &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, and the upsert pattern for handling "insert or update" cleanly.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>database</category>
      <category>mysql</category>
    </item>
    <item>
      <title>Getting Into PostgreSQL: psql, Connections, and the First Commands You Actually Need</title>
      <dc:creator>Nishant Gaurav </dc:creator>
      <pubDate>Mon, 12 Oct 2026 17:30:00 +0000</pubDate>
      <link>https://dev.to/im_nishant/getting-into-postgresql-psql-connections-and-the-first-commands-you-actually-need-1mgg</link>
      <guid>https://dev.to/im_nishant/getting-into-postgresql-psql-connections-and-the-first-commands-you-actually-need-1mgg</guid>
      <description>&lt;p&gt;Most tutorials rush past this part. "Install Postgres, connect, start querying" — as if opening the database is the boring bit you skip through to get to the real content. When I was starting, that gap was exactly where I felt lost. You're connected, technically, but you have no idea what's actually in front of you.&lt;/p&gt;

&lt;p&gt;This article covers those first few minutes: getting into &lt;code&gt;psql&lt;/code&gt;, figuring out what you're connected to, and a handful of commands that eventually become reflexes if you use Postgres regularly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not knowing your way around costs you time
&lt;/h2&gt;

&lt;p&gt;Say you inherit a project. Someone hands you a connection string, you open &lt;code&gt;psql&lt;/code&gt;, and you're in. Now what? You don't know what databases exist on the server. You don't know what tables are in the one you're connected to. Someone mentioned a table in a Slack message three months ago and you have no idea what columns it has.&lt;/p&gt;

&lt;p&gt;Without a small set of orientation commands, every one of those questions turns into either opening a GUI (if one's set up) or trying to remember how to query Postgres's internal system catalogs directly, which honestly nobody has memorized. There's a small handful of &lt;code&gt;psql&lt;/code&gt; shortcuts that answer all of this in a couple of seconds, and once they're second nature, this stops being a problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting, and the commands that orient you
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Connecting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="nt"&gt;-U&lt;/span&gt; username &lt;span class="nt"&gt;-d&lt;/span&gt; database_name &lt;span class="nt"&gt;-h&lt;/span&gt; host &lt;span class="nt"&gt;-p&lt;/span&gt; 5432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each flag does one job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-U&lt;/code&gt; — the user you're connecting as&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-d&lt;/code&gt; — the database to connect to&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-h&lt;/code&gt; — the host, a remote server or &lt;code&gt;localhost&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-p&lt;/code&gt; — the port, and 5432 is Postgres's default. It's a small detail, but it comes up as a quick sanity-check question fairly often.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A few commands worth knowing by heart
&lt;/h3&gt;

&lt;p&gt;Once you're inside &lt;code&gt;psql&lt;/code&gt;, there's a set of meta-commands — not SQL, just shortcuts specific to &lt;code&gt;psql&lt;/code&gt; itself, which is why they start with a backslash and don't need a semicolon:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\l&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;list databases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\c dbname&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;connect to a database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\dt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;list tables in the current schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\d tablename&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;describe a table — columns, types, constraints, indexes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\du&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;list roles/users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\q&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;quit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;\d tablename&lt;/code&gt; is the one I use the most. It's the fastest way to see what a table actually looks like without leaving the terminal, and it shows constraints and indexes too, not just column names — something a plain &lt;code&gt;SELECT * LIMIT 1&lt;/code&gt; won't tell you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why these are separate from SQL
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;psql&lt;/code&gt; splits things into two categories on purpose. SQL is for talking to the database engine about your data — it gets parsed, planned, and executed on the server. Meta-commands are for talking to the client about your environment, so &lt;code&gt;\dt&lt;/code&gt; doesn't need a semicolon and responds more or less instantly, because it's not going through the same query pipeline.&lt;/p&gt;

&lt;p&gt;If you get asked how to check a table's structure without a GUI, &lt;code&gt;\d tablename&lt;/code&gt; is basically the expected answer. It's less about the command itself and more a quick check on whether you've actually spent time in a terminal with Postgres, versus only clicking around in a visual tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the confusion usually starts: databases, clusters, templates
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Creating and dropping a database
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;project_db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;project_db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One convention worth knowing early: one database per logical application is the norm. A geospatial dashboard would get its own database, separate from anything unrelated, even if they're sitting on the same server.&lt;/p&gt;

&lt;p&gt;In multi-tenant setups, people sometimes reach for "one database per tenant" because it feels like clean isolation. It usually isn't the right call — hundreds of databases becomes a real management headache. A &lt;code&gt;tenant_id&lt;/code&gt; column, or separate schemas per tenant within one database, tends to hold up much better as things grow.&lt;/p&gt;

&lt;h3&gt;
  
  
  What CREATE DATABASE is actually doing
&lt;/h3&gt;

&lt;p&gt;Here's a detail that surprised me the first time I read about it: &lt;code&gt;CREATE DATABASE&lt;/code&gt; doesn't build an empty database from scratch. It clones a template database — &lt;code&gt;template1&lt;/code&gt; by default. That explains a specific category of confusing bugs. If &lt;code&gt;template1&lt;/code&gt; has an unexpected encoding or locale setting, every database you create afterward quietly inherits it, and the symptoms show up much later as garbled text or sorting that doesn't behave the way you'd expect.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy5p5ow0fh8xr44ry0njy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy5p5ow0fh8xr44ry0njy.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up a project database, step by step
&lt;/h2&gt;

&lt;p&gt;Say you're starting the geospatial dashboard from nothing. Here's roughly how that goes in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-h&lt;/span&gt; localhost &lt;span class="nt"&gt;-p&lt;/span&gt; 5432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're connected to the default &lt;code&gt;postgres&lt;/code&gt; database — not your project's yet, just the entry point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirms nothing named &lt;code&gt;venue_dashboard&lt;/code&gt; already exists, so you're not about to collide with something.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;venue_dashboard&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="n"&gt;venue_dashboard&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you're connected to your project's own database, kept separate from anything else on the server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing comes back — no tables yet, which is expected on a fresh database. It also confirms you're actually in the right place before running any &lt;code&gt;CREATE TABLE&lt;/code&gt; statements.&lt;/p&gt;

&lt;p&gt;Connect, list, create, switch, verify. It's a small sequence, but it's one you end up running at the start of most new projects, and doing it deliberately once means you're not fumbling through it later, especially in something like a live technical interview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;The few minutes before your first &lt;code&gt;CREATE TABLE&lt;/code&gt; matter more than they seem to. Knowing &lt;code&gt;\dt&lt;/code&gt;, &lt;code&gt;\d&lt;/code&gt;, and &lt;code&gt;\l&lt;/code&gt; well enough that you don't have to think about them, and understanding that &lt;code&gt;CREATE DATABASE&lt;/code&gt; clones a template rather than starting from nothing, makes navigating Postgres a lot less stressful day to day.&lt;/p&gt;

&lt;p&gt;Next time you sit down in &lt;code&gt;psql&lt;/code&gt;, even in a database you know well, try running the orientation commands first anyway. It's a small habit, but it holds up well as the number of databases and tables you're dealing with grows.&lt;/p&gt;

&lt;p&gt;Next up: actually shaping data — tables, data types, and constraints that make bad data hard to insert in the first place.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>postgressql</category>
      <category>database</category>
    </item>
  </channel>
</rss>
