Skip to content

Structure Rules

This section contains 12 rule(s) related to structure.

Rule Summary

CodeNameAliasesFix Compatible
ST01structure.else_nullL035
ST02structure.simple_caseL043
ST03structure.unused_cteL045
ST04structure.nested_caseL058
ST05structure.subqueryL042
ST06structure.column_orderL034
ST07structure.usingL032
ST08structure.distinctL015
ST09structure.join_condition_order-
ST10structure.constant_expression-
ST11structure.unused_join-
ST12structure.consecutive_semicolons-

ST01: structure.else_null

Do not specify else null in a case when statement (redundant).

PropertyValue
Namestructure.else_null
AliasesL035
Groupsall, structure
Auto-fixableYes ✅

Anti-pattern

sql
select
    case
        when name like '%cat%' then 'meow'
        when name like '%dog%' then 'woof'
        else null
    end
from x

Best practice

Omit else null

sql
select
    case
        when name like '%cat%' then 'meow'
        when name like '%dog%' then 'woof'
    end
from x

ST02: structure.simple_case

Unnecessary CASE statement.

PropertyValue
Namestructure.simple_case
AliasesL043
Groupsall, structure
Auto-fixableYes ✅

Anti-pattern

CASE statement returns booleans.

sql
select
    case
        when fab > 0 then true
        else false
    end as is_fab
from fancy_table

-- This rule can also simplify CASE statements
-- that aim to fill NULL values.

select
    case
        when fab is null then 0
        else fab
    end as fab_clean
from fancy_table

-- This also covers where the case statement
-- replaces NULL values with NULL values.

select
    case
        when fab is null then null
        else fab
    end as fab_clean
from fancy_table

Best practice

Reduce to WHEN condition within COALESCE function.

sql
select
    coalesce(fab > 0, false) as is_fab
from fancy_table

-- To fill NULL values.

select
    coalesce(fab, 0) as fab_clean
from fancy_table

-- NULL filling NULL.

select fab as fab_clean
from fancy_table

ST03: structure.unused_cte

Query defines a CTE (common-table expression) but does not use it.

PropertyValue
Namestructure.unused_cte
AliasesL045
Groupsall, core, structure
Auto-fixableNo ❌

Anti-pattern

Defining a CTE that is not used by the query is harmless, but it means the code is unnecessary and could be removed.

sql
WITH cte1 AS (
  SELECT a
  FROM t
),
cte2 AS (
  SELECT b
  FROM u
)

SELECT *
FROM cte1

Best practice

Remove unused CTEs.

sql
WITH cte1 AS (
  SELECT a
  FROM t
)

SELECT *
FROM cte1

ST04: structure.nested_case

Nested CASE statement in ELSE clause could be flattened.

PropertyValue
Namestructure.nested_case
AliasesL058
Groupsall, structure
Auto-fixableYes ✅

Anti-pattern

In this example, the outer CASE's ELSE is an unnecessary, nested CASE.

sql
SELECT
  CASE
    WHEN species = 'Cat' THEN 'Meow'
    ELSE
    CASE
       WHEN species = 'Dog' THEN 'Woof'
    END
  END as sound
FROM mytable

Best practice

Move the body of the inner CASE to the end of the outer one.

sql
SELECT
  CASE
    WHEN species = 'Cat' THEN 'Meow'
    WHEN species = 'Dog' THEN 'Woof'
  END AS sound
FROM mytable

ST05: structure.subquery

Join/From clauses should not contain subqueries. Use CTEs instead.

PropertyValue
Namestructure.subquery
AliasesL042
Groupsall, structure
Auto-fixableYes ✅

By default this rule is configured to allow subqueries within FROM clauses but not within JOIN clauses. If you prefer a stricter lint then this is configurable.

NOTE

Some dialects don't allow CTEs, and for those dialects this rule makes no sense and should be disabled.

Anti-pattern

sql
select
    a.x, a.y, b.z
from a
join (
    select x, z from b
) using(x)

Best practice

sql
with c as (
    select x, z from b
)
select
    a.x, a.y, c.z
from a
join c using(x)

Configuration

OptionDescription
forbid_subquery_inWhich clauses should be linted for subqueries? Must be one of ['join', 'from', 'both'].

ST06: structure.column_order

Select wildcards then simple targets before calculations and aggregates.

PropertyValue
Namestructure.column_order
AliasesL034
Groupsall, structure
Auto-fixableYes ✅

Anti-pattern

sql
select
    a,
    *,
    row_number() over (partition by id order by date) as y,
    b
from x

Best practice

Order select targets in ascending complexity

sql
select
    *,
    a,
    b,
    row_number() over (partition by id order by date) as y
from x

ST07: structure.using

Prefer specifying join keys instead of using USING.

PropertyValue
Namestructure.using
AliasesL032
Groupsall, structure
Auto-fixableYes ✅

NOTE

This rule was originally taken from the dbt Style Guide <https://github.com/dbt-labs/corp/blob/ main/dbt_style_guide.md> which notes that:

Certain warehouses have inconsistencies in USING results (specifically Snowflake).

In fact dbt removed it from their style guide in February 2022 https://github.com/dbt-labs/corp/pull/58. However, some like the rule, so for now we will keep it in SQLFluff, but encourage those that do not find value in the rule, to turn it off.

NOTE

This rule is disabled for ClickHouse as it supports USING without brackets which this rule does not support.

Anti-pattern

sql
SELECT
    table_a.field_1,
    table_b.field_2
