ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TIL] 스파르타) SQL (SELECT) 강의 2~5주차 수강
    TIL 2024. 4. 25. 17:03

    이번 2~5주차의 SQL 강의 정보 중 중요하다고 생각되는 부분들을 이해한 그대로 적어보았다.

     

    --

     

    학습 키워드: MySQL, query, select, order, group, if, case, subquery

     

    1. SQL query select 기본

    참고할 만한 이전 게시글:

     

    [240421] TIL - 스파르타) 웹개발 종합반 완강, MySQL Query SELECT 연습 01

    -- 학습 키워드: Javascript, module, MySQL, SELECT 1. Script module - script 태그의 type을 module로 지정하면 해당 스크립트가 가장 마지막에 로드된다고 한다. 어쩐지 저번에 firebase 데이터를 불러오는 스크립트

    donkim0122.tistory.com

    SELECT ANIMAL_ID, NAME, DATETIME from ANIMAL_INS
        ORDER by NAME, DATETIME desc

     - ORDER by: 쉼표( , )를 사용하면 여러개 column을 대상으로 정렬 할 수 있다고한다.

     - ORDER by 부분만 해석하면 'NAME 컬럼으로 우선 정렬하고 (기본값 asc여서 생략), NAME이 겹치는 데이터끼리는 DATETIME 컬럼을 기준으로 내림차순으로 정렬'한다는 뜻이다.

     

    select cuisine_type, min(price) as min_price, max(price) as max_price  from food_orders
    	group by cuisine_type
    	order by min_price desc

     - GROUP by: 뒤에 적힌 cuisine_type 이라는 column을 카테고리로 지정해서 데이터를 묶는 개념이라고 한다. 만약 이 줄을 빼먹으면 테이블 전체에서 cuisine_type 하나와 price가 min인 값, max인 값 하나가 담긴 row 한 줄만 받게된다. 하지만 group by로 cuisine_type을 카테고리 column으로 지정하면 테이블에서 cuisine_type이 겹치는 항목들 끼리의 min_price와 max_price를 한 row에 담은 데이터를 받게된다.

     

     

    2. SQL query 결과 가공하기, if, case

    select addr, replace(addr, '문곡리', '문가리') "바뀐주소" from food_orders
    	where addr like '%문곡리%'

     - replace(target_column, 'original', 'replaced'): target_column 에서 원래의 문자열을 다른 문자열로 교체해주는 작업이다. 다른 문자열 기능으로는 substring(target_column, index(1부터), count_from_index(생략하면 마지막까지))과 concat('붙일', '스트링', '쉼표로', '구분', substring(), '같은', '식도', '추가', '가능')이 있다

     - %: wildcard 개념인데, 앞에 붙으면 앞 쪽에 아무 문자열, 뒤에 붙으면 뒤 쪽에 아무 문자열, 앞 뒤로 붙으면 양쪽에 아무 문자열이라는 의미다.

     

    select substr(addr, 1, 2) "지역", cuisine_type, avg(price) "평균 금액" from food_orders
    	where addr like '서울%'	
    	group by 1, 2

     - group by에서 컬럼을 반복적으로 적어줄 필요 없이 select에서 적은 순서로 1부터 해서 골라줄 수 있다.

     - 여기서는 1번이 "지역" 컬럼, 2번이 cuisine_type 컬럼이 된다.

     

    select substring(if(email like '%gmail%', replace(email, 'gmail', '@gmail'), email), 10) "이메일 도메인",
           count(customer_id) "고객 수",
           avg(age) "평균 연령"
    from customers
    group by 1

     - if(condition, if_true, if_false): 조건문은 삼항 연산자 처럼 if(조건, 참일때, 거짓일때) 순서로 사용된다.

     

    select restaurant_name,
           addr,
           case when addr like '%경기도%' then '경기도'
                when addr like '%특별%' or addr like '%광역%' then substring(addr, 1, 5)
                else substring(addr, 1, 2) end "변경된 주소"
    from food_orders

     - case when 조건 then ~: if를 많이 작성해야하는 상황은 case문으로 작성한다. 조건에 걸리지 않는 나머지는 else에, case문의 종료는 end를 사용한다. end 뒤에 오는 부분이 이 컬럼의 이름이 된다.

     

    --숫자로 변경
    cast(if(rating='Not given', '1', rating) as decimal) 
    
    --문자로 변경
    concat(restaurant_name, '-', cast(order_id as char))

     - 이런 식으로 숫자에서 문자, 문자에서 숫자로의 casting도 가능하다고 하니 참고하자.

     

    3. Subquery

     - 앞에서처럼 쓰다보면 생기는 의문점이 조건이나 연산이 엄청 많은 쿼리를 어떻게 작성할까 하는 부분인데, 이걸 해결하는 방법이 바로 subquery문이다.

    select column1, special_column
    from
        ( /* subquery */
        select column1, column2 special_column
        from table1
        ) a
    select column1, column2
    from table1
    where column1 = (select col1 from table2)

     - 쿼리의 중첩, 즉 한 쿼리의 결과에 대해서 쿼리를 한다고 생각하면 된다.

     - 첫 번째 블록처럼 table에 해당하는 부분을 subquery로 작성하거나, 두 번째 블록처럼 where에서 column1을 subquery로 정의해줄 수도 있다.

     

     

    3. Join

    -- LEFT JOIN
    select a.컬럼명, b.컬럼명
    from a left join b on a.공통컬럼명=b.공통컬럼명
    
    -- INNER JOIN
    select a.컬럼명, b.컬럼명
    from a inner join b on a.공통컬럼명=b.공통컬럼명

     - a join b on a.colName=b.colName: 테이블 여러개에서 데이터를 취합하고 싶을 때 사용하는 키워드다. left join과 inner join으로 나뉜다고 한다 on뒤에는 두 테이블에서 공통으로 존재하는 (기준이 되는) 컬럼을 a.colName=b.colName의 형태로 정의해준다. 

     - a left join b: 밴 다이어그램으로 이해하면 쉽다. 공통컬럼명을 기준으로 a b 테이블 모두에 데이터가 있는 (a교집합b) row와 a테이블에만 데이터가 있는(a 그 자체) row의 데이터를 받아온다.

     - a inner join b: a b 테이블 모두에 데이터가 존재하는 (a교집합b)에 해당하는 데이터만 불러온다.

     

    /*  식당별 평균 음식 주문 금액과 주문자의 평균 연령을 기반으로 Segmentation 하기
    평균 음식 주문 금액 기준 : 5,000 / 10,000 / 30,000 / 30,000 초과
    평균 연령 : ~ 20대 / 30대 / 40대 / 50대 이상
    두 테이블 모두에 데이터가 있는 경우만 조회, 식당 이름 순으로 오름차순 정렬 */
    select	restaurant_name,
    		case when avg_price <= 5000 then 'price_group1'
    			 when avg_price > 5000 and avg_price <= 10000 then 'price_group2'
    			 when avg_price > 10000 and avg_price <= 30000 then 'price_group3'
    			 when avg_price > 30000 then 'price_group4'
    			 end 'price_group',
    		case when avg_age < 30 then 'age_group1'
    			 when avg_age >= 30 and avg_age < 40 then 'age_group2'
    			 when avg_age >= 40 and avg_age < 50 then 'age_group3'
    			 when avg_age >= 50 then 'age_group4'
    			 end 'age_group'
    from
    (
    	select	fo.restaurant_name,
    			avg(fo.price) 'avg_price',
    			avg(c.age) 'avg_age'
    	from food_orders fo inner join customers c on fo.customer_id=c.customer_id
    	group by 1
    ) a
    order by 1

     - 앞의 학습 내용을 기반으로 쿼리를 작성해보았다. price 정보는 food_orders 테이블에, age 정보는 customers 테이블에 흩어져 있기 때문에 먼저 subquery a를 통해 food_orders 테이블과 customers 테이블에 공통으로 존재하는 customer_id 를 기준으로 두 테이블을 inner join (교집합) 으로 묶어서 각 restaurant_name의 '평균 주문 가격'과 '평균 주문자 나이'를 얻어냈다.

     - 다시 subquery a의 결과물인 테이블을 대상으로 쿼리문을 작성했는데, case 문을 이용하여 각 가격대 별로 price_group을 나눠주고, 나이대 별로 age_group을 나눠줬다.

     - 마지막으로 order by 1로 첫 번째 column으로 지정된 restaurant_name을 기준으로 (기본 오름차순) 정렬했다.

     

    4. 잘 못된 값의 처리 (coalesce)

    select a.order_id,
           a.customer_id,
           a.restaurant_name,
           a.price,
           b.name,
           b.age,
           coalesce(b.age, 20) "null 제거",
           b.gender
    from food_orders a left join customers b on a.customer_id=b.customer_id
    where b.age is null

     - coalesce (colName, value): 잘못된 값을 처리하는 방식 중 하나인데, '데이터의 colName 컬럼에 해당하는 부분에 값이 없을 때 value의 값으로 대체한다' 라는 의미라고 한다.

     - 그 외 방법으로는 where 에서 is not null로 null이 아닌 데이터만 받아오는 방법 (저번 연습때 사용했던 그 방법)이나 if 구문의 조건을 통해 null 값인 항목을 체크하는 방법도 있다.

     

    5. Pivot Table

     - 강의에서는 pivot table을 '2개 이상의 기준으로 데이터를 집계할 때, 보기 쉽게 배열하여 보여주는 것' 이라고 정의했다. Pivot table의 개념만 알면 앞에서 배운 내용의 반복(혹은 응용)이다.

    pivot table
    구조
    구분 컬럼
    집계 기준 데이터

     

    select restaurant_name,
           max(if(hh='15', cnt_order, 0)) "15",
           max(if(hh='16', cnt_order, 0)) "16",
           max(if(hh='17', cnt_order, 0)) "17",
           max(if(hh='18', cnt_order, 0)) "18",
           max(if(hh='19', cnt_order, 0)) "19",
           max(if(hh='20', cnt_order, 0)) "20"
    from 
    (
    select a.restaurant_name,
           substring(b.time, 1, 2) hh,
           count(1) cnt_order
    from food_orders a inner join payments b on a.order_id=b.order_id
    where substring(b.time, 1, 2) between 15 and 20
    group by 1, 2
    ) a
    group by 1
    order by 7 desc

     - 위 쿼리를 기준으로 설명하면, restaurant_name 이 집계 기준이 되고, 각 restaurant_name 마다 15시부터 20시까지 매 시간 별 주문 건 수를 나타낸 pivot table을 얻을 수 있다.

     

    select cuisine_type,
    	sum(if(age >= 10 and age < 20, cnt_order, 0)) '10대',
    	sum(if(age >= 20 and age < 30, cnt_order, 0)) '20대',
    	sum(if(age >= 30 and age < 40, cnt_order, 0)) '30대',
    	sum(if(age >= 40 and age < 50, cnt_order, 0)) '40대',
    	sum(if(age >= 50 and age < 60, cnt_order, 0)) '50대'
    from
    (
    	select fo.cuisine_type,
    		c.age,
    		count(1) cnt_order
    	from food_orders fo inner join customers c on fo.customer_id=c.customer_id
    	group by 1, 2
    ) a
    group by 1

     - 다른 예시는 지문을 보고 직접 작성한 query 인데, 음식 타입과 연령대 별로 주문 건 수를 보여주는 pivot table을 만들어준다. subquery a를 음식 타입과 age 별로 묶어서 count 값을 지정하고, 바깥 쿼리에서 sum을 해주는동안 if 구문으로 해당 나이대만 걸러서 count를 사용해 합산해준다.

     - 이번 쿼리 강의중 가장 고전했던 파트인 것 같다. 내부의 서브쿼리는 쉽게 구현했는데, 바깥쪽 쿼리에서 나이를 case로 구현하고서는 한참을 헤맸다. 깨달은 점은 일단 내부 쿼리를 확실하게 만들어두는 것, if() 구문이 자체로 삼항 연산자처럼 작동한다는 사실을 까먹지 말 것, select 옆에 들어간 이름들은 column이 된다는 것도 까먹지 말 것 정도가 되겠다.

     

     

    6. window 함수와 over

    -- window function의 기본 구조
    window_function(argument) over (partition by 그룹 기준 컬럼 order by 정렬 기준)
    select cuisine_type, restaurant_name, cnt_order,
    	rank() over (partition by cuisine_type order by cnt_order desc) ranking
    from
    (
    	select cuisine_type, fo.restaurant_name, count(1) cnt_order
    	from food_orders fo 
    	group by 1, 2
    ) a

     - rank() over(): 윈도우 함수인 rank() 인데, 윈도우 함수는 항상 over와 한 쌍이라 무조건 over를 붙여준다고 한다.

     - 또 다른 윈도우 함수인 sum() over() 는 말 그대로 합계를 구하는 함수다. partition by 에서 order by 절과 결합하여 누적 합계를 얻어낼 수도 있다.

     

     

    7. date_format 과 date

    select date_format(date(date), '%Y') '년',
    	date_format(date(date), '%m') '월',
    	date_format(date(date), '%Y%m') '년월',
    	count(1) '주문건수'
    from food_orders fo inner join payments p on fo.order_id=p.order_id
    where date_format(date(date), '%m')='03'
    group by 1, 2, 3
    order by 1

     - 문자열 값으로 저장된 날짜가 있거나 할 때 쉽게 cast 하는 방법으로 date(날짜컬럼)가 있다. date() 안의 날짜 컬럼의 정보가 자동으로 date 형식으로 변환되는데, 이걸 date_format 이라는 함수에 적용하면 날짜 형식에서 년, 월, 일, 요일 등의 정보를 뽑아낼 수 있다.

     - %Y 는 연도, %m는 월, %d 는 일, %w는 요일 등으로 작성하고, %Y-%m-%d 처럼 중복 사용하여 원하는 형태로 조립할 수도 있다. %w의 경우 0이 일요일이라고 한다.

     

     

    728x90
Designed by Tistory.