<?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>Forem: Makroumi</title>
    <description>The latest articles on Forem by Makroumi (@makroumi).</description>
    <link>https://forem.com/makroumi</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%2F3815394%2F1b71acfe-393f-4d81-92a7-f575ee8b272c.png</url>
      <title>Forem: Makroumi</title>
      <link>https://forem.com/makroumi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/makroumi"/>
    <language>en</language>
    <item>
      <title>Real-Time SQL Analysis in VS Code: Catch Dangerous Queries Before You Save the File</title>
      <dc:creator>Makroumi</dc:creator>
      <pubDate>Mon, 16 Mar 2026 10:27:47 +0000</pubDate>
      <link>https://forem.com/makroumi/real-time-sql-analysis-in-vs-code-catch-dangerous-queries-before-you-save-the-file-2ami</link>
      <guid>https://forem.com/makroumi/real-time-sql-analysis-in-vs-code-catch-dangerous-queries-before-you-save-the-file-2ami</guid>
      <description>&lt;p&gt;Most SQL bugs are caught in one of three places. Code review, &lt;br&gt;
where a tired engineer might miss them. Staging, where the &lt;br&gt;
dataset is too small to reveal the problem. Production, at 2am, &lt;br&gt;
where they cause the most damage.&lt;/p&gt;

&lt;p&gt;The SlowQL VS Code extension adds a fourth option: your editor, &lt;br&gt;
before you even save the file.&lt;/p&gt;


&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;SlowQL for VS Code runs 272 static analysis rules against your &lt;br&gt;
SQL as you type. Diagnostics appear in the Problems panel &lt;br&gt;
automatically. Open a .sql file and it works. No database &lt;br&gt;
connection, no configuration, no pipeline to set up first.&lt;/p&gt;

&lt;p&gt;The kind of bugs it catches are the ones that look completely &lt;br&gt;
fine until they aren't. A DELETE with no WHERE clause that &lt;br&gt;
wipes your entire customers table. A LIKE pattern starting &lt;br&gt;
with a wildcard that silently bypasses your index on every &lt;br&gt;
single query. A GRANT ALL PRIVILEGES that gives your app user &lt;br&gt;
god mode access to your entire database. A cartesian join &lt;br&gt;
that returns every row times every row because someone forgot &lt;br&gt;
the JOIN condition.&lt;/p&gt;

&lt;p&gt;All of it flagged in real time. Before you save. Before you &lt;br&gt;
commit. Before anyone reviews it.&lt;/p&gt;


&lt;h2&gt;
  
  
  What gets flagged
&lt;/h2&gt;

&lt;p&gt;272 rules across six dimensions:&lt;/p&gt;

&lt;p&gt;Security catches SQL injection vectors, hardcoded credentials &lt;br&gt;
in migration files, privilege escalation and dynamic SQL &lt;br&gt;
construction that opens injection surfaces.&lt;/p&gt;

&lt;p&gt;Performance catches full table scans, leading wildcards that &lt;br&gt;
bypass indexes, functions on indexed columns, N+1 patterns &lt;br&gt;
and unbounded queries with no LIMIT.&lt;/p&gt;

&lt;p&gt;Reliability catches DELETE and UPDATE without WHERE clauses, &lt;br&gt;
missing transactions around multi-statement operations and &lt;br&gt;
data loss risks.&lt;/p&gt;

&lt;p&gt;Quality catches NULL comparison errors, implicit joins, &lt;br&gt;
deprecated syntax and naming violations.&lt;/p&gt;

&lt;p&gt;Cost catches cloud warehouse antipatterns. SELECT * on wide &lt;br&gt;
tables in BigQuery and Athena, unbounded scans in scheduled &lt;br&gt;
jobs, repeated subqueries that could be materialized.&lt;/p&gt;

&lt;p&gt;Compliance catches GDPR violations, HIPAA risks, PCI-DSS &lt;br&gt;
and SOX compliance issues at the query level.&lt;/p&gt;


&lt;h2&gt;
  
  
  14 dialects, dialect-specific rules
&lt;/h2&gt;

