{"post":{"slug":"how-to-write-a-sql-query-2026","title":"How to write a SQL query in 2026","excerpt":"SQL is 50 years old and still the fastest path from question to answer in most databases. This guide covers SELECT fundamentals, joins, aggregations, and modern window functions.","category":"code","date":"March 18, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">S</span>QL remains the most universally useful skill a developer can have in 2026. Whether you are querying Postgres, MySQL, SQLite, DuckDB, or a cloud warehouse, the core language is the same. This guide moves from basic SELECT to window functions with runnable examples throughout.\n\n## What changed in 2026\n\n- **DuckDB went mainstream** for analytical queries on local files — the same SQL you know runs on Parquet, CSV, and JSON directly.\n- **Postgres 17 added more parallel query improvements** — queries that used sequential scans on 16-core machines are noticeably faster without any schema changes.\n- **AI assistants generate plausible but wrong SQL** — understanding the plan yourself is more valuable than ever because LLM output needs verification.\n- **Window functions are no longer \"advanced\"** — any developer working with time-series or ranked results is expected to know them.\n\n## The anatomy of a SELECT\n\n```sql\nSELECT   column1, column2, aggregate(column3)   -- 5. what to return\nFROM     table_name                              -- 1. source\nJOIN     other_table ON condition                -- 2. join\nWHERE    row_filter                              -- 3. filter rows\nGROUP BY column1, column2                        -- 4. aggregate\nHAVING   aggregate_filter                        -- 6. filter groups\nORDER BY column1 DESC                            -- 7. sort\nLIMIT    100;                                    -- 8. cap rows\n```\n\nThe numbers show logical execution order — not the written order. `WHERE` runs before `GROUP BY`; `HAVING` runs after. That matters when you wonder why you can't filter on an alias defined in SELECT.\n\n## JOINs explained\n\n```sql\n-- INNER: only rows that match in both tables\nSELECT o.id, c.name\nFROM orders o\nINNER JOIN customers c ON c.id = o.customer_id;\n\n-- LEFT: every row from orders, NULL for missing customers\nSELECT o.id, c.name\nFROM orders o\nLEFT JOIN customers c ON c.id = o.customer_id;\n\n-- Count orders per customer (including customers with zero orders)\nSELECT c.name, COUNT(o.id) AS order_count\nFROM customers c\nLEFT JOIN orders o ON o.customer_id = c.id\nGROUP BY c.name\nORDER BY order_count DESC;\n```\n\n| JOIN type | Returns |\n| --- | --- |\n| INNER JOIN | Rows with a match in both tables |\n| LEFT JOIN | All left rows; NULLs for unmatched right |\n| RIGHT JOIN | All right rows; NULLs for unmatched left |\n| FULL OUTER JOIN | All rows from both; NULLs where no match |\n| CROSS JOIN | Cartesian product — use rarely and deliberately |\n\n## CTEs (WITH clauses)\n\nCTEs replace subqueries and make complex logic readable:\n\n```sql\nWITH monthly_revenue AS (\n    SELECT\n        DATE_TRUNC('month', created_at) AS month,\n        SUM(amount) AS revenue\n    FROM orders\n    WHERE status = 'paid'\n    GROUP BY 1\n)\nSELECT\n    month,\n    revenue,\n    revenue - LAG(revenue) OVER (ORDER BY month) AS mom_change\nFROM monthly_revenue\nORDER BY month;\n```\n\nUse CTEs whenever a query has more than two joins or needs a derived table more than once.\n\n## Window functions\n\nWindow functions compute a value across a set of rows related to the current row — without collapsing them into groups.\n\n```sql\n-- Rank products by revenue within each category\nSELECT\n    category,\n    product_name,\n    revenue,\n    RANK() OVER (\n        PARTITION BY category\n        ORDER BY revenue DESC\n    ) AS rank_in_category\nFROM products;\n\n-- Running total\nSELECT\n    order_date,\n    amount,\n    SUM(amount) OVER (ORDER BY order_date) AS running_total\nFROM orders;\n\n-- Previous row value\nSELECT\n    day,\n    signups,\n    signups - LAG(signups, 1, 0) OVER (ORDER BY day) AS daily_delta\nFROM signups_daily;\n```\n\n| Function | Use |\n| --- | --- |\n| ROW_NUMBER() | Unique sequential number per partition |\n| RANK() | Rank with gaps on ties |\n| DENSE_RANK() | Rank without gaps on ties |\n| LAG(col, n) | Value from n rows before |\n| LEAD(col, n) | Value from n rows ahead |\n| SUM / AVG OVER | Running or windowed aggregates |\n\n## How to pick the right approach\n\n1. **Single table, filter and sort?** → Simple SELECT + WHERE + ORDER BY.\n2. **Multiple tables?** → JOIN — INNER if both sides must match, LEFT if one side is optional.\n3. **Aggregating?** → GROUP BY + aggregate functions; HAVING to filter on the aggregate.\n4. **Need ranked or running values without collapsing rows?** → Window function.\n5. **Query is getting hard to read?** → Break it into CTEs.\n\n## Reading EXPLAIN ANALYZE\n\n```sql\nEXPLAIN ANALYZE\nSELECT * FROM orders WHERE customer_id = 42;\n```\n\nLook for:\n- **Seq Scan on a large table** — usually means a missing index.\n- **Rows=** estimate vs actual rows — large gaps mean stale statistics; run `ANALYZE table_name`.\n- **Nested Loop with large outer rows** — may be better as a Hash Join; Postgres should switch automatically if statistics are current.\n\n## Common mistakes\n\n**Filtering on a function in WHERE.** `WHERE YEAR(created_at) = 2025` cannot use an index on `created_at`; rewrite as `WHERE created_at >= '2025-01-01' AND created_at < '2026-01-01'`.\n\n**Using HAVING instead of WHERE to filter rows.** HAVING runs after aggregation; filtering non-aggregated columns with WHERE is faster because it reduces the row count before grouping.\n\n**Implicit cross joins.** `FROM a, b WHERE a.id = b.a_id` works but hides intent; explicit JOIN is clearer and less error-prone.\n\n**Forgetting NULL in comparisons.** `WHERE col != 'foo'` excludes NULLs silently. Use `WHERE col != 'foo' OR col IS NULL` if you want NULLs included.\n\n## What to skip\n\n- **SELECT *** in application code — list columns explicitly; it prevents index-only scans and breaks when the schema changes.\n- **Correlated subqueries in SELECT** for large tables — they execute once per row; rewrite as a JOIN or CTE.\n- **OR on indexed columns** — `WHERE a = 1 OR b = 2` often skips both indexes; use UNION ALL of two queries instead.\n\n## FAQ\n\n**What is the difference between WHERE and HAVING?**\nWHERE filters individual rows before grouping; HAVING filters groups after aggregation. You cannot reference aggregate functions in WHERE.\n\n**When should I use a subquery vs a JOIN?**\nPrefer JOINs for readability and optimizer flexibility. Subqueries (or CTEs) are useful when the derived set is referenced more than once or the logic is clearer that way.\n\n**How do I deduplicate rows?**\nUse `SELECT DISTINCT` for simple cases. For \"keep one row per group by some key\" use `ROW_NUMBER() OVER (PARTITION BY key ORDER BY tiebreaker) = 1` in a CTE.\n\n**What is the fastest way to count rows in a large table?**\nOn Postgres, `SELECT reltuples FROM pg_class WHERE relname = 'your_table'` returns an approximate count instantly. For an exact count you must do a full `COUNT(*)`.\n\n## Where to go next\n\n- [How to connect to a database in 2026](/blog/how-to-connect-to-a-database-2026)\n- [How to design a database schema in 2026](/blog/how-to-design-a-database-schema-2026)\n- [How to normalize a database in 2026](/blog/how-to-normalize-a-database-2026)","hue":315,"metaTitle":"How to Write a SQL Query in 2026","metaDescription":"Practical SQL query guide for 2026. Covers SELECT, JOINs, GROUP BY, CTEs, window functions, and EXPLAIN ANALYZE with real examples for Postgres and MySQL.","keywords":["how to write a sql query 2026","sql query tutorial 2026","sql joins explained","sql window functions","sql cte example","postgres query optimization","sql group by having","explain analyze postgres","sql subquery vs join","sql for developers 2026"],"tldr":["**Start every query with the smallest possible result set** — filter early with WHERE before joining, never after.","**JOINs are not scary** — INNER, LEFT, and a CTE cover 95% of real-world queries; the rest are edge cases.","**Window functions replace many self-joins** — ROW_NUMBER, RANK, and LAG are your most useful additions after GROUP BY.","**EXPLAIN ANALYZE before optimizing** — never guess; read the query plan to find seq scans on large tables.","**Skip:** SELECT * in production code — it breaks when columns are added and prevents index-only scans."],"readTime":"5 min read","wordCount":1105},"related":[{"slug":"how-to-write-sql-joins-2026","title":"How to write SQL joins in 2026: INNER, LEFT, and beyond with real examples","excerpt":"SQL joins are the most important query concept for working with relational data. This guide covers every join type with practical Postgres examples and performance tips.","category":"code","date":"April 20, 2026","author":"ByteLedger Team","readTime":"6 min read","hue":314},{"slug":"how-to-optimize-sql-queries-2026","title":"How to optimize SQL queries in 2026: indexes, plans, and real fixes","excerpt":"Slow SQL is rarely a mystery — the query plan tells you exactly what is wrong. This 2026 guide covers the tools and techniques to find and fix slow queries fast.","category":"code","date":"April 1, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":309},{"slug":"sql-joins-explained-2026","title":"SQL joins explained in 2026: the only guide you need","excerpt":"INNER, LEFT, RIGHT, FULL, CROSS, and LATERAL joins demystified with real examples, performance notes, and the 2026 patterns that actually ship.","category":"code","date":"March 19, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":308}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"how-to-write-a-resume-2026","title":"How to write a resume in 2026: beat the ATS and impress humans","excerpt":"Most resumes fail before a human sees them. This 2026 guide covers ATS optimization, modern formatting, and the exact structure that gets callbacks.","category":"guides","date":"March 18, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":280},"older":{"slug":"how-to-write-clean-functions-2026","title":"How to write clean functions in 2026","excerpt":"Clean functions are the single highest-leverage thing you can do for code quality. Here is the 2026 playbook with concrete rules and real examples.","category":"code","date":"March 18, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":318}}