ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SQL 스타일 가이드
    DB 2022. 1. 16. 17:27

    개요

    SQL 스타일을 통일시켜 직관적인 SQL 코드를 짜고 읽을 수 있도록 합니다.

    mordern sql style guide를 기반으로 깃랩, 모질라 문서를 참고하여 정리했습니다.

    ‼️가 붙어 있는 부분은 lint가 적용되지 않아 개개인이 주의해야 되는 부분입니다.

    Case

    네이밍에는 문자, 숫자, _ 만 사용합니다.

    네이밍의 시작은 문자로 시작하고 _로 끝나지 않습니다.

    테이블 명의 최대 길이는 20자입니다.

    한 줄에 표현할 수 있는 쿼리의 길이는 100자입니다.

    탭은 사용하지 않고 공백만 사용합니다.

    • jetbrains 제품을 사용한다면 탭을 공백으로 변환하도록 합니다.

    쿼리 내 들여쓰기는 4칸 들여쓰기로 합니다.

    필드 네이밍

    필드 이름은 모두 소문자로 제공하며 스네이크 케이스를 지향합니다. (카멜케이스 X)

    -- GOOD
    SELECT item_no
    
    -- BAD
    SELECT ItemId

    ‼️ keyword, type, function

    키워드, 타입, 함수들은 모두 대문자로 제공합니다.

    SELECT item_id, SUBSTR(description, 0, 6) AS item_description, 
    FROM item 
    WHERE margin_rate > 50%

    function(count, substr 등)과 같은 함수는 jetbrain에서 linter로 지원을 하지 않음

    ‼️ 필드의 의미 명시

    id, name, type과 같이 SQL에 예약어이거나 애매한 필드일 경우, 테이블 명이나 해당 컬럼의 의미를 접두사로 붙여 명확하게 표현합니다.

    -- Good
      SELECT
            id    AS account_id,
            name  AS account_name,
            type  AS account_type,
            ...
    
      -- Bad
      SELECT
            id,
            name,
            type,
            ...

    ‼️ boolean type

    boolean 타입일 경우, (true/false) 접두사는 반드시 has_ , is_ 또는 does_ 와 같이 시작해야 합니다.

    • 정확하게 is, 조동사(can, does), has 로 시작합니다.
    -- Good
      SELECT
        deleted AS is_deleted,
        sla     AS has_sla
      FROM table
    
      -- Bad
      SELECT
        deleted,
        sla
      FROM table

    세미콜론은 쿼리의 마지막 줄에 배치

    세미콜론을 마지막 줄에 추가하면 쿼리 수정이 수월합니다.

    SELECT 1
    ;

    Queries

    공통

    절의 첫 단어 정렬

    한 줄의 첫 단어는 왼쪽 정렬을 합니다.

    -- GOOD
    SELECT *
    FROM item
    
    -- BAD
    SELECT *
      FROM item

    절의 요소 배치

    키워드 또는 문장은 항상 다음 새로운 줄에서 시작합니다.

    -- GOOD
    SELECT
        max(q4)
    FROM
        my_staff_size;
    WHERE 
            year BETWEEN 1996 AND 2017
    
    -- BAD
    SELECT max(q4)
    FROM my_staff_size;
    WHERE year BETWEEN 1996 AND 2017

    콤마 배치

    콤마는 항상 왼쪽에 배치합니다.

    -- GOOD
    SELECT
        x - y    AS z -- the delta between X and Y
      , p * q    AS pq -- the pq
      , tau * mu AS intermediate_result -- another expression
      , something_else -- something else again
    FROM
        third_table;
    
    -- BAD
    SELECT
        x - y    AS z,
        p * q    AS pq,
        tau * mu AS intermediate_result,
        something_else
    FROM
        third_table;

    짧은 문장은 생략

    subquery일 경우에만 짧은 문장이면 위의 규칙을 어기고 1줄로 표현합니다.

    -- GOOD
    SELECT
            (SELECT 1 FROM dual WHERE 1 IS NOT NULL) x
    FROM
            (SELECT 1 FROM dual WHERE 1 IS NOT NULL) y
    
    -- BAD
    SELECT
        (SELECT
             1
         FROM
             dual
         WHERE
             1 IS NOT NULL) x

    섹션 요소 정렬

    들여쓰기 (4칸)의 규칙을 무시하고 상위 필드와 동일한 라인에 맞도록 정렬합니다.

    -- GOOD
    SELECT
        id
      , name
      , note
    
    -- BAD
    SELECT
        id
        , name
        , note

    ‼️ <> 보다 !=를 사용

    !=가 <>보다 프로그래밍 언어에서 더 일반적이고 같지 않음 이라고 직관적으로 이해할 수 있습니다.

    -- GOOD
    item != 'text'
    
    -- BAD
    item <> 'text'

    UPDATE문

    = 정렬

    SET에 포함되는 = 는 동일한 위치에 있도록 정렬합니다.

    -- GOOD
    UPDATE my_staff_size
    SET
        id              = id + 1
      , name            = name || '.duplicate'
      , note            = note || ' (another one)'
      , additional_note = 'Processed.'
      , status          = -1
    
    -- BAD
    UPDATE my_staff_size
    SET
        id = id + 1
      , name = name || '.duplicate'
      , note = note || ' (another one)'
      , additional_note = 'Processed.'
      , status = -1

    WITH절

    with문은 쿼리의 상단에 배치합니다.

    with문의 이름은 명확하고 간결하게 짓습니다.

    = table 별칭과 동일

    • replace_sfdc_account_id_with_master_record_id 와 같은 이름은 피하도록 합니다.

    혼란을 줄 수 있거나 헷갈릴만한 with문은 주석을 추가합니다.

    WITH events AS ( -- think of these select statements as your import statements.
        ...
    )
      , filtered_events AS ( -- CTE comments go here
      ...
    )
    
    SELECT *
    FROM 
            filtered_events

    요소 배치

    WITH 절 다음에 나오는 이름은 새로운 줄에 쓰지 않고 붙여씁니다.

    -- GOOD
    WITH something AS (
    ...
    )
    
    -- BAD
    WITH
        something AS (
    ...
    )

    WITH 절 내의 쿼리는 새로운 줄에서 4칸 들여쓰기를 적용하여 진행합니다.

    --GOOD
    WITH something AS (
        SELECT
            26 AS x
        FROM
                (SELECT dep_id FROM department WHERE actual IS NOT NULL)
        WHERE
            1 IS NOT NULL
    )
    
    -- BAD
    WITH something AS (SELECT
                           26 AS x
                       FROM
                               (SELECT dep_id FROM department WHERE actual IS NOT NULL)
                       WHERE
                           1 IS NOT NULL
    )

    여러 with 절

    with 절이 여러개가 등록될 경우, 공통의 섹션 요소 정렬 과 같이 정렬합니다.

    -- GOOD
    WITH something AS (
        SELECT
            26 AS x
        FROM
                (SELECT dep_id FROM department WHERE actual IS NOT NULL)
        WHERE
            1 IS NOT NULL
    )
        , another AS (
        SELECT
            26 AS x
        FROM
            dual
        WHERE
            1 IS NOT NULL
    )
    
    -- BAD
    WITH something AS (
        SELECT
            26 AS x
        FROM
                (SELECT dep_id FROM department WHERE actual IS NOT NULL)
        WHERE
            1 IS NOT NULL
    )
    , another AS (
        SELECT
            26 AS x
        FROM
            dual
        WHERE
            1 IS NOT NULL
    )

    ‼️ 중첩쿼리 지양

    중첩 쿼리 (inner query 등등)은 가독성을 떨어트리기 때문에 사용을 지양합니다.

    -- Good
    WITH sample AS (
      SELECT
        client_id,
        submission_date
      FROM
        main_summary
      WHERE
        sample_id = '42'
    )
    
    SELECT 
            *
    FROM sample
    LIMIT 10
    
    -- Bad
    SELECT *
    FROM (
      SELECT
        client_id,
        submission_date
      FROM
        main_summary
      WHERE
        sample_id = '42'
    )
    LIMIT 10

    SELECT문

    요소 배치

    공통의 절의 요소 배치 속성을 그대로 적용하지만 *, distinct만 예외로 한 줄로 표현합니다.

    -- GOOD
    SELECT
        a.*
    FROM
        small_table_1 a
    
    -- GOOD
    SELECT *
    FROM
        small_table_1 a
    
    -- GOOD
    SELECT DISTINCT 
            d.dep_id
      , p.per_id
      , count(*)      AS per_cnt
      , max(m.actual) AS has_actual_template
    FROM
    
    -- BAD
    SELECT
          *
    FROM
        small_table_1 a

    별칭 사용

    별칭에는 항상 AS 를 붙입니다.

    -- GOOD
    SELECT
            (SELECT 1 FROM dual WHERE 1 IS NOT NULL) AS x
    
    -- BAD
    SELECT
            (SELECT 1 FROM dual WHERE 1 IS NOT NULL) x

    별칭 정렬

    별칭은 항상 동등한 위치에 정렬합니다.

    -- GOOD
    SELECT
        x - y    AS z
      , p * q    AS pq
      , tau * mu AS intermediate_result
    
    -- BAD
    SELECT
        x - y AS z
      , p * q AS pq
      , tau * mu AS intermediate_result

    FROM절

    요소 배치

    공통의 절의 요소배치 속성과 동일합니다.

    -- GOOD
    SELECT *
    FROM
        item
        INNER JOIN options
    -- BAD
    SELECT *
    FROM item INNER JOIN options
    

    join indent

    join은 from 절의 첫 테이블과 동일한 인덴트를 갖습니다.

    -- GOOD
    SELECT *
    FROM
        item
        INNER JOIN options
    -- BAD
    SELECT *
    FROM 
        item 
                    INNER JOIN options
    

    별칭 사용

    별칭에는 항상 AS 를 붙입니다.

    -- GOOD
    SELECT *
    FROM
        item               AS i
        INNER JOIN options AS o
    
    -- BAD
    -- GOOD
    SELECT *
    FROM
        item               i
        INNER JOIN options o

    별칭 정렬

    별칭은 항상 동등한 위치에 정렬합니다.

    -- GOOD
    SELECT *
    FROM
        item               AS i
        INNER JOIN options AS o
    
    -- BAD
    SELECT *
    FROM
        item AS i
        INNER JOIN options AS o

    ‼️ JOIN 명시

    조인을 사용할 땐, 어떤 조인인지 명시하도록 합니다.

    
    -- GOOD
    SELECT
        submission_date
      , experiment.key   AS experiment_id
      , experiment.value AS experiment_branch
      , count(*)         AS count
    FROM
        telemetry.clients_daily
        CROSS JOIN
            UNNEST(experiments.key_value) AS experiment
    
    -- BAD
    SELECT *
    FROM
        item         AS i
        JOIN options AS o
    
    -- BAD
    SELECT
        submission_date
      , experiment.key   AS experiment_id
      , experiment.value AS experiment_branch
      , count(*)         AS count
    FROM
        clients_daily
      , UNNEST(experiments.key_value) AS experiment -- Implicit JOIN

    ‼️ JOIN 후 네이밍

    테이블을 조인할 때, 접두사로 테이블 명을 명시해 줍니다.

    -- GOOD
    SELECT
        sfdc_account.account_id  AS sfdc_account_id
      , zuora_account.account_id AS zuora_account_id
    FROM
        sfdc_account
        LEFT JOIN zuora_account
    
    -- BAD
    SELECT
        sfdc_account.account_id
      , zuora_account.account_id AS zuora_id
    FROM
        sfdc_account
        LEFT JOIN zuora_account

    테이블을 조인하고 양 테이블의 컬럼들을 참조할 때 alias 대신 전체 테이블 이름을 참조하는 것이 좋습니다.

    • 테이블 이름이 20자가 넘는다면 해당 테이블을 잘 표현하도록 영작을 잘해봅시다.
    -- Good
      SELECT
        budget_forecast_cogs_opex.account_id,
        date_details.fiscal_year,
        date_details.fiscal_quarter,
        date_details.fiscal_quarter_name,
        cost_category.cost_category_level_1,
        cost_category.cost_category_level_2
      FROM 
            budget_forecast_cogs_opex
          LEFT JOIN date_details
          ON date_details.first_day_of_month = budget_forecast_cogs_opex.accounting_period
          LEFT JOIN cost_category
          ON budget_forecast_cogs_opex.unique_account_name = cost_category.unique_account_name
    
      -- 괜찮긴 하지만 별칭이 함축되어 있어 의미가 모호함. 별칭은 해당 테이블의 성격을 잘 나타내도록 지어야한다.
      SELECT
        bfcopex.account_id,
        -- 15 more columns
        date_details.fiscal_year,
        date_details.fiscal_quarter,
        date_details.fiscal_quarter_name,
        cost_category.cost_category_level_1,
        cost_category.cost_category_level_2
      FROM 
            budget_forecast_cogs_opex AS bfcopex
          LEFT JOIN date_details
          ON date_details.first_day_of_month = bfcopex.accounting_period
          LEFT JOIN cost_category
        ON bfcopex.unique_account_name = cost_category.unique_account_name
    
      -- Bad
      SELECT
        a.*,
        -- 15 more columns
        b.fiscal_year,
        b.fiscal_quarter,
        b.fiscal_quarter_name,
        c.cost_category_level_1,
        c.cost_category_level_2
      FROM budget_forecast_cogs_opex a
      LEFT JOIN date_details b
       ON b.first_day_of_month = a.accounting_period
      LEFT JOIN cost_category c
       ON b.unique_account_name = c.unique_account_name

    ON절 정렬

    on절은 join에서 4칸 들여쓰기를 하며 항상 새로운 줄에 작성합니다.

    SELECT *
    FROM
        small_table_1                  a
        JOIN medium_table_4            d
                USING (id)
        LEFT OUTER JOIN medium_table_5 e
                  ON d.x = e.x
        LEFT OUTER JOIN medium_table_6 f
                ON e.y = f.y
        INNER JOIN medium_table_7      g
                ON f.z = g.z;

    ‼️ USING 사용 지양

    using은 snowflake에서 부정확한 결과를 생성하므로 사용을 지양합니다.

    -- BETTER
    SELECT *
    FROM
        small_table_1
        JOIN medium_table_4
        ON small_table_1.id = medium_table_4.id
    
    -- OK
    SELECT *
    FROM
        small_table_1
        JOIN medium_table_4
        USING (id)

    WHERE와 HAVING절

    요소 배치

    공통의 절의 요소배치 속성과 동일합니다.

    -- GOOD
    WHERE
         1 IS NOT NULL
    
    -- BAD
    WHERE 1 IS NOT NULL

    AND/OR 위치

    and와 or는 콤마와 같이 항상 왼쪽에 배치합니다.

    -- GOOD
    WHERE
          year BETWEEN 1996 AND 2017
      AND (q1 > 0 OR q4 = 20)
      AND actual IS NOT NULL;
    
    --BAD
    WHERE
        year BETWEEN 1996 AND 2017 AND
        (q1 > 0 OR q4 = 20) AND
        actual IS NOT NULL;

    ‼️ AND/OR 혼합 사용

    and와 or를 섞어서 사용할 때, 순서에 의존하지 말고 괄호로 묶어줍니다.

    SELECT *
    FROM
        production.product AS prd
    WHERE
         (prd.weight > 10.0 AND color IN ('Red','Silver'))
      OR color IS NULL

    GROUP BY와 ORDER BY절

    숫자 사용 금지

    숫자로 사용할 시, ALL(*)과 같은 경우에 어떤 컬럼인지 알 수가 없으므로 필드를 명확하게 명시합니다.

    만약, select 절에 복잡한 수식이 있다면 WITH절로 사전에 처리하도록 합니다.

    -- GOOD
    WITH result AS (
        SELECT
            submission_date
          , normalized_channel IN ('nightly', 'aurora', 'beta') AS is_prerelease
          , COUNT(*)                                            AS count
        FROM
            telemetry.clients_daily
        WHERE
            submission_date > '20190701'
    
    )
    
    SELECT
        submission_date
      , normalized_channel IN ('nightly', 'aurora', 'beta') AS is_prerelease
      , COUNT(*)                                            AS count
    FROM
        result
    GROUP BY
        submission_date, is_prerelease
    ORDER BY
        submission_date
    
    -- Bad
    SELECT
      submission_date,
      normalized_channel IN ('nightly', 'aurora', 'beta') AS is_prerelease,
      count(*) AS count
    FROM
      telemetry.clients_daily
    WHERE
      submission_date > '20190701'
    GROUP BY 1, 2 -- Implicit grouping avoids repeating expressions
    ORDER BY 1

    요소 배치

    공통의 절의 요소 배치속성과 동일합니다.

    -- GOOD
    GROUP BY
        1, 2
    
    -- BAD
    GROUP BY 1, 2

    요소 정렬 기준

    요소는 100자가 넘지 않으면 한 줄로 표현합니다.

    -- GOOD
    GROUP BY
        1, 2
    
    -- BAD
    GROUP BY 
            1
      , 2

    Subquery (괄호)

    여는 괄호는 해당 줄의 가장 마지막에 위치해야 합니다.

    괄호 안의 쿼리는 4칸 들여쓰기여야 합니다.

    닫는 괄호는 해당 줄의 가장 첫 번째에 위치해야 합니다.

    -- GOOD
    WITH o AS (
        SELECT id AS org_id
             , name
             , description
             , another_field
        FROM all_organizations
             NATURAL JOIN all_names
        WHERE status >= 1
          AND item_no IN (
            SELECT no
            FROM item
            WHERE item_no IN (1, 2, 3, 4, 5)
        )
    )
    
    -- GOOD
    SELECT (
               SELECT 1
               FROM dual
               WHERE 1 IS NOT NULL
           ) AS x
    FROM (
             SELECT 1
             FROM dual
             WHERE 1 IS NOT NULL
         ) y
    WHERE (
        SELECT TRUE
        FROM dual
        WHERE 1 IS NOT NULL
    )

    표현식

    괄호와 콤마

    ( 괄호 앞에 띄어쓰기

    괄호 앞에는 띄어쓰기를 추가해 줍니다.

    -- GOOD
    SELECT (item)
    -- BAD
    SELECT(item)

    콤마 뒤에 띄어쓰기

    콤마 뒤에는 띄어쓰기를 추가해 줍니다.

    -- GOOD
    SELECT (cell_1, cell_2, cell_3, cell_4)
    -- BAD
    SELECT (cell_1,cell_2,cell_3,cell_4)

    연산자

    연산자 앞,뒤 띄어쓰기

    연산자 앞, 뒤는 띄어쓰기를 추가해 줍니다.

    -- GOOD
    SELECT (1 + 2 + 3 + 4)
    
    -- BAD
    SELECT (1+2+3+4)
    

    CASE문

    WHEN 정렬

    WHEN은 CASE문 다음 줄에 표현하며 4칸 들여쓰기를 적용합니다.

    -- GOOD
    , CASE
          WHEN kind = 'O' AND item = 1
    -- BAD
    , CASE
      WHEN kind = 'O'

    THEN 정렬

    THEN은 WHEN 다음 줄에 표현하면 4칸 들여쓰기를 적용합니다.

    -- GOOD
    , CASE
          WHEN kind = 'O' AND item = 1
                        THEN 'organization'
    -- BAD
    , CASE
      WHEN kind = 'O' THEN 'organization'

    ELSE 정렬

    ELSE는 WHEN과 동일한 위치에 정렬합니다.

    -- GOOD
    WHEN kind = ''
            THEN 'alias'
    ELSE 'unknown subject'
    -- BAD
    WHEN kind = ''
            THEN 'alias'
            ELSE 'unknown subject'

    END 정렬

    END는 CASE와 동일한 위치에 정렬합니다.

    -- GOOD
    CASE
            WHEN kind = 'O'
                THEN 'organization'
            ELSE 'unknown subject'
    END
    
    -- BAD
    CASE
            WHEN kind = 'O'
                THEN 'organization'
            ELSE 'unknown subject' END

    짧은 case문은 생략

    만약 case 문이 짧다면 위의 규칙을 생략합니다.

    SELECT
        id
      , name
      , kind
      , CASE WHEN id < 0 THEN 'C' ELSE NULL END AS was_converted
      , CASE
            WHEN kind = 'O'
                THEN 'organization'
            WHEN kind = 'P'
                THEN 'person'
            WHEN kind = 'XX'
                THEN
                    'something gone wrong here, looks like' + 'this code should be unreachable'
            WHEN kind = ''
                THEN 'alias'
            ELSE 'unknown subject'
        END                                     AS kind_name_1

    ‼️ CASE문 단순화

    단순화 할 수 있으면 단순화 하도록 합니다.

    -- OK
      CASE
        WHEN field_id = 1 THEN 'date'
        WHEN field_id = 2 THEN 'integer'
        WHEN field_id = 3 THEN 'currency'
        WHEN field_id = 4 THEN 'boolean'
        WHEN field_id = 5 THEN 'variant'
        WHEN field_id = 6 THEN 'text'
      END AS field_type
    
      -- Better
      CASE field_id
        WHEN 1 THEN 'date'
        WHEN 2 THEN 'integer'
        WHEN 3 THEN 'currency'
        WHEN 4 THEN 'boolean'
        WHEN 5 THEN 'variant'
        WHEN 6 THEN 'text'
      END AS field_type

    ‼️ Window Function

    각 절마다 하나씩 여러줄로 분할하여 정렬합니다.

    SELECT
        post_no
      , ROW_NUMBER()
        OVER (
            PARTITION BY p.productline , LEFT(p.productnumber, 2)
            ORDER BY RIGHT(p.productnumber, 4) DESC
            ) AS sequencenum
      , FIRST_VALUE(CONCAT('https://img.co.kr', image_url))
        OVER (
            PARTITION BY cart_tb.user_no, item.item_no
            ORDER BY item_image_no
            ) AS image_url_first
    FROM
        production.product

    문서

    Mozilla Data Documentation

    SQL Style Guide

    Modern SQL Style Guide

    SQL Style Guide (ko-KR)

    댓글