&lt;p&gt;107 of the 272 rules are dialect-aware. Set your dialect &lt;br&gt;
once in VS Code settings and SlowQL fires only the rules &lt;br&gt;
relevant to your database engine. No false positives from &lt;br&gt;
rules that don't apply to your stack.&lt;/p&gt;

&lt;p&gt;PostgreSQL, MySQL, SQL Server, Oracle, SQLite, Snowflake, &lt;br&gt;
BigQuery, Redshift, ClickHouse, DuckDB, Presto, Trino, &lt;br&gt;
Spark, Databricks.&lt;/p&gt;


&lt;h2&gt;
  
  
  Schema-aware validation
&lt;/h2&gt;

&lt;p&gt;Point SlowQL at your DDL files and it validates your queries &lt;br&gt;
against your actual schema. Catches references to tables and &lt;br&gt;
columns that don't exist. Suggests missing indexes on &lt;br&gt;
filtered columns.&lt;/p&gt;

&lt;p&gt;This is where static analysis stops being generic and starts &lt;br&gt;
understanding your specific database. A SELECT on a column &lt;br&gt;
that doesn't exist in your schema gets flagged before the &lt;br&gt;
query ever runs.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why offline matters
&lt;/h2&gt;

&lt;p&gt;Everything runs on your machine. Your SQL never leaves your &lt;br&gt;
editor. No API calls, no telemetry on your query content, &lt;br&gt;
no external service processing your database schema.&lt;/p&gt;

&lt;p&gt;This matters more than people realise. Engineers at banks, &lt;br&gt;
healthcare companies and fintech teams often cannot pipe &lt;br&gt;
their SQL to an external service for analysis. The offline &lt;br&gt;
constraint is not a limitation. It is a deliberate design &lt;br&gt;
decision that makes SlowQL usable in environments where &lt;br&gt;
cloud-based tools are not an option.&lt;/p&gt;


&lt;h2&gt;
  
  
  Three ways to run it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;VS Code extension&lt;/strong&gt; for real-time diagnostics as you write. &lt;br&gt;
Install from the marketplace, open a .sql file, done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI&lt;/strong&gt; for local analysis and scripting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;slowql
slowql queries.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Docker&lt;/strong&gt; if you don't want to install Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;:/src makroumi/slowql /src/queries.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GitHub Actions&lt;/strong&gt; to block dangerous SQL from merging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;makroumi/slowql-action@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./sql/**/*.sql"&lt;/span&gt;
    &lt;span class="na"&gt;fail-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The feedback loop SQL never had
&lt;/h2&gt;

&lt;p&gt;Application code gets linted in the editor, type-checked &lt;br&gt;
at compile time and reviewed by automated tools before it &lt;br&gt;
merges. SQL gets reviewed by a human who is focused on &lt;br&gt;
business logic, not injection surfaces or index bypass &lt;br&gt;
patterns.&lt;/p&gt;

&lt;p&gt;The SlowQL VS Code extension closes that gap at the &lt;br&gt;
earliest possible point in the development cycle. The &lt;br&gt;
same query that would have paged you at 2am gets a red &lt;br&gt;
squiggly line at 2pm before you even commit it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;SlowQL is open source. VS Code extension, CLI, Docker &lt;br&gt;
and GitHub Action all available at &lt;br&gt;
&lt;a href="https://github.com/makroumi/slowql" rel="noopener noreferrer"&gt;github.com/makroumi/slowql&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>sql</category>
      <category>vscode</category>
      <category>database</category>
      <category>security</category>
    </item>
    <item>
      <title>7 SQL patterns that look fine in review and destroy you in production</title>
      <dc:creator>Makroumi</dc:creator>
      <pubDate>Fri, 13 Mar 2026 11:03:46 +0000</pubDate>
      <link>https://forem.com/makroumi/7-sql-patterns-that-look-fine-in-review-and-destroy-you-in-production-4jp9</link>
      <guid>https://forem.com/makroumi/7-sql-patterns-that-look-fine-in-review-and-destroy-you-in-production-4jp9</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://makroumi.hashnode.dev/7-sql-patterns-that-look-fine-in-review-and-destroy-you-in-production" rel="noopener noreferrer"&gt;makroumi.hashnode.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A teammate pushed a query on a Friday afternoon. No red flags in review.&lt;br&gt;
It had been running fine in staging for weeks.&lt;/p&gt;

