Link Search Menu Expand Document

갱신과 데이터 모델

갱신은 효율적으로

NULL 채우기

  • 문제. NULL인 값을 이전 레코드의 값으로 채우기
    UPDATE OmitTbl
       SET val = (SELECT val
    							FROM OmitTbl OT1
         				 WHERE OT1.keycol = OmitTbl.keycol
              		 AND OT1.seq = (SELECT MAX(seq)
                 										FROM OmitTbl OT2
                           				 WHERE OT2.keycol = OmitTbl.keycol
                                		 AND OT2.seq < OmitTbl.seq
                                   	 AND OT2.val IS NOT NULL))
     WHERE val IS NULL
    
    • 다음과 같은 조건을 갖는 레코드 집합을 구하고, 그 중 가장 큰 seq 필드를 가진 레코드를 찾음
      1. 같은 keycol 필드를 가짐
      2. 현재 레코드보다 작은 seq 필드를 가짐
      3. val 필드가 NULL이 아님

        반대로 NULL을 작성

  • 문제. 위에서 채워진 값을 반대로 NULL로 바꾸기
    UPDATE OmitTbl
       SET val = CASE WHEN val
    				 = (SELECT val
    							FROM OmitTbl O1
         				 WHERE O1.keycol = OmitTbl.keycol
              		 AND O1.seq = (SELECT MAX(seq)
                 										FROM OmitTbl O2
                           				 WHERE O2.keycol = OmitTbl.keycol
                                		 AND O2.seq < OmitTbl.seq
                                   	 AND O2.val IS NOT NULL))
               THEN NULL
               ELSE val END
    

    레코드에서 필드로의 갱신

  • 문제. 레코드 기반 테이블에서 필드 기반 테이블로 데이터 이동

    필드를 하나씩 갱신

    UPDATE ScoreCols
       SET score_en = (SELECT score
    									 FROM ScoreRows SR
              				WHERE SR.student_id = ScoreCols.student_id
                  			AND subject = '영어'),
         score_nl = (SELECT score
    									 FROM ScoreRows SR
              				WHERE SR.student_id = ScoreCols.student_id
                  			AND subject = '국어'),
         score_mt = (SELECT score
    									 FROM ScoreRows SR
              				WHERE SR.student_id = ScoreCols.student_id
                  			AND subject = '수학'),
    
  • 3개의 상관 서브쿼리를 실행하므로 성능적으로 좋지 않음
  • 과목 수가 늘어날 수록 성능 악화

    다중 필드 할당

    UPDATE ScoreCols
       SET (score_en, score_nl, score_mt)
    				= (SELECT MAX(CASE WHEN subject = '영어'
        											 THEN score
                    					 ELSE NULL END) AS score_en,
                                          MAX(CASE WHEN subject = '국어'
        											 THEN score
                    					 ELSE NULL END) AS score_nl,
                                          MAX(CASE WHEN subject = '수학'
        											 THEN score
                    					 ELSE NULL END) AS score_mt,
                 FROM ScoreRows SR
                WHERE SR.student_id = ScoreCols.student_id)
    
  • 테이블 접근은 1회로 감소, INDEX UNIQUE SCANINDEX RANGE SCAN으로 변함, MAX 함수 정렬 추가
  • 다중 필드 할당을 모든 SQL에서 지원하지는 않지만 표준 SQL 기능이므로 지원 가능성 높음
  • MAX 함수를 이용하여 서브쿼리로 단일한 값을 리턴

    NOT NULL 제약이 걸려있는 경우

  • UPDATE 구문 사용
    • 필드를 하나씩 갱신
      UPDATE ScoreColsNN
      SET score_en = COALESCE((SELECT score
                                                           FROM ScoreRows
                 						 WHERE student_id = ScoreColsNN.student_id
                        				 AND subject = '영어'), 0),
              score_nl = COALESCE((SELECT score
                                                           FROM ScoreRows
                 						 WHERE student_id = ScoreColsNN.student_id
                        				 AND subject = '국어'), 0),
              score_mt = COALESCE((SELECT score
                                                           FROM ScoreRows
                 						 WHERE student_id = ScoreColsNN.student_id
                        				 AND subject = '수학'), 0)
       WHERE EXISTS (SELECT *
       								 FROM ScoreRows
           			WHERE student_id = ScoreColsNN.student_id)
      
    • 다중 필드 할당
      UPDATE ScoreColsNN
       SET (score_en, score_nl, score_mt)
                    = (SELECT COALESCE(MAX(CASE WHEN subject = '영어'
        											 					THEN score
                    					 					ELSE NULL END), 0) AS score_en,
                                          COALESCE(MAX(CASE WHEN subject = '국어'
        											 					THEN score
                    					 					ELSE NULL END), 0) AS score_nl,
                      COALESCE(MAX(CASE WHEN subject = '수학'
        											 					THEN score
                    					 					ELSE NULL END), 0) AS score_mt,
                 FROM ScoreRows SR
                WHERE SR.student_id = ScoreColsNN.student_id)
       WHERE EXISTS (SELECT *
       								 FROM ScoreRows)
            			WHERE student_id = ScoreColsNN.student_id)
      
    • 처음부터 테이블 사이에 일치하지 않는 레코드가 존재할 경우 제외
    • 학생은 존재하지만 과목이 없는 경우, 레코드는 있지만 필드가 없는 경우 제외
    • COALESCE 함수를 사용하여 NULL을 0으로 변경
  • MERGE 구문 사용
    MERGE INTO ScoreColsNN
       USING (SELECT student_id,
    							 COALESCE(MAX(CASE WHEN subject = '영어'
        									 					 THEN score
                  				 					 ELSE NULL END), 0) AS score_en,
                                   COALESCE(MAX(CASE WHEN subject = '국어'
        											 			 THEN score
                    					 			 ELSE NULL END), 0) AS score_nl,
                   COALESCE(MAX(CASE WHEN subject = '수학'
        									 					 THEN score
                    					 		 	 ELSE NULL END), 0) AS score_mt
              FROM ScoreRows
             GROUP BY student_id) SR
        ON (ScoreColsNN.student_id = SR.student_id)
     WHEN MATCHED THEN
     			UPDATE SET ScoreColsNN.score_en = SR.score_en,
      						 ScoreColsNN.score_nl = SR.score_nl,
             			 ScoreColsNN.score_mt = SR.score_mt
    
  • UPDATE 사용 시 두 곳에 분산되어 있던 결합조건을 ON 구로 한 번에 처리할 수 있음
  • MERGE 구문은 UPDATEINSERT를 한 번에 시행하는 것이 목적이지만 UPDATE 또는 INSERT만 수행해도 문제는 없음

    필드에서 레코드로 변경

  • 갱신 대상 레코드를 subject 값에 따라 분기
    UPDATE ScoreRows
       SET score = (SELECT CASE ScoreRows.subject
    													WHEN '영어' THEN score_en
    													WHEN '국어' THEN score_nl
    													WHEN '수학' THEN score_mt
                 							ELSE NULL
                         END
                    FROM ScoreCols
                   WHERE student_id = ScoreRows.student_id)
    

    같은 테이블의 다른 레코드로 갱신

  • 문제. 이전 종가와 현재 종가를 비교해서 trend값 채우기

    상관 서브쿼리 사용

    INSERT INTO Stocks2
    SELECT brand, sale_date, price,
               CASE SIGN(price -
      								(SELECT price
              					 FROM Stocks S1
                    		WHERE brand = Stocks.brand
                      		AND sale_date = 
                        			 (SELECT MAX(sale_date)
                            			FROM Stocks S2
                               	 WHERE brand = Stocks.brand
                                 	 AND sale_date < Stocks.sale_date)))
               WHEN -1 THEN '↓'
               WHEN 0 THEN '→'
               WHEN -1 THEN '↑'
               ELSE NULL
          END
     FROM Stocks
    
  • SIGN 함수는 매개변수로 받은 숫자가 양수라면 1, 음수라면 -1, 0이라면 0을 반환하는 부호 조사 함수
  • 상관 서브쿼리 사용으로 인해 테이블 접근 횟수가 많음

    윈도우 함수 사용

    INSERT INTO Stocks2
    SELECT brand, sale_date, price,
               CASE SIGN(price - 
      							MAX(price) OVER (PARTITION BY brand
             														 ORDER BY sale_date
                            				 ROWS BETWEEN 1 PRECEDING
                                 							AND 1 PRECEDING))
               WHEN -1 THEN '↓'
               WHEN 0 THEN '→'
               WHEN -1 THEN '↑'
               ELSE NULL
          END
     FROM Stocks S2
    

    INSERT와 UPDATE 어떤 것이 좋을까?

  • INSERT SELECT의 장점
    • UPDATE에 비해 우수한 성능
    • MySQL처럼 갱신 SQL에서 자기 참조를 허가하지 않는 DB에서도 사용 가능
  • INSERT SELECT의 단점
    • 같은 크기와 구조를 가진 데이터를 두 개 만들어야 하므로 저장소 용량을 2배 이상 소비

      갱신이 초래하는 트레이드오프

  • 문제. 주문마다 주문일(order_date)와 상품의 배송 예정일(delivery_date)의 차이를 구해, 그 차이가 3일 이상인 주문번호를 찾기

    SQL을 사용하는 방법

  • 주문일과 배송 예정일의 차이
    SELECT O.order_id,
               O.order_name,
      	 ORC.delivery_date - O.order_date AS diff_days
    FROM Orders O
    		 INNER JOIN OrderReceipts ORC
       			ON O.order_id = ORC.order_id
     WHERE ORC.delivery_date - O.order_date >= 3
    
  • 주문 단위로 집약
    SELECT O.order_id
               MAX(O.order_name),
      	 MAX(ORC.delivery_date - O.order_date) AS diff_days
    FROM Orders O
    		 INNER JOIN OrderReceipts ORC
       			ON O.order_id = ORC.order_id
     WHERE ORC.delivery_date - O.order_date >= 3
     GROUP BY O.order_id
    
  • order_id와 order_name이 일대일 대응한다면 GROUP BY 구에 order_name을 추가해도 됨

    모델 갱신을 사용하는 방법

  • SQL을 사용할 경우 검색 처리에 드는 비용이 높음
  • 결합은 실행 계획의 변동 리스크가 있어 장기적으로 봤을 때 성능을 불안정하게 만듦
  • SQL을 사용하지 않고 Orders 테이블에 배송 지연 플래그를 추가하고 플래그를 갱신

    모델 갱신의 주의점

    높아지는 갱신비용

  • 검색 부하를 갱신 부하로 바꿈
  • 플래그 값이 변경되는 경우는 갱신 비용 상

    갱신까지의 시간 랙(Time Rag) 발생

  • 배송 예정일이 주문 등록 후에 갱신되는 경우 Orders 테이블의 배송 지연 플래그 필드와 OrderReceipts 테이블의 배송 예정일 필드가 실시간으로 동기화되지 않으므로 차이 발생
  • 실시간성이 중요한 업무일수록 생신 간격이 짧아져야 함
  • 완전한 실시간을 요구하는 경우 동일 트랜잭션으로 처리해야 하는데 성능과 실시간성 사이 트레이드오프 발생

    모델 갱신비용 발생

  • RDB 데이터 모델 갱신은 코드 기반의 수정에 비해 대대적인 수정이 필요
  • 갱신 대상 테이블을 사용하는 다른 처리에 문제가 발생할 가능성 존재
  • 실제 운용에 들어가면 모델 변경은 거의 불가능

    시야 협착 : 관련 문제

  • 문제. 주문번호마다 몇 개의 상품이 주문되었는지 조회

    다시 SQL을 사용한다면

  • 집약 함수를 사용
    SELECT O.order_id,
               MAX(O.order_name) AS order_name,
         MAX(O.order_date) AS order_date,
         COUNT(*) AS item_count
    FROM Orders O
    		 INNER JOIN OrderReceipts ORC
         		ON O.order_id = ORC.order_id
     GROUP BY O.order_id
    
  • 윈도우 함수를 사용
    SELECT O.order_id,
               O.order_name
         O.order_date
         COUNT(*) OVER (PARTITION BY O.order_id) AS item_count
    FROM Orders O
    		 INNER JOIN OrderReceipts ORC
         		ON O.order_id = ORC.order_id
    
  • 두 방법 모두 실행 비용, 실행 계획은 비슷
  • 가독성, 확장성 측면에서는 윈도우 함수가 좋음

    다시 모델 갱신을 사용한다면

  • Orders 테이블에 상품 수를 추가
  • 한 번 등록한 주문을 나중에 변경할 경우 수정 가능성 있음, 동기/비동기 문제 고려 필요

    초보자보다 중급자가 경계해야

  • 어려운 문제를 어려운 상태인 채 풀면 복잡한 코드 또는 프로그램이 만들어져 시스템 전체 관점에서 비효율이 발생할 수 있음

    데이터 모델을 지배하는 자가 시스템을 지배한다

  • ‘현명한 데이터 구조와 멍청한 코드의 조합’이 ‘멍청한 데이터 구조와 현명한 코드의 조합’보다 좋음