SQL शैली मार्गदर्शक

हे पान अद्याप तुमच्या भाषेत अनुवादित झालेले नाही, म्हणून ते इंग्रजीत दाखवले आहे.

A practical set of SQL conventions for naming, keyword case, indentation, aliases, joins, comments and CTEs. Each rule comes with the reason behind it, so your team can adopt it, adapt it or reject it on purpose.

A style guide is only useful if people actually follow it. The conventions below are the ones that pay off most often in real codebases: they make queries easier to scan, they make diffs smaller, and they remove whole classes of mistakes. Where there is no single right answer, such as singular or plural table names, the guide presents both options and asks you to pick one and stay consistent.

Naming conventions

Use lowercase snake_case for tables, columns, views, functions and constraints. Databases disagree on identifier case: some fold unquoted names to lowercase, some to uppercase, and in some the behavior depends on the operating system or a collation setting. Mixed case only survives if you quote it everywhere. PostgreSQL, for example, folds unquoted names to lowercase, so a table created as "CustomerOrder" must be quoted in every query forever. snake_case avoids the problem and reads well in any tool.

  • Avoid reserved words as names (user, order, group, month). They force quoting in some databases and confuse readers in all of them. app_user or customer_order is clearer.
  • Name columns for what they hold, not for their type: created_at, total_amount, country_code. Prefix booleans with is_ or has_: is_paid, has_discount.
  • Use the same name for the same thing everywhere. If the key is customer_id in customer, the foreign key in other tables is also customer_id, not cust or client_ref.
  • Avoid abbreviations unless they are universal in your domain. qty saves three characters and costs every new reader a second of thought.

Singular or plural table names

Both conventions are common and both are defensible:

  • Singular (customer, customer_order) treats the table as the definition of one entity. Names compose cleanly (customer_address), match class names in most ORMs and avoid irregular plurals such as person/people or status/statuses.
  • Plural (customers, orders) treats the table as a collection of rows, which reads naturally in a query: FROM orders. Many frameworks generate plural names by default.

The choice matters much less than consistency. A schema with customer, orders and product_list forces everyone to look up each name. Pick one rule, write it down, and apply it to new tables. The examples on this site mostly use plural names because they read naturally in short queries; nothing in the formatter depends on that choice.

Key and constraint names

Give every constraint and index an explicit name. Generated names differ between databases and environments, which turns a simple DROP CONSTRAINT in a migration into a lookup. A prefix that states the kind of object, followed by the table and the columns, is easy to read in error messages:

ObjectPatternExample
Primary keypk_<table>pk_customer
Foreign keyfk_<table>_<referenced_table>fk_customer_order_customer
Uniqueuq_<table>_<columns>uq_customer_email
Checkck_<table>_<column_or_rule>ck_customer_country_code
Indexix_<table>_<columns>ix_customer_order_customer_id

Keep an eye on length limits: older Oracle versions allow 30 characters per identifier, PostgreSQL 63 and MySQL 64, so long table and column names may need a shorter, agreed abbreviation inside constraint names. Here is a hand-written table definition that follows these rules:

CREATE TABLE customer (
    customer_id   BIGINT       GENERATED ALWAYS AS IDENTITY,
    email         VARCHAR(255) NOT NULL,
    country_code  CHAR(2)      NOT NULL,
    created_at    TIMESTAMPTZ  NOT NULL DEFAULT now(),
    CONSTRAINT pk_customer PRIMARY KEY (customer_id),
    CONSTRAINT uq_customer_email UNIQUE (email),
    CONSTRAINT ck_customer_country_code CHECK (country_code = upper(country_code))
);

CREATE TABLE customer_order (
    customer_order_id  BIGINT        GENERATED ALWAYS AS IDENTITY,
    customer_id        BIGINT        NOT NULL,
    total_amount       NUMERIC(12,2) NOT NULL,
    is_paid            BOOLEAN       NOT NULL DEFAULT false,
    CONSTRAINT pk_customer_order PRIMARY KEY (customer_order_id),
    CONSTRAINT fk_customer_order_customer
        FOREIGN KEY (customer_id) REFERENCES customer (customer_id)
);

CREATE INDEX ix_customer_order_customer_id ON customer_order (customer_id);

The column alignment in that block is hand-made. For DDL, many teams keep table definitions hand-aligned like this in migration files rather than running them through a formatter. SQL Format does lay out CREATE TABLE, with one column definition per line and type parameters such as NUMERIC(12, 2) kept inline, but it does not align the columns into a grid.

Keyword case

Write SQL keywords in one case and identifiers in another, so the structure of a statement stands out from the names in it. The traditional choice is uppercase keywords with lowercase identifiers; lowercase everything is also popular, especially in codebases where SQL sits inside application code with syntax highlighting. What hurts readability is mixing both in the same file.

You do not have to retype anything to get there. The Keywords option rewrites keyword case for you:

