<?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: LeoJ</title>
    <description>The latest articles on DEV Community by LeoJ (@leoj).</description>
    <link>https://dev.to/leoj</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F814392%2F2bfdccfb-898a-4693-82ea-32b93dad7b78.jpeg</url>
      <title>DEV Community: LeoJ</title>
      <link>https://dev.to/leoj</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/leoj"/>
    <language>en</language>
    <item>
      <title>10 SQL Query Optimization Tips Every Developer Should Know</title>
      <dc:creator>LeoJ</dc:creator>
      <pubDate>Mon, 25 May 2026 03:04:39 +0000</pubDate>
      <link>https://dev.to/leoj/10-sql-query-optimization-tips-every-developer-should-know-g3k</link>
      <guid>https://dev.to/leoj/10-sql-query-optimization-tips-every-developer-should-know-g3k</guid>
      <description>&lt;p&gt;Every application hits a point where the database becomes the bottleneck. Response times creep up, connection pools fill, and users start complaining. Most slow queries share common problems, and most fixes are straightforward once you know where to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Stop Using SELECT *
&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;-- Bad: fetches 25 columns including blobs&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;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1042&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Good: fetches only what you need&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1042&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Selecting only needed columns reduces disk I/O and network bandwidth. If an index covers all requested columns, the database can satisfy the query from the index alone (covering index scan).&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Add the Right Indexes
&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;-- No index: Seq Scan on 10M rows, takes seconds&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_amount&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1042&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Fix: add an index&lt;/span&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_orders_customer_id&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- For filter + sort, composite indexes are even better:&lt;/span&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_orders_customer_date&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_date&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;Indexes turn O(n) scans into O(log n) lookups. The trade-off is write overhead, but for read-heavy workloads the gains far outweigh the cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Use EXPLAIN ANALYZE Before Guessing
&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;-- Don't guess, measure:&lt;/span&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="n"&gt;o&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;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_date&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;name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2026-01-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output tells you exactly what happened: which nodes are slow, where Seq Scans occur, whether estimates match reality. It replaces speculation with data.&lt;/p&gt;

&lt;p&gt;Look for: Seq Scans on large tables, large gaps between estimated and actual row counts, sorts spilling to disk, nested loops with high loop counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Replace Correlated Subqueries with JOINs
&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;-- Bad: subquery runs once per customer row (50,000 executions)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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;c&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="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_date&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;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_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;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;last_order_date&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Good: single pass with a JOIN&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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;c&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;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_date&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;last_order_date&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&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;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_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;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="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;c&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;The JOIN version scans &lt;code&gt;orders&lt;/code&gt; once. Performance difference grows linearly with the number of outer rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Use CTEs Carefully
&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;-- Bad (MySQL, PG &amp;lt; 12): materializes entire table first&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;all_orders&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;all_orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1042&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Good: push filters into the CTE, or just skip it&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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;order_date&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1042&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In PostgreSQL 12+, CTEs are automatically inlined. In older versions and MySQL, they materialize as temporary tables, blocking predicate pushdown. Use &lt;code&gt;WITH ... AS NOT MATERIALIZED&lt;/code&gt; (PG 12+) when you need the CTE for readability but want optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Fix N+1 Queries at the Application Layer
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Bad: 201 queries for 200 customers
&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM customers WHERE region = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;EU&lt;/span&gt;&lt;span class="sh"&gt;'"&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;customer&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM orders WHERE customer_id = %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Good: single query with JOIN
&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
SELECT c.id, c.name, o.id AS order_id, o.order_date, o.total_amount
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE c.region = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;EU&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The overhead is round-trip latency, not SQL execution. 200 queries at 2ms network round-trip = 400ms on network alone. Most ORMs have eager loading (&lt;code&gt;select_related&lt;/code&gt;, &lt;code&gt;includes&lt;/code&gt;, &lt;code&gt;eager&lt;/code&gt;) to solve this.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Paginate with Cursors, Not OFFSET
&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;-- Bad: page 100 scans 5,000 rows, returns 50&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;articles&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;50&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;4950&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Good: cursor-based, always scans only 50 rows&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;articles&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="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="s1"&gt;'2026-05-20T10:15:00Z'&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;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cursor-based pagination maintains constant performance regardless of depth. OFFSET degrades linearly. The trade-off: you lose arbitrary page jumping, but most modern UIs don't need that.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Batch Your Writes
&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;-- Bad: 10,000 individual INSERTs&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;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&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;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'click'&lt;/span&gt;&lt;span class="p"&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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&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;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'view'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="c1"&gt;-- ... 9,998 more&lt;/span&gt;

&lt;span class="c1"&gt;-- Good: single multi-row INSERT&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;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&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;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'click'&lt;/span&gt;&lt;span class="p"&gt;,&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;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'view'&lt;/span&gt;&lt;span class="p"&gt;,&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;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'click'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="c1"&gt;-- batch up to 500-1,000 rows per statement&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For batch updates in PostgreSQL:&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;price&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;new_price&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;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;102&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;103&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&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;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;span class="n"&gt;new_price&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;p&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="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;Batching reduces round-trips, WAL overhead, and allows write path optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Use Parameterized Queries
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Bad: string concatenation (SQL injection + no plan caching)
&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM users WHERE email = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;

&lt;span class="c1"&gt;# Good: parameterized (safe + plan reuse)
&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT id, name, email FROM users WHERE email = $1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;user_email&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parameterized queries let the database parse once and reuse the plan. Also completely prevents SQL injection. Every modern driver supports this.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Cache Expensive Query Results
&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;-- PostgreSQL: materialized view for expensive aggregations&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;monthly_revenue&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;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&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_orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_amount&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;revenue&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;total_amount&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_order_value&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'12 months'&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;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Refresh on schedule (e.g., hourly via pg_cron)&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;CONCURRENTLY&lt;/span&gt; &lt;span class="n"&gt;monthly_revenue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For application-level caching, store results in Redis with a TTL matching your freshness requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;p&gt;When you encounter a slow query:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; (PostgreSQL) or &lt;code&gt;EXPLAIN FORMAT=JSON&lt;/code&gt; (MySQL)&lt;/li&gt;
&lt;li&gt;Check for missing indexes (Seq Scans on large tables)&lt;/li&gt;
&lt;li&gt;Eliminate &lt;code&gt;SELECT *&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Flatten correlated subqueries into JOINs&lt;/li&gt;
&lt;li&gt;Fix N+1 patterns with eager loading&lt;/li&gt;
&lt;li&gt;Switch to keyset pagination&lt;/li&gt;
&lt;li&gt;Batch writes&lt;/li&gt;
&lt;li&gt;Use prepared statements&lt;/li&gt;
&lt;li&gt;Cache expensive results&lt;/li&gt;
&lt;li&gt;Re-measure with &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; to confirm the fix&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;I'm building &lt;a href="https://www.querydeck.app" rel="noopener noreferrer"&gt;QueryDeck&lt;/a&gt;, a native macOS database client with visual EXPLAIN ANALYZE and AI-assisted SQL. Currently in early access.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>10 Production Database Safety Rules Every Developer Needs</title>
      <dc:creator>LeoJ</dc:creator>
      <pubDate>Mon, 25 May 2026 03:03:34 +0000</pubDate>
      <link>https://dev.to/leoj/10-production-database-safety-rules-every-developer-needs-2o9p</link>
      <guid>https://dev.to/leoj/10-production-database-safety-rules-every-developer-needs-2o9p</guid>
      <description>&lt;p&gt;Somewhere right now, a developer is staring at a terminal, realizing the &lt;code&gt;DELETE FROM users&lt;/code&gt; they just ran did not have a &lt;code&gt;WHERE&lt;/code&gt; clause. Their staging tab was not the active tab. The table had 2.4 million rows. It has zero now.&lt;/p&gt;

