SQL 스타일 가이드
개요
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