9장. 갱신과 데이터 모델 - 망치와 못

26강. 갱신을 효율적으로

  • 갱신을 효율적으로 수행하는 SQL을 케이스 스터디

1. NULL 채우기

1
2
3
4
5
6
7
8
9
UPDATE OmitTbl
	SET val = (SELECT val
		FROM OmitTbl	OT1
		WHERE OT1.keycol = (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;

2. 반대로 NULL을 설정

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
UPDATE OmitTbl
	SET val = CASE WHEN val
		= (SELECT val
		FROM OmitTbl 01
		WHERE 01.keycol = OmitTbl.keycol
		AND 01.seq
		= (SELECT MAX(seq)
		FROM OmitTbl 02
		WHERE 02.keycol = OmitTbl.keycol
		AND 02.seq < OmitTbl.seq))
 	THEN NULL
 	ELSE val END;

27강. 레코드에서 필드로의 갱신

1. 필드를 하나씩 갱신

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
UPDATE ScoreCols
	SET score_en = (SELECT score
		FROM ScoreRow SR
		WHERE SR.studend_id = ScoreCols.studend_id
		AND subject = '영어'),
		score_nl = (SELECT score
		FROM ScoreRow SR
		WHERE SR.studend_id = ScoreCols.studend_id
		AND subject = '국어'),
		score_mt = (SELECT score
		FROM ScoreRow SR
		WHERE SR.studend_id = ScoreCols.studend_id
		AND subject = '수학');
  • 명확하지만 항목별로 서브쿼리를 필요로하기 때문에 비효율적이다.

2. 다중 필드 할당

  • 여러 개의 필드를 리스트화하고 한번에 갱신
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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);
  • 상관 서브쿼리가 하나로 정리된 대신, ScoreRows 테이블에 대한 접근이 INDEX UNIQUE SCAN -> INDEX RANGE SCAN, MAX 함수의 정렬리 추가되므로 트레이드오프가 있다.

1) 다중 필드 할당

2) 스칼라 서브쿼리

  • MAX함수를 적용해 집약시킴으로써 가능해진다.

3. NOT NULL 제약이 걸려있는 경

1) UPDATE 구문 사용

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
UPDATE ScoreColsNN
	SET score_en = COALESCE((SELECT score
		FROM ScoreRow SR
		WHERE SR.studend_id = ScoreCols.studend_id
		AND subject = '영어'), 0),
		score_nl = COALESCE((SELECT score
		FROM ScoreRow SR
		WHERE SR.studend_id = ScoreCols.studend_id
		AND subject = '국어'), 0),
		score_mt = COALESCE((SELECT score
		FROM ScoreRow SR
		WHERE SR.studend_id = ScoreCols.studend_id
		AND subject = '수학'), 0);
WHERE EXISTS (SELECT *
			FROM ScoreRows
			WHERE student_id = ScoreColsNN.studend_id);
  • 테이블 사이에 일치하지 않는 레코드는 제거
  • 학생은 존재하지만 과목이 없는 경우 처리

2) MERGE 구문 사용

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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 studend_id) SR
			ON	(ScoreColsNN.student_id = SR.student_id)
	WHEN MATCHED THEN
	UPDATE SET ScoreColsNN.score_en = SR.score_en,
			coreColsNN.score_nl = SR.score_nl,
			ScoreColsNN.score_mt = SR.score_mt;
  • 결합조건을 ON 구 하나에 묶을 수 있다.
  • ScoreRows 테이블 풀 스캔 1회 + 정렬 1회로 고정된다.
  • 나은 선택지로 고려해볼만 하다.

28강. 필드에서 레코드로 변경

1
2
3
4
5
6
7
8
9
UPDATE ScoreRows
	SET Score = (SELECT CASE ScoreRows.subject
		WHEN '영어' THEN score_en
		WHEN '국어' THEN score_nl
		WHEN '수학' THEN score_nt
		ELSE NULL
		END
	FROM ScoreCols
	WHERE student_id = ScoreRow.student_id);

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

1. 상관 서브쿼리 사용

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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 < Stock.sale_date)))
	WHEN -1 THEN '아래화살표'
	WHEN 0 THEN '오른쪽화살표'
	WHEN 1 THEN '위쪽화살표'
	ELSE NULL
	END
FROM Stocks S2;
  • 상관서브쿼리 때문에 테이블에 여러번 접근해야 한다.

2. 윈도우 함수 적용

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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;
  • 매우 간단하고 효율적이 된다.