&lt;p&gt;GitLab famously lost six hours of production data in 2017 when an engineer ran &lt;code&gt;rm -rf&lt;/code&gt; on the wrong database directory during a late-night incident. This is not rare. It happens constantly.&lt;/p&gt;

&lt;p&gt;Production database safety is not about being careful. Careful people make mistakes at 2 AM after sixteen hours of debugging. Safety comes from systems, defaults, and friction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rule 1: Production Is Red, Always
&lt;/h2&gt;

&lt;p&gt;The most common cause of accidental production writes is tab confusion. Four database connections open. Staging and production look identical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix is visual.&lt;/strong&gt; Production should look unmistakably different from every other environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Production: &lt;strong&gt;red&lt;/strong&gt; background, red title bar&lt;/li&gt;
&lt;li&gt;Staging: orange or yellow&lt;/li&gt;
&lt;li&gt;Development: green&lt;/li&gt;
&lt;li&gt;Local: default / neutral&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not cosmetic. This is the cheapest safety measure with the highest return. Every team that adopts environment coloring reports fewer "wrong environment" incidents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 2: Read-Only by Default
&lt;/h2&gt;

&lt;p&gt;How often do you actually &lt;em&gt;write&lt;/em&gt; to production? For most developers, 95% of production access is read access. So why does your connection default to full read-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="c1"&gt;-- What you run 95% of the time (safe in read-only mode):&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&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;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&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;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;48291&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;o&lt;/span&gt;&lt;span class="p"&gt;.&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;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- PostgreSQL: set read-only at the session level&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;default_transaction_read_only&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- When you genuinely need to write:&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;default_transaction_read_only&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you need to write, you explicitly switch. One extra step that prevents more accidental &lt;code&gt;UPDATE&lt;/code&gt; statements than any code review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 3: Back Up Before ALTER
&lt;/h2&gt;

&lt;p&gt;Schema changes are irreversible in a way data changes are not. You can roll back a bad &lt;code&gt;UPDATE&lt;/code&gt; from a backup. But &lt;code&gt;ALTER TABLE DROP COLUMN&lt;/code&gt; destroys data immediately.&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;-- Before dropping a column, back it up:&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_backup_20260524&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;legacy_metadata&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Now you have a safety net:&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;orders&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;legacy_metadata&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;ALTER TABLE&lt;/code&gt; operations, always estimate lock duration. In PostgreSQL, many &lt;code&gt;ALTER TABLE&lt;/code&gt; operations acquire &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; locks, blocking all reads and writes:&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;-- This locks the entire table until it finishes:&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;orders&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;processed_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- This is instant in PostgreSQL 11+:&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;orders&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;processed_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&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="c1"&gt;-- Adding a column with a non-volatile default is instant since PG 11.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plan schema changes like surgery: with imaging, preparation, and a way to abort.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 4: Never DELETE Without Verifying First
&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;-- The pattern that kills databases:&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;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Missing WHERE clause. Everything gone.&lt;/span&gt;

&lt;span class="c1"&gt;-- The safe pattern:&lt;/span&gt;
&lt;span class="c1"&gt;-- Step 1: Verify scope&lt;/span&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;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'cancelled'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="s1"&gt;'2026-01-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Returns: 847&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2: Inspect a sample&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'cancelled'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="s1"&gt;'2026-01-01'&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 3: Delete with the exact same WHERE clause&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;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'cancelled'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="s1"&gt;'2026-01-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- 847 rows deleted. Matches the count.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SELECT&lt;/code&gt; first, then &lt;code&gt;DELETE&lt;/code&gt;. The oldest database safety trick, still the most effective.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 5: Wrap Mutations in Transactions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Every manual mutation on production should be wrapped in a transaction.&lt;/strong&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;-- DANGEROUS: runs immediately, no undo&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'enterprise'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;company_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4012&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- SAFE: wrapped in a transaction&lt;/span&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;users&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'enterprise'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;company_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4012&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Check what you just did:&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;company_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4012&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Does this look right?&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;-- If yes&lt;/span&gt;
&lt;span class="k"&gt;ROLLBACK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- If no&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transaction gives you a window between execution and commitment. You can inspect results before they become permanent.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;UPDATE&lt;/code&gt; is the most dangerous DML operation. A bad &lt;code&gt;INSERT&lt;/code&gt; adds rows you can delete. A bad &lt;code&gt;DELETE&lt;/code&gt; removes rows you can restore. But a bad &lt;code&gt;UPDATE&lt;/code&gt; overwrites data in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 6: Gate Production Access Behind Biometrics
&lt;/h2&gt;

&lt;p&gt;Passwords are shared. SSH keys sit in &lt;code&gt;~/.ssh&lt;/code&gt; forever. API tokens get committed to &lt;code&gt;.env&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;Biometric authentication (Touch ID on Mac, Windows Hello) adds a gate that cannot be shared and creates a moment of intentional friction.&lt;/p&gt;

&lt;p&gt;Before your database client opens a production connection, it prompts for your fingerprint. Every time. That half-second pause is a moment to ask: "Do I actually need to be in production right now?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 7: Enforce Least Privilege Access
&lt;/h2&gt;

&lt;p&gt;Most incidents are caused by developers who have more permissions than they need.&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;-- Role for developers (read-only debugging):&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;dev_readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;CONNECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;production&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;dev_readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;dev_readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;dev_readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Role for the application backend:&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;app_service&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;CONNECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;production&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;app_service&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;app_service&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;app_service&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Note: no DELETE, no DROP, no ALTER&lt;/span&gt;