&lt;p&gt;By 2am Saturday, the on-call engineer was paged. Response times had gone from 50ms to 8s. We didn't recover until Sunday.&lt;/p&gt;

&lt;p&gt;One query. One pattern that nobody caught. A weekend gone.&lt;/p&gt;

&lt;p&gt;I've catalogued the SQL patterns that cause incidents, not the obscure edge cases, but the ones that pass code review, behave on small datasets, and silently wait for your table to grow past a threshold nobody anticipated.&lt;/p&gt;

&lt;p&gt;These are the seven I see most often, with short diagnostics and fixes you can apply in code review, CI, and monitoring.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The leading wildcard&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;-- This query will never use your index&lt;br&gt;
SELECT * FROM users WHERE email LIKE '%@gmail.com';&lt;/p&gt;

&lt;p&gt;Why it fails: A leading wildcard (%...) prevents index seeks; the database must scan the entire table. Works fine on small data, catastrophic at scale.&lt;/p&gt;

&lt;p&gt;Fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Use trailing wildcards when possible (LIKE 'gmail%').&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use full-text search for substring/suffix matching.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Store and index a reversed string column if you frequently need suffix matches.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Consider trigram or specialized indexes (pg_trgm in Postgres) for substring searches.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Detection: EXPLAIN shows sequential scan; cardinality estimate equals table size.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A function wrapped around an indexed column&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;-- Index on created_at is ignored&lt;br&gt;
SELECT * FROM orders WHERE YEAR(created_at) = 2024;&lt;/p&gt;

&lt;p&gt;-- Index-friendly version&lt;br&gt;
SELECT * FROM orders&lt;br&gt;
WHERE created_at &amp;gt;= '2024-01-01' AND created_at &amp;lt; '2025-01-01';&lt;/p&gt;

&lt;p&gt;Why it fails: Applying a function to a column in WHERE disables sargability, the engine can't use the index for seeks.&lt;/p&gt;

&lt;p&gt;Fixes:&lt;/p&gt;

&lt;p&gt;-Push computation to the constant side (range predicates).&lt;/p&gt;

&lt;p&gt;-Create a functional/indexed expression if you must query that function (functional indexes in Postgres, expression indexes in MySQL 8+).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoid repeated function evaluation in hot paths.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Detection: EXPLAIN shows index not used; add an index on the expression if necessary.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SELECT * in production queries&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;-- Fetches 47 columns when you need 3&lt;br&gt;
SELECT * FROM events WHERE user_id = 123 ORDER BY created_at DESC LIMIT 20;&lt;/p&gt;

&lt;p&gt;Why it fails:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Rakes unnecessary bytes across the wire and memory.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Breaks covering/index-only plans.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On columnar warehouses (BigQuery/Redshift/Athena) you pay per byte scanned — SELECT * can become extremely expensive.&lt;/p&gt;

&lt;p&gt;Fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Explicitly list columns you need.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use narrower projections in APIs and services.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For warehouses, test cost on representative data.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Detection: Review changesets for SELECT *; run explain/cost estimation on representative data.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;DELETE or UPDATE without WHERE&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;-- Runs at 3am in a cleanup job&lt;br&gt;
DELETE FROM sessions;&lt;/p&gt;

&lt;p&gt;-- What was intended&lt;br&gt;
DELETE FROM sessions WHERE expires_at &amp;lt; NOW();&lt;/p&gt;

&lt;p&gt;Why it fails: A missing or accidentally removed WHERE clause can wipe an entire table. This often happens during refactors, copy/paste, or when a developer runs a query interactively and forgets to reapply the filter in application code.&lt;/p&gt;

&lt;p&gt;Fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Use explicit transactions and backups for risky maintenance jobs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add safety clauses (e.g., LIMIT with repeated runs) or require an explicit flag to run destructive jobs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Author review checklists and CI/PR checks to catch mass-deletes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run destructive jobs from separate, audited service accounts.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Detection: Linting, CI checks that flag DELETE/UPDATE without WHERE, changelogs and deployment gating.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Missing or inefficient pagination&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;-- OFFSET scans previous rows every time&lt;br&gt;
SELECT * FROM posts ORDER BY created_at DESC LIMIT 20 OFFSET 10000;&lt;/p&gt;

