sql-style-M
Skill1 starsChanged 4 months agoMITView on GitHub
SQL style and structure rules for Claude-written SQL. Snowflake-tested but mostly vendor-neutral. Auto-applies whenever SQL is being written in dbt projects, .sql files, or Dagster asset files.
In this repository6 other files it gives its agents.
- grill-me-MSkill
- handoff-MSkill
- html-report-MSkill
- notes-MSkill
- pre-compactSkill
- skill-MSkill
The file
We split this skill into the rules it gives an agent
6 rules. About 0 are common, 2 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 104
CTE naming: use the _cte suffix — active_users_cte, simple_features_cte, etc.
- Very specific: line 106
read from exactly one upstream table/asset ({{ ref(...) }} or {{ source(...) }} in dbt)
- Very specific: line 118
Sub-numbers
N️⃣.M️⃣: when a section groups multiple related CTEs - Very specific: line 119
Use 🔟 for top-level section 10.
- Distinctive: line 162
Default: do JOINs in the final SELECT (or in the smallest possible set of CTEs).
- Distinctive: line 223
Don't comment what good naming already conveys.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
SQL Style
Two sections: General SQL rules (any dialect) and Snowflake-specific rules (apply when the target is Snowflake). Some general rules are marked (aspirational) — they pull new code toward the standard; legacy code is what it is.
Real-world reference: a well-sectioned feature-engineering model (numbered sections, source CTEs, progressive JOIN CTEs) is the canonical example of these conventions in action — especially the sectioning and progressive-JOIN patterns below.
Section 1 — General SQL rules
Layout & syntax
- Keywords UPPERCASE —
SELECT,FROM,WHERE,INNER JOIN,QUALIFY, etc. (needed for SQL syntax highlighting inside Python string literals). - Identifiers lower_snake_case —
user_id,order_date. No.in names. No leading digits. - Trailing commas, not leading.
- 4-space indent inside SELECT lists, CTE bodies, subqueries, and
ONcontinuation lines. - Spaces around operators —
price > 100,a.id = b.user_id. - Parentheses for complex logic — group conditions in WHERE / CASE for clarity.
- Single quotes for string literals —
'active','2026-01-01'.
Aliases
- Always
ASfor aliases —users AS usr,total AS order_total(aspirational). - Descriptive initials —
usr,ord,prd,evt— never single letters (u,o,a). - Cap at 3 letters; extend to 4 only when 3 would collide (e.g.,
paymentsvspayouts→pmt/pyt). - Same length per CTE/block — vertically aligned
AScolumns (aspirational). - Same alias for the same table across the entire query, unless impossible (self-joins).
- No alias needed in single-table CTEs — no JOIN, no ambiguity, no alias.
-- Good
SELECT
usr.email,
ord.total
FROM users AS usr
JOIN orders AS ord
ON usr.id = ord.user_idClauses
- Clause order is a hard rule:
SELECT → FROM → JOIN → WHERE → GROUP BY → HAVING → QUALIFY → ORDER BY → LIMIT. - SELECT layout: stacked. SELECT keyword at column 0, each column on its own line indented 4 spaces, trailing commas.
SELECT
col_a,
col_b,
col_c
FROM my_table- Column grouping inside wide SELECTs — when a SELECT has 10+ columns, group them logically with
-- group titlecomment lines:
SELECT
-- Identifiers
event_id,
external_job_id,
-- Event attributes
event_code,
event_reason,
event_type,
-- Time features
DATE_TRUNC('hour', event_datetime) AS rounded_event_ts,
DAYOFWEEKISO(event_datetime) AS weekday_isoCTEs
- No CTEs for very simple queries. If the query is a single-table SELECT with simple WHERE filtering, no joins, no aggregations, no window functions, no subqueries — write it directly without CTEs. The rest of the CTE rules below apply only when the query actually needs CTEs.
- CTE layout: name at column 0, body indented 4 spaces, blank line between CTEs.
WITH
active_users_cte AS (
SELECT
user_id,
email
FROM users
WHERE is_active = TRUE
),
recent_orders_cte AS (
SELECT
order_id,
user_id
FROM orders
WHERE order_date >= '2026-01-01'
)- CTE naming: use the
_ctesuffix —active_users_cte,simple_features_cte, etc. - Source CTE pattern. Unless the query is very simple, the top of the query has dedicated "source CTEs" that:
- read from exactly one upstream table/asset (
{{ ref(...) }}or{{ source(...) }}in dbt) - list all columns explicitly (no
SELECT *here) - may filter rows as needed
- serve as the only source of truth — all downstream CTEs reference these, not the raw refs
- read from exactly one upstream table/asset (
SELECT *: OK in middle CTEs (e.g., passing through after a JOIN). Never in source CTEs. Never in the final SELECT.
Sectioning
For non-trivial queries, organize CTEs into numbered sections. Match this convention:
- A
---...---dash separator (~126 dashes) above every section / sub-section - A
-- N️⃣ Section titleline — one line preferred; multi-line--description allowed when needed - Sub-numbers
N️⃣.M️⃣when a section groups multiple related CTEs - Use
🔟for top-level section 10; use1️⃣0️⃣inside sub-numbers;1️⃣1️⃣for 11, etc. - Each CTE's purpose is captured by its section/sub-section header (no extra
--line needed per CTE — the header serves that role)
------------------------------------------------------------------------------------------------------------------------------
-- 1️⃣ Load and filter source tables
events_cte AS (...),
users_cte AS (...),
------------------------------------------------------------------------------------------------------------------------------
-- 5️⃣ Encoding for embedding: Map categorical features to embedding index IDs
------------------------------------------------------------------------------------------------------------------------------
-- 5️⃣.1️⃣ Join user segment embedding
with_user_segment_embedding_cte AS (...),
------------------------------------------------------------------------------------------------------------------------------
-- 5️⃣.2️⃣ Join product category embedding
with_product_category_embedding_cte AS (...),Subqueries
- Prefer CTEs over subqueries by default. A subquery may remain inline only if it's clearly simple (e.g., a scalar
(SELECT MAX(...))used in one place). - Hard cap: never more than 1 level of nesting — no subquery inside a subquery, ever.
JOINs
- Always explicit JOIN type —
INNER JOIN,LEFT JOIN,RIGHT JOIN,FULL JOIN. Never bareJOIN. - Never implicit comma joins (no
FROM a, b WHERE a.id = b.id). - One JOIN per line.
ONclause on the next line, indented 4. Multi-conditionONcontinues withANDon indented lines. ONalias order follows FROM/JOIN reading order — the alias that appeared first goes on the left:
FROM users AS usr
INNER JOIN orders AS ord
ON usr.id = ord.user_id -- usr first → usr on left
LEFT JOIN items AS itm
ON ord.id = itm.order_id -- ord came before itm → ord on left
AND itm.is_active = TRUE- JOIN placement:
- Default: do JOINs in the final SELECT (or in the smallest possible set of CTEs).
- Exception: progressive JOIN CTEs are allowed for feature engineering / ML pipelines when each enrichment step is a meaningfully named, individually inspectable transformation (one CTE = one JOIN). Use judgment.
WHERE patterns (neutral-element prefix)
For a single static condition, just write the condition directly — no neutral element needed:
WHERE _snowflake_deleted = FALSEFor multiple conditions OR any Jinja-templated condition, prefix with the appropriate neutral element so every condition line is uniform and the first condition can be conditionally absent:
WHERE 1 = 1for AND-chained filters (the common case)WHERE 0 = 1for OR-chained filters (less common; use when it improves readability)
-- AND chain
FROM events
WHERE 1 = 1
AND event_type = 'ASSIGNMENT'
{% if is_incremental() %}
AND ingested_at >= DATEADD(DAY, -7, CURRENT_TIMESTAMP)
{% endif %}
-- OR chain
FROM events
WHERE 0 = 1
{% if include_active %}
OR status = 'active'
{% endif %}
{% if include_pending %}
OR status = 'pending'
{% endif %}Audit columns
- Every asset must have a
created_atcolumn — non-negotiable. Every model/table carries a row-creation timestamp. - When
created_atis propagated from an upstream model, suffix with the source model name —created_at__stg_events,created_at__int_orders. Makes lineage visible in the row itself and avoids collisions when a downstream model pullscreated_atfrom multiple sources. - The freshly-generated
created_atin the current model stays unsuffixed — e.g.CURRENT_TIMESTAMP AS created_at. So a downstream model can pullcreated_atfrom an upstream and end up with bothcreated_at(its own) andcreated_at__<upstream>(inherited).
SELECT
-- Identifiers
event_id,
-- Inherited lineage
stg.created_at AS created_at__stg_events,
dim.created_at AS created_at__dim_customers,
-- This model's own timestamp
CURRENT_TIMESTAMP AS created_at
FROM {{ ref('stg_events') }} AS stg
LEFT JOIN {{ ref('dim_customers') }} AS dim
ON stg.customer_id = dim.customer_idComments
- Comment non-obvious logic, big steps (CTE sections already covered above), and non-trivial parts. The goal: someone reading the file should grasp it fast without digging into every line.
- Don't comment what good naming already conveys.
Section 2 — Snowflake-specific rules
These apply when the target dialect is Snowflake. Skip when writing for Postgres / BigQuery / etc.
QUALIFYfor filtering window functions — no subquery wrap:-- Good SELECT * FROM events QUALIFY ROW_NUMBER() OVER ( PARTITION BY event_id ORDER BY created_at DESC ) = 1 -- Bad SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (...) AS rn FROM events ) WHERE rn = 1Reuse computed column aliases in the same SELECT (important):
SELECT a + b AS sum_ab, sum_ab * 2 AS doubled, -- reuses sum_ab in the same SELECT sum_ab / total AS ratio -- and again FROM tTrailing comma on the last column is allowed and encouraged — produces cleaner diffs when adding/removing columns:
SELECT a, b, c, -- trailing comma OK in Snowflake FROM tPrefer
IFF(cond, x, y)overCASE WHENfor simple binary cases:IFF(qty > 0, 'positive', 'zero_or_neg')* EXCLUDE (col1, col2)when selecting "everything except a few" — avoids listing 30+ columns to drop 2:SELECT * EXCLUDE (id, created_at) FROM t- Prefer Snowflake datetime functions:
DATE_TRUNC,DATEDIFF,DATEADD,EXTRACT,DAYOFWEEKISO,DATE_PART. - Prefer Snowflake array functions:
ARRAY_CONSTRUCT,ARRAY_SIZE,ARRAY_EXCEPT,ARRAY_CONTAINS,ARRAY_AGG. HASH()for deterministic surrogate keys / encoded IDs — fast, deterministic, 64-bit:HASH(order_id) AS order_id_int
Section 3 — Reference example
The strongest demonstration of these conventions applied together is a feature-engineering model for an ML pipeline — the shape that exercises every rule at once: source CTEs at the top, numbered sections with sub-numbers, progressive JOIN CTEs (one enrichment per CTE), column grouping in a wide final SELECT, QUALIFY for last-event-per-key selection, and Snowflake functions (HASH, DATE_TRUNC, IFF). When you write one, build it to this standard and it becomes the reference for the rest of the codebase.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

