💡 SQL 에서의 순서
- SQL 은 순번을 다루기 위한 기능 없음
- 관계 모델 이론 때문
- 테이블 레코드를 순서 없게 정의
- 하지만 순번은 필요한 경우가 많기 때문에 윈도우 함수를 이용해 절차지향의 순번을 활용
23강. 레코드에 순번 붙이기
기본키가 필드 1개
윈도우 함수 방법
- 테이블 스캔 횟수 1회
- 인덱스 온리 스캔 사용하므로 테이블 직접 접근 회피
select student_id,
row_number() over (order by student_id) as seq
from weights;
상관 서브쿼리 방법
- row_number 함수 사용 불가한 시스템에서의 방법 → aka MySQL
- 성능적으로 지양해야 함
select student_id,
(select count(*)
from weights w2
where w2.student_id <= w1.student_id) as seq
from weights w1;
기본키가 필드 여러 개
윈도우 함수 방법
- 키가 하나일 때의 쿼리에서 order by 구에 필드 하나만 더 추가하면 됨
select class, student_id,
row_number() over (***order by class, student_id***) as seq
from weights2;
상관 서브쿼리 방법
- 다중 필드 비교
- 복합적인 필드를 하나의 값으로 연결해 한번에 비교하는 기능
- 필드 자료형은 원하는 대로 지정 가능
- 암묵적 자료형 변환 발생 하지 않음 → 기본 키 인덱스로 사용 가능
- 필드가 점점 늘어나도 사용 가능
select class, student_id,
(select count(*)
from weights w2
where ***(w2.class, w2.student_id) <= (w1.class, w1.student_id)*** as seq
from weights w1;
그룹마다 순번 붙이는 경우
- 테이블을 그룹으로 나누고, 그룹마다 내부 레코드에 순번 할당
윈도우 함수 방법
- partition by 로 클래스 기준으로 테이블 그룹 분할
- 분할된 그룹에 student_id 를 이용해 순번 할당
select class, student_id,
row_number() over (***partition by*** ***class*** order by student_id) as seq
from weights;
상관 서브쿼리 방법
select class, student_id,
(select count(*)
from weights w2
where ***w2.class = w1.class*** and w2.student_id <= w1.student_id) as seq
from weights w1;
순번과 갱신
윈도우 함수 방법
- row_number 쓸 경우에는 서브쿼리 필요
update weights
set seq = (select seq
from (select class, student_id,
row_number() over (partition by class order by student_id) as seq
from weights) seqtbl
where weights.class = seqtbl.class and weights.student_id = seqtbl.student_id);
상관 서브쿼리 방법
update weights
set seq = (select count(*)
from weights w2
where w2.class = weights.class and w2.student_id <= weights.student_id);
24강. 레코드에 순번 붙이기 응용
💡 자연 수열 (순번) 성질
- 연속성 : 건너뛰는 일이 없는 것
- 유일성 : 수열 내부에 있는 하나의 숫자는 한 번만 나타나는 것
중앙값 구하기
- 숫자 정렬해 양쪽 끝부터 수를 세는 경우 정중에 오는 값
- 홀수, 짝수에 따라 중앙값 구하는 방법이 다름
집합 지향적 방법
- 테이블을 상위집합, 하위집합으로 분할하고 공통 부분 검색
- 단점
select avg(weight)
from (select w1.weight
from weights w1, weights w2
group by w1.weight
having sum(case when w2.weight >= w1.weight then 1 else 0 end) >= count(*) / 2
and sum(case when w2.weight <= w1.weight then 1 else 0 end) >= count(*) / 2) tmp;
절차 지향적 방법1
- lo와 hi 가 만나는 지점이 결국 중앙값이다.
- 짝수의 경우
- 홀수의 경우
- 정렬 2회 사용
- row_number 함수만 사용 가능
- rank, dense_rank 함수는 유일성 혹은 연속성이 깨지기 때문
- 반드시 order by 정렬 조건에 student_id 를 포함해야 함
- 그렇지 않으면 null 값이 나올 수 있음
- 항상 같은 결과를 도출할 수 없음
select avg(weight) as median
from (select weight,
row_number() over (order by weight asc, student_id asc) as hi,
row_number() over (order by weight desc, student_id desc) as lo
from weights) tmp
where hi in (lo, lo-1, lo+1);
절차 지향적 방법2
- 성능적으로 개선한 방법
- 표준 SQL 에서 중앙값 구하는 가장 빠른 방법
- 정렬 1회 사용
select avg(weight)
from (select weight,
2 * row_number() over (order by weight) - count(*) over() as diff
from weights) tmp
where diff between 0 and 2;
순번을 사용한 테이블 분할
단절 구간 찾기
- 중간중간 비어있는 숫자를 이렇게 출력하고 싶음
집합 지향적 방법
- 집합의 경계선을 구하는 방법
- 단절 지점
n1.num+1 ≠ min(n2.num)
인 지점
gap_start
= 비어있는 숫자 시작값 = n1.num 다음 숫자
gap_end
= 비어있는 숫자 종료값 = n2.num-1
- 반드시 자기 결합 사용해야 함.
select (n1.num+1) as gap_start,
(min(n2.num)-1) as gap_end
from numbers n1 inner join numbers n2 on n2.num >= n1.num
group by n1.num
having (n1.num+1) < min(n2.num);
절차 지향적 방법
- 다음 레코드와 비교하는 방법
- 현재 레코드와 다음 레코드 숫자 차이가 1이 아니라면 사이에 빈 숫자가 있다.
윈도우 함수
로 현재 레코드의 다음 레코드 구하고
, 두 레코드 숫자 차이를 diff 필드에 저장
- 성능
- 결합 사용하지 않음
- 테이블 접근 1회
- 윈도우 함수에서 정렬 실행
select num+1 as gap_start,
(num+diff-1) as gap_end
from (select num,
max(num) over (order by num rows between 1 following and 1 following) - num
from numbers) tmp(num, diff)
where diff <> 1;
테이블에 존재하는 시퀀스 구하기
집합 지향적 방법
- 집합 경계선 구하는 방법
- max / min 함수 이용해 시퀀스 경계를 직접적으로 구하기
- 자기 결합으로 num 필드 조합 만들고, 최댓값 최솟값으로 집합 경계 구하는 방식
select min(num) as low,
max(num) as high
from (select n1.num,
count(n2.num) - n1.num
from numbers n1 inner join numbers n2 on n2.num <= n1.num
group by n1.num) n(num, gp)
group by gp;
절차 지향적 방법
- 다음 레코드와 비교하는 방법
- 정렬 2회 발생 → tmp1, tmp3 임시 테이블
- 서브쿼리 스캔 2회 발생 → tmp1, tmp2 임시 테이블
- 중간 결과를 메모리에 유지하고, 모자르면 저장소를 사용하므로 성능 측면에서 안정성이 조금 부족함
25강. 시퀀스 객체, IDENTITY 필드, 채번 테이블
💡 세 개 모두 최대한 사용하지 않기
굳이 써야한다면 그나마 나은 시퀀스 객체 쓰기
Oracle에선 IDENTITY, MySQL에선 시퀀스 객체 지원 안 함
시퀀스 객체
- 테이블이나 뷰처럼 스키마 내부에 존재하는 객체 → Create 구문으로 정의
create sequence testseq
start with 1
increment by 1
maxvalue 10000
minvalue 1
cycle;
insert into tbl values(next value for nextval, 'a', 'b', ...);
시퀀스 객체의 문제점
- 표준화가 늦어, 이식성 없고 사용할 수 없는 구현 존재함
- 실제 엔티티 속성이 아님
- 성능적인 문제 일으킴
시퀀스 객체로 발생하는 성능 문제
- 시퀀스 객체가 생성하는 순번 특성
- 유일성
- 중복값 생성되지 않음
- 중복값이 있으면 기본키 불가
- 연속성
- 생성된 값에 비어있는 부분이 없음
- 기본키 필수 제약 아니므로, 옵션 설정 가능
- 순서성
- 순번의 대소 관계가 역전되지 않음
- 기본키 필수 제약 아니므로, 옵션 설정 가능
- 순번 특성 만족하기 위해
락 매커니즘 사용
- 시퀀스 객체 사용 시
배타 락
락 매커니즘의 성능 문제
- 시퀀스 객체에 배타 락 적용
- next value 검색
- current value를 1만큼 증가
- 시퀀스 객체에 배타 락 해제
핫 스팟
문제
- 순번처럼 비슷한 데이터를 연속으로 insert 시 물리적으로 인접한 영역에 과부하 몰림.
- 특정 구역에 I/O 부하가 몰리는 핫 스팟 (핫 블록) 문제 발생
- 연속된 값 도입할 때 DBMS 내부에서
해시 등을 이용해서 분산할 수 있는 구조
로 해결
인덱스에 필드를 추가
해 데이터 분산도 올리기
문제점 대처
- CACHE
- 새로운 값이 필요할 때마다 메모리에 읽을 필요 있는 값의 숫자 설정
- 장애 시 연속성 담보 불가
- NOORDER
IDENTITY 필드
- 자동 순번 필드
- 테이블 필드로 정의하고 테이블에 insert가 발생할 때마다 자동으로 순번 붙여주는 기능
- 기능, 성능적 측면에서 시퀀스 객체보다 심각한 문제
- 기능적 측면
- IDENTITY 필드는 특정 테이블과 연결 → 재사용 불가, 여러 테이블 간 공유 불가
- 성능적 측면
- 시퀀스 객체에서 성능 튜닝 방법인 CACHE, NOORDER 등 사용 어려움
- 필자 의견에 따르면 무쓸모라는 평
채번 테이블
정리
절차 지향형
이 윈도우 함수
라는 형태로 SQL에서 살아감
윈도우 함수 사용으로 코드 간결
해짐
윈도우 함수 사용으로 결합, 테이블 접근 줄어들어
성능 향상
시퀀스 객체
, IDENTITY 필드
는 성능 문제 원인
연습문제
💡 중간값을 구하는 SQL 쿼리에서 student_id 필드를 정렬 기준으로 사용하지 않으면 왜 정상 동작하지 않을까?
- 체중이 같은 학생들이 언제나 같은 순서로 정렬된다는 보장이 없기 때문
- 정렬 기준을 유일성을 가진 student_id 필드를 포함하여 언제나 같은 순서를 보장하기 위함
JPA 식별자와 순번
- JPA에서 엔티티를 영속상태로 관리하기 위해선, 즉 엔티티를 영속성 컨텍스트에 저장하기 위해선 식별자 값이 필요
- JPA 식별자 (기본 키) 매핑 방법
- 직접 할당
- 자동 생성
- IDENTITY
- 기본 키 생성을 데이터베이스에 위임
- 순서대로 순번을 매기는 데이터베이스의 순번 IDENTITY와 의미가 다름에 유의
- mysql에서는 auto_increment 방식 사용 → 이건 데이터베이스 순번 IDENTITY와 같은 의미
- 데이터베이스에 엔티티를 insert 한 후에 기본 키 값 조회 가능
- 따라서 엔티티에 식별자 값 할당하려면 추가 테이블 접근이 필요하지만, 하이버네이트는 JDBC3 getGeneratedKeys() 메소드 활용해 한 번만 테이블 접근
- 엔티티를 DB에 저장해야만 식별자 값이 생성되므로, 트랜잭션 지원하는 쓰기 지연 동작 안 함
- SEQUENCE
- 데이터베이스 시퀀스 사용해서 기본 키 할당
- SQL CACHE = JPA allocationSize
- 엔티티 저장 전에 데이터베이스 시퀀스 사용해 식별자 조회하여 엔티티 할당 후 저장
- 사전에 시퀀스를 조회하므로 테이블 접근 2회 필요
- 이때 성능 향상을 위해 allocationSize(CACHE) 를 활용
- TABLE
- 키 생성 테이블 사용
- 채번 테이블 방법으로 추정
- allocationSize 방식으로 성능 개선 가능
- AUTO
- 선택한 데이터베이스 방언에 따라 다른 전략 중 하나를 자동으로 선택
- 기본값
SQL에서는 순번 쓰지 말라는데, 그럼 식별자는 어떻게 선택해?
- 권장하는 식별자 선택 전략
- 식별자 요구사항
- not nullable
- unique
- not modifiable
- 선택 전략
- 자연 키
- 비즈니스에 의미 있는 키
- 주민번호, 이메일, 전화번호
- 대리 키 (인조 키)
- 임의로 만들어진 비즈니스 연관없는 키
- 오라클 시퀀스, auto_increment
- 결론
SQL 성능에 하자가 있더라도 대리 키 사용을 권장
- 비즈니스 환경은 변할 수 있음
- JPA 는 모든 엔티티에 일관된 방식으로 대리 키 사용을 권장