&lt;p&gt;Why it fails: OFFSET causes the database to scan and discard rows up to the offset. At high offsets this becomes O(offset). Cursor-based or keyset pagination keeps response time stable.&lt;/p&gt;

&lt;p&gt;Fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Use keyset pagination (WHERE created_at &amp;lt; last_seen_created_at ORDER BY created_at DESC LIMIT 20).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use indexed columns for pagination predicates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For APIs, return opaque cursors instead of numeric offsets.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Detection: Look for OFFSET in queries; test with high offsets on representative data.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;JOINs without restrictive predicates (fan-out)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;-- Produces massive intermediate results&lt;br&gt;
SELECT u.id, o.id, oi.*&lt;br&gt;
FROM users u&lt;br&gt;
JOIN orders o ON o.user_id = u.id&lt;br&gt;
JOIN order_items oi ON oi.order_id = o.id&lt;br&gt;
WHERE u.region = 'EU';&lt;/p&gt;

&lt;p&gt;Why it fails: Missing or weak join/filter conditions cause explosive intermediate result sets. If one side is large, the join multiplies rows and memory/IO usage spikes.&lt;/p&gt;

&lt;p&gt;Fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Filter early, push predicates into join drivers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ensure join keys are indexed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use EXISTS/IN appropriately to avoid duplication when you only need existence.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Denormalize or pre-aggregate in OLAP workloads.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Detection: EXPLAIN shows large row estimates for join stages; monitoring spikes in temporary disk usage.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Implicit type conversions and mismatched types&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;-- Implicit cast prevents index usage&lt;br&gt;
SELECT * FROM users WHERE id = '123';&lt;/p&gt;

&lt;p&gt;Why it fails: Comparing different types often triggers implicit casting; the database may cast the column, not the constant, disabling index use. It also leads to unexpected behavior.&lt;/p&gt;

&lt;p&gt;Fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Use consistent types for columns and constants.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cast constants, not columns (e.g., WHERE id = 123::bigint).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add strict linting and schema checks in CI.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Detection: EXPLAIN shows index ignored; static analysis/linting can catch literal-type mismatches.&lt;/p&gt;

&lt;p&gt;Why these keep shipping&lt;/p&gt;

&lt;p&gt;None of these are exotic. Every engineer reading this has probably written at least one of them.&lt;/p&gt;

&lt;p&gt;They ship because code review is done by humans under time pressure who are focused on logic and architecture, not query execution plans. They ship because they work perfectly in staging where tables have thousands of rows, not millions. They ship because there's no automated check in the pipeline asking whether a WHERE clause is missing or a wildcard is leading.&lt;/p&gt;

&lt;p&gt;The fix isn't better engineers. It's automated checks that run before anything merges.&lt;/p&gt;

&lt;p&gt;I built SlowQL to do exactly that. It's a SQL static analyzer, 171 rules across performance, security, reliability, compliance, quality and cost. Zero dependencies. Completely offline. Point it at your SQL files and it catches these patterns before they ship.&lt;/p&gt;

&lt;p&gt;pip install slowql&lt;br&gt;
slowql --input-file ./sql&lt;/p&gt;

&lt;p&gt;It won't catch everything. It doesn't execute your queries or read your schema. But it catches the universal patterns - the ones in this article - statically, in CI, before they ever reach a table with 10 million rows.&lt;/p&gt;

&lt;p&gt;If you've been on call at 2am because of a query that should have been caught in review, this is for you.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/makroumi/slowql" rel="noopener noreferrer"&gt;https://github.com/makroumi/slowql&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Conclusion and quick checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Always EXPLAIN queries that will run on production tables.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reject SELECT * in PRs; require explicit projections.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add CI checks (static analysis, linting) to catch simple sargability and safety issues.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Prefer keyset pagination and index-friendly predicates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add functional/expression indexes only when necessary and after benchmarking.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run realistic load tests on representative data sizes.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Canonical URL: &lt;a href="https://makroumi.hashnode.dev/7-sql-patterns-that-look-fine-in-review-and-destroy-you-in-production" rel="noopener noreferrer"&gt;https://makroumi.hashnode.dev/7-sql-patterns-that-look-fine-in-review-and-destroy-you-in-production&lt;/a&gt;&lt;/p&gt;