आधी
select customer_id, count(*) as order_count, sum(total_amount) as revenue from orders where status = 'shipped' and created_at >= date '2026-01-01' group by customer_id having count(*) > 5 order by revenue desc;
नंतर, SQL Format ने फॉरमॅट केलेले
पर्याय: इंडेंट स्पेस 4 · कमाल स्तंभ 120 · स्वल्पविराम: नंतर · कीवर्ड: मोठी अक्षरे · कंस नवीन ओळीवर: नाही
SELECT customer_id,
    COUNT(*) AS order_count,
    SUM(total_amount) AS revenue
FROM orders
WHERE status = 'shipped'
AND created_at >= DATE '2026-01-01'
GROUP BY customer_id
HAVING COUNT(*) > 5
ORDER BY revenue DESC;

In this output the keywords and the common functions (COUNT, SUM, CURRENT_DATE) are uppercase, while column names and the string literal are untouched. With case=lower the same query comes out entirely in lowercase. One detail to know: an unquoted, unqualified identifier that is also a keyword, such as an alias named month, follows keyword case too, which is one more reason to avoid reserved words as column names. Quoted names and qualified names such as o.month are never re-cased.

Indentation and alignment

There are two main layout families:

  • Left-aligned clauses: each clause (SELECT, FROM, WHERE, GROUP BY, ORDER BY) starts at the same column and its contents are indented by a fixed number of spaces. It is simple to type and survives any editor.
  • "River" alignment: keywords are right-aligned so that the clause bodies form a straight vertical line. It looks tidy, but every new keyword of a different length can force you to realign, and editors rarely help.
-- "River" style: keywords right-aligned so the clause bodies line up.
SELECT o.order_id,
       c.customer_name,
       o.total_amount
  FROM orders AS o
  JOIN customers AS c
    ON c.customer_id = o.customer_id
 WHERE o.status = 'open'
   AND o.total_amount > 100
 ORDER BY o.created_at;

SQL Format uses the first family. Each major clause starts a new line, the first select item stays on the SELECT line and the rest go one per line, indented. (When the first item begins with a function call, a CASE or a parenthesis, it moves to the next line with the others.) Joins are indented under FROM with their ON condition indented one level further, and each AND condition starts its own line:

आधी
select o.order_id, c.customer_name, p.product_name, oi.quantity from orders o join customers c on c.customer_id = o.customer_id join order_items oi on oi.order_id = o.order_id left join products p on p.product_id = oi.product_id where o.status = 'open';
नंतर, SQL Format ने फॉरमॅट केलेले
पर्याय: इंडेंट स्पेस 4 · कमाल स्तंभ 120 · स्वल्पविराम: नंतर · कीवर्ड: मोठी अक्षरे · कंस नवीन ओळीवर: नाही
SELECT o.order_id,
    c.customer_name,
    p.product_name,
    oi.quantity
FROM orders o
    JOIN customers c
        ON  c.customer_id = o.customer_id
    JOIN order_items oi
        ON  oi.order_id = o.order_id
    LEFT JOIN products p
        ON  p.product_id = oi.product_id
WHERE o.status = 'open';

Whatever you choose, use spaces rather than tabs so the layout looks the same in every editor, code review tool and terminal. The formatter replaces tabs with spaces, except inside string literals and quoted identifiers, using the Indent spaces setting, which is four by default. Keep lines to a reasonable width as well; the Maximum columns option wraps any line that grows past the limit.

Meaningful aliases

Aliases exist to save typing without losing meaning. Single letters taken from the alphabet (a, b, c) save typing and lose all of the meaning: by the third join, nobody remembers that c is the order items table.

SELECT a.id, b.name, c.qty, d.name
FROM orders a, customers b, order_items c, products d
WHERE a.cust = b.id
  AND c.ord = a.id
  AND d.id = c.prod
  AND status = 'open';

Better options, in order of preference:

  • Initials of the table name: o for orders, c for customers, oi for order_items.
  • A short word when initials collide or the same table appears twice: buyer and seller for two joins to customers, or parent and child for a self-join.
  • No alias at all for a single-table query.

Use the same alias for the same table across the codebase, so oi always means order_items. For column aliases, write AS explicitly. It is optional in most databases, but without it a forgotten comma silently turns the next column into an alias, a trap covered in detail in the leading vs trailing commas guide.

Explicit JOIN ... ON instead of comma joins

The query with bad aliases above also uses the old comma-join syntax: all tables listed in FROM, separated by commas, with the join conditions mixed into WHERE together with the real filters. It works, but it has real costs:

  • Forgetting one join condition produces a Cartesian product, and nothing in the syntax points at the missing line.
  • Join conditions and filters are interleaved, so the reader has to sort out which predicate does what.
  • Outer joins cannot be expressed in standard comma syntax at all.

With explicit JOIN ... ON, each table is introduced together with the condition that links it, and WHERE only contains filters. The formatted join example in the previous section shows the rewrite: each join sits on its own line with its ON clause right below it. Prefer ON over USING or NATURAL JOIN in shared code; NATURAL JOIN in particular matches on every column with the same name, so adding an innocent created_at to a table can change the result of a query that never mentions it.

