dev-writeup-2024 / october

개발 1주 1글 스터디
3 stars 0 forks source link

[10-20] K-SQL 프로젝트 1 #2

Open mingnuj opened 1 month ago

mingnuj commented 1 month ago

저는 회사에서 DB를 만들고 있습니다. 이렇게 얘기하면 꼭 듣는 질문이 "아 데이터베이스 다룬다고?"인데, 그게 아니고 OLAP를 만들고 있습니다.

OLAP 중에 SQL parsing, optimization을 담당하고 있고 그 외에도 이런저런 다른 일도 합니다. 개발을 3년 정도 하니 저도 하고 싶은 프로젝트가 생깁니다. 이거 parser를 한국어로 만들면 한국어 SQL 만들 수 있겠다.

그래서 이번 스터디는 K-SQL, 즉 Korean SQL을 만들겁니다.

SQL Tokenizer는 어떻게 만드는지 대강 알고 있으니 문법을 만드는 게 더 중요합니다. 전 개인적으로 ANSI보다는 PostgreSQL을 선호하는데요, ANSI는 유료인 까닭도 있습니다. 그럼 PostgreSQL SELECT 문 까서 한국어로 번역하는 것부터 시작합니다.

PostgreSQL SQL Commands

PostgreSQL17: SELECT SQL Commands

[ WITH [ RECURSIVE ] with_query [, ...] ]
SELECT [ ALL | DISTINCT [ ON ( expression [, ...] ) ] ]
    [ { * | expression [ [ AS ] output_name ] } [, ...] ]
    [ FROM from_item [, ...] ]
    [ WHERE condition ]
    [ GROUP BY [ ALL | DISTINCT ] grouping_element [, ...] ]
    [ HAVING condition ]
    [ WINDOW window_name AS ( window_definition ) [, ...] ]
    [ { UNION | INTERSECT | EXCEPT } [ ALL | DISTINCT ] select ]
    [ ORDER BY expression [ ASC | DESC | USING operator ] [ NULLS { FIRST | LAST } ] [, ...] ]
    [ LIMIT { count | ALL } ]
    [ OFFSET start [ ROW | ROWS ] ]
    [ FETCH { FIRST | NEXT } [ count ] { ROW | ROWS } { ONLY | WITH TIES } ]
    [ FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE } [ OF from_reference [, ...] ] [ NOWAIT | SKIP LOCKED ] [...] ]

where from_item can be one of:

    [ ONLY ] table_name [ * ] [ [ AS ] alias [ ( column_alias [, ...] ) ] ]
                [ TABLESAMPLE sampling_method ( argument [, ...] ) [ REPEATABLE ( seed ) ] ]
    [ LATERAL ] ( select ) [ [ AS ] alias [ ( column_alias [, ...] ) ] ]
    with_query_name [ [ AS ] alias [ ( column_alias [, ...] ) ] ]
    [ LATERAL ] function_name ( [ argument [, ...] ] )
                [ WITH ORDINALITY ] [ [ AS ] alias [ ( column_alias [, ...] ) ] ]
    [ LATERAL ] function_name ( [ argument [, ...] ] ) [ AS ] alias ( column_definition [, ...] )
    [ LATERAL ] function_name ( [ argument [, ...] ] ) AS ( column_definition [, ...] )
    [ LATERAL ] ROWS FROM( function_name ( [ argument [, ...] ] ) [ AS ( column_definition [, ...] ) ] [, ...] )
                [ WITH ORDINALITY ] [ [ AS ] alias [ ( column_alias [, ...] ) ] ]
    from_item join_type from_item { ON join_condition | USING ( join_column [, ...] ) [ AS join_using_alias ] }
    from_item NATURAL join_type from_item
    from_item CROSS JOIN from_item

and grouping_element can be one of:

    ( )
    expression
    ( expression [, ...] )
    ROLLUP ( { expression | ( expression [, ...] ) } [, ...] )
    CUBE ( { expression | ( expression [, ...] ) } [, ...] )
    GROUPING SETS ( grouping_element [, ...] )