</description>
      <category>sql</category>
      <category>database</category>
      <category>performance</category>
      <category>backend</category>
    </item>
    <item>
      <title>The Query That Looks Instant in Development and Destroys Production at Scale</title>
      <dc:creator>Makroumi</dc:creator>
      <pubDate>Fri, 13 Mar 2026 10:50:39 +0000</pubDate>
      <link>https://forem.com/makroumi/the-query-that-looks-instant-in-development-and-destroys-production-at-scale-5h6l</link>
      <guid>https://forem.com/makroumi/the-query-that-looks-instant-in-development-and-destroys-production-at-scale-5h6l</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://makroumi.hashnode.dev/the-query-that-looks-instant-in-development-and-destroys-production-at-scale" rel="noopener noreferrer"&gt;makroumi.hashnode.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You push the code on Friday afternoon. Tests pass. Staging looks fine. You deploy and go home.&lt;/p&gt;

&lt;p&gt;At 2am your phone rings.&lt;/p&gt;

&lt;p&gt;Response times are 8 seconds. Users are abandoning. The on-call engineer is staring at a dashboard full of red. And somewhere in the codebase is a query that ran in 4 milliseconds in development and is now bringing down production on a table with 50 million rows.&lt;/p&gt;

&lt;p&gt;This is not a rare story. It happens every week at companies of every size. The query was never dangerous in development because development doesn't look like production. Different data volumes, different indexes, different query plans. The bug was always there. You just couldn't see it yet.&lt;/p&gt;

&lt;p&gt;Why development lies to you&lt;/p&gt;

&lt;p&gt;Your development database has maybe 10,000 rows. Your production database has 50 million. The query planner makes completely different decisions at different scales.&lt;/p&gt;

&lt;p&gt;A full table scan on 10,000 rows takes 2 milliseconds. The same full table scan on 50 million rows takes 45 seconds. The query is identical. The environment is not.&lt;/p&gt;

&lt;p&gt;This is why code review fails to catch these bugs. The reviewer runs the query locally, it's fast, they approve it. Nobody in the review process ever sees production scale data. The bug ships because the feedback loop is broken.&lt;/p&gt;

&lt;p&gt;The patterns that are invisible in development&lt;/p&gt;

&lt;p&gt;The leading wildcard&lt;/p&gt;

&lt;p&gt;SELECT * FROM products WHERE name LIKE '%wireless%'&lt;/p&gt;

&lt;p&gt;On 1,000 products this returns instantly. On 10 million products this performs a full table scan because a leading wildcard cannot use a B-tree index. It has to read every single row.&lt;/p&gt;

&lt;p&gt;In development you have 1,000 products. In production you have 10 million. The query plan is completely different and you will never know until it's too late.&lt;/p&gt;

&lt;p&gt;The function on an indexed column&lt;/p&gt;

&lt;p&gt;SELECT * FROM orders WHERE YEAR(created_at) = 2025&lt;/p&gt;

&lt;p&gt;You have an index on created_at. This query doesn't use it. Wrapping an indexed column in a function forces the database to evaluate the function for every row before it can apply the filter. The index is completely bypassed.&lt;/p&gt;

&lt;p&gt;In development with 500 orders this is instant. In production with 20 million orders this is a full table scan that nobody saw coming.&lt;/p&gt;

&lt;p&gt;The implicit type conversion&lt;/p&gt;

&lt;p&gt;SELECT * FROM users WHERE user_id = '12345'&lt;/p&gt;

&lt;p&gt;user_id is an integer. You're comparing it to a string. The database has to cast every value in the column before comparing. Your index on user_id is ignored entirely.&lt;/p&gt;

&lt;p&gt;This works fine in development. It works fine in production until the table grows large enough that the full scan takes longer than your timeout allows.&lt;/p&gt;

&lt;p&gt;The N+1 hiding in the ORM&lt;/p&gt;

&lt;p&gt;orders = Order.objects.all()&lt;br&gt;
for order in orders:&lt;br&gt;
    print(order.customer.name)&lt;/p&gt;