Qualifying columns

As soon as a query reads from more than one table, prefix every column with its table alias, including columns in WHERE, GROUP BY and ORDER BY.

-- Which table does each column come from?
SELECT order_id, customer_name, total_amount, created_at
FROM orders
JOIN customers USING (customer_id)
WHERE status = 'open';

A reader cannot tell where status or created_at come from without opening the schema. Worse, the query can break later: if someone adds a created_at column to customers, the unqualified reference becomes ambiguous and the query fails. Qualified columns make the query self-explanatory and resistant to schema changes. For a single-table query, qualification is optional.

Comments

SQL has two comment forms:

  • -- runs to the end of the line. Use it for most comments: a header above a query, a note above a tricky condition.
  • /* ... */ can span lines or sit in the middle of one. It is handy for a short note inside an expression or for temporarily disabling a block, and it is safer than -- when SQL may end up flattened onto a single line by some tool, since a line comment would swallow the rest of the statement.

Comment the why, not the what. -- join customers above a join adds nothing. Things worth a comment: the business rule behind a filter, why a LEFT JOIN is needed, a known data quirk, the expected number of rows in a data fix, or which report or job uses the query. The formatter keeps both comment forms in place:

आधी
-- Active customers with at least one order in the last 90 days.
-- Used by the weekly retention report.
select c.customer_id, c.email, max(o.created_at) as last_order_at from customers c join orders o on o.customer_id = c.customer_id where c.deleted_at is null /* soft-deleted rows stay in the table */ and o.created_at >= current_date - 90 group by c.customer_id, c.email;
नंतर, SQL Format ने फॉरमॅट केलेले
पर्याय: इंडेंट स्पेस 4 · कमाल स्तंभ 120 · स्वल्पविराम: नंतर · कीवर्ड: मोठी अक्षरे · कंस नवीन ओळीवर: नाही
-- Active customers with at least one order in the last 90 days.
-- Used by the weekly retention report.
SELECT c.customer_id,
    c.email,
    MAX(o.created_at) AS last_order_at
FROM customers c
    JOIN orders o
        ON  o.customer_id = c.customer_id
WHERE c.deleted_at IS NULL /* soft-deleted rows stay in the table */
AND o.created_at >= CURRENT_DATE - 90
GROUP BY c.customer_id,
    c.email;

CTEs vs subqueries for readability

Nested subqueries are read inside out: to understand the outer query, you first have to find the innermost SELECT and work your way back. Here is a two-level derived table:

नंतर, SQL Format ने फॉरमॅट केलेले
पर्याय: इंडेंट स्पेस 4 · कमाल स्तंभ 120 · स्वल्पविराम: नंतर · कीवर्ड: मोठी अक्षरे · कंस नवीन ओळीवर: नाही
SELECT c.customer_name,
    ct.total_30d
FROM (
    SELECT customer_id,
        SUM(total_amount) AS total_30d
    FROM (
        SELECT customer_id,
            total_amount
        FROM orders
        WHERE created_at >= CURRENT_DATE - 30
    ) ro
    GROUP BY customer_id
) ct
    JOIN customers c
        ON  c.customer_id = ct.customer_id
WHERE ct.total_30d > 1000
ORDER BY ct.total_30d DESC;

The same logic written with common table expressions reads top to bottom, and each step has a name that documents its purpose:

नंतर, SQL Format ने फॉरमॅट केलेले
पर्याय: इंडेंट स्पेस 4 · कमाल स्तंभ 120 · स्वल्पविराम: नंतर · कीवर्ड: मोठी अक्षरे · कंस नवीन ओळीवर: नाही
WITH recent_orders AS (
    SELECT customer_id,
        total_amount
    FROM orders
    WHERE created_at >= CURRENT_DATE - 30
),
customer_totals AS (
    SELECT customer_id,
        SUM(total_amount) AS total_30d
    FROM recent_orders
    GROUP BY customer_id
)
SELECT c.customer_name,
    ct.total_30d
FROM customer_totals ct
    JOIN customers c
        ON  c.customer_id = ct.customer_id
WHERE ct.total_30d > 1000
ORDER BY ct.total_30d DESC;

Prefer a CTE when a step has a meaningful name, when it is referenced more than once, or when the nesting goes past one level. A short subquery in an IN list or a single derived table is often fine inline. Be aware that the optimizer may treat CTEs and subqueries differently depending on the database and version, so check the execution plan when a rewrite is performance-sensitive. The guide to formatting complex queries goes further into CTE chains, CASE, window functions and UNION.

Adopting a style guide in a team

Write the rules down in the repository, keep them short, and let a tool handle the mechanical parts. Layout, keyword case and comma placement are exactly the kind of rules a formatter can apply for you; naming, aliases and comments still need a human. The code review and version control guide explains how to roll out a shared format without flooding your history with noise, and common SQL readability mistakes lists the habits worth catching in review.