3. INSERT와 UPDATE 어떤 것이 좋을까?

  • INSERT SELECT 장점
  • 처리가 더 빠름
  • 자기참조를 허가하지 않는 DB에서도 사용가능
  • 단점
  • 같은 데이털르 두개 만들어야하므로 저장소를 2배 이상 소비
  • 뷰로 만들어도 되나 트레이드 오프가 있다

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

  • 주문일과 배송 예정일 차이가 3일 이상 차이나는 것

1. SQL을 사용하는 방법

1
2
3
4
5
6
7
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;
  • 주문별로 최대 지연일을 알고 싶은 경우
1
2
3
4
5
6
7
8
SELECT O.order_id,
	MAX(O.order_name), -- SELECT 하기 위해 MAX를 사용
	MAX(ORC.delivery_date - O.order_date) AS max_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;

2. 모델 갱신을 사용하는 방법

  • 쿼리를 통해 찾는게 아니라 필드를 하나 추가해 해결할수도 있을 것
  • 코딩 외의 방법도 해결수단이 된다.

31강. 모델 갱신의 주의점

  • 3가지의 트레이드 오프가 있다

1. 높아지는 갱신 비용

  • 배송 지연 플래그 필드를 갱신하는 비용이 든다

2. 갱신까지의 시간 랙 발생

  • 갱신 되기 전까지의 시간차이가 발생

3. 모델 갱신 비용 발생

  • 모델의 수정은 대단히 큰 수정이 요구됨, 특히나 이미 운영을 시작했다면 더욱 더

32강. 시야 협착 : 관련 문제

  • 시야 협착에 빠지기 쉬운 경우를 살펴본다
  • 31강의 문제를 그대로 사용한다
  • 주문번호마다 몇개의 상품이 주문되었는지 확인(주문번호, 주문자 이름, 주문일, 상품수)

1. 다시 SQL을 사용한다면

1
2
3
4
5
6
7
8
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;
1
2
3
4
5
6
7
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;
  • 집약을 쓰든 윈도우 함수를 쓰든 모두 결합과 집약을 수행하기 때문에 실행 비용은 비슷하다. 가독성과 확장성 측면에서 윈도우 함수를 쓰는 것이 더 좋다.

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

  • 주문이 등록될 때 수를 알 수 있으므로 필드를 추가해 개수를 넣기 쉽다. 다만, 주문 변경이 일어나는 경우의 처리를 고려해야 한다.

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

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

  • 현명한 데이터 구조와 멍청한 코드의 조합이 멍청한 데이터 구조와 현명한 코드의 조합보다 좋다.
  • 엔지니어의 사명은 전략적 실패를 만회하는 전술을 찾는 것이 아닌 올바른 전략을 고려하는 것

34강. 인덱스와 B-tree

  • RDB의 인덱스 구조, 1. B-tree, 2. 비트맵, 3. 해시

1. 만능형 : B-tree

  • 대부분 DB의 B+tree 라는 수정 버전을 사용한다.
  • B+tree는 리프노드에만 키값을 저장한다.
  • B+tree는 루트와 리프의 거리를 가능한 일정하게 유지하려고 하기 때문에 검색 속도가 안정적이다.

2. 기타 인덱스

  • 비트맵 인덱스 : 데이터를 비트 플래그로 변환해서 저장하는 형태의 인덱스로 카디널리티가 낮은 필드에 대해 효과를 발휘한다. 하지만 갱신할 때 오버헤드가 키 BI/DWH 용도로 사용된다.
  • 해시 인덱스 : 검색 외에 효과가 없어 거의 사용되지 않는다.

35강. 인덱스를 잘 활용하려면

  • B+tree는 양이 증가해도 검색이 안정적이고, 범위 검색도 쉬움

1. 카디널리티(값의 균형)와 선택률(전체중에서 몇개가 선택되는지)

  • 클러스터링 팩터(저장소에 같은 값이 어느정도 물리적으로 뭉쳐 존재하는지)가 낮을수록 좋다. 하지만 이것은 구현에 의족한다.

2. 인덱스를 사용하는 것이 좋은지 판단하려면

  • 카디널리티가 높은 것
  • 선택률이 낮은 것 5 ~ 10 % 이하, 저장소의 성능향상과 반비례한다.

36강. 인덱스로 성능 향상이 어려운 경우

  • 인덱스 설계는 테이블 정의와 SQL만 봐서는 할 수 없다.

1. 압축 조건이 존재하지 않음

  • WHERE 구가 없이 SELECT 하는 경우

2. 레코드를 제대로 압축하지 못하는 경우

  • WHERE 구 조건의 선택률이 너무 높아 인덱스를 만들기 비효율적이다.