&lt;p&gt;This looks like two queries. It's actually one query to fetch all orders and then one additional query per order to fetch the customer. With 10 orders in development that's 11 queries. With 10,000 orders in production that's 10,001 queries and your database is on its knees.&lt;/p&gt;

&lt;p&gt;ORMs make N+1 invisible. The code looks clean. The query count is catastrophic.&lt;/p&gt;

&lt;p&gt;SELECT * on a wide table&lt;/p&gt;

&lt;p&gt;SELECT * FROM events WHERE user_id = 123&lt;/p&gt;

&lt;p&gt;Your events table has 47 columns including several TEXT and JSON columns. You need 3 of them. SELECT * fetches all 47 columns for every matching row, transfers all of that data over the network, and loads it all into memory.&lt;/p&gt;

&lt;p&gt;In development your events table has 200 rows. In production it has 200 million rows and this query is reading gigabytes of data you never needed.&lt;/p&gt;

&lt;p&gt;On cloud warehouses like BigQuery or Athena you pay per byte scanned. SELECT * doesn't just slow you down. It charges you for every column you didn't need.&lt;/p&gt;

&lt;p&gt;The moment everything changes&lt;/p&gt;

&lt;p&gt;The reason these patterns are so dangerous is not just that they're slow. It's that they're invisible until they aren't.&lt;/p&gt;

&lt;p&gt;Your query runs fine for months. Then the table crosses a threshold — 1 million rows, 10 million rows, 50 million rows and the query plan changes. What was fast yesterday is catastrophic today. Nothing in the code changed. Only the data.&lt;/p&gt;

&lt;p&gt;The on-call engineer at 2am is not dealing with a new bug. They're dealing with a bug that shipped months ago and finally found the right conditions to detonate.&lt;/p&gt;

&lt;p&gt;The feedback loop is broken by design&lt;/p&gt;

&lt;p&gt;The fundamental problem is that static code review happens at development scale and production incidents happen at production scale. These two environments will never be the same and no amount of careful review can bridge that gap if the reviewer is running queries against a development database.&lt;/p&gt;

&lt;p&gt;The only way to catch these patterns before they ship is to analyze the query itself, not the query's performance on a small dataset. Leading wildcards are always dangerous regardless of table size. Functions on indexed columns always bypass the index regardless of row count. SELECT * is always fetching more than you need.&lt;/p&gt;

&lt;p&gt;These patterns don't need production data to be identified as dangerous. They need static analysis.&lt;/p&gt;

&lt;p&gt;Catching these before they ship&lt;/p&gt;

&lt;p&gt;SlowQL runs against your SQL files before they merge. It doesn't execute queries. It doesn't need a database connection. It reads the query itself and flags the patterns that are universally dangerous regardless of scale.&lt;/p&gt;

&lt;p&gt;Leading wildcards. Functions on indexed columns. SELECT * in production queries. Implicit type conversions. Unbounded queries with no LIMIT. All of them flagged before the code ever reaches review.&lt;/p&gt;

&lt;p&gt;pip install slowql&lt;br&gt;
slowql --input-file sql/ --fail-on high&lt;/p&gt;

&lt;p&gt;Add it to your CI pipeline and the query that would have paged you at 2am gets caught before it merges on Friday afternoon.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;uses: makroumi/slowql-action@v1
with:
input: sql/
fail-on: high&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;171 rules across performance, security, reliability, compliance and cost. Completely offline. Your SQL never leaves your machine.&lt;/p&gt;

&lt;p&gt;github.com/makroumi/slowql&lt;/p&gt;

&lt;p&gt;The query is already in your codebase&lt;/p&gt;

&lt;p&gt;The pattern that will page you at 2am is probably already there. It shipped in a PR that looked fine in review. It ran fast in staging. It's been in production for months waiting for the table to grow large enough.&lt;/p&gt;

&lt;p&gt;Static analysis won't catch everything. It won't replace EXPLAIN ANALYZE or query plan monitoring. But it catches the patterns that are universally dangerous before they ever reach a database, before they ever reach review, before they ever get the chance to detonate at scale.&lt;/p&gt;

&lt;p&gt;The 2am page is optional. The query that causes it is not.&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://makroumi.hashnode.dev/the-query-that-looks-instant-in-development-and-destroys-production-at-scale" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;makroumi.hashnode.dev&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