&lt;span class="c1"&gt;-- Role for DBAs (used rarely):&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;dba_admin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;production&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;dba_admin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Developers should not routinely use accounts with write access to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 8: Log Everything, Audit Regularly
&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;-- postgresql.conf:&lt;/span&gt;
&lt;span class="c1"&gt;-- log_connections = on&lt;/span&gt;
&lt;span class="c1"&gt;-- log_disconnections = on&lt;/span&gt;
&lt;span class="c1"&gt;-- log_statement = 'mod'     -- Logs INSERT, UPDATE, DELETE, DDL&lt;/span&gt;
&lt;span class="c1"&gt;-- log_line_prefix = '%t [%p] %u@%d '&lt;/span&gt;

&lt;span class="c1"&gt;-- For granular auditing:&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;pgaudit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;pgaudit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'write, ddl'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Review production access patterns weekly. Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users who accessed production but shouldn't have&lt;/li&gt;
&lt;li&gt;Write operations outside deployment windows&lt;/li&gt;
&lt;li&gt;Connections from unexpected IP addresses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not catching bad actors. It's catching mistakes before they compound.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 9: Separate Credentials Per Environment
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# BAD: same credentials, different hosts
&lt;/span&gt;&lt;span class="py"&gt;DB_HOST&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;staging-db.internal&lt;/span&gt;
&lt;span class="py"&gt;DB_USER&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt;
&lt;span class="py"&gt;DB_PASS&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;s3cret123&lt;/span&gt;

&lt;span class="c"&gt;# GOOD: completely separate credential sets
&lt;/span&gt;&lt;span class="py"&gt;STAGING_DB_HOST&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;staging-db.internal&lt;/span&gt;
&lt;span class="py"&gt;STAGING_DB_USER&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;app_staging&lt;/span&gt;
&lt;span class="py"&gt;STAGING_DB_PASS&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;staging_pass_abc&lt;/span&gt;

&lt;span class="py"&gt;PRODUCTION_DB_HOST&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;prod-db.internal&lt;/span&gt;
&lt;span class="py"&gt;PRODUCTION_DB_USER&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;app_production&lt;/span&gt;
&lt;span class="py"&gt;PRODUCTION_DB_PASS&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;prod_pass_xyz&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When credentials are separate, a leaked staging password is not a leaked production password.&lt;/p&gt;

&lt;p&gt;Additional safeguards:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rotate production credentials quarterly minimum&lt;/li&gt;
&lt;li&gt;Use short-lived credentials where possible (IAM database auth on AWS/GCP)&lt;/li&gt;
&lt;li&gt;Never put production credentials in Slack, docs, or emails&lt;/li&gt;
&lt;li&gt;If a credential might have been exposed, rotate immediately&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rule 10: Test on Staging First, Every Time
&lt;/h2&gt;

