<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged sql</title>
<link>https://coderlegion.com/tag/sql</link>
<description>Powered by Question2Answer</description>
<item>
<title>How to Format SQL Queries in Python: Best Practices, Gotchas, and Real-World Examples</title>
<link>https://coderlegion.com/33002/how-to-format-sql-queries-in-python-best-practices-gotchas-and-real-world-examples</link>
<description>&lt;p&gt;&lt;em&gt;Stop writing SQL strings that look like a ransom note. Here's how to write queries that are readable, safe, and maintainable.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem-with-good-enough-sql-formatting&quot;&gt;The Problem With &quot;Good Enough&quot; SQL Formatting&lt;/h3&gt;
&lt;p&gt;Most Python developers start here:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;user_id = 5
query = &quot;SELECT * FROM users WHERE id = &quot; + str(user_id)
cursor.execute(query)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It works. Until it doesn't — and when it breaks, it breaks &lt;em&gt;badly&lt;/em&gt;: SQL injection, cryptic errors from mismatched types, and queries that take 45 minutes to debug at 2am. Let's fix that, permanently.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-never-concatenate-user-input-use-parameterized-queries&quot;&gt;1. Never Concatenate User Input — Use Parameterized Queries&lt;/h3&gt;
&lt;p&gt;This is rule #1 and it's non-negotiable.&lt;/p&gt;
&lt;h4 id=&quot;-the-wrong-way-sql-injection-waiting-to-happen-&quot;&gt;❌ The Wrong Way (SQL Injection Waiting to Happen)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;username = request.args.get(&quot;username&quot;)  # Could be: ' OR '1'='1
query = f&quot;SELECT * FROM users WHERE username = '{username}'&quot;
cursor.execute(query)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;username&lt;/code&gt; is &lt;code&gt;' OR '1'='1&lt;/code&gt;, your entire users table just got exposed.&lt;/p&gt;
&lt;h4 id=&quot;-the-right-way-parameterized-queries&quot;&gt;✅ The Right Way: Parameterized Queries&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;username = request.args.get(&quot;username&quot;)

# psycopg2 (PostgreSQL)
cursor.execute(&quot;SELECT * FROM users WHERE username = %s&quot;, (username,))

# sqlite3
cursor.execute(&quot;SELECT * FROM users WHERE username = ?&quot;, (username,))

# SQLAlchemy Core
from sqlalchemy import text
result = conn.execute(text(&quot;SELECT * FROM users WHERE username = :name&quot;), {&quot;name&quot;: username})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The database driver handles escaping. You never touch it. This pattern is immune to SQL injection by design.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; Note the trailing comma in &lt;code&gt;(username,)&lt;/code&gt;. Without it, Python treats the string as an iterable and passes each &lt;em&gt;character&lt;/em&gt; as a separate parameter. This is one of the most common beginner bugs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 💥 Bug: passes ('a', 'l', 'i', 'c', 'e') instead of ('alice',)
cursor.execute(&quot;SELECT * FROM users WHERE username = %s&quot;, (username))

# ✅ Correct: single-element tuple
cursor.execute(&quot;SELECT * FROM users WHERE username = %s&quot;, (username,))
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-multi-line-queries-triple-quotes-consistent-indentation&quot;&gt;2. Multi-Line Queries: Triple Quotes + Consistent Indentation&lt;/h3&gt;
&lt;p&gt;For anything longer than one clause, use triple-quoted strings. They're readable, diffable, and don't require escape characters.&lt;/p&gt;
&lt;h4 id=&quot;-string-concatenation-chaos&quot;&gt;❌ String Concatenation Chaos&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;query = &quot;SELECT u.id, u.name, o.total &quot; + \
        &quot;FROM users u &quot; + \
        &quot;JOIN orders o ON u.id = o.user_id &quot; + \
        &quot;WHERE o.total &amp;gt; 100&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;-triple-quoted-sql&quot;&gt;✅ Triple-Quoted SQL&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;query = &quot;&quot;&quot;
    SELECT
        u.id,
        u.name,
        u.email,
        o.total,
        o.created_at
    FROM users u
    JOIN orders o
        ON u.id = o.user_id
    WHERE
        o.total &amp;gt; 100
        AND u.active = TRUE
    ORDER BY o.created_at DESC
    LIMIT 50
&quot;&quot;&quot;

cursor.execute(query)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Style conventions that matter:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SQL keywords in &lt;code&gt;UPPERCASE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;One clause per line (&lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;FROM&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;ORDER BY&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Indent column lists by 4 spaces&lt;/li&gt;
&lt;li&gt;Put &lt;code&gt;ON&lt;/code&gt; conditions on their own line, indented under &lt;code&gt;JOIN&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-dynamic-queries-building-them-safely&quot;&gt;3. Dynamic Queries: Building Them Safely&lt;/h3&gt;
&lt;p&gt;Real applications need dynamic filters. Here's how to do it without string concatenation.&lt;/p&gt;
&lt;h4 id=&quot;scenario-a-search-endpoint-with-optional-filters&quot;&gt;Scenario: A search endpoint with optional filters&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def get_orders(user_id: int, status: str = None, min_total: float = None):
    base_query = &quot;&quot;&quot;
        SELECT
            o.id,
            o.status,
            o.total,
            o.created_at
        FROM orders o
        WHERE o.user_id = %s
    &quot;&quot;&quot;
    params = [user_id]
    conditions = []

    if status:
        conditions.append(&quot;AND o.status = %s&quot;)
        params.append(status)

    if min_total is not None:
        conditions.append(&quot;AND o.total &amp;gt;= %s&quot;)
        params.append(min_total)

    full_query = base_query + &quot;\n        &quot;.join(conditions)

    cursor.execute(full_query, params)
    return cursor.fetchall()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the &lt;strong&gt;accumulator pattern&lt;/strong&gt;: build a list of clauses and params in parallel, then join them. The number of &lt;code&gt;%s&lt;/code&gt; placeholders always matches &lt;code&gt;len(params)&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; Never dynamically interpolate column names or table names with parameters — the DB driver only parameterizes &lt;em&gt;values&lt;/em&gt;, not identifiers. If you need a dynamic column name, whitelist it explicitly:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;ALLOWED_SORT_COLUMNS = {&quot;created_at&quot;, &quot;total&quot;, &quot;status&quot;}

def get_orders_sorted(sort_by: str):
    if sort_by not in ALLOWED_SORT_COLUMNS:
        raise ValueError(f&quot;Invalid sort column: {sort_by}&quot;)

    # Now safe to interpolate — it came from our whitelist, not user input
    query = f&quot;&quot;&quot;
        SELECT id, status, total, created_at
        FROM orders
        ORDER BY {sort_by} DESC
    &quot;&quot;&quot;
    cursor.execute(query)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-in-clauses-the-tricky-one&quot;&gt;4. IN Clauses: The Tricky One&lt;/h3&gt;
&lt;p&gt;Parameterizing &lt;code&gt;IN (...)&lt;/code&gt; trips up almost everyone the first time.&lt;/p&gt;
&lt;h4 id=&quot;-this-won-t-work&quot;&gt;❌ This Won't Work&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;ids = [1, 2, 3, 4]
cursor.execute(&quot;SELECT * FROM users WHERE id IN (%s)&quot;, (ids,))
# Error: query expects 1 placeholder, got a list
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;-generate-placeholders-dynamically&quot;&gt;✅ Generate Placeholders Dynamically&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;ids = [1, 2, 3, 4]

# sqlite3
placeholders = &quot;, &quot;.join(&quot;?&quot; * len(ids))
query = f&quot;SELECT * FROM users WHERE id IN ({placeholders})&quot;
cursor.execute(query, ids)

# psycopg2 — uses %s for each
placeholders = &quot;, &quot;.join([&quot;%s&quot;] * len(ids))
query = f&quot;SELECT * FROM users WHERE id IN ({placeholders})&quot;
cursor.execute(query, ids)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;-sqlalchemy-makes-this-trivial&quot;&gt;✅ SQLAlchemy makes this trivial&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from sqlalchemy import select, bindparam

stmt = select(users_table).where(users_table.c.id.in_([1, 2, 3, 4]))
result = conn.execute(stmt)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-use-sqlalchemy-core-for-complex-queries&quot;&gt;5. Use SQLAlchemy Core for Complex Queries&lt;/h3&gt;
&lt;p&gt;When your raw SQL starts looking like a maze, SQLAlchemy Core gives you composable, Pythonic query building without the full ORM overhead.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from sqlalchemy import create_engine, select, func, and_, desc
from sqlalchemy import Table, MetaData

engine = create_engine(&quot;postgresql://user:pass@localhost/mydb&quot;)
meta = MetaData()
meta.reflect(bind=engine)

orders = meta.tables[&quot;orders&quot;]
users = meta.tables[&quot;users&quot;]

stmt = (
    select(
        users.c.id,
        users.c.name,
        func.sum(orders.c.total).label(&quot;lifetime_value&quot;),
        func.count(orders.c.id).label(&quot;order_count&quot;),
    )
    .join(orders, users.c.id == orders.c.user_id)
    .where(
        and_(
            orders.c.status == &quot;completed&quot;,
            orders.c.created_at &amp;gt;= &quot;2024-01-01&quot;,
        )
    )
    .group_by(users.c.id, users.c.name)
    .having(func.sum(orders.c.total) &amp;gt; 500)
    .order_by(desc(&quot;lifetime_value&quot;))
    .limit(20)
)

with engine.connect() as conn:
    results = conn.execute(stmt).fetchall()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This compiles to safe, parameterized SQL automatically. You can also print the generated SQL for debugging:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;print(stmt.compile(dialect=engine.dialect, compile_kwargs={&quot;literal_binds&quot;: True}))
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-formatting-for-logging-and-debugging&quot;&gt;6. Formatting for Logging and Debugging&lt;/h3&gt;
&lt;p&gt;When you need to log a query (for debugging, audit trails, etc.), format it cleanly — but &lt;em&gt;never&lt;/em&gt; log with real user data embedded.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import logging
import re

logger = logging.getLogger(__name__)

def log_query(query: str, params: tuple):
    # Normalize whitespace for clean log output
    clean_query = re.sub(r'\s+', ' ', query.strip())
    logger.debug(&quot;Executing SQL: %s | Params: %s&quot;, clean_query, params)

log_query(&quot;&quot;&quot;
    SELECT id, name
    FROM users
    WHERE active = %s
&quot;&quot;&quot;, (True,))

# Output: Executing SQL: SELECT id, name FROM users WHERE active = %s | Params: (True,)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; Don't use &lt;code&gt;cursor.mogrify()&lt;/code&gt; (psycopg2) to log queries with real values in production — it defeats the purpose of parameterization from an audit standpoint, and if logs are exfiltrated, you've exposed real data.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-query-constants-sql-as-module-level-variables&quot;&gt;7. Query Constants: SQL as Module-Level Variables&lt;/h3&gt;
&lt;p&gt;For queries used across your codebase, define them as constants at the top of your module. This makes them findable, testable, and easy to update.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# queries.py

GET_USER_BY_ID = &quot;&quot;&quot;
    SELECT id, name, email, created_at
    FROM users
    WHERE id = %s
      AND active = TRUE
&quot;&quot;&quot;

GET_USER_ORDERS = &quot;&quot;&quot;
    SELECT
        o.id,
        o.status,
        o.total,
        o.created_at
    FROM orders o
    WHERE o.user_id = %s
    ORDER BY o.created_at DESC
&quot;&quot;&quot;

INSERT_ORDER = &quot;&quot;&quot;
    INSERT INTO orders (user_id, total, status, created_at)
    VALUES (%s, %s, %s, NOW())
    RETURNING id
&quot;&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Usage in your service layer
from queries import GET_USER_BY_ID, GET_USER_ORDERS

def get_user_with_orders(user_id: int):
    cursor.execute(GET_USER_BY_ID, (user_id,))
    user = cursor.fetchone()

    cursor.execute(GET_USER_ORDERS, (user_id,))
    orders = cursor.fetchall()

    return user, orders
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes SQL grep-able, version-controllable, and reviewable in PRs.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-gotchas-quick-reference&quot;&gt;8. Gotchas Quick Reference&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Gotcha &lt;/th&gt;&lt;th&gt;What Goes Wrong &lt;/th&gt;&lt;th&gt;Fix&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;(value)&lt;/code&gt; vs &lt;code&gt;(value,)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;String unpacked as characters &lt;/td&gt;&lt;td&gt;Always use trailing comma for single-element tuples&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;f-string with user input &lt;/td&gt;&lt;td&gt;SQL injection &lt;/td&gt;&lt;td&gt;Use parameterized queries&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Dynamic &lt;code&gt;IN (...)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Wrong number of placeholders &lt;/td&gt;&lt;td&gt;Generate placeholders programmatically&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Dynamic column names &lt;/td&gt;&lt;td&gt;Parameters don't work for identifiers &lt;/td&gt;&lt;td&gt;Whitelist allowed column names&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Logging with real values &lt;/td&gt;&lt;td&gt;Data exposure in logs &lt;/td&gt;&lt;td&gt;Log query template + params separately&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;None&lt;/code&gt; in params &lt;/td&gt;&lt;td&gt;NULL comparison breaks &lt;/td&gt;&lt;td&gt;Use &lt;code&gt;IS NULL&lt;/code&gt; in SQL, not &lt;code&gt;= NULL&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# None/NULL gotcha
user_role = None

# ❌ Won't match any rows — SQL NULL != NULL
cursor.execute(&quot;SELECT * FROM users WHERE role = %s&quot;, (user_role,))

# ✅ Use IS NULL when the value might be None
if user_role is None:
    cursor.execute(&quot;SELECT * FROM users WHERE role IS NULL&quot;)
else:
    cursor.execute(&quot;SELECT * FROM users WHERE role = %s&quot;, (user_role,))
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Situation &lt;/th&gt;&lt;th&gt;Recommended Approach&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Simple parameterized query &lt;/td&gt;&lt;td&gt;&lt;code&gt;cursor.execute(sql, (val,))&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Multi-line query &lt;/td&gt;&lt;td&gt;Triple-quoted string with clause-per-line formatting&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Dynamic filters &lt;/td&gt;&lt;td&gt;Accumulator pattern (list of clauses + params)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;IN&lt;/code&gt; clause &lt;/td&gt;&lt;td&gt;Generate placeholders: &lt;code&gt;&quot;, &quot;.join([&quot;%s&quot;] * len(ids))&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Complex joins/aggregates &lt;/td&gt;&lt;td&gt;SQLAlchemy Core&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Reusable queries &lt;/td&gt;&lt;td&gt;Module-level string constants&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Dynamic column names &lt;/td&gt;&lt;td&gt;Whitelist validation + f-string&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The core rule is simple: &lt;strong&gt;SQL structure is yours to format; SQL values are the driver's job to escape.&lt;/strong&gt; Internalize that boundary and most of these best practices follow naturally.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Found a gotcha I missed? Drop it in the comments — SQL has a way of surprising everyone eventually.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33002/how-to-format-sql-queries-in-python-best-practices-gotchas-and-real-world-examples</guid>
<pubDate>Thu, 29 Oct 2026 16:30:33 +0000</pubDate>
</item>
<item>
<title>Advanced SQL Analytics for Data Analysis.</title>
<link>https://coderlegion.com/31618/advanced-sql-analytics-for-data-analysis</link>
<description>&lt;h2 id=&quot;advanced-sql-analytics-for-running-a-hospital-database-&quot;&gt;Advanced SQL analytics for running a hospital database.&lt;/h2&gt;
&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Structured Query Language is essential and common in most industry sectors. In healthcare reporting, SQL plays a critical role in analytics. PostgreSQL lets data scientists take those transaction records and pull out operational insights, so you can make decisions. In this guide we go through the main SQL techniques, using a hospital database schema; patients , doctors, departments , wards, diagnoses, procedures, and visits. &lt;/p&gt;
&lt;p&gt;First, before anything else we need to create our database schema. Connect to PostgerSQL DBMS and run a new sql script to start.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a schema &lt;code&gt;hospital&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;create schema hospital;&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Check to which schema you are connected.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;show search_path;&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;If connected to a different schema, set connection to your &lt;code&gt;hospital&lt;/code&gt; schema.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;set search_path to hospital;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Now you are set to begin creating your tables. Run the following codes to create the different tables and insert data into the newly created tables:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Doctors Table&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;--create doctors table

create table hospital.doctors (
doctor_id int primary key,
doctor_name varchar(50) not null,
doctor_gender varchar(20),
doctor_specialty varchar(20),
doctor_experience_years int
);

select * from hospital.doctors;

-- insert data into doctors

insert into hospital.doctors (doctor_id, doctor_name, doctor_gender, doctor_specialty, doctor_experience_years)
values 
(101, 'DR.Brian Mwangi', 'Male', 'Cardiologist', 12),
(102, 'Dr. Sarah Wanjiku', 'Female', 'General Physician', 9), 
(103, 'Dr. Peter Otieno', 'Male', 'Orthopedic Surgeon', 15),
(104, 'Dr. Faith Chebet', 'Female', 'Pediatrician', 8);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Departments Table&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- create departments table

create table hospital.department (
department_id int primary key,
department_name varchar (50) not null
);


select * from hospital.department;

-- insert data into departments

insert into hospital.department (department_id, department_name)
values 
(10, 'Cardiology'),
(20, 'General Medicine'),
(30, 'Orthopedics'),
(40, 'Pediatrics');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Departments Table&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- create diagnosis table

create table hospital.diagnosis (
diagnosis_id int primary key,
diagnosis_name varchar(50),
diagnosis_category varchar(50)
);

select * from hospital.diagnosis;

--insert data into diagnosis table

insert into hospital.diagnosis (diagnosis_id, diagnosis_name, diagnosis_category)
values 
(501,'Hypertension', 'Chronic Disease'),
(502, 'Malaria', 'Infectious Disease'),
(503, 'Fracture', 'Injury'),
(504, 'Pneumonia', 'Respiratory Disease'),
(505, 'Diabetes', 'Chronic Disease'),
(506, 'Typhoid', 'Infectious'),
(507, 'Asthma', 'Respiratory Disease'),
(508, 'Back Pian', 'Musculoskeletal'),
(509, 'Gastritis', 'Digestive Disease'),
(510, 'Heart Failure', 'Chronic Disease'),
(511, 'Flu', 'Respiratoey Disease'),
(512, 'Kidney Infection', 'Infectious Disease'),
(513, 'Anaemia', 'Blood Disorder'),
(514, 'Sprain', 'Injury'),
(515, 'Gastroenteritis', 'Digestive Disease'),
(516, 'Dislocation', 'Injury'),
(517, 'Migraine', 'Neurological Disorder'),
(518, 'Chest Pain', 'Cardiovascular Disease');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Wards Table&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- create wards table 

create table hospital.wards (
ward_id int primary key, 
ward_name varchar (50)
);

-- insert data into wards
insert into hospital.wards (ward_id, ward_name)
values 
(100, 'Cardiac Care Unit'),
(200, 'General Ward'),
(300, 'Surgical Ward'),
(400, 'Childrens Ward');

select * from hospital.wards;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Patients Table&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- create patients table 
create table hospital.patients (
patient_id serial primary key, 
patient_name varchar(50) not null,
patient_gender varchar(20),
patient_age int,
county varchar(30),
sub_county varchar(30)
);

--insert data into patients
insert into hospital.patients (patient_name, patient_gender, patient_age, county, sub_county)
values 
('Amina Otieno', 'Female', 34, 'Nairobi', 'Westlands'),
('John Kariuki', 'Male', 42,' Kiambu', 'Thika'),
('Grace Naliaka', 'Female', 28, 'Kakamega', 'Lurambi'),
('David Mutua', 'Male', 55, 'Machakos', 'Athi River'),
('Esther Achieng', 'Female', 63, 'Kisumu', 'Kisumu Central'),
('Mohamed Ali', 'Male', 19, 'Mombasa', 'Nyali'),
('Lucy Wambui', 'Female', 7, 'Nakuru', 'Naivasha'),
('Samuel Kiptoo', 'Male', 39, 'Uasin Gishu', 'Eldoret East'),
('Mary Wairumu', 'Female', 31, 'Murang''a', 'Kiharu'),
('Peter Ouma', 'Male', 46, 'Siaya', 'Bondo'),
('Brian Omondi', 'Male', 24, 'Kisumu', 'Kisumu East'),
('Catherine Musyoka', 'Female', 52, 'Kitui', 'Kitui Central'),
('Daniel Maina', 'Male', 16, 'Nyeri', 'Othaya'),
('Eunice Moraa', 'Female', 44, 'Kisii', 'Bonchari'),
('Francis Njuguna', 'Male', 60, 'Laikipia', 'Nyanyuki'),
('Hellen Jepchirchir', 'Female', 37, 'Bomet', 'Sotik'),
('Isaac Kamau', 'Male', 29, 'Nairobi', 'Kasarani'),
('Jane Muthoni', 'Female', 22, 'Meru', 'Imenti North'),
('Kevin Barasa', 'Male', 48, 'Bungoma', 'Webuye'),
('Lilian Adhiambo', 'Female', 26, 'Homa Bay', 'Rangwe'),
('Moses Cheruiyot', 'Male', 33, 'Kericho', 'Ainamoi'),
('Nadia Hussein', 'Female', 5, 'Garissa', 'Dadaab'),
('Patrick Muli', 'Male', 51, 'Makueni', 'Wote'),
('Ruth Nekesa', 'Female', 41, 'Trans Nzoia', 'Kitale'),
('Josephine Wekesa', 'Female', 36, 'Busia', 'Matayos');

select * from hospital.patients;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Procedures Table&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;create table hospital.procedures (
procedure_id int primary key,
procedure_name varchar (50) not null,
procedure_category varchar (20)
);

--insert data into procedures
insert into hospital.procedures (procedure_id, procedure_name, procedure_category)
values
(701, 'Blood Pressure Monitoring', 'Diagnostic'),
(702, 'Malaria Test', 'Laboratory'),
(703, 'X-ray', 'Imaging'),
(704, 'Chest X-ray', 'Imaging'),
(705, 'Blood Sugar Test', 'Laboratory'),
(706, 'Full Blood Count', 'Laboratory'),
(707, 'Nebulization', 'Treatment'),
(708, 'Physiotherapy', 'Treatment'),
(709, 'Endoscopy', 'Diagnostic'),
(710, 'ECG', 'Diagnostic'),
(711, 'Consultation', 'Consultation'),
(712, 'Joint Support Bandage', 'Treatment'),
(713, 'IV Fluids', 'Treatment'),
(714, 'Closed Reduction', 'Treatment'),
(715, 'Pain Management', 'Treatment');


select * from hospital.procedures;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Visits Table&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- create visits table (Fact Table)

create table hospital.visits (
visit_id int primary key,
visit_date date not null,
patient_id int not null references hospital.patients(patient_id),
doctor_id int not null references hospital.doctors(doctor_id),
department_id int not null references hospital.department(department_id),
ward_id int references hospital.wards(ward_id),
diagnosis_id int references hospital.diagnosis(diagnosis_id),
procedure_id int references hospital.procedures(procedure_id),
admission_type varchar(20) not null
    check (admission_type in ('Outpatient', 'Inpatient', 'Emergency')),
length_of_stay_days int default 0
    check (length_of_stay_days &amp;gt;= 0),
triage_level varchar(10)
    check (triage_level in ('Red', 'Orange', 'Yellow', 'Green')),
insurance_provider varchar(50),
total_bill_kes numeric(10,2) not null
    check (total_bill_kes &amp;gt;= 0),
payment_method varchar(20),
follow_up_required varchar(3)
    check (follow_up_required in ('Yes', 'No')),
patient_satisfaction_score numeric(3,1)
    check (patient_satisfaction_score between 0 and 5)
);

--insert data into visits table
insert into hospital.visits (visit_id, visit_date, patient_id, doctor_id, department_id, ward_id, diagnosis_id,
procedure_id, admission_type, length_of_stay_days, triage_level, insurance_provider, total_bill_kes, payment_method,
follow_up_required, patient_satisfaction_score)
values
(1001, '2026-06-01', 1, 101, 10, 100, 501, 701, 'Outpatient', 0, 'Green', 'NHIF', 3500, 'Cash', 'Yes', 4.5),
(1002, '2026-06-01', 2, 102, 20, 200, 502, 702, 'Outpatient', 0, 'Yellow', 'Britam', 2500, 'Insurance', 'No', 4.1),
(1003, '2026-06-02', 3, 103, 30, 300, 503, 703, 'Inpatient', 3, 'Red', 'AAR', 28000, 'Insurance', 'Yes', 4.3),
(1004, '2026-06-02', 4, 104, 40, 400, 504, 704, 'Inpatient', 4, 'Red', 'None', 42000, 'Cash', 'Yes', 3.9),
(1005, '2026-06-03', 5, 101, 10, 100, 505, 705, 'Outpatient', 0, 'Yellow', 'CIC', 4000, 'Mobile Money', 'Yes', 4.7),
(1006, '2026-06-03', 6, 102, 20, 200, 506, 706, 'Outpatient', 0, 'Green', 'None', 3000, 'Cash', 'No', 4.0),
(1007, '2026-06-04', 7, 104, 40, 400, 507, 707, 'Outpatient', 0, 'Yellow', 'NHIF', 5500, 'Insurance', 'Yes', 4.6),
(1008, '2026-06-04', 8, 103, 30, 300, 508, 708, 'Outpatient', 0, 'Green', 'None', 6500, 'Cash', 'Yes', 4.2),
(1009, '2026-06-05', 9, 102, 20, 200, 509, 709, 'Outpatient', 0, 'Yellow', 'Jubilee', 15000, 'Insurance', 'No', 3.8),
(1010, '2026-06-05', 10, 101, 10, 100, 510, 710, 'Inpatient', 5, 'Red', 'AAR', 76000, 'Insurance', 'Yes', 4.4),
(1011, '2026-06-06', 1, 101, 10, 100, 505, 705, 'Outpatient', 0, 'Green', 'NHIF', 4200, 'Mobile Money', 'Yes', 4.8),
(1012, '2026-06-06', 11, 102, 20, 200, 502, 702, 'Outpatient', 0, 'Green', 'None', 2300, 'Cash', 'No', 4.0),
(1013, '2026-06-07', 12, 101, 10, 100, 501, 710, 'Outpatient', 0, 'Yellow', 'Britam', 7000, 'Insurance', 'Yes', 4.5),
(1014, '2026-06-07', 13, 104, 40, 400, 511, 711, 'Outpatient', 0, 'Green', 'None', 1800, 'Cash', 'No', 4.1),
(1015, '2026-06-08', 14, 103, 30, 300, 503, 703, 'Inpatient', 2, 'Red', 'CIC', 24000, 'Mobile Money', 'Yes', 4.6),
(1016, '2026-06-08', 15, 102, 20, 200, 512, 706, 'Inpatient', 3, 'Orange', 'Jubilee', 36000, 'Insurance', 'Yes', 3.7),
(1017, '2026-06-09', 16, 101, 10, 100, 510, 710, 'Inpatient', 6, 'Red', 'AAR', 82000, 'Insurance', 'Yes', 4.2),
(1018, '2026-06-09', 17, 102, 20, 200, 509, 711, 'Outpatient', 0, 'Green', 'None', 2000, 'Cash', 'No', 4.0),
(1019, '2026-06-10', 18, 104, 40, 400, 507, 707, 'Outpatient', 0, 'Yellow', 'NHIF', 5200, 'Mobile Money', 'Yes', 4.7),
(1020, '2026-06-10', 19, 103, 30, 300, 508, 708, 'Outpatient', 0, 'Green', 'Britam', 6500, 'Insurance', 'No', 4.1),
(1021, '2026-06-11', 2, 101, 10, 100, 501, 701, 'Outpatient', 0, 'Yellow', 'Britam', 3800, 'Insurance', 'Yes', 4.3),
(1022, '2026-06-11', 20, 102, 20, 200, 513, 706, 'Outpatient', 0, 'Green', 'None', 3200, 'Mobile Money', 'No', 4.4),
(1023, '2026-06-12', 21, 103, 30, 300, 514, 712, 'Outpatient', 0, 'Yellow', 'NHIF', 7200, 'Cash', 'Yes', 4.0),
(1024, '2026-06-12', 22, 104, 40, 400, 515, 713, 'Inpatient', 2, 'Orange', 'None', 18500, 'Cash', 'Yes', 4.5),
(1025, '2026-06-13', 5, 101, 10, 100, 501, 710, 'Outpatient', 0, 'Yellow', 'CIC', 7600, 'Insurance', 'Yes', 4.6),
(1026, '2026-06-13', 23, 102, 20, 200, 506, 706, 'Inpatient', 2, 'Orange', 'Jubilee', 29000, 'Insurance', 'Yes', 3.9),
(1027, '2026-06-14', 24, 103, 30, 300, 516, 714, 'Inpatient', 1, 'Red', 'AAR', 33000, 'Insurance', 'Yes', 4.2),
(1028, '2026-06-14', 7, 104, 40, 400, 511, 711, 'Outpatient', 0, 'Green', 'NHIF', 1900, 'Mobile Money', 'No', 4.8),
(1029, '2026-06-15', 25, 102, 20, 200, 517, 715, 'Outpatient', 0, 'Yellow', 'Britam', 4800, 'Cash', 'Yes', 4.1),
(1030, '2026-06-15', 10, 101, 10, 100, 518, 710, 'Emergency', 1, 'Red', 'AAR', 22000, 'Insurance', 'Yes', 4.4);

select * from hospital.visits;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After creating our database schema, we will begin modelling the data using different advanced SQL functions.&lt;/p&gt;
&lt;h3 id=&quot;1-sql-window-functions&quot;&gt;1. SQL Window Functions&lt;/h3&gt;
&lt;p&gt;Window functions do computations on related rows in a table while maintaining the individuality of each rows.&lt;/p&gt;
&lt;h4 id=&quot;over-partition-by-order-by-and-window-frames&quot;&gt;OVER, PARTITION BY, ORDER BY, and Window Frames&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;OVER&lt;/code&gt; clause describes the rows for the window function operation.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PARTITION BY&lt;/code&gt; defines different partitions for each result set.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ORDER BY&lt;/code&gt; defines row order in each partition.&lt;/li&gt;
&lt;li&gt;Window frames define different limits such as&lt;code&gt;ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW&lt;/code&gt; for cumulative sums and &lt;code&gt;ROWS BETWEEN 2 PRECEDING AND CURRENT ROW&lt;/code&gt; for moving averages.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;ranking-window-functions&quot;&gt;Ranking Window Functions&lt;/h4&gt;
&lt;p&gt;Ranking functions provide numeric ordering according to specified criteria.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ROW_NUMBER&lt;/code&gt; provides a distinct numeric integer value to each row within a partition.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RANK&lt;/code&gt; provides identical rank values to ties, but skips other numbers.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DENSE_RANK&lt;/code&gt; provides identical rank values to ties, but does not skip numbers.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NTILE&lt;/code&gt; divides rows in partitions into buckets of equal size.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How can hospital management rank inpatient visits by total bill within each admission type?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    v.visit_id,
    p.patient_name,
    v.admission_type,
    v.total_bill_kes,
    row_number() over (partition by v.admission_type order by v.total_bill_kes desc) as row_num,
    rank() over (partition by v.admission_type order by v.total_bill_kes desc) as rnk,
    dense_rank() over (partition by v.admission_type order by v.total_bill_kes desc) as dense_rnk,
    ntile(2) over (partition by v.admission_type order by v.total_bill_kes desc) as bill_tier
from hospital.visits v
join hospital.patients p on v.patient_id = p.patient_id
where v.admission_type = 'Inpatient'
limit 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/m1wdvqqfnf9c1tdhy7l2.png&quot; alt=&quot;Visit Report&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ROW_NUMBER&lt;/code&gt; gives values from 1 to 5. &lt;/li&gt;
&lt;li&gt;The values obtained by &lt;code&gt;RANK&lt;/code&gt; and &lt;code&gt;DENSE_RANK&lt;/code&gt; will be same as there are no duplicate bill values. &lt;/li&gt;
&lt;li&gt;NTILE assigns the best bills to Tier 1.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;value-amp-navigation-window-function&quot;&gt;Value &amp;amp; Navigation Window Function&lt;/h4&gt;
&lt;p&gt;Navigation functions retrieve values based on relative rows.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LAG&lt;/code&gt; retrieves values from previous row.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LEAD&lt;/code&gt; retrieves values from next row.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FIRST_VALUE&lt;/code&gt; gives the first value of the expression in the frame.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LAST_VALUE&lt;/code&gt; gives the last value of the expression in the frame bounds like &lt;code&gt;ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How can we monitor patient billing process through multiple hospital visits?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    v.visit_id,
    v.patient_id,
    v.visit_date,
    v.total_bill_kes,
    lag(v.total_bill_kes, 1, 0.00) over (partition by v.patient_id order by v.visit_date) as prev_bill,
    lead(v.total_bill_kes, 1, 0.00) over (partition by v.patient_id order by v.visit_date) as next_bill,
    first_value(v.total_bill_kes) over (partition by v.patient_id order by v.visit_date) as initial_bill,
    last_value(v.total_bill_kes) over (
        partition by v.patient_id 
        order by v.visit_date 
        rows between unbounded preceding and unbounded following
    ) as final_bill
from visits v
where v.patient_id in (1, 2)
order by v.patient_id, v.visit_date;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/3iqglknz6vze0tocm3o6.png&quot; alt=&quot;Patient Billing Process Monitoring&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LAG&lt;/code&gt; retrieves previous visit invoices and &lt;code&gt;LEAD&lt;/code&gt; retrieves future visit invoices.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FIRST_VALUE&lt;/code&gt; and &lt;code&gt;LAST_VALUE&lt;/code&gt; store the first and last visit invoices respectively.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;aggregate-window-functions&quot;&gt;Aggregate Window Functions&lt;/h4&gt;
&lt;p&gt;Aggregate functions &lt;code&gt;SUM&lt;/code&gt;, &lt;code&gt;AVG&lt;/code&gt;, &lt;code&gt;COUNT&lt;/code&gt;, &lt;code&gt;MIN&lt;/code&gt;, and &lt;code&gt;MAX&lt;/code&gt; calculate moving sums and moving averages when combined with &lt;code&gt;OVER&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;In what way do we determine cumulative income, rolling average billings, categories, and billings for departments on each visit?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    v.visit_id,
    v.visit_date,
    v.total_bill_kes,
    sum(v.total_bill_kes) over (order by v.visit_date, v.visit_id) as cumulative_revenue,
    avg(v.total_bill_kes) over (
        order by v.visit_date 
        rows between 2 preceding and current row
    ) as rolling_avg_bill,
    count(*) over (partition by v.admission_type) as total_admission_type_count,
    min(v.total_bill_kes) over (partition by v.admission_type) as min_admission_bill,
    max(v.total_bill_kes) over (partition by v.admission_type) as max_admission_bill
from hospital.visits v
order by v.visit_date, v.visit_id
limit 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/ft1sqrdykuhpi7j0ehnv.png&quot; alt=&quot;Aggregate Window Functions&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SUM&lt;/code&gt; forms a running total. &lt;/li&gt;
&lt;li&gt;&lt;code&gt;AVG&lt;/code&gt; calculates a 3 row moving average. &lt;/li&gt;
&lt;li&gt;&lt;code&gt;COUNT&lt;/code&gt;, &lt;code&gt;MIN&lt;/code&gt;, and &lt;code&gt;MAX&lt;/code&gt; present group statistics without collapsing rows.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2-sql-joins&quot;&gt;2. SQL Joins&lt;/h3&gt;
&lt;p&gt;Joins combine columns from multiple tables using related key columns.&lt;/p&gt;
&lt;h4 id=&quot;inner-join&quot;&gt;INNER JOIN&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;INNER JOIN&lt;/code&gt; returns rows with matching key values in both tables.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How do we generate a visit report combining patient names, attending doctors, and clinical departments?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    v.visit_id,
    p.patient_name,
    d.doctor_name,
    dept.department_name,
    v.total_bill_kes
from hospital.visits v
inner join hospital.patients p on v.patient_id = p.patient_id
inner join hospital.doctors d on v.doctor_id = d.doctor_id
inner join hospital.department dept on v.department_id = dept.department_id
limit 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/cpriilp4a3qc6w9wh6v5.png&quot; alt=&quot;Inner Join&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;INNER JOIN&lt;/code&gt; Matches keys across visits, patients, doctors, and department tables.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;left-join&quot;&gt;LEFT JOIN&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;LEFT JOIN&lt;/code&gt; produces all the rows from the left table and the matched rows from the right table. Fields in the right table without a match produce &lt;code&gt;NULL&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How can we study the number of visits and expenses of patients without losing those that have no visits at all?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    p.patient_id,
    p.patient_name,
    count(v.visit_id) as total_visits,
    coalesce(sum(v.total_bill_kes), 0.00) as total_spent_kes
from hospital.patients p
left join hospital.visits v on p.patient_id = v.patient_id
group by p.patient_id, p.patient_name
order by total_visits desc, p.patient_id
limit 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/8osejvnidi5913phipk8.png&quot; alt=&quot;Left Join&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All patient records are retained using &lt;code&gt;LEFT JOIN&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;Patients having no visits will have &lt;code&gt;COUNT&lt;/code&gt; as 0 while &lt;code&gt;COALESCE&lt;/code&gt; will convert &lt;code&gt;NULL&lt;/code&gt; sums to &lt;code&gt;0.00.&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;right-join&quot;&gt;RIGHT JOIN&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;RIGHT JOIN&lt;/code&gt; retrieves all records from right table and matching records from left table.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How can hospital administration ensure that all physical wards are reflected in admission records?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    w.ward_id,
    w.ward_name,
    v.visit_id,
    v.total_bill_kes
from hospital.visits v
right join hospital.wards w on v.ward_id = w.ward_id
where v.visit_id is null or v.visit_id &amp;lt;= 1003;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/trk5b98a06x3pfebvxub.png&quot; alt=&quot;Right Join&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All wards are kept in &lt;code&gt;RIGHT JOIN&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;The Childrens Ward will be with &lt;code&gt;NULL&lt;/code&gt; visits if no match occurs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;full-outer-join&quot;&gt;FULL OUTER JOIN&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;FULL OUTER JOIN&lt;/code&gt; will display all records if there is a match in either left or right record.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How do we perform auditing of doctor and department records for unassigned doctors or unmapped departments?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    d.doctor_name,
    dept.department_name
from hospital.doctors d
full outer join hospital.department dept on d.doctor_id = dept.department_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/lxx54jj6rrte3qgctuzv.png&quot; alt=&quot;Full Outer Join&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Null values will be returned in the rows where there is no relationship between doctor and department IDs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;cross-join&quot;&gt;CROSS JOIN&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;CROSS JOIN&lt;/code&gt; results in a Cartesian product where each row in the first table is paired up with each row of the second table.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How can hospital managers get a complete list of all pairings of physicians with hospital wards for coverage planing?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    d.doctor_name,
    w.ward_name
from hospital.doctors d
cross join hospital.wards w
order by d.doctor_name, w.ward_name
limit 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/1wr0ga53xrcehptw5ysf.png&quot; alt=&quot;Cross Join&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Joining 4 doctors with 4 wards results in 16 combination rows.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;self-join&quot;&gt;SELF JOIN&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;SELF JOIN&lt;/code&gt; involves joining a table to itself for comparing rows in the same table.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;In what way can readmission of patients be identified by matching their first visit to their next visit?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    v1.patient_id,
    v1.visit_id as original_visit,
    v1.visit_date as original_date,
    v2.visit_id as return_visit,
    v2.visit_date as return_date
from hospital.visits v1
inner join hospital.visits v2 on v1.patient_id = v2.patient_id and v1.visit_id &amp;lt; v2.visit_id
order by v1.patient_id
limit 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/6gu6l8j459l4c0l89mg4.png&quot; alt=&quot;Self Join&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linking visits on patient ID where &lt;code&gt;v1.visit_id&lt;/code&gt; is less than &lt;code&gt;v2.visit_id&lt;/code&gt; matches distinct visits per patient.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;join-summary&quot;&gt;Join Summary&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;INNER JOIN&lt;/code&gt; matches rows in both tables.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LEFT JOIN&lt;/code&gt; keeps all left table rows.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RIGHT JOIN&lt;/code&gt; keeps all right table rows.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FULL OUTER JOIN&lt;/code&gt; keeps all rows from both tables.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CROSS JOIN&lt;/code&gt; produces Cartesian pairs.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SELF JOIN&lt;/code&gt; compares rows within one table.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;3-common-table-expressions-ctes-&quot;&gt;3. Common Table Expressions (CTEs)&lt;/h3&gt;
&lt;p&gt;A CTE is a named temporary table created within a &lt;code&gt;WITH&lt;/code&gt; clause.&lt;/p&gt;
&lt;h4 id=&quot;simple-ctes&quot;&gt;Simple CTEs&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;What approach should be used to identify costly inpatient treatments and connect them with patients and their physicians?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;with high_cost_visits as (
    select 
        visit_id,
        patient_id,
        doctor_id,
        total_bill_kes,
        admission_type
    from hospital.visits
    where total_bill_kes &amp;gt;= 35000.00
)
select 
    h.visit_id,
    p.patient_name,
    d.doctor_name,
    h.total_bill_kes
from high_cost_visits h
join hospital.patients p on h.patient_id = p.patient_id
join hospital.doctors d on h.doctor_id = d.doctor_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/lx3qdtqcxagxsp5vwmxv.png&quot; alt=&quot;Simple CTE&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CTE will filter out high cost visits first to keep the subsequent join clean.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;multiple-chained-ctes&quot;&gt;Multiple Chained CTEs&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How do we find out department revenues, overall average, and top performing departments?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;with dept_revenue as (
    select 
        department_id,
        sum(total_bill_kes) as total_revenue,
        count(visit_id) as total_visits
    from hospital.visits
    group by department_id
),
dept_benchmark as (
    select 
        avg(total_revenue) as benchmark_avg_revenue
    from dept_revenue
)
select 
    d.department_name,
    dr.total_revenue,
    dr.total_visits,
    round(db.benchmark_avg_revenue, 2) as benchmark_avg
from dept_revenue dr
join hospital.department d on dr.department_id = d.department_id
cross join dept_benchmark db
where dr.total_revenue &amp;gt; db.benchmark_avg_revenue;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/449qhx3hzealxl7t6ncw.png&quot; alt=&quot;Multiple CTEs&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dept_revenue&lt;/code&gt; calculates the total revenue for each &lt;code&gt;department&lt;/code&gt;, &lt;code&gt;dept_benchmark&lt;/code&gt; calculates the average and the last &lt;code&gt;SELECT&lt;/code&gt; statement filters departments whose revenues are greater than the average.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;recursive-ctes&quot;&gt;Recursive CTEs&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Recursive CTEs&lt;/code&gt; create a sequence by means of an &lt;code&gt;anchor query&lt;/code&gt; and &lt;code&gt;recursive query&lt;/code&gt; linked by &lt;code&gt;UNION ALL&lt;/code&gt; operator.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How can the hospital management create a biweekly follow up schedule?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;with recursive follow_up_schedule as (
    select 
        1 as cycle_number,
        cast('2026/07/01' as date) as scheduled_date
    union all
    select 
        cycle_number + 1,
        cast(scheduled_date + interval '14 days' as date)
    from follow_up_schedule
    where cycle_number &amp;lt; 4
)
select * from follow_up_schedule;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/w9avtbp5ixud7pfq9ysy.png&quot; alt=&quot;Recursive CTE&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cycle number 1 starts from &lt;code&gt;July 1 2026&lt;/code&gt; as the &lt;code&gt;anchor date&lt;/code&gt;, and the recursive query keeps adding 14 days till cycle number is 4.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;ctes-vs-subqueries&quot;&gt;CTEs vs. Subqueries&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;CTEs&lt;/code&gt; provide well-defined top-level modules, which can be used again and again, while subqueries within the WHERE or FROM clause tend to complicate code and cannot be executed recursively.&lt;/p&gt;
&lt;h3 id=&quot;4-sql-subqueries&quot;&gt;4. SQL Subqueries&lt;/h3&gt;
&lt;p&gt;A subquery is a query nested inside another SQL query.&lt;/p&gt;
&lt;h4 id=&quot;subquery-types&quot;&gt;Subquery Types&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Scalar&lt;/code&gt; Subquery fetches one row and one column.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Non-correlated&lt;/code&gt;Subquery executes only once and independently.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Correlated&lt;/code&gt; Subquery uses columns of outer query and executes for every row of outer query.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Nested&lt;/code&gt; Subquery uses subqueries in subqueries.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;subqueries-in-select-clauses&quot;&gt;Subqueries in SELECT Clauses&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How can individual patient bills be shown along with the total hospital average bill?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    visit_id,
    total_bill_kes,
    (select round(avg(total_bill_kes), 2) from hospital.visits) as overall_avg_bill
from hospital.visits
where visit_id between 1001 and 1005;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/gx3q0v7r1kr5sjyfeasm.png&quot; alt=&quot;Select Subqueries&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scalar subquery evaluates to average &lt;code&gt;16266.67&lt;/code&gt; for all rows.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;using-subqueries-in-from-clauses-derived-table-&quot;&gt;Using Subqueries in FROM Clauses (Derived Table)&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How do we query the pre-aggregated metrics of patients using an inline derived table?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    patient_summary.patient_id,
    patient_summary.visit_count,
    patient_summary.total_spent
from (
    select 
        patient_id,
        count(visit_id) as visit_count,
        sum(total_bill_kes) as total_spent
    from hospital.visits
    group by patient_id
) as patient_summary
where patient_summary.visit_count &amp;gt; 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/gpg7zu85w5vr1pebl4ap.png&quot; alt=&quot;From Subquery&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;FROM&lt;/code&gt; subquery groups the number of visits per patient into table alias patient_summary, subject to WHERE.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;subqueries-in-where-clause&quot;&gt;Subqueries in WHERE Clause&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;What is the correlated subquery to find visits where the total bill is greater than the average bill of that particular department?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    v1.visit_id,
    v1.patient_id,
    v1.department_id,
    v1.total_bill_kes
from hospital.visits v1
where v1.total_bill_kes &amp;gt; (
    select avg(v2.total_bill_kes)
    from hospital.visits v2
    where v2.department_id = v1.department_id
)
limit 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/08o8bu15t93paib73o1b.png&quot; alt=&quot;Where Subqueries&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The inner query calculates the average billing amount for each department based on outer query department IDs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;Which physicians have admitted any patients as inpatients?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    d.doctor_id,
    d.doctor_name,
    d.doctor_specialty
from hospital.doctors d
where exists (
    select 1 
    from hospital.visits v 
    where v.doctor_id = d.doctor_id 
      and v.admission_type = 'inpatient'
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/x34b2dlyj5gbu387srum.png&quot; alt=&quot;Exists&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;EXISTS&lt;/code&gt; returns true when there is a row for a visit that matches.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How can we find visits where the billing value is greater than all the outpatient visit bills?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    visit_id,
    patient_id,
    total_bill_kes,
    admission_type
from hospital.visits
where total_bill_kes &amp;gt; all (
    select total_bill_kes
    from hospital.visits
    where admission_type = 'outpatient'
)
limit 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/vdfcjvi0dr5ifpb94esd.png&quot; alt=&quot;where&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The internal query locates outpatient charges up to &lt;code&gt;15000.00&lt;/code&gt;. ALL filters visits more than &lt;code&gt;15000.00&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Understanding the concepts of window functions, joins, common table expressions, and subqueries helps in creating strong PostgreSQL queries that enable healthcare workers to review their operations and enhance financial efficiency. Such information enables healthcare providers to offer quality pateint care which meets patient outcomes. &lt;/p&gt;
&lt;h3 id=&quot;github&quot;&gt;GitHub&lt;/h3&gt;
&lt;p&gt;For the completed project visit my GitHub project &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MakoriNyachaki/sql_for_data_science&quot;&gt;Hospital Database&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31618/advanced-sql-analytics-for-data-analysis</guid>
<pubDate>Sun, 18 Oct 2026 13:54:35 +0000</pubDate>
</item>
<item>
<title>Instant at 10 Million Rows, Dead at a Billion: A Decision Framework for Billion-Row SQL</title>
<link>https://coderlegion.com/30403/instant-at-10-million-rows-dead-at-a-billion-a-decision-framework-for-billion-row-sql</link>
<description>&lt;p&gt;&lt;img src=&quot;https://unrivaled-starlight-d655be.netlify.app/blog/visuals/billion-row-sql-hero.png?v=2&quot; alt=&quot;Abstract brand-toned data pathway backdrop for the billion-row SQL decision framework.&quot;&gt;&lt;/p&gt;
&lt;p&gt;A query that returns in 40 milliseconds on ten million rows can time out completely on a billion. Same SQL, same schema, same database. I've watched a lot of teams hit that wall and go hunting for the one fix: a cleverer index, a bigger instance, whatever database was trending that month. There isn't one fix. There are two questions that decide almost everything. What engine class is this workload on — row-store, columnar, or distributed? And does the query's working set fit in memory, or does it spill to disk? Answer those honestly and most &quot;billion-row problems&quot; quietly resolve. Answer them wrong and no amount of tuning bails you out.&lt;/p&gt;
&lt;h3 id=&quot;the-query-that-was-instant-at-10-million-rows-and-timed-out-at-a-billion&quot;&gt;The query that was instant at 10 million rows and timed out at a billion&lt;/h3&gt;
&lt;p&gt;Here's the shape of it. It's a composite; I've seen it often enough that the specifics blur.&lt;/p&gt;
&lt;p&gt;A team builds a dashboard on their main OLTP database. One query carries it: count distinct users, grouped by day, over a date range. Against a 10-million-row staging copy it comes back instantly, everyone signs off, ship it. Months later production crosses a billion rows — roughly the &lt;a rel=&quot;nofollow&quot; href=&quot;https://tech.marksblogg.com/benchmarks.html&quot;&gt;1.1-billion-row scale in Mark Litwintschik's cross-engine benchmarks&lt;/a&gt; — and the identical query simply stops returning. On-call adds an index; nothing. A composite index; nothing. A bigger instance; somehow slightly worse. &quot;Throw it at Spark,&quot; one person says. &quot;Just use ClickHouse,&quot; says another. Nobody can explain why either would help. That's the real problem: everyone is arguing tactics with no framework for the decision.&lt;/p&gt;
&lt;p&gt;The query didn't regress. It's doing exactly what it always did. What changed is that the workload crossed two lines at once — it outgrew the engine class that suited it at 10M, and its sorts and hashes stopped fitting in memory. Name those two lines and the mystery turns into a decision you can defend.&lt;/p&gt;
&lt;h3 id=&quot;the-two-axis-framework&quot;&gt;The two-axis framework&lt;/h3&gt;
&lt;p&gt;When a query dies at scale, the reflex is to index harder. Sometimes that's right. At a billion rows it's usually the wrong axis. Two levers actually decide whether a query survives.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Axis 1 — Engine class.&lt;/strong&gt; Every database is built around one dominant access pattern, and that choice shapes everything downstream. A &lt;strong&gt;row-store OLTP&lt;/strong&gt; engine (PostgreSQL, MySQL) keeps whole rows together and excels at &quot;fetch or update this one record&quot; and narrow ranges. A &lt;strong&gt;columnar/OLAP&lt;/strong&gt; engine (ClickHouse, DuckDB, BigQuery) stores each column separately, so scanning and aggregating hundreds of millions of rows is the design point rather than the emergency. A &lt;strong&gt;distributed&lt;/strong&gt; engine (Citus, Spark, Cassandra) spreads data across nodes for horizontal scale, trading query flexibility to get it. These are peers, not rungs on a ladder; none is &quot;more advanced.&quot; Run a workload on the wrong one and you get a query that flies at 10M and dies at 1B.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Axis 2 — Memory budget.&lt;/strong&gt; Independent of engine class, every non-trivial query builds intermediate structures: sort buffers, hash tables for &lt;code&gt;GROUP BY&lt;/code&gt; and &lt;code&gt;DISTINCT&lt;/code&gt;, join tables. Fit those in RAM and the operation runs at memory speed. Overflow the memory the engine is allowed, and it spills to disk and falls off a cliff. PostgreSQL ships &lt;code&gt;shared_buffers&lt;/code&gt; at &lt;strong&gt;128MB&lt;/strong&gt; and &lt;code&gt;work_mem&lt;/code&gt; at just &lt;strong&gt;4MB&lt;/strong&gt;; the docs are explicit that a sort or hash exceeding &lt;code&gt;work_mem&lt;/code&gt; starts &quot;writing to temporary disk files&quot; (&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/docs/current/runtime-config-resource.html&quot;&gt;PostgreSQL 18 — Resource Consumption&lt;/a&gt;, as of 2026). Four megabytes is nothing at a billion rows. This is the axis most write-ups skip, and it's why the same query is fast one day and glacial the next. The SQL didn't change; the data just got big enough to spill.&lt;/p&gt;
&lt;p&gt;The axes are orthogonal. You can pick the right engine class and still hit the memory cliff, or keep memory in check and still be on an engine that's wrong for the query's shape. So the move is: work out which quadrant you're actually in, then act on the axis that matches — change engine class, or change the memory budget — instead of adding another index to the axis that was never the problem.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://unrivaled-starlight-d655be.netlify.app/blog/visuals/v01-two-axis-framework.png&quot; alt=&quot;Two-axis decision framework: engine class (row-store, columnar, distributed) by memory budget (fits RAM vs spills to disk), with each quadrant's failure mode and the right move.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;engine-class-1-row-store-oltp-great-at-point-lookups-brutal-on-full-table-aggregates&quot;&gt;Engine class 1: Row-store OLTP — great at point lookups, brutal on full-table aggregates&lt;/h3&gt;
&lt;p&gt;Row-stores are the workhorse for good reason. Ask for &quot;order #48213 and its line items&quot; or &quot;the last 50 events for this user&quot; and a row-store with the right index is hard to beat: it seeks to a handful of rows and returns. At 10M rows, the dashboard query was riding on that.&lt;/p&gt;
&lt;p&gt;Aggregates over the whole table are a different story, and the reason is baked into how MVCC row-stores stay correct under concurrency. Because different transactions can see different versions of a row, the engine keeps no cached count of how many rows a table has. An exact &lt;code&gt;count(*)&lt;/code&gt; therefore has to visit rows and check visibility — a sequential scan that grows linearly with the table.&lt;/p&gt;
&lt;p&gt;The numbers are blunt. In Joe Nelson's &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.citusdata.com/blog/2016/10/12/count-performance/&quot;&gt;Citus benchmark on counting&lt;/a&gt; (PostgreSQL 9.5.4, 2016 — treat the milliseconds as illustrative of the mechanism, which is unchanged today, not as current figures), an exact &lt;code&gt;count(*)&lt;/code&gt; ran &lt;strong&gt;85ms at 1M rows, 161ms at 2M, and 343ms at 4M&lt;/strong&gt;: a straight line, with the scan about &lt;strong&gt;88%&lt;/strong&gt; of the cost. Extend that to a billion rows and a single count is tens of seconds before any filtering. &lt;code&gt;count(1)&lt;/code&gt; doesn't rescue you; it measured slightly &lt;em&gt;slower&lt;/em&gt; (99ms vs 85ms at 1M), because Postgres special-cases the argument-free &lt;code&gt;count(*)&lt;/code&gt; while &lt;code&gt;count(1)&lt;/code&gt; re-checks the argument on every row. And this isn't Postgres being difficult — MySQL's InnoDB shares the MVCC property and also has no O(1) table count.&lt;/p&gt;
&lt;p&gt;When you don't need an exact number, stop asking for one. Every row-store keeps a maintained estimate for its planner, and you can read it directly. In PostgreSQL that's &lt;code&gt;pg_class.reltuples&lt;/code&gt; (the &lt;a rel=&quot;nofollow&quot; href=&quot;https://wiki.postgresql.org/wiki/Count_estimate&quot;&gt;Count-estimate pattern&lt;/a&gt;: &lt;code&gt;SELECT reltuples::bigint FROM pg_class WHERE oid = 'schema.table'::regclass&lt;/code&gt;). In the same benchmark the estimate returned in about &lt;strong&gt;0.3ms against 85ms&lt;/strong&gt; for the exact count at 1M rows — roughly &lt;strong&gt;280× faster&lt;/strong&gt;, and effectively constant time at any size. The catch is accuracy: it's an estimate and can't apply a &lt;code&gt;WHERE&lt;/code&gt; or &lt;code&gt;DISTINCT&lt;/code&gt;. For &quot;roughly how many rows&quot; on a dashboard, that's the difference between a billion-row scan and reading one catalog row.&lt;/p&gt;
&lt;p&gt;None of this means &quot;Postgres is slow.&quot; It means a full-table aggregate is off-design for a row-store. Approximate counts, materialized rollups, and summary tables all soften it. But if scans and aggregates are your &lt;em&gt;dominant&lt;/em&gt; pattern, you're fighting the engine class — and that's your first crossover signal.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://unrivaled-starlight-d655be.netlify.app/blog/visuals/v02-aggregate-cliff.png&quot; alt=&quot;Exact count(*) rises linearly with table size while an approximate reltuples read stays flat and constant-time — roughly a 280x gap.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Exact `count(&lt;/em&gt;)&lt;code&gt; vs the &lt;/code&gt;reltuples` estimate. Source: Citus / Joe Nelson, PostgreSQL 9.5.4, 2016 — illustrative of the mechanism, not current figures.*&lt;/p&gt;
&lt;h3 id=&quot;engine-class-2-columnar-olap-where-scans-and-aggregates-are-the-design-point&quot;&gt;Engine class 2: Columnar / OLAP — where scans and aggregates are the design point&lt;/h3&gt;
&lt;p&gt;If aggregates are killing your row-store, what's built for them, and how much faster is it really? This is where the framework gets counter-intuitive.&lt;/p&gt;
&lt;p&gt;Columnar engines store each column contiguously instead of packing whole rows. A scan-and-aggregate query that touches 3 of 51 columns reads only those 3 off disk; the other 48 are never opened. And because a column holds one type with heavy repetition, it compresses hard, so there's less to read to begin with. Column pruning plus compression is why &quot;scan a billion rows and aggregate&quot; is routine here instead of an incident.&lt;/p&gt;
&lt;p&gt;The headline number: Litwintschik's &lt;a rel=&quot;nofollow&quot; href=&quot;https://tech.marksblogg.com/benchmarks.html&quot;&gt;1.1-billion-row taxi benchmark&lt;/a&gt; (1.1B rows, 51 columns, ~500GB uncompressed; page updated March 2024) runs one analytical query across dozens of engines. On Query 1, a single desktop ran &lt;strong&gt;ClickHouse in 0.088s&lt;/strong&gt; and &lt;strong&gt;DuckDB in 0.498s&lt;/strong&gt;. The same query took &lt;strong&gt;2.36s on a 21-node Spark cluster&lt;/strong&gt;, &lt;strong&gt;3.54s on 21-node Presto&lt;/strong&gt;, and &lt;strong&gt;1–3s on BigQuery&lt;/strong&gt;. Postgres with the &lt;code&gt;cstore_fdw&lt;/code&gt; columnar extension took &lt;strong&gt;152–368s&lt;/strong&gt; across the four queries, and plain SQLite's format took &lt;strong&gt;31,193s on Query 1 — about 8.7 hours&lt;/strong&gt;, included to show what the wrong engine class actually costs.&lt;/p&gt;
&lt;p&gt;A single desktop beat a 21-node cluster on a 1.1-billion-row scan by more than an order of magnitude. That one result is the whole framework in miniature. It isn't that ClickHouse or DuckDB is &quot;the best database&quot; — it's that for a scan-aggregate workload a columnar engine is in the right quadrant while the cluster pays coordination overhead it doesn't need at this size. (Caveats: one benchmark, one hardware generation, recent desktop CPUs for the fastest single-node runs. Trust the shape of the gap, not the exact milliseconds.)&lt;/p&gt;
&lt;p&gt;Compression is the other half, and it's an economics lever as much as a speed one. ClickBench — the ClickHouse team's &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ClickHouse/ClickBench&quot;&gt;scan/aggregate benchmark&lt;/a&gt; across 60+ systems — publishes a live &quot;Data Size&quot; column so you can compare on-disk footprint on the same ~100M-row dataset. Columnar layouts routinely pack this kind of wide, repetitive data down by a large multiple versus a row-store, though the exact ratio moves with engine, codec, and data, so read the live figure for the systems you're weighing rather than trusting a headline number. (ClickBench is ClickHouse-run, so treat it as a scan-workload reference, not a neutral verdict.)&lt;/p&gt;
&lt;p&gt;The honest counter-point: the same columnar engine that crushes the scan is the wrong tool for high-QPS single-row writes and point lookups. Insert a row and you touch every column store; update a field and compression works against you. The wide-column and key-value stores built for that job — Cassandra, ScyllaDB — sit at the opposite pole: excellent at point reads and writes on a known partition key, and structurally unfit for ad-hoc full-table aggregates. Cassandra's &lt;a rel=&quot;nofollow&quot; href=&quot;https://cassandra.apache.org/doc/latest/cassandra/developing/cql/dml.html&quot;&gt;CQL docs&lt;/a&gt; say it plainly: CQL &quot;only allows select queries that don't involve a full scan of all partitions,&quot; and forcing one needs &lt;code&gt;ALLOW FILTERING&lt;/code&gt;, whose performance the docs call &quot;unpredictable.&quot; A store modeled query-first around the partition key isn't a scan-aggregate engine, which is exactly why ClickBench lists Cassandra and ScyllaDB among the systems it couldn't benchmark on that suite. Right engine, wrong job, in both directions.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://unrivaled-starlight-d655be.netlify.app/blog/visuals/v03-cross-engine-scan.png&quot; alt=&quot;Litwintschik 1.1-billion-row Query 1: a single desktop running ClickHouse or DuckDB beats a 21-node Spark/Presto cluster; SQLite is the wrong-tool extreme (log scale).&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Same query, same 1.1B-row dataset, across engine classes. Source: Mark Litwintschik, 1.1 Billion Taxi Rides benchmarks, page updated March 2024.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;engine-class-3-distributed-horizontal-scale-and-the-fan-out-ceiling&quot;&gt;Engine class 3: Distributed — horizontal scale, and the fan-out ceiling&lt;/h3&gt;
&lt;p&gt;When one node genuinely can't hold the data or serve the writes, distributed engines spread the work across many. The mental model most people carry — more nodes, more speed — holds right up until it doesn't, and knowing where that line sits is the difference between a defensible cluster and a single box with a network bill attached.&lt;/p&gt;
&lt;p&gt;The line: does the query push down to the shards, or force a reshuffle? Distribute by &lt;code&gt;tenant_id&lt;/code&gt; and group by &lt;code&gt;tenant_id&lt;/code&gt;, and each node computes its slice while the coordinator stitches the results together. That's pushdown, and it parallelizes beautifully. But the moment a query needs data spread across nodes — a &lt;code&gt;count(DISTINCT)&lt;/code&gt; on a non-distribution column, or a join across the distribution boundary — the engine has to reshuffle rows between workers first. That shuffle is the ceiling.&lt;/p&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.citusdata.com/blog/2016/10/12/count-performance/&quot;&gt;Citus benchmark&lt;/a&gt; puts numbers on it (100M rows, 8 nodes, 32 shards; PG 9.5.4, 2016 — again mechanism-illustrative). A plain &lt;code&gt;count(*)&lt;/code&gt; across the cluster ran about &lt;strong&gt;1.2s&lt;/strong&gt;. A &lt;code&gt;count(DISTINCT)&lt;/code&gt; on the distribution column, which pushes down, ran about &lt;strong&gt;3.4s&lt;/strong&gt;. On a non-distribution column it can't push down, and pulling every row back to the coordinator is, in Nelson's words, &quot;really no better than counting on a single database instance … plus high network overhead.&quot; An HLL approximate distinct ran &lt;strong&gt;3.2–3.8s&lt;/strong&gt; — the row-store's approximation trick again, now buying a way around the shuffle. This generalizes well past Citus: in Spark the expensive stage is almost always the shuffle, and distributed query design is mostly the craft of keeping the hot queries pushed down.&lt;/p&gt;
&lt;p&gt;So distributed isn't a &quot;scale&quot; button. It's a trade — horizontal capacity for query flexibility and operational complexity. Align your access pattern with the distribution key and it's transformative. Keep needing cross-node data and you've bought nodes &lt;em&gt;and&lt;/em&gt; network latency to arrive back at roughly single-box performance. That mismatch is its own crossover signal, and the one most likely to be discovered the hard way in production.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://unrivaled-starlight-d655be.netlify.app/blog/visuals/v04-fanout-pushdown.png&quot; alt=&quot;Two distributed query paths: one pushes down to shards and merges at the coordinator; the other forces a cross-node shuffle that collapses to a single box plus network overhead.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-memory-spill-axis-the-one-most-write-ups-skip&quot;&gt;The memory / spill axis — the one most write-ups skip&lt;/h3&gt;
&lt;p&gt;Now the second axis, and the reason &quot;just add an index&quot; so often does nothing. A query can be on exactly the right engine class and still fall off a cliff, because its sorts and hash tables outgrew the memory the engine was allowed and spilled to disk. This is engine-independent: columnar and distributed engines build the same buffers and spill the same way when they overflow their limits. Postgres is just the easiest place to watch it, because its docs name the knob.&lt;/p&gt;
&lt;h4 id=&quot;the-spill-cliff&quot;&gt;The spill cliff&lt;/h4&gt;
&lt;p&gt;At the default 4MB &lt;code&gt;work_mem&lt;/code&gt; (&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/docs/current/runtime-config-resource.html&quot;&gt;PostgreSQL docs&lt;/a&gt;), a &lt;code&gt;GROUP BY&lt;/code&gt;, &lt;code&gt;DISTINCT&lt;/code&gt;, or &lt;code&gt;ORDER BY&lt;/code&gt; that builds something larger switches from an in-memory hash or sort to an external merge that writes temp files to disk. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.citusdata.com/blog/2016/10/12/count-performance/&quot;&gt;Citus benchmark&lt;/a&gt; shows the size of the drop (PG 9.5.4, 2016; mechanism unchanged per the current docs): at default &lt;code&gt;work_mem&lt;/code&gt;, &lt;code&gt;count(DISTINCT)&lt;/code&gt; on an integer column averaged &lt;strong&gt;743ms&lt;/strong&gt;, but on a text column &lt;strong&gt;31,747ms&lt;/strong&gt; — a &lt;strong&gt;~43× gap&lt;/strong&gt; — with &lt;code&gt;Sort Method: external merge Disk&lt;/code&gt; in the plan. Give the operation enough &lt;code&gt;work_mem&lt;/code&gt; to stay in RAM and the integer case drops to about &lt;strong&gt;372ms&lt;/strong&gt; as an in-memory &lt;code&gt;HashAggregate&lt;/code&gt;. That in-RAM-versus-disk boundary is the axis. Stay on the right side and the query flies; cross it and it collapses.&lt;/p&gt;
&lt;p&gt;This is exactly why indexing didn't help the composite team. An index changes how you &lt;em&gt;find&lt;/em&gt; rows; it does nothing for a &lt;code&gt;GROUP BY&lt;/code&gt;/&lt;code&gt;DISTINCT&lt;/code&gt; whose result set is too big for &lt;code&gt;work_mem&lt;/code&gt;. The lever there is memory, or a smaller working set — not another index.&lt;/p&gt;
&lt;h4 id=&quot;when-to-re-architect-instead-of-tune&quot;&gt;When to re-architect instead of tune&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;work_mem&lt;/code&gt; is allocated per operation per query, and the docs warn total usage &quot;could be many times&quot; that value under load, so raising it globally just trades a spill problem for an out-of-memory one. Past a point the honest move is structural. PostgreSQL's &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/docs/current/ddl-partitioning.html&quot;&gt;partitioning guidance&lt;/a&gt; offers a clean rule: partition &quot;when the size of the table [would] exceed the physical memory of the database server.&quot; Pruning then skips whole partitions — but it isn't free. The planner handles &quot;up to a few thousand&quot; partitions well only if pruning leaves few, since &quot;each partition requires its metadata to be loaded into the local memory of each session that touches it.&quot; Thousands of poorly-pruned partitions is its own problem.&lt;/p&gt;
&lt;h4 id=&quot;one-more-trap-deep-pagination&quot;&gt;One more trap: deep pagination&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;LIMIT ... OFFSET n&lt;/code&gt; gets slower the further you page. As Markus Winand explains in &lt;a rel=&quot;nofollow&quot; href=&quot;https://use-the-index-luke.com/no-offset&quot;&gt;&quot;No Offset&quot;&lt;/a&gt;, the database &quot;fetches and drops&quot; all &lt;code&gt;n&lt;/code&gt; preceding rows before returning the page, and results drift when rows are inserted between requests. A keyset (seek) query filters on the last-seen sort key instead and stays roughly constant-time however deep you go. Page latency that climbs with depth is a spill-adjacent signal, not a hardware one.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://unrivaled-starlight-d655be.netlify.app/blog/visuals/v05-spill-cliff.png&quot; alt=&quot;The spill cliff: an in-RAM hash aggregate vs an external merge to disk, and the collapse back to memory speed when work_mem is raised.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;count(DISTINCT) at default vs raised work_mem. Source: Citus / Joe Nelson, PostgreSQL 9.5.4, 2016 — illustrative of the mechanism, not current figures.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-crossover-signal-checklist-change-engine-class-don-t-add-another-index&quot;&gt;The crossover-signal checklist: change engine class, don't add another index&lt;/h3&gt;
&lt;p&gt;This is the piece worth saving. A framework's job is to tell you &lt;em&gt;when&lt;/em&gt; you've hit the boundary and it's time to move on an axis instead of tuning further. Eight signals I watch for, each tied to the axis it moves. Two or more firing means you're past tuning and into an engine-class or memory-budget decision.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Scans and aggregates dominate the workload&lt;/strong&gt; (not point lookups). → Engine class, toward &lt;strong&gt;columnar&lt;/strong&gt;. The row-store aggregate cliff; indexing won't fix an off-design pattern.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The working set exceeds server RAM.&lt;/strong&gt; → Memory. Postgres's own partition threshold; partition, re-architect, or move to columnar/distributed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sorts and &lt;code&gt;DISTINCT&lt;/code&gt;s keep spilling even after tuning &lt;code&gt;work_mem&lt;/code&gt;.&lt;/strong&gt; → Memory. You've hit the ceiling of per-query memory; the working set is the problem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pagination or scan latency rises with position.&lt;/strong&gt; → Memory/scan; switch to keyset/seek before switching engines.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A single node has hit its write or IO ceiling.&lt;/strong&gt; → Engine class, toward &lt;strong&gt;distributed&lt;/strong&gt;. The one genuine &quot;add nodes&quot; signal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Key &lt;code&gt;DISTINCT&lt;/code&gt;s or joins need data from several nodes&lt;/strong&gt; (a shuffle). → A warning on the distributed axis: without a distribution-key redesign, more nodes ≈ one box plus network.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You need thousands of partitions to stay fast.&lt;/strong&gt; → Memory/metadata; the per-partition cost says a columnar or distributed store fits the data better.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You're keeping 500GB+ on one box for analytics.&lt;/strong&gt; → Engine class plus economics; columnar compression and distributed storage costs start driving the decision.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;What the list deliberately doesn't do is name a winning database. Each signal points to an axis and a direction, because the right specific engine depends on your query shapes, your team's ops maturity, and your budget. &quot;Change engine class&quot; is an architectural decision with a reason attached — something you can hand a skeptical peer or a budget owner instead of &quot;the internet said use ClickHouse.&quot;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://unrivaled-starlight-d655be.netlify.app/blog/visuals/v06-crossover-checklist.png&quot; alt=&quot;The eight crossover signals, each tagged with the axis it moves (engine class or memory budget) and the recommended direction.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;cost-and-economics-for-the-decision-makers&quot;&gt;Cost and economics for the decision-makers&lt;/h3&gt;
&lt;p&gt;If you own the stack decision, latency is half the case; the half that gets budget approved is cost and defensibility. The benchmarks reframe cleanly as money.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A single big box can be cheaper &lt;em&gt;and&lt;/em&gt; faster than a cluster.&lt;/strong&gt; The &lt;a rel=&quot;nofollow&quot; href=&quot;https://tech.marksblogg.com/benchmarks.html&quot;&gt;Litwintschik result&lt;/a&gt; — one desktop-class node beating a 21-node cluster on a 1.1B-row scan — reads louder as a cost story than a speed one: 21 nodes carry ~21× the compute plus coordination, networking, and a standing ops burden, and still lose on this workload. For &quot;big but not planetary&quot; analytics, the first move is often the right engine class on one well-sized box, not a cluster. Distributed earns its cost when you truly exceed single-node capacity or need the availability.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compression is a storage-cost lever.&lt;/strong&gt; A columnar layout shrinks wide, repetitive data by a large multiple (measure it for your engines from &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ClickHouse/ClickBench&quot;&gt;ClickBench's &quot;Data Size&quot; column&lt;/a&gt; — it varies by engine and codec), which is a recurring cut in bytes stored and, in a warehouse, fewer dollars per query scanned.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Per-query billing cuts both ways.&lt;/strong&gt; A serverless warehouse (BigQuery ran 1–3s per query here) fits spiky, ad-hoc analytics where an idle cluster would bleed money, and fits badly for a high-frequency dashboard hammering one aggregate, where a right-sized columnar box wins on total cost.&lt;/p&gt;
&lt;p&gt;Every price and benchmark here will rotate — pricing shifts, hardware moves the single-node ceiling, vendor benchmarks get re-run. Don't anchor the decision to one dated figure. The framework doesn't rotate: two axes, eight signals. If a price moves, re-run the comparison.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://unrivaled-starlight-d655be.netlify.app/blog/visuals/v07-cost-ops-profile.png&quot; alt=&quot;Cost and operations profile per engine class: relative storage cost, node count, operational burden, query flexibility, and best-fit workload.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;build-it-yourself&quot;&gt;Build it yourself&lt;/h3&gt;
&lt;p&gt;The framework is cheap until you feel it on your own data. Three projects, beginner to advanced. Each names a real tool (links verified reachable in 2026, but repos move — check before you lean on them).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Project 1 — Count your table three ways (~30 min).&lt;/strong&gt; Feel the row-store aggregate cliff on real data. You'll need a Postgres or MySQL table with 10M+ rows and &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/duckdb/duckdb&quot;&gt;DuckDB&lt;/a&gt;. Run &lt;code&gt;EXPLAIN ANALYZE SELECT count(*) FROM your_table;&lt;/code&gt; and note the time and the &lt;code&gt;Seq Scan&lt;/code&gt;; then &lt;code&gt;SELECT reltuples::bigint FROM pg_class WHERE oid = 'schema.your_table'::regclass;&lt;/code&gt;; then get a slice into columnar form — export CSV (&lt;code&gt;COPY your_table TO 'slice.csv' (FORMAT csv)&lt;/code&gt;) and load it into DuckDB, or read the table directly with &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/duckdb/pg_duckdb&quot;&gt;pg_duckdb&lt;/a&gt; — and &lt;code&gt;count(*)&lt;/code&gt; there. &lt;strong&gt;Done when&lt;/strong&gt; you can state your own numbers for all three and point to the &lt;code&gt;Seq Scan&lt;/code&gt;. Stretch: add &lt;code&gt;GROUP BY day&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Project 2 — Reproduce the spill cliff (~1–2 hrs).&lt;/strong&gt; Watch a query cross the RAM→disk boundary. You'll need Postgres and a 1M+ row table with an integer and a text column (the NYC-taxi or ClickBench sets work). At default &lt;code&gt;work_mem&lt;/code&gt;, run &lt;code&gt;EXPLAIN ANALYZE SELECT count(DISTINCT int_col) FROM t;&lt;/code&gt; then the same on &lt;code&gt;text_col&lt;/code&gt;, and confirm &lt;code&gt;Sort Method: external merge Disk&lt;/code&gt; on the slow one. &lt;code&gt;SET work_mem = '1GB';&lt;/code&gt;, re-run both. &lt;strong&gt;Done when&lt;/strong&gt; you've captured your own before/after ratio and seen the plan flip to in-memory &lt;code&gt;HashAggregate&lt;/code&gt;. Stretch: chart latency as you step &lt;code&gt;work_mem&lt;/code&gt; from 4MB to 1GB and find your own cliff edge.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Project 3 — Run a slice of the 1.1B benchmark (~half a day).&lt;/strong&gt; Reproduce the crossover on your hardware. You'll need the same big table in a row-store and a columnar engine, plus the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ClickHouse/ClickBench&quot;&gt;ClickBench loaders&lt;/a&gt; (60+ DBMS setup scripts) and &lt;a rel=&quot;nofollow&quot; href=&quot;https://tech.marksblogg.com/benchmarks.html&quot;&gt;Litwintschik's posts&lt;/a&gt; for reference. Load an identical scan-aggregate table into each, time one representative query on both, and record the ratio and your hardware. &lt;strong&gt;Done when&lt;/strong&gt; you reproduce a &amp;gt;100× gap — or can explain from the plans why yours differs. Stretch: add a distributed engine and build a query that forces a shuffle to find your own fan-out ceiling.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://unrivaled-starlight-d655be.netlify.app/blog/visuals/v08-project-ladder.png&quot; alt=&quot;Build-it-yourself project ladder: beginner to advanced, each rung mapped to an axis — aggregate cliff, spill cliff, cross-engine crossover.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-to-do-monday-morning&quot;&gt;What to do Monday morning&lt;/h3&gt;
&lt;p&gt;The whole thing, compressed. When a query dies at scale, stop asking &quot;which index?&quot; and ask the two questions that decide it. Engine class: is the dominant shape point-lookups (row-store), scans and aggregates (columnar), or genuinely bigger than one node (distributed)? Memory budget: does the working set fit in RAM, or is it spilling? Then walk the eight signals; two or more means the move is on an axis, not another index. There's no single answer to &quot;how do I handle a billion rows&quot; — the answer is a decision you can write down and defend.&lt;/p&gt;
&lt;p&gt;So go run it. Take your biggest table this week and do Project 1: &lt;code&gt;count(*)&lt;/code&gt;, then &lt;code&gt;reltuples&lt;/code&gt;, then a columnar count in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/duckdb/duckdb&quot;&gt;DuckDB&lt;/a&gt;. Watch where the plan shows a &lt;code&gt;Seq Scan&lt;/code&gt; and how far apart the three numbers land. Want the full crossover? The &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ClickHouse/ClickBench&quot;&gt;ClickBench repo&lt;/a&gt; has loaders for 60+ engines, so you can time the same scan on a row-store and a columnar engine side by side. Bring back your own numbers — the framework only lands once the cliff is yours.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30403/instant-at-10-million-rows-dead-at-a-billion-a-decision-framework-for-billion-row-sql</guid>
<pubDate>Fri, 09 Oct 2026 06:03:06 +0000</pubDate>
</item>
<item>
<title>Inside the Database Stack</title>
<link>https://coderlegion.com/30144/inside-the-database-stack</link>
<description>&lt;p&gt;Think of a restaurant. Your Django code is the &lt;strong&gt;waiter&lt;/strong&gt;, PostgreSQL is the &lt;strong&gt;kitchen&lt;/strong&gt;, and the disk is the &lt;strong&gt;pantry&lt;/strong&gt;. When dinner is slow, people blame the waiter's handwriting (the ORM). Far more often the waiter is making 200 separate trips to the kitchen, or the cook is checking every shelf in the pantry because nobody labeled them.&lt;/p&gt;
&lt;p&gt;This article follows one order from the waiter's pad to the pantry shelf and back, and ends with what actually makes databases slow.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;part-i-the-waiter-python-amp-django-orm-&quot;&gt;Part I: The Waiter (Python &amp;amp; Django ORM)&lt;/h4&gt;
&lt;p&gt;Django open-sourced its ORM in 2005. Its core idea is the &lt;strong&gt;QuerySet&lt;/strong&gt;, a &lt;em&gt;description&lt;/em&gt; of the data you want rather than the data itself.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;orders = Order.objects.filter(status=&quot;paid&quot;)   # no SQL has run
orders = orders.filter(total__gt=100)          # still nothing
print(orders.query)                            # shows the SQL it *would* run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is &lt;strong&gt;lazy evaluation&lt;/strong&gt;. The waiter writes the order on the pad but doesn't walk to the kitchen until you actually need food: when you loop over the results, call &lt;code&gt;list()&lt;/code&gt;, slice with a step, or ask for &lt;code&gt;len()&lt;/code&gt;. Each &lt;code&gt;.filter()&lt;/code&gt; returns a &lt;em&gt;new&lt;/em&gt; QuerySet, and Django compiles the final chain into a single SQL statement only at the last moment.&lt;/p&gt;
&lt;p&gt;That laziness is a benefit. You can build queries across functions without touching the database. It is also a trap, because a harmless-looking attribute access inside a loop can silently send the waiter running.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ORM vs raw SQL.&lt;/strong&gt; The ORM buys you safety (parameterized queries that block SQL injection), portability, and readable code. It costs you some CPU: building the query objects, compiling SQL, and turning every row into a full Python model instance. For a few hundred rows, this overhead is invisible. For a hundred thousand rows, &lt;code&gt;.values()&lt;/code&gt;, &lt;code&gt;.only()&lt;/code&gt;, or &lt;code&gt;.iterator()&lt;/code&gt; can shrink it considerably.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;part-ii-too-many-trips-query-performance-&quot;&gt;Part II: Too Many Trips (Query Performance)&lt;/h4&gt;
&lt;p&gt;The classic failure is the &lt;strong&gt;N+1 query problem&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;for order in Order.objects.all():          # 1 query
    print(order.customer.name)             # +1 query per order
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Show 500 orders and you've sent 501 queries. It is like a waiter walking to the kitchen 500 times for 500 glasses of water. Each trip is fast, but the &lt;em&gt;trips&lt;/em&gt; add up, especially when the database sits across a network.&lt;/p&gt;
&lt;p&gt;Django has two fixes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;select_related(&quot;customer&quot;)&lt;/code&gt;&lt;/strong&gt; performs a SQL &lt;code&gt;JOIN&lt;/code&gt; and fetches the related row in the same query. Use it for single-valued relations (foreign keys, one-to-one).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;prefetch_related(&quot;items&quot;)&lt;/code&gt;&lt;/strong&gt; runs a &lt;em&gt;second&lt;/em&gt; query and stitches the results together in Python. Use it for many-valued relations, where a join would duplicate parent rows.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;Order.objects.select_related(&quot;customer&quot;).prefetch_related(&quot;items&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two metrics matter here, and people often confuse them. &lt;strong&gt;Query count&lt;/strong&gt; is how many trips you made. &lt;strong&gt;Query latency&lt;/strong&gt; is how long each one took. A page with 400 fast queries and a page with 3 slow ones need &lt;em&gt;different&lt;/em&gt; cures. Tools like &lt;code&gt;django-debug-toolbar&lt;/code&gt; or &lt;code&gt;CaptureQueriesContext&lt;/code&gt; show both.&lt;/p&gt;
&lt;p&gt;Beyond N+1, the usual bottlenecks are fetching columns you never use, calling &lt;code&gt;len(qs)&lt;/code&gt; when &lt;code&gt;qs.count()&lt;/code&gt; or &lt;code&gt;qs.exists()&lt;/code&gt; would do, saving rows one at a time instead of &lt;code&gt;bulk_create()&lt;/code&gt;, and filtering in Python what the database could filter itself.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;part-iii-the-pantry-database-fundamentals-&quot;&gt;Part III: The Pantry (Database Fundamentals)&lt;/h4&gt;
&lt;p&gt;PostgreSQL is a &lt;strong&gt;process-per-connection&lt;/strong&gt; system. Each client gets its own backend process, and all of them share memory buffers and a write-ahead log (&lt;strong&gt;WAL&lt;/strong&gt;), which records changes before they hit the main files so a crash can't lose them.&lt;/p&gt;
&lt;p&gt;Rows live in a &lt;strong&gt;table&lt;/strong&gt; stored as fixed-size &lt;strong&gt;pages&lt;/strong&gt;, 8 KB each by default. Imagine a warehouse of unlabeled boxes. Finding &quot;all orders from customer 42&quot; means opening every box. That is a &lt;strong&gt;sequential scan&lt;/strong&gt;, and it is the default when nothing smarter is available.&lt;/p&gt;
&lt;p&gt;An &lt;strong&gt;index&lt;/strong&gt; is the label system. The standard type is the &lt;strong&gt;B-tree&lt;/strong&gt;, a balanced, sorted tree dating back to 1970. Like a phone book, it lets Postgres jump to the right place in a handful of steps instead of reading everything.&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;composite index&lt;/strong&gt; covers several columns, and &lt;em&gt;order matters&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE INDEX idx_orders_cust_date ON orders (customer_id, created_at);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This helps queries filtering on &lt;code&gt;customer_id&lt;/code&gt;, or on &lt;code&gt;customer_id&lt;/code&gt; plus &lt;code&gt;created_at&lt;/code&gt;. It barely helps a query filtering on &lt;code&gt;created_at&lt;/code&gt; alone, just as a phone book sorted by surname is useless for finding everyone named &quot;Ahmed.&quot;&lt;/p&gt;
&lt;p&gt;The key idea is &lt;strong&gt;selectivity&lt;/strong&gt;: how well a value narrows the search. An index on &lt;code&gt;customer_id&lt;/code&gt; (many distinct values) is sharp. An index on a boolean &lt;code&gt;is_active&lt;/code&gt; where 98% of rows are &lt;code&gt;true&lt;/code&gt; is nearly worthless, because the database would still read most of the table.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Indexes hurt, too.&lt;/strong&gt; Every insert, update, or delete must also update every index. They consume disk and memory, and they slow writes. An index is a trade, not a gift.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;part-iv-the-kitchen-s-brain-query-planning-&quot;&gt;Part IV: The Kitchen's Brain (Query Planning)&lt;/h4&gt;
&lt;p&gt;When SQL arrives, PostgreSQL's &lt;strong&gt;optimizer&lt;/strong&gt; doesn't just run it. It considers many possible strategies, estimates the &lt;strong&gt;cost&lt;/strong&gt; of each using table statistics, and picks the cheapest. Think of a GPS comparing routes.&lt;/p&gt;
&lt;p&gt;You can ask to see the route:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;EXPLAIN SELECT * FROM orders WHERE customer_id = 42;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This shows the &lt;em&gt;plan&lt;/em&gt; and the &lt;em&gt;estimated&lt;/em&gt; costs without running anything. Add &lt;code&gt;ANALYZE&lt;/code&gt; and Postgres actually executes the query and reports real times and row counts:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE customer_id = 42;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Careful:&lt;/strong&gt; &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; really runs the statement. For an &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt;, wrap it in a transaction and &lt;code&gt;ROLLBACK&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The plan names the access method:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sequential scan:&lt;/strong&gt; read the whole table. Fine for small tables, or when you need most rows anyway.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Index scan:&lt;/strong&gt; walk the index, then fetch each matching row. Great for a few rows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bitmap scan:&lt;/strong&gt; collect matching locations from the index, sort them, then read the pages in order. This is the middle path for &quot;more than a few, fewer than most.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For joins, three algorithms compete: &lt;strong&gt;nested loop&lt;/strong&gt; (for each row on the left, look up the right, good for small inputs), &lt;strong&gt;hash join&lt;/strong&gt; (build a lookup table in memory, good for large unsorted inputs), and &lt;strong&gt;merge join&lt;/strong&gt; (zip two sorted lists together).&lt;/p&gt;
&lt;p&gt;The most useful habit is comparing the planner's &lt;em&gt;estimated&lt;/em&gt; rows with the &lt;em&gt;actual&lt;/em&gt; rows. A big gap usually means stale statistics, and &lt;code&gt;ANALYZE table_name;&lt;/code&gt; often fixes it. The optimizer is only as good as its guesses.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;part-v-many-diners-one-kitchen-transactions-amp-concurrency-&quot;&gt;Part V: Many Diners, One Kitchen (Transactions &amp;amp; Concurrency)&lt;/h4&gt;
&lt;p&gt;A &lt;strong&gt;transaction&lt;/strong&gt; bundles steps into one all-or-nothing unit. Databases promise &lt;strong&gt;ACID&lt;/strong&gt;, a term formalized in 1983:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Atomicity:&lt;/strong&gt; all steps happen, or none do.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consistency:&lt;/strong&gt; rules (constraints) always hold.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Isolation:&lt;/strong&gt; concurrent transactions don't trample each other.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Durability:&lt;/strong&gt; once committed, it survives a crash.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PostgreSQL delivers isolation through &lt;strong&gt;MVCC&lt;/strong&gt; (multi-version concurrency control). Instead of making readers wait for writers, it keeps multiple versions of a row. Readers see a consistent snapshot while writers create new versions. Readers don't block writers, and writers don't block readers. The price is cleanup: old versions pile up until &lt;code&gt;VACUUM&lt;/code&gt; removes them.&lt;/p&gt;
&lt;p&gt;Postgres defaults to &lt;strong&gt;Read Committed&lt;/strong&gt; isolation, where each statement sees data committed before it began. &lt;strong&gt;Repeatable Read&lt;/strong&gt; and &lt;strong&gt;Serializable&lt;/strong&gt; are stricter, and Serializable can abort transactions with errors you must retry.&lt;/p&gt;
&lt;p&gt;Snapshots don't stop every conflict. Take a classic &lt;strong&gt;race condition&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;acct = Account.objects.get(pk=1)
acct.balance -= 50      # two requests both read 100, both write 50
acct.save()             # one withdrawal vanishes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The fix is to lock the row:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.db import transaction

with transaction.atomic():
    acct = Account.objects.select_for_update().get(pk=1)
    acct.balance -= 50
    acct.save()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;select_for_update()&lt;/code&gt; takes a &lt;strong&gt;row lock&lt;/strong&gt;, so the second request waits its turn. For simple arithmetic, an &lt;code&gt;F(&quot;balance&quot;) - 50&lt;/code&gt; expression pushes the math into the database itself and is even safer.&lt;/p&gt;
&lt;p&gt;Locks bring the risk of &lt;strong&gt;deadlocks&lt;/strong&gt;: transaction A holds row 1 and wants row 2, while B holds row 2 and wants row 1. It is two people in a narrow hallway, each waiting for the other to step aside. Postgres detects this (after &lt;code&gt;deadlock_timeout&lt;/code&gt;, 1 second by default) and kills one transaction. The cure is to &lt;strong&gt;lock rows in a consistent order&lt;/strong&gt; everywhere.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;part-vi-how-to-measure-honestly&quot;&gt;Part VI: How to Measure Honestly&lt;/h4&gt;
&lt;p&gt;I'm deliberately not printing benchmark numbers here. Figures from my machine would be meaningless on yours, and invented ones would be worse. What matters is the method:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fix the environment.&lt;/strong&gt; Same hardware, same Postgres version, same config, warm cache &lt;em&gt;and&lt;/em&gt; cold cache runs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scale the dataset.&lt;/strong&gt; Test at 1K, 100K, and 10M rows. Many problems only appear at scale, and a missing index looks harmless on a small table.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measure several things:&lt;/strong&gt; latency (report p50 and p95, not just the average), throughput (queries per second under concurrency), memory (&lt;code&gt;tracemalloc&lt;/code&gt;), and &lt;strong&gt;query count&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compare like with like:&lt;/strong&gt; ORM vs equivalent hand-written SQL, and indexed vs unindexed, changing &lt;em&gt;one variable at a time&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import time
from django.db import connection
from django.test.utils import CaptureQueriesContext

with CaptureQueriesContext(connection) as ctx:
    start = time.perf_counter()
    list(Order.objects.select_related(&quot;customer&quot;)[:1000])
    elapsed = time.perf_counter() - start

print(len(ctx), &quot;queries,&quot;, round(elapsed * 1000, 1), &quot;ms&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it many times, discard the first (cold) run, and keep the distribution.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;part-vii-findings&quot;&gt;Part VII: Findings&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;What actually causes slowness.&lt;/strong&gt; In most web applications, the usual suspects are, in rough order of how often they appear: too many queries (N+1), missing or unusable indexes on filtered columns, fetching far more rows or columns than needed, and long-held locks. Rarely is the culprit that Python is &quot;too slow&quot; at building SQL.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Where the ORM is expensive.&lt;/strong&gt; Materializing huge result sets into model instances, hidden per-row queries, and ORM-generated queries nobody has ever inspected.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Where ORM optimizations matter.&lt;/strong&gt; &lt;code&gt;select_related&lt;/code&gt;, &lt;code&gt;prefetch_related&lt;/code&gt;, &lt;code&gt;only()&lt;/code&gt;/&lt;code&gt;defer()&lt;/code&gt;, &lt;code&gt;values()&lt;/code&gt;, &lt;code&gt;bulk_create()&lt;/code&gt;, &lt;code&gt;exists()&lt;/code&gt; and &lt;code&gt;iterator()&lt;/code&gt;. These change the &lt;em&gt;shape&lt;/em&gt; of the work, and that is where the big wins live.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Where indexes matter.&lt;/strong&gt; On columns used in &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt;, and &lt;code&gt;ORDER BY&lt;/code&gt;, in large tables, with high selectivity. Always confirm with &lt;code&gt;EXPLAIN&lt;/code&gt; that the index is actually used.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When raw SQL is justified.&lt;/strong&gt; Complex reporting, window functions or CTEs the ORM expresses awkwardly, or a measured hot path where the ORM's generated query is demonstrably poor. Use parameterized queries, never string formatting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The trade-offs.&lt;/strong&gt; Readability and safety against fine control. Fast reads against slower writes (indexes). Strict isolation against throughput.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Myths worth retiring:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&quot;The ORM is slow.&quot;&lt;/em&gt; Usually the &lt;em&gt;queries&lt;/em&gt; are slow, not the ORM.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&quot;Raw SQL is always faster.&quot;&lt;/em&gt; Not if it has the same plan and the same number of round trips.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&quot;More indexes, more speed.&quot;&lt;/em&gt; Each one taxes every write.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&quot;An index will always be used.&quot;&lt;/em&gt; The planner may rightly ignore it.&lt;/li&gt;
&lt;li&gt;_&quot;&lt;code&gt;select_related&lt;/code&gt; everywhere.&quot;_ Joining tables you never read just makes the kitchen work harder.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;The one-sentence takeaway:&lt;/strong&gt; measure first, count your queries, read your plans, and only then optimize.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h4 id=&quot;references&quot;&gt;References&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.djangoproject.com/en/stable/topics/db/optimization/&quot;&gt;Django Docs: Database access optimization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.djangoproject.com/en/stable/ref/models/querysets/#select-for-update&quot;&gt;Django Docs: &lt;code&gt;select_for_update()&lt;/code&gt; and QuerySet API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/docs/current/using-explain.html&quot;&gt;PostgreSQL Docs: Using EXPLAIN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/docs/current/indexes-types.html&quot;&gt;PostgreSQL Docs: Index Types (B-tree)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/docs/current/mvcc-intro.html&quot;&gt;PostgreSQL Docs: Concurrency Control and MVCC&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;find-me-across-the-web-&quot;&gt;Find me across the web:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Portfolio:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;http://ahmershah.dev&quot;&gt;&lt;strong&gt;ahmershah.dev&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Crunchbase:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.crunchbase.com/person/syed-ahmer-shah&quot;&gt;&lt;strong&gt;@syed-ahmer-shah&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Crunchbase Company:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.crunchbase.com/organization/syedahmershah&quot;&gt;&lt;strong&gt;@syedahmershah&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clutch:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://clutch.co/profile/syed-ahmer-shah&quot;&gt;&lt;strong&gt;@syed-ahmer-shah&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tech Behemoth:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://techbehemoths.com/company/syed-ahmer-shah&quot;&gt;&lt;strong&gt;@syed-ahmer-shah&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Design Rush:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.designrush.com/agency/profile/syed-ahmer-shah&quot;&gt;&lt;strong&gt;@syed-ahmer-shah&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Edverise:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://edverise.com/profile/syed-ahmer-shah&quot;&gt;&lt;strong&gt;@syed-ahmer-shah&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Trust Pilot:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.trustpilot.com/review/ahmershah.dev&quot;&gt;&lt;strong&gt;@&lt;/strong&gt;&lt;/a&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;http://ahmershah.dev&quot;&gt;&lt;strong&gt;ahmershah.dev&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/syedahmershah&quot;&gt;&lt;strong&gt;Syed Ahmer Shah&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ahmershahdev&quot;&gt;&lt;strong&gt;@ahmershahdev&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AWS Builder Profile:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://builder.aws.com/community/syedahmershah&quot;&gt;&lt;strong&gt;@syedahmershah&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DEV:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/syedahmershah&quot;&gt;&lt;strong&gt;@syedahmershah&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Medium:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/@syedahmershah&quot;&gt;&lt;strong&gt;@syedahmershah&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hashnode:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://hashnode.com/@syedahmershah&quot;&gt;&lt;strong&gt;@syedahmershah&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Substack:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://substack.com/@syedahmershah&quot;&gt;&lt;strong&gt;@syedahmershah&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;HackerNoon:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://hackernoon.com/u/syedahmershah&quot;&gt;&lt;strong&gt;@syedahmershah&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Substack:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://syedahmershah.substack.com/&quot;&gt;&lt;strong&gt;@syedahmershah&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Facebook:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.facebook.com/ahmershahdev&quot;&gt;&lt;strong&gt;@ahmershahdev&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Linkedin Page:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://linkedin.com/company/syedahmershah&quot;&gt;&lt;strong&gt;@syedahmershah&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;YouTube:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/@ahmershahdev&quot;&gt;&lt;strong&gt;@ahmershahdev&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Instagram:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.instagram.com/ahmershahdev/&quot;&gt;&lt;strong&gt;@ahmershahdev&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;TikTok:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.tiktok.com/@ahmershahdev&quot;&gt;&lt;strong&gt;@ahmershahdev&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30144/inside-the-database-stack</guid>
<pubDate>Wed, 07 Oct 2026 13:42:37 +0000</pubDate>
</item>
<item>
<title>Reverse-Engineering Messy Databases: My End-to-End Data Audits Across 17,000+ Relational Schema Logs</title>
<link>https://coderlegion.com/28337/reverse-engineering-messy-databases-end-end-data-audits-across-000-relational-schema-logs</link>
<description>&lt;h3 id=&quot;from-raw-logs-to-business-strategy-how-i-taught-myself-data-engineering-amp-analytics&quot;&gt;From Raw Logs to Business Strategy: How I Taught Myself Data Engineering &amp;amp; Analytics&lt;/h3&gt;
&lt;p&gt;I didn’t take the traditional path into data. Over the past 12 months, I bypassed the university lecture halls to teach myself relational database architecture, pipeline optimization, and growth analytics by diving straight into raw datasets.&lt;/p&gt;
&lt;p&gt;I look at data through an engineering and business lens. Instead of building passive charts that just look pretty, I query transaction logs and user event data to find hidden operational friction, map out user behavior patterns, and isolate exactly where systems are dropping value. I build end-to-end data systems: raw SQL schemas, data sanitization scripts, interactive Power BI dashboards, and single-page executive reports.&lt;/p&gt;
&lt;p&gt;Below are the architectural breakdowns, technical challenges, and business impact metrics of the four core data infrastructure projects I have engineered from scratch.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;project-1-stockpulse-multi-regional-supply-chain-audit&quot;&gt;Project 1: StockPulse — Multi-Regional Supply Chain Audit&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Technical Stack:&lt;/strong&gt; MySQL (Mathematical Window Functions), Power BI, Advanced Excel&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub Codebase &amp;amp; Dashboard Layout:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hafsamotiwala/Enterprise-Procurement-Supply-Chain-Resilience-Pipeline&quot;&gt;[LINK]&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-operational-bottleneck&quot;&gt;The Operational Bottleneck&lt;/h4&gt;
&lt;p&gt;Horizon Marketplace faced shrinking profit margins from slow-moving warehouse stock, while simultaneous supplier shipping delays caused high-demand items to go out of stock on our mobile application. Traditional procurement tracking models failed because they relied on simple historical averages, which immediately miscalculate when data hits high seasonal sales volatility spikes.&lt;/p&gt;
&lt;h4 id=&quot;the-technical-implementation&quot;&gt;The Technical Implementation&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Audited 17,646 operational and sales ledger transactions&lt;/strong&gt; using MySQL window functions to calculate cumulative revenue contributions per SKU.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coded a predictive restocking matrix script&lt;/strong&gt; utilizing sales volatility standard deviations and rolling manufacturer lead timelines.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Formulated dynamic Safety Stock buffers&lt;/strong&gt; and programmed automated database Reorder Point triggers to flag supplier order alerts before shelves hit zero.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-business-impact-metrics&quot;&gt;The Business Impact Metrics&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ABC inventory segmentation&lt;/strong&gt; proved that exactly 23 Class A items drive 80% of total marketplace revenue, showing that the remaining catalog space was tying up critical capital.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quantified a hidden revenue bleed of $63,897.26&lt;/strong&gt; caused by a single high-demand apparel product sitting entirely out of stock for 42 days due to upstream logistics bottlenecks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transitioned a multi-regional operation&lt;/strong&gt; from manual, time-intensive spreadsheets to an automated, predictive data framework.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;project-2-funnelopt-habitarcade-b2c-telemetry-audit&quot;&gt;Project 2: FunnelOpt — HabitArcade B2C Telemetry Audit&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Technical Stack:&lt;/strong&gt; MySQL (Session Overrides), Funnel Analysis, User Lifecycle Mapping&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub Codebase &amp;amp; Database Logs:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hafsamotiwala/-HabitArcade-Enterprise-B2C-SaaS-Funnel-Telemetry-Analysis&quot;&gt;[LINK]&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-operational-bottleneck-1&quot;&gt;The Operational Bottleneck&lt;/h4&gt;
&lt;p&gt;A B2C application tracking engine suffered from severe data execution drops where product behavioral logs completely failed to map user activation patterns. The relational schema dropped child telemetry records because they were importing prior to the absolute database finalization of the parent account index logs.&lt;/p&gt;
&lt;h4 id=&quot;the-technical-implementation-1&quot;&gt;The Technical Implementation&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Audited full-funnel growth logs&lt;/strong&gt; across 6 independent acquisition pipelines to analyze customer conversion latency and drop-offs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Structured specific MySQL session overrides&lt;/strong&gt; to temporarily bypass structural foreign key constraints.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Forced clean data synchronization&lt;/strong&gt; across child records to completely restore parent-child index synchronization without corrupting historical system logs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-business-impact-metrics-1&quot;&gt;The Business Impact Metrics&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Isolated a systemic “Streak-Breaking Crash Tax”&lt;/strong&gt; where application sync errors wiped 30-day user milestones, triggering immediate 1-star reviews and an active 6% to 7% monthly subscriber bleed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exposed an “Inverted Value Curve”&lt;/strong&gt; revealing that high-value annual checkout tiers underperformed monthly subscriptions due to silent gateway billing processor failures blocking top-priced subscription cash settlement.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;project-3-leakstop-olist-b2b-revenue-leakage-amp-onboarding-audit&quot;&gt;Project 3: LeakStop — Olist B2B Revenue Leakage &amp;amp; Onboarding Audit&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Technical Stack:&lt;/strong&gt; MySQL (Schema Engineering), Power BI, Business Intelligence&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub Codebase &amp;amp; Analytical Readme:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hafsamotiwala/B2B-Saas-Funnel-Optimization-Audit&quot;&gt;[LINK]&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-operational-bottleneck-2&quot;&gt;The Operational Bottleneck&lt;/h4&gt;
&lt;p&gt;Cross-vendor data duplication across digital marketplace ledgers was heavily skewing gross reporting metrics. Standard relational database joins double-counted total transactions when a customer order spanned multiple independent business vendors in a single checkout loop.&lt;/p&gt;
&lt;h4 id=&quot;the-technical-implementation-2&quot;&gt;The Technical Implementation&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Engineered robust MySQL ingestion and data sanitization scripts&lt;/strong&gt; to enforce strict primary key integrity down to individual item vectors, removing all reporting bias.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Processed user account lifecycles&lt;/strong&gt; to calculate onboarding completion latency across specific B2B verticals.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Programmed a real-time database filter&lt;/strong&gt; to flag slipping user accounts precisely on Day 16 of transaction inactivity.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-business-impact-metrics-2&quot;&gt;The Business Impact Metrics&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Isolated an annual revenue leakage of R$ 600,600&lt;/strong&gt; (approximately $119,175 USD) stemming from 462 merchant accounts that stalled out completely during the onboarding phase.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exposed an invisible latency anomaly&lt;/strong&gt; where logistically complex business profiles stalled for an average of 112 days in domain routing and tax mapping phases.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Implemented a Day 16 early-warning database filter&lt;/strong&gt; to flag at-risk accounts before they reached a terminal, zero-recovery retention cliff.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;project-4-cohortpace-saas-behavioral-retention-engine&quot;&gt;Project 4: CohortPace — SaaS Behavioral Retention Engine&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Technical Stack:&lt;/strong&gt; MySQL (Subqueries &amp;amp; Relational Joins), Cohort Modeling, Data Analytics&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub Codebase &amp;amp; Technical Report:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hafsamotiwala/SaaS-User-Behavior-Analysis-Email-Creator-Platforms-&quot;&gt;[LINK]&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-operational-bottleneck-3&quot;&gt;The Operational Bottleneck&lt;/h4&gt;
&lt;p&gt;Initial application dashboards focused blindly on feature exploration depth, which completely masked why healthy-looking active user profiles were suddenly canceling subscriptions. Traditional active-user metrics acted as a lagging indicator, hiding friction because churning accounts remain active inside broken workflows right up to their exit timelines.&lt;/p&gt;
&lt;h4 id=&quot;the-technical-implementation-3&quot;&gt;The Technical Implementation&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Modeled simulated user cohort transactions&lt;/strong&gt; across multi-platform SaaS behavioral profiles to analyze the core drivers of long-term Monthly Recurring Revenue (MRR).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Used advanced MySQL subqueries and relational joins&lt;/strong&gt; to reconstruct full lifecycle interaction sequences.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mapped user interaction velocity trends&lt;/strong&gt; against subscription lifecycles to isolate clear behavioral churn signatures.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-business-impact-metrics-3&quot;&gt;The Business Impact Metrics&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Proved through data pacing&lt;/strong&gt; sending signals that churning users maintain consistent interaction velocity close to their exit date, showing churn is driven by late-stage workflow utility limits rather than gradual disengagement.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Demonstrated that long-term user monetization&lt;/strong&gt; is strictly dependent on habit formation within a single core workflow rather than broad, multi-feature platform exploration.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion-turning-code-into-business-strategy&quot;&gt;Conclusion: Turning Code Into Business Strategy&lt;/h3&gt;
&lt;p&gt;I don’t need a playbook to get started, and I don’t need a manager to hand me a static checklist. I find the question, ingest the data, clean the schemas, model the answer, and present the metric that directly protects business margins.&lt;/p&gt;
&lt;p&gt;I am actively available for analytical remote contract roles, standalone data audits, and freelance pipeline optimization. If your startup is scaling fast and dealing with messy relational pipelines, let’s connect and fix your backend infrastructure loops.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt; Interactive Portfolio Hub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://lucky-bit-036.notion.site/HAFSA-5fd489cedd70459ca0237c36a168f30a&quot;&gt;[LINK]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt; Contact:&lt;/strong&gt; motiwala603@gmail.com&lt;/li&gt;
&lt;li&gt;&lt;strong&gt; Connect with me:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/hafsa-motiwala-a9608835b/&quot;&gt;[LinkedIn]&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Open to analyst remote roles across product, growth, and business analytics. Based in Pakistan, available worldwide.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28337/reverse-engineering-messy-databases-end-end-data-audits-across-000-relational-schema-logs</guid>
<pubDate>Wed, 23 Sep 2026 11:56:39 +0000</pubDate>
</item>
<item>
<title>Speed Up Your Queries: A Plain-English Guide to Database Indexing</title>
<link>https://coderlegion.com/25254/speed-up-your-queries-a-plain-english-guide-to-database-indexing</link>
<description>&lt;h4 id=&quot;what-is-database-indexing-&quot;&gt;What is Database Indexing?&lt;/h4&gt;
&lt;p&gt;Database indexing is a technique used by software systems to speed up the retrieval of data from a database table. By creating a separate, organized pointer structure, the database can locate specific records without having to search through every single row of data. Think of it as a specialized shortcut map for your digital filing cabinet.&lt;/p&gt;
&lt;h4 id=&quot;the-library-card-catalog-analogy&quot;&gt;The Library Card Catalog Analogy&lt;/h4&gt;
&lt;p&gt;Imagine walking into a massive metropolitan library containing 100,000 books. If you want to find a book titled &quot;The Midnight Mystery&quot; and there is no catalog or organization system, you would have to start at the first shelf in the corner and look at every single book cover one by one until you find it. This slow, painful process is what a database does during a &quot;full table scan.&quot;&lt;/p&gt;
&lt;p&gt;Now, imagine the library has an alphabetical card catalog. You walk straight to the drawer marked &quot;M,&quot; find the index card for &quot;The Midnight Mystery,&quot; and read the card. It tells you exactly which aisle, shelf, and position the book is on. You walk directly there and grab it in seconds. In this scenario, the card catalog is the index, the books are the rows of data in your table, and the physical location is the memory address of the database record.&lt;/p&gt;
&lt;h4 id=&quot;why-it-matters-daily-in-tech&quot;&gt;Why It Matters Daily in Tech&lt;/h4&gt;
&lt;p&gt;In the tech industry, engineers use database indexing to prevent application lag and catastrophic server crashes. Without indexes, as a company's database grows from thousands of rows to millions, simple daily actions like logging in, searching for an item, or pulling up a user profile would take seconds or even minutes instead of milliseconds.&lt;/p&gt;
&lt;p&gt;Slow database queries hog the system's central processing unit (CPU) and memory, causing other requests to back up and eventually freeze the entire application. By adding appropriate indexes, developers keep application programming interfaces (APIs) lightning-fast, reduce cloud hosting bills (because servers do not have to work as hard), and ensure a smooth user experience. However, engineers must find a balance: every index takes up storage space and slows down write operations (like adding or updating data), because the database has to update both the main table and the index every single time a change is made.&lt;/p&gt;
&lt;h4 id=&quot;how-it-works-in-practice&quot;&gt;How It Works in Practice&lt;/h4&gt;
&lt;p&gt;Here is a simple example in SQL (Structured Query Language). Imagine a system trying to look up a user by their email address.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Without an index, the database must scan every row in the users table
SELECT * FROM users WHERE email = 'developer@example.com';

-- To speed this query up, we create an index on the email column
CREATE INDEX idx_users_email ON users(email);

-- Now, the database uses the index to jump directly to the correct user
SELECT * FROM users WHERE email = 'developer@example.com';
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-takeaway&quot;&gt;The Takeaway&lt;/h4&gt;
&lt;p&gt;Database indexing is the ultimate balancing act of software performance optimization. It turns grueling, resource-heavy data searches into near-instant lookups, keeping applications snappy as they scale up to handle millions of users. Just remember that indexes are not free; treat them like directory signs in a physical store—use them where visitors frequently get lost, but do not clutter every wall with them, or you will slow down the inventory restocking process.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;resources&quot;&gt;Resources&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Saurav-TB-Pandey/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;react-hook-lab:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;npm package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connect with me on LinkedIn:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/pandeysaurav/&quot;&gt;Saurav Pandey&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/08/the-secret-to-fast-apps-understanding.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/saurav_tb_pandey/speed-up-your-queries-a-plain-english-guide-to-database-indexing-21a3&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25254/speed-up-your-queries-a-plain-english-guide-to-database-indexing</guid>
<pubDate>Fri, 11 Sep 2026 02:43:19 +0000</pubDate>
</item>
<item>
<title>Three Steps That Decide Whether Plain English Database Queries Actually Work</title>
<link>https://coderlegion.com/25884/three-steps-that-decide-whether-plain-english-database-queries-actually-work</link>
<description>&lt;p&gt;Most of us here have either built a text-to-SQL layer or been asked to. The demos always look great and the production version is the one that quietly returns the wrong number, so here is what actually separates the two.&lt;/p&gt;
&lt;h3 id=&quot;the-schema-is-the-feature-not-the-prompt&quot;&gt;The Schema Is The Feature, Not The Prompt&lt;/h3&gt;
&lt;p&gt;A natural language query layer is not a prompt engineering problem. When you connect a database, the model has to read the whole schema first: every table, every column, every data type, every foreign key. That is what lets it decide which tables to touch, which columns to filter on, and how to join them.&lt;/p&gt;
&lt;p&gt;Skip that step and you still get SQL back. It runs, it returns rows, and it is wrong in a way nobody catches, because the join went through the wrong key. The systems that hold up are the ones that expose the generated SQL so a human can spot-check it, and that read relationships rather than guessing them from column names. There is a longer walkthrough of how that works in practice on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.aiappsapi.com/articles/aidatabases/naturallanguagequery.php&quot;&gt;asking your database questions in plain English&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;dirty-data-breaks-it-before-the-model-does&quot;&gt;Dirty Data Breaks It Before The Model Does&lt;/h3&gt;
&lt;p&gt;The second failure mode is upstream. Names stored three different ways, phone numbers in four formats, the same customer entered twice with a typo, blank fields where a required value should be. Your query is correct and your answer is still wrong, because COUNT DISTINCT on a column with fuzzy duplicates is not counting what you think.&lt;/p&gt;
&lt;p&gt;This is the least glamorous part and it is where most of the accuracy lives. Fuzzy matching to find near-duplicate records, normalizing formats across the table, validating each field against the rules it should obey, flagging rows with missing required values instead of silently averaging around them. A practical rundown is in &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.aiappsapi.com/articles/aidatabases/cleandata.php&quot;&gt;cleaning data with AI&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;let-the-model-find-what-you-did-not-ask-about&quot;&gt;Let The Model Find What You Did Not Ask About&lt;/h3&gt;
&lt;p&gt;The third step is the one teams skip entirely. Every query you write encodes something you already suspected. Anomaly detection covers the rest: point anomalies like an order fifty times the average, contextual ones like revenue that is normal for a retailer and impossible for a two person consultancy, and collective ones where ten individually boring transactions land in the same minute.&lt;/p&gt;
&lt;p&gt;That last category is the argument for running it at all, because no dashboard you would have thought to build catches it. Worth reading &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.aiappsapi.com/articles/dataanalysis/anomalydetection.php&quot;&gt;how AI anomaly detection actually runs&lt;/a&gt; if you are adding this to an existing stack.&lt;/p&gt;
&lt;h3 id=&quot;the-takeaway-1&quot;&gt;The Takeaway&lt;/h3&gt;
&lt;p&gt;Plain English querying is not one feature, it is three: clean the data, give the model the real schema, then let it surface what your questions never covered. Ship it in that order and the demo and the production system finally agree with each other.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25884/three-steps-that-decide-whether-plain-english-database-queries-actually-work</guid>
<pubDate>Mon, 31 Aug 2026 02:43:48 +0000</pubDate>
</item>
<item>
<title>A SQL interview is a translation test, not a syntax test</title>
<link>https://coderlegion.com/25140/a-sql-interview-is-a-translation-test-not-a-syntax-test</link>
<description>&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://four-leaf.ai/blog/sql-interview-guide&quot;&gt;Four-Leaf's SQL Interview Guide&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A SQL interview is a translation test. The syntax is the easy half, and most candidates who fail were fluent in SQL. What sinks them is starting to type before the question is pinned down, or writing a correct query that answers something nobody asked. The round scores three things: whether you clarify before you write, whether your query is correct on the edge cases, and whether you can explain your reasoning while your hands are moving. It also looks meaningfully different depending on whether you're interviewing as a data analyst, a data scientist, or an engineer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What does a SQL interview test?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The round tests whether you can turn an underspecified business question into a query that survives contact with real data. Interviewers rarely hand you a clean specification, because the job never does either.&lt;/p&gt;
&lt;p&gt;SQL is worth preparing for on volume alone. The Stack Overflow 2025 Developer Survey found 58.6% of developers had used SQL in the past year, making it the third most-used language behind JavaScript at 66% and HTML/CSS at 61.9%, and ahead of Python at 57.9%. It shows up in loops for roles that aren't nominally data roles at all, which is part of why candidates underprepare for it.&lt;/p&gt;
&lt;p&gt;The tell that separates a strong candidate is what happens in the first sixty seconds. Weak candidates read the prompt and start typing. Strong candidates restate the question, ask what counts as an active user or a completed order, confirm whether the answer should include rows with nulls, and only then write. That opening exchange is often worth more to the interviewer than the query itself, because it's the part of the job that can't be looked up.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What kinds of SQL questions come up?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Three shapes cover most of what gets asked, and they escalate in a predictable order.&lt;/p&gt;
&lt;p&gt;The first is a join and aggregation question. Given two or three tables, produce a count or a sum grouped by something. These look trivial and catch people on join type, because an inner join silently drops the users who never ordered, and the question usually wanted them counted as zero.&lt;/p&gt;
&lt;p&gt;The second is a window function question. Rank purchases per customer, find each user's second transaction, compute a running total, or calculate a month-over-month change. This is where a lot of loops separate candidates, because window functions are the boundary between people who write SQL occasionally and people who use it as a primary tool.&lt;/p&gt;
&lt;p&gt;The third is an open business question against a schema you've just been shown. &quot;Tell me whether retention improved after the March release.&quot; There's no single correct query. The interviewer is watching how you decompose the question, what you decide retention means, and whether you notice that the March cohort has less time to churn than the February one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How is a SQL interview scored?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Three dimensions, and they map to those question shapes. It helps to run each one against the questions the round keeps returning to: give me a count by group, rank something per user, and answer this vague business question.&lt;/p&gt;
&lt;p&gt;Do you clarify before you write? A passing answer asks one question about the schema. A strong answer names the ambiguity that would change the query and resolves it. &quot;Does an active user mean any event in the window, or a purchase?&quot; is the difference between two very different numbers, and interviewers plant that ambiguity on purpose.&lt;/p&gt;
&lt;p&gt;Is your query correct on the edges? A passing answer runs. A strong answer accounts for nulls, duplicates, ties in a ranking, and rows on the boundary of a date range. Say the edge case out loud when you handle it, because a silent LEFT JOIN looks identical to a lucky one.&lt;/p&gt;
&lt;p&gt;Can you narrate while you write? A passing answer goes quiet and produces a query. A strong answer talks through the plan first, writes, then checks the result against a rough expectation out loud. Interviewers score reasoning they can hear, and a correct query delivered in silence gets a weaker write-up than a slightly imperfect one that was explained.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How does a SQL interview differ for a data analyst versus a data scientist?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Same language, meaningfully different round, and mixing them up is the most common preparation mistake in the function.&lt;/p&gt;
&lt;p&gt;Data analyst. The emphasis is breadth and business translation. Expect more questions, less depth per question, and heavy weighting on whether your numbers would hold up in front of a stakeholder. Definitional precision matters more than query elegance. You're likely to be asked what a metric should mean before you're asked to compute it, and an answer that flags a misleading denominator scores higher than one that optimizes a join.&lt;/p&gt;
&lt;p&gt;Data scientist. The emphasis shifts toward experiment and cohort logic. Expect fewer questions with more depth, and expect at least one that touches an A/B test readout, a cohort definition, or a sampling problem hiding inside the SQL. A question about whether a March cohort had less time to churn than a February one is a data scientist question. Correctness under a statistical framing is what's being scored, not query volume.&lt;/p&gt;
&lt;p&gt;Analytics engineer. The round moves toward modeling. Expect questions about how you'd structure the model rather than write a one-off answer, including idempotency, incremental logic, and what happens when the query is rerun.&lt;/p&gt;
&lt;p&gt;Software engineer. SQL usually appears inside a broader technical round rather than on its own, and the framing is performance and correctness at scale. Expect indexing, query plans, and the consequences of a full table scan rather than business definitions.&lt;/p&gt;
&lt;p&gt;Product management. Increasingly common and usually light. The bar is whether you can pull your own numbers without asking an analyst, so expect one aggregation question and no window functions.&lt;/p&gt;
&lt;p&gt;Four-Leaf covers the wider data loop in how to prepare for a data science interview.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is overrated&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Memorizing exotic syntax. Recursive CTEs and pivot tricks appear in practice sets far more often than in interviews, and time spent there is time not spent on window functions, which appear constantly.&lt;/p&gt;
&lt;p&gt;Speed is the other overrated thing. Candidates rush because they assume the clock is the test, then miss a join type. The interviewer is almost always more interested in whether you caught the null case than in whether you finished ninety seconds early.&lt;/p&gt;
&lt;p&gt;Using an AI assistant to get through this round is a losing trade. CodeSignal reported in February 2026 that cheating and fraud attempts on proctored assessments more than doubled, from 16% in 2024 to 35% in 2025. Interviewers noticed. A 2025 interviewing.io survey of 67 of them, 52 at FAANG companies, found 81% suspected candidates of using AI and about a third had caught someone at it. The visible response has been more live, narrated rounds, which is precisely the format where a candidate who can't explain their own query falls apart.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A five-step playbook for a SQL round&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Drill window functions until they're automatic. ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, and a running sum. These carry more interview weight per hour of study than anything else in SQL.&lt;/p&gt;
&lt;p&gt;Practice restating the question out loud before writing. Give yourself a rule that you don't type until you've named one ambiguity.&lt;/p&gt;
&lt;p&gt;Build a null and duplicate checklist. Before you call a query done, ask what happens to rows with nulls, rows that appear twice, and rows exactly on the boundary date.&lt;/p&gt;
&lt;p&gt;Do at least three problems on a whiteboard or plain text editor. Autocomplete hides gaps that a bare editor exposes, and many live rounds use a bare editor.&lt;/p&gt;
&lt;p&gt;Narrate a solved problem end to end. Explaining a query you already understand is a separate skill from writing it, and it's the one being scored. A voice mock interview is a reasonable way to rehearse the narration, since the failure mode is going quiet under pressure rather than not knowing the syntax.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Where this is heading&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;SQL rounds are getting more conversational, and the reason is the same force reshaping every technical loop. As assistants make it trivial to produce a syntactically correct query, interviewers move the bar to the part that's harder to fake, which is deciding what to compute and defending the choice. Four-Leaf's AI-Era Hiring Index, a study of 3,502 open roles at 16 top tech employers, found LLM or foundation-model experience listed in 57% of data and machine learning job descriptions against 21% of engineering ones, so the expectation that data candidates work alongside these tools is already written into the postings.&lt;/p&gt;
&lt;p&gt;That direction rewards the candidate who understood the question. The syntax was never the hard part, and it's about to matter even less. Where this round sits in the wider sequence is covered in how an onsite interview loop actually works. To rehearse it out loud rather than read about it, start with eight coding interview platforms, compared.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25140/a-sql-interview-is-a-translation-test-not-a-syntax-test</guid>
<pubDate>Sat, 22 Aug 2026 14:27:55 +0000</pubDate>
</item>
<item>
<title>We caught a SQL injection with an offline AI security scanner — here's the exact query it found</title>
<link>https://coderlegion.com/25124/we-caught-sql-injection-with-an-offline-ai-security-scanner-heres-the-exact-query-it-found</link>
<description>&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://youtu.be/GknBmsqaFk4&quot;&gt;https://youtu.be/GknBmsqaFk4&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-bug&quot;&gt;The bug&lt;/h3&gt;
&lt;p&gt;Here's a login endpoint from a small Express demo app&lt;br&gt;
(&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/sendwavehub/scan-target-demo-apps&quot;&gt;&lt;code&gt;scan-target-demo-apps/apps/01-sql-injection&lt;/code&gt;&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;app.post('/login', (req, res) =&amp;gt; {
  const { username = '', password = '' } = req.body;
  const query = `SELECT id, username, email, is_admin FROM users
                  WHERE username = '${username}' AND password = '${password}'`;
  const result = db.exec(query);
  // ...
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You've seen this shape before. &lt;code&gt;username&lt;/code&gt; and &lt;code&gt;password&lt;/code&gt; go straight into the SQL string — no&lt;br&gt;
parameter binding, no escaping. Submit &lt;code&gt;admin' -- &lt;/code&gt; as the username and anything as the password,&lt;br&gt;
and the query becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT id, username, email, is_admin FROM users
WHERE username = 'admin' -- ' AND password = 'anything'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-- &lt;/code&gt; comments out the rest of the line. The password check never runs. &lt;code&gt;{&quot;ok&quot;: true}&lt;/code&gt;, logged in&lt;br&gt;
as &lt;code&gt;admin&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Classic CWE-89, OWASP A03:2021. Nothing novel here — the interesting part is what caught it.&lt;/p&gt;
&lt;h3 id=&quot;what-actually-caught-it&quot;&gt;What actually caught it&lt;/h3&gt;
&lt;p&gt;We ran this target through &lt;strong&gt;AI Security Studio&lt;/strong&gt;, an offline security research platform we've&lt;br&gt;
been building. No cloud calls, no code leaves the machine — everything you're about to read&lt;br&gt;
happened on a laptop with no network access to anywhere but &lt;code&gt;localhost:3001&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The important design decision: &lt;strong&gt;the LLM never sees raw source code or raw HTTP traffic.&lt;/strong&gt;&lt;br&gt;
The pipeline looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Parser → Rule Engine → Knowledge Retrieval → Summarization → LLM → Reasoning → Finding → Report
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A deterministic parser and rule engine do the actual &lt;em&gt;finding&lt;/em&gt;. The local LLM only explains&lt;br&gt;
&lt;em&gt;already-discovered&lt;/em&gt; evidence — it's not asked to eyeball a codebase and guess where the bugs&lt;br&gt;
might be. If the evidence isn't there, the platform is built to say &quot;Needs Manual Verification&quot;&lt;br&gt;
instead of inventing a conclusion.&lt;/p&gt;
&lt;p&gt;For this run, that meant:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Point the scanner at &lt;code&gt;http://localhost:3001&lt;/code&gt; as scope.&lt;/li&gt;
&lt;li&gt;Let it run a real passive crawl — no exploitation, just discovery.&lt;/li&gt;
&lt;li&gt;Run a source scan against the target's own directory.&lt;/li&gt;
&lt;li&gt;The rule engine flags the string-concatenated query pattern in &lt;code&gt;server.js:56&lt;/code&gt; and the missing&lt;br&gt;
auth check on the state-changing &lt;code&gt;POST /login&lt;/code&gt; route.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The generated report includes the actual vulnerable line as evidence, not a generic &quot;possible SQL&lt;br&gt;
injection&quot; label:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Source preview.
  app.post('/login', (req, res) =&amp;gt; {
    const { username = '', password = '' } = req.body;
    const query = `SELECT id, username, email, is_admin FROM users WHERE username = '${username}' AND password = '${password}'`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Risk: High. Root cause: CWE-89 (and a separate CWE-306 finding for the missing authorization&lt;br&gt;
check). Suggested fix: parameterized queries. Confidence explicitly marked — some findings from&lt;br&gt;
this pass are &quot;Confirmed,&quot; others are flagged &quot;Needs Manual Verification&quot; rather than asserted,&lt;br&gt;
because header/config-level evidence alone doesn't prove exploitability the way a manual&lt;br&gt;
Active Test / Repeater run does.&lt;/p&gt;
&lt;h3 id=&quot;why-offline-is-the-actual-point&quot;&gt;Why &quot;offline&quot; is the actual point&lt;/h3&gt;
&lt;p&gt;It's easy to gloss over &quot;runs locally&quot; as a checkbox feature. In practice it means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You can point it at unreleased/internal code without a vendor's cloud pipeline ever touching it.&lt;/li&gt;
&lt;li&gt;The rule engine — not the LLM — does secret detection, JWT/header/cookie analysis, and pattern&lt;br&gt;
matching, so results are reproducible and don't drift between runs.&lt;/li&gt;
&lt;li&gt;The LLM's job shrinks to something it's actually reliable at: turning structured findings into a&lt;br&gt;
clear explanation, not hunting for vulnerabilities in raw text.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;try-it-yourself&quot;&gt;Try it yourself&lt;/h3&gt;
&lt;p&gt;The demo app is intentionally vulnerable and open source — safe to point any scanner at, including&lt;br&gt;
this one:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Demo target: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/sendwavehub/scan-target-demo-apps&quot;&gt;https://github.com/sendwavehub/scan-target-demo-apps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;AI Security Studio: &lt;a rel=&quot;nofollow&quot; href=&quot;https://apps.microsoft.com/store/detail/9PJ0J7BK1M27?cid=DevShareMCLPCB&quot;&gt;https://apps.microsoft.com/store/detail/9PJ0J7BK1M27?cid=DevShareMCLPCB&lt;/a&gt;&lt;br&gt;
{% embed &lt;a rel=&quot;nofollow&quot; href=&quot;https://sendwavehub.tech/th/apps/ai-security-studio-4&quot;&gt;https://sendwavehub.tech/th/apps/ai-security-studio-4&lt;/a&gt; %}&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Built for security researchers, pentesters, and AppSec teams doing work they're authorized to do.&lt;/p&gt;
&lt;p&gt;Next up in this series: stored XSS — one comment field, every visitor who loads the page.&lt;/p&gt;
</description>
<category>Videos</category>
<guid isPermaLink="true">https://coderlegion.com/25124/we-caught-sql-injection-with-an-offline-ai-security-scanner-heres-the-exact-query-it-found</guid>
<pubDate>Sat, 22 Aug 2026 07:06:23 +0000</pubDate>
</item>
<item>
<title>I added DuckDB to my SQL playground, and it turned out to be a sandboxing problem</title>
<link>https://coderlegion.com/24989/i-added-duckdb-to-my-sql-playground-and-it-turned-out-to-be-a-sandboxing-problem</link>
<description>&lt;h2 id=&quot;i-added-duckdb-to-my-sql-playground-and-it-turned-out-to-be-a-sandboxing-problem&quot;&gt;I added DuckDB to my SQL playground, and it turned out to be a sandboxing problem&lt;/h2&gt;
&lt;p&gt;Sharing a small build-in-public update: &lt;a rel=&quot;nofollow&quot; href=&quot;https://sqlize.online&quot;&gt;sqlize.online&lt;/a&gt;, a free tool for testing SQL queries, checking JOINs, and sharing reproducible examples, just got DuckDB support alongside the ~30 engines it already had (MySQL, Postgres, SQL Server, Oracle, Firebird, ClickHouse, SQLite...).&lt;/p&gt;
&lt;p&gt;I'd wanted to add DuckDB for a while — I use it a lot in my own projects and like having local OLAP around. The holdup was boring: the whole thing runs on PHP, and PHP's PDO talks to MySQL, Postgres, and SQLite out of the box, but had nothing for DuckDB.&lt;/p&gt;
&lt;p&gt;That changed when I found &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/iliaal/pdo_duckdb&quot;&gt;pdo_duckdb&lt;/a&gt; by Ilia Alshanetsky — a real PDO driver for it. That's the piece that made this worth doing.&lt;/p&gt;
&lt;h3 id=&quot;why-duckdb-specifically&quot;&gt;Why DuckDB specifically&lt;/h3&gt;
&lt;p&gt;Short version: it's &quot;SQLite for analytics.&quot; Vectorized engine, built for heavy aggregations on a single core, not for transactions.&lt;/p&gt;
&lt;p&gt;What actually got me was that it reads Parquet on the fly, from disk or over HTTP, with no separate load step. For a playground, that's great — instead of a two-row toy table, you can hand people something real. So I preloaded NYC's Yellow Taxi trip data for January 2024, about three million rows, sitting in a table called &lt;code&gt;yellow_tripdata&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
    PULocationID,
    COUNT(*) AS total_trips,
    ROUND(AVG(total_amount), 2) AS avg_fare
FROM yellow_tripdata
GROUP BY PULocationID
ORDER BY total_trips DESC
LIMIT 10;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's a table, not a file path — more on why that matters below.&lt;/p&gt;
&lt;h3 id=&quot;building-the-driver-plus-an-unrelated-debian-surprise&quot;&gt;Building the driver, plus an unrelated Debian surprise&lt;/h3&gt;
&lt;p&gt;pdo_duckdb needs PHP 8.1+. My setup runs several PHP versions side by side for different engines, so that already cuts out a couple of pools. Worse, the latest release only ships prebuilt binaries for PHP 8.4/8.5 — on 8.1, where this feature currently lives, I had to build from source: &lt;code&gt;phpize &amp;amp;&amp;amp; ./configure --with-pdo-duckdb=... &amp;amp;&amp;amp; make install&lt;/code&gt;, linked against &lt;code&gt;libduckdb.so&lt;/code&gt;. Not exotic, but not a one-command install either — about half the engines in this project get built by hand at this point.&lt;/p&gt;
&lt;p&gt;While I was at it I hit a completely unrelated bug: bumped that PHP pool to the latest patch release, and the image build broke for no obvious reason. Turned out the base image had quietly moved from Debian bookworm to trixie, which renamed &lt;code&gt;libaio1&lt;/code&gt; to &lt;code&gt;libaio1t64&lt;/code&gt;. Rolled the base image back. Cost me about forty minutes of confusion for something that had nothing to do with DuckDB at all.&lt;/p&gt;
&lt;h3 id=&quot;the-actual-problem-duckdb-runs-inside-the-php-process&quot;&gt;The actual problem: DuckDB runs inside the PHP process&lt;/h3&gt;
&lt;p&gt;MySQL and Postgres live in their own containers and can't see the PHP worker's filesystem at all. DuckDB is embedded — it opens up right inside PHP-FPM. Good for speed, no sockets or IPC in the way. Bad because by default it can reach anything the process can reach: &lt;code&gt;COPY ... TO '/etc/whatever'&lt;/code&gt;, &lt;code&gt;read_csv&lt;/code&gt; from any path, &lt;code&gt;read_parquet&lt;/code&gt; from any URL, &lt;code&gt;INSTALL&lt;/code&gt; for arbitrary extensions.&lt;/p&gt;
&lt;p&gt;Not something you want available to arbitrary user SQL.&lt;/p&gt;
&lt;p&gt;First attempt was the obvious one — set it in SQL directly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SET enable_external_access = false;
SET allow_community_extensions = false;
SET lock_configuration = true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This genuinely works, and &lt;code&gt;lock_configuration = true&lt;/code&gt; really does stop the same session from undoing it later. But the driver has its own separate protection: it flatly refuses a chunk of security-sensitive settings — paths, extension autoload — when they're passed via the DSN or &lt;code&gt;PDO::DUCKDB_ATTR_CONFIG&lt;/code&gt; at connect time. On purpose, so the app using the driver can't accidentally weaken its own config.&lt;/p&gt;
&lt;p&gt;The real mechanism turned out to be PHP's own &lt;code&gt;open_basedir&lt;/code&gt;. Set it — even at runtime, with &lt;code&gt;ini_set()&lt;/code&gt; — and the driver shuts off DuckDB's entire external-access surface: &lt;code&gt;read_csv&lt;/code&gt;, &lt;code&gt;read_parquet&lt;/code&gt;, &lt;code&gt;COPY&lt;/code&gt;, &lt;code&gt;ATTACH&lt;/code&gt;, &lt;code&gt;httpfs&lt;/code&gt;, &lt;code&gt;INSTALL&lt;/code&gt;, all of it, no matter which path gets requested. Regular &lt;code&gt;CREATE TABLE&lt;/code&gt;/&lt;code&gt;INSERT&lt;/code&gt;/&lt;code&gt;SELECT&lt;/code&gt; against the session's own file keep working fine.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;ini_set('open_basedir', '/code:/tmp/sessions:/tmp/databases');
$pdo = new PDO(&quot;duckdb:{$sessionFile}&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I tried stacking both — &lt;code&gt;open_basedir&lt;/code&gt; plus the &lt;code&gt;SET&lt;/code&gt; statements on top, just for extra safety. Didn't work: once &lt;code&gt;open_basedir&lt;/code&gt; is active, the driver rejects &lt;code&gt;allow_community_extensions&lt;/code&gt; outright (&quot;not allowed when open_basedir is set&quot;) and the connection breaks. Had to pick one. Kept &lt;code&gt;open_basedir&lt;/code&gt; — it covers everything on its own.&lt;/p&gt;
&lt;p&gt;And yes, there was a bug. I was applying &lt;code&gt;open_basedir&lt;/code&gt; right before running a query, but not before handing a raw PDO connection to user-written PHP code — which happens when someone runs PHP and SQL together and picks DuckDB as the engine. So: submit a PHP script with no SQL in the separate field, get back a completely unrestricted &lt;code&gt;$pdo&lt;/code&gt;, and &lt;code&gt;$pdo-&amp;gt;exec(&quot;COPY (SELECT 1) TO '/tmp/whatever.csv'&quot;)&lt;/code&gt; just works. Caught it myself before it shipped anywhere public.&lt;/p&gt;
&lt;h3 id=&quot;why-the-dataset-isn-t-loaded-on-every-request&quot;&gt;Why the dataset isn't loaded on every request&lt;/h3&gt;
&lt;p&gt;Three million rows over HTTPS on every session would be brutal — a cold build takes anywhere from 5 to 35 seconds. So it's built once at deploy time, by a separate script using a plain connection with no &lt;code&gt;open_basedir&lt;/code&gt; set (downloading the file there is a legitimate job for that script, not for user SQL), and cached to a shared file. Every session after that is just a &lt;code&gt;copy()&lt;/code&gt; — a fraction of a second.&lt;/p&gt;
&lt;h3 id=&quot;try-to-break-it&quot;&gt;Try to break it&lt;/h3&gt;
&lt;p&gt;If you find a way to read a server file or get around &lt;code&gt;open_basedir&lt;/code&gt;, I'd rather hear about it directly than see the service go down. No bug bounty budget, but I'll credit you publicly if you find something real.&lt;/p&gt;
&lt;p&gt;Try it here: &lt;a rel=&quot;nofollow&quot; href=&quot;https://sqlize.online&quot;&gt;sqlize.online&lt;/a&gt; — pick &quot;DuckDB&quot; or &quot;DuckDB Preloaded&quot; as the SQL version. Happy to answer questions about how any of this is built.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24989/i-added-duckdb-to-my-sql-playground-and-it-turned-out-to-be-a-sandboxing-problem</guid>
<pubDate>Thu, 20 Aug 2026 12:38:29 +0000</pubDate>
</item>
<item>
<title>From GitHub Repositories to a Professional Data Analytics Portfolio Website</title>
<link>https://coderlegion.com/24288/from-github-repositories-to-a-professional-data-analytics-portfolio-website</link>
<description>&lt;h3 id=&quot;building-more-than-just-projects-building-my-professional-identity&quot;&gt;Building More Than Just Projects — Building My Professional Identity&lt;/h3&gt;
&lt;p&gt;Completing my &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://yasirawaninfo-data.vercel.app/&quot;&gt;Data Analyst Internship&lt;/a&gt;&lt;/strong&gt; at &lt;strong&gt;Logic Stack&lt;/strong&gt; was a significant milestone. Over four weeks, I worked on real-world business problems using &lt;strong&gt;Excel, Power BI, Python and SQL&lt;/strong&gt;, successfully completing four practical analytics projects.&lt;/p&gt;
&lt;p&gt;Like many learners, I initially thought that uploading my projects to GitHub would be enough.&lt;/p&gt;
&lt;p&gt;After all, GitHub is the standard platform for developers and data professionals.&lt;/p&gt;
&lt;p&gt;But then I asked myself a simple question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;If a recruiter visits my profile, will they truly understand my journey just by opening four separate repositories?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The answer was &lt;strong&gt;No.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That realization inspired me to build something much bigger than another GitHub repository.&lt;/p&gt;
&lt;p&gt;It inspired me to build my own &lt;strong&gt;Production-Level Data Analytics &lt;a rel=&quot;nofollow&quot; href=&quot;https://yasirawaninfo-data.vercel.app/&quot;&gt;Portfolio Website&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-github-alone-wasn-t-enough&quot;&gt;Why GitHub Alone Wasn't Enough&lt;/h2&gt;
&lt;p&gt;GitHub is excellent for storing source code and documentation.&lt;/p&gt;
&lt;p&gt;However, recruiters usually spend only a few minutes reviewing a candidate's profile.&lt;/p&gt;
&lt;p&gt;Opening multiple repositories one by one makes it difficult to understand:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The overall learning journey&lt;/li&gt;
&lt;li&gt;Project progression&lt;/li&gt;
&lt;li&gt;Skills gained&lt;/li&gt;
&lt;li&gt;Business impact&lt;/li&gt;
&lt;li&gt;Technical growth&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I wanted to create a place where everything could be explored from a &lt;strong&gt;single professional dashboard&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-idea&quot;&gt;The Idea&lt;/h2&gt;
&lt;p&gt;Instead of sharing multiple independent repositories, I designed a portfolio website that presents my internship as one complete journey.&lt;/p&gt;
&lt;p&gt;Rather than saying,&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&quot;Here are my projects.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I wanted the website to tell a story:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;How I started, what I learned, which challenges I faced, how I solved them and what I built by the end of the internship.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That became the foundation of the entire portfolio.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;designing-the-portfolio&quot;&gt;Designing the Portfolio&lt;/h2&gt;
&lt;p&gt;The goal was simple:&lt;/p&gt;
&lt;p&gt;Create a clean, modern, recruiter-friendly website that highlights both my &lt;strong&gt;Data Analytics skills&lt;/strong&gt; and my &lt;strong&gt;Web Development experience&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The portfolio includes carefully designed sections that guide visitors through my internship journey.&lt;/p&gt;
&lt;h4 id=&quot;home&quot;&gt;Home&lt;/h4&gt;
&lt;p&gt;A professional landing page introducing my internship, background, and career focus.&lt;/p&gt;
&lt;h4 id=&quot;projects&quot;&gt;Projects&lt;/h4&gt;
&lt;p&gt;A dedicated section for all four internship projects.&lt;/p&gt;
&lt;p&gt;Each project contains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Project overview&lt;/li&gt;
&lt;li&gt;Technologies used&lt;/li&gt;
&lt;li&gt;GitHub Repository&lt;/li&gt;
&lt;li&gt;LinkedIn post&lt;/li&gt;
&lt;li&gt;YouTube explanation video&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Everything is connected from one location.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;skills-learned&quot;&gt;Skills Learned&lt;/h2&gt;
&lt;p&gt;Instead of simply listing technologies, I organized everything I learned during the internship into one dedicated section.&lt;/p&gt;
&lt;p&gt;The portfolio highlights skills including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Microsoft Excel&lt;/li&gt;
&lt;li&gt;Power BI&lt;/li&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;li&gt;SQL&lt;/li&gt;
&lt;li&gt;Data Cleaning&lt;/li&gt;
&lt;li&gt;Exploratory Data Analysis&lt;/li&gt;
&lt;li&gt;KPI Development&lt;/li&gt;
&lt;li&gt;Business Intelligence&lt;/li&gt;
&lt;li&gt;Dashboard Design&lt;/li&gt;
&lt;li&gt;Documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This gives recruiters a quick overview of my technical capabilities.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenges-solved&quot;&gt;Challenges Solved&lt;/h2&gt;
&lt;p&gt;One of my favourite sections is the &lt;strong&gt;Challenges Solved&lt;/strong&gt; page.&lt;/p&gt;
&lt;p&gt;Instead of hiding the problems I encountered, I documented them professionally.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python environment setup&lt;/li&gt;
&lt;li&gt;Library installation issues&lt;/li&gt;
&lt;li&gt;SQL query debugging&lt;/li&gt;
&lt;li&gt;Data cleaning problems&lt;/li&gt;
&lt;li&gt;Documentation challenges&lt;/li&gt;
&lt;li&gt;GitHub repository organization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each challenge is paired with the solution I implemented.&lt;/p&gt;
&lt;p&gt;I believe problem-solving demonstrates professional growth more effectively than simply showing successful projects.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;certificate-amp-internship-documents&quot;&gt;Certificate &amp;amp; Internship Documents&lt;/h2&gt;
&lt;p&gt;The portfolio also includes my internship certificate and professional documents.&lt;/p&gt;
&lt;p&gt;To keep them organized, I implemented a simple password-protected section that allows authorized viewers to access these files when needed.&lt;/p&gt;
&lt;p&gt;This keeps the homepage clean while still making important documents available.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;building-with-modern-technologies&quot;&gt;Building with Modern Technologies&lt;/h2&gt;
&lt;p&gt;This project also became an opportunity to strengthen my web development skills.&lt;/p&gt;
&lt;p&gt;The website was built using modern technologies including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Next.js&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;li&gt;Framer Motion&lt;/li&gt;
&lt;li&gt;Responsive Design&lt;/li&gt;
&lt;li&gt;SEO Optimization&lt;/li&gt;
&lt;li&gt;Structured Data (JSON-LD)&lt;/li&gt;
&lt;li&gt;Robots.txt&lt;/li&gt;
&lt;li&gt;Sitemap.xml&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By combining these technologies, I transformed a simple portfolio into a production-ready web application.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;seo-matters-too&quot;&gt;SEO Matters Too&lt;/h2&gt;
&lt;p&gt;I didn't want the website to look professional only for people who already had the link.&lt;/p&gt;
&lt;p&gt;I wanted search engines to understand it as well.&lt;/p&gt;
&lt;p&gt;To improve visibility, I implemented:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Metadata optimization&lt;/li&gt;
&lt;li&gt;Open Graph tags&lt;/li&gt;
&lt;li&gt;Structured Data&lt;/li&gt;
&lt;li&gt;Robots configuration&lt;/li&gt;
&lt;li&gt;XML Sitemap&lt;/li&gt;
&lt;li&gt;Performance optimization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Achieving high Lighthouse scores became another goal during development, ensuring the website follows modern web standards.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;combining-two-different-skill-sets&quot;&gt;Combining Two Different Skill Sets&lt;/h2&gt;
&lt;p&gt;This project became much more than a portfolio.&lt;/p&gt;
&lt;p&gt;It combined two different fields I enjoy:&lt;/p&gt;
&lt;h4 id=&quot;data-analytics&quot;&gt;Data Analytics&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Business insights&lt;/li&gt;
&lt;li&gt;SQL&lt;/li&gt;
&lt;li&gt;Power BI&lt;/li&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;li&gt;Excel&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;web-development&quot;&gt;Web Development&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Next.js&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;UI Design&lt;/li&gt;
&lt;li&gt;Responsive Layouts&lt;/li&gt;
&lt;li&gt;SEO&lt;/li&gt;
&lt;li&gt;Performance Optimization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bringing these two disciplines together allowed me to create something that represents my complete technical profile.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;lessons-beyond-the-internship&quot;&gt;Lessons Beyond the Internship&lt;/h2&gt;
&lt;p&gt;Building this website taught me several important lessons.&lt;/p&gt;
&lt;p&gt;A portfolio is not just about displaying work.&lt;/p&gt;
&lt;p&gt;It is about presenting your journey professionally.&lt;/p&gt;
&lt;p&gt;I learned that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Presentation matters.&lt;/li&gt;
&lt;li&gt;Documentation matters.&lt;/li&gt;
&lt;li&gt;User experience matters.&lt;/li&gt;
&lt;li&gt;Personal branding matters.&lt;/li&gt;
&lt;li&gt;Professional storytelling matters.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A recruiter should be able to understand who you are within minutes.&lt;/p&gt;
&lt;p&gt;That became the guiding principle behind this project.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Looking back, I'm glad I didn't stop after uploading four repositories.&lt;/p&gt;
&lt;p&gt;Creating this portfolio allowed me to organize my internship experience into one professional platform that showcases both my technical skills and my continuous learning journey.&lt;/p&gt;
&lt;p&gt;This website represents more than projects.&lt;/p&gt;
&lt;p&gt;It represents persistence.&lt;/p&gt;
&lt;p&gt;It represents growth.&lt;/p&gt;
&lt;p&gt;It represents every challenge solved throughout the internship.&lt;/p&gt;
&lt;p&gt;Most importantly, it reminds me that learning never truly ends.&lt;/p&gt;
&lt;p&gt;Every project becomes a stepping stone toward the next opportunity.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;explore-my-complete-portfolio&quot;&gt;Explore My Complete Portfolio&lt;/h3&gt;
&lt;p&gt;🌐 &lt;strong&gt;Live Portfolio:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://yasirawaninfo-data.vercel.app/&quot;&gt;https://yasirawaninfo-data.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;💻 &lt;strong&gt;GitHub:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831&quot;&gt;https://github.com/YasirAwan4831&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Thank you for taking the time to explore my journey. I hope it inspires other learners to build not only projects—but also a professional identity around their work.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;#DataAnalytics #SQL #SQLite #PowerBI #Python #Panda  #Numpy #Matplotlib #Excel #BusinessIntelligence  #DataVisualization #Dashboard #DataCleaning #JupyterNotebook #ProblemSolving  #GitHub #WebDevelopment #LearningInPublic #Portfolio #CareerGrowth ##MuhammadYasir #YasirAwan4831&lt;/em&gt; &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24288/from-github-repositories-to-a-professional-data-analytics-portfolio-website</guid>
<pubDate>Sat, 08 Aug 2026 07:35:02 +0000</pubDate>
</item>
<item>
<title>Continuing My Data Analyst Journey: Week 3, 4 and Challenges I Faced During My Data Analyst Intern</title>
<link>https://coderlegion.com/24267/continuing-my-data-analyst-journey-week-and-challenges-faced-during-data-analyst-intern</link>
<description>&lt;p&gt;In my previous blog, I shared my journey through Week 1 and Week 2, where I worked on Excel-based data cleaning, sales analysis, Pivot Tables and Power BI dashboards.&lt;/p&gt;
&lt;p&gt;As the internship progressed, the learning curve became much steeper. The focus shifted from spreadsheet analytics to programming, databases and solving real-world business problems using professional data analysis tools.&lt;/p&gt;
&lt;p&gt;Weeks 3 and 4 introduced completely new technologies, larger datasets and more technical challenges that pushed me far beyond my comfort zone.&lt;/p&gt;
&lt;p&gt;These two weeks transformed my internship from learning business reporting into building complete end-to-end data analytics solutions.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;week-3-supply-chain-analytics-using-python-amp-power-bi&quot;&gt;Week 3: Supply Chain Analytics using Python &amp;amp; Power BI&lt;/h3&gt;
&lt;p&gt;Week 3 marked my first serious experience with &lt;strong&gt;Python for Data Analytics.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Unlike previous weeks, this project required setting up a complete Python development environment before writing a single line of analysis. I worked extensively with &lt;strong&gt;Pandas, NumPy, Matplotlib and Jupyter Notebook&lt;/strong&gt; to clean, analyse and visualize a real-world supply chain dataset containing thousands of records.&lt;/p&gt;
&lt;p&gt;After completing the &lt;strong&gt;Exploratory Ddata Analysis (EDA)&lt;/strong&gt;, I integrated the insights into &lt;strong&gt;Power BI&lt;/strong&gt;, creating an interactive dashboard that highlighted sales performance, shipment modes, product groups, vendors and country-wise business metrics.&lt;/p&gt;
&lt;p&gt;This project taught me that successful data analysis begins with clean, reliable data—and that Python is one of the most powerful tools for transforming raw datasets into meaningful business insights.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt;  &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831/week-3-python-powerbi-supply-chain-analytics&quot;&gt;https://github.com/YasirAwan4831/week-3-python-powerbi-supply-chain-analytics&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;YouTube Video:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://youtu.be/SQk41XxxpsY?si=ElhxePDt916bh_VJ&quot;&gt;https://youtu.be/SQk41XxxpsY?si=ElhxePDt916bh_VJ&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LinkedIn Post:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/posts/yasirawan4831_eda-dataabranalyst-fullabrstackabrdeveloper-activity-7482280841147006976-YKho?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAFyt-1EB5XKMOcxukQpAzVmx6pIKiXdCz64&quot;&gt;https://www.linkedin.com/posts/yasirawan4831_eda-dataabranalyst-fullabrstackabrdevel&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;week-4-funnel-amp-revenue-analysis-using-sql-amp-power-bi&quot;&gt;Week 4: Funnel &amp;amp; Revenue Analysis using SQL &amp;amp; Power BI&lt;/h3&gt;
&lt;p&gt;The final week introduced one of the most important skills in modern data analytics: &lt;strong&gt;SQL&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For this project, I worked with SQLite to analyse customer funnel performance, revenue generation and conversion metrics directly from a relational database. Instead of relying on spreadsheets, I learned how businesses retrieve insights using SQL queries, aggregate functions, filtering, grouping and KPI calculations.&lt;/p&gt;
&lt;p&gt;The extracted data was then connected to &lt;strong&gt;Power BI&lt;/strong&gt;, where I built an interactive business dashboard presenting conversion rates, revenue trends and overall funnel performance.&lt;/p&gt;
&lt;p&gt;This project completed my internship journey by combining database querying, business intelligence and visualization into one complete analytics workflow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub Repository&lt;/strong&gt;:  &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831/week-4-sql-powerbi-funnel-analysis&quot;&gt;https://github.com/YasirAwan4831/week-4-sql-powerbi-funnel-analysis&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;YouTube Video:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://youtu.be/sdBYOKqT6YM?si=HfNw1CrgG71dHIzi&quot;&gt;https://youtu.be/sdBYOKqT6YM?si=HfNw1CrgG71dHIzi&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LinkedIn Post:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/posts/yasirawan4831_sqlite-sqlabrbased-dataabranalyst-activity-7486642277780742144-lXpI?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAFyt-1EB5XKMOcxukQpAzVmx6pIKiXdCz64&quot;&gt;https://www.linkedin.com/posts/yasirawan4831_sqlite-sqlabrbased-dataabranalys&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;from-learning-tools-to-solving-problems&quot;&gt;From Learning Tools to Solving Problems&lt;/h3&gt;
&lt;p&gt;By the end of Week 4, I had worked with:&lt;br&gt;
Microsoft Excel&lt;br&gt;
Power BI&lt;br&gt;
Python&lt;br&gt;
SQL&lt;br&gt;
Business Analytics&lt;br&gt;
Data Cleaning&lt;br&gt;
Exploratory Data Analysis (EDA)&lt;br&gt;
KPI Dashboard Development&lt;br&gt;
Funnel Analysis&lt;br&gt;
GitHub Documentation&lt;/p&gt;
&lt;p&gt;However, learning these tools was only part of the journey.&lt;br&gt;
The real growth came from overcoming technical challenges, debugging errors, researching solutions and continuously improving every project.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;10-real-challenges-i-faced-during-my-data-analyst-internship-and-how-i-solved-them-&quot;&gt;10 Real Challenges I Faced During My Data Analyst Internship (And How I Solved Them)&lt;/h2&gt;
&lt;p&gt; Every Internship Teaches Skills — But Real Growth Comes From Solving Problems&lt;/p&gt;
&lt;p&gt;When people look at a completed project, they usually see the dashboard, the charts, the SQL queries, or the final GitHub repository.&lt;/p&gt;
&lt;p&gt;What they don't see are the countless hours spent fixing errors, learning unfamiliar tools, debugging environments, researching documentation and solving problems that never appeared in any tutorial.&lt;/p&gt;
&lt;p&gt;During my one-month Data Analyst Internship at &lt;strong&gt;Logic Stack&lt;/strong&gt;, I completed four industry-based projects covering &lt;strong&gt;Excel, Power BI, Python, SQL and Business Analytics&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;However, the most valuable outcome wasn't just the projects themselves.&lt;/p&gt;
&lt;p&gt;It was learning &lt;strong&gt;how to solve problems independently.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This blog shares the biggest technical and practical challenges I faced during my internship—and how each one made me a better Data Analyst.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;week-3-project&quot;&gt;Week 3 Project&lt;/h2&gt;
&lt;h3 id=&quot;supply-chain-analytics-using-python-amp-power-bi&quot;&gt;Supply Chain Analytics using Python &amp;amp; Power BI&lt;/h3&gt;
&lt;p&gt;By Week 3, the internship shifted from Excel into Python-based analytics.&lt;/p&gt;
&lt;p&gt;This project focused on cleaning, analysing and visualizing a real-world Supply Chain dataset using &lt;strong&gt;Python (Pandas, NumPy, Matplotlib)&lt;/strong&gt; alongside &lt;strong&gt;Power BI&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Unlike previous weeks, this project introduced a completely new development environment, making it both exciting and challenging.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;week-4-project&quot;&gt;Week 4 Project&lt;/h2&gt;
&lt;h3 id=&quot;funnel-amp-revenue-analysis-using-sql-amp-power-bi&quot;&gt;Funnel &amp;amp; Revenue Analysis using SQL &amp;amp; Power BI&lt;/h3&gt;
&lt;p&gt;The final week introduced SQL for business analytics.&lt;/p&gt;
&lt;p&gt;The objective was to analyse conversion funnels, revenue performance and customer behaviour using SQL queries before presenting business insights inside Power BI dashboards.&lt;/p&gt;
&lt;p&gt;This project simulated real-world reporting workflows where raw database queries become executive dashboards.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenge-1&quot;&gt;Challenge 1&lt;/h2&gt;
&lt;h3 id=&quot;starting-with-zero-practical-experience&quot;&gt;Starting With Zero Practical Experience&lt;/h3&gt;
&lt;h4 id=&quot;challenge&quot;&gt;Challenge&lt;/h4&gt;
&lt;p&gt;Although I had experience in web development, I had almost no practical exposure to professional Data Analytics workflows.&lt;/p&gt;
&lt;p&gt;Everything—from Excel reporting to SQL analytics—was new.&lt;/p&gt;
&lt;h4 id=&quot;solution&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;Instead of trying to learn everything at once, I followed a structured approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831/week-1-retail-sales-excel-analysis&quot;&gt;Week 1 → Excel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831/week-2-excel-powerbi-sales-dashboard&quot;&gt;Week 2 → Power BI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831/week-3-python-powerbi-supply-chain-analytics&quot;&gt;Week 3 → Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831/week-4-sql-powerbi-funnel-analysis&quot;&gt;Week 4 → SQL&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Learning one technology at a time helped me build confidence while creating portfolio-ready projects every week.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenge-2&quot;&gt;Challenge 2&lt;/h2&gt;
&lt;h3 id=&quot;building-a-professional-excel-workflow&quot;&gt;Building a Professional Excel Workflow&lt;/h3&gt;
&lt;h4&gt;Challenge&lt;/h4&gt;
&lt;p&gt;Creating clean spreadsheets is easy.&lt;/p&gt;
&lt;p&gt;Creating professional business reports is not.&lt;/p&gt;
&lt;p&gt;I had to learn:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Structured Tables&lt;/li&gt;
&lt;li&gt;Pivot Tables&lt;/li&gt;
&lt;li&gt;Professional Formatting&lt;/li&gt;
&lt;li&gt;Multiple Worksheets&lt;/li&gt;
&lt;li&gt;Formula Organization&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;solution-1&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;I researched Excel best practices and redesigned my workbook several times until it followed professional reporting standards.&lt;/p&gt;
&lt;p&gt;The result was an organized multi-sheet Excel project that later became the foundation for my Power BI dashboard.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenge-3&quot;&gt;Challenge 3&lt;/h2&gt;
&lt;h3 id=&quot;learning-power-bi-dashboard-design&quot;&gt;Learning Power BI Dashboard Design&lt;/h3&gt;
&lt;h4&gt;Challenge&lt;/h4&gt;
&lt;p&gt;Power BI wasn't difficult because of the software.&lt;/p&gt;
&lt;p&gt;It was difficult because dashboards must answer business questions—not simply display charts.&lt;/p&gt;
&lt;h4 id=&quot;solution-2&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;I learned:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;KPI Cards&lt;/li&gt;
&lt;li&gt;DAX Basics&lt;/li&gt;
&lt;li&gt;Dashboard Layout Principles&lt;/li&gt;
&lt;li&gt;Visual Hierarchy&lt;/li&gt;
&lt;li&gt;Business Storytelling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rather than creating colourful visuals, I focused on dashboards that communicate insights clearly.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenge-4&quot;&gt;Challenge 4&lt;/h2&gt;
&lt;h3 id=&quot;configuring-the-python-development-environment&quot;&gt;Configuring the Python Development Environment&lt;/h3&gt;
&lt;h4&gt;Challenge&lt;/h4&gt;
&lt;p&gt;This was one of the toughest technical challenges.&lt;/p&gt;
&lt;p&gt;Issues included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VS Code Python interpreter problems&lt;/li&gt;
&lt;li&gt;Jupyter Kernel connection failures&lt;/li&gt;
&lt;li&gt;Package installation errors&lt;/li&gt;
&lt;li&gt;Environment configuration&lt;/li&gt;
&lt;li&gt;Version conflicts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These problems had nothing to do with analytics—but they completely blocked development.&lt;/p&gt;
&lt;h4 id=&quot;solution-3&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;Instead of giving up, I researched official documentation, debugging guides and community discussions.&lt;/p&gt;
&lt;p&gt;Eventually I:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fixed interpreter paths&lt;/li&gt;
&lt;li&gt;Installed required libraries&lt;/li&gt;
&lt;li&gt;Connected Jupyter successfully&lt;/li&gt;
&lt;li&gt;Stabilized the development environment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This experience taught me that environment setup is a real engineering skill.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenge-5&quot;&gt;Challenge 5&lt;/h2&gt;
&lt;h3 id=&quot;cleaning-a-real-world-dataset&quot;&gt;Cleaning a Real-World Dataset&lt;/h3&gt;
&lt;h4&gt;Challenge&lt;/h4&gt;
&lt;p&gt;The Supply Chain dataset contained:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Missing values&lt;/li&gt;
&lt;li&gt;Mixed data types&lt;/li&gt;
&lt;li&gt;Invalid numeric values&lt;/li&gt;
&lt;li&gt;Text inside number columns&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Real business datasets are rarely clean.&lt;/p&gt;
&lt;h4 id=&quot;solution-4&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;Using Pandas, I:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Converted invalid values safely&lt;/li&gt;
&lt;li&gt;Filled missing values&lt;/li&gt;
&lt;li&gt;Removed inconsistencies&lt;/li&gt;
&lt;li&gt;Validated cleaned data&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Only after proper cleaning did visualization become meaningful.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenge-6&quot;&gt;Challenge 6&lt;/h2&gt;
&lt;h3 id=&quot;installing-sqlite-amp-building-sql-workflows&quot;&gt;Installing SQLite &amp;amp; Building SQL Workflows&lt;/h3&gt;
&lt;h4&gt;Challenge&lt;/h4&gt;
&lt;p&gt;Week 4 introduced SQL.&lt;/p&gt;
&lt;p&gt;Before writing queries, I first had to configure SQLite and import datasets correctly.&lt;/p&gt;
&lt;h4 id=&quot;solution-5&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;I installed DB Browser for SQLite, verified database structure, imported CSV datasets correctly and prepared the environment before beginning SQL analysis.&lt;/p&gt;
&lt;p&gt;This preparation prevented many future errors.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenge-7&quot;&gt;Challenge 7&lt;/h2&gt;
&lt;h3 id=&quot;writing-complex-sql-queries&quot;&gt;Writing Complex SQL Queries&lt;/h3&gt;
&lt;h4&gt;Challenge&lt;/h4&gt;
&lt;p&gt;Simple SELECT statements were easy.&lt;/p&gt;
&lt;p&gt;Business reporting required:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GROUP BY&lt;/li&gt;
&lt;li&gt;Aggregate Functions&lt;/li&gt;
&lt;li&gt;Filtering&lt;/li&gt;
&lt;li&gt;Revenue Calculations&lt;/li&gt;
&lt;li&gt;Conversion Metrics&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;solution-6&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;Instead of memorizing syntax, I practiced writing increasingly complex queries while documenting each command inside my repository.&lt;/p&gt;
&lt;p&gt;This improved both my SQL knowledge and documentation skills.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenge-8&quot;&gt;Challenge 8&lt;/h2&gt;
&lt;h3 id=&quot;turning-data-into-business-insights&quot;&gt;Turning Data Into Business Insights&lt;/h3&gt;
&lt;h4&gt;Challenge&lt;/h4&gt;
&lt;p&gt;Anyone can generate numbers.&lt;/p&gt;
&lt;p&gt;A Data Analyst must explain what those numbers actually mean.&lt;/p&gt;
&lt;h4 id=&quot;solution-7&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;For every project I focused on answering business questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which products perform best?&lt;/li&gt;
&lt;li&gt;Where is revenue growing?&lt;/li&gt;
&lt;li&gt;Which funnel stage loses customers?&lt;/li&gt;
&lt;li&gt;What actions should businesses take?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This mindset transformed technical work into business analysis.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenge-9&quot;&gt;Challenge 9&lt;/h2&gt;
&lt;h3 id=&quot;professional-github-documentation&quot;&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831/week-1-retail-sales-excel-analysis/blob/main/README.md&quot;&gt;Professional GitHub Documentation&lt;/a&gt;&lt;/h3&gt;
&lt;h4&gt;Challenge&lt;/h4&gt;
&lt;p&gt;Writing code is only half the job.&lt;/p&gt;
&lt;p&gt;Presenting &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831/week-1-retail-sales-excel-analysis/blob/main/README.md&quot;&gt;projects professionally&lt;/a&gt; is equally important.&lt;/p&gt;
&lt;h4 id=&quot;solution-8&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;I created consistent repositories with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Detailed README files&lt;/li&gt;
&lt;li&gt;Professional documentation&lt;/li&gt;
&lt;li&gt;SVG animations&lt;/li&gt;
&lt;li&gt;Shields.io badges&lt;/li&gt;
&lt;li&gt;Folder organization&lt;/li&gt;
&lt;li&gt;Screenshots&lt;/li&gt;
&lt;li&gt;Business explanations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These repositories became portfolio-quality projects rather than simple assignments.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;challenge-10&quot;&gt;Challenge 10&lt;/h2&gt;
&lt;h3 id=&quot;building-my-own-data-analyst-portfolio-website&quot;&gt;Building My Own Data Analyst Portfolio Website&lt;/h3&gt;
&lt;h4&gt;Challenge&lt;/h4&gt;
&lt;p&gt;Instead of simply uploading four GitHub repositories, I wanted to create something unique.&lt;/p&gt;
&lt;h4 id=&quot;solution-9&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;I designed and developed a complete interactive portfolio website using &lt;strong&gt;Next.js&lt;/strong&gt; and &lt;strong&gt;TypeScript&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The website includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Internship Overview&lt;/li&gt;
&lt;li&gt;Four Complete Projects&lt;/li&gt;
&lt;li&gt;GitHub Repositories&lt;/li&gt;
&lt;li&gt;LinkedIn Posts&lt;/li&gt;
&lt;li&gt;YouTube Project Videos&lt;/li&gt;
&lt;li&gt;Skills Learned&lt;/li&gt;
&lt;li&gt;Challenges Solved&lt;/li&gt;
&lt;li&gt;Password-Protected Certificate Section&lt;/li&gt;
&lt;li&gt;Resume Download&lt;/li&gt;
&lt;li&gt;Modern Responsive UI&lt;/li&gt;
&lt;li&gt;SEO Optimization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This project combined my web development background with my newly acquired Data Analytics skills, creating a portfolio that reflects both technical implementation and professional presentation.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-this-internship-really-taught-me&quot;&gt;What This Internship Really Taught Me&lt;/h2&gt;
&lt;p&gt;This internship wasn't just about learning Excel, Python, SQL, or Power BI.&lt;/p&gt;
&lt;p&gt;It taught me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How to research independently.&lt;/li&gt;
&lt;li&gt;How to debug unfamiliar problems.&lt;/li&gt;
&lt;li&gt;How to organize professional projects.&lt;/li&gt;
&lt;li&gt;How to document technical work.&lt;/li&gt;
&lt;li&gt;How to communicate business insights.&lt;/li&gt;
&lt;li&gt;How to keep learning when solutions aren't immediately obvious.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These lessons will remain valuable long after the internship ends.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thoughts-1&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Looking back, the dashboards and repositories represent only the visible outcome.&lt;/p&gt;
&lt;p&gt;The real achievement was overcoming every obstacle that appeared along the way.&lt;/p&gt;
&lt;p&gt;Every error message, every failed installation, every debugging session and every rewritten query contributed to my growth as a Data Analyst.&lt;/p&gt;
&lt;p&gt;The internship has ended—but the learning journey continues.&lt;/p&gt;
&lt;p&gt;Thank you, &lt;strong&gt;Logic Stack&lt;/strong&gt;, for providing a practical environment where I could build real projects, solve real problems and gain valuable industry experience.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;connect-with-me&quot;&gt;Connect With Me&lt;/h3&gt;
&lt;p&gt;🌐 Portfolio&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://yasirawaninfo-data.vercel.app/&quot;&gt;https://yasirawaninfo-data.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;💻 GitHub&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831&quot;&gt;https://github.com/YasirAwan4831&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;💼 LinkedIn&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://linkedin.com/in/yasirawan4831&quot;&gt;https://linkedin.com/in/yasirawan4831&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Thank you for reading. If this journey inspires someone starting their own Data Analytics path, then sharing these experiences has been worthwhile.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;#DataAnalytics #SQL #SQLite #PowerBI #BusinessIntelligence #DataVisualization #Dashboard #GitHub #Internship #LogicStack #DataAnalyst #LearningJourney #OpenToWork #Portfolio #YasirAwan4831&lt;/em&gt; &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24267/continuing-my-data-analyst-journey-week-and-challenges-faced-during-data-analyst-intern</guid>
<pubDate>Fri, 07 Aug 2026 19:22:22 +0000</pubDate>
</item>
<item>
<title>My Data Analyst Internship Journey: From Learning the Fundamentals to Building Production-Ready Anal</title>
<link>https://coderlegion.com/24250/data-analyst-internship-journey-from-learning-fundamentals-building-production-ready</link>
<description>&lt;h3 id=&quot;introduction-1&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Every learning journey begins with a single decision.&lt;/p&gt;
&lt;p&gt;For me, that decision was to step into the world of &lt;strong&gt;Data Analytics&lt;/strong&gt;—a field that combines problem-solving, business understanding, and technology to transform raw data into meaningful insights.&lt;/p&gt;
&lt;p&gt;When I joined the &lt;strong&gt;Logic Stack Data Analyst Internship&lt;/strong&gt;, I wasn't simply looking to complete another internship. My goal was much bigger: I wanted to build practical skills, work on real-world projects, document every step professionally and create a portfolio that genuinely reflected my growth.&lt;/p&gt;
&lt;p&gt;Over the course of one month, I worked on multiple projects involving &lt;strong&gt;Excel, Python, SQL, Power BI, GitHub, technical documentation, and portfolio development&lt;/strong&gt;. Every week introduced new concepts, new challenges and new opportunities to improve.&lt;/p&gt;
&lt;p&gt;This blog shares my complete journey—from the first day of the internship to the completion of the final project. Rather than focusing only on the outcomes, I want to share the process, the lessons I learned and how each project helped me grow as an aspiring Data Analyst.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-i-chose-this-internship&quot;&gt;Why I Chose This Internship&lt;/h2&gt;
&lt;p&gt;The field of Data Analytics has become one of the fastest-growing domains in technology. Organizations across every industry rely on data to make better decisions, optimize operations, and understand customer behaviour.&lt;/p&gt;
&lt;p&gt;I wanted hands-on experience with the tools and workflows used by professionals instead of learning only through theory.&lt;/p&gt;
&lt;p&gt;This internship provided exactly that opportunity.&lt;/p&gt;
&lt;p&gt;It focused on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Practical business datasets&lt;/li&gt;
&lt;li&gt;Real analytical workflows&lt;/li&gt;
&lt;li&gt;Professional documentation&lt;/li&gt;
&lt;li&gt;Dashboard development&lt;/li&gt;
&lt;li&gt;Industry-standard tools&lt;/li&gt;
&lt;li&gt;Portfolio building&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of completing isolated exercises, every week contributed to a larger learning journey.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;about-the-logic-stack-internship&quot;&gt;About the Logic Stack Internship&lt;/h2&gt;
&lt;p&gt;The internship was designed as a structured four-week program where every week introduced a new challenge.&lt;/p&gt;
&lt;p&gt;Rather than repeating similar exercises, each project focused on solving a different business problem using different analytical techniques.&lt;/p&gt;
&lt;p&gt;Throughout the internship I worked with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Microsoft Excel&lt;/li&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;li&gt;Pandas&lt;/li&gt;
&lt;li&gt;NumPy&lt;/li&gt;
&lt;li&gt;SQL (SQLite)&lt;/li&gt;
&lt;li&gt;Power BI&lt;/li&gt;
&lt;li&gt;GitHub&lt;/li&gt;
&lt;li&gt;Technical Documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every project required not only completing the technical work but also presenting it professionally through GitHub repositories, documentation, dashboards, screenshots and project reports.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;my-goals-before-starting&quot;&gt;My Goals Before Starting&lt;/h2&gt;
&lt;p&gt;Before beginning the internship, I set a few personal goals for myself:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Learn modern Data Analytics tools through practical work.&lt;/li&gt;
&lt;li&gt;Improve my analytical thinking.&lt;/li&gt;
&lt;li&gt;Build projects suitable for a professional portfolio.&lt;/li&gt;
&lt;li&gt;Learn proper GitHub documentation.&lt;/li&gt;
&lt;li&gt;Create dashboards that communicate business insights effectively.&lt;/li&gt;
&lt;li&gt;Gain confidence working with real datasets.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Looking back now, these goals helped me stay focused throughout the internship.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;week-1-learning-the-foundations&quot;&gt;Week 1 – Learning the Foundations&lt;/h2&gt;
&lt;p&gt;The first week focused on building a strong analytical foundation.&lt;/p&gt;
&lt;p&gt;Instead of jumping directly into advanced visualization, the emphasis was placed on understanding datasets, identifying business questions, cleaning information and extracting useful insights.&lt;/p&gt;
&lt;p&gt;This project taught me that successful data analysis begins long before dashboards are created.&lt;/p&gt;
&lt;p&gt;Some of the concepts I worked on included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Understanding structured datasets&lt;/li&gt;
&lt;li&gt;Data cleaning&lt;/li&gt;
&lt;li&gt;Exploratory analysis&lt;/li&gt;
&lt;li&gt;Identifying patterns&lt;/li&gt;
&lt;li&gt;Basic business insights&lt;/li&gt;
&lt;li&gt;Professional project documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;More importantly, I learned how to organize a project professionally from the beginning—a habit that became extremely valuable during the following weeks.&lt;/p&gt;
&lt;p&gt;GitHub Repository: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831/week-1-retail-sales-excel-analysis&quot;&gt;https://github.com/YasirAwan4831/week-1-retail-sales-excel-analysis&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;YouTube Video:  &lt;a rel=&quot;nofollow&quot; href=&quot;https://youtube.com/shorts/-vCaOvUj-DA?si=4DzXsE1v3Idt84xs&quot;&gt;https://youtube.com/shorts/-vCaOvUj-DA?si=4DzXsE1v3Idt84xs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;LinkedIn Post: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/posts/yasirawan4831_excel-logicstack-dataanalyst-activity-7476570834262945792-XcJ4?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAFyt-1EB5XKMOcxukQpAzVmx6pIKiXdCz64&quot;&gt;https://www.linkedin.com/posts/yasirawan4831_excel-logicstack-dataanalyst-activity-&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;week-2-building-analytical-confidence&quot;&gt;Week 2 – Building Analytical Confidence&lt;/h2&gt;
&lt;p&gt;The second week pushed my skills further.&lt;/p&gt;
&lt;p&gt;The datasets became larger, the questions became more analytical and the expectations shifted from simply calculating values to explaining business performance.&lt;/p&gt;
&lt;p&gt;This project strengthened my ability to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Interpret business metrics&lt;/li&gt;
&lt;li&gt;Compare trends&lt;/li&gt;
&lt;li&gt;Identify opportunities&lt;/li&gt;
&lt;li&gt;Present findings clearly&lt;/li&gt;
&lt;li&gt;Think from a business perspective instead of only a technical perspective&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One important lesson from this week was that data alone has very little value unless it is converted into actionable insights.&lt;/p&gt;
&lt;p&gt;That mindset completely changed how I approached the remaining internship projects.&lt;/p&gt;
&lt;p&gt;GitHub Repository: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831/week-2-excel-powerbi-sales-dashboard&quot;&gt;https://github.com/YasirAwan4831/week-2-excel-powerbi-sales-dashboard&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;YouTube Video: &lt;a rel=&quot;nofollow&quot; href=&quot;https://youtube.com/shorts/JLg-B-rs0gE?si=kCWG7KHxdxw2d-Pa&quot;&gt;https://youtube.com/shorts/JLg-B-rs0gE?si=kCWG7KHxdxw2d-Pa&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;LinkedIn Post: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/posts/yasirawan4831_weekabr2-excel-powerabrbi-activity-7480188105535209473-ekdu?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAFyt-1EB5XKMOcxukQpAzVmx6pIKiXdCz64&quot;&gt;https://www.linkedin.com/posts/yasirawan4831_weekabr2-excel-powerabrbi-activity-&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;continuing-the-journey-&quot;&gt;Continuing the Journey...&lt;/h3&gt;
&lt;p&gt;By the end of the first two weeks, I had gained confidence working with datasets, understanding business requirements, and documenting projects professionally.&lt;/p&gt;
&lt;p&gt;However, the most challenging—and most rewarding—part of the internship was still ahead.&lt;/p&gt;
&lt;p&gt;In the next part of this journey, I'll cover:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Week 3: Supply Chain Analytics with Python &amp;amp; Power BI&lt;/li&gt;
&lt;li&gt;Week 4: SQL + Power BI Funnel Analysis&lt;/li&gt;
&lt;li&gt;Building production-quality GitHub repositories&lt;/li&gt;
&lt;li&gt;Creating a modern Data Analytics Portfolio Website&lt;/li&gt;
&lt;li&gt;Earning my internship completion certificate&lt;/li&gt;
&lt;li&gt;Key lessons and reflections from the entire experience&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The second half of this journey represents the point where I transitioned from completing internship tasks to building projects that reflect my professional capabilities as a Data Analyst.&lt;/p&gt;
&lt;p&gt;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ---------------------------------------------------&lt;br&gt;
&lt;em&gt;#DataAnalytics #Portfolio #SQL #PowerBI #Python #Excel #GitHub #WebDevelopment #LearningInPublic #CareerGrowth #OpenToWork Muhammad Yasir #MuhammadYasir #YasirAwan4831&lt;/em&gt; &lt;/p&gt;
&lt;p&gt;-&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24250/data-analyst-internship-journey-from-learning-fundamentals-building-production-ready</guid>
<pubDate>Fri, 07 Aug 2026 06:12:48 +0000</pubDate>
</item>
<item>
<title>How to get body of the query using aiosql module? is that enough?</title>
<link>https://coderlegion.com/22573/how-to-get-body-of-the-query-using-aiosql-module-is-that-enough</link>
<description>&lt;p&gt;In Python development, keeping your database queries clean often leads us to use aiosql, which lets us write pure SQL in .sql files and load them directly as callable Python methods. However, when building APIs or debugging, you often need to inspect, log, or manipulate the actual raw SQL query string from inside your application.  Here is how you can access the underlying SQL query body dynamically, the hurdles you will face, and how to elegantly solve them.&lt;/p&gt;
&lt;p&gt;Extracting the Query Body DirectlyTo retrieve the raw SQL string of any loaded query, you do not need to read the physical files again. The aiosql library attaches metadata directly to your query objects once they are loaded.  Every query loaded onto your queries namespace has a .sql attribute containing the raw SQL body.  &lt;/p&gt;
&lt;p&gt;You can access metadata like the .operation type to see if it is a SELECT, INSERT, or UPDATE statement.  Pythonimport aiosql&lt;/p&gt;
&lt;h2 id=&quot;load-queries-from-your-local-sql-directory&quot;&gt;Load queries from your local SQL directory&lt;/h2&gt;
&lt;p&gt;queries = aiosql.from_path(&quot;./queries&quot;, &quot;sqlite3&quot;)&lt;/p&gt;
&lt;h2 id=&quot;fetch-the-raw-sql-query-body-directly-from-the-method&quot;&gt;Fetch the raw SQL query body directly from the method&lt;/h2&gt;
&lt;p&gt;raw_sql = queries.get_user_by_id.sql&lt;br&gt;
print(raw_sql)&lt;/p&gt;
&lt;p&gt;Real-World Hurdles You Will EncounterWhile fetching the raw string is simple, relying on it for runtime debugging brings a few modern development challenges.Parameter placeholder mismatch: The raw .sql string still holds the un-interpolated named placeholders like :user_id or ?, meaning it cannot be copied directly into a database GUI for manual testing.Driver-specific syntax shifts: Depending on the driver you bind (such as asyncpg or sqlite3), the way parameters are prepared can alter how the underlying SQL engine interprets the query.  Missing runtime values: The raw query string itself does not carry the actual live parameters that were passed during the execution context.&lt;/p&gt;
&lt;p&gt;The SolutionsTo get around these limitations and use your query bodies effectively for logging, auditing, or dry-running, you can implement these clean workarounds.Inspect metadata side-by-side: Combine the .sql attribute with the .parameters attribute to map variables to their placeholders programmatically before execution.Intercept with context managers: Write a lightweight wrapper to capture the query state dynamically, making it incredibly easy to log exact executed query structures.Integrate dry-run formatting: Build a helper function that swaps out :placeholder keys with mock values for easier diagnostic testing.Python# Extract parameters alongside the query text&lt;br&gt;
query_func = queries.get_user_by_id&lt;br&gt;
print(&quot;SQL Body:&quot;, query_func.sql)&lt;br&gt;
print(&quot;Expected Parameters:&quot;, query_func.parameters)&lt;br&gt;
By leveraging these built-in attributes, you can easily inspect your database interactions without cluttering your clean SQL file structure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sumita&lt;br&gt;
Web Developer&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22573/how-to-get-body-of-the-query-using-aiosql-module-is-that-enough</guid>
<pubDate>Tue, 14 Jul 2026 02:42:44 +0000</pubDate>
</item>
<item>
<title>SQL Syntax Highlighter</title>
<link>https://coderlegion.com/22159/sql-syntax-highlighter</link>
<description>&lt;h2 id=&quot;lightweight-sql-syntax-highlighter-for-the-browser-why-i-built-sql-highlighter&quot;&gt;Lightweight SQL Syntax Highlighter for the Browser: Why I Built sql-highlighter&lt;/h2&gt;
&lt;p&gt;If you need a &lt;strong&gt;lightweight SQL syntax highlighter&lt;/strong&gt; for docs, tutorials, or training platforms, this article explains a practical approach that works without heavy dependencies.&lt;/p&gt;
&lt;p&gt;My name is &lt;strong&gt;Slava Rozhnev&lt;/strong&gt;. I build free SQL learning projects &lt;a rel=&quot;nofollow&quot; href=&quot;https://sqlize.online&quot;&gt;sqlize.online&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://sqltest.online&quot;&gt;sqltest.online&lt;/a&gt;, where users practice SQL on real examples.&lt;/p&gt;
&lt;p&gt;In this post, I share:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;why I created my own browser SQL highlighter&lt;/li&gt;
&lt;li&gt;how it works internally&lt;/li&gt;
&lt;li&gt;how to support modern SQL keywords and functions&lt;/li&gt;
&lt;li&gt;how to extend highlighted tokens without changing the library itself&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;tl-dr&quot;&gt;TL;DR&lt;/h3&gt;
&lt;p&gt;I built &lt;strong&gt;sql-highlighter&lt;/strong&gt;, a small dependency-free JavaScript SQL highlighter, because standard configurations in larger libraries did not fully cover modern, dialect-specific SQL used in educational content.&lt;/p&gt;
&lt;p&gt;The implementation uses ordered regex passes with safe handling of strings/comments, supports light and dark themes, and allows extending keywords/functions via &lt;code&gt;SQLHighlighter.extend(...)&lt;/code&gt; without updating the core script.&lt;/p&gt;
&lt;h3 id=&quot;why-i-built-a-custom-sql-syntax-highlighter&quot;&gt;Why I Built a Custom SQL Syntax Highlighter&lt;/h3&gt;
&lt;p&gt;On &lt;a rel=&quot;nofollow&quot; href=&quot;https://sqltest.online&quot;&gt;sqltest.online&lt;/a&gt;, I initially used &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/highlightjs/highlight.js&quot;&gt;highlight.js&lt;/a&gt; for SQL code highlighting.&lt;/p&gt;
&lt;p&gt;In practice, many modern or dialect-specific SQL words were not highlighted. I forked the SQL definition, added missing keywords, and submitted a pull request, but it was rejected.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The reason: those additions were not part of standard SQL.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is reasonable for a universal library. But for a learning platform, I needed practical coverage of real-world SQL syntax. So I built a focused highlighter designed for this exact use case.&lt;/p&gt;
&lt;h3 id=&quot;what-sql-highlighter-is&quot;&gt;What sql-highlighter Is&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;sql-highlighter&lt;/strong&gt; is a dependency-free SQL syntax highlighter for the browser.&lt;/p&gt;
&lt;p&gt;It is designed to be simple to adopt:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;no framework required&lt;/li&gt;
&lt;li&gt;no mandatory build step&lt;/li&gt;
&lt;li&gt;works with regular script and CSS includes&lt;/li&gt;
&lt;li&gt;also available via npm&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Repository and docs: &lt;a rel=&quot;nofollow&quot; href=&quot;README.md&quot;&gt;README.md&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-gets-highlighted&quot;&gt;What Gets Highlighted&lt;/h3&gt;
&lt;p&gt;The library highlights the SQL tokens that matter most in real queries and training material:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SQL keywords&lt;/li&gt;
&lt;li&gt;built-in SQL functions&lt;/li&gt;
&lt;li&gt;data types&lt;/li&gt;
&lt;li&gt;string literals&lt;/li&gt;
&lt;li&gt;comments&lt;/li&gt;
&lt;li&gt;numbers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Function names are highlighted only when used as function calls, which helps avoid false positives when the same word is used as an identifier.&lt;/p&gt;
&lt;h3 id=&quot;how-the-highlighter-works&quot;&gt;How the Highlighter Works&lt;/h3&gt;
&lt;p&gt;The architecture is intentionally small: dictionaries + regular expressions + sequential text passes.&lt;/p&gt;
&lt;p&gt;A key detail is &lt;strong&gt;processing order&lt;/strong&gt;. Multi-word constructs such as &lt;code&gt;SELECT DISTINCT&lt;/code&gt; and &lt;code&gt;GROUP BY&lt;/code&gt; are processed before single-word keywords. This prevents partial or repeated matching after a longer construct has already been highlighted.&lt;/p&gt;
&lt;p&gt;Algorithm:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Find strings and comments and replace them with temporary placeholders.&lt;/li&gt;
&lt;li&gt;Highlight keywords, functions, constants, and types.&lt;/li&gt;
&lt;li&gt;Highlight numbers while avoiding matches inside identifiers.&lt;/li&gt;
&lt;li&gt;Restore placeholders as &lt;code&gt;.comment&lt;/code&gt; and &lt;code&gt;.string&lt;/code&gt; spans.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The goal is not full SQL parsing. The goal is fast, reliable SQL highlighting for web pages.&lt;/p&gt;
&lt;h3 id=&quot;why-regex-instead-of-a-full-sql-parser&quot;&gt;Why Regex Instead of a Full SQL Parser&lt;/h3&gt;
&lt;p&gt;For this product, a full parser would be too heavy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;more maintenance complexity&lt;/li&gt;
&lt;li&gt;larger frontend footprint&lt;/li&gt;
&lt;li&gt;more dependencies and parser-specific edge cases&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With careful ordering and token isolation, regex gives an effective balance of quality, speed, and bundle size.&lt;/p&gt;
&lt;h3 id=&quot;extend-keywords-and-functions-without-updating-the-script&quot;&gt;Extend Keywords and Functions Without Updating the Script&lt;/h3&gt;
&lt;p&gt;This is one of the most useful capabilities.&lt;/p&gt;
&lt;p&gt;You can extend supported SQL keywords/functions for your dialect directly in your project, without forking or modifying the base library.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;SQLHighlighter.extend({
  keyWords: ['MERGE', 'PIVOT'],
  functions: ['ARRAY_AGG', 'STRING_AGG'],
  constants: ['CURRENT_ROLE'],
  types: ['JSONB', 'UUID']
});

SQLHighlighter.highlightCodeBlocks();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This keeps the core script stable while allowing product-level customization for specific SQL courses or database dialects.&lt;/p&gt;
&lt;h3 id=&quot;styling-theming-and-ux&quot;&gt;Styling, Theming, and UX&lt;/h3&gt;
&lt;p&gt;The project uses token classes and CSS variables, so it is easy to integrate into existing design systems.&lt;/p&gt;
&lt;p&gt;It also supports both light and dark themes, which is useful for modern docs, tutorials, and developer portals.&lt;/p&gt;
&lt;h3 id=&quot;who-should-use-sql-highlighter&quot;&gt;Who Should Use sql-highlighter&lt;/h3&gt;
&lt;p&gt;Use this tool if you need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;highlight SQL in learning content or technical documentation&lt;/li&gt;
&lt;li&gt;add SQL code highlighting to a site quickly&lt;/li&gt;
&lt;li&gt;avoid heavy dependencies for a simple frontend feature&lt;/li&gt;
&lt;li&gt;customize highlighted SQL keywords/functions by dialect&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;development-notes&quot;&gt;Development Notes&lt;/h3&gt;
&lt;p&gt;I used Claude for parts of the development workflow to speed up repetitive implementation tasks and quickly validate ideas.&lt;/p&gt;
&lt;p&gt;Architecture and behavior decisions were driven by real requirements and user feedback from my SQL learning platforms.&lt;/p&gt;
&lt;h3 id=&quot;roadmap&quot;&gt;Roadmap&lt;/h3&gt;
&lt;p&gt;Next improvements include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;broader SQL dialect coverage&lt;/li&gt;
&lt;li&gt;expanded automated test coverage&lt;/li&gt;
&lt;li&gt;improved theming and UX integration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you build SQL education tools or SQL-heavy docs, feedback and contributions are welcome.&lt;/p&gt;
&lt;h3 id=&quot;faq&quot;&gt;FAQ&lt;/h3&gt;
&lt;h4 id=&quot;is-this-a-full-sql-parser-&quot;&gt;Is this a full SQL parser?&lt;/h4&gt;
&lt;p&gt;No. It is a regex-based highlighter designed for speed and practical browser usage.&lt;/p&gt;
&lt;h4 id=&quot;can-i-add-custom-sql-keywords-&quot;&gt;Can I add custom SQL keywords?&lt;/h4&gt;
&lt;p&gt;Yes. Use &lt;code&gt;SQLHighlighter.extend(...)&lt;/code&gt; to add custom keywords, functions, constants, and types.&lt;/p&gt;
&lt;h4 id=&quot;do-i-need-to-fork-the-project-to-support-my-sql-dialect-&quot;&gt;Do I need to fork the project to support my SQL dialect?&lt;/h4&gt;
&lt;p&gt;No. Dialect-specific extensions can be added in your own code without changing the core library.&lt;/p&gt;
&lt;h4 id=&quot;is-it-suitable-for-production-websites-&quot;&gt;Is it suitable for production websites?&lt;/h4&gt;
&lt;p&gt;Yes, especially when you need fast, lightweight SQL highlighting for docs, tutorials, and educational content.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Repository: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rozhnev/sql-highlighter&quot;&gt;sql-highlighter&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Thanks for reading.&lt;/p&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/22159/sql-syntax-highlighter</guid>
<pubDate>Thu, 09 Jul 2026 12:51:06 +0000</pubDate>
</item>
<item>
<title>How to Format SQL Queries for Better Readability</title>
<link>https://coderlegion.com/22157/how-to-format-sql-queries-for-better-readability</link>
<description>&lt;p&gt;You know the feeling. Someone shares a query in Slack, or you open a &lt;code&gt;.sql&lt;/code&gt; file left behind by a developer who's long gone, and it's just... one giant wall of text. Lowercase here, uppercase there, no line breaks, five joins crammed into a single sentence. You end up scrolling sideways just to figure out what table a column came from.&lt;/p&gt;
&lt;p&gt;That's not a &quot;them&quot; problem. Every SQL developer has written a query like that under deadline pressure. But queries get read far more often than they get written, and a badly formatted one costs everyone — including future you — real time.&lt;/p&gt;
&lt;p&gt;This guide walks through practical, no-nonsense SQL formatting best practices you can start using today. Every rule comes with a before-and-after example, so you can see exactly why the &quot;after&quot; version is easier to work with.&lt;/p&gt;
&lt;h3 id=&quot;why-sql-readability-matters&quot;&gt;Why SQL Readability Matters&lt;/h3&gt;
&lt;p&gt;SQL isn't a private notebook. It's a shared language between developers, analysts, and database administrators — often people who didn't write the original query and have no context for it.&lt;/p&gt;
&lt;p&gt;Readable SQL matters because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You'll debug faster.&lt;/strong&gt; When a report breaks at 2 AM, you don't want to spend twenty minutes just figuring out what a query does before you can even start fixing it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code reviews go quicker.&lt;/strong&gt; A reviewer can catch a logic error in seconds when the structure is clear. They can't catch anything in a 400-character single line.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Onboarding is smoother.&lt;/strong&gt; New team members inherit your queries. Clean formatting is a small kindness that saves them hours.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mistakes get caught earlier.&lt;/strong&gt; Misplaced &lt;code&gt;AND&lt;/code&gt;/&lt;code&gt;OR&lt;/code&gt; logic, missing join conditions, and typo'd column names all hide more easily in dense, unformatted SQL.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's a quick taste of what a difference formatting makes, even for a simple &lt;code&gt;SELECT&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select id,name,email,created_at from users where status='active' and created_at&amp;gt;'2023-01-01' order by created_at desc;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
    id,
    name,
    email,
    created_at
FROM users
WHERE status = 'active'
    AND created_at &amp;gt; '2023-01-01'
ORDER BY created_at DESC;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same query, same result set — but the second version tells you what's happening at a glance. Each column has its own line, each clause starts on its own line, and the keywords stand out from the data. That's the whole game.&lt;/p&gt;
&lt;h3 id=&quot;why-poorly-formatted-sql-becomes-hard-to-maintain&quot;&gt;Why Poorly Formatted SQL Becomes Hard to Maintain&lt;/h3&gt;
&lt;p&gt;Unformatted SQL doesn't just look messy — it actively works against you over time.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Git diffs turn into noise.&lt;/strong&gt; If your whole query lives on one line, changing a single condition rewrites the entire line in version control, making it impossible to see what actually changed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logic errors hide in plain sight.&lt;/strong&gt; A missing parenthesis or a misplaced &lt;code&gt;OR&lt;/code&gt; is easy to miss when everything blends together.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nested queries lose their shape.&lt;/strong&gt; Without indentation, you can't tell which &lt;code&gt;SELECT&lt;/code&gt; belongs to which level of nesting.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every edit gets riskier.&lt;/strong&gt; When you can't quickly understand a query, you're more likely to introduce a bug while changing it.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Common Mistake:&lt;/strong&gt; Mixing tabs and spaces in the same file. It looks fine in your editor but turns into a jagged mess in anyone else's, since editors render tab width differently.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;the-real-benefits-of-consistent-sql-formatting&quot;&gt;The Real Benefits of Consistent SQL Formatting&lt;/h3&gt;
&lt;p&gt;Consistent formatting compounds over time. A few concrete payoffs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Faster reviews.&lt;/strong&gt; Reviewers spend their attention on logic, not on decoding structure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Predictable structure.&lt;/strong&gt; Once you know the pattern, you can scan any query on the team and know where to look for the &lt;code&gt;WHERE&lt;/code&gt; clause, the joins, or the aggregation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cleaner diffs.&lt;/strong&gt; One clause or column per line means version control shows exactly what changed — nothing more.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fewer &quot;wait, what does this do?&quot; moments&lt;/strong&gt; during incident response, when you least want to be puzzling over syntax.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;common-sql-formatting-mistakes-developers-make&quot;&gt;Common SQL Formatting Mistakes Developers Make&lt;/h3&gt;
&lt;p&gt;Before we get into the fixes, here's what tends to go wrong most often:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Writing the entire query on a single line.&lt;/li&gt;
&lt;li&gt;Random, inconsistent indentation from line to line.&lt;/li&gt;
&lt;li&gt;Inconsistent capitalization — &lt;code&gt;Select&lt;/code&gt;, &lt;code&gt;SELECT&lt;/code&gt;, and &lt;code&gt;select&lt;/code&gt; all in the same file.&lt;/li&gt;
&lt;li&gt;Vague, meaningless aliases like &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, &lt;code&gt;t1&lt;/code&gt;, &lt;code&gt;t2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;No spacing around operators (&lt;code&gt;total&amp;gt;100&lt;/code&gt; instead of &lt;code&gt;total &amp;gt; 100&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Mixing comma-first and comma-last styles in the same codebase.&lt;/li&gt;
&lt;li&gt;Deeply nested subqueries with no indentation to show the hierarchy.&lt;/li&gt;
&lt;li&gt;Skipping comments on non-obvious business logic.&lt;/li&gt;
&lt;li&gt;Lines so long you have to scroll horizontally to read them.&lt;/li&gt;
&lt;li&gt;Join conditions that aren't aligned or grouped clearly.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If a few of those look familiar, don't worry — the rest of this guide fixes each one.&lt;/p&gt;
&lt;h3 id=&quot;proper-indentation-techniques&quot;&gt;Proper Indentation Techniques&lt;/h3&gt;
&lt;p&gt;Indentation is the backbone of readable SQL. The rule is simple: &lt;strong&gt;subordinate clauses and conditions get indented under the keyword they belong to.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Pick either 2 or 4 spaces (never tabs, since they render inconsistently across editors and terminals) and stick with it everywhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select * from orders where (status='pending' or status='processing') and customer_id in (select id from customers where region='west' and active=1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT *
FROM orders
WHERE (status = 'pending' OR status = 'processing')
    AND customer_id IN (
        SELECT id
        FROM customers
        WHERE region = 'west'
            AND active = 1
    );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; Indent each subquery one level deeper than its parent. That way, nesting depth is something you can see, not something you have to mentally track.&lt;/p&gt;
&lt;h3 id=&quot;formatting-select-statements&quot;&gt;Formatting SELECT Statements&lt;/h3&gt;
&lt;p&gt;Once you have more than two or three columns, put each one on its own line. It's easier to scan, and it means adding or removing a column later only touches one line in your diff.&lt;/p&gt;
&lt;p&gt;You'll see two common styles for commas:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Comma-last&lt;/strong&gt; (most common, reads like prose):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
    id,
    name,
    email
FROM users;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Comma-first&lt;/strong&gt; (makes missing or trailing commas easy to spot):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
    id
    , name
    , email
FROM users;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both are valid. What matters is picking one and using it consistently across your team — mixing the two in the same codebase is what actually causes confusion.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; Avoid &lt;code&gt;SELECT *&lt;/code&gt; outside of quick, exploratory queries. An explicit column list is easier to read, easier to review, and won't silently break your application if someone adds a column to the table later.&lt;/p&gt;
&lt;h3 id=&quot;formatting-where-clauses&quot;&gt;Formatting WHERE Clauses&lt;/h3&gt;
&lt;p&gt;Every condition gets its own line. Boolean operators (&lt;code&gt;AND&lt;/code&gt;, &lt;code&gt;OR&lt;/code&gt;) go at the start of the line, indented under &lt;code&gt;WHERE&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT * FROM orders WHERE status='shipped' AND total&amp;gt;100 AND customer_id IS NOT NULL AND created_at BETWEEN '2024-01-01' AND '2024-12-31' AND region IN ('west','east','central');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT *
FROM orders
WHERE status = 'shipped'
    AND total &amp;gt; 100
    AND customer_id IS NOT NULL
    AND created_at BETWEEN '2024-01-01' AND '2024-12-31'
    AND region IN ('west', 'east', 'central');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you can scan down the left edge and read the filter logic like a checklist, instead of parsing one long run-on sentence.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; When you're mixing &lt;code&gt;AND&lt;/code&gt; and &lt;code&gt;OR&lt;/code&gt; in the same clause, always use parentheses — even when operator precedence would technically get it right without them. It removes any doubt for the next reader.&lt;/p&gt;
&lt;h3 id=&quot;formatting-joins&quot;&gt;Formatting JOINs&lt;/h3&gt;
&lt;p&gt;Always use explicit &lt;code&gt;JOIN ... ON&lt;/code&gt; syntax instead of old-style comma joins. Put each join on its own line, with the &lt;code&gt;ON&lt;/code&gt; condition right after it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select o.id, c.name, p.title, o.total from orders o, customers c, products p, order_items oi where o.customer_id=c.id and oi.order_id=o.id and oi.product_id=p.id and o.status='completed';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
    o.id,
    c.name,
    p.title,
    o.total
FROM orders o
INNER JOIN customers c ON c.id = o.customer_id
INNER JOIN order_items oi ON oi.order_id = o.id
INNER JOIN products p ON p.id = oi.product_id
WHERE o.status = 'completed';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Explicit joins separate &quot;how tables connect&quot; from &quot;what rows I want,&quot; which makes the whole query easier to trace top to bottom. You can also add or remove a join without touching the &lt;code&gt;WHERE&lt;/code&gt; clause at all.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Common Mistake:&lt;/strong&gt; Comma joins put join logic and filter logic in the same place. Forget one join condition, and you've accidentally created a cartesian product — a silent bug that can quietly multiply your row count.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;formatting-group-by-order-by-and-having&quot;&gt;Formatting GROUP BY, ORDER BY, and HAVING&lt;/h3&gt;
&lt;p&gt;Keep every major clause — &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;FROM&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;GROUP BY&lt;/code&gt;, &lt;code&gt;HAVING&lt;/code&gt;, &lt;code&gt;ORDER BY&lt;/code&gt; — starting at the same indentation level (column zero). This &quot;clause skeleton&quot; lets you see the shape of a query in about two seconds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select customer_id, count(*) as order_count, sum(total) as total_spent from orders where status='completed' group by customer_id having sum(total)&amp;gt;500 order by total_spent desc;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
    customer_id,
    COUNT(*) AS order_count,
    SUM(total) AS total_spent
FROM orders
WHERE status = 'completed'
GROUP BY customer_id
HAVING SUM(total) &amp;gt; 500
ORDER BY total_spent DESC;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you're grouping or ordering by several columns, break those out too:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;GROUP BY
    customer_id,
    region,
    order_year
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;formatting-nested-queries-and-subqueries&quot;&gt;Formatting Nested Queries and Subqueries&lt;/h3&gt;
&lt;p&gt;Each level of nesting gets its own indentation level. This is where formatting discipline pays off the most, because nested logic is exactly where bugs like to hide.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select name from products where id in (select product_id from order_items where order_id in (select id from orders where customer_id=42));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT name
FROM products
WHERE id IN (
    SELECT product_id
    FROM order_items
    WHERE order_id IN (
        SELECT id
        FROM orders
        WHERE customer_id = 42
    )
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; If you find yourself nesting more than two or three levels deep, that's usually a sign to reach for a CTE instead. It's the same logic, but far easier to read top to bottom.&lt;/p&gt;
&lt;h3 id=&quot;formatting-common-table-expressions-ctes-&quot;&gt;Formatting Common Table Expressions (CTEs)&lt;/h3&gt;
&lt;p&gt;CTEs turn nested, hard-to-follow subqueries into named, sequential steps. Here's the same logic from above, rewritten as a CTE:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH customer_orders AS (
    SELECT id
    FROM orders
    WHERE customer_id = 42
),
ordered_items AS (
    SELECT product_id
    FROM order_items
    WHERE order_id IN (SELECT id FROM customer_orders)
)
SELECT name
FROM products
WHERE id IN (SELECT product_id FROM ordered_items);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A few formatting habits that make CTEs easier to work with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Name each CTE for what it represents, not &lt;code&gt;cte1&lt;/code&gt;, &lt;code&gt;cte2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Indent the body of each CTE one level in, and keep the final &lt;code&gt;SELECT&lt;/code&gt; unindented so it's obvious that's the main query.&lt;/li&gt;
&lt;li&gt;Put a blank line between CTEs when you have more than one — it gives your eyes a place to rest between logical steps.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;aliasing-tables-and-columns-clearly&quot;&gt;Aliasing Tables and Columns Clearly&lt;/h3&gt;
&lt;p&gt;Once you're joining more than one table, aliases stop being optional in practice. Use short, meaningful aliases — &lt;code&gt;o&lt;/code&gt; for &lt;code&gt;orders&lt;/code&gt;, &lt;code&gt;c&lt;/code&gt; for &lt;code&gt;customers&lt;/code&gt; — rather than arbitrary letters like &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; that force the reader to scroll back up to the &lt;code&gt;FROM&lt;/code&gt; clause to remember what's what.&lt;/p&gt;
&lt;p&gt;Also alias every computed or aggregated column with &lt;code&gt;AS&lt;/code&gt;, so the result set is self-explanatory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
    o.id,
    COUNT(oi.id) AS item_count,
    SUM(oi.price * oi.quantity) AS order_total
FROM orders o
INNER JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without &lt;code&gt;AS order_total&lt;/code&gt;, that column would show up as some generated, meaningless expression in your output.&lt;/p&gt;
&lt;h3 id=&quot;consistent-capitalization-strategies&quot;&gt;Consistent Capitalization Strategies&lt;/h3&gt;
&lt;p&gt;The most common convention: &lt;strong&gt;uppercase for SQL keywords, lowercase or snake_case for identifiers.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT customer_id, order_date
FROM orders
WHERE status = 'completed';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some teams — particularly in the modern data/analytics engineering world — prefer everything lowercase, including keywords:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select customer_id, order_date
from orders
where status = 'completed';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both are legitimate SQL style guide choices. What actually matters is picking one for your team and applying it everywhere, so readers don't have to context-switch between styles from file to file.&lt;/p&gt;
&lt;h3 id=&quot;spacing-conventions&quot;&gt;Spacing Conventions&lt;/h3&gt;
&lt;p&gt;Small, consistent spacing habits go a long way:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Space around comparison and arithmetic operators: &lt;code&gt;total &amp;gt; 100&lt;/code&gt;, not &lt;code&gt;total&amp;gt;100&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Space after every comma: &lt;code&gt;id, name, email&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;No space between a function name and its parenthesis: &lt;code&gt;COUNT(*)&lt;/code&gt;, not &lt;code&gt;COUNT (*)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;One space before and after &lt;code&gt;AS&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Avoid
SELECT id,name,COUNT (*) AS cnt FROM orders WHERE total&amp;gt;=100;

-- Prefer
SELECT id, name, COUNT(*) AS cnt
FROM orders
WHERE total &amp;gt;= 100;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;line-breaks&quot;&gt;Line Breaks&lt;/h3&gt;
&lt;p&gt;Use a new line for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every major clause (&lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;FROM&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt;, &lt;code&gt;GROUP BY&lt;/code&gt;, &lt;code&gt;ORDER BY&lt;/code&gt;, &lt;code&gt;HAVING&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Every column, once you have more than two or three.&lt;/li&gt;
&lt;li&gt;Every &lt;code&gt;JOIN&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Each &lt;code&gt;CTE&lt;/code&gt;, with a blank line separating them for visual breathing room.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal isn't &quot;more line breaks are always better&quot; — it's giving each logical piece of the query its own visual space so your eyes can jump straight to the part you need.&lt;/p&gt;
&lt;h3 id=&quot;formatting-case-statements&quot;&gt;Formatting CASE Statements&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;CASE&lt;/code&gt; expressions get unreadable fast when crammed onto one line. Break each &lt;code&gt;WHEN&lt;/code&gt; onto its own line, indented under &lt;code&gt;CASE&lt;/code&gt;, with &lt;code&gt;END&lt;/code&gt; aligned back at the &lt;code&gt;CASE&lt;/code&gt; level.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select id, case when total&amp;gt;1000 then 'high' when total&amp;gt;500 then 'medium' else 'low' end as tier from orders;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
    id,
    CASE
        WHEN total &amp;gt; 1000 THEN 'high'
        WHEN total &amp;gt; 500 THEN 'medium'
        ELSE 'low'
    END AS tier
FROM orders;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This layout makes it trivial to scan the conditions in order and confirm the logic is doing what you expect — especially useful when there are five or six &lt;code&gt;WHEN&lt;/code&gt; branches instead of two.&lt;/p&gt;
&lt;h3 id=&quot;formatting-window-functions-briefly-&quot;&gt;Formatting Window Functions (Briefly)&lt;/h3&gt;
&lt;p&gt;Window functions can get long fast once you add &lt;code&gt;PARTITION BY&lt;/code&gt; and &lt;code&gt;ORDER BY&lt;/code&gt; inside the &lt;code&gt;OVER()&lt;/code&gt; clause. Break those onto their own indented lines so the window definition doesn't blend into the rest of the &lt;code&gt;SELECT&lt;/code&gt; list:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
    customer_id,
    order_date,
    total,
    SUM(total) OVER (
        PARTITION BY customer_id
        ORDER BY order_date
    ) AS running_total
FROM orders;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This keeps the &quot;what am I calculating&quot; (&lt;code&gt;SUM(total)&lt;/code&gt;) visually separate from the &quot;how is it grouped and ordered&quot; (the &lt;code&gt;OVER()&lt;/code&gt; clause) — two different ideas that shouldn't be squeezed into one dense line.&lt;/p&gt;
&lt;h3 id=&quot;commenting-sql-code-effectively&quot;&gt;Commenting SQL Code Effectively&lt;/h3&gt;
&lt;p&gt;Good comments explain &lt;strong&gt;why&lt;/strong&gt;, not &lt;strong&gt;what&lt;/strong&gt;. The SQL itself already tells the reader what's happening; your comments should fill in context that isn't obvious from the syntax alone.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Excludes test accounts flagged by the fraud team in March 2024
SELECT *
FROM users
WHERE is_test_account = 0
    AND created_at &amp;gt; '2024-03-01';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;--&lt;/code&gt; for short, single-line notes and &lt;code&gt;/* ... */&lt;/code&gt; for longer explanations above a complex section of logic.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Common Mistake:&lt;/strong&gt; Leaving a strange-looking filter or condition uncommented. Six months from now, nobody — including you — will remember why &lt;code&gt;region != 'archived'&lt;/code&gt; needed to be there, and someone will waste an hour rediscovering it (or worse, delete it and break something).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;team-coding-standards-and-sql-style-guides&quot;&gt;Team Coding Standards and SQL Style Guides&lt;/h3&gt;
&lt;p&gt;Formatting debates burn a surprising amount of time in code review — &quot;tabs vs. spaces,&quot; &quot;comma-first vs. comma-last,&quot; &quot;uppercase vs. lowercase.&quot; A short, documented SQL style guide settles these once, instead of relitigating them in every pull request.&lt;/p&gt;
&lt;p&gt;A useful team style guide covers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Capitalization rules for keywords and identifiers.&lt;/li&gt;
&lt;li&gt;Indentation size (2 or 4 spaces).&lt;/li&gt;
&lt;li&gt;Comma placement.&lt;/li&gt;
&lt;li&gt;Alias naming conventions.&lt;/li&gt;
&lt;li&gt;Table and column naming conventions (snake_case is the most common default).&lt;/li&gt;
&lt;li&gt;How and when to use CTEs versus subqueries.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keep it short — a single &lt;code&gt;STYLE.md&lt;/code&gt; file in your repository is often enough. The goal isn't a rulebook; it's a shared reference so reviewers can say &quot;see the style guide&quot; instead of relitigating formatting from scratch every time.&lt;/p&gt;
&lt;h3 id=&quot;when-automatic-formatting-is-useful&quot;&gt;When Automatic Formatting Is Useful&lt;/h3&gt;
&lt;p&gt;Manually reformatting a long legacy query, or something a teammate pasted in from a spreadsheet export, is tedious — and it's easy to introduce a typo while you're at it.&lt;/p&gt;
&lt;p&gt;That's where automatic formatting tools earn their keep. An online &lt;a rel=&quot;nofollow&quot; href=&quot;https://99tools.net/sql-formatter-beautifier/&quot;&gt;SQL Formatter&lt;/a&gt; can take a dense, single-line query and instantly restructure the indentation, casing, and spacing for you. It's especially handy for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cleaning up inherited or legacy scripts you didn't write.&lt;/li&gt;
&lt;li&gt;Standardizing formatting across a team without manual nitpicking in every review.&lt;/li&gt;
&lt;li&gt;Quickly making sense of a query someone pasted into a chat message.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Automatic formatting is a great starting point, but it's still worth understanding the underlying rules yourself. That way you can fine-tune the output, format queries on the fly at the terminal, and actually catch logic issues instead of just relying on a tool to make things look tidy.&lt;/p&gt;
&lt;h3 id=&quot;common-formatting-conventions-followed-by-professional-developers&quot;&gt;Common Formatting Conventions Followed by Professional Developers&lt;/h3&gt;
&lt;p&gt;If you want a quick checklist to build habits around, most professional SQL style guides agree on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Uppercase keywords, lowercase snake_case identifiers.&lt;/li&gt;
&lt;li&gt;2 or 4 space indentation — never tabs.&lt;/li&gt;
&lt;li&gt;One clause per line.&lt;/li&gt;
&lt;li&gt;One column per line once you're past two or three columns.&lt;/li&gt;
&lt;li&gt;Explicit &lt;code&gt;JOIN&lt;/code&gt; syntax, never comma joins.&lt;/li&gt;
&lt;li&gt;Meaningful, consistent table aliases.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AS&lt;/code&gt; for every derived or aggregated column.&lt;/li&gt;
&lt;li&gt;Parentheses around mixed &lt;code&gt;AND&lt;/code&gt;/&lt;code&gt;OR&lt;/code&gt; logic, even when not strictly required.&lt;/li&gt;
&lt;li&gt;Trailing semicolons on every statement.&lt;/li&gt;
&lt;li&gt;Comments that explain business context, not syntax.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;frequently-asked-questions&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Does SQL formatting affect query performance?&lt;/strong&gt;&lt;br&gt;
No. Whitespace, line breaks, and capitalization don't change how the database engine parses or executes a query. Formatting is entirely for the benefit of the humans reading the code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Should SQL keywords always be uppercase?&lt;/strong&gt;&lt;br&gt;
It's not required, but it's a widely used convention because it visually separates syntax (&lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt;) from your actual data — table and column names. Pick uppercase or lowercase and apply it consistently.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What's the difference between comma-first and comma-last formatting?&lt;/strong&gt;&lt;br&gt;
Comma-last (the comma at the end of a line) reads more like natural prose. Comma-first (the comma at the start of the next line) makes it easier to spot a missing or accidental trailing comma at a glance. Both are valid — consistency across your team matters more than which one you choose.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How many spaces should I use for indentation in SQL?&lt;/strong&gt;&lt;br&gt;
Two or four spaces are both common choices. Four spaces makes nested subqueries more visually distinct; two spaces keeps deeply nested queries from drifting too far to the right. Avoid tabs, since they render at different widths across editors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Should I always alias tables, even in single-table queries?&lt;/strong&gt;&lt;br&gt;
It's not necessary for a single-table query, but it becomes important as soon as you join a second table, since it removes any ambiguity about which table a column belongs to.&lt;/p&gt;
&lt;p&gt;*&lt;em&gt;Is it okay to use SELECT &lt;/em&gt; in formatted SQL?**&lt;br&gt;
It works fine for quick, exploratory queries, but it's generally discouraged in production code or reports. An explicit column list is easier to review and won't silently change if someone adds a column to the underlying table later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How do I format SQL in a way that's friendly to version control?&lt;/strong&gt;&lt;br&gt;
Keep each clause and each column on its own line. That way, a small change — like adding one more column — shows up as a single-line diff instead of rewriting an entire block, which makes reviews much faster.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can I mix tabs and spaces in the same SQL file?&lt;/strong&gt;&lt;br&gt;
No. Mixed tabs and spaces render inconsistently across different editors and terminals, and it's one of the most common causes of formatting drift on a team. Pick one and configure your editor to enforce it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do CTEs perform better than subqueries?&lt;/strong&gt;&lt;br&gt;
Not inherently — performance depends on the specific database engine and how it optimizes the query plan. CTEs are primarily a readability tool that lets you break complex logic into named, sequential, easy-to-follow steps.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What's the best way to enforce consistent formatting across a team?&lt;/strong&gt;&lt;br&gt;
Write down your conventions in a short style guide, use an automatic formatter as a shared starting point, and check formatting during code review (or with a pre-commit hook) so it doesn't slip through.&lt;/p&gt;
&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h3&gt;
&lt;p&gt;SQL formatting isn't about being precious over style — it's a maintenance investment that pays off every time someone (including you) has to read, debug, or extend a query later.&lt;/p&gt;
&lt;p&gt;A few habits carry most of the value:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One clause per line, one column per line.&lt;/li&gt;
&lt;li&gt;Consistent capitalization and indentation.&lt;/li&gt;
&lt;li&gt;Explicit joins with clear, meaningful aliases.&lt;/li&gt;
&lt;li&gt;Comments that explain the &quot;why,&quot; not the &quot;what.&quot;&lt;/li&gt;
&lt;li&gt;Parentheses whenever &lt;code&gt;AND&lt;/code&gt; and &lt;code&gt;OR&lt;/code&gt; mix.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Build these into your daily workflow and your queries — and your team's — get easier to review, debug, and hand off over time. And when you're short on time or inheriting a messy legacy query, running it through an online SQL Formatter first gives you a clean, consistent starting point you can fine-tune by hand from there.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/22157/how-to-format-sql-queries-for-better-readability</guid>
<pubDate>Thu, 09 Jul 2026 12:43:06 +0000</pubDate>
</item>
<item>
<title>Working with nested JSON: exploring it, mocking it, and querying it in Postgres</title>
<link>https://coderlegion.com/21844/working-with-nested-json-exploring-it-mocking-it-and-querying-it-in-postgres</link>
<description>&lt;p&gt;Flat JSON is easy. Nested JSON is where the day goes: an API response with orders inside customers inside accounts, a jsonb column three levels deep, a config file that grew a tree. Three tasks come up over and over, and each one has a trick that makes it painless. Here they are.&lt;/p&gt;
&lt;h3 id=&quot;1-reading-a-huge-nested-payload&quot;&gt;1. Reading a huge nested payload&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=5919876841020931874&quot; alt=&quot;Tree View&quot;&gt;&lt;/p&gt;
&lt;p&gt;Pretty-printing a 4,000-line response does not actually help. The indentation is correct, but finding one thing still means scrolling through thousands of lines. The fix is to stop treating JSON as text to scroll and start treating it as data to query.&lt;/p&gt;
&lt;p&gt;Say the question is: does any item in this order have a null price?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;account&quot;: {
    &quot;id&quot;: 91,
    &quot;customer&quot;: {
      &quot;orders&quot;: [
        { &quot;id&quot;: 4821, &quot;items&quot;: [ { &quot;sku&quot;: &quot;A-19&quot;, &quot;price&quot;: 12.5 } ] },
        { &quot;id&quot;: 4822, &quot;items&quot;: [ { &quot;sku&quot;: &quot;B-04&quot;, &quot;price&quot;: null } ] }
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three moves turn this from a scroll into a lookup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Filter by path, key, type, or value. Filtering to price pulls every price field out of the whole tree at once. Filtering by the value null jumps straight to the offending row. The needle in 4,000 lines becomes a single filtered result.&lt;/li&gt;
&lt;li&gt;Select the nesting level. Instead of expanding everything, expand the tree to level 2 to see the shape of the data, then drill only into the branch that matters. This is the difference between reading structure and drowning in leaves.&lt;/li&gt;
&lt;li&gt;Switch to a table view when a branch is really a list of records. Two hundred objects with the same keys is a spreadsheet pretending to be JSON, and reading it as rows and columns is far faster than reading it as nested braces.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once JSON is filterable and foldable, payload size stops mattering.&lt;/p&gt;
&lt;h3 id=&quot;2-generating-realistic-nested-json-fast&quot;&gt;2. Generating realistic nested JSON, fast&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=4516247635221435725&quot; alt=&quot;Generate Mock JSON&quot;&gt;&lt;/p&gt;
&lt;p&gt;The opposite problem: there is no payload yet, and building a parser, a UI, or a test needs one. Hand-writing mock data is slow, and worse, it is too clean. Every record ends up with the same id, the same date, the same shape, which is exactly the uniformity that hides off-by-one bugs, dedupe bugs, and sort bugs.&lt;/p&gt;
&lt;p&gt;Two shortcuts cover most cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Load a realistic nested sample. Start from a ready-made nested document (an order, a log line, an analytics event) and adapt it, instead of inventing structure from scratch.&lt;/li&gt;
&lt;li&gt;Multiply one object into many varied records. Write a single object by hand, pick a count, and get back an array where fields like id, email, and dates actually vary per record. That variation is the point: a list of 50 rows that are genuinely different is what surfaces the bug that 50 identical rows would hide.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Realistic nested test data in seconds beats a hand-typed array of clones every time.&lt;/p&gt;
&lt;h3 id=&quot;3-querying-and-updating-nested-json-in-postgres&quot;&gt;3. Querying and updating nested JSON in Postgres&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=14815475663515361781&quot; alt=&quot;Querying in PostgreSQL&quot;&gt;&lt;/p&gt;
&lt;p&gt;Eventually that nested JSON lands in a jsonb column, and a viewer cannot help anymore. This is where people get stuck, so here is the practical core.&lt;/p&gt;
&lt;p&gt;Assume a table orders with a jsonb column data holding the payload above.&lt;/p&gt;
&lt;p&gt;Selecting. The two operators to internalize are &lt;code&gt;-&amp;gt;&lt;/code&gt; and &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt;. &lt;code&gt;-&amp;gt;&lt;/code&gt; returns JSON, &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; returns text:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- returns jsonb
SELECT data -&amp;gt; 'account' -&amp;gt; 'customer' FROM orders;

-- returns text (note the final -&amp;gt;&amp;gt;)
SELECT data -&amp;gt; 'account' -&amp;gt;&amp;gt; 'id' AS account_id FROM orders;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a deep path, &lt;code&gt;#&amp;gt;&lt;/code&gt; and &lt;code&gt;#&amp;gt;&amp;gt;&lt;/code&gt; take an array of keys and are much cleaner than chaining arrows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- text value deep in the structure
SELECT data #&amp;gt;&amp;gt; '{account,customer,orders,0,id}' AS first_order_id
FROM orders;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To filter rows by a nested value, containment (&lt;code&gt;@&amp;gt;&lt;/code&gt;) is both readable and indexable with a GIN index:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM orders
WHERE data @&amp;gt; '{&quot;account&quot;: {&quot;id&quot;: 91}}';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And to work across an array, expand it into rows with &lt;code&gt;jsonb_array_elements&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT elem -&amp;gt;&amp;gt; 'id' AS order_id
FROM orders,
     jsonb_array_elements(data #&amp;gt; '{account,customer,orders}') AS elem;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Updating. The workhorse is &lt;code&gt;jsonb_set(target, path, new_value)&lt;/code&gt;. The gotcha that trips everyone: &lt;code&gt;new_value&lt;/code&gt; must itself be &lt;code&gt;jsonb&lt;/code&gt;, so cast it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- set a deep scalar (note the ::jsonb cast)
UPDATE orders
SET data = jsonb_set(data, '{account,customer,orders,0,id}', '9001'::jsonb)
WHERE id = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add or replace a key at a level with the || merge operator, and remove a path with &lt;code&gt;#-&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- shallow-merge a new key into the top level
UPDATE orders SET data = data || '{&quot;archived&quot;: true}'::jsonb WHERE id = 1;

-- delete a nested key
UPDATE orders SET data = data #- '{account,customer,orders,0,items}' WHERE id = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One honest warning: &lt;code&gt;||&lt;/code&gt; merges only one level deep, so it will not deep-merge nested objects. For nested changes, reach for &lt;code&gt;jsonb_set&lt;/code&gt;. And the genuinely hard case, updating a specific array element chosen by a condition rather than a fixed index, needs more than &lt;code&gt;jsonb_set&lt;/code&gt; alone. That is a whole topic on its own, and it is the one worth reading a dedicated write-up on before you fight it in production.&lt;/p&gt;
&lt;p&gt;Here is the honest reason all of this matters: nobody keeps these operators in their head. It is &lt;code&gt;-&amp;gt;&lt;/code&gt; versus &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt;, the &lt;code&gt;#&amp;gt;&amp;gt;&lt;/code&gt; path syntax, the &lt;code&gt;::jsonb&lt;/code&gt; cast that &lt;code&gt;jsonb_set&lt;/code&gt; quietly requires, the fact that &lt;code&gt;||&lt;/code&gt; only merges one level. I forget them between projects every single time. So I wrote them down as a permanent, on-page reference next to the formatter: &lt;a rel=&quot;nofollow&quot; href=&quot;https://catssaymeow.org/json-formatter/#querying-json-in-postgresql&quot;&gt;Querying nested JSON in PostgreSQL (SELECT and UPDATE)&lt;/a&gt; collects the common reads and writes in one place, for the moment memory does not serve. It is reference for the everyday cases, not a full Postgres manual.&lt;/p&gt;
&lt;h3 id=&quot;where-the-tool-fits&quot;&gt;Where the tool fits&lt;/h3&gt;
&lt;p&gt;I built all three of these into one place, at &lt;a rel=&quot;nofollow&quot; href=&quot;https://catssaymeow.org/json-formatter/&quot;&gt;catssaymeow.org/json-formatter&lt;/a&gt;, because I got tired of one tab that only pretty-prints and another that only explores.&lt;/p&gt;
&lt;p&gt;It formats, validates, and minifies (invalid JSON is pinpointed by line and column, not a bare &quot;invalid&quot;), and then lets you actually work with the structure: explore it as a tree, graph, or table, filter by path, key, type, or value, and expand to a chosen nesting level. When there is no data to work with yet, it can load a realistic nested sample or multiply one object into an array of varied mock records. It runs as client-side JavaScript, so the JSON stays in the browser, and clean JSON can be sent straight to a built-in API Client as a request body.&lt;/p&gt;
&lt;p&gt;And for the part that happens after the database call, the same page carries a &lt;a rel=&quot;nofollow&quot; href=&quot;https://catssaymeow.org/json-formatter/#querying-json-in-postgresql&quot;&gt;Postgres jsonb querying reference&lt;/a&gt; for the SELECT and UPDATE syntax that never sticks. Three recurring nested-JSON tasks, one page to come back to.&lt;/p&gt;
&lt;p&gt;If you put it up against your own worst nested payload, I would like to hear what it struggles with.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21844/working-with-nested-json-exploring-it-mocking-it-and-querying-it-in-postgres</guid>
<pubDate>Fri, 03 Jul 2026 13:52:48 +0000</pubDate>
</item>
<item>
<title>Chai aur SQL — A Beginner's Journey into Databases</title>
<link>https://coderlegion.com/21811/chai-aur-sql-a-beginners-journey-into-databases</link>
<description>&lt;hr&gt;
&lt;p&gt;title: Chai aur SQL — A Beginner's Journey into Databases&lt;br&gt;
published: true&lt;br&gt;
canonical_url: &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.abdulrdeveloper.me/chai-aur-sql-a-beginner-s-journey-into-databases&quot;&gt;https://blog.abdulrdeveloper.me/chai-aur-sql-a-beginner-s-journey-into-databases&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;tags-sql-postgresql-database-backend&quot;&gt;tags: sql, postgresql, database, backend&lt;/h3&gt;
&lt;p&gt;SQL stands for Structured Query Language. It is the language we use to talk to relational databases like MySQL, PostgreSQL, and SQLite.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-a-server-and-where-does-the-database-live-&quot;&gt;What is a Server and Where Does the Database Live?&lt;/h3&gt;
&lt;p&gt;Most beginners think the database is somewhere floating in the &quot;cloud.&quot; It is not.&lt;/p&gt;
&lt;p&gt;A database is software running on a server. A server is just a powerful computer that runs 24/7 inside a data center — like Amazon AWS, Google Cloud, or DigitalOcean. When that software saves your data, it writes it to actual files on the server's SSD or hard drive.&lt;/p&gt;
&lt;p&gt;MySQL and PostgreSQL save data as &lt;code&gt;.ibd&lt;/code&gt; files or WAL (Write-Ahead Log) files. MongoDB saves as &lt;code&gt;.bson&lt;/code&gt; files. These files sit on disk just like any file on your laptop.&lt;/p&gt;
&lt;p&gt;When you write this in your backend code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const pool = new Pool({ host: 'localhost', port: 5432 })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You are telling your backend to open a connection to the port where the database is listening. That is it. It is a TCP connection — like a phone call between your application and the database.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-docker-for-postgresql-&quot;&gt;Why Docker for PostgreSQL?&lt;/h3&gt;
&lt;p&gt;Instead of installing PostgreSQL directly on my system, I used Docker. Docker creates an isolated environment (called a container) so I do not mess up my laptop's setup.&lt;/p&gt;
&lt;p&gt;Here is the command I used to create a PostgreSQL container with persistent data:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mac / Linux / WSL:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run -d --name sql_class \
  -e POSTGRES_PASSWORD=postgres \
  -e POSTGRES_DB=sql_class_01_db \
  -p 5433:5432 \
  -v &quot;$(pwd)/pgdata:/var/lib/postgresql/data&quot; \
  postgres
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Windows (PowerShell):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;docker run -d --name sql_class -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=sql_class_01_db -p 5433:5432 -v &quot;${PWD}/pgdata:/var/lib/postgresql/data&quot; postgres
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;-v&lt;/code&gt; flag maps a &lt;code&gt;pgdata&lt;/code&gt; folder inside your current directory to the container's storage. So even if the container is deleted, your data stays on your laptop.&lt;/p&gt;
&lt;p&gt;To check if the container is running:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To remove the container:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker rm -f sql_class
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-learn-raw-sql-when-we-have-prisma-and-drizzle-&quot;&gt;Why Learn Raw SQL When We Have Prisma and Drizzle?&lt;/h3&gt;
&lt;p&gt;This was my first question. Why learn raw SQL when Prisma can generate the queries for me?&lt;/p&gt;
&lt;p&gt;Here is what I found out:&lt;/p&gt;
&lt;p&gt;ORMs like Prisma and Drizzle translate your JavaScript/TypeScript code into SQL strings and send them to the database. If you do not know SQL, you do not know what Prisma is actually doing behind the scene.&lt;/p&gt;
&lt;p&gt;The N+1 problem is a real thing. ORMs sometimes generate 100 queries instead of 1 optimized query. You will only catch this if you understand SQL.&lt;/p&gt;
&lt;p&gt;For complex queries — joining 6 tables, generating financial reports — ORM syntax breaks down. Developers fall back to raw SQL.&lt;/p&gt;
&lt;p&gt;And SQL has not changed in 50 years. Sequelize came and went. Prisma came. Drizzle came. SQL is still the same.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;ddl-how-you-create-tables&quot;&gt;DDL — How You Create Tables&lt;/h3&gt;
&lt;p&gt;DDL stands for Data Definition Language. These are the commands that define the structure of your database — not the data, just the structure.&lt;/p&gt;
&lt;p&gt;The four main DDL commands are &lt;code&gt;CREATE&lt;/code&gt;, &lt;code&gt;ALTER&lt;/code&gt;, &lt;code&gt;DROP&lt;/code&gt;, and &lt;code&gt;TRUNCATE&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here is a real table I created while practicing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE students (
    student_id SERIAL PRIMARY KEY,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50),
    email VARCHAR(100) UNIQUE NOT NULL,
    phone_number CHAR(10) UNIQUE,
    age INT CHECK (age &amp;gt; 12),
    current_status VARCHAR(20) DEFAULT 'active'
        CHECK (current_status IN ('active', 'graduated', 'dropped_out', 'on_leave')),
    has_joined_masterji BOOLEAN DEFAULT FALSE,
    current_score NUMERIC(5, 2) CHECK (current_score &amp;gt;= 0 AND current_score &amp;lt;= 100),
    enrollment_date DATE DEFAULT CURRENT_DATE
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SERIAL&lt;/code&gt; means auto-incrementing integer — PostgreSQL handles the ID for you. &lt;code&gt;NOT NULL&lt;/code&gt; means the column cannot be empty. &lt;code&gt;UNIQUE&lt;/code&gt; means no two rows can have the same value. &lt;code&gt;CHECK&lt;/code&gt; is a custom condition — here age must be greater than 12. &lt;code&gt;DEFAULT&lt;/code&gt; sets a value automatically if nothing is provided.&lt;/p&gt;
&lt;p&gt;To add a new column to an existing table later:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;ALTER TABLE students ADD COLUMN batch_name VARCHAR(50) DEFAULT 'Web Dev 2026';
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;select-queries-reading-your-data&quot;&gt;SELECT Queries — Reading Your Data&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;SELECT&lt;/code&gt; is the command to read data from a table.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Get everything
SELECT * FROM ipl_players;

-- Get specific columns
SELECT name, team, auction_price_crores FROM ipl_players;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Filtering with WHERE:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Basic condition
SELECT * FROM ipl_players WHERE team = 'Mumbai Indians';

-- Multiple conditions
SELECT * FROM ipl_players WHERE role = 'All-Rounder' AND wickets_taken &amp;gt; 10;

-- Either condition
SELECT * FROM ipl_players WHERE team = 'CSK' OR team = 'RCB';

-- Range
SELECT * FROM ipl_players WHERE auction_price_crores BETWEEN 5 AND 12;

-- From a list
SELECT * FROM ipl_players WHERE team NOT IN ('Mumbai Indians', 'CSK', 'RCB');

-- Pattern matching — names starting with R
SELECT * FROM ipl_players WHERE name LIKE 'R%';

-- Check for empty values
SELECT * FROM ipl_players WHERE team IS NULL;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;%&lt;/code&gt; means zero or more characters. &lt;code&gt;_&lt;/code&gt; means exactly one character. &lt;code&gt;LIKE&lt;/code&gt; is case-sensitive. &lt;code&gt;ILIKE&lt;/code&gt; is case-insensitive (PostgreSQL only).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sorting and limiting:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Sort by price, highest first
SELECT name, auction_price_crores FROM ipl_players ORDER BY auction_price_crores DESC;

-- Top 3 results
SELECT name, auction_price_crores FROM ipl_players ORDER BY auction_price_crores DESC LIMIT 3;

-- Skip first 3, get next 3 (pagination)
SELECT name, auction_price_crores FROM ipl_players ORDER BY auction_price_crores DESC LIMIT 3 OFFSET 3;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;dml-insert-update-delete&quot;&gt;DML — Insert, Update, Delete&lt;/h3&gt;
&lt;p&gt;DML stands for Data Manipulation Language. This is how you actually work with the data inside the tables.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;INSERT:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Single row
INSERT INTO canteen_menu (item_name, category, price)
VALUES ('Vada Pav', 'Snacks', 15);

-- Multiple rows at once
INSERT INTO canteen_menu (item_name, category, price) VALUES
('Masala Chai', 'Beverages', 10),
('Samosa', 'Snacks', 12),
('Maggi', 'Snacks', 25);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;UPDATE:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;UPDATE canteen_menu
SET price = 20
WHERE item_name = 'Vada Pav';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;DELETE:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;DELETE FROM canteen_menu WHERE item_name = 'Cold Coffee';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Always use &lt;code&gt;WHERE&lt;/code&gt; with &lt;code&gt;UPDATE&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt;. If you forget the WHERE clause, SQL will update or delete every single row in the table. I learned this the hard way in practice.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;aggregate-functions-count-sum-avg-max-min&quot;&gt;Aggregate Functions — COUNT, SUM, AVG, MAX, MIN&lt;/h3&gt;
&lt;p&gt;Aggregate functions take a whole column and return one number.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- How many rows?
SELECT COUNT(*) FROM smart_watch_sales;

-- Total revenue
SELECT SUM(units_sold * price_per_unit) AS total_revenue FROM smart_watch_sales;

-- Average price
SELECT AVG(price_per_unit) AS avg_price FROM smart_watch_sales;

-- Cheapest and most expensive
SELECT MIN(price_per_unit) AS cheapest, MAX(price_per_unit) AS costliest FROM smart_watch_sales;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;GROUP BY&lt;/code&gt; lets you split these calculations by category:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Total units sold per brand
SELECT brand, SUM(units_sold) AS total_units_sold
FROM smart_watch_sales
GROUP BY brand
ORDER BY total_units_sold DESC;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;HAVING&lt;/code&gt; filters the groups after they are created. &lt;code&gt;WHERE&lt;/code&gt; cannot do this because WHERE runs before grouping:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Only show brands that sold more than 20 units total
SELECT brand, SUM(units_sold) AS total_units
FROM smart_watch_sales
GROUP BY brand
HAVING SUM(units_sold) &amp;gt; 20;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;joins-connecting-multiple-tables&quot;&gt;Joins — Connecting Multiple Tables&lt;/h3&gt;
&lt;p&gt;This is where relational databases get powerful.&lt;/p&gt;
&lt;p&gt;We keep data in separate tables to avoid duplication. Students in one table, internships in another. Joins let you combine them in one query.&lt;/p&gt;
&lt;p&gt;Foreign Key is what connects the tables. The internships table has a &lt;code&gt;student_id&lt;/code&gt; column that references the &lt;code&gt;student_id&lt;/code&gt; in the students table:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE internships (
    internship_id SERIAL PRIMARY KEY,
    student_id INT REFERENCES students(student_id) ON DELETE SET NULL,
    company_name VARCHAR(100),
    role VARCHAR(50),
    stipend INT,
    status VARCHAR(20)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ON DELETE SET NULL&lt;/code&gt; means if a student is deleted, the internship record stays but &lt;code&gt;student_id&lt;/code&gt; becomes NULL. &lt;code&gt;ON DELETE CASCADE&lt;/code&gt; would delete the internship too.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;INNER JOIN — only matching rows:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT s.name, s.branch, i.company_name, i.role
FROM students s
INNER JOIN internships i ON s.student_id = i.student_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Students without internships will not show up here.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LEFT JOIN — all students, matched or not:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
    s.name,
    s.branch,
    COALESCE(i.company_name, 'No Internship') AS company_name,
    COALESCE(i.stipend, 0) AS stipend
FROM students s
LEFT JOIN internships i ON s.student_id = i.student_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;COALESCE&lt;/code&gt; replaces NULL with a default value. Students without internships will appear with 'No Internship' instead of NULL.&lt;/p&gt;
&lt;p&gt;To find students who have NOT applied anywhere:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT s.name, s.email
FROM students s
LEFT JOIN internships i ON s.student_id = i.student_id
WHERE i.internship_id IS NULL;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;RIGHT JOIN&lt;/strong&gt; returns all rows from the right table. In practice, developers rarely use RIGHT JOIN — they just flip the table order and use LEFT JOIN instead. It reads more naturally.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;FULL OUTER JOIN — everything from both tables:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT s.name AS student_name, i.company_name
FROM students s
FULL OUTER JOIN internships i ON s.student_id = i.student_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This shows all students and all internships. Where there is no match on either side, you get NULL.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;indexes-making-queries-fast&quot;&gt;Indexes — Making Queries Fast&lt;/h3&gt;
&lt;p&gt;I inserted 1,000,000 rows into a table to test this.&lt;/p&gt;
&lt;p&gt;Without an index, when you search by name, PostgreSQL scans every single row from top to bottom. This is called a Sequential Scan.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;EXPLAIN ANALYZE SELECT marks FROM marks WHERE name = '809E15792322';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; shows you the query plan and execution time. Before adding an index, my query took around 40-50 milliseconds on 1 million rows.&lt;/p&gt;
&lt;p&gt;After creating an index:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE INDEX idx_name ON marks (name);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same query dropped to under 1 millisecond. PostgreSQL now uses a B-Tree structure to find the row directly instead of scanning everything.&lt;/p&gt;
&lt;p&gt;A covering index goes one step further. If your query asks for &lt;code&gt;marks&lt;/code&gt; along with &lt;code&gt;name&lt;/code&gt;, you can store both in the index itself:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE INDEX idx_name ON marks (name) INCLUDE (marks);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now PostgreSQL does not even need to visit the main table. It gets everything from the index directly. This is called an Index Only Scan.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;transactions-and-acid&quot;&gt;Transactions and ACID&lt;/h3&gt;
&lt;p&gt;A transaction is a way to group multiple SQL operations into one unit of work. Either all of them succeed, or none of them do.&lt;/p&gt;
&lt;p&gt;The classic example is a bank transfer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;BEGIN;

    UPDATE accounts SET balance = balance - 500 WHERE owner = 'Shubham';
    UPDATE accounts SET balance = balance + 500 WHERE owner = 'Hitesh';

COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the server crashes between the two UPDATE statements, the database will rollback both changes. The money will not disappear from Shubham's account and fail to reach Hitesh.&lt;/p&gt;
&lt;p&gt;If something goes wrong, you can manually undo everything:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;BEGIN;
    UPDATE accounts SET balance = balance - 5000 WHERE owner = 'Shubham';
ROLLBACK;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;ACID&lt;/strong&gt; is the set of guarantees that make this work:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Atomicity&lt;/strong&gt; — All or nothing. If Step 2 fails, Step 1 is also undone. I tested this by putting &lt;code&gt;SELECT 1/0&lt;/code&gt; (division by zero error) in the middle of a transaction. The whole thing rolled back.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Consistency&lt;/strong&gt; — The database only moves from one valid state to another. I set &lt;code&gt;CHECK (balance &amp;gt;= 0)&lt;/code&gt; on the accounts table. When I tried to deduct 5000 from an account with 1000, PostgreSQL threw an error and refused the operation completely.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Isolation&lt;/strong&gt; — Uncommitted transactions are invisible to other connections. I opened two terminal windows to test this. Terminal 1 started a transaction and updated a balance but did not COMMIT. Terminal 2 could still see the old balance. Only after Terminal 1 ran &lt;code&gt;COMMIT&lt;/code&gt; did Terminal 2 see the new number.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Durability&lt;/strong&gt; — Once committed, data is permanent. PostgreSQL writes committed transactions to the Write-Ahead Log (WAL) on disk before returning a success response. Even if the server crashes one second after the COMMIT, the data will be there when the server restarts.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;All the code from this blog is in my GitHub repo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/abdulrdeveloper/Learn-SQL&quot;&gt;github.com/abdulrdeveloper/Learn-SQL&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;You can find more of my work at &lt;a rel=&quot;nofollow&quot; href=&quot;https://abdulrdeveloper.me&quot;&gt;abdulrdeveloper.me&lt;/a&gt;&lt;br&gt;
Read more posts at &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.abdulrdeveloper.me&quot;&gt;blog.abdulrdeveloper.me&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21811/chai-aur-sql-a-beginners-journey-into-databases</guid>
<pubDate>Fri, 03 Jul 2026 04:56:02 +0000</pubDate>
</item>
<item>
<title>Why not Sakila? Building a Modern SQL Learning Database for MariaDB</title>
<link>https://coderlegion.com/21426/why-not-sakila-building-a-modern-sql-learning-database-for-mariadb</link>
<description>&lt;p&gt;If you've taught SQL or learned it from a course, you've almost certainly met &lt;strong&gt;Sakila&lt;/strong&gt;. The little DVD rental database has been the go-to sample schema for MySQL and MariaDB tutorials for nearly two decades. It's clean, well-normalized, and comes pre-loaded with enough data to write interesting queries.&lt;/p&gt;
&lt;p&gt;But here's the thing: Sakila was designed in &lt;strong&gt;2006 for MySQL 5.0&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A lot has changed since then.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-sakila-can-t-show-you&quot;&gt;What Sakila can't show you&lt;/h3&gt;
&lt;p&gt;Open the Sakila schema today and count the column types: &lt;code&gt;INT&lt;/code&gt;, &lt;code&gt;VARCHAR&lt;/code&gt;, &lt;code&gt;TEXT&lt;/code&gt;, &lt;code&gt;ENUM&lt;/code&gt;, &lt;code&gt;DATETIME&lt;/code&gt;, &lt;code&gt;DECIMAL&lt;/code&gt;, &lt;code&gt;TINYINT&lt;/code&gt;. That's it. No &lt;code&gt;JSON&lt;/code&gt;. No &lt;code&gt;FULLTEXT&lt;/code&gt; search beyond a basic demo. No &lt;code&gt;SET&lt;/code&gt; type. And absolutely no &lt;code&gt;VECTOR&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Meanwhile, MariaDB 11.7 ships with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Native &lt;code&gt;VECTOR(N)&lt;/code&gt; type for AI-era similarity search&lt;/li&gt;
&lt;li&gt;Rich &lt;code&gt;JSON&lt;/code&gt; functions (&lt;code&gt;JSON_TABLE&lt;/code&gt;, &lt;code&gt;JSON_VALUE&lt;/code&gt;, &lt;code&gt;JSON_EXTRACT&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Window functions (&lt;code&gt;RANK&lt;/code&gt;, &lt;code&gt;LAG&lt;/code&gt;, &lt;code&gt;LEAD&lt;/code&gt;, running aggregates)&lt;/li&gt;
&lt;li&gt;Recursive CTEs&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FULLTEXT&lt;/code&gt; with boolean mode and relevance scoring&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Teaching someone SQL with Sakila in 2026 is like teaching someone to drive in a car with no GPS, no reversing camera, and a manual choke. The fundamentals still apply — but they're missing a huge chunk of what the tool can actually do.&lt;/p&gt;
&lt;p&gt;There's also the &lt;strong&gt;domain problem&lt;/strong&gt;. DVD rentals. If you're under 35, you may never have set foot in a video rental shop. The mental model is unfamiliar and the business rules feel arbitrary. What's the difference between a film, an inventory item, and a rental? Why does payment exist independently of rental? Explaining the schema takes time that should go into explaining SQL.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-domain-that-everyone-understands&quot;&gt;The domain that everyone understands&lt;/h3&gt;
&lt;p&gt;I needed something universal. Something with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Obvious entities and relationships&lt;/li&gt;
&lt;li&gt;Multiple natural hierarchies (good for recursive CTEs)&lt;/li&gt;
&lt;li&gt;A mix of small lookup tables and large transactional tables&lt;/li&gt;
&lt;li&gt;A clear reason for JSON (semi-structured data)&lt;/li&gt;
&lt;li&gt;An excuse to use vectors (semantic search is everywhere now)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A &lt;strong&gt;university&lt;/strong&gt; fits perfectly. Students, courses, faculty, enrollments, grades — everyone has lived inside this system. The relationships are intuitive. And the domain naturally produces the data shapes I needed:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Need &lt;/th&gt;&lt;th&gt;University equivalent&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Big analytic table &lt;/td&gt;&lt;td&gt;&lt;code&gt;grade_events&lt;/code&gt; — 120 000+ rows of scored items&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Hierarchy for recursive CTE &lt;/td&gt;&lt;td&gt;Departments (Faculty → Department → Sub-dept) and course prerequisites&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;JSON for semi-structured data &lt;/td&gt;&lt;td&gt;Faculty office hours, student emergency contacts, grant funding details&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;FULLTEXT search &lt;/td&gt;&lt;td&gt;Course descriptions, publication abstracts&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;VECTOR search &lt;/td&gt;&lt;td&gt;Course semantic embeddings for &quot;find similar courses&quot;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Audit trail &lt;/td&gt;&lt;td&gt;Every enrollment and grade change logged with JSON diffs&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-university-db-looks-like&quot;&gt;What University DB looks like&lt;/h3&gt;
&lt;p&gt;The schema has &lt;strong&gt;16 tables&lt;/strong&gt; in four tiers:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lookup&lt;/strong&gt; (tiny, &amp;lt; 100 rows): &lt;code&gt;semesters&lt;/code&gt;, &lt;code&gt;rooms&lt;/code&gt;, &lt;code&gt;scholarships&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Domain&lt;/strong&gt; (small, up to 2 000 rows): &lt;code&gt;departments&lt;/code&gt;, &lt;code&gt;faculty&lt;/code&gt;, &lt;code&gt;students&lt;/code&gt;, &lt;code&gt;courses&lt;/code&gt;, &lt;code&gt;course_prerequisites&lt;/code&gt;, &lt;code&gt;sections&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Transactional&lt;/strong&gt; (medium): &lt;code&gt;enrollments&lt;/code&gt;, &lt;code&gt;student_scholarships&lt;/code&gt;, &lt;code&gt;research_projects&lt;/code&gt;, &lt;code&gt;publications&lt;/code&gt;, &lt;code&gt;project_members&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Analytic&lt;/strong&gt; (big): &lt;code&gt;grade_events&lt;/code&gt; (~120k rows) and &lt;code&gt;audit_log&lt;/code&gt; (~60k rows, populated by triggers)&lt;/p&gt;
&lt;p&gt;Every significant MariaDB datatype appears at least once:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- VECTOR on courses — semantic embeddings for similarity search
embedding  VECTOR(1536) NULL

-- JSON on faculty — semi-structured office hours
office_hours JSON NULL
-- [{&quot;day&quot;:&quot;Mon&quot;,&quot;start&quot;:&quot;10:00&quot;,&quot;end&quot;:&quot;12:00&quot;}, ...]

-- SET on publications — multi-value keyword tags
keywords SET('AI','ML','Databases','Security','Bioinformatics', ...) 

-- FULLTEXT on courses and publications
FULLTEXT KEY ft_course (title, description)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The schema ships with &lt;strong&gt;7 views&lt;/strong&gt;, &lt;strong&gt;6 stored procedures&lt;/strong&gt;, and &lt;strong&gt;7 triggers&lt;/strong&gt; — including one that blocks enrollment when a section is full, and three that write JSON diffs to the &lt;code&gt;audit_log&lt;/code&gt; table.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;four-levels-of-example-queries&quot;&gt;Four levels of example queries&lt;/h3&gt;
&lt;p&gt;I structured the example queries into four files so the database works for everyone from first-day learners to DBAs:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Level 1 — Basics&lt;/strong&gt;: &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;GROUP BY&lt;/code&gt;, single-table aggregation&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Level 2 — Intermediate&lt;/strong&gt;: multi-table &lt;code&gt;JOIN&lt;/code&gt; (up to 5 tables), correlated subqueries, &lt;code&gt;FULLTEXT&lt;/code&gt; search, &lt;code&gt;JSON_VALUE&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Level 3 — Advanced&lt;/strong&gt;: window functions, CTEs, recursive CTEs, &lt;code&gt;JSON_TABLE&lt;/code&gt;, &lt;code&gt;SET&lt;/code&gt;/&lt;code&gt;FIND_IN_SET&lt;/code&gt;, &lt;code&gt;VEC_Distance&lt;/code&gt; similarity search&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Level 4 — DBA/Developer&lt;/strong&gt;: &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;, index strategy, stored procedure authoring, transaction isolation levels, audit log forensics&lt;/p&gt;
&lt;p&gt;A level 3 query to find the full prerequisite chain for a course looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH RECURSIVE prereq_chain AS (
    SELECT cp.prerequisite_id,
           p.code   AS prereq_code,
           p.title  AS prereq_title,
           1        AS depth
    FROM   course_prerequisites cp
    JOIN   courses p ON p.course_id = cp.prerequisite_id
    WHERE  cp.course_id = (SELECT course_id FROM courses WHERE code = 'CS300')

    UNION ALL

    SELECT cp2.prerequisite_id,
           p2.code,
           p2.title,
           pc.depth + 1
    FROM   course_prerequisites cp2
    JOIN   prereq_chain         pc ON pc.prerequisite_id = cp2.course_id
    JOIN   courses              p2 ON p2.course_id = cp2.prerequisite_id
    WHERE  pc.depth &amp;lt; 10
)
SELECT DISTINCT depth, prereq_code, prereq_title
FROM   prereq_chain
ORDER  BY depth, prereq_code;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And a vector similarity search to find courses related to a given one:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT c.code, c.title,
       VEC_Distance(ref.embedding, c.embedding) AS distance
FROM   courses ref
JOIN   courses c ON c.course_id &amp;lt;&amp;gt; ref.course_id
WHERE  ref.code = 'CS101'
ORDER  BY distance
LIMIT  5;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;try-it-right-now&quot;&gt;Try it right now&lt;/h3&gt;
&lt;p&gt;You don't need to install anything to explore the schema.&lt;/p&gt;
&lt;p&gt;You can run queries against University DB directly in &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://sqlize.online&quot;&gt;sqlize.online&lt;/a&gt;&lt;/strong&gt; — my online SQL editor that supports MariaDB 11.7. Paste any query from the example files and see results immediately.&lt;/p&gt;
&lt;p&gt;If you want a more structured learning experience with exercises and instant feedback, check out &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://sqltest.online&quot;&gt;sqltest.online&lt;/a&gt;&lt;/strong&gt; — designed for exactly this kind of hands-on SQL practice.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;get-the-database&quot;&gt;Get the database&lt;/h3&gt;
&lt;p&gt;Everything is open-source under the MIT license:&lt;/p&gt;
&lt;p&gt;👉 &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rozhnev/university-db&quot;&gt;github.com/rozhnev/university-db&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The repository includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;01_schema.sql&lt;/code&gt; — all 16 tables&lt;/li&gt;
&lt;li&gt;&lt;code&gt;02_objects.sql&lt;/code&gt; — views, procedures, triggers&lt;/li&gt;
&lt;li&gt;&lt;code&gt;03_seed_small.sql&lt;/code&gt; — static seed data&lt;/li&gt;
&lt;li&gt;&lt;code&gt;generate_data.py&lt;/code&gt; — Python/Faker script to populate ~130 000 rows&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt; — one command to get a running database&lt;/li&gt;
&lt;li&gt;&lt;code&gt;queries/level1.sql&lt;/code&gt; through &lt;code&gt;level4.sql&lt;/code&gt; — 50+ example queries&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/rozhnev/university-db.git
cd university-db
cp .env.example .env
docker compose up --build
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-to-contribute&quot;&gt;How to contribute&lt;/h3&gt;
&lt;p&gt;The project is open source and contributions of any kind are welcome.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Found a bug?&lt;/strong&gt; — &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rozhnev/university-db/issues&quot;&gt;Open an issue on GitHub&lt;/a&gt;. Mistakes in the schema, incorrect queries, typos in comments — all worth reporting. The more specific the description, the faster it gets fixed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Have an interesting query?&lt;/strong&gt; — Send a pull request to &lt;code&gt;queries/&lt;/code&gt;. Level 3–4 examples are especially valuable: window functions, recursive CTEs, JSON forensics, vector search.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Want to improve the data generator?&lt;/strong&gt; — &lt;code&gt;generate_data.py&lt;/code&gt; is intentionally kept simple. More realistic grade distributions, additional data scenarios, or faster bulk-insert batching are all good targets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Need support for another environment?&lt;/strong&gt; — A Kubernetes manifest, Helm chart, or a setup script for a cloud managed service (RDS, Cloud SQL, PlanetScale) would be a useful addition.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Educational materials?&lt;/strong&gt; — Exercises with solutions, workshop slides, or Jupyter notebooks built on this schema are all welcome.&lt;/p&gt;
&lt;p&gt;Fork the repository and send a pull request — code review within a few days.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;is-sakila-dead-&quot;&gt;Is Sakila dead?&lt;/h3&gt;
&lt;p&gt;Not at all. It's still a perfectly valid database for learning basic SQL, and its portability (it runs on any MySQL 5.x+ install) is a genuine advantage. But as a primary teaching tool for modern MariaDB, it's showing its age.&lt;/p&gt;
&lt;p&gt;University DB fills the gap for anyone who wants to teach or learn the full surface area of what MariaDB 11.7 can do — from a first &lt;code&gt;SELECT&lt;/code&gt; to a vector similarity search in a recursive CTE inside a stored procedure.&lt;/p&gt;
&lt;p&gt;I hope it's useful. Feedback, issues, and pull requests are very welcome.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Slava Rozhnev — &lt;a rel=&quot;nofollow&quot; href=&quot;https://sqlize.online&quot;&gt;sqlize.online&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://sqltest.online&quot;&gt;sqltest.online&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rozhnev&quot;&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21426/why-not-sakila-building-a-modern-sql-learning-database-for-mariadb</guid>
<pubDate>Fri, 26 Jun 2026 20:42:52 +0000</pubDate>
</item>
<item>
<title>INNER JOIN vs Correlated Subquery in SQL: What Is Actually Faster?</title>
<link>https://coderlegion.com/21160/inner-join-vs-correlated-subquery-in-sql-what-is-actually-faster</link>
<description>&lt;p&gt;Most SQL developers hear the same rule early: always prefer &lt;code&gt;INNER JOIN&lt;/code&gt; over correlated subqueries, because subqueries create an N+1 performance problem.&lt;/p&gt;
&lt;p&gt;That rule is useful, but it is not always true.&lt;/p&gt;
&lt;p&gt;I tested this on four engines (MySQL, Oracle, PostgreSQL, SQLite) and got mixed results. In some cases, the correlated subquery was faster than the join query.&lt;/p&gt;
&lt;p&gt;If you want to validate the same patterns yourself, you can run these examples in live sandboxes and practice similar SQL tasks on &lt;a rel=&quot;nofollow&quot; href=&quot;https://sqltest.online&quot;&gt;sqltest.online&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;what-you-will-learn&quot;&gt;What You Will Learn&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;what &lt;code&gt;INNER JOIN&lt;/code&gt; does best;&lt;/li&gt;
&lt;li&gt;when a correlated subquery can be competitive;&lt;/li&gt;
&lt;li&gt;why execution plans matter more than &quot;golden rules&quot;;&lt;/li&gt;
&lt;li&gt;how to benchmark both approaches correctly.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-is-inner-join-in-sql-&quot;&gt;What Is INNER JOIN in SQL?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;INNER JOIN&lt;/code&gt; returns only rows that match in both tables.&lt;/p&gt;
&lt;p&gt;Why it matters: it is usually the cleanest set-based way to combine data, and often gives the optimizer strong options.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
    c.customer_id,
    c.name,
    o.order_id
FROM customers c
INNER JOIN orders o
    ON c.customer_id = o.customer_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In many workloads, this is exactly what you want. But performance still depends on table sizes, indexes, and optimizer decisions.&lt;/p&gt;
&lt;h3 id=&quot;benchmark-scenario&quot;&gt;Benchmark Scenario&lt;/h3&gt;
&lt;p&gt;I used a small outer table and a larger inner table:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;customers&lt;/code&gt;: 25 rows&lt;/li&gt;
&lt;li&gt;&lt;code&gt;orders&lt;/code&gt;: 1000 rows&lt;/li&gt;
&lt;li&gt;index on &lt;code&gt;orders(customer_id)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Task: return each customer with the number of their orders.&lt;/p&gt;
&lt;h3 id=&quot;two-query-patterns&quot;&gt;Two Query Patterns&lt;/h3&gt;
&lt;h4 id=&quot;pattern-a-join-group-by&quot;&gt;Pattern A: JOIN + GROUP BY&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
    c.customer_id,
    COUNT(o.order_id) AS orders_count
FROM customers c
LEFT JOIN orders o
    ON c.customer_id = o.customer_id
GROUP BY c.customer_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: I used &lt;code&gt;LEFT JOIN&lt;/code&gt; here to keep customers with zero orders. If you only need matched rows, use &lt;code&gt;INNER JOIN&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;pattern-b-correlated-subquery&quot;&gt;Pattern B: Correlated Subquery&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
    c.customer_id,
    (
        SELECT COUNT(o.order_id)
        FROM orders o
        WHERE o.customer_id = c.customer_id
    ) AS orders_count
FROM customers c;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second form looks like classic N+1, but an index can make each lookup very cheap.&lt;/p&gt;
&lt;h3 id=&quot;how-to-inspect-the-real-cost&quot;&gt;How to Inspect the Real Cost&lt;/h3&gt;
&lt;p&gt;Use execution plans, not assumptions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MySQL / PostgreSQL: &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;SQLite: &lt;code&gt;EXPLAIN QUERY PLAN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Oracle: &lt;code&gt;EXPLAIN PLAN FOR ...&lt;/code&gt; + &lt;code&gt;DBMS_XPLAN.DISPLAY&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;results-summary-across-engines&quot;&gt;Results Summary Across Engines&lt;/h3&gt;
&lt;h4 id=&quot;mysql-8-and-oracle-23c&quot;&gt;MySQL 8 and Oracle 23c&lt;/h4&gt;
&lt;p&gt;In my tests, the correlated subquery often won.&lt;/p&gt;
&lt;p&gt;Why: with a small outer table (25 customers), the optimizer could do very fast indexed lookups on &lt;code&gt;orders(customer_id)&lt;/code&gt; per customer. That was cheaper than building larger join/group intermediate work.&lt;/p&gt;
&lt;h4 id=&quot;postgresql-16&quot;&gt;PostgreSQL 16&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;JOIN + GROUP BY&lt;/code&gt; won in my test.&lt;/p&gt;
&lt;p&gt;Why: PostgreSQL produced an efficient set-based plan and handled the join/aggregate path very well.&lt;/p&gt;
&lt;h4 id=&quot;sqlite&quot;&gt;SQLite&lt;/h4&gt;
&lt;p&gt;Practical tie.&lt;/p&gt;
&lt;p&gt;Both variants ended with very similar plans and near-identical timing on this dataset.&lt;/p&gt;
&lt;h3 id=&quot;why-the-inner-join-rule-sometimes-fails&quot;&gt;Why the INNER JOIN Rule Sometimes Fails&lt;/h3&gt;
&lt;p&gt;The &quot;always use INNER JOIN&quot; advice fails when it ignores three variables:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Database engine optimizer behavior&lt;/li&gt;
&lt;li&gt;Data distribution and table size ratio&lt;/li&gt;
&lt;li&gt;Index availability and quality&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;SQL performance is not about syntax preference. It is about the physical plan your engine chooses.&lt;/p&gt;
&lt;h3 id=&quot;common-mistakes&quot;&gt;Common Mistakes&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;assuming &lt;code&gt;INNER JOIN&lt;/code&gt; is always faster without checking plans;&lt;/li&gt;
&lt;li&gt;forgetting that &lt;code&gt;INNER JOIN&lt;/code&gt; removes non-matching rows;&lt;/li&gt;
&lt;li&gt;testing once without warm-up runs;&lt;/li&gt;
&lt;li&gt;benchmarking without a supporting index;&lt;/li&gt;
&lt;li&gt;comparing query text instead of comparing actual execution plans.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;practical-checklist&quot;&gt;Practical Checklist&lt;/h3&gt;
&lt;p&gt;Before choosing &lt;code&gt;INNER JOIN&lt;/code&gt; or correlated subquery:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Add the right index (here: &lt;code&gt;orders(customer_id)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Run both query variants.&lt;/li&gt;
&lt;li&gt;Capture execution plans.&lt;/li&gt;
&lt;li&gt;Compare timing over several runs.&lt;/li&gt;
&lt;li&gt;Pick the query that is both correct and fast for your engine.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;final-takeaway&quot;&gt;Final Takeaway&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;INNER JOIN&lt;/code&gt; is a strong default and often the right choice, but it is not a universal performance guarantee.&lt;/p&gt;
&lt;p&gt;The best query is the one that produces the best execution plan on your actual database, with your actual data.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21160/inner-join-vs-correlated-subquery-in-sql-what-is-actually-faster</guid>
<pubDate>Mon, 22 Jun 2026 20:57:21 +0000</pubDate>
</item>
<item>
<title>SQL Formatting Best Practices: Writing Readable, Maintainable Queries</title>
<link>https://coderlegion.com/20971/sql-formatting-best-practices-writing-readable-maintainable-queries</link>
<description>&lt;p&gt;Messy SQL is a productivity killer. A query that took 20 minutes to write can take 2 hours to debug if it's unformatted. Good SQL formatting isn't about aesthetics — it's about reducing cognitive load when you're six levels deep in a JOIN and trying to figure out where the bug is.&lt;/p&gt;
&lt;h2&gt;The Core Conventions&lt;/h2&gt;
&lt;p&gt;Every SQL style guide agrees on the fundamentals. These apply across MySQL, PostgreSQL, SQL Server, BigQuery, and SQLite.&lt;/p&gt;
&lt;h3&gt;1. Uppercase keywords&lt;/h3&gt;
&lt;p&gt;``&lt;code&gt;sql&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;-- Good&lt;/p&gt;
&lt;p&gt;SELECT id, name, email&lt;/p&gt;
&lt;p&gt;FROM users&lt;/p&gt;
&lt;p&gt;WHERE status = 'active'&lt;/p&gt;
&lt;p&gt;ORDER BY created_at DESC;&lt;/p&gt;
&lt;p&gt;-- Bad&lt;/p&gt;
&lt;p&gt;select id, name, email from users where status = 'active' order by created_at desc;&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Uppercase keywords make the structure of a query immediately scannable. Most formatters apply this automatically.&lt;/p&gt;
&lt;h3&gt;2. One clause per line&lt;/h3&gt;
&lt;p&gt;Each major clause — SELECT&lt;code&gt;, &lt;/code&gt;FROM&lt;code&gt;, &lt;/code&gt;WHERE&lt;code&gt;, &lt;/code&gt;JOIN&lt;code&gt;, &lt;/code&gt;GROUP BY&lt;code&gt;, &lt;/code&gt;ORDER BY&lt;code&gt;, &lt;/code&gt;HAVING&lt;code&gt; — should start on its own line. This makes it easy to add, remove, or comment out individual clauses during development.&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;3. Indent continuation lines&lt;/h3&gt;
&lt;p&gt;Sub-expressions, JOIN conditions, and WHERE conditions should be indented:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;sql
&amp;lt;p&amp;gt;SELECT&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;u.id,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;u.name,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;o.total,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;p.name AS product_name&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;FROM users u&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;INNER JOIN orders o ON o.user_id = u.id&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;INNER JOIN order_items oi ON oi.order_id = o.id&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;INNER JOIN products p ON p.id = oi.product_id&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;WHERE u.status = 'active'&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;AND o.created_at &amp;gt;= '2024-01-01'&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;AND o.total &amp;gt; 100&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;ORDER BY o.created_at DESC;&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;4. Leading AND/OR in WHERE clauses&lt;/h3&gt;
&lt;p&gt;Put AND&lt;code&gt; and &lt;/code&gt;OR&lt;code&gt; at the &lt;em&gt;beginning&lt;/em&gt; of a condition line, not the end. This makes it trivial to comment out a single condition without breaking the query:&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;sql
&amp;lt;p&amp;gt;-- Easy to comment out individual conditions:&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;WHERE status = 'active'&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;AND created_at &amp;gt;= '2024-01-01'&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;-- AND deleted_at IS NULL  ← just add -- to disable&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;AND role IN ('admin', 'editor')&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;If you put AND&lt;code&gt; at the end of each line, commenting out one line breaks the syntax.&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;5. Meaningful aliases&lt;/h3&gt;
&lt;p&gt;Aliases should clarify, not obscure:&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;sql&lt;br&gt;
-- Good&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;FROM users u&lt;/p&gt;&lt;br&gt;
&lt;p&gt;INNER JOIN orders o ON o.user_id = u.id&lt;/p&gt;
&lt;p&gt;-- Bad&lt;/p&gt;
&lt;p&gt;FROM users a&lt;/p&gt;
&lt;p&gt;INNER JOIN orders b ON b.a_id = a.id&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Single-letter aliases are fine when the table name is obvious (u&lt;code&gt; for &lt;/code&gt;users&lt;code&gt;), but avoid arbitrary letters that don't relate to the table name.&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Naming Conventions&lt;/h2&gt;
&lt;p&gt;Consistent naming is part of readable SQL. The most widely adopted conventions:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Tables&lt;/strong&gt;: snake_case&lt;code&gt;, plural nouns — &lt;/code&gt;users&lt;code&gt;, &lt;/code&gt;order_items&lt;code&gt;, &lt;/code&gt;product_categories&lt;code&gt;&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Columns&lt;/strong&gt;: snake_case&lt;code&gt; — &lt;/code&gt;user_id&lt;code&gt;, &lt;/code&gt;created_at&lt;code&gt;, &lt;/code&gt;is_active&lt;code&gt;&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Primary keys&lt;/strong&gt;: id&lt;code&gt; or &lt;/code&gt;{table}_id&lt;code&gt; (e.g. &lt;/code&gt;user_id&lt;code&gt; in the &lt;/code&gt;users&lt;code&gt; table)&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Foreign keys&lt;/strong&gt;: {referenced_table}_id&lt;code&gt; (e.g. &lt;/code&gt;user_id&lt;code&gt; in &lt;/code&gt;orders&lt;code&gt;)&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Booleans&lt;/strong&gt;: is_&lt;code&gt; or &lt;/code&gt;has_&lt;code&gt; prefix — &lt;/code&gt;is_active&lt;code&gt;, &lt;/code&gt;has_paid&lt;code&gt;&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Timestamps&lt;/strong&gt;: created_at&lt;code&gt;, &lt;/code&gt;updated_at&lt;code&gt;, &lt;/code&gt;deleted_at&lt;code&gt;&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;Avoid CamelCase — most databases are case-insensitive by default, so UserID&lt;code&gt; and &lt;/code&gt;userid&lt;code&gt; are the same identifier unless quoted.&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;CTEs Over Subqueries&lt;/h2&gt;
&lt;p&gt;Common Table Expressions (CTEs) make complex queries readable by naming intermediate results:&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;sql&lt;br&gt;
-- Hard to read: nested subqueries&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;SELECT u.name, order_summary.total&lt;/p&gt;&lt;br&gt;
&lt;p&gt;FROM users u&lt;/p&gt;&lt;br&gt;
&lt;p&gt;INNER JOIN (&lt;/p&gt;&lt;br&gt;
&lt;p&gt;SELECT user_id, SUM(total) AS total&lt;/p&gt;&lt;br&gt;
&lt;p&gt;FROM orders&lt;/p&gt;&lt;br&gt;
&lt;p&gt;WHERE status = 'completed'&lt;/p&gt;&lt;br&gt;
&lt;p&gt;GROUP BY user_id&lt;/p&gt;&lt;br&gt;
&lt;p&gt;) order_summary ON order_summary.user_id = u.id;&lt;/p&gt;
&lt;p&gt;-- Easy to read: CTE&lt;/p&gt;
&lt;p&gt;WITH completed_orders AS (&lt;/p&gt;
&lt;p&gt;SELECT user_id, SUM(total) AS total&lt;/p&gt;
&lt;p&gt;FROM orders&lt;/p&gt;
&lt;p&gt;WHERE status = 'completed'&lt;/p&gt;
&lt;p&gt;GROUP BY user_id&lt;/p&gt;
&lt;p&gt;)&lt;/p&gt;
&lt;p&gt;SELECT u.name, co.total&lt;/p&gt;
&lt;p&gt;FROM users u&lt;/p&gt;
&lt;p&gt;INNER JOIN completed_orders co ON co.user_id = u.id;&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;CTEs also make it easy to debug intermediate steps — you can run the CTE in isolation to verify its output before using it in the main query.&lt;/p&gt;
&lt;h2&gt;Long WHERE Clauses&lt;/h2&gt;
&lt;p&gt;When a WHERE clause has many conditions, group related conditions together and add a blank line between groups:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;sql
&amp;lt;p&amp;gt;WHERE&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;-- User filters&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;u.status = 'active'&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;AND u.role IN ('admin', 'editor')&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;-- Date range&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;AND o.created_at &amp;gt;= '2024-01-01'&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;AND o.created_at &amp;lt; '2025-01-01'&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;-- Amount filter&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;AND o.total BETWEEN 50 AND 500&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Use parentheses whenever you mix AND&lt;code&gt; and &lt;/code&gt;OR&lt;code&gt; — operator precedence rules can cause subtle bugs:&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;sql&lt;br&gt;
-- BUG: this is parsed as: status = 'active' AND (role = 'admin' OR role = 'editor' AND deleted_at IS NULL)&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;WHERE status = 'active' AND role = 'admin' OR role = 'editor' AND deleted_at IS NULL&lt;/p&gt;
&lt;p&gt;-- CORRECT: explicit grouping&lt;/p&gt;
&lt;p&gt;WHERE status = 'active'&lt;/p&gt;
&lt;p&gt;AND (role = 'admin' OR role = 'editor')&lt;/p&gt;
&lt;p&gt;AND deleted_at IS NULL&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Automating SQL Formatting&lt;/h2&gt;
&lt;p&gt;Manual formatting is not sustainable across a team. Automate it:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;VS Code&lt;/strong&gt;: Install the &quot;SQL Formatter&quot; extension — format on save with Ctrl+Shift+P → Format Document&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Pre-commit hooks&lt;/strong&gt;: Use sql-formatter&lt;code&gt; npm package with husky + lint-staged to format &lt;/code&gt;.sql&lt;code&gt; files before every commit&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Python projects&lt;/strong&gt;: sqlparse&lt;code&gt; library — &lt;/code&gt;sqlparse.format(sql, reindent=True, keyword_case='upper')&lt;code&gt;&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;CI/CD&lt;/strong&gt;: sqlfluff` supports format checking and enforcement in pipelines&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;For one-off formatting without installing anything, &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/sql-formatter-beautifier/&quot;&gt;this free SQL formatter&lt;/a&gt; runs in your browser, supports 19+ dialects (MySQL, PostgreSQL, BigQuery, Snowflake, T-SQL, SQLite, and more), and never sends your queries to a server.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Good SQL formatting comes down to a few consistent habits:&lt;/p&gt;
&lt;ol&gt;&lt;li&gt;Uppercase keywords, lowercase identifiers&lt;/li&gt;&lt;li&gt;One clause per line, indented continuation lines&lt;/li&gt;&lt;li&gt;Leading AND/OR for easy debugging&lt;/li&gt;&lt;li&gt;Meaningful aliases that relate to table names&lt;/li&gt;&lt;li&gt;CTEs over nested subqueries for anything complex&lt;/li&gt;&lt;li&gt;Explicit parentheses when mixing AND/OR&lt;/li&gt;&lt;li&gt;Automate it — formatters remove the decision overhead entirely&lt;/li&gt;&lt;/ol&gt;
&lt;p&gt;The goal is SQL that a colleague can read and understand at a glance — both when you're writing it and when you're debugging it six months later.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Format SQL instantly in your browser at &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/sql-formatter-beautifier/&quot;&gt;snappytools.app/sql-formatter-beautifier&lt;/a&gt; — supports 19+ dialects, keyword case options, and runs entirely client-side.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20971/sql-formatting-best-practices-writing-readable-maintainable-queries</guid>
<pubDate>Fri, 19 Jun 2026 10:07:57 +0000</pubDate>
</item>
<item>
<title>Why SQL Still Rules the Data World: Surprising Truths from the Relational Revolution</title>
<link>https://coderlegion.com/20585/why-sql-still-rules-the-data-world-surprising-truths-from-the-relational-revolution</link>
<description>&lt;p&gt;In the hyper-accelerated world of software development, where programming languages often have the shelf life of a smartphone, SQL stands as a defiant outlier. With roots stretching back to the early 1970s, it has not only survived the rise and fall of countless &quot;modern&quot; competitors but has thrived. &lt;/p&gt;
&lt;p&gt;As we struggle to navigate an unprecedented deluge of digital information, it is a fascinating paradox: this &quot;ancient&quot; tool, born in the era of magnetic tapes, remains the industry’s most sophisticated way to manage the complexities of data. To understand why, we have to look past the syntax and into the architectural soul of the relational model.&lt;/p&gt;
&lt;h4 id=&quot;the-high-power-zoom-lens-of-data&quot;&gt;The &quot;High-Power Zoom Lens&quot; of Data&lt;/h4&gt;
&lt;p&gt;The enduring success of relational databases lies in their unique ability to handle massive datasets without losing focus.  Many alternative database management systems break down under heavy loads. &lt;/p&gt;
&lt;p&gt;&lt;em&gt;The reason is usually architectural:&lt;/em&gt; their focus is too narrow. They are designed to do one thing well, but they lack versatility. In these systems, the “lens” is effectively stuck on maximum zoom, making it impossible to see the forest for the trees.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;SQL, by contrast, functions like a high-precision instrument for data. As Alan Beaulieu notes in &lt;em&gt;Learning SQL&lt;/em&gt;:&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;SQL is akin to one of those snazzy digital cameras with the&lt;br&gt;
high-power zoom lens in that you can use SQL to look at large sets of&lt;br&gt;
data, or you can zoom in on individual rows (or anywhere in between).&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This versatility allows us to act as both microscopes and telescopes. We can perform macro-scale analysis—identifying multi-year trends across millions of records—and then instantly zoom in to inspect a single micro-scale transaction. It is this specific ability to scale perspective that has allowed the relational model to defeat almost every attempt to dethrone it.&lt;/p&gt;
&lt;h4 id=&quot;the-zen-of-giving-up-control&quot;&gt;The Zen of Giving Up Control&lt;/h4&gt;
&lt;p&gt;For developers raised on Java, C++, or Python, the hardest part of mastering SQL isn’t the keywords—it’s the fundamental shift in mindset. Most languages are &lt;strong&gt;procedural&lt;/strong&gt;; the programmer is the micromanager, defining both the desired result and the specific process to achieve it. SQL is a &lt;strong&gt;nonprocedural&lt;/strong&gt; language, which requires a &quot;Zen-like&quot; surrender of control to an external agent: the &lt;strong&gt;Optimizer&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The &quot;What&quot; (SQL):&lt;/strong&gt; You define the inputs and the desired output (e.g., &quot;Show me all customers in &lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/Alexandria&quot;&gt;Alexandria&lt;/a&gt; with active accounts&quot;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The &quot;How&quot; (Procedural):&lt;/strong&gt; The Optimizer analyzes your query, evaluates table configurations and available indexes, and then picks an &lt;strong&gt;Execution Plan&lt;/strong&gt;, which the server uses to actually retrieve the data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Trusting the &lt;strong&gt;Optimizer&lt;/strong&gt; to generate this &lt;strong&gt;Execution Plan&lt;/strong&gt; is a rite of passage. While we can occasionally influence these decisions with &quot;hints,&quot; the database engine is almost always better at navigating the physical complexities of the data than a human writing manual loops and pointers.&lt;/p&gt;
&lt;h4 id=&quot;the-four-letter-word-that-isn-t-what-you-think&quot;&gt;The &quot;Four-Letter Word&quot; That Isn't What You Think&lt;/h4&gt;
&lt;p&gt;In the realm of data integrity, there is one four-letter word that causes more bugs for beginners than any other: &lt;code&gt;NULL&lt;/code&gt;. &lt;/p&gt;
&lt;p&gt;To a junior developer, &lt;code&gt;NULL&lt;/code&gt; looks like a zero or an empty string. To an architect, it is the &lt;strong&gt;absence of value&lt;/strong&gt;. According to the source context, &lt;code&gt;NULL&lt;/code&gt; specifically represents data that is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Not applicable&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unknown&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An empty set&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A senior practitioner will tell you that a zero is a number and an empty string is a character, but a &lt;code&gt;NULL&lt;/code&gt; is a question mark. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pro-Tip:&lt;/strong&gt; You cannot use the equality operator (&lt;code&gt;=&lt;/code&gt;) with &lt;code&gt;NULL&lt;/code&gt;. To find non-terminated employees, for example, a Senior Architect would warn you to use the specific syntax &lt;code&gt;WHERE end_date IS NULL&lt;/code&gt;. Attempting to use &lt;code&gt;WHERE end_date = NULL&lt;/code&gt; will return nothing, as the absence of a value cannot be &quot;equal&quot; to anything.&lt;/p&gt;
&lt;h4 id=&quot;why-redundant-data-is-actually-a-clean-design&quot;&gt;Why &quot;Redundant&quot; Data Is Actually a Clean Design&lt;/h4&gt;
&lt;p&gt;It is counter-intuitive, but the relational model’s use of &quot;redundant&quot; data—specifically foreign keys—is the secret to its reliability. This is the heart of &lt;strong&gt;Normalization&lt;/strong&gt;. We refine a design to ensure every independent piece of information exists in exactly one place.&lt;/p&gt;
&lt;p&gt;Consider a &quot;Person Table.&quot; A common beginner mistake is to create a &quot;compound object&quot; column, like a &lt;code&gt;name&lt;/code&gt; column containing both first and last names, or an &lt;code&gt;address&lt;/code&gt; column containing street, city, and zip. This is a violation of normalization. Why? Because as the source material warns:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;...otherwise, the data might be changed in one place but not another,&lt;br&gt;
causing the data in the database to be unreliable.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;By ensuring a &quot;single source of truth,&quot; we prevent data corruption. We store a person's name once in the &lt;code&gt;person&lt;/code&gt; table and use their unique ID (the foreign key) everywhere else. If their name changes, we update one row in one table, and the change is logically reflected across the entire system.&lt;/p&gt;
&lt;h4 id=&quot;the-evolution-from-pointers-to-tables&quot;&gt;The Evolution from Pointers to Tables&lt;/h4&gt;
&lt;p&gt;To appreciate SQL, one must understand the chaos it replaced. Before &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.ibm.com/history/edgar-codd&quot;&gt;Dr. E.F. Codd proposed the Relational Model in 1970,&lt;/a&gt; we relied on &lt;strong&gt;Hierarchical&lt;/strong&gt; and &lt;strong&gt;Network&lt;/strong&gt; systems. These were rigid structures where data was connected by physical &quot;pointers.&quot; To find a record, you had to manually traverse these links like a navigator in a dark forest.&lt;/p&gt;
&lt;p&gt;Codd’s revolution replaced these physical links with &lt;strong&gt;logical joins&lt;/strong&gt;. This transition was reflected in the language's own evolution. It began as &lt;strong&gt;DSL/Alpha&lt;/strong&gt;, was simplified into &lt;strong&gt;SQUARE&lt;/strong&gt;, and was then refined into &lt;strong&gt;SEQUEL&lt;/strong&gt; (Structured English Query Language) before finally becoming the standard &lt;strong&gt;SQL&lt;/strong&gt; we use today. By moving from &quot;pointers&quot; to &quot;sets of tables,&quot; Codd made data accessible to the user rather than just the specialist, allowing us to link information dynamically based on values rather than hard-coded physical paths.&lt;/p&gt;
&lt;h5 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h5&gt;
&lt;p&gt;The enduring nature of SQL is a testament to the elegant efficiency of the relational model. In an era where the industry is frequently obsessed with &quot;new&quot; tools and NoSQL alternatives, the fundamentals established in the 1970s are more relevant than ever.&lt;/p&gt;
&lt;p&gt;We now manage terabytes of data spread across fast-access drives, with tens of gigabytes held in high-speed memory, yet the core problems of retrieval and integrity remain the same. SQL has entered &quot;middle age,&quot; but its future is bright because it solves these problems with a rigor that modern alternatives often lack. We must ask ourselves: in our obsession with &quot;new&quot; tools, have we overlooked the sophisticated power of the relational model in favor of modern tool-bloat? Mastering these &quot;old&quot; fundamentals is not just a history lesson—it is the only way to truly master the data-driven world of tomorrow.&lt;/p&gt;
&lt;h4 id=&quot;reference-material&quot;&gt;Reference Material&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.amazon.com/Learning-SQL-Generate-Manipulate-Retrieve/dp/1492057614&quot;&gt;Learning SQL: Generate, Manipulate, and Retrieve Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.amazon.com/Performance-Explained-Everything-Developers-about/dp/3950307826&quot;&gt;SQL Performance Explained Everything Developers Need to Know about SQL Performance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.amazon.com/Designing-Data-Intensive-Applications-Reliable-Maintainable/dp/B08VL1BLHB/ref=sr_1_2?crid=ETB66REECYXE&amp;amp;dib=eyJ2IjoiMSJ9.lrr2OoDCN-gtMboNEF2nJ_vEL2GKSv29phJRy_xd2NOF9zCnS7_pNfEydrbsfSMgy-udkQ5AEm8BzEDtxXUPX8cTFYi6qO9AmnCFqUDLWfmliUDR83ljMpklBvpi1O4NHQGAjHjXCiAByMziyupAd7XX5Ux6Sz3VuURmuDT_EodIliGqCwUvh2_LCdksgM_0i0Oy3rWZ1o4e5NcGse2DV6pzcMYDkqn8nkIIl5i7ZTE.4XVKzcZ7GNpR6IDKHWfnkL_5ZcrJsYPlwXhfl0Of9d4&amp;amp;dib_tag=se&amp;amp;keywords=Designing%20Data-Intensive%20Applications_%20The%20Big%20Ideas%20Behind%20Reliable,%20Scalable,%20and%20Maintainable%20Systems&amp;amp;nsdOptOutParam=true&amp;amp;qid=1781447342&amp;amp;s=books&amp;amp;sprefix=designing%20data-intensive%20applications_%20the%20big%20ideas%20behind%20reliable,%20scalable,%20and%20maintainable%20systems,stripbooks-intl-ship,292&amp;amp;sr=1-2&quot;&gt;Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20585/why-sql-still-rules-the-data-world-surprising-truths-from-the-relational-revolution</guid>
<pubDate>Sun, 14 Jun 2026 14:39:21 +0000</pubDate>
</item>
<item>
<title>SQL Formatting and Beautification: Standards, Tools, and Best Practices</title>
<link>https://coderlegion.com/19352/sql-formatting-and-beautification-standards-tools-and-best-practices</link>
<description>&lt;p&gt;Unformatted SQL is a maintenance problem. A 200-character single-line query is hard to review in a PR, harder to debug under pressure, and inconsiderate to hand off to a teammate. SQL formatting costs nothing and pays back immediately.&lt;/p&gt;
&lt;h2&gt;What a SQL Formatter Does&lt;/h2&gt;
&lt;p&gt;A formatter restructures query text into a consistent, indented layout — without changing what the query does. The database ignores whitespace; formatting is entirely for humans.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;&lt;p&gt;&lt;code&gt;`&amp;lt;code&amp;gt;sql&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;select u.id,u.name,o.total from users u inner join orders o on u.id=o.user_id where o.status='paid' order by o.total desc limit 100;&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;&amp;lt;code&amp;gt;sql
&amp;lt;p&amp;gt;SELECT&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;u.id,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;u.name,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;o.total&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;FROM&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;users u&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;INNER JOIN orders o ON u.id = o.user_id&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;WHERE&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;o.status = 'paid'&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;ORDER BY&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;o.total DESC&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;LIMIT 100;&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Same query. Dramatically more readable.&lt;/p&gt;
&lt;h2&gt;Key Formatting Decisions&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Keyword case&lt;/strong&gt; — SQL keywords are case-insensitive. SELECT&lt;code&gt; vs &lt;/code&gt;select&lt;code&gt;. Uppercase is the most common convention (keywords stand out visually from identifiers). Choose one and stick to it across your codebase.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Clause alignment&lt;/strong&gt; — Each major clause (SELECT&lt;code&gt;, &lt;/code&gt;FROM&lt;code&gt;, &lt;/code&gt;WHERE&lt;code&gt;, &lt;/code&gt;GROUP BY&lt;code&gt;, &lt;/code&gt;ORDER BY&lt;code&gt;) starts a new line. Conditions within a clause are indented.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Leading vs trailing commas:&lt;/strong&gt;&lt;br&gt;
`&lt;code&gt;sql&lt;br&gt;
-- Trailing (most common)&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;SELECT&lt;/p&gt;&lt;br&gt;
&lt;p&gt;id,&lt;/p&gt;&lt;br&gt;
&lt;p&gt;name,&lt;/p&gt;&lt;br&gt;
&lt;p&gt;email&lt;/p&gt;
&lt;p&gt;-- Leading (easier to add/remove first item)&lt;/p&gt;
&lt;p&gt;SELECT&lt;/p&gt;
&lt;p&gt;id&lt;/p&gt;
&lt;p&gt;,name&lt;/p&gt;
&lt;p&gt;,email&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AND/OR placement:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;&amp;lt;code&amp;gt;sql
&amp;lt;p&amp;gt;-- AND at start of line (easy to comment out one condition)&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;WHERE&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;status = 'active'&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;AND created_at &amp;gt; '2026-01-01'&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;AND amount &amp;gt; 100&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;SQL Dialects&lt;/h2&gt;
&lt;p&gt;SQL is not one standard. Each database has extensions:&lt;/p&gt;
&lt;p&gt;| Database | Notes |&lt;/p&gt;
&lt;p&gt;|---|---|&lt;/p&gt;
&lt;p&gt;| PostgreSQL | ILIKE&lt;code&gt;, &lt;/code&gt;::&lt;code&gt; casting, &lt;/code&gt;$$&lt;code&gt; dollar quoting, CTEs everywhere |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| MySQL | Backtick identifiers, LIMIT x,y&lt;code&gt;, &lt;/code&gt;AUTO_INCREMENT&lt;code&gt; |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| SQLite | Minimal syntax, no stored procedures |&lt;/p&gt;
&lt;p&gt;| SQL Server (T-SQL) | TOP&lt;code&gt;, &lt;/code&gt;NOLOCK&lt;code&gt;, &lt;/code&gt;GETDATE()&lt;code&gt;, &lt;/code&gt;GO&lt;code&gt; |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| BigQuery | STRUCT&lt;code&gt;, &lt;/code&gt;ARRAY_AGG&lt;code&gt;, &lt;/code&gt;UNNEST&lt;code&gt;, standard SQL mode |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;A good formatter knows which dialect you're using. &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/sql-formatter-beautifier/&quot;&gt;SnappyTools' SQL formatter&lt;/a&gt; supports all major dialects via a dropdown selector.&lt;/p&gt;
&lt;h2&gt;Beautification: Compressing SQL Back&lt;/h2&gt;
&lt;p&gt;The reverse of formatting — collapsing multi-line SQL to a single line — is useful for:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Embedding SQL in JSON configuration&lt;/li&gt;&lt;li&gt;Storing queries in environment variables&lt;/li&gt;&lt;li&gt;Code generation output&lt;/li&gt;&lt;li&gt;Sending queries in HTTP request bodies&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;Both directions are available at &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/sql-formatter-beautifier/&quot;&gt;snappytools.app/sql-formatter-beautifier/&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Automating SQL Formatting in CI&lt;/h2&gt;
&lt;p&gt;For teams, use sqlfluff&lt;code&gt;:&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;bash&lt;br&gt;
pip install sqlfluff&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;Lint&lt;/h1&gt;
&lt;p&gt;sqlfluff lint queries/ --dialect postgres&lt;/p&gt;
&lt;h1&gt;Auto-fix&lt;/h1&gt;
&lt;p&gt;sqlfluff fix queries/ --dialect postgres&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;.sqlfluff&lt;code&gt; config:&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&amp;lt;code&amp;gt;ini
&amp;lt;p&amp;gt;[sqlfluff]&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;dialect = postgres&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;indent_unit = space&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;tab_space_size = 2&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;For pre-commit hooks:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;yaml
&amp;lt;h1&amp;gt;.pre-commit-config.yaml&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;repos:&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;- repo: https://github.com/sqlfluff/sqlfluff&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;rev: 3.0.0&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;hooks:&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;- id: sqlfluff-lint&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;args: [--dialect, postgres]&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Anti-Patterns to Avoid&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Implicit joins&lt;/strong&gt; (hard to see the join condition):&lt;br&gt;
`&lt;code&gt;sql&lt;br&gt;
-- Avoid&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;SELECT * FROM orders, users WHERE orders.user_id = users.id;&lt;/p&gt;
&lt;p&gt;-- Better&lt;/p&gt;
&lt;p&gt;SELECT * FROM orders INNER JOIN users ON orders.user_id = users.id;&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SELECT &lt;em&gt; in production&lt;/em&gt;&lt;/strong&gt; — Not formatting, but worth mentioning: always name columns explicitly. SELECT &lt;code&gt; breaks when the schema changes.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Inconsistent keyword case within one query:&lt;/strong&gt;&lt;br&gt;
`&lt;code&gt;sql&lt;br&gt;
-- Bad&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;select id FROM users WHERE status='active';&lt;/p&gt;
&lt;p&gt;-- Good&lt;/p&gt;
&lt;p&gt;SELECT id FROM users WHERE status = 'active';&lt;/p&gt;
&lt;p&gt;``&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;SQL formatting is about maintainability and team consistency, not performance. Consistent uppercase keywords, clause-per-line layout, and proper indentation make queries reviewable and debuggable. For one-off formatting of queries from APIs, schema dumps, or colleagues: &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/sql-formatter-beautifier/&quot;&gt;snappytools.app/sql-formatter-beautifier/&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/sql-formatter-beautifier/&quot;&gt;https://snappytools.app/sql-formatter-beautifier/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19352/sql-formatting-and-beautification-standards-tools-and-best-practices</guid>
<pubDate>Sat, 06 Jun 2026 23:48:06 +0000</pubDate>
</item>
<item>
<title>How to Format SQL for Readability: A Practical Style Guide</title>
<link>https://coderlegion.com/19471/how-to-format-sql-for-readability-a-practical-style-guide</link>
<description>&lt;p&gt;SQL doesn't enforce any whitespace rules — the engine treats a one-liner and a 40-line query exactly the same. But your teammates and future self will thank you for the 40-line version.&lt;/p&gt;
&lt;p&gt;Here's a concise SQL formatting guide that works across MySQL, PostgreSQL, SQLite, and SQL Server.&lt;/p&gt;
&lt;h2&gt;Core rules&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. Uppercase keywords, lowercase names&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This single convention does the most work:&lt;/p&gt;
&lt;p&gt;``&lt;code&gt;sql&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;SELECT u.name, o.total&lt;/p&gt;
&lt;p&gt;FROM users u&lt;/p&gt;
&lt;p&gt;JOIN orders o ON u.id = o.user_id&lt;/p&gt;
&lt;p&gt;WHERE o.status = 'completed'&lt;/p&gt;
&lt;p&gt;ORDER BY o.total DESC;&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;SQL keywords stand out visually; your table and column names don't compete for attention.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. One clause per line&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Each major clause (SELECT&lt;code&gt;, &lt;/code&gt;FROM&lt;code&gt;, &lt;/code&gt;WHERE&lt;code&gt;, &lt;/code&gt;JOIN&lt;code&gt;, &lt;/code&gt;GROUP BY&lt;code&gt;, &lt;/code&gt;HAVING&lt;code&gt;, &lt;/code&gt;ORDER BY&lt;code&gt;, &lt;/code&gt;LIMIT&lt;code&gt;) starts on a new line at the left margin.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Indent continuation items&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Items in a SELECT list or multiple WHERE conditions are indented one level:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;sql
&amp;lt;p&amp;gt;SELECT&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;u.name,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;u.email,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;o.total,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;o.created_at&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;FROM users u&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;WHERE&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;o.status = 'completed'&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;AND o.created_at &amp;gt; '2024-01-01';&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Alias everything consistently&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you alias users&lt;code&gt; as &lt;/code&gt;u&lt;code&gt;, use &lt;/code&gt;u.column&lt;code&gt; everywhere in that query. Mixing &lt;/code&gt;users.id&lt;code&gt; and &lt;/code&gt;u.name&lt;code&gt; in the same query causes unnecessary cognitive load.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Use CTEs for complex queries&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Named CTEs (WITH ... AS (...)&lt;code&gt;) replace nested subqueries with human-readable steps:&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;sql
&amp;lt;p&amp;gt;WITH active_users AS (&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;SELECT id, name&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;FROM users&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;WHERE last_login &amp;gt; NOW() - INTERVAL '30 days'&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;),&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;their_orders AS (&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;SELECT user_id, COUNT(*) AS order_count&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;FROM orders&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;GROUP BY user_id&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;)&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;SELECT u.name, t.order_count&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;FROM active_users u&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;JOIN their_orders t ON u.id = t.user_id&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;ORDER BY t.order_count DESC;&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Each CTE reads like a named step. The final SELECT is obvious.&lt;/p&gt;
&lt;h2&gt;Formatting inherited SQL&lt;/h2&gt;
&lt;p&gt;When you receive SQL from an ORM, export tool, or legacy codebase, it often arrives as one long string. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/sql-formatter-beautifier/&quot;&gt;SQL Formatter on SnappyTools&lt;/a&gt; takes minified or unformatted SQL and outputs properly indented, keyword-uppercased SQL for MySQL, PostgreSQL, SQLite, or T-SQL. Paste in one query or a full migration file — it runs in your browser with no upload.&lt;/p&gt;
&lt;h2&gt;What to avoid&lt;/h2&gt;
&lt;ul&gt;&lt;li&gt;Mixed case keywords (Select&lt;code&gt;, &lt;/code&gt;WHERE&lt;code&gt;, &lt;/code&gt;from&lt;code&gt;)&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Comma-first style (debated — choose one and stick to it)&lt;/li&gt;&lt;li&gt;Deeply nested subqueries when a CTE would be clearer&lt;/li&gt;&lt;li&gt;Aliases that are cryptic abbreviations (u2&lt;code&gt;, &lt;/code&gt;x&lt;code&gt;, &lt;/code&gt;t3`)&lt;/li&gt;&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;SQL style is team infrastructure. Establish a convention in your team's contributing guide, enforce it with a formatter in CI, and you'll spend less time decoding queries and more time understanding what they do.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19471/how-to-format-sql-for-readability-a-practical-style-guide</guid>
<pubDate>Sat, 06 Jun 2026 10:03:48 +0000</pubDate>
</item>
<item>
<title>SQL Formatting Best Practices: Make Your Queries Readable and Maintainable</title>
<link>https://coderlegion.com/19655/sql-formatting-best-practices-make-your-queries-readable-and-maintainable</link>
<description>&lt;p&gt;SQL is one of the few languages where formatting is genuinely optional — the engine doesn't care about whitespace, indentation, or casing. But your teammates do. And so does future-you.&lt;/p&gt;
&lt;h2&gt;The Core Rules&lt;/h2&gt;
&lt;h3&gt;1. Uppercase keywords&lt;/h3&gt;
&lt;p&gt;Most style guides recommend uppercase SQL keywords and lowercase table/column names. It creates visual hierarchy:&lt;/p&gt;
&lt;p&gt;``&lt;code&gt;sql&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;SELECT id, name, email&lt;/p&gt;
&lt;p&gt;FROM users&lt;/p&gt;
&lt;p&gt;WHERE is_active = true&lt;/p&gt;
&lt;p&gt;ORDER BY name ASC;&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;vs. the unformatted version:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;sql
&amp;lt;p&amp;gt;select id,name,email from users where is_active=true order by name asc;&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;2. One clause per line&lt;/h3&gt;
&lt;p&gt;Each major clause starts on a new line. Conditions are indented:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;sql
&amp;lt;p&amp;gt;SELECT&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;u.id,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;u.name,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;COUNT(o.id) AS order_count&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;FROM users u&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;LEFT JOIN orders o&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;ON o.user_id = u.id&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;WHERE u.is_active = true&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;AND u.created_at &amp;gt; '2026-01-01'&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;GROUP BY u.id, u.name&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;ORDER BY order_count DESC&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;LIMIT 100;&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;3. Indent subqueries&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;sql
&amp;lt;p&amp;gt;SELECT *&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;FROM (&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;SELECT user_id, SUM(amount) AS total&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;FROM orders&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;WHERE status = 'completed'&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;GROUP BY user_id&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;) AS spending&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;WHERE spending.total &amp;gt; 1000;&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;4. Use CTEs for complex logic&lt;/h3&gt;
&lt;p&gt;Common Table Expressions make multi-step queries readable:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;sql
&amp;lt;p&amp;gt;WITH active_users AS (&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;SELECT id, name&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;FROM users&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;WHERE is_active = true&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;),&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;high_spenders AS (&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;SELECT user_id, SUM(amount) AS total&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;FROM orders&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;GROUP BY user_id&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;HAVING SUM(amount) &amp;gt; 1000&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;)&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;SELECT au.name, hs.total&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;FROM active_users au&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;JOIN high_spenders hs ON hs.user_id = au.id&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;ORDER BY hs.total DESC;&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;`&lt;/p&gt;
&lt;h2&gt;Formatting Reference&lt;/h2&gt;
&lt;p&gt;| Element | Convention |&lt;/p&gt;
&lt;p&gt;|---|---|&lt;/p&gt;
&lt;p&gt;| SQL keywords | UPPERCASE |&lt;/p&gt;
&lt;p&gt;| Table/column names | lowercase_snake_case |&lt;/p&gt;
&lt;p&gt;| Aliases | Explicit AS keyword |&lt;/p&gt;
&lt;p&gt;| Indentation | 4 spaces |&lt;/p&gt;
&lt;p&gt;| Max line length | ~100 characters |&lt;/p&gt;
&lt;p&gt;| Semicolons | Always close statements |&lt;/p&gt;
&lt;h2&gt;Why This Matters in Code Review&lt;/h2&gt;
&lt;p&gt;Badly formatted SQL creates two problems during review:&lt;/p&gt;
&lt;ol&gt;&lt;li&gt;&lt;strong&gt;The reviewer can't spot logic errors&lt;/strong&gt; because they're fighting the formatting&lt;/li&gt;&lt;li&gt;&lt;strong&gt;The author can't catch their own bugs&lt;/strong&gt; because nesting obscures the query structure&lt;/li&gt;&lt;/ol&gt;
&lt;p&gt;With consistent formatting, a five-level JOIN query becomes scannable. Without it, you're debugging character-by-character.&lt;/p&gt;
&lt;h2&gt;Format Without Thinking About It&lt;/h2&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/sql-formatter-beautifier/&quot;&gt;SnappyTools SQL Formatter &amp;amp; Beautifier&lt;/a&gt; formats any SQL query in one paste. It handles indentation, keyword casing, and clause alignment. Takes 5 seconds and saves everyone who reads the code 5 minutes.&lt;/p&gt;
&lt;p&gt;Runs in-browser, no upload, no account.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Does your team have a SQL style guide? Or does formatting vary by developer?&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/19655/sql-formatting-best-practices-make-your-queries-readable-and-maintainable</guid>
<pubDate>Fri, 05 Jun 2026 23:53:21 +0000</pubDate>
</item>
<item>
<title>SQL Formatting and Beautification: Standards, Tools, and Best Practices</title>
<link>https://coderlegion.com/19353/sql-formatting-and-beautification-standards-tools-and-best-practices</link>
<description>&lt;p&gt;Unformatted SQL is a maintenance problem. A 200-character single-line query is hard to review in a PR, harder to debug under pressure, and inconsiderate to hand off to a teammate. SQL formatting costs nothing and pays back immediately.&lt;/p&gt;
&lt;h2&gt;What a SQL Formatter Does&lt;/h2&gt;
&lt;p&gt;A formatter restructures query text into a consistent, indented layout — without changing what the query does. The database ignores whitespace; formatting is entirely for humans.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;&lt;p&gt;&lt;code&gt;`&amp;lt;code&amp;gt;sql&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;select u.id,u.name,o.total from users u inner join orders o on u.id=o.user_id where o.status='paid' order by o.total desc limit 100;&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;&amp;lt;code&amp;gt;sql
&amp;lt;p&amp;gt;SELECT&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;u.id,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;u.name,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;o.total&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;FROM&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;users u&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;INNER JOIN orders o ON u.id = o.user_id&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;WHERE&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;o.status = 'paid'&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;ORDER BY&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;o.total DESC&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;LIMIT 100;&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Same query. Dramatically more readable.&lt;/p&gt;
&lt;h2&gt;Key Formatting Decisions&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Keyword case&lt;/strong&gt; — SQL keywords are case-insensitive. SELECT&lt;code&gt; vs &lt;/code&gt;select&lt;code&gt;. Uppercase is the most common convention (keywords stand out visually from identifiers). Choose one and stick to it across your codebase.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Clause alignment&lt;/strong&gt; — Each major clause (SELECT&lt;code&gt;, &lt;/code&gt;FROM&lt;code&gt;, &lt;/code&gt;WHERE&lt;code&gt;, &lt;/code&gt;GROUP BY&lt;code&gt;, &lt;/code&gt;ORDER BY&lt;code&gt;) starts a new line. Conditions within a clause are indented.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Leading vs trailing commas:&lt;/strong&gt;&lt;br&gt;
`&lt;code&gt;sql&lt;br&gt;
-- Trailing (most common)&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;SELECT&lt;/p&gt;&lt;br&gt;
&lt;p&gt;id,&lt;/p&gt;&lt;br&gt;
&lt;p&gt;name,&lt;/p&gt;&lt;br&gt;
&lt;p&gt;email&lt;/p&gt;
&lt;p&gt;-- Leading (easier to add/remove first item)&lt;/p&gt;
&lt;p&gt;SELECT&lt;/p&gt;
&lt;p&gt;id&lt;/p&gt;
&lt;p&gt;,name&lt;/p&gt;
&lt;p&gt;,email&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AND/OR placement:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;&amp;lt;code&amp;gt;sql
&amp;lt;p&amp;gt;-- AND at start of line (easy to comment out one condition)&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;WHERE&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;status = 'active'&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;AND created_at &amp;gt; '2026-01-01'&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;AND amount &amp;gt; 100&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;SQL Dialects&lt;/h2&gt;
&lt;p&gt;SQL is not one standard. Each database has extensions:&lt;/p&gt;
&lt;p&gt;| Database | Notes |&lt;/p&gt;
&lt;p&gt;|---|---|&lt;/p&gt;
&lt;p&gt;| PostgreSQL | ILIKE&lt;code&gt;, &lt;/code&gt;::&lt;code&gt; casting, &lt;/code&gt;$$&lt;code&gt; dollar quoting, CTEs everywhere |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| MySQL | Backtick identifiers, LIMIT x,y&lt;code&gt;, &lt;/code&gt;AUTO_INCREMENT&lt;code&gt; |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| SQLite | Minimal syntax, no stored procedures |&lt;/p&gt;
&lt;p&gt;| SQL Server (T-SQL) | TOP&lt;code&gt;, &lt;/code&gt;NOLOCK&lt;code&gt;, &lt;/code&gt;GETDATE()&lt;code&gt;, &lt;/code&gt;GO&lt;code&gt; |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| BigQuery | STRUCT&lt;code&gt;, &lt;/code&gt;ARRAY_AGG&lt;code&gt;, &lt;/code&gt;UNNEST&lt;code&gt;, standard SQL mode |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;A good formatter knows which dialect you're using. &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/sql-formatter-beautifier/&quot;&gt;SnappyTools' SQL formatter&lt;/a&gt; supports all major dialects via a dropdown selector.&lt;/p&gt;
&lt;h2&gt;Beautification: Compressing SQL Back&lt;/h2&gt;
&lt;p&gt;The reverse of formatting — collapsing multi-line SQL to a single line — is useful for:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Embedding SQL in JSON configuration&lt;/li&gt;&lt;li&gt;Storing queries in environment variables&lt;/li&gt;&lt;li&gt;Code generation output&lt;/li&gt;&lt;li&gt;Sending queries in HTTP request bodies&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;Both directions are available at &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/sql-formatter-beautifier/&quot;&gt;snappytools.app/sql-formatter-beautifier/&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Automating SQL Formatting in CI&lt;/h2&gt;
&lt;p&gt;For teams, use sqlfluff&lt;code&gt;:&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;bash&lt;br&gt;
pip install sqlfluff&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;Lint&lt;/h1&gt;
&lt;p&gt;sqlfluff lint queries/ --dialect postgres&lt;/p&gt;
&lt;h1&gt;Auto-fix&lt;/h1&gt;
&lt;p&gt;sqlfluff fix queries/ --dialect postgres&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;.sqlfluff&lt;code&gt; config:&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&amp;lt;code&amp;gt;ini
&amp;lt;p&amp;gt;[sqlfluff]&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;dialect = postgres&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;indent_unit = space&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;tab_space_size = 2&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;For pre-commit hooks:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;yaml
&amp;lt;h1&amp;gt;.pre-commit-config.yaml&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;repos:&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;- repo: https://github.com/sqlfluff/sqlfluff&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;rev: 3.0.0&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;hooks:&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;- id: sqlfluff-lint&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;args: [--dialect, postgres]&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Anti-Patterns to Avoid&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Implicit joins&lt;/strong&gt; (hard to see the join condition):&lt;br&gt;
`&lt;code&gt;sql&lt;br&gt;
-- Avoid&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;SELECT * FROM orders, users WHERE orders.user_id = users.id;&lt;/p&gt;
&lt;p&gt;-- Better&lt;/p&gt;
&lt;p&gt;SELECT * FROM orders INNER JOIN users ON orders.user_id = users.id;&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SELECT &lt;em&gt; in production&lt;/em&gt;&lt;/strong&gt; — Not formatting, but worth mentioning: always name columns explicitly. SELECT &lt;code&gt; breaks when the schema changes.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Inconsistent keyword case within one query:&lt;/strong&gt;&lt;br&gt;
`&lt;code&gt;sql&lt;br&gt;
-- Bad&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;select id FROM users WHERE status='active';&lt;/p&gt;
&lt;p&gt;-- Good&lt;/p&gt;
&lt;p&gt;SELECT id FROM users WHERE status = 'active';&lt;/p&gt;
&lt;p&gt;``&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;SQL formatting is about maintainability and team consistency, not performance. Consistent uppercase keywords, clause-per-line layout, and proper indentation make queries reviewable and debuggable. For one-off formatting of queries from APIs, schema dumps, or colleagues: &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/sql-formatter-beautifier/&quot;&gt;snappytools.app/sql-formatter-beautifier/&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/sql-formatter-beautifier/&quot;&gt;https://snappytools.app/sql-formatter-beautifier/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19353/sql-formatting-and-beautification-standards-tools-and-best-practices</guid>
<pubDate>Sun, 31 May 2026 14:37:02 +0000</pubDate>
</item>
<item>
<title>5 SQL Habits That Make Your Queries Unmaintainable</title>
<link>https://coderlegion.com/19050/5-sql-habits-that-make-your-queries-unmaintainable</link>
<description>&lt;p&gt;Most SQL problems aren't about syntax. They're about habits. The same bad patterns show up in every codebase, written by developers who knew better but were in a hurry. Here are five to stop immediately.&lt;/p&gt;
&lt;h2&gt;1. Single-line queries&lt;/h2&gt;
&lt;p&gt;``&lt;code&gt;sql&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;SELECT u.id, u.name, u.email, o.total FROM users u INNER JOIN orders o ON u.id = o.user_id WHERE u.active = 1 AND o.total &amp;gt; 100 ORDER BY o.created_at DESC LIMIT 50&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This works. Nothing will break. But when you have to debug it at 2am — or when a colleague has to touch it — you'll regret every character on that line.&lt;/p&gt;
&lt;p&gt;Fix: one clause per line, always.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;sql
&amp;lt;p&amp;gt;SELECT&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;u.id,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;u.name,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;u.email,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;o.total&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;FROM users u&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;INNER JOIN orders o ON u.id = o.user_id&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;WHERE u.active = 1&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;AND o.total &amp;gt; 100&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;ORDER BY o.created_at DESC&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;LIMIT 50&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Same query. Completely different to work with.&lt;/p&gt;
&lt;h2&gt;2. SELECT *&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;sql
&amp;lt;p&amp;gt;SELECT * FROM users&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I know. It's fast to write. But it creates invisible dependencies — your application code may break silently when someone adds or removes a column. You also fetch data you don't need, which hits performance at scale.&lt;/p&gt;
&lt;p&gt;Always name your columns. Yes, even for quick exploratory queries in development — the habit transfers to production code.&lt;/p&gt;
&lt;h2&gt;3. Implicit joins&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;sql
&amp;lt;p&amp;gt;-- Old style&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;SELECT u.name, o.total&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;FROM users u, orders o&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;WHERE u.id = o.user_id&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This is valid SQL from the 1980s. It still runs. But mixing data relationships (the u.id = o.user_id&lt;code&gt; filter) with data filters (business logic in &lt;/code&gt;WHERE&lt;code&gt;) is a recipe for accidental Cartesian products and confused reviewers.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Explicit JOIN&lt;code&gt; syntax is unambiguous:&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;sql
&amp;lt;p&amp;gt;SELECT u.name, o.total&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;FROM users u&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;INNER JOIN orders o ON u.id = o.user_id&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;4. Magic numbers in conditions&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;sql
&amp;lt;p&amp;gt;WHERE status = 2&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;WHERE type = 7&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;WHERE permission_level &amp;gt;= 4&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;What is status 2? What is type 7? You might know today. In three months, neither you nor anyone else will.&lt;/p&gt;
&lt;p&gt;Use comments, constants in your application layer, or at minimum leave a comment in the SQL:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;sql
&amp;lt;p&amp;gt;WHERE status = 2 -- 2 = active&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;WHERE type = 7   -- 7 = premium_user&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;`&lt;/p&gt;
&lt;p&gt;Better yet, if your database supports it, use an ENUM or a lookup table.&lt;/p&gt;
&lt;h2&gt;5. No formatting before committing&lt;/h2&gt;
&lt;p&gt;Raw, unformatted SQL in migrations, stored procedures, and ORM raw queries gets committed and stays there forever. Once it's in the codebase, nobody reformats it — touching it means potentially breaking it.&lt;/p&gt;
&lt;p&gt;The fix takes 10 seconds. Before you commit any SQL, paste it into a formatter. &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/sql-formatter-beautifier/&quot;&gt;SnappyTools SQL Formatter&lt;/a&gt; handles 19 dialects (PostgreSQL, MySQL, SQL Server, SQLite, BigQuery, and more). Pick your dialect, choose your indent style, format, and copy. It runs in your browser — no setup, no signup.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Good SQL is readable SQL. These aren't academic style preferences — they directly affect how quickly bugs get caught in reviews, how fast new team members can understand the codebase, and how much time you spend debugging vs. shipping.&lt;/p&gt;
&lt;p&gt;Pick the habit that's costing you the most time right now and fix it first.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Format SQL in seconds: &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/sql-formatter-beautifier/&quot;&gt;SnappyTools SQL Formatter&lt;/a&gt; — 19 dialects, browser-based, no signup.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/sql-formatter-beautifier/&quot;&gt;&lt;/a&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/sql-formatter-beautifier/&amp;lt;/a&amp;gt;&amp;lt;/em&amp;gt;&amp;lt;/p&amp;gt;&quot;&gt;https://snappytools.app/sql-formatter-beautifier/&amp;lt;/a&amp;gt;&amp;lt;/em&amp;gt;&amp;lt;/p&amp;gt;&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19050/5-sql-habits-that-make-your-queries-unmaintainable</guid>
<pubDate>Wed, 27 May 2026 10:09:37 +0000</pubDate>
</item>
<item>
<title>SQL Formatting: Why It Matters and How to Do It Fast</title>
<link>https://coderlegion.com/18815/sql-formatting-why-it-matters-and-how-to-do-it-fast</link>
<description>&lt;p&gt;SQL is the language that powers most of the world's data — and it is also one of the most commonly written in the least readable way. A query that runs correctly often looks like this in the wild:&lt;/p&gt;
&lt;p&gt;``&lt;code&gt;sql&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;SELECT u.id,u.name,o.total FROM users u JOIN orders o ON u.id=o.user_id WHERE o.created_at&amp;gt;'2024-01-01' AND o.status='completed' ORDER BY o.total DESC LIMIT 50&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Nobody reviews that in a pull request. Nobody debugs that at 2am. Formatting is not just aesthetic — it is a correctness tool.&lt;/p&gt;
&lt;h2&gt;What SQL Formatting Actually Does&lt;/h2&gt;
&lt;p&gt;A SQL formatter applies consistent rules to your query structure:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Uppercase keywords&lt;/strong&gt; — SELECT&lt;code&gt;, &lt;/code&gt;FROM&lt;code&gt;, &lt;/code&gt;WHERE&lt;code&gt;, &lt;/code&gt;JOIN&lt;code&gt;, &lt;/code&gt;GROUP BY&lt;code&gt; stand out visually from column names and values&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Indentation&lt;/strong&gt; — each clause starts on a new line; nested subqueries are indented further&lt;/li&gt;&lt;li&gt;&lt;strong&gt;One clause per line&lt;/strong&gt; — makes it easy to add, remove, or comment out individual conditions&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Aligned expressions&lt;/strong&gt; — long SELECT&lt;code&gt; lists and &lt;/code&gt;WHERE&lt;code&gt; conditions line up vertically&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;The same query formatted:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;sql
&amp;lt;p&amp;gt;SELECT&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;u.id,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;u.name,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;o.total&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;FROM users u&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;JOIN orders o ON u.id = o.user_id&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;WHERE&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;o.created_at &amp;gt; '2024-01-01'&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;AND o.status = 'completed'&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;ORDER BY o.total DESC&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;LIMIT 50&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The logic is identical. The readability is not.&lt;/p&gt;
&lt;h2&gt;Dialect Differences Matter&lt;/h2&gt;
&lt;p&gt;SQL has a standard, but every major database deviates from it. Formatting should reflect the dialect your team uses:&lt;/p&gt;
&lt;p&gt;| Database | Notable formatting differences |&lt;/p&gt;
&lt;p&gt;|----------|-------------------------------|&lt;/p&gt;
&lt;p&gt;| MySQL | BACKTICK&lt;code&gt; identifiers, &lt;/code&gt;LIMIT n, m&lt;code&gt; syntax |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| PostgreSQL | $$&lt;code&gt; dollar-quoting, &lt;/code&gt;::&lt;code&gt; cast syntax |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| SQL Server / T-SQL | [BRACKET]&lt;code&gt; identifiers, &lt;/code&gt;TOP n&lt;code&gt; instead of &lt;/code&gt;LIMIT&lt;code&gt; |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| BigQuery | Backtick identifiers, STRUCT&lt;code&gt; and &lt;/code&gt;ARRAY&lt;code&gt; types |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| SQLite | Minimal type system, AUTOINCREMENT&lt;code&gt; quirks |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;A good formatter understands which keywords are dialect-specific and won't flag them as errors.&lt;/p&gt;
&lt;h2&gt;When to Format&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Code review&lt;/strong&gt; — unformatted SQL in a migration or stored procedure is a review blocker. Format before you commit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Debugging&lt;/strong&gt; — when a query returns wrong results, formatting it makes the logical structure visible. Missing parentheses, wrong JOIN conditions, and misplaced AND&lt;code&gt;/&lt;/code&gt;OR&lt;code&gt; groupings reveal themselves immediately.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Refactoring&lt;/strong&gt; — adding a new JOIN&lt;code&gt; or changing a &lt;/code&gt;WHERE&lt;code&gt; clause is far easier on formatted code. You can see exactly where to insert the new clause.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sharing with a DBA&lt;/strong&gt; — formatted queries communicate intent. A DBA who can't parse your query can't help you optimise it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Legacy code review&lt;/strong&gt; — inherited scripts often look like one long line. Format them first, understand them second.&lt;/p&gt;
&lt;h2&gt;Inline vs. Standalone Formatting&lt;/h2&gt;
&lt;p&gt;For &lt;strong&gt;day-to-day development&lt;/strong&gt;: most IDEs have SQL formatting built in (VS Code SQLTools extension, DataGrip's built-in formatter, DBeaver's Ctrl+Shift+F&lt;code&gt;). Set a standard style in your team's &lt;/code&gt;.editorconfig&lt;code&gt; or IDE settings and format on save.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;For &lt;strong&gt;quick ad-hoc formatting&lt;/strong&gt; (a query from a log, a schema from Stack Overflow, a vendor-supplied migration): a browser-based tool is fastest. No configuration, no install, paste and go.&lt;/p&gt;
&lt;h2&gt;Building a Team SQL Style Guide&lt;/h2&gt;
&lt;p&gt;Consistent formatting is more important than which convention you choose. A short style guide prevents formatting debates in code review and makes automated formatting enforceable:&lt;/p&gt;
&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Keyword case&lt;/strong&gt;: UPPERCASE keywords (near-universal convention)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Identifier case&lt;/strong&gt;: lowercase table and column names&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Indentation&lt;/strong&gt;: 2 or 4 spaces (pick one, enforce it)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Join style&lt;/strong&gt;: explicit JOIN&lt;code&gt; always (never implicit comma-joins)&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Trailing comma vs leading comma&lt;/strong&gt;: leading commas (  , column_name&lt;code&gt;) help with git diffs — controversial but worth a team discussion&lt;/code&gt;&lt;/li&gt;&lt;/ol&gt;
&lt;p&gt;Document the guide in your repo's README or a CONTRIBUTING.md and point your formatter configuration at it.&lt;/p&gt;
&lt;h2&gt;SQL Formatting in CI&lt;/h2&gt;
&lt;p&gt;For teams serious about consistency, format checking can run in CI:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;sqlfluff&lt;/strong&gt; — Python-based SQL linter + formatter with dialect support and CI integration&lt;/li&gt;&lt;li&gt;&lt;strong&gt;pgFormatter&lt;/strong&gt; — PostgreSQL-specific, used in pre-commit hooks&lt;/li&gt;&lt;li&gt;&lt;strong&gt;prettier-plugin-sql&lt;/strong&gt; — Prettier plugin for SQL files&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;A CI gate that rejects unformatted SQL is the same pattern as eslint&lt;code&gt; or &lt;/code&gt;black` for JavaScript or Python. It removes the &quot;formatting nit&quot; category from code reviews entirely.&lt;/p&gt;
&lt;h2&gt;Try It in Your Browser&lt;/h2&gt;
&lt;p&gt;If you need to quickly format a query without installing anything, &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/&quot;&gt;SnappyTools&lt;/a&gt; provides free, browser-based tools for developers. All processing happens in your browser — no code is sent to a server.&lt;/p&gt;
&lt;p&gt;Clean queries are debuggable queries. Format early, format consistently, and your future self (and your teammates) will thank you.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/&quot;&gt;https://snappytools.app/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18815/sql-formatting-why-it-matters-and-how-to-do-it-fast</guid>
<pubDate>Sun, 24 May 2026 10:12:27 +0000</pubDate>
</item>
<item>
<title>Understanding Logic, Reusability and Integrity On SQL ; Procedures, Functions and Transactions.</title>
<link>https://coderlegion.com/17301/understanding-logic-reusability-and-integrity-sql-procedures-functions-transactions</link>
<description>&lt;p&gt;SQL is widely known for data querying and manipulation but systems do grow; data becomes larger; processes become repetitive and operations become sensitive. SQL has some features which enables it to be considered a fully fledged programming language. Some of the features which I discuss in this article are procedures, functions and transactions. Each of these concepts serve distinct purposes.&lt;br&gt;
Procedures execute operations, functions return values, and transactions ensure those operations are safe.&lt;br&gt;
&lt;/p&gt;&lt;h2&gt;Stored Procedures&lt;/h2&gt;&lt;br&gt;
These are set of SQL statements stored in the database and executed as a unit which are used to perform tasks such as &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt; etc.&lt;br&gt;
They are triggered by calling them and passing the expected parameters by the procedure.&lt;br&gt;
Here is an example of a procedure:
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE OR REPLACE PROCEDURE increase_salary(p_dept TEXT, p_percent NUMERIC)
LANGUAGE plpgsql
AS \(
BEGIN
    UPDATE employees
    SET salary = salary + (salary * p_percent / 100)
    WHERE department = p_dept;
END;
\);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is how a procedure is called:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CALL increase_salary('IT', 10);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Functions&lt;/h2&gt;
&lt;p&gt;This is a reusable logic block which can return values and can be used inside queries for data selection. &lt;br&gt;
It can be used with &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt; and is great for reusability.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE OR REPLACE FUNCTION get_avg_salary(p_dept TEXT)
RETURNS NUMERIC
LANGUAGE plpgsql
AS \(
BEGIN
    RETURN (
        SELECT AVG(salary)
        FROM employees
        WHERE department = p_dept
    );
END;
\);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is an example of how the function can be used:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT name, salary
FROM employees
WHERE salary &amp;gt; get_avg_salary(department);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Transactions&lt;/h2&gt;
&lt;p&gt;Transactions are used to group a set of code operations in which if any of it fails then the whole execution is aborted. This ensures that data is only changed when the full code execution structure is successful.&lt;br&gt;
Transactions are best for data safety as they prevent partial updates.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;BEGIN;

UPDATE accounts
SET balance = balance - 100
WHERE id = 1;

UPDATE accounts
SET balance = balance + 100
WHERE id = 2;

COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If it fails:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;ROLLBACK;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;How They Work Together&lt;/h2&gt;
&lt;p&gt;It will not be complete to conclude the article without showing an example of how these concepts can be used together. &lt;br&gt;
Here is a simple scenario showing the use of the three concepts interdependently.&lt;br&gt;
&lt;b&gt;Function:&lt;/b&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE OR REPLACE FUNCTION get_balance(acc_id INT)
RETURNS NUMERIC AS \(
BEGIN
    RETURN (SELECT balance FROM accounts WHERE id = acc_id);
END;
\) LANGUAGE plpgsql;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;b&gt;Procedure:&lt;/b&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE OR REPLACE PROCEDURE transfer_money(from_id INT, to_id INT, amount NUMERIC)
LANGUAGE plpgsql
AS $$
BEGIN
    IF get_balance(from_id) &amp;lt; amount THEN
        RAISE EXCEPTION 'Insufficient funds';
    END IF;

    UPDATE accounts SET balance = balance - amount WHERE id = from_id;
    UPDATE accounts SET balance = balance + amount WHERE id = to_id;
END;
$$;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;b&gt;Transaction:&lt;/b&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;BEGIN;
CALL transfer_money(1, 2, 100);
COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;SQL has a wide range of capabilities more than just being a querying language. Having features such as procedures, functions, transactions and many others helps it to be an efficient tool for use on data in whichever way needed. Mastering it comes a long way in helping the analysis of data directly from the database.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17301/understanding-logic-reusability-and-integrity-sql-procedures-functions-transactions</guid>
<pubDate>Sun, 10 May 2026 16:23:47 +0000</pubDate>
</item>
<item>
<title>Sub-queries vs Window Functions vs Common Table Expressions: Which Should You Use in SQL?</title>
<link>https://coderlegion.com/16078/sub-queries-vs-window-functions-vs-common-table-expressions-which-should-you-use-in-sql</link>
<description>&lt;p&gt;SQL  is the most used tool for data manipulation but what happens when there are some concepts that seem to work towards the same ultimate outputs. As queries become more complex, developers often encounter different techniques that appear to produce similar results—namely sub-queries, common table expressions (CTEs), and window functions.&lt;br&gt;
At first glance, these concepts can seem interchangeable. You might solve the same problem using any of them, which raises a natural question: &lt;i&gt;which one is better?&lt;/i&gt;&lt;br&gt;
&lt;strong&gt;The goal is not to choose one, but to understand when each is the right tool.&lt;/strong&gt;&lt;br&gt;
In this article I will discuss about sub-queries, window functions and common table expressions;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sub-Queries&lt;/strong&gt;&lt;br&gt;
These are queries inside other queries and are executed either before or alongside the outer query. They can return scalar value, row or table.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT *
FROM employees e
WHERE salary = (
    SELECT MAX(salary)
    FROM employees
    WHERE department = e.department
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Window functions&lt;/strong&gt;&lt;br&gt;
These are used when you are trying to preserve your rows. Used together with components such as - &lt;code&gt;OVER()&lt;/code&gt;,&lt;code&gt;PARTITION BY&lt;/code&gt;,&lt;code&gt;ORDER BY&lt;/code&gt; and common functions such as - &lt;code&gt;SUM()&lt;/code&gt;,&lt;code&gt;AVG()&lt;/code&gt;,&lt;code&gt;RANK()&lt;/code&gt;,&lt;code&gt;LAG()&lt;/code&gt;,&lt;code&gt;LEAD()&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT employee_id,first_name, last_name,salary, job_title, department
FROM (
  SELECT *,
    RANK() OVER(PARTITION BY department ORDER BY salary DESC) AS rnk
  FROM employees
) t
WHERE rnk = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Common Table Expressions (CTEs)&lt;/strong&gt;&lt;br&gt;
These are temporary results set that are initialized using -&lt;code&gt;WITH&lt;/code&gt;. They are often used to improve readability and modularity of scripts.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH max_salary AS (
    SELECT department, MAX(salary) AS max_sal
    FROM employees
    GROUP BY department
)
SELECT e.*
FROM employees e
JOIN max_salary m
    ON e.department = m.department
WHERE e.salary = m.max_sal;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All these features of SQL are used to perform advanced data querying and transformations. They can all be tweaked to perform almost similar queries but it all depends on what the final output is expected to be.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When to use what&lt;/strong&gt;&lt;br&gt;
Each of the features has an instance when it is the most convenient over the others. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For instance when it is a simple filtering such as retaining all those above average marks then a sub-query is the best option.&lt;/li&gt;
&lt;li&gt;When we are more focused on row level analytics then the window functions come in handy for that task.&lt;/li&gt;
&lt;li&gt;In a situation where it is a multi-step logic and re-usability is a core factor the common table expressions are best suited for use.&lt;br&gt;
&lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Using in collaboration&lt;/strong&gt;&lt;br&gt;
The best thing about this features is that they can be used together for the best solution. This helps in the data transformation where there are steps within it in which either is considered to be superior over the other.&lt;br&gt;
Here is an example where all of them are used together:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;with ranked AS (
    SELECT 
        *,
        AVG(salary) OVER (PARTITION BY department) AS avg_salary
    FROM employees
)
SELECT *
FROM ranked
WHERE 
    salary &amp;gt; avg_salary
    AND salary &amp;gt; (
        SELECT AVG(salary) FROM employees
    );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Best Practices&lt;/strong&gt;&lt;br&gt;
These are some of the recommended practices when working with these SQL features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It is best to avoid deep nesting of queries by using CTEs which help in the structuring of the script for easy readability.&lt;/li&gt;
&lt;li&gt;When using CTEs it is necessary to name them well so that their purpose can be identified easily without having to read all the code snippets manually.&lt;/li&gt;
&lt;li&gt;Window functions are often more efficient than sub queries in large datasets, so where possible it is best to use window functions over sub queries but in instances of small datasets any can be used depending on preference.&lt;br&gt;
&lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
These are to be considered more as &lt;strong&gt;complementary tools&lt;/strong&gt; than competing tools. In most advanced queries you will have to use them together or interchangeably to get to the results. &lt;br&gt;
What makes a major difference is how you use them as they can heavily impact the performance. It is best to master all of them and understand the strength and weaknesses of each so that when it get to a point of picking one or more you do it from a point of information.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16078/sub-queries-vs-window-functions-vs-common-table-expressions-which-should-you-use-in-sql</guid>
<pubDate>Tue, 28 Apr 2026 16:33:58 +0000</pubDate>
</item>
<item>
<title>The 5 Biggest Traps When Writing Your Own SQL Injection Filter</title>
<link>https://coderlegion.com/15477/the-5-biggest-traps-when-writing-your-own-sql-injection-filter</link>
<description>&lt;p&gt;SQL injection is one of those vulnerabilities every developer &lt;em&gt;thinks&lt;/em&gt; they understand — until their handcrafted “secure” filter gets bypassed by a payload they never anticipated.&lt;/p&gt;
&lt;p&gt;The pattern repeats constantly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A project launches fast.&lt;/li&gt;
&lt;li&gt;Someone adds a few blacklist rules.&lt;/li&gt;
&lt;li&gt;A regex grows into a monster.&lt;/li&gt;
&lt;li&gt;Edge cases accumulate.&lt;/li&gt;
&lt;li&gt;Attackers eventually walk straight through it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Despite two decades of awareness, SQL injection remains part of the OWASP Top 10 because many defenses are still fundamentally flawed at the architectural level.&lt;/p&gt;
&lt;p&gt;This article breaks down five common traps teams fall into when building their own SQL injection filtering logic, along with real-world bypass techniques that continue to work in production systems.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-diy-sql-injection-filters-keep-failing&quot;&gt;Why DIY SQL Injection Filters Keep Failing&lt;/h2&gt;
&lt;p&gt;Most custom filters are based on a false assumption:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Malicious SQL can be reliably identified through string matching.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That assumption collapses immediately under real attacker behavior.&lt;/p&gt;
&lt;p&gt;Modern SQL injection payloads are not static strings. They are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Context-aware&lt;/li&gt;
&lt;li&gt;Encoding-aware&lt;/li&gt;
&lt;li&gt;Database-specific&lt;/li&gt;
&lt;li&gt;Parser-dependent&lt;/li&gt;
&lt;li&gt;Often multi-stage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Attackers are not fighting your regex. They are fighting the SQL parser behind your application.&lt;/p&gt;
&lt;p&gt;That distinction matters.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;trap-1-keyword-blacklists-are-fundamentally-fragile&quot;&gt;Trap #1: Keyword Blacklists Are Fundamentally Fragile&lt;/h2&gt;
&lt;p&gt;The classic beginner defense looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;blocked = [&quot;select&quot;, &quot;union&quot;, &quot;drop&quot;, &quot;--&quot;]for keyword in blocked:    if keyword in user_input.lower():        reject()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This fails almost instantly.&lt;/p&gt;
&lt;p&gt;Attackers can mutate payloads endlessly while preserving SQL semantics.&lt;/p&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SeLeCtUN/**/IONUNI%0aON/*!50000SELECT*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MySQL comments alone create countless bypass opportunities:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT/**/password/**/FROM/**/users
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UNIunionON SELECT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your filter removes &lt;code&gt;union&lt;/code&gt;, the payload may still parse successfully depending on reconstruction behavior.&lt;/p&gt;
&lt;p&gt;The core issue is simple:&lt;/p&gt;
&lt;p&gt;SQL is not a plain text format. It is a grammar.&lt;/p&gt;
&lt;p&gt;Trying to secure a grammar using substring matching is structurally weak.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;trap-2-encoding-and-charset-edge-cases&quot;&gt;Trap #2: Encoding and Charset Edge Cases&lt;/h2&gt;
&lt;p&gt;This is where many “works in testing” filters completely collapse in production.&lt;/p&gt;
&lt;p&gt;A famous example is the wide-byte injection problem in older MySQL + PHP environments using GBK encoding.&lt;/p&gt;
&lt;p&gt;Example payload:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;%bf%27 OR 1=1 --
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Under certain multibyte encodings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;%bf%27&lt;/code&gt; becomes a valid multibyte character&lt;/li&gt;
&lt;li&gt;The quote escaping breaks&lt;/li&gt;
&lt;li&gt;The trailing SQL becomes executable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The application believes the quote was escaped safely.&lt;/p&gt;
&lt;p&gt;The database parser disagrees.&lt;/p&gt;
&lt;p&gt;That parser mismatch is exactly what attackers exploit.&lt;/p&gt;
&lt;p&gt;Other common encoding tricks include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Double URL encoding&lt;/li&gt;
&lt;li&gt;UTF-16 transformations&lt;/li&gt;
&lt;li&gt;Unicode normalization&lt;/li&gt;
&lt;li&gt;Mixed charset confusion&lt;/li&gt;
&lt;li&gt;Overlong UTF-8 sequences&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most homemade filters never even inspect requests at this layer.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;trap-3-second-order-sql-injection&quot;&gt;Trap #3: Second-Order SQL Injection&lt;/h2&gt;
&lt;p&gt;Many teams only inspect incoming requests.&lt;/p&gt;
&lt;p&gt;That misses one of the nastiest categories of SQL injection entirely.&lt;/p&gt;
&lt;p&gt;Second-order SQL injection happens when:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Malicious input is stored safely&lt;/li&gt;
&lt;li&gt;The payload appears harmless initially&lt;/li&gt;
&lt;li&gt;Another backend process later reuses the stored data unsafely&lt;/li&gt;
&lt;li&gt;Injection triggers downstream&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;p&gt;A registration form stores this username safely:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;admin'--
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No immediate damage occurs.&lt;/p&gt;
&lt;p&gt;Months later:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;query = f&quot;SELECT * FROM audit WHERE user = '{stored_username}'&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the payload executes.&lt;/p&gt;
&lt;p&gt;This is why “input filtering” alone is insufficient.&lt;/p&gt;
&lt;p&gt;The dangerous moment is often not ingestion. It is reuse.&lt;/p&gt;
&lt;p&gt;Second-order injection is especially common in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Admin dashboards&lt;/li&gt;
&lt;li&gt;Reporting systems&lt;/li&gt;
&lt;li&gt;Legacy migration scripts&lt;/li&gt;
&lt;li&gt;Analytics tooling&lt;/li&gt;
&lt;li&gt;Scheduled jobs&lt;/li&gt;
&lt;li&gt;Internal APIs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Teams frequently assume internal data is trustworthy. Attackers rely on that assumption.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;trap-4-database-dialect-differences&quot;&gt;Trap #4: Database Dialect Differences&lt;/h2&gt;
&lt;p&gt;A filter tested against MySQL may fail completely against PostgreSQL or MSSQL.&lt;/p&gt;
&lt;p&gt;Each database engine has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Different syntax&lt;/li&gt;
&lt;li&gt;Different comments&lt;/li&gt;
&lt;li&gt;Different casting behavior&lt;/li&gt;
&lt;li&gt;Different concatenation operators&lt;/li&gt;
&lt;li&gt;Different escaping semantics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;p&gt;MySQL:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT @@version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PostgreSQL:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT version()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MSSQL:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT @@version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;String concatenation differences:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;'a' + 'b'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;vs&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;'a' || 'b'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A blacklist that blocks one dialect often misses another.&lt;/p&gt;
&lt;p&gt;Even worse, many modern applications support multiple database backends simultaneously through ORM abstraction layers.&lt;/p&gt;
&lt;p&gt;Your filter may not even know which parser ultimately executes the query.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;trap-5-regex-maintenance-becomes-an-operational-nightmare&quot;&gt;Trap #5: Regex Maintenance Becomes an Operational Nightmare&lt;/h2&gt;
&lt;p&gt;This is the part many engineering teams underestimate.&lt;/p&gt;
&lt;p&gt;The first regex seems manageable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(select|union|drop|insert)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Six months later:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(?i)(union(\s+all)?\s+select|select.+from|benchmark\(|sleep\(|load_file\()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A year later:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;False positives explode&lt;/li&gt;
&lt;li&gt;Legitimate requests break&lt;/li&gt;
&lt;li&gt;Performance degrades&lt;/li&gt;
&lt;li&gt;Nobody fully understands the rules anymore&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now every framework upgrade becomes risky.&lt;/p&gt;
&lt;p&gt;Every new database feature expands attack surface.&lt;/p&gt;
&lt;p&gt;Every edge case requires another patch.&lt;/p&gt;
&lt;p&gt;Eventually the “simple filter” evolves into an unmaintainable shadow-WAF hidden inside business code.&lt;/p&gt;
&lt;p&gt;At that point, your application team is spending engineering time rebuilding a lower-quality security engine.&lt;/p&gt;
&lt;p&gt;That is usually the wrong tradeoff.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-real-problem-sql-injection-defense-is-a-parsing-problem&quot;&gt;The Real Problem: SQL Injection Defense Is a Parsing Problem&lt;/h2&gt;
&lt;p&gt;The central mistake behind most DIY defenses is treating SQL injection as a text-matching problem.&lt;/p&gt;
&lt;p&gt;It is not.&lt;/p&gt;
&lt;p&gt;It is a parser interpretation problem.&lt;/p&gt;
&lt;p&gt;Attackers succeed by creating discrepancies between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Application-layer assumptions&lt;/li&gt;
&lt;li&gt;Middleware transformations&lt;/li&gt;
&lt;li&gt;Database parser behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is why modern defenses increasingly rely on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AST analysis&lt;/li&gt;
&lt;li&gt;Semantic inspection&lt;/li&gt;
&lt;li&gt;Behavioral modeling&lt;/li&gt;
&lt;li&gt;Context-aware parsing&lt;/li&gt;
&lt;li&gt;Multi-layer decoding&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rather than brittle keyword matching.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-actually-works&quot;&gt;What Actually Works&lt;/h2&gt;
&lt;p&gt;Prepared statements and parameterized queries remain the foundation.&lt;/p&gt;
&lt;p&gt;But real production systems are messy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Legacy code exists&lt;/li&gt;
&lt;li&gt;Dynamic query builders exist&lt;/li&gt;
&lt;li&gt;Third-party plugins exist&lt;/li&gt;
&lt;li&gt;Internal tools exist&lt;/li&gt;
&lt;li&gt;Old endpoints survive for years&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Which is why runtime protection still matters.&lt;/p&gt;
&lt;p&gt;A modern WAF should not simply scan for suspicious words.&lt;/p&gt;
&lt;p&gt;It should understand:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SQL grammar structure&lt;/li&gt;
&lt;li&gt;Payload intent&lt;/li&gt;
&lt;li&gt;Encoding transformations&lt;/li&gt;
&lt;li&gt;Obfuscation techniques&lt;/li&gt;
&lt;li&gt;Parser behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is a fundamentally different level of analysis.&lt;/p&gt;
&lt;p&gt;Solutions like SafeLine take this approach by using semantic analysis instead of relying purely on manually maintained blacklist rules. The practical advantage is operational: developers stop wasting time maintaining endless regex patches inside application code and can focus on actual business logic instead.&lt;/p&gt;
&lt;p&gt;That division of responsibility is usually the sane architecture.&lt;/p&gt;
&lt;p&gt;Application code should implement secure query practices.&lt;/p&gt;
&lt;p&gt;Traffic inspection engines should handle hostile payload analysis at scale.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thoughts-2&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;The history of SQL injection defense is full of teams attempting to out-regex attackers.&lt;/p&gt;
&lt;p&gt;It rarely ends well.&lt;/p&gt;
&lt;p&gt;Attackers only need one parser discrepancy.&lt;/p&gt;
&lt;p&gt;Defenders maintaining blacklist logic need to correctly anticipate all of them.&lt;/p&gt;
&lt;p&gt;That asymmetry is why handcrafted SQL injection filters keep failing — even in mature systems.&lt;/p&gt;
&lt;p&gt;The deeper lesson is architectural:&lt;/p&gt;
&lt;p&gt;If your security model depends on continuously guessing every future payload mutation attackers might invent, the model is already losing.&lt;/p&gt;
&lt;p&gt;SafeLine Live Demo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://demo.waf.chaitin.com:9443/statistics&quot;&gt;https://demo.waf.chaitin.com:9443/statistics&lt;/a&gt;&lt;br&gt;
Website: &lt;a rel=&quot;nofollow&quot; href=&quot;https://safepoint.cloud/landing/safeline&quot;&gt;https://safepoint.cloud/landing/safeline&lt;/a&gt;&lt;br&gt;
Docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chaitin/SafeLine&quot;&gt;https://github.com/chaitin/SafeLine&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15477/the-5-biggest-traps-when-writing-your-own-sql-injection-filter</guid>
<pubDate>Wed, 22 Apr 2026 07:54:55 +0000</pubDate>
</item>
<item>
<title>5 Things This Playwright SQL Fixture Does So You Don't Have To</title>
<link>https://coderlegion.com/14599/5-things-this-playwright-sql-fixture-does-so-you-dont-have-to</link>
<description>&lt;h2 id=&quot;5-things-this-playwright-sql-fixture-does-so-you-don-t-have-to&quot;&gt;5 Things This Playwright SQL Fixture Does So You Don't Have To&lt;/h2&gt;
&lt;p&gt;If you write Playwright tests that touch a database, you know the boilerplate: connect, seed, clean up, close. Repeat for every file. Hope nothing leaks.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@playwright-labs/fixture-sql&lt;/code&gt; handles that boilerplate. Here is exactly what it takes off your plate.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-opens-and-closes-connections-automatically&quot;&gt;1. Opens and closes connections automatically&lt;/h3&gt;
&lt;p&gt;Without the fixture, every test file that needs a database connection looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;let client: SqlClient;
beforeEach(async () =&amp;gt; { client = await createConnection(); });
afterEach(async () =&amp;gt; { await client.close(); });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the fixture, it is one line:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;test.use({ sqlAdapter: sqliteAdapter(':memory:') });

test('my test', async ({ sql: db }) =&amp;gt; {
  // db is ready — no setup needed
  // db.close() is called by the fixture after this function, even if it throws
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The connection lifecycle is entirely handled by Playwright's fixture system.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-prevents-connection-leaks-when-tests-fail&quot;&gt;2. Prevents connection leaks when tests fail&lt;/h3&gt;
&lt;p&gt;When a test throws an error, code in &lt;code&gt;afterEach&lt;/code&gt; might not run correctly depending on how the error propagates. Fixtures are different — Playwright guarantees their teardown phase runs regardless of test outcome.&lt;/p&gt;
&lt;p&gt;Every connection opened through &lt;code&gt;sql&lt;/code&gt; or &lt;code&gt;useSql&lt;/code&gt; is tracked and closed in teardown. You cannot leak a connection through a failing test.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-catches-sql-errors-at-compile-time&quot;&gt;3. Catches SQL errors at compile time&lt;/h3&gt;
&lt;p&gt;The fixture integrates with &lt;code&gt;@playwright-labs/sql-core&lt;/code&gt;'s compile-time SQL validator. Write your queries with &lt;code&gt;sql(&quot;…&quot;)&lt;/code&gt; instead of a raw string:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// ❌ runtime error
await db.query('SELECT * WHERE id = ?', [1]);

// ✅ compile error — TypeScript catches missing FROM before tests run
await db.query(sql(&quot;SELECT * WHERE id = ?&quot;), [1]);
//             ~~~~~~~~~~~~~~~~~~~~~~~~~~~ never
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The type system models SQL grammar as a finite-state machine. Invalid structure → &lt;code&gt;never&lt;/code&gt;. Wrong number of params → compile error.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const q = sql(&quot;SELECT name FROM users WHERE id = ?&quot;);
// typeof q → SqlStatement&amp;lt;[unknown]&amp;gt;

await db.query(q, [1]);        // ✅
await db.query(q);             // ❌ params required
await db.query(q, [1, 2, 3]); // ❌ too many params
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-handles-multiple-connections-with-one-call&quot;&gt;4. Handles multiple connections with one call&lt;/h3&gt;
&lt;p&gt;Need to test replication, or talk to two databases in one test? &lt;code&gt;useSql&lt;/code&gt; opens additional connections and registers them all for teardown:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;test('primary → replica', async ({ useSql }) =&amp;gt; {
  const primary = await useSql(pgAdapter(process.env.PRIMARY_URL!));
  const replica  = await useSql(pgAdapter(process.env.REPLICA_URL!));

  await primary.execute(sql(&quot;INSERT INTO events (type) VALUES ($1)&quot;), ['login']);

  const { rows } = await replica.query(sql(&quot;SELECT * FROM events WHERE type = $1&quot;), ['login']);
  expect(rows).toHaveLength(1);
});
// both connections closed automatically
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No manual cleanup. No try/finally.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-works-with-postgresql-mysql-and-sqlite-out-of-the-box&quot;&gt;5. Works with PostgreSQL, MySQL, and SQLite out of the box&lt;/h3&gt;
&lt;p&gt;Same API, three drivers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// SQLite — in-memory, zero infrastructure
import { sqliteAdapter } from '@playwright-labs/fixture-sql/sqlite';
test.use({ sqlAdapter: sqliteAdapter(':memory:') });

// PostgreSQL
import { pgAdapter } from '@playwright-labs/fixture-sql/pg';
test.use({ sqlAdapter: pgAdapter(process.env.DATABASE_URL!) });

// MySQL / MariaDB
import { mysqlAdapter } from '@playwright-labs/fixture-sql/mysql';
test.use({ sqlAdapter: mysqlAdapter(process.env.MYSQL_URL!) });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Drivers are optional peer dependencies — install only the one you use.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;bonus-editor-autocomplete&quot;&gt;Bonus: editor autocomplete&lt;/h3&gt;
&lt;p&gt;Pair with &lt;code&gt;@playwright-labs/ts-plugin-sql&lt;/code&gt; and add a schema file to get table/column autocomplete and diagnostics inside &lt;code&gt;sql&lt;/code&gt; templates directly in your editor.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# generate schema types from your live DB
pnpm playwright-labs-sql-core pull --adapter pg --url postgresql://localhost/mydb --out ./src/db-types.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// tsconfig.json
{
  &quot;compilerOptions&quot;: {
    &quot;plugins&quot;: [{ &quot;name&quot;: &quot;@playwright-labs/ts-plugin-sql&quot;, &quot;schemaFile&quot;: &quot;./src/db-types.ts&quot; }]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;get-started&quot;&gt;Get started&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pnpm add -D @playwright-labs/fixture-sql
pnpm add -D better-sqlite3   # or pg / mysql2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Source: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/vitalics/playwright-labs&quot;&gt;github.com/vitalics/playwright-labs&lt;/a&gt; — MIT license.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14599/5-things-this-playwright-sql-fixture-does-so-you-dont-have-to</guid>
<pubDate>Mon, 13 Apr 2026 07:53:00 +0000</pubDate>
</item>
<item>
<title>Design Data Model for Multi-Tenant RBAC</title>
<link>https://coderlegion.com/14636/design-data-model-for-multi-tenant-rbac</link>
<description>&lt;h2 id=&quot;part-1-identity-foundation&quot;&gt;Part 1: Identity Foundation&lt;/h2&gt;
&lt;h3 id=&quot;behavior-description&quot;&gt;Behavior Description&lt;/h3&gt;
&lt;p&gt;Assume we are building a KPI management platform. Multiple companies can create accounts (tenants) and let their employees access the platform.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A user can be assigned to one or several roles. Their permissions are the sum of those roles' permissions.&lt;/li&gt;
&lt;li&gt;A user cannot access features outside their permissions. Even if they know the API specs and bypass the UI, they will still be blocked at the API layer.&lt;/li&gt;
&lt;li&gt;Admins can change a user's roles or a role's permissions at any time. The only inconvenience is that the affected user needs to log out and log back in for the changes to take effect.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sounds straightforward, right? Let's look at what happens under the hood.&lt;/p&gt;
&lt;h3 id=&quot;deriving-the-data-model-from-use-cases&quot;&gt;Deriving the Data Model from Use Cases&lt;/h3&gt;
&lt;p&gt;Before implementing, I take time to think about what the system needs to do and what data structures are required to support those use cases.&lt;/p&gt;
&lt;p&gt;The first thing we hear in the spec is &quot;multiple tenants.&quot; A system that serves multiple companies and their users. That gives us at least two objects to start with: &lt;code&gt;tenants&lt;/code&gt; and &lt;code&gt;users&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://schemity.com/images/blog/tenants-and-users-entity.png&quot; alt=&quot;Tenants and users entity&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;iframe width=&quot;100%&quot; style=&quot;aspect-ratio: 16 / 9;&quot; src=&quot;https://www.youtube.com/embed/m9pjE7FtMJc&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;allowfullscreen&quot;&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Note: I use the plural convention for table names throughout this post.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;defining-the-relationship-between-tenants-and-users&quot;&gt;Defining the Relationship Between Tenants and Users&lt;/h3&gt;
&lt;p&gt;Now we have the foundation, but the relationship between &lt;code&gt;tenants&lt;/code&gt; and &lt;code&gt;users&lt;/code&gt; is not yet defined. To define it, we need to find the right constraint. There are two cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Case 1:&lt;/strong&gt; One tenant can have many users. One user must use a unique email per tenant.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Case 2:&lt;/strong&gt; One tenant can have many users. One user can use the same email across different tenants.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Consider this scenario: you are a contractor working for two different companies that both use this platform. If we go with Case 1, the best you can do is maintain two separate emails for two separate tenants on the same system. That is a real inconvenience.&lt;/p&gt;
&lt;p&gt;Notice I used the word &lt;strong&gt;person&lt;/strong&gt; instead of &lt;strong&gt;user&lt;/strong&gt; here. Technically, every tenant is independent. There is no good reason to force a person to use a different email just to become a user of another tenant.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Case 2 is the preferred design.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;tenant-identification-at-login&quot;&gt;Tenant Identification at Login&lt;/h3&gt;
&lt;p&gt;This raises a natural question: if a user can log in with the same email across multiple tenants, how do we know which tenant they belong to?&lt;/p&gt;
&lt;p&gt;The answer is that when a user logs in, we need to identify the tenant. There are a few approaches:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ask the user to type a tenant code in the login form. Bad idea.&lt;/li&gt;
&lt;li&gt;Provide a typeahead field to help the user find their tenant code. Still a bad idea.&lt;/li&gt;
&lt;li&gt;Give each tenant a subdomain or sub-path, such as &lt;code&gt;tenant1.ourplatform.com&lt;/code&gt; or &lt;code&gt;ourplatform.com/tenant1&lt;/code&gt;. This is the widely accepted approach, especially the subdomain variant. The only tradeoff is that instead of a standard SSL certificate, you need a wildcard SSL certificate.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-identity-table&quot;&gt;The Identity Table&lt;/h3&gt;
&lt;p&gt;Another natural question follows: if users share an email across tenants, can they also share a password?&lt;/p&gt;
&lt;p&gt;No. Password policies can differ per tenant, so sharing passwords across tenants is not acceptable. This leads us to a third object: the &lt;code&gt;identities&lt;/code&gt; table. An identity represents a user's membership in a specific tenant, containing tenant's specific information.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://schemity.com/images/blog/identities-entity.png&quot; alt=&quot;Identities entity&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;iframe width=&quot;100%&quot; style=&quot;aspect-ratio: 16 / 9;&quot; src=&quot;https://www.youtube.com/embed/v6y8P4V8y54&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;allowfullscreen&quot;&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;p&gt;The relationship is clear: one user can have many identities, and each identity belongs to exactly one tenant. With this simple three-table model - &lt;code&gt;tenants&lt;/code&gt;, &lt;code&gt;users&lt;/code&gt;, and &lt;code&gt;identities&lt;/code&gt; - we satisfy the core constraint cleanly: a person can use the same email across multiple independent tenants.&lt;/p&gt;
&lt;h3 id=&quot;recognizing-the-many-to-many-relationship&quot;&gt;Recognizing the Many-to-Many Relationship&lt;/h3&gt;
&lt;p&gt;Look closely at the diagram. Both the N (of 1:N) ends from &lt;code&gt;users&lt;/code&gt; and &lt;code&gt;tenants&lt;/code&gt; point to the &lt;code&gt;identities&lt;/code&gt; table. This is a sign that &lt;code&gt;users&lt;/code&gt; and &lt;code&gt;tenants&lt;/code&gt; have an N:N relationship - a user can belong to multiple tenants, and a tenant can have multiple users. The &lt;code&gt;identities&lt;/code&gt; table is effectively the join table.&lt;/p&gt;
&lt;p&gt;With that insight, we can express the data model more precisely:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://schemity.com/images/blog/identities-as-nn.png&quot; alt=&quot;Identities entity&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;iframe width=&quot;100%&quot; style=&quot;aspect-ratio: 16 / 9;&quot; src=&quot;https://www.youtube.com/embed/rLdnE6_MKZ4&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;allowfullscreen&quot;&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;In Part 2, we will build on this foundation and design the roles and permissions model.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14636/design-data-model-for-multi-tenant-rbac</guid>
<pubDate>Mon, 13 Apr 2026 04:29:26 +0000</pubDate>
</item>
<item>
<title>How to Handle NULLs in Snowflake: A Complete Guide!</title>
<link>https://coderlegion.com/14166/how-to-handle-nulls-in-snowflake-a-complete-guide</link>
<description>&lt;p&gt;&lt;em&gt;Dear Readers,&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;NULL in data is like a weed, it can mess up your analysis and calculations, and ultimately bleed the value out of your analytics work. In this article, we’ll dive into what NULL is, why it exists, when it appears, and how to deal with it in Snowflake ?&lt;/p&gt;
&lt;p&gt;So, without further ado, let’s meet this culprit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What NULL?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In SQL, NULL is basically the database equivalent of a shrug.It is a special marker that represents a missing or unknown value. It's not the same as an empty string ('') or the number zero (0). It simply means “I don’t know” or “there’s no value here at all.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why NULL Exists?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;NULL is essential because sometimes your data just doesn’t exist yet, or never will. NULL allows the database to acknowledge the gap without faking a value.&lt;/p&gt;
&lt;p&gt;Eg:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A customer doesn’t have a middle name.&lt;/li&gt;
&lt;li&gt;A product doesn’t have a discount yet.&lt;/li&gt;
&lt;li&gt;A delivery date is unknown at the time of record insertion.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;When NULL Appears?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;NULL occurs anytime there is a lack of value from the user. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Common reasons include:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Optional fields with no provided value.&lt;/li&gt;
&lt;li&gt;Data not yet available (future delivery dates, pending approvals)&lt;/li&gt;
&lt;li&gt;Inapplicable values (someone’s “date of divorce” who never married)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;How to Deal with NULL in Snowflake?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Snowflake provides multiple functions and methods to deal with NULL dataset aka unknown data similar to other databases. I have listed some of them below:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use IS NULL / IS NOT NULL to identify the NULL data.&lt;/li&gt;
&lt;li&gt;Use COALESCE(col, default) or IFNULL(col, default) to replace them in select , joins, aggregates etc&lt;/li&gt;
&lt;li&gt;Use explicit filter on WHERE clauses to include NULL rows as it can can drop NULL rows unexpectedly.&lt;/li&gt;
&lt;li&gt;Use NVL, CASE WHEN, or similar functions to handle them in calculations or strings.&lt;/li&gt;
&lt;li&gt;Comparisons with NULL return UNKNOWN in SQL’s three-valued logic. Use COALESCE or IS NULL checks to avoid surprises.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Eg:&lt;/p&gt;
&lt;p&gt;==Using NULL/NOT NULL &lt;/p&gt;
&lt;p&gt;-- Finds all employees without a manager&lt;br&gt;
SELECT name FROM employees WHERE manager_id IS NULL;&lt;/p&gt;
&lt;p&gt;-- Finds all employees with a manager&lt;br&gt;
SELECT name FROM employees WHERE manager_id IS NOT NULL;&lt;/p&gt;
&lt;p&gt;==Using COALESCE&lt;/p&gt;
&lt;p&gt;-- Calculates total compensation, treating a NULL bonus as 0&lt;/p&gt;
&lt;p&gt;SELECT salary + COALESCE(bonus, 0) AS total_compensation FROM employees&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Trouble NULL Causes:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Comparison Traps&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;NULL comparisons are a common pitfall because standard operators like = or != don’t work as expected. Any comparison involving NULL returns UNKNOWN, not TRUE or FALSE. Use IS NULL or IS NOT NULL to handle the traps.&lt;/p&gt;
&lt;p&gt;Eg:&lt;/p&gt;
&lt;p&gt;--Evaluates to UNKNOWN when &lt;code&gt;NULL = NULL&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;SELECT *&lt;br&gt;
FROM employees&lt;br&gt;
WHERE manager_id = NULL;&lt;/p&gt;
&lt;p&gt;-- The correct way to find employees with a NULL manager_id is:&lt;br&gt;
SELECT *&lt;br&gt;
FROM employees&lt;br&gt;
WHERE manager_id IS NULL;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Math &amp;amp; Concatenation Failures:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;NULL acts like a virus in arithmetic and string concatenation operations. Any operation involving NULL will result in NULL.&lt;/p&gt;
&lt;p&gt;Eg:&lt;/p&gt;
&lt;p&gt;--Return of NULL total+compensation when bonus is NULL:&lt;/p&gt;
&lt;p&gt;SELECT salary + bonus AS total_compensation&lt;br&gt;
FROM employees;&lt;/p&gt;
&lt;p&gt;--The correct way to add bonus is use COALESCE :&lt;/p&gt;
&lt;p&gt;SELECT salary + COALESCE(bonus, 0) AS total_compensation&lt;br&gt;
FROM employees;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Aggregate Surprises:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Aggregate functions like SUM() and AVG() ignore NULL values. This can lead to misleading results if you’re not aware of it.&lt;/p&gt;
&lt;p&gt;Eg:&lt;/p&gt;
&lt;p&gt;--Excludes NULL rows &lt;/p&gt;
&lt;p&gt;SELECT AVG(salary) AS avg_salary, SUM(salary) AS total_salary&lt;br&gt;
FROM employees;&lt;/p&gt;
&lt;p&gt;--Use COALESCE to treat NULL salaries as 0 to get more accurate company-wide average&lt;/p&gt;
&lt;p&gt;SELECT AVG(COALESCE(salary, 0)) FROM employees;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Join Issues:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;NULL values in join keys never match each other. If an employee has a NULL in manager_id, that row won’t link to any id in the employees table even if other employees also have NULL as manager_id.&lt;/p&gt;
&lt;p&gt;Use a LEFT JOIN + COALESCE to tackle the NULL issue.In following demonstrated way, you keep all employees in the output, and NULL managers are clearly labeled without risking false matches.&lt;/p&gt;
&lt;p&gt;Eg:&lt;/p&gt;
&lt;p&gt;--This INNER JOIN excludes employees with no manager_id&lt;/p&gt;
&lt;p&gt;SELECT &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   E.name AS employee,
   M.name AS manager
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FROM employees AS E&lt;br&gt;
INNER JOIN &lt;br&gt;
employees AS M &lt;br&gt;
ON E.manager_id = M.id;&lt;/p&gt;
&lt;p&gt;--Use COALESCE to include employees without a manager&lt;/p&gt;
&lt;p&gt;SELECT &lt;br&gt;
  E.name AS employee,&lt;br&gt;
  COALESCE(M.name, 'No Manager') AS manager&lt;br&gt;
FROM employees E&lt;br&gt;
LEFT JOIN employees M &lt;br&gt;
  ON E.manager_id = M.id;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5.Logic Surprises&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In SQL’s three-valued logic (TRUE, FALSE, UNKNOWN), an UNKNOWN result in a WHERE clause acts like FALSE and discards the row. However, UNKNOWN can have different effects in other clauses such as CASE.&lt;/p&gt;
&lt;p&gt;Eg:&lt;/p&gt;
&lt;p&gt;--Logic Mishandling for NULL data.&lt;/p&gt;
&lt;p&gt;SELECT&lt;br&gt;
  CASE&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WHEN bonus &amp;gt; 10000 THEN 'High Bonus'
WHEN bonus &amp;gt; 5000 THEN 'Mid Bonus'
ELSE 'Low or No Bonus'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  END AS bonus_category&lt;br&gt;
FROM employees;&lt;/p&gt;
&lt;p&gt;-- Explicitly checks for NULL first&lt;/p&gt;
&lt;p&gt;SELECT&lt;br&gt;
  CASE&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WHEN bonus IS NULL THEN 'No Bonus'
WHEN bonus &amp;gt; 10000 THEN 'High Bonus'
WHEN bonus &amp;gt; 5000 THEN 'Mid Bonus'
ELSE 'Low Bonus'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  END AS bonus_category&lt;br&gt;
FROM employees;&lt;br&gt;
Closing thoughts,&lt;/p&gt;
&lt;p&gt;NULLs aren’t a bug, they’re part of SQL’s design.NULL is tricky. The safest approach is to scan your dataset for every attribute that could be NULL, then apply handling strategies suited to your business context. In Snowflake, a few well-placed COALESCE, IS NULL, and CASE statements can save you from major data quality headaches.&lt;/p&gt;
&lt;p&gt;Hope you enjoyed the article! If you like my writing, feel free to connect with me on Linkedin and follow for more such interesting reads.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14166/how-to-handle-nulls-in-snowflake-a-complete-guide</guid>
<pubDate>Tue, 07 Apr 2026 14:39:38 +0000</pubDate>
</item>
<item>
<title>The Data Engineer’s Sandbox: Architecting for Autonomy with LXD</title>
<link>https://coderlegion.com/13481/the-data-engineers-sandbox-architecting-for-autonomy-with-lxd</link>
<description>&lt;p&gt;As Data Engineers, we often live in the cloud. Our daily lives are governed by S3 buckets, Glue jobs, and Kafka clusters. But there is a silent pressure that every senior professional feels: the cost of experimentation.&lt;/p&gt;
&lt;p&gt;How many times have you hesitated to spin up a resource just to validate a simple schema or a transformation logic? We've all been there—balancing the need for agility with the responsibility of cost-awareness.&lt;/p&gt;
&lt;p&gt;I’ve always believed that autonomy is a superpower. True technical mastery comes when you can prototype &quot;blindly,&quot; without an internet connection, directly on your machine, and with zero impact on your company's AWS bill.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why I moved away from heavy VMs&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For years, the choice was either a heavy Virtual Machine (VM) that drained my laptop’s RAM or a Docker container that, while fast, sometimes lacked the persistence and system-level isolation I needed for a long-term practice environment.&lt;/p&gt;
&lt;p&gt;When I started working with Ubuntu 24.04, I found the perfect middle ground: LXD. It offers the &quot;system feel&quot; of a VM with the &quot;instant speed&quot; of a container. It became my go-to for building local SQL sandboxes where I can fail fast, learn deep, and spend zero.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Blueprint: Setting up your Local Engine&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The goal is simple: a functional database server running in minutes.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The Foundation&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We start by initializing the hypervisor to manage our internal networking.&lt;/p&gt;
&lt;p&gt;sudo snap install lxd&lt;br&gt;
sudo lxd init --auto&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Launching the Instance&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We choose a lightweight Ubuntu image as our dedicated server.&lt;/p&gt;
&lt;p&gt;lxc launch ubuntu:24.04 sql-practice&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &quot;Hardware&quot; Setup&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We enter the container and transform that empty shell into a robust PostgreSQL engine.&lt;/p&gt;
&lt;p&gt;lxc exec sql-practice -- bash&lt;br&gt;
apt update &amp;amp;&amp;amp; apt install -y postgresql postgresql-contrib&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;From Infrastructure to Impact&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Setting up the &quot;pipes&quot; is only half the battle. As engineers, we care about the data. In my experience building event-driven architectures and streaming pipelines with Kafka, I’ve learned that a local sink is the best place to test idempotency and schema evolution before touching a production environment.&lt;/p&gt;
&lt;p&gt;Using the COPY command for massive local ingestion or pushing CSV files directly into the container (lxc file push) are the types of &quot;unsexy&quot; but highly efficient workflows that define a professional.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Closing thoughts: Design first&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;My philosophy is simple: Systems must be resilient and observable from day one. That includes our local dev environments. Complexity is a cost, and simplicity scales better.&lt;/p&gt;
&lt;p&gt;By mastering these local sandboxes, we aren't just saving a few dollars on an AWS bill; we are reclaiming our creative freedom as builders.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;About the Author&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I am Rocío Baigorria, a Senior Data Engineer and co-leader of AWS Girls Argentina. I specialize in building &quot;institutional-grade&quot; data platforms where reliability, cost-efficiency, and business impact are core design principles.&lt;/p&gt;
&lt;p&gt;Connect with me on LinkedIn: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/rociobaigorria/&quot;&gt;https://www.linkedin.com/in/rociobaigorria/&lt;/a&gt; &lt;/p&gt;
&lt;p&gt;Explore my Resources: &lt;a rel=&quot;nofollow&quot; href=&quot;https://tr.ee/2t-m587551&quot;&gt;https://tr.ee/2t-m587551&lt;/a&gt; &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13481/the-data-engineers-sandbox-architecting-for-autonomy-with-lxd</guid>
<pubDate>Wed, 25 Mar 2026 14:21:54 +0000</pubDate>
</item>
<item>
<title>Getting Started with PostgreSQL: Best Practices from Development to Production</title>
<link>https://coderlegion.com/12899/getting-started-with-postgresql-best-practices-from-development-to-production</link>
<description>&lt;p&gt;Databases are the backbone of modern applications. Whether you're building APIs, SaaS platforms, or enterprise systems, choosing the right database and using it correctly can make a huge difference in performance and scalability.&lt;/p&gt;
&lt;p&gt;One of the most reliable and powerful databases available today is &lt;strong&gt;PostgreSQL&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;PostgreSQL is known for its &lt;strong&gt;stability, extensibility, performance, and strong SQL compliance&lt;/strong&gt;, which is why many modern systems rely on it for production workloads.&lt;/p&gt;
&lt;p&gt;In this guide, we’ll explore how to &lt;strong&gt;get started with PostgreSQL and follow best practices from development all the way to production environments&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-choose-postgresql-&quot;&gt;Why Choose PostgreSQL?&lt;/h2&gt;
&lt;p&gt;PostgreSQL has become a favorite among developers and companies because it offers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Advanced SQL support&lt;/li&gt;
&lt;li&gt;Strong data integrity&lt;/li&gt;
&lt;li&gt;JSON support for hybrid workloads&lt;/li&gt;
&lt;li&gt;Extensibility through extensions&lt;/li&gt;
&lt;li&gt;Excellent performance for large-scale systems&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Many major platforms rely on PostgreSQL, including &lt;strong&gt;Netflix&lt;/strong&gt;, &lt;strong&gt;Apple&lt;/strong&gt;, and &lt;strong&gt;Instagram&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;It works well for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;web applications&lt;/li&gt;
&lt;li&gt;analytics workloads&lt;/li&gt;
&lt;li&gt;enterprise systems&lt;/li&gt;
&lt;li&gt;microservices architectures&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;installing-postgresql&quot;&gt;Installing PostgreSQL&lt;/h2&gt;
&lt;p&gt;You can install PostgreSQL locally using the official installer or through a package manager.&lt;/p&gt;
&lt;p&gt;Typical installation steps:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# macOS (Homebrew)
brew install postgresql

# Ubuntu
sudo apt install postgresql

# Windows
Download installer from postgresql.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After installation, start the database service and connect using the CLI tool:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;psql -U postgres
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;basic-database-setup&quot;&gt;Basic Database Setup&lt;/h2&gt;
&lt;p&gt;A typical project setup includes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE DATABASE myapp;

CREATE USER myapp_user WITH PASSWORD 'securepassword';

GRANT ALL PRIVILEGES ON DATABASE myapp TO myapp_user;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This separation between &lt;strong&gt;database owner and application user&lt;/strong&gt; is an important security best practice.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;designing-tables-properly&quot;&gt;Designing Tables Properly&lt;/h2&gt;
&lt;p&gt;Good database design is essential for long-term scalability.&lt;/p&gt;
&lt;p&gt;Example table:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email TEXT UNIQUE NOT NULL,
    name TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Important principles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;always define a &lt;strong&gt;primary key&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;enforce &lt;strong&gt;unique constraints where necessary&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;avoid nullable fields unless truly optional&lt;/li&gt;
&lt;li&gt;use timestamps for auditing&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;use-indexes-for-performance&quot;&gt;Use Indexes for Performance&lt;/h2&gt;
&lt;p&gt;Indexes help speed up queries significantly.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE INDEX idx_users_email ON users(email);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Indexes are useful for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;search queries&lt;/li&gt;
&lt;li&gt;filtering large datasets&lt;/li&gt;
&lt;li&gt;join operations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, too many indexes can slow down writes, so use them strategically.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;use-migrations-instead-of-manual-changes&quot;&gt;Use Migrations Instead of Manual Changes&lt;/h2&gt;
&lt;p&gt;Schema changes should always be version-controlled.&lt;/p&gt;
&lt;p&gt;Popular migration tools include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Prisma&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeORM&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flyway&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example migration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;ALTER TABLE users ADD COLUMN last_login TIMESTAMP;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures that all environments stay &lt;strong&gt;consistent&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;use-transactions-for-data-safety&quot;&gt;Use Transactions for Data Safety&lt;/h2&gt;
&lt;p&gt;Transactions ensure that multiple operations succeed or fail together.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;BEGIN;

INSERT INTO orders (...) VALUES (...);
UPDATE users SET balance = balance - 100 WHERE id = 1;

COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If something fails:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;ROLLBACK;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Transactions prevent &lt;strong&gt;partial updates and inconsistent data&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;use-connection-pooling&quot;&gt;Use Connection Pooling&lt;/h2&gt;
&lt;p&gt;Opening a database connection for every request is expensive.&lt;/p&gt;
&lt;p&gt;Connection pooling improves performance by reusing connections.&lt;/p&gt;
&lt;p&gt;Popular tools include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PgBouncer&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pg-pool&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Connection pooling becomes essential in &lt;strong&gt;high traffic production systems&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;logging-and-monitoring&quot;&gt;Logging and Monitoring&lt;/h2&gt;
&lt;p&gt;Monitoring your database helps detect issues early.&lt;/p&gt;
&lt;p&gt;PostgreSQL provides built-in logging capabilities.&lt;/p&gt;
&lt;p&gt;You can monitor:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;slow queries&lt;/li&gt;
&lt;li&gt;long-running transactions&lt;/li&gt;
&lt;li&gt;connection usage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tools commonly used with PostgreSQL include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;pgAdmin&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Grafana&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Monitoring is crucial for production reliability.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;backup-strategy-for-production&quot;&gt;Backup Strategy for Production&lt;/h2&gt;
&lt;p&gt;Always have automated backups.&lt;/p&gt;
&lt;p&gt;Typical strategies include:&lt;/p&gt;
&lt;p&gt;Daily backups:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pg_dump myapp &amp;gt; backup.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Full database backups:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pg_dumpall &amp;gt; full_backup.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For production systems, consider:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;automated cloud backups&lt;/li&gt;
&lt;li&gt;replication clusters&lt;/li&gt;
&lt;li&gt;point-in-time recovery&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This protects your data from accidental loss.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;scaling-postgresql-in-production&quot;&gt;Scaling PostgreSQL in Production&lt;/h2&gt;
&lt;p&gt;As your application grows, scaling becomes important.&lt;/p&gt;
&lt;p&gt;Common scaling strategies include:&lt;/p&gt;
&lt;h4 id=&quot;read-replicas&quot;&gt;Read Replicas&lt;/h4&gt;
&lt;p&gt;Separate read queries from write operations.&lt;/p&gt;
&lt;p&gt;Example architecture:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Primary DB → Write operations
Replica DB → Read queries
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This improves performance under heavy load.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;partitioning-large-tables&quot;&gt;Partitioning Large Tables&lt;/h4&gt;
&lt;p&gt;When tables become very large, partitioning improves performance.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE logs_2025 PARTITION OF logs
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Partitioning helps with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;faster queries&lt;/li&gt;
&lt;li&gt;easier data management&lt;/li&gt;
&lt;li&gt;improved maintenance&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;security-best-practices&quot;&gt;Security Best Practices&lt;/h2&gt;
&lt;p&gt;Production databases must be secured properly.&lt;/p&gt;
&lt;p&gt;Important steps include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;enforce strong passwords&lt;/li&gt;
&lt;li&gt;restrict database network access&lt;/li&gt;
&lt;li&gt;use SSL connections&lt;/li&gt;
&lt;li&gt;avoid superuser access for applications&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Principle of &lt;strong&gt;least privilege&lt;/strong&gt; is essential.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;development-vs-production-setup&quot;&gt;Development vs Production Setup&lt;/h2&gt;
&lt;p&gt;Development environments can be simple, but production requires more structure.&lt;/p&gt;
&lt;p&gt;Development setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;local PostgreSQL instance&lt;/li&gt;
&lt;li&gt;minimal security configuration&lt;/li&gt;
&lt;li&gt;small datasets&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Production setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;managed PostgreSQL service&lt;/li&gt;
&lt;li&gt;automated backups&lt;/li&gt;
&lt;li&gt;monitoring and alerts&lt;/li&gt;
&lt;li&gt;connection pooling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Many companies use managed services like &lt;strong&gt;Amazon RDS&lt;/strong&gt; or &lt;strong&gt;Azure Database for PostgreSQL&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thoughts-3&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;PostgreSQL is one of the most &lt;strong&gt;powerful and reliable databases available today&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;But simply installing it is not enough. Building scalable systems requires:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;proper schema design&lt;/li&gt;
&lt;li&gt;indexing strategies&lt;/li&gt;
&lt;li&gt;migration workflows&lt;/li&gt;
&lt;li&gt;monitoring and backups&lt;/li&gt;
&lt;li&gt;production-grade security&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When used correctly, PostgreSQL can power applications from &lt;strong&gt;small startups to massive enterprise platforms&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If you're building modern applications today, PostgreSQL is definitely a database worth mastering.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.enterprisedb.com/downloads/postgres-postgresql-downloads&quot;&gt;https://www.enterprisedb.com/downloads/postgres-postgresql-downloads&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12899/getting-started-with-postgresql-best-practices-from-development-to-production</guid>
<pubDate>Sun, 15 Mar 2026 17:40:28 +0000</pubDate>
</item>
<item>
<title>Gnoke Dataforge</title>
<link>https://coderlegion.com/12856/gnoke-dataforge</link>
<description>&lt;h3 id=&quot;the-community&quot;&gt;The Community&lt;/h3&gt;
&lt;p&gt;Vibe developers — people who ship fast with AI assistants like Claude, Cursor, or ChatGPT.&lt;/p&gt;
&lt;p&gt;One thing I've learned after years of building: when you hand an AI assistant a well-structured schema upfront, it hallucinates less, asks fewer clarifying questions, and produces far better code. But there was no lightweight, offline-first tool to quickly model a table and export it in AI-ready formats. So I built one.&lt;/p&gt;
&lt;h3 id=&quot;what-i-built&quot;&gt;What I Built&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Gnoke DataForge&lt;/strong&gt; — a browser-based relational schema modeler that runs 100% offline.&lt;/p&gt;
&lt;p&gt;You open it, design your tables visually, define column types and constraints (PK, NOT NULL, UNIQUE), and export in formats your AI assistant can immediately use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.json&lt;/code&gt; — flat data format&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.entity.json&lt;/code&gt; — structured entity format ideal for pasting into AI prompts&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.sql&lt;/code&gt; — CREATE TABLE + INSERT statements&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.sqlite&lt;/code&gt; — a real binary SQLite database file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.csv&lt;/code&gt; — universal fallback&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Projects are saved locally via IndexedDB. No account. No server. No telemetry.&lt;/p&gt;
&lt;h3 id=&quot;demo&quot;&gt;Demo&lt;/h3&gt;
&lt;p&gt;???? &lt;strong&gt;Live:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://edmundsparrow.github.io/gnoke-dataforge&quot;&gt;https://edmundsparrow.github.io/gnoke-dataforge&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Open it, edit the demo table, switch to SQLite mode to define types and constraints, then hit Export — all without leaving the browser.&lt;/p&gt;
&lt;h3 id=&quot;code&quot;&gt;Code&lt;/h3&gt;
&lt;p&gt;{% embed &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/edmundsparrow/gnoke-dataforge&quot;&gt;https://github.com/edmundsparrow/gnoke-dataforge&lt;/a&gt; %}&lt;/p&gt;
&lt;h3 id=&quot;how-i-built-it&quot;&gt;How I Built It&lt;/h3&gt;
&lt;p&gt;The entire app is vanilla HTML, CSS, and JavaScript — no framework, no build step.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SQLite in the browser&lt;/strong&gt; via &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/sql-js/sql-js&quot;&gt;sql.js&lt;/a&gt; (WebAssembly) — handles &lt;code&gt;.sqlite&lt;/code&gt; export and import&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IndexedDB&lt;/strong&gt; for local project persistence — each saved table is stored as a real SQLite binary blob&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A custom JSON normalizer&lt;/strong&gt; that accepts multiple JSON shapes (array of objects, entity format, flat arrays) so importing from AI-generated output just works&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Light/dark theme&lt;/strong&gt;, mobile drawer, and a skip-intro toggle for repeat visits&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The design prioritizes speed: open → build → export → paste into AI. That's the whole loop.&lt;/p&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/12856/gnoke-dataforge</guid>
<pubDate>Sat, 14 Mar 2026 06:32:54 +0000</pubDate>
</item>
<item>
<title>Beyond the Basics: 5 Game-Changing Secrets of SQL Joins and Window Functions</title>
<link>https://coderlegion.com/12308/beyond-the-basics-5-game-changing-secrets-of-sql-joins-and-window-functions</link>
<description>&lt;h3 id=&quot;1-introduction-the-data-relationships-hook&quot;&gt;1. Introduction: The Data Relationships Hook&lt;/h3&gt;
&lt;p&gt;Think of a database as a digital filing cabinet. In this architecture, information is organized into drawers—or schemas—such as HR, Finance, or Sales. Within these drawers sit the documents, which represent our individual tables.&lt;/p&gt;
&lt;p&gt;While this structure ensures clean organization, real-world business intelligence rarely lives in a single drawer. To answer strategic questions, you must bridge the gaps between disparate tables without creating a &quot;data mess&quot;. Whether you are joining sales records with inventory levels or HR data with finance budgets, mastering these relationships is what separates a basic query-writer from a Senior Architect. This guide distills complex SQL operations into high-impact takeaways, focusing on maintaining relational integrity while scaling your analysis.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-the-horizontal-vs-vertical-divide-joins-vs-unions&quot;&gt;2. The Horizontal vs. Vertical Divide: Joins vs. Unions&lt;/h3&gt;
&lt;p&gt;The most fundamental distinction in data architecture is how you choose to expand your result set.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Joins&lt;/strong&gt; combine tables &lt;em&gt;horizontally&lt;/em&gt;. They add columns based on a shared key (like a Primary or Foreign Key), making the result set &quot;wider&quot;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unions&lt;/strong&gt; combine tables &lt;em&gt;vertically&lt;/em&gt;. They stack rows from one dataset on top of another, making the result set &quot;longer&quot;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a UNION to be architecturally sound, the query must meet three strict structural requirements: the columns must be in the same order, have the same count, and—most importantly—possess matching data types. As an architect, you must remember that the database engine is indifferent to your column aliases but ruthless regarding data types.&lt;/p&gt;
&lt;p&gt;&quot;The database doesn't care about naming but cares about data types in Unions... If a VARCHAR is matched with an INT, the database will throw a mismatch error.&quot; — &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.datawithbaraa.com/wiki/sql&quot;&gt;Data with Baraa&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Mental Model: Distinct vs. All&lt;/strong&gt; From a strategy perspective, think of UNION as a DISTINCT operation for stacked rows; it automatically removes duplicates. If you need the &quot;data as is&quot; without the overhead of deduplication, &lt;strong&gt;UNION ALL&lt;/strong&gt; is your preferred tool.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sample Query: Vertical Stacking&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;``-- Combining customer and employee names into a single master list&lt;br&gt;
SELECT first_name, last_name FROM customers&lt;br&gt;
UNION&lt;br&gt;
SELECT first_name, last_name FROM employees&lt;br&gt;
LIMIT 100;&lt;/p&gt;
&lt;h3 id=&quot;-&quot;&gt;``&lt;/h3&gt;
&lt;h3 id=&quot;3-the-workhorse-joins-why-left-is-often-better-than-right&quot;&gt;3. The &quot;Workhorse&quot; Joins: Why Left is Often Better than Right&lt;/h3&gt;
&lt;p&gt;In relational logic, the Inner Join is the default state. When you use the JOIN keyword, SQL is explicitly looking for matching values in the ON statement. If a record in the first table—for example, a customer who hasn't placed an order yet—has no match in the second table, that record &quot;disappears&quot; from the final result set.&lt;/p&gt;
&lt;p&gt;For high-level reporting where data integrity is paramount, the Left Join is the industry workhorse. It preserves every record from the left (first) table and fills missing data from the right table with NULL values. This ensures you don't accidentally drop crucial business entities (like customers or products) simply because they lack transaction history.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Architectural Insight&lt;/strong&gt;: The Right Join is functionally redundant. Any Right Join can be reframed as a Left Join by simply reordering the tables. In production environments, Left Joins are the standard because they align with a left-to-right reading flow, making queries significantly easier to audit and maintain.&lt;/p&gt;
&lt;p&gt;Sample Query: Production-Ready Left Join&lt;br&gt;
``-- Retrieving all customers and any associated event data&lt;br&gt;
SELECT&lt;br&gt;
  c.customer_id,&lt;br&gt;
  c.first_name,&lt;br&gt;
  e.event_name&lt;br&gt;
FROM customers AS c&lt;br&gt;
LEFT JOIN events AS e&lt;br&gt;
  ON c.customer_id = e.customer_id&lt;br&gt;
WHERE c.customer_id IS NOT NULL&lt;br&gt;
LIMIT 500;&lt;/p&gt;
&lt;h3 id=&quot;--1&quot;&gt;``&lt;/h3&gt;
&lt;h3 id=&quot;4-the-cartesian-chaos-understanding-the-cross-join&quot;&gt;4. The Cartesian Chaos: Understanding the Cross Join&lt;/h3&gt;
&lt;p&gt;A Cross Join represents the &quot;Cartesian Product&quot; of two tables. Unlike other joins, it ignores matching values entirely, attaching every row of the second table to every row of the first. If you join a 1,000-row table with a 500-row table, you will generate a massive result set of 500,000 rows.&lt;/p&gt;
&lt;p&gt;In standard relational reporting, Cross Joins are often avoided because they lack the primary/foreign key bond that defines logical relationships. Joining a car table and an item table by a shared color attribute &quot;wouldn't make any sense&quot; for data integrity. However, from a strategic standpoint, Cross Joins are powerful for generating permutations—such as creating a master grid of every possible product-color combination for an inventory audit.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-window-functions-grouping-without-the-collapse-&quot;&gt;5. Window Functions: Grouping Without the &quot;Collapse&quot;&lt;/h3&gt;
&lt;p&gt;The true &quot;game-changer&quot; for intermediate learners is the OVER() clause. Traditional GROUP BY operations &quot;collapse&quot; your data, rolling multiple rows into a single aggregate level. While useful for summaries, this loses the individual row detail.&lt;/p&gt;
&lt;p&gt;Window Functions allow you to perform calculations across a result set while keeping every unique row intact. This allows you to view an individual's salary right next to the department average, or calculate a Rolling Total.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Strategic Use Case&lt;/strong&gt;: The Rolling Total In finance and healthcare, tracking cumulative sums is vital. By adding an ORDER BY clause inside the OVER() window, you transform a static sum into a running balance.&lt;/p&gt;
&lt;p&gt;Sample Query: Rolling Totals and Averages&lt;br&gt;
&lt;code&gt;-- Calculating a cumulative salary sum and a static department average
SELECT
  first_name,
  gender,
  salary,
  SUM(salary) OVER(PARTITION BY gender ORDER BY employee_id) AS rolling_total,
  AVG(salary) OVER(PARTITION BY gender) AS avg_dept_salary
FROM employees;&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-the-ranking-trio-row-number-rank-and-dense-rank&quot;&gt;6. The Ranking Trio: Row Number, Rank, and Dense Rank&lt;/h3&gt;
&lt;p&gt;Sequencing data is a core requirement for leaderboards and performance tracking. SQL offers three nuances for handling ties within a window:&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;- Row Number&lt;/strong&gt;: Assigns a unique, sequential integer to every row (1, 2, 3, 4). Even in the event of a tie, the numbers will not repeat.&lt;br&gt;
**&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;- Rank&lt;/strong&gt;: Assigns the same number to ties but skips the next position based on the count of duplicates. If two rows tie for 1st, the next is 3rd (1, 1, 3). This is &quot;positional&quot; ranking.&lt;br&gt;
**- &lt;/p&gt;
&lt;p&gt; &lt;strong&gt;- Dense Rank&lt;/strong&gt;: Assigns the same number to ties but keeps the next number sequential (1, 1, 2). &lt;/p&gt;
&lt;p&gt;This is &quot;numerical&quot; ranking.&lt;br&gt;
Architectural Preference: DENSE_RANK is typically preferred for professional reporting. It ensures there are no &quot;gaps&quot; in your leaderboard, maintaining a clean hierarchy regardless of how many entities share the same value.7. &lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion-levelling-up-your-query-game&quot;&gt;Conclusion: Levelling Up Your Query Game&lt;/h3&gt;
&lt;p&gt;Moving from a beginner to an advanced analyst is less about learning syntax and more about understanding the logic of relationships. Every time you approach a new dataset, you must make a strategic choice: do you need to collapse your data into an aggregate summary with a Join or Group By, or do you need to partition it with a Window Function to maintain row-level detail?&lt;/p&gt;
&lt;p&gt;The absolute grounding rule of SQL is that it is a language of relationships. By mastering the nuances of how data stacks vertically via Unions or expands horizontally via Joins, you ensure the relational integrity of your architecture.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12308/beyond-the-basics-5-game-changing-secrets-of-sql-joins-and-window-functions</guid>
<pubDate>Mon, 02 Mar 2026 07:40:25 +0000</pubDate>
</item>
<item>
<title>Cool SQL Server Features You Can Use in Real World.</title>
<link>https://coderlegion.com/12233/cool-sql-server-features-you-can-use-in-real-world</link>
<description>&lt;p&gt;If you ever had to “fix a batch” of invoices, reset workflow statuses, or reprocess failed documents, you know the pain, one mistake and you either update too much, or you update the right rows but lose the evidence of what changed.&lt;/p&gt;
&lt;p&gt;SQL Server has a few features that make these scripts safer, easier to review, and much easier to debug later. Below is a clean demo that you can paste in SSMS and use in your daily work.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET NOCOUNT ON;
SET XACT_ABORT ON;

BEGIN TRY
    BEGIN TRAN;

    /* 1) Demo table (drop + create) */
    IF OBJECT_ID('dbo.DocumentRepositoryDemo', 'U') IS NOT NULL
        DROP TABLE dbo.DocumentRepositoryDemo;

    CREATE TABLE dbo.DocumentRepositoryDemo
    (
        DocumentRepositoryID bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_DocumentRepositoryDemo PRIMARY KEY,
        OriginalInvoiceID    varchar(50) NOT NULL,
        WorkflowStatus       int NOT NULL,
        HasResponse          bit NOT NULL,
        IsForwardedTP        bit NOT NULL,
        ModifiedAtUtc        datetime2(3) NULL,
        ModifiedBy           sysname NULL
    );

    /* 2) Seed some fake data */
    INSERT dbo.DocumentRepositoryDemo
        (OriginalInvoiceID, WorkflowStatus, HasResponse, IsForwardedTP, ModifiedAtUtc, ModifiedBy)
    VALUES
        ('7601002721400-318242', 2, 1, 1, SYSUTCDATETIME(), SUSER_SNAME()),
        ('7601002721400-318161', 3, 1, 0, SYSUTCDATETIME(), SUSER_SNAME()),
        ('7601001392038-318127', 4, 0, 1, SYSUTCDATETIME(), SUSER_SNAME()),
        ('7601000777447-967851', 9, 0, 0, SYSUTCDATETIME(), SUSER_SNAME()),
        ('7601000777447-968159', 1, 1, 1, SYSUTCDATETIME(), SUSER_SNAME());

    /* 3) Target list (table variable) */
    DECLARE @InvoiceIds TABLE
    (
        OriginalInvoiceID varchar(50) NOT NULL PRIMARY KEY
    );

    INSERT INTO @InvoiceIds (OriginalInvoiceID)
    VALUES
        ('7601002721400-318242'),
        ('7601002721400-318161'),
        ('7601000777447-968159');

    /* 4) Change tracking (audit table variable) */
    DECLARE @Changed TABLE
    (
        DocumentRepositoryID bigint,
        OriginalInvoiceID    varchar(50),
        OldWorkflowStatus    int,
        NewWorkflowStatus    int,
        OldHasResponse       bit,
        NewHasResponse       bit,
        OldIsForwardedTP     bit,
        NewIsForwardedTP     bit,
        ChangedAtUtc         datetime2(3),
        ChangedBy            sysname
    );

    /* 5) Update + capture before/after with OUTPUT */
    UPDATE dr
        SET dr.WorkflowStatus = 5,
            dr.HasResponse = 0,
            dr.IsForwardedTP = 0,
            dr.ModifiedAtUtc = SYSUTCDATETIME(),
            dr.ModifiedBy = SUSER_SNAME()
    OUTPUT
        deleted.DocumentRepositoryID,
        deleted.OriginalInvoiceID,
        deleted.WorkflowStatus, inserted.WorkflowStatus,
        deleted.HasResponse, inserted.HasResponse,
        deleted.IsForwardedTP, inserted.IsForwardedTP,
        inserted.ModifiedAtUtc,
        inserted.ModifiedBy
    INTO @Changed
    (
        DocumentRepositoryID,
        OriginalInvoiceID,
        OldWorkflowStatus, NewWorkflowStatus,
        OldHasResponse, NewHasResponse,
        OldIsForwardedTP, NewIsForwardedTP,
        ChangedAtUtc,
        ChangedBy
    )
    FROM dbo.DocumentRepositoryDemo dr
    INNER JOIN @InvoiceIds i
        ON i.OriginalInvoiceID = dr.OriginalInvoiceID
    WHERE dr.WorkflowStatus &amp;lt;&amp;gt; 5
       OR dr.HasResponse &amp;lt;&amp;gt; 0
       OR dr.IsForwardedTP &amp;lt;&amp;gt; 0;

    /* 6) Results */
    SELECT *
    FROM @Changed
    ORDER BY OriginalInvoiceID, DocumentRepositoryID;

    SELECT COUNT(*) AS RowsUpdated
    FROM @Changed;

    COMMIT TRAN
END TRY
BEGIN CATCH
    IF XACT_STATE() &amp;lt;&amp;gt; 0
        ROLLBACK TRAN
    THROW
END CATCH
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What makes this “SQL Server cool” (and why it matters)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;SQL Server normally prints messages like “(1 row affected)” after every statement. That is fine in ad hoc work, but noisy in scripts that do multiple steps.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;With SET NOCOUNT ON:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;result sets stay clean (only the output you intentionally select),&lt;/li&gt;
&lt;li&gt;less chatter between server and client,&lt;/li&gt;
&lt;li&gt;stored procedures often become easier to consume from apps and jobs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;SET XACT_ABORT ON&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;With XACT_ABORT ON, when a runtime error happens (conversion, constraint violation, etc.), SQL Server forces the transaction into a state where it cannot silently continue. In practice, this reduces the chance you end up with a transaction left open, or partially executed logic that you forget to undo.&lt;/p&gt;
&lt;p&gt;It pairs perfectly with TRY/CATCH because it pushes you toward “all or nothing”.&lt;/p&gt;
&lt;p&gt;3) TRY/CATCH + XACT_STATE(), predictable rollback logic&lt;/p&gt;
&lt;p&gt;XACT_STATE() helps because it tells you the transaction state:&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;1 there is an active, committable transaction&lt;br&gt;
-1 the transaction is uncommittable, you must rollback &lt;br&gt;
 0 no active transaction&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That is why this pattern is so common:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IF XACT_STATE() &amp;lt;&amp;gt; 0
    ROLLBACK TRAN;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4) Table variables as “scoped in memory tables”&lt;/p&gt;
&lt;p&gt;This script uses two table variables:&lt;/p&gt;
&lt;p&gt;@InvoiceIds is a safe target list&lt;/p&gt;
&lt;p&gt;@Changed is an audit collector&lt;/p&gt;
&lt;p&gt;Why that’s useful:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;you avoid long IN (...) lists inside the update,&lt;/li&gt;
&lt;li&gt;your target list can be validated and deduped (the PRIMARY KEY&lt;br&gt;
prevents duplicates),&lt;/li&gt;
&lt;li&gt;you keep the update set based, readable, and reviewable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Important detail for accuracy: table variables are often called “in memory”, but they can spill to tempdb depending on size and query plan. Still, they are great for small and medium sets.&lt;/p&gt;
&lt;p&gt;5) OUTPUT deleted, inserted, instant before/after audit&lt;/p&gt;
&lt;p&gt;This is one of the most underrated features for operational work.&lt;/p&gt;
&lt;p&gt;OUTPUT gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;deleted.* values before the update &lt;/li&gt;
&lt;li&gt;inserted.* values after the update&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;show exactly what changed, &lt;/li&gt;
&lt;li&gt;store it into @Changed, optionally persist&lt;/li&gt;
&lt;li&gt;it into a real audit table.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the difference between “I ran a script” and “I can prove exactly what the script did”.&lt;/p&gt;
&lt;p&gt;Takeaways : &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;NOCOUNT keeps scripts readable and reduces noise.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;XACT_ABORT makes failure behavior safer.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;TRY/CATCH plus XACT_STATE() gives you consistent rollback behavior.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Table variables make targeting and audit capture clean and scoped.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;OUTPUT gives you real auditing with almost no extra effort.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Avoiding unnecessary updates is a performance win and a safety win.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12233/cool-sql-server-features-you-can-use-in-real-world</guid>
<pubDate>Fri, 27 Feb 2026 08:17:57 +0000</pubDate>
</item>
<item>
<title>Corrupted‑to‑Gold</title>
<link>https://coderlegion.com/10836/corrupted-to-gold</link>
<description>&lt;p&gt;Corrupted‑to‑Gold: MDM Foundations&lt;/p&gt;
&lt;p&gt;This build takes intentionally corrupted CSV files and transforms them into clean, trusted, analytics‑ready Delta tables. The pipeline demonstrates real‑world data engineering constraints: inconsistent delimiters, malformed dates, broken encodings, duplicate entities, and multi‑format ingestion.&lt;/p&gt;
&lt;p&gt;The solution includes:&lt;/p&gt;
&lt;p&gt;✔ RAW → BRONZE ingestion&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Normalized delimiters&lt;pre&gt;&lt;code&gt;   Removed non‑ASCII noise
   Safe splitting logic
   Header detection
   Schema validation
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✔ SILVER cleansing&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Email, phone, country, and status normalization&lt;pre&gt;&lt;code&gt;   Multi‑format date parsing
   Null handling and type correction
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✔ Survivorship scoring (MDM‑light)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Email validity&lt;pre&gt;&lt;code&gt;  Phone completeness
  Date reliability
  Status correctness
  Row‑level scoring + best‑record selection
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✔ GOLD outputs&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Clean, deduped, resequenced customer entities&lt;pre&gt;&lt;code&gt;   Delta‑optimized storage
   Ready for downstream MDM and ADF Mapping Data Flows
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This project sets the foundation for the full MDM pipeline (matching, clustering, survivorship, and golden‑record creation) and the upcoming ADF MDF suite demonstrating schema drift, parameterization, joins, window functions, branching, SCD‑light, partitioning, and KPI generation.&lt;/p&gt;
&lt;p&gt;If you want the full code, architecture, or notebooks, the repo is here:&lt;/p&gt;
&lt;p&gt;github.com/usman19zafar/Project_corrupted-to-gold-data-journey&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/10836/corrupted-to-gold</guid>
<pubDate>Sat, 31 Jan 2026 17:35:50 +0000</pubDate>
</item>
<item>
<title>Is OpenAI really running ChatGPT on a single PostgreSQL instance?</title>
<link>https://coderlegion.com/10554/is-openai-really-running-chatgpt-on-a-single-postgresql-instance</link>
<description>&lt;p&gt;The headline of OpenAI’s recent article &lt;a rel=&quot;nofollow&quot; href=&quot;https://openai.com/index/scaling-postgresql/&quot;&gt;https://openai.com/index/scaling-postgresql/&lt;/a&gt; feels a bit clickbaity, &lt;br&gt;
If they truly used only one database instance, ChatGPT would’ve been dead on arrival.&lt;/p&gt;
&lt;p&gt;But the reality is far more impressive: they’re using the right tool for each layer of the persistent stack:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;They push PostgreSQL to its absolute limits, with a single primary writer, yes, but backed by nearly 50 read replicas across global regions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For write-heavy workloads, they’ve wisely migrated to sharded systems like Azure Cosmos DB.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;They’ve added layers of resilience:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;connection pooling with PgBouncer,&lt;/li&gt;
&lt;li&gt;query rate limiting,&lt;/li&gt;
&lt;li&gt;caching with lock leasing,&lt;/li&gt;
&lt;li&gt;cascading replication (in testing),&lt;/li&gt;
&lt;li&gt;strict schema-change policies.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s not magic, it’s mature, thoughtful engineering at scale.&lt;/p&gt;
&lt;p&gt;If you work with databases, this post is absolutely worth reading.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/10554/is-openai-really-running-chatgpt-on-a-single-postgresql-instance</guid>
<pubDate>Tue, 27 Jan 2026 09:05:05 +0000</pubDate>
</item>
<item>
<title>Mini Ingestion Framework (MIF-1)</title>
<link>https://coderlegion.com/10349/mini-ingestion-framework-mif-1</link>
<description>&lt;p&gt;I built the &lt;strong&gt;Mini Ingestion Framework (MIF)&lt;/strong&gt; — a deterministic, SOP‑driven &lt;strong&gt;future engine&lt;/strong&gt; that turns simple user requirements into a complete Spark ingestion pipeline.&lt;/p&gt;
&lt;p&gt;MIF uses a numbered SOP library to generate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;a minimal SOP subset&lt;/p&gt;
&lt;p&gt;ordered ingestion steps&lt;/p&gt;
&lt;p&gt;deterministic Spark code&lt;/p&gt;
&lt;p&gt;ASCII documentation&lt;/p&gt;
&lt;p&gt;a validated Parquet output&lt;/p&gt;
&lt;p&gt;No guesswork. No drift. Just reproducible ingestion.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;How it works:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;User provides requirements (rename, drop, metadata, schema, output).&lt;/p&gt;
&lt;p&gt;Rules Engine evaluates them against the SOP library.&lt;/p&gt;
&lt;p&gt;Minimal SOP subset is selected.&lt;/p&gt;
&lt;p&gt;Spark code is generated step‑by‑step.&lt;/p&gt;
&lt;p&gt;Output is a ready‑to‑run ingestion notebook.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example input:&lt;/p&gt;
&lt;p&gt;Code&lt;br&gt;
rename:&lt;br&gt;
  circuited: circuit_id&lt;br&gt;
drop:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;url&lt;br&gt;
metadata:&lt;/li&gt;
&lt;li&gt;ingestion_date&lt;br&gt;
output: parquet&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MIF returns:  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;SOP steps → 7B, 7A, 7C, 9B, 9J&lt;/p&gt;
&lt;p&gt;Spark code for each step&lt;/p&gt;
&lt;p&gt;Final ingestion notebook&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Why it matters: (Need &amp;amp; Utility)  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Most ingestion pipelines are inconsistent and hard to audit.&lt;/p&gt;
&lt;p&gt;MIF introduces a repeatable, governed, standards‑based ingestion&lt;br&gt;
pattern for real data engineering teams.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Roadmap:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Automated CLI&lt;/p&gt;
&lt;p&gt;Silver/Gold SOP subsets&lt;/p&gt;
&lt;p&gt;Schema registry integration&lt;/p&gt;
&lt;p&gt;Notebook generator&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/usman19zafar/Standardizing-Apache-Spark-&quot;&gt;https://github.com/usman19zafar/Standardizing-Apache-Spark-&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Author:&lt;br&gt;
Usman Zafar — Foundational Architect &amp;amp; Cognitive Architect -- Rookie Data Engineer!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/10349/mini-ingestion-framework-mif-1</guid>
<pubDate>Fri, 23 Jan 2026 11:37:17 +0000</pubDate>
</item>
<item>
<title>Case study for Stored Procedures.</title>
<link>https://coderlegion.com/9892/case-study-for-stored-procedures</link>
<description>&lt;p&gt;Just wrapped up a real‑world debugging arc inside Azure Synapse — the kind that reminds you data engineering is built on investigation, not just pipelines.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A simple task (“load 18 months of NYC Taxi data into the Silver&lt;br&gt;
layer”) turned into a full breakdown of:&lt;/p&gt;
&lt;p&gt;schema drift (ehail_fee missing in Bronze)&lt;/p&gt;
&lt;p&gt;CTAS behavior in distributed compute&lt;/p&gt;
&lt;p&gt;why Synapse writes 1 file for some months and 3 for others&lt;/p&gt;
&lt;p&gt;how two stored procedures (instructor vs. corrected) produce&lt;br&gt;
different outputs&lt;/p&gt;
&lt;p&gt;and how to validate a lake house pipeline end‑to‑end&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I documented the entire journey — including the side‑by‑side stored procedure comparison, ASCII diagrams, and the debugging narrative — so others can see what real data engineering looks like when things don’t go as planned.&lt;/p&gt;
&lt;p&gt;If you want to see the technical breakdown and the final working solution, check it out here:&lt;/p&gt;
&lt;p&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/usman19zafar/Azure-Synapse-Project-/blob/main/Data%20Ingestion/Stored%20Procedures%202%2CCaseStudy.md&quot;&gt;https://github.com/usman19zafar/Azure-Synapse-Project-/blob/main/Data Ingestion/Stored Procedures 2,CaseStudy.md&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is the part of the craft that makes you a better engineer — understanding why systems behave the way they do, not just how to make them run.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/9892/case-study-for-stored-procedures</guid>
<pubDate>Sat, 17 Jan 2026 10:07:57 +0000</pubDate>
</item>
<item>
<title>Why learning to build your own ORM is worth it (even if you use EF Core or Dapper)</title>
<link>https://coderlegion.com/8633/why-learning-to-build-your-own-orm-is-worth-it-even-if-you-use-ef-core-or-dapper</link>
<description>&lt;h3 id=&quot;why-learning-to-build-your-own-orm-is-worth-it-even-if-you-use-ef-core-or-dapper-&quot;&gt;Why learning to build your own ORM is worth it (even if you use EF Core or Dapper)&lt;/h3&gt;
&lt;p&gt;Most developers meet ORMs the same way they meet electricity, you flip a switch and things work. That’s great, until something gets slow, a query behaves oddly, or you hit a transaction or concurrency edge case and you suddenly need to know what’s happening under the hood.&lt;/p&gt;
&lt;p&gt;Building a small ORM, even a deliberately limited one, is one of the fastest ways to close the gap between “I can use EF Core or Dapper” and “I understand data access.”&lt;/p&gt;
&lt;p&gt;To keep it grounded, I’ll anchor this around my tiny demo ORM (MicroOrm), a .NET 8 sample using SQLite, simple POCOs, a lightweight &lt;code&gt;Include&lt;/code&gt; for eager loading, and CancellationToken propagation end to end. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/MicroOrm&quot; title=&quot;GitHub - stevsharp/MicroOrm&quot;&gt;GitHub&lt;/a&gt;)&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-you-stop-treating-the-database-like-a-black-box&quot;&gt;1) You stop treating the database like a black box&lt;/h3&gt;
&lt;p&gt;When you write mapping yourself, you’re forced to answer questions full ORMs often hide:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What SQL is actually executed?&lt;/li&gt;
&lt;li&gt;How are parameters created and bound?&lt;/li&gt;
&lt;li&gt;How do nulls map into value types vs nullable types?&lt;/li&gt;
&lt;li&gt;What happens when schema changes, column names drift, or types don’t match?&lt;/li&gt;
&lt;li&gt;How do you retrieve identities safely after inserts?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is where “I know EF” turns into “I can debug a data issue under pressure.”&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-reflection-becomes-a-tool-not-a-mystery&quot;&gt;2) Reflection becomes a tool, not a mystery&lt;/h3&gt;
&lt;p&gt;A tiny ORM almost always starts with reflection.&lt;/p&gt;
&lt;p&gt;You inspect a type, find its attributes, discover the key property, build a column list, and map rows back into objects. In MicroOrm’s demo, entities like &lt;code&gt;Blog&lt;/code&gt; and &lt;code&gt;Post&lt;/code&gt; use familiar attributes like &lt;code&gt;[Table]&lt;/code&gt;, &lt;code&gt;[Key]&lt;/code&gt;, &lt;code&gt;[ForeignKey]&lt;/code&gt;, and &lt;code&gt;[InverseProperty]&lt;/code&gt;, which is exactly the kind of metadata you typically read via reflection. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/MicroOrm&quot; title=&quot;GitHub - stevsharp/MicroOrm&quot;&gt;GitHub&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;The payoff is immediate because you hit two truths fast:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reflection is powerful, it removes a ton of repetitive glue code.&lt;/li&gt;
&lt;li&gt;Reflection is not free, so you learn to cache metadata and keep reflection out of hot paths.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those instincts carry straight into EF Core too.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-generics-give-you-type-safety-and-a-clean-api-surface&quot;&gt;3) Generics give you type safety and a clean API surface&lt;/h3&gt;
&lt;p&gt;Generics are the other superpower of a micro ORM.&lt;/p&gt;
&lt;p&gt;They let you write APIs that feel natural in C# and stay strongly typed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GetById&amp;lt;T&amp;gt;(id)&lt;/code&gt; returns a &lt;code&gt;T&lt;/code&gt;, not &lt;code&gt;object&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GetAll&amp;lt;T&amp;gt;()&lt;/code&gt; returns &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt;, not dictionaries&lt;/li&gt;
&lt;li&gt;One set of operations can work across your entities without repetition&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice, building a micro ORM becomes a “generics + reflection” exercise done right, compile-time safety on the outside, runtime discovery on the inside.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-you-finally-learn-ado-net-for-real-sqlconnection-sqlcommand-and-friends-&quot;&gt;4) You finally learn ADO.NET for real (SqlConnection, SqlCommand, and friends)&lt;/h3&gt;
&lt;p&gt;EF Core and Dapper sit on top of ADO.NET. When you build your own ORM, you stop skipping the fundamentals:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Connections (&lt;code&gt;SqlConnection&lt;/code&gt;, &lt;code&gt;SqliteConnection&lt;/code&gt;, or &lt;code&gt;DbConnection&lt;/code&gt;)&lt;/strong&gt;&lt;br&gt;
You learn what “open late, close early” actually looks like, why &lt;code&gt;using&lt;/code&gt; or &lt;code&gt;await using&lt;/code&gt; matters, and how connection pooling changes the way you should think about lifetime.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Commands (&lt;code&gt;SqlCommand&lt;/code&gt;, &lt;code&gt;DbCommand&lt;/code&gt;)&lt;/strong&gt;&lt;br&gt;
You get comfortable with parameterized queries (and why they’re non negotiable), command timeouts, and how provider differences show up.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Readers (&lt;code&gt;DbDataReader&lt;/code&gt;)&lt;/strong&gt;&lt;br&gt;
You learn what streaming results means, how mapping happens row by row, and where conversions and null handling can bite you.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Transactions (&lt;code&gt;DbTransaction&lt;/code&gt;)&lt;/strong&gt;&lt;br&gt;
You start treating transaction boundaries as a deliberate design choice, not an incidental side effect of the ORM.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MicroOrm’s demo explicitly highlights cancellation support across opening connections and executing commands, which is exactly the sort of detail you only appreciate once you’re directly using these classes. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/MicroOrm&quot; title=&quot;GitHub - stevsharp/MicroOrm&quot;&gt;GitHub&lt;/a&gt;)&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-you-learn-the-real-price-of-convenience-and-why-loading-strategy-matters-&quot;&gt;5) You learn the real price of convenience (and why loading strategy matters)&lt;/h3&gt;
&lt;p&gt;MicroOrm’s &lt;code&gt;Include&lt;/code&gt; supports both references and collections, implemented as extra queries per entity. It’s intentionally simple, and extremely educational because you can feel the tradeoff immediately: clearer code and predictable behavior, plus a very real risk of N+1 query explosions if you’re not careful. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/MicroOrm&quot; title=&quot;GitHub - stevsharp/MicroOrm&quot;&gt;GitHub&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;After you build even a basic Include, you start spotting inefficient loading patterns everywhere, including in mature ORMs.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-you-learn-portability-the-honest-way&quot;&gt;6) You learn portability the honest way&lt;/h3&gt;
&lt;p&gt;“Database agnostic” always has seams.&lt;/p&gt;
&lt;p&gt;The sample calls it out plainly: switching from SQLite to SQL Server means swapping the provider and adjusting identity retrieval in &lt;code&gt;SqlBuilder.Insert&lt;/code&gt; to use &lt;code&gt;SELECT SCOPE_IDENTITY();&lt;/code&gt;. That single detail teaches a big lesson, database behavior differences are real and they show up fast. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/MicroOrm&quot; title=&quot;GitHub - stevsharp/MicroOrm&quot;&gt;GitHub&lt;/a&gt;)&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-takeaway-2&quot;&gt;The takeaway&lt;/h3&gt;
&lt;p&gt;Writing a tiny ORM is not about shipping a competitor to EF Core.&lt;/p&gt;
&lt;p&gt;It’s about gaining X-ray vision for what’s underneath, reflection, generics, ADO.NET primitives like &lt;code&gt;SqlConnection&lt;/code&gt; and &lt;code&gt;SqlCommand&lt;/code&gt;, query cost, round trips, and transactions. Then, when you choose EF Core or Dapper, you’re not just using them, you’re using them with intent.&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/8633/why-learning-to-build-your-own-orm-is-worth-it-even-if-you-use-ef-core-or-dapper</guid>
<pubDate>Fri, 19 Dec 2025 13:53:30 +0000</pubDate>
</item>
<item>
<title>TextQL rewrote their codebase seven times to solve natural language querying at petabyte scale.</title>
<link>https://coderlegion.com/6283/textql-rewrote-their-codebase-seven-times-solve-natural-language-querying-petabyte-scale</link>
<description>&lt;h2 id=&quot;textql-built-a-rosetta-stone-for-enterprise-data-and-it-actually-works&quot;&gt;TextQL Built a Rosetta Stone for Enterprise Data — and It Actually Works&lt;/h2&gt;
&lt;p&gt;Most natural language query tools fail when you move beyond your laptop. &lt;a rel=&quot;nofollow&quot; href=&quot;https://textql.com/&quot;&gt;TextQL&lt;/a&gt; built theirs to work with data that doesn't fit on a single computer.&lt;/p&gt;
&lt;p&gt;The company spent three years teaching AI how to work with tables at enterprise scale. Not the tables you find in a Google Doc or Excel file. The ones with hundreds of thousands of tables, trillions of rows, and petabytes of data spread across different systems.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/theethanding/&quot;&gt;Ethan Ding,&lt;/a&gt; TextQL's CEO, explained their approach during the 64th &lt;a rel=&quot;nofollow&quot; href=&quot;http://www.itpresstour.net&quot;&gt;IT Press Tour&lt;/a&gt;. &quot;First generation AI products were designed for documents that fit on your computer,&quot; he said. &quot;Try to put more than 10 gigabytes of data in an Excel file and your computer starts to fall apart.&quot;&lt;/p&gt;
&lt;h3 id=&quot;the-real-problem&quot;&gt;The Real Problem&lt;/h3&gt;
&lt;p&gt;Enterprise data lives in data warehouses, ERP systems, CRM platforms, and ITSM tools. Each platform has its own query language, table format, and way of storing data. Moving data between these systems costs millions of dollars and takes months.&lt;/p&gt;
&lt;p&gt;The expense isn't accidental. These platforms deliberately make it expensive to leave. They charge 10 to 20 times markup on the underlying infrastructure costs. A CFO who wants to move business logic from SAP to NetSuite faces a contract with Accenture that runs $50 million over five years — and only moves 10% of the actual business logic.&lt;/p&gt;
&lt;p&gt;&quot;If your landlord knew it cost you $50 million to move apartments, they could charge whatever they wanted,&quot; Ding said. &quot;That's why these platforms have really high lock-in.&quot;&lt;/p&gt;
&lt;h3 id=&quot;the-technical-challenge&quot;&gt;The Technical Challenge&lt;/h3&gt;
&lt;p&gt;TextQL built what Ding calls a &quot;Rosetta Stone system.&quot; Instead of translating between every possible combination of query languages and table formats (an n-by-n complexity problem), they created an intermediary layer that makes it a one-by-n problem.&lt;/p&gt;
&lt;p&gt;The system can connect to any database and translate queries on the fly. It doesn't require data migration or lengthy setup. You connect your database, ask a question in natural language, and the AI agent writes code, checks results, and tries different approaches when queries fail.&lt;/p&gt;
&lt;p&gt;During the demo, Ding showed how the system handles a complex query about flight routes and passenger data. The AI wrote SQL queries, caught execution errors, rewrote the queries, and eventually produced visualizations on a satellite map. All without manual intervention.&lt;/p&gt;
&lt;p&gt;&quot;The AI is problem-solving the same way a human data scientist would,&quot; Ding explained. &quot;You try something, it fails, you keep working through it.&quot;&lt;/p&gt;
&lt;h3 id=&quot;what-makes-it-different&quot;&gt;What Makes It Different&lt;/h3&gt;
&lt;p&gt;Most natural language query tools work fine with documents and small datasets. TextQL focused on the hard stuff. They can handle petabytes of data across multiple systems with different schemas. The system reconciles when a customer appears as &quot;Fred&quot; in one database, &quot;Fred Frankel&quot; in another, and &quot;&lt;em&gt;Emails are not allowed&lt;/em&gt;&quot; in a third.&lt;/p&gt;
&lt;p&gt;The company uses Claude and GPT models but runs their own infrastructure layer. &quot;We can spin up a really big computer on the fly,&quot; Ding said. &quot;Historically, you had to do this work where the database lives. We can do it on the fly, which means you only pay for minutes in transit.&quot;&lt;/p&gt;
&lt;p&gt;Their pricing model reflects this approach. They charge based on compute usage, not on tokens or seats. Customers commit to an amount and burn it down as they use the system.&lt;/p&gt;
&lt;h3 id=&quot;the-hard-road&quot;&gt;The Hard Road&lt;/h3&gt;
&lt;p&gt;TextQL started in December 2022. They rewrote their entire codebase seven times. The first version didn't work. Neither did the second. Or the third.&lt;/p&gt;
&lt;p&gt;&quot;We lost every customer until January of this year,&quot; Ding admitted. &quot;All the code is new as of January. We haven't lost a single customer or pilot since then.&quot;&lt;/p&gt;
&lt;p&gt;The 15-person team is mostly senior engineers — dropouts from high-frequency trading firms, former researchers, and people with Master's and PhDs. The CTO was the youngest senior engineer ever at Facebook, working on a team led by Andrew Bosworth (the person who got the famous billion-dollar offer from Facebook).&lt;/p&gt;
&lt;h3 id=&quot;current-state&quot;&gt;Current State&lt;/h3&gt;
&lt;p&gt;TextQL raised $5 million and projects $7 to 7.5 million in revenue. They started serious customer conversations in June and closed their first major contracts. Sales cycles in this space typically run 12 months. TextQL shortened theirs to six.&lt;/p&gt;
&lt;p&gt;Their target customers are CFO offices, financial services firms, and healthcare organizations. The team recently hired experienced enterprise sales executives who've sold to Fortune 500 companies for decades.&lt;/p&gt;
&lt;p&gt;&quot;Put us in a room next to competing software,&quot; Ding challenged. &quot;Put us in a one-to-one comparison. I will bet on our system every time.&quot;&lt;/p&gt;
&lt;p&gt;The company's approach works because they focused on the hardest problems first. When data is spread across multiple systems, when queries need to run against trillions of rows, when the cost of vendor lock-in reaches millions of dollars — that's where TextQL operates.&lt;/p&gt;
&lt;p&gt;Most companies promise natural language querying. TextQL built a system that can actually deliver it at enterprise scale. That's a meaningful difference.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/6283/textql-rewrote-their-codebase-seven-times-solve-natural-language-querying-petabyte-scale</guid>
<pubDate>Fri, 10 Oct 2025 14:43:01 +0000</pubDate>
</item>
<item>
<title>What actually happens when you run an SQL query?</title>
<link>https://coderlegion.com/5227/what-actually-happens-when-you-run-an-sql-query</link>
<description>&lt;p&gt;Do you know what actually happens when you run an SQL query?&lt;/p&gt;
&lt;p&gt;It’s not as simple as: “SQL just fetches rows from a table.”&lt;/p&gt;
&lt;p&gt;Behind the scenes, your query goes through a step-by-step execution order and knowing this is often an interview question for beginners.&lt;/p&gt;
&lt;p&gt;Execution Order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;FROM (pick the table)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;WHERE (filter rows)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GROUP BY (make buckets)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;HAVING (filter buckets)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SELECT (choose what to show)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ORDER BY (sort the results)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;LIMIT (control how much to show)&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Notice how WHERE comes before grouping, while HAVING comes after grouping.&lt;/p&gt;
&lt;p&gt;Notice how ORDER BY is one of the last steps, even though we often write it after SELECT.&lt;/p&gt;
&lt;p&gt;Common Confusions:&lt;/p&gt;
&lt;p&gt;a) WHERE vs HAVING&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;WHERE filters rows before grouping.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;HAVING filters groups after aggregation.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;p&gt;WHERE city = 'Delhi' → filter students from Delhi first.&lt;/p&gt;
&lt;p&gt;HAVING COUNT(*) &amp;gt; 10 → only keep cities with more than 10 students.&lt;/p&gt;
&lt;p&gt;b) GROUP BY vs ORDER BY&lt;/p&gt;
&lt;p&gt;GROUP BY → bundles rows into categories (e.g., students per city).&lt;/p&gt;
&lt;p&gt;ORDER BY → decides the sequence in which results are displayed (e.g., sort cities by student count).&lt;/p&gt;
&lt;p&gt;c) LIMIT with ORDER BY&lt;/p&gt;
&lt;p&gt;Important tasks like “find top 3 highest salaries” or “top 5 cheapest products.”&lt;/p&gt;
&lt;p&gt;If you found this useful, drop a like or share it with someone preparing for SQL interviews.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/@rahulphysicsmishra&quot;&gt;Follow&lt;/a&gt; for more such insightful posts , I’m breaking SQL down into small, clear lessons you’ll actually remember.&lt;/p&gt;
</description>
<category>Group By</category>
<guid isPermaLink="true">https://coderlegion.com/5227/what-actually-happens-when-you-run-an-sql-query</guid>
<pubDate>Sun, 07 Sep 2025 11:40:41 +0000</pubDate>
</item>
<item>
<title>SQL vs NoSQL: Choosing the Right Database Before It Chooses Your Fate</title>
<link>https://coderlegion.com/4923/sql-vs-nosql-choosing-the-right-database-before-it-chooses-your-fate</link>
<description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;SQL or NoSQL - the debate isn’t just about databases, it’s about how your application will think, grow, and scale.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Pick the wrong one, and you might end up fighting your database instead of building your product.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExOGlld3ZzMTJhZDhiZmw5ZXE3b2g5b2Zwa3c5c2thZmh2c205YTU1MyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/9o9dh1JRGThC1qxGTJ/giphy.gif&quot; alt=&quot;database Rage&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;What if the database you choose today becomes the reason your app slows down, fails to scale, or costs you thousands to fix later?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Let’s make sure that never happens.&lt;/p&gt;
&lt;p&gt;Choosing between SQL and NoSQL is one of the most important decisions you’ll make when designing a data-driven application.&lt;/p&gt;
&lt;p&gt;Both have their strengths, weaknesses, and ideal use cases and the choice can shape your system’s performance, scalability, and even your development speed.&lt;/p&gt;
&lt;p&gt;In this guide, we’ll break down what SQL and NoSQL are, where each shines, their pros and cons, and how to decide which one’s right for you.&lt;/p&gt;
&lt;h3 id=&quot;what-is-sql-&quot;&gt;What is SQL?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;SQL (Structured Query Language) is a standardized, domain-specific programming language used to access, manipulate, and manage data within relational database management systems (RDBMS).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Data is organized into tables with rows and columns, enforcing a strict schema. Examples of SQL operations include &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, and advanced queries involving &lt;code&gt;JOINs&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7fu39h2jlhfjjio1hkan.webp&quot; alt=&quot;SQL&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-is-nosql-&quot;&gt;What is NoSQL?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;NoSQL (“Not Only SQL”) refers to a broad category of non-relational databases that store and retrieve data differently from traditional table-based databases.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;NoSQL databases come in many types: key-value, document, wide-column, and graph. They allow for flexible schemas and are designed for high scalability, performance, and handling unstructured or semi-structured data.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mtpslet9j4myjc638av3.png&quot; alt=&quot;NoSQL&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;uses-of-sql-and-nosql-databases&quot;&gt;Uses of SQL and NoSQL Databases&lt;/h3&gt;
&lt;h4 id=&quot;sql&quot;&gt;SQL&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Suitable for applications needing consistent, structured data: finance systems, ERP, CRM, e-commerce, and any domain where ACID transactions and complex queries are crucial.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Commonly found in analytical workloads, data warehousing, and transactional systems.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;nosql&quot;&gt;NoSQL&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Used when flexibility and scalability are necessary: big data analytics, social networks, content management, IoT, and mobile applications.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ideal for handling rapidly evolving data models, storing large amounts of unstructured data, and supporting horizontal scaling across multiple servers.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p0tqc5e690vf5swmxk8v.png&quot; alt=&quot;SQLvsNOSQL&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-should-you-use-sql-vs-nosql-&quot;&gt;When Should You Use SQL vs NoSQL?&lt;/h3&gt;
&lt;h4 id=&quot;sql-is-best-when-&quot;&gt;SQL is best when:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Your data is structured and fits well into tables (tabular model).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You need strong data consistency (e.g., financial transactions).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Your application requires complex queries and joins.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;nosql-is-preferable-when-&quot;&gt;NoSQL is preferable when:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The data is unstructured, semi-structured, or changing frequently.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scalability and distributed architectures are important (e.g., web-scale apps, IoT, streaming data).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You need high write/read throughput, flexible schema, or must store large datasets with minimal overhead.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;popular-sql-and-nosql-databases&quot;&gt;Popular SQL and NoSQL Databases&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=2295131140390580882&quot; alt=&quot;Databases&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;pros-and-cons&quot;&gt;Pros and Cons&lt;/h3&gt;
&lt;h4 id=&quot;sql-databases&quot;&gt;SQL Databases&lt;/h4&gt;
&lt;h5 id=&quot;pros-&quot;&gt;Pros:&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Strong data consistency (ACID properties).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Powerful query capabilities, including JOINs and aggregations.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mature ecosystem, well-supported in enterprise environments.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Standardized language and strong transactional support.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;cons-&quot;&gt;Cons:&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Rigid schema; structure changes are complex.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Vertical scaling (limited by hardware upgrades).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Can become resource-intensive with large volumes of data.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Less ideal for unstructured or semi-structured data.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;nosql-databases&quot;&gt;NoSQL Databases&lt;/h4&gt;
&lt;h5 id=&quot;pros--1&quot;&gt;Pros:&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Highly scalable via horizontal distribution across servers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Flexible schema; dynamic data models.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Good performance under heavy load or with big data volumes.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Suitable for rapid development and changing requirements.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;cons--1&quot;&gt;Cons:&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;May sacrifice strong consistency for performance (eventual consistency).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Limited support for complex queries and joins.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Diverse models (key-value, document, graph, etc.) – learning curve can &lt;br&gt;
be steeper.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ACID guarantees may be absent or limited.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Now let’s dive deep into the Pros and Cons&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-are-acid-properties-&quot;&gt;What Are ACID Properties?&lt;/h3&gt;
&lt;p&gt;ACID is a set of four guarantees that make transactions in a database reliable:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=14760247648998739619&quot; alt=&quot;ACID&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;how-sql-databases-support-acid&quot;&gt;How SQL Databases Support ACID&lt;/h3&gt;
&lt;p&gt;Traditional relational databases like MySQL, PostgreSQL, Oracle, SQL Server are designed around ACID.&lt;/p&gt;
&lt;h4 id=&quot;how-they-do-it-&quot;&gt;How they do it:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Atomicity → Transactions can be started with &lt;code&gt;BEGIN&lt;/code&gt; and rolled back with &lt;code&gt;ROLLBACK&lt;/code&gt; if anything fails.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Consistency → Enforced using:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Data types&lt;/li&gt;
&lt;li&gt;Constraints (&lt;code&gt;PRIMARY KEY&lt;/code&gt;, &lt;code&gt;FOREIGN KEY&lt;/code&gt;, &lt;code&gt;CHECK&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Triggers &amp;amp; rules&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Isolation → Achieved using locking and isolation levels (&lt;code&gt;READ COMMITTED&lt;/code&gt;, &lt;code&gt;SERIALIZABLE&lt;/code&gt;, etc.).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Durability → Achieved through:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Write-ahead logging (WAL) -changes are logged before applying.&lt;/li&gt;
&lt;li&gt;Data replication and backups.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;BEGIN;
UPDATE accounts SET balance = balance - 500 WHERE id = 1;
UPDATE accounts SET balance = balance + 500 WHERE id = 2;
COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If any update fails, SQL ensures the whole operation rolls back.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nd3bcxhf68tqo0kz0c36.gif&quot; alt=&quot;ACID&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;why-many-nosql-databases-don-t-fully-support-acid&quot;&gt;Why Many NoSQL Databases Don’t Fully Support ACID&lt;/h3&gt;
&lt;p&gt;Many NoSQL databases (e.g., MongoDB, Cassandra, Couchbase) prioritize scalability and availability over strict ACID — especially when distributed across many servers.&lt;/p&gt;
&lt;h4 id=&quot;challenges-&quot;&gt;Challenges:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Distributed nature → Keeping strong ACID across multiple nodes is expensive and slow.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Instead, many follow BASE:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Basically Available → System is always responsive.&lt;/li&gt;
&lt;li&gt;Soft state → Data may change over time (eventual consistency).&lt;/li&gt;
&lt;li&gt;Eventual consistency → All replicas will agree eventually, but not instantly.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Many NoSQL systems relax Isolation and Consistency to gain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Faster writes&lt;/li&gt;
&lt;li&gt;Easier horizontal scaling&lt;/li&gt;
&lt;li&gt;High availability in case of network failures&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;-important-note&quot;&gt;❗Important Note&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Not all NoSQL is non-ACID, some have partial or optional ACID support.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MongoDB → ACID transactions for multi-document operations (since v4.0), but with performance trade-offs.&lt;/li&gt;
&lt;li&gt;Cassandra → Lightweight transactions with limited isolation.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;But by default, most NoSQL systems lean towards BASE for performance and scalability.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;sql-databases-are-less-ideal-for-unstructured-or-semi-structured-data&quot;&gt;SQL databases are less ideal for unstructured or semi-structured data&lt;/h3&gt;
&lt;p&gt;SQL databases are less ideal for unstructured or semi-structured data mainly because of how they’re designed at their core.&lt;/p&gt;
&lt;h4 id=&quot;1-sql-databases-expect-a-fixed-schema&quot;&gt;1. SQL Databases Expect a Fixed Schema&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;In SQL, you must define a schema (tables, columns, data types) before inserting data.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Every row in a table must follow this schema.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Problem with unstructured data:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Unstructured (e.g., images, videos, free text) and semi-structured (e.g., JSON, XML) data doesn’t fit neatly into rigid tables.&lt;/li&gt;
&lt;li&gt;If the shape of the data changes often, you’d need to alter the schema repeatedly which is expensive and disruptive.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;2-data-fits-poorly-into-rows-and-columns&quot;&gt;2. Data Fits Poorly into Rows and Columns&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;SQL databases are row-and-column oriented.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Unstructured data often has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Varying fields per record.&lt;/li&gt;
&lt;li&gt;Optional or nested attributes.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Trying to store this in SQL means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Many &lt;code&gt;NULL&lt;/code&gt; values for unused columns.&lt;/li&gt;
&lt;li&gt;Complex join tables to represent nested relationships.&lt;/li&gt;
&lt;li&gt;Reduced performance.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-complex-storage-for-nested-or-variable-data&quot;&gt;3. Complex Storage for Nested or Variable Data&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Semi-structured data like JSON can be stored in modern SQL databases (e.g., PostgreSQL’s jsonb), but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;It loses many of the query optimizations of relational tables.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Indexing and searching within JSON fields is slower and more resource-intensive.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;NoSQL document databases (like MongoDB) handle nested, variable fields natively, making them faster for such use cases.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;4-scaling-and-flexibility-issues&quot;&gt;4. Scaling and Flexibility Issues&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Unstructured/semi-structured data grows in unpredictable ways.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SQL’s strict schema + vertical scaling approach makes it harder to adapt.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;NoSQL’s flexible schema + horizontal scaling is a better fit for changing and large-scale unstructured datasets.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;nosql-databases-support-a-flexible-schema&quot;&gt;NoSQL databases support a flexible schema&lt;/h3&gt;
&lt;p&gt;When we say NoSQL databases support a flexible schema, we mean:&lt;/p&gt;
&lt;h4 id=&quot;1-no-predefined-table-structure&quot;&gt;1. No Predefined Table Structure&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;In SQL, you must define all columns and their types before inserting data.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In NoSQL (e.g., MongoDB, Cassandra, DynamoDB), you don’t have to predefine all fields.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can insert a record with certain fields and another record with completely different fields in the same collection/table.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;2-different-records-can-have-different-structures&quot;&gt;2. Different Records Can Have Different Structures&lt;/h4&gt;
&lt;p&gt;Example in MongoDB (Document Database):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// Document 1
{
  &quot;name&quot;: &quot;Vignesh&quot;,
  &quot;email&quot;: &quot;*Emails are not allowed*&quot;
}

// Document 2
{
  &quot;name&quot;: &quot;Raj&quot;,
  &quot;phone&quot;: &quot;9876543210&quot;,
  &quot;address&quot;: {
    &quot;city&quot;: &quot;Chennai&quot;,
    &quot;zip&quot;: &quot;600001&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Vignesh&lt;/code&gt;has an &lt;code&gt;email&lt;/code&gt;field but no &lt;code&gt;phone&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Raj&lt;/code&gt; has a &lt;code&gt;phone&lt;/code&gt; and nested &lt;code&gt;address&lt;/code&gt;but no &lt;code&gt;email&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Both are stored in the same collection without schema changes.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-easy-to-evolve&quot;&gt;3. Easy to Evolve&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You can add, remove, or rename fields anytime without altering the entire database structure.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This is especially useful when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Your data model changes often.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You’re storing unstructured or semi-structured data (e.g., JSON, XML).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You have to handle diverse data sources with different formats.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;4-schema-on-read-vs-schema-on-write&quot;&gt;4. Schema-on-Read vs Schema-on-Write&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;SQL uses schema-on-write → structure is enforced when data is inserted.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;NoSQL often uses schema-on-read → structure is applied when data is retrieved/processed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This means you can store raw, irregular data and shape it later.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-vertical-scaling-scale-up-&quot;&gt;What is Vertical Scaling (Scale Up)?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Meaning:&lt;/strong&gt; Increasing the capacity of a single machine (server) to handle more load.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;How it’s done:&lt;/strong&gt; Add more CPU, RAM, storage, or faster disks to the same server.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Analogy:&lt;/strong&gt; Like buying a bigger, faster laptop instead of having multiple laptops.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nap8l6l644hms5nu4hxj.webp&quot; alt=&quot;Vertical Scaling&quot;&gt;&lt;/p&gt;
&lt;h4 id=&quot;sql-databases-and-vertical-scaling&quot;&gt;SQL Databases and Vertical Scaling&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Traditional SQL databases (like MySQL, PostgreSQL, Oracle) store data in a structured, relational format.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;They are often designed to run on a single powerful machine for consistency and complex queries.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scaling horizontally (across multiple servers) is harder for SQL because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;They rely heavily on transactions (ACID compliance).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Data is often interrelated (joins across multiple tables).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Splitting (sharding) the data while preserving relationships is complex.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;So the simpler approach has been:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Make the one machine stronger → Vertical scaling.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;what-is-horizontal-scaling-scale-out-&quot;&gt;What is Horizontal Scaling (Scale Out)?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Meaning:&lt;/strong&gt; Adding more machines (servers) to distribute the load.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;How it’s done:&lt;/strong&gt; Use multiple servers that share the workload, often with data spread across them.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Analogy:&lt;/strong&gt; Instead of buying a supercomputer, you buy 10 normal computers and make them work together.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lygjsl9dpjk745lavdt7.webp&quot; alt=&quot;Horizontal Scaling&quot;&gt;&lt;/p&gt;
&lt;h4 id=&quot;nosql-databases-and-horizontal-scaling&quot;&gt;NoSQL Databases and Horizontal Scaling&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;NoSQL databases (like MongoDB, Cassandra, Couchbase) store data in a non-relational way (documents, key-value pairs, wide-columns, etc.).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;They are built with distribution in mind from the start:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Data can be sharded across multiple servers easily.&lt;/li&gt;
&lt;li&gt;Each server handles a portion of the data.&lt;/li&gt;
&lt;li&gt;This allows handling huge datasets and high traffic by just adding more machines.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;So the common scaling method is:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Add more servers to share the work → Horizontal scaling.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;conclusion-2&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Choosing between &lt;strong&gt;SQL&lt;/strong&gt; and &lt;strong&gt;NoSQL&lt;/strong&gt; isn’t about which one is better in a vacuum. It’s about which one aligns with your &lt;strong&gt;data model&lt;/strong&gt;, &lt;strong&gt;growth plans&lt;/strong&gt;, and &lt;strong&gt;application needs&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Think of it like building a house: you wouldn’t choose the same foundation for a skyscraper as you would for a cabin.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If you need &lt;strong&gt;structure, consistency, and reliability&lt;/strong&gt;, SQL databases offer proven stability.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you need &lt;strong&gt;flexibility, speed at scale, and evolving data models&lt;/strong&gt;, NoSQL can give you the agility you need.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The most successful projects start with &lt;strong&gt;clarity, not guesswork&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Pick the right database today, and you won’t just avoid costly mistakes, you’ll set your application up to scale gracefully for years to come.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Follow me on&lt;/em&gt;&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/vigneshj-&quot;&gt;&lt;em&gt;Linkedin&lt;/em&gt;&lt;/a&gt; | &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Vignesh025&quot;&gt;&lt;em&gt;GitHub&lt;/em&gt;&lt;/a&gt; | &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/vigneshtwt_&quot;&gt;&lt;em&gt;Twitter&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Subscribe to my &lt;a rel=&quot;nofollow&quot; href=&quot;https://thedeveloperstack.substack.com&quot;&gt;newsletter&lt;/a&gt; and get articles delivered straight to your inbox.&lt;/p&gt;
</description>
<category>Data Engineering</category>
<guid isPermaLink="true">https://coderlegion.com/4923/sql-vs-nosql-choosing-the-right-database-before-it-chooses-your-fate</guid>
<pubDate>Sat, 23 Aug 2025 11:52:31 +0000</pubDate>
</item>
<item>
<title>Introduction to Subqueries, an advanced technique in SQL</title>
<link>https://coderlegion.com/4724/introduction-to-subqueries-an-advanced-technique-in-sql</link>
<description>&lt;h2 id=&quot;advanced-sql-mastering-subqueries-for-high-performance-data-analysis&quot;&gt;&lt;strong&gt;Advanced SQL : Mastering Subqueries for High-Performance Data Analysis&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;In the realm of &lt;strong&gt;relational database management&lt;/strong&gt;, &lt;strong&gt;SQL subqueries&lt;/strong&gt; are essential tools for building powerful, scalable, and efficient queries. Subqueries, also known as nested queries or inner queries, allow us to perform dynamic filtering, conditional logic, and data aggregation with elegance and precision. This comprehensive guide explores &lt;strong&gt;advanced subquery techniques&lt;/strong&gt;, equipping you with the practical knowledge to build performant SQL code that handles complex data requirements.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-a-subquery-in-sql-&quot;&gt;&lt;strong&gt;What Is a Subquery in SQL?&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;A &lt;strong&gt;subquery&lt;/strong&gt; is a SQL query embedded within another SQL query. It provides an intermediate result set that the outer query can use. Subqueries can be placed in several clauses, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SELECT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FROM&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WHERE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HAVING&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Subqueries may return &lt;strong&gt;scalar values&lt;/strong&gt;, &lt;strong&gt;rows&lt;/strong&gt;, or even &lt;strong&gt;entire result sets&lt;/strong&gt;, depending on how they are written and where they are used.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;types-of-subqueries-in-sql&quot;&gt;&lt;strong&gt;Types of Subqueries in SQL&lt;/strong&gt;&lt;/h3&gt;
&lt;h4 id=&quot;1-scalar-subqueries&quot;&gt;&lt;strong&gt;1. Scalar Subqueries&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;A &lt;strong&gt;scalar subquery&lt;/strong&gt; returns a single value. These are typically used in the &lt;code&gt;SELECT&lt;/code&gt; or &lt;code&gt;WHERE&lt;/code&gt; clause to perform comparisons or calculations against a single derived value.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT employee_name, salary
FROM employees
WHERE salary &amp;gt; (SELECT AVG(salary) FROM employees);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This query returns all employees whose salaries exceed the company-wide average salary.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-row-subqueries&quot;&gt;&lt;strong&gt;2. Row Subqueries&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Row subqueries return a &lt;strong&gt;single row with multiple columns&lt;/strong&gt; and are often used in &lt;code&gt;WHERE&lt;/code&gt; or &lt;code&gt;FROM&lt;/code&gt; clauses.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT department_name
FROM departments
WHERE (manager_id, location_id) = 
      (SELECT manager_id, location_id 
       FROM departments 
       WHERE department_id = 50);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example filters for departments that share the same manager and location as department 50.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;3-column-subqueries&quot;&gt;&lt;strong&gt;3. Column Subqueries&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Column subqueries return &lt;strong&gt;multiple values in a single column&lt;/strong&gt;, useful with operators like &lt;code&gt;IN&lt;/code&gt;, &lt;code&gt;ANY&lt;/code&gt;, &lt;code&gt;ALL&lt;/code&gt;, or &lt;code&gt;EXISTS&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT customer_id, customer_name
FROM customers
WHERE customer_id IN 
      (SELECT customer_id FROM orders WHERE total_amount &amp;gt; 5000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, we filter customers who have made high-value purchases.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;4-correlated-subqueries&quot;&gt;&lt;strong&gt;4. Correlated Subqueries&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;A &lt;strong&gt;correlated subquery&lt;/strong&gt; references columns from the outer query, meaning it executes once for each row in the outer query. This technique is powerful but can be less performant if not optimized.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT e.employee_name, e.salary
FROM employees e
WHERE salary &amp;gt; 
      (SELECT AVG(salary) 
       FROM employees 
       WHERE department_id = e.department_id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This query compares each employee’s salary to the average salary of their own department.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;5-nested-subqueries&quot;&gt;&lt;strong&gt;5. Nested Subqueries&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;SQL allows multiple levels of nesting. These deeply nested subqueries are useful for solving hierarchical or layered logic problems.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT product_name
FROM products
WHERE product_id IN (
    SELECT product_id
    FROM order_details
    WHERE order_id IN (
        SELECT order_id
        FROM orders
        WHERE customer_id = 'C101'
    )
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This query retrieves all products purchased by a specific customer through multiple levels of nesting.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;using-subqueries-in-different-clauses&quot;&gt;&lt;strong&gt;Using Subqueries in Different Clauses&lt;/strong&gt;&lt;/h3&gt;
&lt;h4 id=&quot;subqueries-in-select&quot;&gt;&lt;strong&gt;Subqueries in SELECT&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Useful for creating calculated fields on a per-row basis.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT employee_id,
       (SELECT COUNT(*) FROM tasks WHERE tasks.assigned_to = e.employee_id) AS task_count
FROM employees e;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;subqueries-in-from&quot;&gt;&lt;strong&gt;Subqueries in FROM&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Known as &lt;strong&gt;inline views&lt;/strong&gt;, subqueries in the &lt;code&gt;FROM&lt;/code&gt; clause allow us to define temporary result sets used as tables in joins or filters.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT department_id, avg_salary
FROM (
    SELECT department_id, AVG(salary) AS avg_salary
    FROM employees
    GROUP BY department_id
) dept_stats
WHERE avg_salary &amp;gt; 70000;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;subqueries-in-where&quot;&gt;&lt;strong&gt;Subqueries in WHERE&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Most common use-case for filtering data conditionally.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT product_id, product_name
FROM products
WHERE category_id = (
    SELECT category_id
    FROM categories
    WHERE category_name = 'Electronics'
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;subqueries-in-having&quot;&gt;&lt;strong&gt;Subqueries in HAVING&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Allows filtering of aggregate results after &lt;code&gt;GROUP BY&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT manager_id, COUNT(*) AS team_size
FROM employees
GROUP BY manager_id
HAVING COUNT(*) &amp;gt; 
      (SELECT AVG(team_count)
       FROM (
           SELECT manager_id, COUNT(*) AS team_count
           FROM employees
           GROUP BY manager_id
       ) temp);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This finds managers leading teams larger than the average.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;performance-optimization-for-subqueries&quot;&gt;&lt;strong&gt;Performance Optimization for Subqueries&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;When working with subqueries, especially &lt;strong&gt;correlated subqueries&lt;/strong&gt;, performance can degrade on large datasets. Here’s how we can optimize them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Use JOINs instead of correlated subqueries&lt;/strong&gt; when possible.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Index&lt;/strong&gt; all columns used in subquery filters or joins.&lt;/li&gt;
&lt;li&gt;Replace complex subqueries with &lt;strong&gt;Common Table Expressions (CTEs)&lt;/strong&gt; when appropriate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Avoid repeated subquery execution&lt;/strong&gt; by caching results in temporary tables or views.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;advanced-use-cases-of-subqueries&quot;&gt;&lt;strong&gt;Advanced Use-Cases of Subqueries&lt;/strong&gt;&lt;/h3&gt;
&lt;h4 id=&quot;top-n-records-per-group&quot;&gt;&lt;strong&gt;Top-N Records per Group&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT *
FROM sales s
WHERE sales_amount = (
    SELECT MAX(sales_amount)
    FROM sales
    WHERE region = s.region
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Retrieves the highest sale per region using a correlated subquery.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;detecting-missing-data&quot;&gt;&lt;strong&gt;Detecting Missing Data&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT product_name
FROM products
WHERE product_id NOT IN (
    SELECT DISTINCT product_id
    FROM order_details
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finds products that have never been ordered.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;ranking-using-subqueries&quot;&gt;&lt;strong&gt;Ranking Using Subqueries&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT employee_id, salary,
       (SELECT COUNT(*) FROM employees e2 WHERE e2.salary &amp;gt; e1.salary) + 1 AS rank
FROM employees e1
ORDER BY rank;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Implements a ranking system without window functions.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;conditional-aggregation&quot;&gt;&lt;strong&gt;Conditional Aggregation&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT department_id,
       (SELECT COUNT(*) FROM employees e2 WHERE e2.department_id = d.department_id AND e2.gender = 'F') AS female_count
FROM departments d;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Calculates the number of female employees in each department using correlated subqueries.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;best-practices-when-writing-subqueries&quot;&gt;&lt;strong&gt;Best Practices When Writing Subqueries&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Always &lt;strong&gt;test subqueries independently&lt;/strong&gt; to verify their result sets.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;meaningful aliases&lt;/strong&gt; to keep complex queries readable.&lt;/li&gt;
&lt;li&gt;Avoid &lt;strong&gt;deep nesting&lt;/strong&gt; unless absolutely necessary—refactor into CTEs.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;EXPLAIN plans&lt;/strong&gt; to inspect query cost and identify bottlenecks.&lt;/li&gt;
&lt;li&gt;Be cautious with &lt;strong&gt;NULLs&lt;/strong&gt; when using &lt;code&gt;IN&lt;/code&gt; or &lt;code&gt;NOT IN&lt;/code&gt; subqueries.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion-subqueries-as-the-backbone-of-advanced-sql&quot;&gt;&lt;strong&gt;Conclusion: Subqueries as the Backbone of Advanced SQL&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Subqueries form the core of &lt;strong&gt;advanced SQL querying techniques&lt;/strong&gt;. They allow us to &lt;strong&gt;decompose complex logic&lt;/strong&gt;, &lt;strong&gt;leverage relational data&lt;/strong&gt;, and build &lt;strong&gt;dynamic, reusable queries&lt;/strong&gt;. When mastered, subqueries provide a clean and structured approach to querying that simplifies development and improves data retrieval performance.&lt;/p&gt;
&lt;p&gt;The effective use of &lt;strong&gt;scalar&lt;/strong&gt;, &lt;strong&gt;correlated&lt;/strong&gt;, and &lt;strong&gt;inline subqueries&lt;/strong&gt; enables analysts and developers to create intelligent, high-performance SQL solutions that scale with data complexity.&lt;/p&gt;
</description>
<category>Subqueries</category>
<guid isPermaLink="true">https://coderlegion.com/4724/introduction-to-subqueries-an-advanced-technique-in-sql</guid>
<pubDate>Thu, 14 Aug 2025 19:11:51 +0000</pubDate>
</item>
</channel>
</rss>