1) 입력 매개변수에 따라 선택률이 변동하는 경우 - 1

2) 입력 매개변수에 따라 선택률이 변동하는 경우 - 2

3. 인덱스를 사용하지 않는 검색 조건

1) 중간 일치, 후방 일치 LIKE 연산자

  • LIKE를 사용하는 경우 인덱스는 전방일치에만 적용 가능하다.

2) 색인 필드로 연산하는 경우(함수를 적요하는 경우에도 안됨)

  • 다음과 같이 바꾸면 됨
  • WHERE col_1 * 1.1 > 100 -> col_1 > 100/1.1

3) IS NULL을 사용하는 경우

  • 색인 필드 데이터에는 NULL이 존재하지 않기 때문에 인덱스를 사용불가

4) 부정형을 사용하는 경우

  • <>, !=, NOT IN

37강. 인덱스를 사용할 수 없는 경우 대처법

1. 외부 설정으로 처리 - 깊고 어두운 강 건너기

1) UI 설계로 처리

  • 사용자가 선택할 수 있는 조건을 제한한다.

2. 외부 설정을 사용한 대처 방법의 주의점

  • 개발전에 합의해야 한다. 서로의 이해관계를 파악하는 것이 중요!

3. 데이터 맡로 대처

  • 특정한 쿼리에서 필요한 데이터만을 저장하는, 상대적으로 작은 크기의 테이블이다. 서브셋
  • 원래 대규모의 데이터를 다뤄야 하는(성능 조건이 중요함) BI/DWH 분야에 사용되는 기술이다. 접근 대상 테이블의 크기를 작게해서 I/O양을 줄이는 것이 목적이다.

4. 데이터 마트를 채택할 시 주의점

1) 데이터 신선도

  • 동기 시점에 따라 신선도와 성능간의 트레이드 오프 고려 할 것

2) 데이터 마트 크기

  • I/0 양을 줄이는 것이 목적이므로 테이블의 크기가 딱히 줄어들지 않는다면 필요없다.
  • GROUP BY절을 미리 사용해 집계를 마치고 데이터 마트로 만들면 필드 수와 레코드수를 크게 줄일 수 있다.

3) 데이터 마트수

  • 목적에 맞게 잘 만들어 관리해야, 용량과 성능문제가 안생긴다.
  • 빠르다고 무작점 만들어서 쓰면 안됨

4) 배치 윈도우

  • 데이터 마트도 만드는데 시간이 걸리고 변경에 따른 갱신도 발생하므로 배치 윈도우를 압박한다. 배치윈도우와 Job Net도 고려할 것

5. 인덱스 온리 스캔으로 대처

  • I/O 감소를 목적으로하는 데이터 마트와 접근이 같고, 데이터 동기 문제를 해결할 수 있다.
  • 풀 스캔을 할 때 검사대상을 테이블이 아닌 인덱스로 바꿀 수 있다.
  • 압축 요건이 존재하지 않는 경우
  • CREATE INDEX CoveringIndex ON Orders (order_id, receive_date);
  • SQL 구문에서 필요한 필드를 인덱스만으로 커버할 수 있는 경우, 테이블 접근을 생략하는 기술 이다.
  • 레코드를 제대로 압축하지 못하는 검색 조건
  • CREATE INDEX CoveringIndex_1 ON Orders (process_flg, order_id, receive_date);
  • 압축은 되지만 인덱스를 사용하지 않는 검색 조건
  • CREATE INDEX CoveringIndex_2 ON Orders (shop_name, order_id, receive_date);
  • 로우 지향 DB를 컬럼 지향 DB로 만드는 방법이라고 생각하면 된다.

6. 인덱스 온리 스캔의 주의사항

1) DBMS에 따라 사용할 수 없는 경우도 있다.

2) 한 개의 인덱스에 포함할 수 있는 필드 수에 제한이 있다.

3) 갱신 오버 헤드가 커진다.

  • 커버링 인덱스는 성질살 필연적으로 필드 수가 많아 크기가 큰 인덱스가 되기 쉽다.
  • 따라서 테이블을 갱신할 때의 오버헤드도 일반적인 인덱스에 비해 커진다.

4) 정기적인 인덱스 리빌드가 필요

  • 인덱스에만 접근한다는 것은 성능이 인덱스의 크기에 의존한다는 것이다. 따라서 정기적인 모니터링과 리빌드를 필요로 한다.

5) SQL에 새로운 필드가 추가되면 사용할 수 없다.

  • 일반적인 인덱스에 비해 어플리케이션 유지 보수에 약한 타입의 튜닝이라고 할 수 있다.