and with_query is:

    with_query_name [ ( column_name [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( select | values | insert | update | delete | merge )
        [ SEARCH { BREADTH | DEPTH } FIRST BY column_name [, ...] SET search_seq_col_name ]
        [ CYCLE column_name [, ...] SET cycle_mark_col_name [ TO cycle_mark_value DEFAULT cycle_mark_default ] USING cycle_path_col_name ]

TABLE [ ONLY ] table_name [ * ]

너무 길어 보이니 SELECT 문만 따로 떼서 봅니다.

SELECT [ ALL | DISTINCT [ ON ( expression [, ...] ) ] ]
    [ { * | expression [ [ AS ] output_name ] } [, ...] ]
    [ FROM from_item [, ...] ]
    [ WHERE condition ]
    [ GROUP BY [ ALL | DISTINCT ] grouping_element [, ...] ]
    [ HAVING condition ]
    [ WINDOW window_name AS ( window_definition ) [, ...] ]
    [ { UNION | INTERSECT | EXCEPT } [ ALL | DISTINCT ] select ]
    [ ORDER BY expression [ ASC | DESC | USING operator ] [ NULLS { FIRST | LAST } ] [, ...] ]
    [ LIMIT { count | ALL } ]
    [ OFFSET start [ ROW | ROWS ] ]
    [ FETCH { FIRST | NEXT } [ count ] { ROW | ROWS } { ONLY | WITH TIES } ]
    [ FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE } [ OF from_reference [, ...] ] [ NOWAIT | SKIP LOCKED ] [...] ]

훨씬 낫네요.

SQL에서 SELECT, ALL, DISTINCT 등의 요소는 하나의 키워드로 판별합니다. 이러한 키워드를 겹치지 않게 지정하는 것이 중요합니다. 예를 들어 ONWHERE-에로 애매하게 번역될 수 있으니 WHERE-에 위치한처럼 다른 방식으로 번역해줘야 합니다.

그럼 다음과 같은 모양이 됩니다. output_name, from_name과 같이 키워드가 아닌 것은 번역하지 않았습니다.

선택하다 [ 모두 | 별개로 [ -에 ( expression [, ...] ) ] ]
    [ { * | expression [ [ -로 ] output_name ] } [, ...] ]
    [ -에서 from_item [, ...] ]
    [ -에서 condition ]
    [ -로 묶음 [ 모두 | 별개로 ] grouping_element [, ...] ]
    [ -에 위치한 condition ]
    [ WINDOW window_name AS ( window_definition ) [, ...] ]
    [ { 합집합 | 교집합 | 차집합 } [ 모두 | 별개로 ] select ]
    [ -순서로 expression [ 오름차순 | 내림차순 | -를 사용해서 operator ] [ NULLS { 처음 | 마지막 } ] [, ...] ]
    [ -제한해서 { count | 모두 } ]
    [ 시작 위치 start [ 행 ] ]
    [ 가져오는 { 처음에 | 다음에 } [ count ] { 행 } { ONLY | WITH TIES } ]
    [ -동안 { 업데이트 | NO KEY UPDATE | SHARE | KEY SHARE } [ OF from_reference [, ...] ] [ NOWAIT | SKIP LOCKED ] [...] ]

등등.. window와 마지막 줄은 귀찮고 어떻게 번역할지 애매하네요. 이렇게 번역을 하면 어순이 달라서 이상해집니다. 예를 들어,

SELECT pencil FROM bag;
선택하다 pencil 에서 bag;

아주 이상하죠. 다음과 같이 어순이 바뀌어야 합니다.

bag 에서 pencil을 선택하다;

조금 익숙해진 모양입니다. 다른 키워드도 비슷한 방법으로 바꿔야 합니다. ... 갑자기 후회되네요. 그래도 일단 시작한 거 해 봅시다.

[ from_item [, ...] 에서] [ condition에 위치한 ]
    [ [ ( expression [, ...] )에 ] 모든 | 별개의 ]
    [ { * | expression [ [를] output_name [로서] ] } [, ...] 를/을 ]
    [ [ 모든 | 별개의 ] grouping_element [, ...] 로 묶어서 ]
    [ condition 에서 ]
    [ WINDOW window_name AS ( window_definition ) [, ...] ]
    [ [ 모든 | 별개의 ] select 이/가 { 함께 | 겹치는 | 빠진 } ]
    [ [ 오름차순인 | 내림차순인 | operator를 사용한 ] expression
    [ NULLS { 우선 | 마지막 } ] [, …] 순서로 ]
    [ { count | 모두 } 만큼 ]
    [ start [ 행을 ] 시작 위치로 ]
    [ { 처음 | 다음 } [ count ] { 행 } { 만 | 을 묶어서 } 가져와서 ]
선택한다.

그럼 실제로 예시를 들고 와서 바꿔봅니다.

SELECT 
    customer_id,
    EXTRACT(YEAR FROM sale_date) AS year,
    EXTRACT(MONTH FROM sale_date) AS month,
    SUM(sale_amount) AS total_sales,
    MAX(sale_amount) AS max_sale
FROM 
    sales
WHERE
    sale_date BETWEEN '2023-01-01' AND '2023-12-31'
GROUP BY 
    customer_id, 
    EXTRACT(YEAR FROM sale_date), 
    EXTRACT(MONTH FROM sale_date)
HAVING 
    SUM(sale_amount) > 5000
ORDER BY 
    year, month, sale_rank
LIMIT 5;

함수는 임의로 한국말로 바꿔봤습니다.

-- FROM
sales에서
-- WHERE
sale_date가 '2023-01-01'과 '2023-12-31' 사이에 위치한
-- expressions
customer_id,
sale_date에서 연도 추출 year,
sale_date에서 월 추출 month,
sales_amount 합계 total_sales,
sales_amount 최대값 max_sale 을
-- GROUP BY
customer_id,
sale_date에서 연도 추출,
sale_date에서 월 추출로 묶어서
-- HAVING: WHERE과 의미 상 같음
sales_amount 합계가 5000 초과에서 
-- ORDER BY
year, month, sale_rank 순서로
-- LIMIT
5 만큼
-- SELECT
선택한다.

생각보다 나쁘지 않은데요? 여기서 적지 않은 join이나 표현식들에 대해 규칙을 지정 하다 보면 끝이 없을 것 같네요.

그래도 일단 시작했으니 코드 구현도 해보려 합니다. 오늘은 이만!

k-young-passionate commented 1 month ago

글쓰기를 재밌는 프로젝트로 진행하시는군요 ㅎㅎ

확실히 어순에 맞게 매핑하려니 동사가 앞에 오는 영문과 동사가 뒤에오는 한글의 차이가 드러나네요...!

결과만 보면 납득이 쉽게 되지만, 어순을 맞추는 과정에서 많은 고민이 있으셨을거 같습니다.

다음 장도 기대하겠습니다!!