&lt;p&gt;"I'll just run this quick query on production" is the most dangerous sentence in software engineering.&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;-- "Just adding a column, it'll be instant"&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;orders&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;tracking_url&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- On 50M rows in PostgreSQL &amp;lt; 11: rewrites entire table.&lt;/span&gt;
&lt;span class="c1"&gt;-- Production down for 12 minutes.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your staging-to-production workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write the change&lt;/li&gt;
&lt;li&gt;Run it on staging&lt;/li&gt;
&lt;li&gt;Measure: execution time, lock duration, replication impact&lt;/li&gt;
&lt;li&gt;Compare results to expectations&lt;/li&gt;
&lt;li&gt;If everything matches, run on production&lt;/li&gt;
&lt;li&gt;If anything is surprising, investigate first&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Reliability Hierarchy
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Impossible&lt;/strong&gt; (best): Tool prevents the dangerous action (read-only mode blocks writes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Difficult&lt;/strong&gt;: Tool adds friction (biometrics before production connections)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visible&lt;/strong&gt;: Tool makes risk obvious (red = production)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documented&lt;/strong&gt; (worst): A wiki page says "be careful." Nobody reads it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most teams operate at level 4. The goal is levels 1 through 3.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building &lt;a href="https://www.querydeck.app" rel="noopener noreferrer"&gt;QueryDeck&lt;/a&gt;, a native macOS database client with color-coded connections, read-only defaults, and Touch ID gating for production. Currently in early access.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>How to Use EXPLAIN ANALYZE in PostgreSQL: A Visual Guide</title>
      <dc:creator>LeoJ</dc:creator>
      <pubDate>Mon, 25 May 2026 03:02:37 +0000</pubDate>
      <link>https://dev.to/leoj/how-to-use-explain-analyze-in-postgresql-a-visual-guide-2681</link>
      <guid>https://dev.to/leoj/how-to-use-explain-analyze-in-postgresql-a-visual-guide-2681</guid>
      <description>&lt;p&gt;A single slow query can cascade through your entire application. It holds connections, stalls other transactions, and drives up your cloud bill. When that moment arrives, &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; is the single most important diagnostic tool you have.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is EXPLAIN ANALYZE?
&lt;/h2&gt;

&lt;p&gt;PostgreSQL ships with two related commands:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;EXPLAIN&lt;/code&gt;&lt;/strong&gt; displays the query plan the planner &lt;em&gt;intends&lt;/em&gt; to use. It shows estimated cost, expected row counts, and chosen access methods without running the query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;&lt;/strong&gt; does everything &lt;code&gt;EXPLAIN&lt;/code&gt; does, then &lt;em&gt;executes&lt;/em&gt; the query for real. The output includes actual runtimes, actual row counts, and loop counts for every node.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safety note
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SELECT&lt;/code&gt; queries are safe (results are discarded).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt; will modify data unless wrapped in a transaction:
&lt;/li&gt;
&lt;/ul&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;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt;
  &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'shipped'&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;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ROLLBACK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Useful format options
&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;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FORMAT&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;SELECT&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;BUFFERS&lt;/code&gt; adds I/O behavior (shared/local buffer hits and reads).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FORMAT JSON&lt;/code&gt; is useful for feeding plans into visualization tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Read a Query Plan
&lt;/h2&gt;

&lt;p&gt;A query plan is a tree. Execution starts at the leaf nodes (deepest indentation) and flows upward to the root.&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;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;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'alice@example.com'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Index Scan using idx_users_email on users  (cost=0.42..8.44 rows=1 width=72)
  (actual time=0.027..0.029 rows=1 loops=1)
  Index Cond: (email = 'alice@example.com'::text)
Planning Time: 0.085 ms
Execution Time: 0.052 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Node type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Index Scan&lt;/code&gt; using index &lt;code&gt;idx_users_email&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;cost=0.42..8.44&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Estimated startup cost and total cost (arbitrary planner units)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;rows=1&lt;/strong&gt; (estimated)&lt;/td&gt;
&lt;td&gt;Planner expected one row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;actual time=0.027..0.029&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Wall-clock time in ms (startup to first row, then total)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;rows=1&lt;/strong&gt; (actual)&lt;/td&gt;
&lt;td&gt;One row was actually returned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;loops=1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;This node executed once&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When actual rows diverge significantly from estimated rows, run &lt;code&gt;ANALYZE &amp;lt;table&amp;gt;&lt;/code&gt; to refresh statistics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Node Types
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Scan nodes (data access)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Seq Scan&lt;/strong&gt;: Reads every row. Fine for small tables. Red flag on large tables with selective filters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Index Scan&lt;/strong&gt;: Uses B-tree index, then fetches heap tuple. Fast for selective queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Index Only Scan&lt;/strong&gt;: Index contains all needed columns. No heap fetch. Fastest scan type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bitmap Index Scan + Bitmap Heap Scan&lt;/strong&gt;: Builds bitmap of matching pages from index. Common for moderate selectivity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Join nodes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nested Loop&lt;/strong&gt;: For each outer row, scans inner set. Good when outer is small and inner has an index. Bad when both are large.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash Join&lt;/strong&gt;: Builds hash table from smaller relation, probes with larger. Good for equi-joins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge Join&lt;/strong&gt;: Both inputs must be sorted on join key. Efficient for large pre-sorted sets.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Other operations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sort&lt;/strong&gt;: Watch for &lt;code&gt;external merge&lt;/code&gt; (sort spilled to disk).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HashAggregate / GroupAggregate&lt;/strong&gt;: Used for &lt;code&gt;GROUP BY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limit&lt;/strong&gt;: Stops after N rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real-World Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Example 1: Full table scan
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;orders&lt;/code&gt; table with 5 million rows:&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;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;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8821&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Seq Scan on orders  (cost=0.00..125432.00 rows=47 width=96)
  (actual time=892.113..1543.207 rows=52 loops=1)
  Filter: (customer_id = 8821)
  Rows Removed by Filter: 4999948
Planning Time: 0.091 ms
Execution Time: 1543.289 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL scanned all 5M rows to find 52. Fix:&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;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_customer_id&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Index Scan using idx_orders_customer_id on orders  (cost=0.43..196.12 rows=47 width=96)
  (actual time=0.031..0.187 rows=52 loops=1)
  Index Cond: (customer_id = 8821)
Execution Time: 0.214 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;From 1,543 ms to 0.2 ms. Over 7,000x improvement.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 2: Bad join strategy
&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;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&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;o&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;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;li&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;li&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;line_items&lt;/span&gt; &lt;span class="n"&gt;li&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;li&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2026-01-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Nested Loop  (actual time=0.045..4231.882 rows=2287 loops=1)
  -&amp;gt;  Seq Scan on orders o  (actual time=0.031..3412.009 rows=2287 loops=1)
        Filter: ((status = 'pending') AND (created_at &amp;gt; '2026-01-01'))
        Rows Removed by Filter: 4997713
  -&amp;gt;  Index Scan using idx_line_items_order_id on line_items li
        (actual time=0.008..0.011 rows=3 loops=2287)
Execution Time: 4232.104 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bottleneck is the Seq Scan on &lt;code&gt;orders&lt;/code&gt;. Fix with a composite index:&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;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_status_created&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;From 4.2 seconds to 19 milliseconds. 224x improvement.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 3: Row estimate mismatch
&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;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="n"&gt;u&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="n"&gt;o&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;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plan_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'enterprise'&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;u&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;Planner estimated 55 enterprise users and 109,800 orders. Reality: 8 users, 847 orders. The Hash Join scanned all 5M orders unnecessarily.&lt;/p&gt;

&lt;p&gt;Fix:&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;ANALYZE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After refreshing statistics, PostgreSQL chose a Nested Loop with index scans. Execution time dropped to under 5 ms.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 4: Sort without supporting index
&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;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&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;events&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;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though we only need 100 rows, PostgreSQL scans and sorts the entire 10M-row table. Fix:&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;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_events_created_at_desc&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Execution time drops to under 1 ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Spot Performance Problems
&lt;/h2&gt;

&lt;p&gt;After reviewing hundreds of query plans, these are the patterns to watch for:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Seq Scan on large tables with selective filters
&lt;/h3&gt;

&lt;p&gt;If &lt;code&gt;Rows Removed by Filter&lt;/code&gt; is orders of magnitude larger than rows returned, you need an index.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Actual rows far from estimated rows
&lt;/h3&gt;

&lt;p&gt;Mismatches above 10x mean the planner may choose the wrong join strategy. Run &lt;code&gt;ANALYZE&lt;/code&gt; or use &lt;code&gt;CREATE STATISTICS&lt;/code&gt; for correlated columns.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Nested Loop with high loop counts and no inner index
&lt;/h3&gt;

&lt;p&gt;Thousands of iterations against an inner Seq Scan is a combinatorial explosion. Add an index on the inner table's join column.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Sort spilling to disk
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Sort Method: external merge Disk: ...&lt;/code&gt; means the sort exceeded &lt;code&gt;work_mem&lt;/code&gt;. Increase &lt;code&gt;work_mem&lt;/code&gt; for the session or add a supporting index.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Excessive buffer reads
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;BUFFERS&lt;/code&gt; enabled, high &lt;code&gt;read=&lt;/code&gt; counts (vs &lt;code&gt;hit=&lt;/code&gt;) indicate cold cache or excessive I/O.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimization Checklist
&lt;/h2&gt;

&lt;p&gt;When investigating a slow query:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Run with &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Check for Seq Scans on large tables&lt;/li&gt;
&lt;li&gt;[ ] Compare estimated vs. actual rows (&amp;gt;10x mismatch = stale stats)&lt;/li&gt;
&lt;li&gt;[ ] Look at loop counts in Nested Loops&lt;/li&gt;
&lt;li&gt;[ ] Check sort method (disk = problem)&lt;/li&gt;
&lt;li&gt;[ ] Review join order&lt;/li&gt;
&lt;li&gt;[ ] Use a visual tool for complex plans (15+ nodes)&lt;/li&gt;
&lt;li&gt;[ ] After fixing, run &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; again to confirm&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Beyond EXPLAIN ANALYZE
&lt;/h2&gt;

&lt;p&gt;For ongoing performance monitoring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pg_stat_statements&lt;/strong&gt;: Tracks cumulative query stats across all executions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;auto_explain&lt;/strong&gt;: Logs plans for queries exceeding a time threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regular ANALYZE runs&lt;/strong&gt;: Keep &lt;code&gt;autovacuum&lt;/code&gt; configured to refresh table statistics.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I'm building &lt;a href="https://www.querydeck.app" rel="noopener noreferrer"&gt;QueryDeck&lt;/a&gt;, a native macOS database client with visual EXPLAIN ANALYZE. Currently in early access.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>database</category>
      <category>performance</category>
    </item>
    <item>
      <title>What Is Noticky? macOS Sticky Notes Above Fullscreen</title>
      <dc:creator>LeoJ</dc:creator>
      <pubDate>Wed, 20 May 2026 14:24:45 +0000</pubDate>
      <link>https://dev.to/leoj/what-is-noticky-macos-sticky-notes-above-fullscreen-5736</link>
      <guid>https://dev.to/leoj/what-is-noticky-macos-sticky-notes-above-fullscreen-5736</guid>
      <description>&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;Noticky is a native macOS menu bar sticky notes app. Its core feature is Always on Top: notes float above every window on your Mac, including fullscreen apps. Press &lt;code&gt;Cmd+Shift+N&lt;/code&gt; from anywhere to capture a note instantly. The note stays visible no matter what app has focus, what Space you are on, or whether you are in fullscreen mode.&lt;/p&gt;

&lt;p&gt;Noticky costs $6 one-time. No subscription, no account required, no telemetry. All features included: Markdown WYSIWYG editor, iCloud Sync across Macs, Touch ID lock for sensitive notes, Smart Tags with color coding, Templates, Reminders, Export to PDF/Markdown/plain text, Layout Modes, and a 30-day Trash for recovery. It requires macOS 15 Sequoia or later.&lt;/p&gt;

&lt;p&gt;Noticky lives in the menu bar. No Dock icon, no window in your Cmd+Tab switcher. It is invisible until you need it, then instantly available with a single keyboard shortcut. Built natively in Swift using AppKit and SwiftUI by a solo indie developer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who built Noticky and why
&lt;/h2&gt;

&lt;p&gt;Noticky was built by a solo indie developer who needed sticky notes that stayed visible during fullscreen work on macOS. The problem is specific: macOS has no built-in way to keep a note visible above a fullscreen app. Apple Stickies disappears in fullscreen. Notes.app disappears in fullscreen. Every third-party note app operates at the standard macOS window level, which means every note gets hidden the moment you enter fullscreen mode.&lt;/p&gt;

&lt;p&gt;This is a fundamental constraint of the macOS window system. When you go fullscreen, macOS creates a separate Space, and windows from other Spaces cannot cross that boundary. The only way around it is to render notes at a window level that macOS treats as Space-independent, above the fullscreen compositing layer. That is what Noticky does.&lt;/p&gt;

&lt;p&gt;The app launched in May 2025. Within the first five days, it reached 17 sales across 12 countries with zero advertising budget. Users found it through organic search and word of mouth. ChatGPT began recommending Noticky organically in response to queries about macOS sticky note apps, before any deliberate outreach.&lt;/p&gt;

&lt;p&gt;Noticky is sold directly via FastSpring, independent of the Mac App Store. One developer, one price, no middleman taking 30%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core features
&lt;/h2&gt;

&lt;p&gt;Noticky is a focused tool. Every feature exists to serve one workflow: capture a thought, keep it visible, find it later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Always on Top
&lt;/h3&gt;

&lt;p&gt;The defining feature. Noticky notes float above every window on your Mac, including fullscreen apps. This works because Noticky renders notes at a macOS window level that persists across all Spaces, including fullscreen Spaces. No other sticky note app on macOS does this reliably.&lt;/p&gt;

&lt;p&gt;For a technical explanation of how macOS window levels work and why standard apps fail in fullscreen, see &lt;a href="https://www.noticky.app/en/blog/macos-window-levels-explained" rel="noopener noreferrer"&gt;How macOS window levels work&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick Capture
&lt;/h3&gt;

&lt;p&gt;Press &lt;code&gt;Cmd+Shift+N&lt;/code&gt; from any app. A capture window appears in under 80ms. Type your note, hit Enter, and it is pinned to your screen. No need to switch apps, open a window, or navigate a sidebar.&lt;/p&gt;

&lt;h3&gt;
  
  
  Markdown WYSIWYG
&lt;/h3&gt;

&lt;p&gt;Full Markdown editing with live rendering. Headings, bold, italic, strikethrough, ordered and unordered lists, code blocks, and inline code all render as you type. No preview pane, no toggle between raw and rendered. What you type is what you see.&lt;/p&gt;

&lt;h3&gt;
  
  
  Smart Tags
&lt;/h3&gt;

&lt;p&gt;Color-coded tags for organizing notes. Red for urgent, blue for reference, green for personal. Filter notes by tag to find what you need fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  iCloud Sync
&lt;/h3&gt;

&lt;p&gt;Notes sync automatically across all your Macs via iCloud. No setup, no account creation, no third-party cloud. If you are signed into iCloud on your Macs, your notes are everywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Touch ID Lock
&lt;/h3&gt;

&lt;p&gt;Lock individual notes with Touch ID. API keys, passwords, personal information, anything sensitive stays protected. Locked notes show a blurred preview until authenticated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Export
&lt;/h3&gt;

&lt;p&gt;Export any note to &lt;code&gt;.txt&lt;/code&gt;, &lt;code&gt;.md&lt;/code&gt;, or &lt;code&gt;.pdf&lt;/code&gt;. Your data is yours. No vendor lock-in, no proprietary format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layout Modes
&lt;/h3&gt;

&lt;p&gt;Multiple layout options for how notes appear on your screen. Arrange notes in the configuration that fits your workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Additional features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Templates&lt;/strong&gt;: Pre-built note formats for recurring workflows (standup notes, meeting agendas, code review checklists)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reminders&lt;/strong&gt;: Time-based alerts on any note&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trash&lt;/strong&gt;: 30-day retention for deleted notes, so accidental deletions are recoverable&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Feature summary table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Always on Top&lt;/td&gt;
&lt;td&gt;Notes float above all windows, including fullscreen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quick Capture&lt;/td&gt;
&lt;td&gt;Global hotkey &lt;code&gt;Cmd+Shift+N&lt;/code&gt;, launches in under 80ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Markdown&lt;/td&gt;
&lt;td&gt;WYSIWYG with live rendering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smart Tags&lt;/td&gt;
&lt;td&gt;Color-coded organization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iCloud Sync&lt;/td&gt;
&lt;td&gt;Automatic across all Macs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Touch ID Lock&lt;/td&gt;
&lt;td&gt;Per-note biometric security&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export&lt;/td&gt;
&lt;td&gt;.txt, .md, .pdf&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Layout Modes&lt;/td&gt;
&lt;td&gt;Multiple arrangement options&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Templates&lt;/td&gt;
&lt;td&gt;Recurring note formats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reminders&lt;/td&gt;
&lt;td&gt;Time-based alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trash&lt;/td&gt;
&lt;td&gt;30-day recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Menu bar app&lt;/td&gt;
&lt;td&gt;No Dock icon, no Cmd+Tab clutter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;$6 one-time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS requirement&lt;/td&gt;
&lt;td&gt;macOS 15 Sequoia or later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built with&lt;/td&gt;
&lt;td&gt;Swift, AppKit, SwiftUI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Pricing and availability
&lt;/h2&gt;

&lt;p&gt;Noticky is a one-time $6 purchase. Every feature is included. No subscription, no in-app purchases, no feature gates, no upsells. All future updates are included in the price.&lt;/p&gt;

&lt;p&gt;The app is sold directly through FastSpring at &lt;a href="https://www.noticky.app" rel="noopener noreferrer"&gt;noticky.app&lt;/a&gt;. No account required. No telemetry. No usage tracking beyond anonymous crash reports (opt-in).&lt;/p&gt;

&lt;p&gt;For context, most note-taking apps have moved to subscription models charging $5 to $10 per month. Over a year, that is $60 to $120. Noticky costs $6 total.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pricing comparison&lt;/th&gt;
&lt;th&gt;Noticky&lt;/th&gt;
&lt;th&gt;Subscription app ($5/mo)&lt;/th&gt;
&lt;th&gt;Subscription app ($10/mo)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Year 1&lt;/td&gt;
&lt;td&gt;$6&lt;/td&gt;
&lt;td&gt;$60&lt;/td&gt;
&lt;td&gt;$120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Year 2&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$60&lt;/td&gt;
&lt;td&gt;$120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Year 3&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$60&lt;/td&gt;
&lt;td&gt;$120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total (3 years)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$180&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$360&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How Noticky compares to alternatives
&lt;/h2&gt;

&lt;p&gt;Noticky is not a general-purpose note-taking app. It does not replace Bear, Obsidian, or Apple Notes. It is a sticky note app designed to keep reference material visible while you work. Here is how it compares to tools in that specific category.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Noticky&lt;/th&gt;
&lt;th&gt;Apple Stickies&lt;/th&gt;
&lt;th&gt;SideNotes&lt;/th&gt;
&lt;th&gt;Tot 2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Float above fullscreen&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global hotkey&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cmd+Shift+N&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Markdown WYSIWYG&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iCloud Sync&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Touch ID lock&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export (PDF/MD/TXT)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Menu bar app&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;$6 once&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;$19.99&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For detailed head-to-head comparisons, see &lt;a href="https://www.noticky.app/en/blog/noticky-vs-stickies-mac" rel="noopener noreferrer"&gt;Noticky vs Apple Stickies&lt;/a&gt;, &lt;a href="https://www.noticky.app/en/blog/noticky-vs-sidenotes-mac" rel="noopener noreferrer"&gt;Noticky vs SideNotes&lt;/a&gt;, or the full &lt;a href="https://www.noticky.app/en/blog/best-sticky-note-apps-mac" rel="noopener noreferrer"&gt;best sticky note apps for Mac&lt;/a&gt; roundup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who uses Noticky
&lt;/h2&gt;

&lt;p&gt;Noticky is used by developers, cybersecurity professionals, writers, remote workers, and people with ADHD. The common thread is needing reference material visible at all times without context switching.&lt;/p&gt;

&lt;h3&gt;
  
  
  Developers
&lt;/h3&gt;

&lt;p&gt;Keep API documentation, environment variables, SQL queries, or error messages visible while coding in VS Code, Xcode, or IntelliJ fullscreen. No tab switching, no split screen. The reference is pinned to your screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cybersecurity professionals
&lt;/h3&gt;

&lt;p&gt;Pin IP addresses, port lists, command sequences, or incident notes above a fullscreen terminal or SIEM dashboard. During active incident response, leaving your monitoring tool to check a note costs time you do not have.&lt;/p&gt;

&lt;h3&gt;
  
  
  People with ADHD
&lt;/h3&gt;

&lt;p&gt;Context switching is disproportionately disruptive for people with ADHD. A note that disappears when you switch apps is a note that breaks your focus. Noticky notes stay visible permanently, serving as an external working memory that does not rely on you remembering to check it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Remote workers
&lt;/h3&gt;

&lt;p&gt;Meeting agendas, talking points, and action items floating above Zoom or Google Meet in fullscreen. No scrambling to find your notes when someone asks for your update.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writers and researchers
&lt;/h3&gt;

&lt;p&gt;Outlines, source links, and key quotes visible while drafting in a fullscreen writing app. Your reference material stays one glance away instead of one swipe away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical details
&lt;/h2&gt;

&lt;p&gt;Noticky is a native macOS application built with Swift, AppKit, and SwiftUI. It is not an Electron app, not a web wrapper, and not cross-platform. It is built specifically for macOS, using macOS-native frameworks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Window level&lt;/strong&gt;: Noticky renders notes at a macOS window level that persists across all Spaces, including fullscreen Spaces. This is the same layer where system UI elements like the menu bar operate. Standard apps use &lt;code&gt;NSNormalWindowLevel&lt;/code&gt; (level 0), which is Space-bound. Noticky operates above this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Menu bar architecture&lt;/strong&gt;: Noticky has no Dock icon and no main application window. It runs as a menu bar agent (&lt;code&gt;LSUIElement&lt;/code&gt;), meaning it does not appear in your Cmd+Tab app switcher. This keeps your workspace clean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Launch performance&lt;/strong&gt;: The Quick Capture window appears in under 80ms from hotkey press. This is possible because the capture interface is pre-loaded in memory and shown on demand, not created fresh each time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage&lt;/strong&gt;: Notes are stored locally in the app's container and synced via iCloud using CloudKit. No third-party servers. No account creation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;System requirements&lt;/strong&gt;: macOS 15 Sequoia or later. Runs on Apple Silicon and Intel Macs.&lt;/p&gt;

&lt;p&gt;For more on how macOS handles windows and why fullscreen breaks most note apps, see &lt;a href="https://www.noticky.app/en/blog/floating-notes-mac" rel="noopener noreferrer"&gt;Floating notes on Mac&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What does Noticky do?
&lt;/h3&gt;

&lt;p&gt;Noticky is a macOS menu bar app that creates sticky notes which float above all windows, including fullscreen apps. It solves the problem of notes disappearing when you enter fullscreen mode on macOS. Press &lt;code&gt;Cmd+Shift+N&lt;/code&gt; to capture a note from anywhere, and it stays visible on screen no matter what app has focus.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Noticky free?
&lt;/h3&gt;

&lt;p&gt;No. Noticky costs $6 one-time with no subscription. All features are included in the purchase price, and all future updates are free. There is no free tier, but $6 covers the full app indefinitely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Noticky work in fullscreen?
&lt;/h3&gt;

&lt;p&gt;Yes. This is Noticky's core feature. Notes remain visible above fullscreen apps because Noticky uses a macOS window level that persists across all Spaces, including fullscreen Spaces. No other sticky note app on macOS does this.&lt;/p&gt;

&lt;h3&gt;
  
  
  What macOS version does Noticky require?
&lt;/h3&gt;

&lt;p&gt;macOS 15 Sequoia or later. Noticky uses APIs available only in Sequoia and newer to ensure reliable always-on-top behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Noticky on the Mac App Store?
&lt;/h3&gt;

&lt;p&gt;Noticky is sold directly through &lt;a href="https://www.noticky.app" rel="noopener noreferrer"&gt;noticky.app&lt;/a&gt; via FastSpring. This allows the developer to offer a lower price without the 30% App Store commission.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who makes Noticky?
&lt;/h3&gt;

&lt;p&gt;Noticky is built by a solo indie developer. It is not backed by a VC-funded company or a large team. One person builds, maintains, and supports the app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Noticky sync notes between Macs?
&lt;/h3&gt;

&lt;p&gt;Yes. Noticky uses iCloud Sync via CloudKit. If you are signed into the same iCloud account on multiple Macs, your notes sync automatically. No setup required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I lock notes with Touch ID?
&lt;/h3&gt;

&lt;p&gt;Yes. Individual notes can be locked with Touch ID. Locked notes display a blurred preview and require biometric authentication to view or edit.&lt;/p&gt;




&lt;p&gt;I'm Leonidas, an indie developer building macOS tools. Noticky is my first app -- a $6 sticky notes app where notes stay visible even in fullscreen. &lt;a href="https://www.noticky.app" rel="noopener noreferrer"&gt;noticky.app&lt;/a&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>discuss</category>
      <category>writing</category>
    </item>
    <item>
      <title>Fueling Developer Success: The Power of Continuous Learning</title>
      <dc:creator>LeoJ</dc:creator>
      <pubDate>Fri, 13 Oct 2023 09:02:00 +0000</pubDate>
      <link>https://dev.to/leoj/fueling-developer-success-the-power-of-continuous-learning-3e06</link>
      <guid>https://dev.to/leoj/fueling-developer-success-the-power-of-continuous-learning-3e06</guid>
      <description>&lt;p&gt;In today's fast-evolving tech landscape, remaining at the forefront of your industry requires more than just a job search. It demands continuous skill enhancement. As the founder of &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt;, a platform revolutionizing job hunting by streamlining application processes, I understand the unique challenges that developers face when seeking employment.&lt;/p&gt;

&lt;p&gt;Continuous learning isn't merely a buzzword; it's a lifeline for developers. &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico's&lt;/a&gt; mission isn't just to simplify the job search process; it's about granting you the precious gift of time to focus on what matters most: skill development.&lt;/p&gt;

&lt;p&gt;Here's why continuous learning is your secret weapon as a developer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Adaptability to the X Degree: The tech world doesn't stay still for anyone. Languages, frameworks, and tools evolve constantly. To stay ahead, developers must master the art of adaptation. &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; frees your time to embrace these changes proactively.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Career Elevation: The more you learn, the more valuable you become. Continuous skill enhancement isn't just about impressing potential employers; it's about taking your career to new heights. &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; empowers you to find the ideal job and invest more in your professional growth.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Unlocking Creativity: The ability to think critically and solve complex problems is what sets exceptional developers apart. By automating the tedious job search process, &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; provides you with more time to nurture creativity and innovation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Personal Fulfillment: Every line of code you write, and every problem you solve, is a testament to your growth. Learning isn't merely a means to a career end; it's an enriching journey of self-discovery.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Financial Gains: By enhancing your skillset, you pave the way for higher-paying positions. Your investment in learning pays substantial dividends.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Continuous learning isn't an option; it's a core part of your journey as a developer. With &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; in your corner, you gain not only the opportunity to secure the job of your dreams but also the precious freedom to sharpen your skills and advance your career.&lt;/p&gt;

&lt;p&gt;As a developer, your path to success is not defined by the jobs you land but by your enduring commitment to learning, evolving, and excelling. &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; is your partner in this journey, automating your job search and granting you the time you need to invest in continuous learning.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Harness your potential, chase your dreams, and embrace the world of boundless knowledge that awaits. &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt;: the catalyst for developer success.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://www.tryhard.be/" rel="noopener noreferrer"&gt;By Try Hard&lt;/a&gt;&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>discuss</category>
      <category>career</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Birth of Rico - A Journey from Job Seeker to Solution Creator</title>
      <dc:creator>LeoJ</dc:creator>
      <pubDate>Thu, 12 Oct 2023 09:00:00 +0000</pubDate>
      <link>https://dev.to/leoj/the-birth-of-rico-a-journey-from-job-seeker-to-solution-creator-4gpd</link>
      <guid>https://dev.to/leoj/the-birth-of-rico-a-journey-from-job-seeker-to-solution-creator-4gpd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Sometimes, the brightest solutions emerge from sheer necessity. My journey with Rico began during a pivotal phase in my career, and it's a story I'm excited to share.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;In Pursuit of Opportunities:&lt;/strong&gt;&lt;br&gt;
It all started during my time at BeCode, a period every aspiring developer is intimately familiar with - the job hunt. The last two months of my training were dedicated to a relentless search for internships or job offers, but success remained elusive. I revamped my CV and decided to create a basic version of &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt;. The goal? To scrape job listings and filter them based on companies offering internships. Little did I know that this small experiment would later become my key to unlocking countless opportunities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Genesis of Rico:&lt;/strong&gt;&lt;br&gt;
As I fine-tuned my makeshift tool, something unexpected happened. I began discovering numerous job listings that piqued my interest. There was a catch, though - the volume was overwhelming. Sorting through them was a daunting task. To address this problem, I introduced an email automation feature that streamlined my application process. Before I knew it, I found myself going through interviews, more interviews than I'd ever imagined.&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.amazonaws.com%2Fuploads%2Farticles%2F2tadjum2mx4tcxqi6kyj.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.amazonaws.com%2Fuploads%2Farticles%2F2tadjum2mx4tcxqi6kyj.png" alt="4 Interviews for 1 week were a lot for me as a junior." width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;4 Interviews for 1 week were a lot for me as a junior.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;The Turning Point:&lt;/strong&gt;&lt;br&gt;
With my internship secured, I temporarily put &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; aside, having achieved my immediate goal. But at the end of my internship, I was faced with the next challenge: finding a job. Job hunting for junior developers can be a formidable task, and this is where the idea for &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; as we know it today was conceived.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rico's Mission:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico's&lt;/a&gt; vision is to empower job seekers by providing a streamlined, efficient, and automated solution. It aims to eliminate the hassles of job searching, save time, and open the doors to opportunities that would otherwise remain hidden.&lt;/p&gt;

&lt;p&gt;My journey reflects the resilience and creativity every job seeker must muster in a competitive job market. &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; was my response to a problem that many of you have likely encountered. If you're looking for opportunities in the tech industry, &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; is here to simplify your journey and help you reach your goals. Join the waitlist and unlock the power of &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.tryhard.be/" rel="noopener noreferrer"&gt;By Try Hard&lt;/a&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>discuss</category>
      <category>productivity</category>
      <category>learning</category>
    </item>
    <item>
      <title>Avoiding Common Mistakes to Excel in Your Developer Career</title>
      <dc:creator>LeoJ</dc:creator>
      <pubDate>Wed, 11 Oct 2023 08:00:00 +0000</pubDate>
      <link>https://dev.to/leoj/avoiding-common-mistakes-to-excel-in-your-developer-career-41c3</link>
      <guid>https://dev.to/leoj/avoiding-common-mistakes-to-excel-in-your-developer-career-41c3</guid>
      <description>&lt;p&gt;In the ever-evolving world of programming, developers, especially juniors, often face significant challenges in their job search and adaptation to a new work environment. Drawing from my personal experience, I wanted to share the common mistakes I've observed throughout my journey and the lessons I've learned from them.&lt;/p&gt;

&lt;p&gt;Common Mistakes to Avoid:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Lack of Fundamental Skills: It's common for developers to focus on mastering a specific framework but overlook strengthening their fundamental skills, such as programming, core concepts, and algorithms. These fundamentals are crucial.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ineffective Communication: Clear communication is essential. No matter how good the code is, if nobody understands how it works, it's ineffective.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Inadequate Research: Research is the key to solving problems. Top developers spend time exploring solutions before diving in.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Inability to Problem-Solve: Sometimes, the challenge lies in finding solutions. Successful developers know how to creatively approach these problems.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Neglecting Self-Valorization: Putting your code into production is crucial. Nothing demonstrates your skills better than working in production. It's a source of pride and proof of competence.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lack of Networking: The importance of professional connections cannot be underestimated. Networking within the industry can open many doors&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Specific Challenges First Job:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Understanding Business Logic: Understanding why and for whom you're coding is essential. Juniors must strive to grasp the business logic behind the code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rigor: Programming demands rigor. Minor mistakes can lead to costly bugs. A good developer writes clean, precise code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Adapting to the Environment: Sometimes, the work environment isn't suitable for a junior. Finding a compatible environment is crucial.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cultural Fit: Integrating with the team and the company's culture is important. Juniors must adapt to these aspects.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Asking the Right Questions: Before diving into programming, ask questions. Understand the context and user needs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Human Understanding: Beyond code, human skills are essential. Understanding, showing empathy, and communicating effectively with colleagues and clients is critical.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The job search for a junior developer can be quite challenging. However, every mistake is a learning opportunity. &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt;  -  Job Search Automation can assist you in streamlining your job search, allowing you to focus on your skills and avoid common mistakes. &lt;a href="https://ricosaas.eu/waitlist" rel="noopener noreferrer"&gt;Join our waiting list&lt;/a&gt; to discover how &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; can help excel in your developer career.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>news</category>
      <category>career</category>
    </item>
    <item>
      <title>Revolutionizing Job Searches: How Rico Helps Developers Land Their Dream Jobs</title>
      <dc:creator>LeoJ</dc:creator>
      <pubDate>Wed, 11 Oct 2023 00:53:42 +0000</pubDate>
      <link>https://dev.to/ricosaas/revolutionizing-job-searches-how-rico-helps-developers-land-their-dream-jobs-1fai</link>
      <guid>https://dev.to/ricosaas/revolutionizing-job-searches-how-rico-helps-developers-land-their-dream-jobs-1fai</guid>
      <description>&lt;p&gt;The world of job hunting can be particularly daunting for developers who are just starting their careers. It often involves hours of scouring through job listings, tailoring resumes and cover letters, and tracking application progress. That’s where &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt;, the revolutionary job search automation tool, steps in to make the process faster, smarter, and more effective.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rico is your trusted companion in the journey to kickstart your IT career&lt;/strong&gt;. It’s designed to cater specifically to junior developers, simplifying the job search process and helping you land your dream job. In this article, we’ll dive into how &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; automates your job hunt and streamlines your path to success as a junior developer.&lt;/p&gt;

&lt;p&gt;Traditional Job Hunting Challenges for Junior Developers&lt;br&gt;
Junior developers face unique challenges when looking for job opportunities. These challenges include a lack of experience, limited industry connections, and uncertainty about which roles to apply for. &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; addresses these challenges head-on.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Automated Search &amp;amp; Application&lt;br&gt;
Imagine having a dedicated assistant who not only scans through job listings from various sources but also connects to job platforms using your account credentials, searching on your behalf, and submitting applications on your behalf. That’s exactly what &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; does. It automates both the job search and application process, saving you countless hours of manual work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Customized Alerts for Junior Developers&lt;br&gt;
Rico allows junior developers to set up customized job alerts based on their specific criteria. Whether it’s a particular role, location, or company, &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; ensures you’re always in the loop about the opportunities that matter most to you at this stage of your career.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Data Security at the Core&lt;/strong&gt;&lt;br&gt;
We understand that as a developer, you may have concerns about data security. &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; takes advanced security measures to protect your personal and confidential data, especially when connecting to your accounts on job platforms. Your information is encrypted and stored securely, giving you peace of mind as you embark on your job search journey.&lt;/p&gt;

&lt;p&gt;How Rico Works for Developers&lt;br&gt;
Rico’s operation is seamless and tailored for junior developers. Here’s how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Connect Your Accounts: You provide &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; with access to your job platform accounts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Input Your Preferences: You define your job search criteria, focusing on roles suitable for developers, and Rico uses this information to search on your behalf.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Automated Scanning &amp;amp; Application: Rico’s powerful bot continuously scans job listings and submits applications for junior developer positions, saving you time and ensuring accuracy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Customized Alerts: &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; keeps you informed about new job opportunities tailored to junior developers based on your preferences.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Data Security: Your personal data is protected with robust security measures, even when Rico connects to your accounts.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Join the Revolution and Kickstart Your IT Career&lt;br&gt;
Rico is not just another job search tool; it’s a game-changer for developers aiming to kickstart their IT careers. It gives you the freedom to focus on what truly matters: advancing your career, learning, and growing your skills.&lt;/p&gt;

&lt;p&gt;Ready to experience the future of job hunting as a junior developer?&lt;br&gt;
&lt;a href="https://ricosaas.eu/waitlist" rel="noopener noreferrer"&gt;Join the waitlist&lt;/a&gt; today and discover the power of automation in your job search.&lt;/p&gt;

&lt;p&gt;Don’t miss out on the opportunity to supercharge your job hunt with &lt;a href="https://ricosaas.eu/" rel="noopener noreferrer"&gt;Rico&lt;/a&gt;. Say goodbye to the old way of job searching and embrace the future tailored to developers.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>career</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