</description>
      <category>sql</category>
      <category>database</category>
      <category>performance</category>
      <category>backend</category>
    </item>
    <item>
      <title>The SQL query that ruined my weekend and what I built to prevent it</title>
      <dc:creator>Makroumi</dc:creator>
      <pubDate>Mon, 09 Mar 2026 19:34:07 +0000</pubDate>
      <link>https://forem.com/makroumi/the-sql-query-that-ruined-my-weekend-and-what-i-built-to-prevent-it-52fg</link>
      <guid>https://forem.com/makroumi/the-sql-query-that-ruined-my-weekend-and-what-i-built-to-prevent-it-52fg</guid>
      <description>&lt;p&gt;It was a Friday afternoon when the alerts started coming in.&lt;br&gt;
Response times had jumped from 50ms to 8 seconds across the board. Other queries were timing out. The on-call engineer got paged at 2am. We didn't get back to a clean state until Sunday.&lt;br&gt;
The culprit was a single query. SELECT * against a 10 million row table with no WHERE clause. It had passed code review. It looked fine. It wasn't fine.&lt;br&gt;
That weekend I started writing down every pattern like it I'd seen cause a production incident. Not style issues, not formatting, the queries that actually hurt you.&lt;br&gt;
Here's what the list looked like after two years.&lt;br&gt;
The ones that kill performance&lt;br&gt;
Leading wildcards are probably the most common. LIKE '%keyword%' looks harmless and works fine in development. What it actually does is tell the database it cannot use the index because it doesn't know what the string starts with. Every row gets scanned. On a small table you never notice. On a large one you've just written a query that gets slower every week as the table grows.&lt;br&gt;
Function calls on indexed columns do the same thing silently. WHERE LOWER(email) = '&lt;a href="mailto:john@example.com"&gt;john@example.com&lt;/a&gt;' forces the database to apply the function to every row before it can compare. The index on email is useless. Write it as WHERE email = '&lt;a href="mailto:john@example.com"&gt;john@example.com&lt;/a&gt;' and store emails lowercased at insert time instead.&lt;br&gt;
Implicit type coercions are the sneaky one. If user_id is an integer and you write WHERE user_id = '123' with a string, most databases will cast every value in the column to match your input type. Index bypassed, full scan, no warning anywhere.&lt;br&gt;
The ones that cause data loss&lt;br&gt;
DELETE FROM users is valid SQL. It will execute without complaint. It will delete every row in the table. There is no undo.&lt;br&gt;
Same with UPDATE users SET status = 'inactive' with no WHERE clause. Every row gets updated. These happen in migrations more than anywhere else, someone writes the query, tests it on a filtered subset locally, forgets the WHERE clause, runs it on prod.&lt;br&gt;
The security ones&lt;br&gt;
Dynamic SQL is still showing up in codebases in 2025. EXEC('SELECT * FROM users WHERE id = ' + @userId) is a textbook injection vector. The fix is parameterized queries, always.&lt;br&gt;
Hardcoded credentials in SQL files are more common than you'd think. UPDATE config SET api_key = 'sk_live_abc123' sitting in a migration file, committed to git, forever.&lt;br&gt;
The cost ones&lt;br&gt;
This one is specific to columnar and serverless databases like Athena, BigQuery and Redshift. These engines bill you per byte scanned. SELECT * on a wide table with 50 columns when you needed 3 means you're paying for 50 columns of data on every single query. At scale that adds up fast.&lt;br&gt;
What I built&lt;br&gt;
After two years of collecting these patterns I turned them into a static analyzer called SlowQL. You point it at your SQL files and it flags the issues before anything ships. 171 rules across security, performance, reliability, compliance and cost. Works offline, zero dependencies, plugs into CI as a pre-commit hook or a build step.&lt;br&gt;
The philosophy is the same as any other static analysis tool. You don't wait for a JavaScript runtime error to find a type mismatch, you let the linter catch it at write time. SQL deserves the same treatment.&lt;br&gt;
pip install slowql&lt;br&gt;
github.com/makroumi/slowql&lt;br&gt;
The patterns above are a small sample of what it catches. If you've been burned by something not on this list I'd genuinely like to hear it.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>devops</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
