Background
- Simon Holywell이라는 대단한 스펙의 금융 쪽 개발자가 있는데 이 분이 정리한 [SQL Style Guide]가 있다
- 위 가이드는 Joe Celko’s SQL Programming Style를 바라보고 있음. 이 책에 나온 내용과 호환되는 코딩 스타일이라고 함
- 코드는 오래전부터 전해져 내러오는 것들이 많고, 보통 이런 것들을 레거시(코드 중 쿼리 한정)라고 한다
- 레거시 자체는 긍/부정 모든 의미가 내포되어 있다
- 내가 생각할 때 안 좋은 레거시는 가독성이 떨어지는 쿼리다
- 그럼 대우를 따져보면, 가독성이 좋은 쿼리는 선한 레거시가 될 수 있다
- 어떻게 쿼리를 해야 좋다고 할 수 있는걸까?
- 에 대한 답을 현재 회사에선 Simon Holywell의 가이드 문서를 참조하고 있다.
- 사실 일 잘하시는 한 분이 이런 문화를 구축하려 했다. 지금은 흐지부지 됐다. 조직 차원에서 이런 문화가 잡힌 것은 아니다.
- 나도 이걸 정리하면서 전파하려고 했으나 팀에서 조차 반대 의견에 부딪혔기 때문에.. 마찬가지로 흐지부지
- 어쩌면 레거시는 기술이 아닌 사람 자체가 아닐까라는 오만한 생각도 들었다.
- 개발자가 아니라 분석가라서 그렇게까지 코딩 스타일을 통일시킬 필요 없다는 시각도 물론 있을 수 있다. (심지어 예전의 나)
- 근데 스타일 맞추면 의외로 일하기 정말 편하다. 시야도 확장되는 느낌
- 코딩도 결국 또 하나의 문서라 가독성과 같은 UX를 고려하는 것이 좋다고 생각한다.
White space
예약어는 right aligned, value는 left aligned 시켜야 함
- 가장 가독성이 뛰어난 방식(이라 생각함)
- 대신 작성하기는 다소 번거로움. 예약어마다 몇 칸 띄울지가 달라 탭이 아니라 스페이스로 작업해야 하는 번거로움
- 어차피 자주 쓰는 예약어가 정해져 있다보니 익숙해지면 괜찮음
SELECT c.date
FROM pubg_mart.cash_mtx AS c
WHERE c.date = '2023-08-21'
- 중간에 일렬로 비워져 있는 곳을 river라고 함
- 두 개의 예약어를 사용해야 하는 경우, 첫번째 예약어에 맞춤. 두번째 예약어는 other side of river로 넘김
SELECT date,
COUNT(*) AS log_count
FROM pubg_mart.cash_mtx
GROUP BY date
언제 newlines / vertical space를 하는지?
- 예약어마다
- 쉼표 후에
- 보통 SELECT 할 때 한 줄에 한 column을 넣었었는데, 굳이 이렇게 할 필요 없고, logical group으로 구분지어 줄넘기면 됨
- 줄넘기고 쉼표를 river에 위치시키면서 오른쪽 정렬시키기도 함 (펍지에서만?)
SELECT date
,platform
FROM pubg_mart.cash_mtx
- 개인적으론 logical group이란게 주관적인 기준이 될 수 있어 모든 column을 줄넘기는 것이 좋을 것 같기도 함
- 필자도 [Comma positioning]에서 언급하긴 했는데, 쉼표를 다음 줄에 붙이는건 상당히 hideous, weird 하다는 입장
- 그나마 comment out을 위해서 이렇게 하는 경우를 봐왔지만 이마저도 한계가 있기 때문에 comma seperated list는 term 다음에 comma를 위치시키야 한다고 주장함
- 이건 어디까지나 ‘가이드’라서.. 고민되네. 코더끼리 모여서 합의한 다음 펍지만의 룰은 만드는게 맞겠지
- large chunks of code 후에
Indentaion
Joins
- join은 한 번 들여쓰고 오른쪽 정렬시키면 됨
- join을 두 번 하는 경우는? 지양. 차라리 with 구문으로 새로 파는게
SELECT c.date
m.product_type
FROM pubg_bi.pubg_economy_sales_report_daily AS c
LEFT JOIN pubg_meta.meta_all_sales AS m
ON c.product_id = m.product_id
AND c.sales_id = m.sales_id
Subqueries
- 똑같이 한 번 들여쓰는데, 키워드는 왼쪽 정렬, 나머지는 오른쪽 정렬
SELECT A.device,
A.platform
FROM (SELECT device,
platform
FROM pubg_mart.gcoin_master
WHERE date = '{target_date}'
AND device = '{device}') AS A
- 데이터브릭스의 주피터노트북으로 작업할 때, 여러 코드를 한번에 indent 해야 하는 경우가 있음
- sql query의 경우 command + } 로 indent하면 기껏 맞춰놓은 여백이 뭉개짐
- 해결: 뭉개진 것 하나하나 맞추거나, indent 자체를 모든 줄마다 스페이스 4번으로 실행하거나
SELECT date, status, COUNT(*)
FROM pubg_mart.steam_mtx_report
GROUP BY date, status
SELECT date, status, COUNT(*)
FROM pubg_mart.steam_mtx_report
GROUP BY date, status
기타
- WITH 구문 관련해선 따로 언급 없음. 나라면?
CASE 구문 작성 방법
- CASE, WHEN, ELSE, END로 줄 구분하고, 오른쪽 정렬
SELECT *,
CASE country_os
WHEN 'CN' THEN 'CN'
ELSE country_ip
END AS country
FROM df_user_ready
WHERE rn = 1
Subquery VS Common Table Expression (CTE)
- 서브쿼리: 하나의 쿼리 안에 다른 쿼리를 포함시키는 것
- 주 쿼리 내에 포함된 작은 쿼리
- 주로 SELECT 문의 WHERE 절, FROM 절, HAVING 절에서 사용됨
- CTE: 임시로 결과 집합을 정의하는 것. 이름이 지정된 서브쿼리라고 볼 수 있음
- 주로 긴 쿼리를 더 작은 논리적 단계로 나누기 위해 사용됨
- WITH 문을 사용해서 CTE를 정의하고, 이후 주 쿼리에서 해당 CTE를 참조해서 사용
- 서브쿼리를 반복적으로 사용해야 하는 경우에도 유용함
필자는 SQL이나 DB structure에선 OOP를 고려하면 안된다고 함
- OOP는 코드를 객체 단위로 구조화하여 개발하는 방법론
- SQL에 OOP를 적용하는 방법은 ORM, stored procedure, trigger 등이 있음
- 가독성은 떨어지고 복잡성만 높아질 수 있어 상황에 따라 적용해야 함
Uniform Suffixes: 컬럼명에 아래와 같은 접미어를 붙여서 이해를 도울 수 있음
- _id: a unique identifier such as a column that is a primary key.
- _status: flag value or some other status of any type such as publication_status.
- _total: the total or sum of a collection of values.
- _num: denotes the field contains any kind of number.
- _name: signifies a name such as first_name.
- _seq: contains a contiguous sequence of values.
- _date: denotes a column that contains the date of something.
- _tally: a count.
- _size: the size of something such as a file size or clothing.
- _addr: an address for the record could be physical or intangible such as ip_addr.
실제 적용 예시
- 내가 짰던 쿼리에 이 스타일 적용해봤다
- 이전, 이후 비교하는 테이블로 작성해보고 싶었는데, 마크다운 테이블 내에 코드 스니펫 포함시키면 깨지는 것을 처음 알았다.
이전
SELECT
date
,sales.platform
,sales.product_id
,UPPER(sales.country_code) as country_code
,meta.product_type
,meta.product_name
,sales.gross_unit_sold
,sales.gross_sales
FROM
pubg_bi.pubg_economy_sales_report_daily as sales
LEFT JOIN
pubg_meta.meta_all_sales as meta
ON sales.product_id = meta.product_id
AND sales.platform = meta.platform
AND sales.sales_id = meta.sales_id
WHERE
date between "{start_date}" and "{end_date}"
AND sales.product_id != '3rd_party'
이후
SELECT date,
s.platform, s.product_id,
UPPER(s.country_code) AS country_code,
meta.product_type, meta.product_name,
s.gross_unit_sold, s.gross_sales
FROM pubg_bi.pubg_economy_sales_report_daily AS s
LEFT JOIN pubg_meta.meta_all_sales AS m
ON s.product_id = m.product_id
AND s.platform = m.platform
AND s.sales_id = m.sales_id
WHERE date BETWEEN '{start_date}' AND '{end_date}'
AND s.product_id != '3rd_party'
이전
WITH record_ready (
SELECT *
FROM log_bro_record_live.record
WHERE platform = 'pc'
AND event_date = '{target_date}'
), record_prep (
SELECT DISTINCT
event_date
,SPLIT(file_id, 'record.')[1] AS session_id
,props.Rank
,CAST(props.TimeSurvived AS FLOAT) AS TimeSurvived
,props.User
,region_server AS server
,play_mode
FROM record_ready
WHERE CAST(props.TimeSurvived AS FLOAT) <= 2100
)
SELECT event_date
,server
,play_mode
,session_id
,SUM(TimeSurvived) AS TimeSurvived
FROM record_prep
GROUP BY event_date, server, play_mode, session_id
이후
WITH record_ready (
SELECT *
FROM log_bro_record_live.record
WHERE platform = 'pc'
AND event_date = '{target_date}'
), record_prep (
SELECT DISTINCT event_date,
SPLIT(file_id, 'record.')[1] AS session_id,
props.Rank,
CAST(props.TimeSurvived AS FLOAT) AS TimeSurvived,
props.User, region_server AS server, play_mode
FROM record_ready
WHERE CAST(props.TimeSurvived AS FLOAT) <= 2100
)
SELECT event_date, server, play_mode, session_id,
SUM(TimeSurvived) AS TimeSurvived
FROM record_prep
GROUP BY event_date, server, play_mode, session_id