FROM
    table_a
INNER JOIN table_b USING (id)

Best practice

Specify the keys directly

sql
SELECT
    table_a.field_1,
    table_b.field_2
FROM
    table_a
INNER JOIN table_b
    ON table_a.id = table_b.id

ST08: structure.distinct

DISTINCT used with parentheses.

PropertyValue
Namestructure.distinct
AliasesL015
Groupsall, structure, core
Auto-fixableYes ✅

Anti-pattern

In this example, parentheses are not needed and confuse DISTINCT with a function. The parentheses can also be misleading about which columns are affected by the DISTINCT (all the columns!).

sql
SELECT DISTINCT(a), b FROM foo

Best practice

Remove parentheses to be clear that the DISTINCT applies to both columns.

sql
SELECT DISTINCT a, b FROM foo

ST09: structure.join_condition_order

Joins should list the table referenced earlier/later first.

PropertyValue
Namestructure.join_condition_order
Groupsall, structure
Auto-fixableYes ✅

This rule will break conditions from join clauses down into subconditions using the "and" and "or" binary operators.

Subconditions that are made up of a qualified column reference, a comparison operator and another qualified column reference are then evaluated to check whether they list the table that was referenced earlier - or later, depending on the preferred_first_table_in_join_clause configuration.

Subconditions that do not follow that pattern are ignored by this rule.

NOTE

Joins in WHERE clauses are currently not supported by this rule.

Anti-pattern

In this example, the tables that were referenced later are listed first and the preferred_first_table_in_join_clause configuration is set to earlier.

sql
select
    foo.a,
    foo.b,
    bar.c
from foo
left join bar
    -- This subcondition does not list
    -- the table referenced earlier first:
    on bar.a = foo.a
    -- Neither does this subcondition:
    and bar.b = foo.b

Best practice

List the tables that were referenced earlier first.

sql
select
    foo.a,
    foo.b,
    bar.c
from foo
left join bar
    on foo.a = bar.a
    and foo.b = bar.b

Configuration

OptionDescription
preferred_first_table_in_join_clauseWhich table to list first when joining two tables. Defaults to earlier. Must be one of ['earlier', 'later'].

ST10: structure.constant_expression

Redundant constant expression.

PropertyValue
Namestructure.constant_expression
Groupsall, structure
Auto-fixableNo ❌

Including an expression that always evaluates to either TRUE or FALSE regardless of the input columns is unnecessary and makes statements harder to read and understand.

Constant conditions are sometimes mistakes (by mistyping the column name intended), and sometimes the result of incorrect information that they are necessary in some circumstances. In the former case, they can sometimes result in a cartesian join if it was supposed to be a join condition. Given the ambiguity of intent, this rule does not suggest an automatic fix, and instead invites the user to resolve the problem manually.

Anti-pattern

sql
SELECT *
FROM my_table
-- This following WHERE clause is redundant.
WHERE my_table.col = my_table.col

Best practice

sql
SELECT *
FROM my_table
-- Replace with a condition that includes meaningful logic,
-- or remove the condition entirely.
WHERE my_table.col > 3

ST11: structure.unused_join

Joined table not referenced in query.

PropertyValue
Namestructure.unused_join
Groupsall, structure
Auto-fixableNo ❌

This rule will check if any tables are referenced in the FROM or JOIN clause of a SELECT statement where no columns from that table are referenced in any of the other clauses. Because some types of joins are often used as filters or to otherwise control granularity without being referenced (e.g. INNER and CROSS), this rule only applies to explicit OUTER joins (i.e. LEFT, RIGHT, and FULL joins).

This rule relies on all of the column references in the SELECT statement being qualified with at least the table name, and so is designed to work alongside references.qualification (RF02). This is because, without the knowledge of what columns exist in each upstream table, the rule is unable to resolve which table an unqualified column reference is pulled from.

This rule does not propose a fix because it assumes that an unused table is a mistake, but it doesn't know whether the mistake was due to the join or the failure to use the table.

Anti-pattern

In this example, the table bar is included in the JOIN clause but not columns from it are referenced in

sql
SELECT
    foo.a,
    foo.b
FROM foo
LEFT JOIN bar ON foo.a = bar.a

Best practice

Remove the join, or use the table.

sql
SELECT foo.a, vee.b
FROM foo;

SELECT
    foo.a,
    foo.b,
    bar.c
FROM foo
LEFT JOIN bar ON foo.a = bar.a

In the (very rare) situations that it is logically necessary to include a table in a join clause, but not otherwise refer to it (likely for granularity reasons, or as a stepping stone to another table), we recommend ignoring this rule for that specific line by using -- noqa: ST11 at the end of the line.

.. note:

To avoid sticky situations with casing and quoting in different dialects this rule uses case-insensitive comparison. That means if you have two tables with the same name, but different cases (and you're really sure that's a good idea!), then this rule may not detect if one of them is unused.

    **Groups**: `all`, `structure`

ST12: structure.consecutive_semicolons

Consecutive semicolons detected.

PropertyValue
Namestructure.consecutive_semicolons
Groupsall, structure
Auto-fixableYes ✅

This rule flags runs of two or more semicolons (optionally with intervening whitespace/newlines) which usually indicate an empty statement or an accidental duplicate terminator.

Anti-pattern

sql
SELECT 1;;
;;SELECT 2;

Best practice

Collapse duplicate semicolons unless intentionally separating batches.

sql
SELECT 1;
SELECT 2;

Released under the MIT License.