diff --git a/Document/2023/1105/yubin/1105_yubin.md b/Document/2023/1105/yubin/1105_yubin.md new file mode 100644 index 0000000..b3d3aa6 --- /dev/null +++ b/Document/2023/1105/yubin/1105_yubin.md @@ -0,0 +1,634 @@ +> 1105 김유빈 디비디비딥 정리 + +# 2장. SQL기초 + +> 자연 언어를 사용하는 것처럼 + +## SELECT 구문 + +데이터베이스의 핵심은 **검색** 이다. 검색은 다른 말로 **질의(query)** 또는 **추출(retrieve)** 라고 부른다. + +검색을 위해 사용하는 SQL구문을 SELECT 구문이라고 부른다. 문자 그대로 선택 한다는 의미! + +### SELECT 구와 FROM 구 + +기본적으로 SELECT 구와 FROM 구로 이루어진다. + +FROM구는 반드시 입력하야 하는 것은 아니지만, 테이블에서 데이터를 검색하는 경우에는 반드시 입력해야 한다. (Oracle의 경우 반드시 FROM 구를 입력해야 한다.) + +| name(이름) | phone_nbr(전화번호) | address(주소) | sex(성별) | age(나이) | +| --- | --- | --- | --- | --- | +| 인성 | 080-3333-xxxx | 서울시 | 남 | 30 | +| 하진 | 090-0000-xxxx | 서울시 | 여 | 21 | +| 준 | 090-2984-xxxx | 서울시 | 남 | 45 | +| 민 | 080-3333-xxxx | 부산시 | 남 | 32 | +| 하린 | ​ | 부산시 | 여 | 55 | +| 빛나래 | 080-5848-xxxx | 인천시 | 여 | 19 | +| 인아 | ​ | 인천시 | 여 | 20 | + +여기서 SQL의 특징을 알 수 있는데 데이터를 '**어떤 방법으로**' 선택할지 쓰여있지 않다는 것이다. + +→ 어떤 데이터가 필요한지 정하기만 하면 DBMS가 프로그래밍에서 절차 지향 같은 부분은 알아서 처리해준다. + +아래 sql 문을 보면 + +```sql +SELECT username, phone_bs, address, sex, age +FROM ADDRESS; +``` + +| name(이름) | phone_nbr(전화번호) | address(주소) | sex(성별) | age(나이) | +| --- | --- | --- | --- | --- | +| 인성 | 080-3333-xxxx | 서울시 | 남 | 30 | +| 하진 | 090-0000-xxxx | 서울시 | 여 | 21 | +| 준 | 090-2984-xxxx | 서울시 | 남 | 45 | +| 민 | 080-3333-xxxx | 부산시 | 남 | 32 | +| 하린 | ​ | 부산시 | 여 | 55 | +| 빛나래 | 080-5848-xxxx | 인천시 | 여 | 19 | +| 인아 | ​ | 인천시 | 여 | 20 | + +공란으로 되어있는 하인과 인아의 전화번호를 통해 불명한 데이터를 공란(NULL)로 처리함을 확인할 수 있다. + +### WHERE 구 + +WHERE 구를 사용해 추가적인 조건을 지정한다. WHERE은 ***어디*** 라는 의미가 아니라 ***~라는 경우*** 를 의미한다. + +- WHERE 구의 다양한 조건 지정 연산자 → WHERE 구는 다양한 조건 지정 가능. + +| 연산자 | 의미 | +| --- | --- | +| = | ~와 같음 | +| <> | ~와 같지 않음 | +| >= | ~ 이상 | +| > | ~보다 큼 | +| <= | ~ 이하 | +| < | ~ 보다 작음 | +- **WHERE 구는 거대한 벤다이어그램이다.** WHERE 구를 사용하면 테이블에 필터 조건을 붙일 수 있다. 하지만 실제로는 복합적인 조건을 사용할 때가 많다. 그럴땐 ‘AND’ 또는 ‘OR’로 연결 한다. +- ‘그리고’ 를 나타내는 AND는 다음과 작성할 수 있다. + + ```sql + SELECT name, address.age + FROM Address + WHERE address = '서울시' + AND age >= 30; + ``` + + +**IN 으로 OR 조건을 간단하게 작성** + +```sql +**SELECT** name, address +**FROM** address +**WHERE** address **=** '서울시' **OR** address **=** '부산시' **OR** address **=** '인천시'; +``` + +```sql +**SELECT** name, address +**FROM** address +**WHERE** address **IN** ('서울시', '부산시', '인천시'); +``` + +- 실행 결고나는 같지만 훨씬 깔끔하게 바뀌었다. + +**NULL 조건 검색** + +WHERE 구로 조건을 지정할때 흔히 하는 실수 + +```sql +**SELECT** name, address +**FROM** address +**WHERE** phone_nbr **=** **NULL**; +``` + +위 쿼리는 사람 사람의 눈에는 정상적인 SELECT 구문이다. 하지만 실제로 작동하는 SELECT구문은 아니다. + +→ NULL은 데이터 값이 아니므로, 데이터에 사용하는 연산자(=)를 사용할 수 없다. + +반대로 NULL이 아닌 레코드는 IS NOT NULL이라는 키워드 사용. + +NULL은 데이터값이 아니므로, 데이터값에 적용하는 연산자인 = 을 적용할 수 없다. (=NULL을 사용하지 않는 이유) + +SELECT 구문은 절차 지향형 언어의 **함수**와 동일한 역할을 한다. + +SELECT 구문의 입력과 출력 자료형은 **테이블** 뿐이다. 이러한 성질 때문에 폐쇄성(closure property, 관계가 닫혀있다는 의미)을 띈다고 부른다. 이는 뷰와 서브쿼리를 함께 이해할 때 매우 중요한 개념이다. 75p. + +### GROUP BY 구 + +GROUP BY 구를 사용해 합계, 평균과 같은 집계 연산을 수행한다. GROUP BY 구는 케이크를 자르는 칼과 같은 역할을 한다. + +GROUP BY 구문을 사용해 여러 **그룹**을 만들고 숫자 관련 함수를 이용해 집계한다. + +- GROUP BY 구에서 사용하는 집계함수 (생략)GROUP BY 구는 "GROUP BY ()"(생략 가능)를 이용해 테이블 전부를 하나의 그룹으로 만들 수 있다. + +**그룹을 나누었을 때의 장점** + +```sql +**SELECT** sex, **COUNT**(*****) +**FROM** Address +**GROUP** **BY** sex; + +*-- 결과* +**|** sex **|** **count** **| +|**------+------| +****|** 남 **|** 4 **| +|** 여 **|** 5 **|** +``` + +케이크를 자르는 기준을 변경한다면 + +```sql +**SELECT** sex, **COUNT**(*****) +**FROM** Address +**GROUP** **BY** address; + +*-- 결과* +**|** address **|** **count** **| +|**------+------| +****|** 서울시 **|** 3 **| +|** 인천시 **|** 2 **| +|** 부산시 **|** 2 **| +|** 속초시 **|** 1 **|** +| 서귀포시 | 1 | +``` + + 전체 인원수를 계산 (전체 케이크를 자르지 않고 먹는 경우) + +```sql +**SELECT** sex, **COUNT**(*****) + **FROM** Address +**GROUP** **BY** (); + +*-- 결과* +**count** +*-----* +9 +``` + +위 경우는 보통 GROUP BY를 생략한다. + +```sql +**SELECT** sex, **COUNT**(*****) + **FROM** Address +``` + +### HAVING 구 + +GROUP BY를 이용해 구한 그룹에 조건을 건다. + +```sql +**SELECT** address, **COUNT**(*****) +**FROM** Address +**GROUP** **BY** address +**HAVING** **COUNT**(*****) **=** 1; + +*-- 결과* +address **|** **count** +--------+------- +**속초시 **|** 1 +서귀포시 **|** 1 +``` + +HAVING 구를 사용하면 선택된 결과 집합에 또 다시 조건을 지정할 수 있다. 즉 WHERE 구가 ‘레코드’에 조건을 지정한다면, HAVING 구는 ‘집합’에 조건을 지정하는 기능이다. + +### ORDER BY 구 + +결과 레코드들은 DBMS에 따라서 특정한 규칙을 가지고 정렬될 수 있겠지만, SQL의 일반적인 규칙에서는 정렬과 관련된 내용이 없다. 따라서 어떤 DBMS에서 순서를 가지고 출력된다 해도, 다른 DBMS에서는 그렇게 출력되지 않을 수 있다. 따라서 명시적으로 순서를 정할 때 ORDER BY 구를 사용한다. + +ASC(ascending order, 오름차순), DESC(descending order, 내림차순)을 키워드로 사용해 차순을 정한다. (명시하지 않으면 ASC. 모든 DBMS의 공통 규칙) + +```sql +**SELECT** name, age +**FROM** Address +**ORDER** **BY** age **DESC**; + +*-- 결과* +name **|** age +-----+------- +**만혁 **|** 55 +준 **|** 32 +철수 **|** 32 +준 **|** 20 +``` + +실행 결과를 보면 32세의 사람이 두명이 있다. 이 사람들의 순서도 DBMS마다 다를 수 있다. 이 순서도 맞추고 싶다면 정렬 키워드를 추가해야 한다. 예를 들어 `ORDER BY age DESC, name ASC` 와 같이 작성해야 한다. + +## 뷰와 서브쿼리 + +### 뷰 + +SELECT 구문을 DB에 저장하는 것을 **뷰**(View)라고 한다. + +다만 테이블과 달리 내부에 데이터를 저장하지 않는다. 뷰는 어디까지나 '**SELECT 구문'을 저장**한 것이다. + +따라서 SELECT 구문의 FROM 구에 뷰가 있다면 내부적으론 SELECT 구문이 중첩(nested)된 상태이다. + +**뷰 만드는 방법** + +```sql +**CREATE** **VIEW** [뷰이름] ([필드이름1], [필드이름2] ...) **AS** +``` + +아래는 주소별 사람수를 구하는 SELECT 구문을 뷰로 저장한 것. + +```sql +**CREATE** **VIEW** CountAddress (v_address, cnt) +**AS +SELECT** address, **COUNT**(*****) + **FROM** Address +**GROUP** **BY** address; +``` + +이렇게 만들어진 뷰는 일반적인 테이블처럼 SELECT 구문에서 사용할 수 있다. + +```sql +**SELECT** v_address, cnt + **FROM** CountAddress; -- 테이블 대신 뷰를 FROM 구에 지정 +``` + +→ **테이블의 모습을 한 SELECT 구문!** + +**익명 뷰** + +뷰는 사용방법이 테이블과 같지만 내부에는 테이블을 보유하지 않는다는 점이 테이블과 다르다. 따라서 데이터를 선택하는 SELECT 구문은, 실제로는 내부적으로 ‘**추가적인 SELECT 구문**’을 실행하는 중첩(nested) 구조가 된다. + +```sql +*-- 뷰에서 데이터를 선택할 때* +**SELECT** v_address, cnt +**FROM** CountAddress; + +*-- 뷰는 실행할 때 SELECT 구문으로 전개* + v_address, cnt + ( address, **COUNT**(****) + Address + **GROUP** **BY** address) **AS** CountAddress; +``` + +위 코드는 뷰를 사용하는 경우와 뷰의 내용을 SELECT 구문으로 전개 했을때의 코드이다.이렇게 FROM 구에 직접 지정하는 SELECT 구문을 **서브쿼리** 라고 한다. + +### 서브쿼리 + +SELECT 구문의 FROM 구에 직접 지정하는 SELECT 구문을 **서브쿼리**(subquery)라고 부른다. + +IN 내부에서 서브쿼리를 사용하면 데이터가 변경되어도 따로 수정할 필요가 없다는 점에서 효율적이다. + +**서브쿼리를 사용한 편리한 조건 지정** + +WHERE 구의 조건에 서브쿼리를 사용할 수 있다. 이 방법을 통해서 **매칭** 을 쉽게 만들 수 있다. (이때 Addr2 테이블의 데이터는 addr 와 공통되는 2개의 데이터가 존재한다.) + +```sql +-*- 전개 전: IN 내부에서 서브쿼리 사용* +**SELECT** name + **FROM** Address +**WHERE** name **IN** (**SELECT** name + **FROM** Address2); +-*- 서브쿼리 전개 후* +**SELECT** name + **FROM** Address +**WHERE** name **IN** ('인성', '민', '준서', '지연', '서준', '중진'); +``` + +이러한 IN 과 서브쿼리를 함께 사용하는 구문은 데이터가 변경되어도 따로 수정할 필요가 없다는 점에서 굉장히 편리하다. 서브쿼리를 사용하면 IN 내부의 서브쿼리가 SELECT 구문 전체가 실행될때마다 다시 실행된다. + +- 서브쿼리 추가 설명 (ref. [https://inpa.tistory.com/entry/MYSQL-📚-서브쿼리-정리#서브쿼리의_위치에_따른_명칭](https://inpa.tistory.com/entry/MYSQL-%F0%9F%93%9A-%EC%84%9C%EB%B8%8C%EC%BF%BC%EB%A6%AC-%EC%A0%95%EB%A6%AC#%EC%84%9C%EB%B8%8C%EC%BF%BC%EB%A6%AC%EC%9D%98_%EC%9C%84%EC%B9%98%EC%97%90_%EB%94%B0%EB%A5%B8_%EB%AA%85%EC%B9%AD)) + + * 아래 내용은 mySQL 기준입니다. + + ### **서브쿼리의 위치에 따른 명칭** + + SQL + + ```sql + SELECT col1, (SELECT ...)-- 스칼라 서브쿼리(Scalar Sub Query): 하나의 컬럼처럼 사용 (표현 용도) + FROM (SELECT ...)-- 인라인 뷰(Inline View): 하나의 테이블처럼 사용 (테이블 대체 용도) + WHERE col = (SELECT ...)-- 중첩 서브쿼리: 하나의 변수(상수)처럼 사용 (서브쿼리의 결과에 따라 달라지는 조건절) + ``` + + ### **중첩 서브쿼리( Nested Subquery )** + + - **WHERE** 문에 나타나는 서브쿼리 + + SQL + + ```sql + select name, height + from userTbl + where height > 177; + ``` + + → 조건값을 상수로 할때 + + SQL + + ```sql + select name, height + from userTbl + where height > (select height from userTbl where name in ('김경호')); + ``` + + → 조건값을 select로 특정할때 (단 결과가 값이 하나여야됨) + + SQL + + ```sql + select name, height + from userTbl + where height = any(select height from userTbl where addr in ('경남')); + ``` + + → 조건에 값이 여러개 들어올땐 any. + + → any는 in과 동일한 의미. + + → or를 의미한다. + + SQL + + ```sql + select * + from city + where population > all( select population from city where district = 'New York' ); + ``` + + → all은 도출된 모든 조건값에 대해 만족할때. + + → and를 의미한다. + + ### **인라인 뷰(Inline View)** + + - **FROM** 문에 나타나는 서브쿼리 + - **참고로 서브 쿼리가 FROM 절에 사용되 경우 무조건 AS 별칭을 지정해 주어야 한다.** + + SQL + + ```sql + SELECT EX1.name,EX1.salary + FROM ( + SELECT * + FROM employee AS Ii + WHERE Ii.office_worker='사원' + ) EX1; -- 서브쿼리 별칭 + ``` + + ### **스칼라 서브쿼리( Scalar Subquery )** + + - **SELECT** 문에 나타나는 서브쿼리 + - **딴 테이블에서 어떠한 값을 가져올때 쓰임** + - **하나의 레코드만 리턴**이 가능하며, **두개 이상의 레코드는 리턴할 수 없다**. + - 일치하는 데이터가 없더라도 NULL값을 리턴할 수 있다. 이는 원래 그룹함수의 특징중에 하나인데 스칼라 서브쿼리 또한 이 특징을 가지고 있다. + + SQL + + ```sql + SELECT D.DEPTNO, (SELECT MIN(EMPNO) FROM EMP WHERE DEPTNO = D.DEPTNO) as EMPNO + FROM DEPT D + ORDER BY D.DEPTNO + ``` + + --- + + ### **서브 쿼리 실행 조건** + + 1. 서브쿼리는 SELECT문으로만 작성 할 수 있다. (정확히 SELECT문 쿼리밖에 사용 할 수 없는것 이다.) + 2. 반드시 괄호()안에 존재하여야 한다. + 3. 괄호가 끝나고 끝에 ;(세미콜론)을 쓰지 않는다. + 4. ORDER BY를 사용 할 수 없다. + + ### **서브쿼리 사용 가능 한 곳** + + MySQL에서 서브쿼리를 포함할 수 있는 외부쿼리는 SELECT, INSERT, UPDATE, DELETE, SET, DO 문이 있다. + + 이러한 서브쿼리는 또 다시 다른 서브쿼리 안에 포함될 수 있다. + + - SELECT + - FROM + - WHERE + - HAVING + - ORDER BY + - INSERT문의 VALUES 부분 대체제 + - UPDATE문의 SET 부분 대체제 + +## 조건 분기, 집합 연산, 윈도우 함수, 갱신 + +### SQL과 조건 분기 + +일반적인 절차 지향형 프로그래밍 언어에는 if, switch 조건문 등이 있다. + +SQL은 프로그래밍 언어와 달리 절차적으로 기술하지 않기 때문에 '문장'이 아닌 '식'을 기준으로 조건 분기를 정한다. + +SQL에서 조건 분기를 실현하는 기능이 **CASE 식**이다. CASE 식은 절차 지향의 switch 문과 거의 동일한 방식으로 작동한다. + +**CASE 식의 구문** + +CASE 식의 구문에는 ‘단순 CASE 식’과 ‘검색 CASE 식’이라는 두 종류가 있다. 검색 CASE 식은 단순 CASE 식의 기능을 모두 포함하고 있다. + +```sql +**CASE** **WHEN** [평가식] **THEN** [식] + **WHEN** [평가식] **THEN** [식] + **WHEN** [평가식] **THEN** [식] + *생략* + **ELSE** [식] +**END** +``` + +WHEN 구의 평가식이란 ‘필드 = 값’처럼 조건을 지정하는 식을 말한다. + +**CASE 식의 작동** + +CASE 식의 작동은 절차 지향형 프로그래밍 언어의 switch 조건문과 비슷하다. 절차 지향형 언어의 조건 분기와 SQL 조건 분기 사이의 가장 큰 차이점은 **리턴 값** 이다. 절차 지향형 언어의 조건 분기는 문장을 실행하고 딱히 리턴하지는 않는다. 반면 SQL의 조건 분기는 특정한 값(상수)를 리턴한다. + +CASE 식의 강점은 '식'이라는 것이다. 따라서 식을 적을 수 있는 모든 곳: SELECT, WHERE, GROUP BY, HAVING, ORDER BY 구와 같은 곳 어디에나 작성할 수 있다. + +### SQL의 집합 연산 + +SQL에서 테이블을 활용해 집합 연산을 할 수 있다. + +**UNION으로 합집합 구하기** + +집합 연산의 기본은 합집합과 교집합이다. WHERE 구에서 합집합은 OR, 교집합은 AND를 사용했다. 하지만 집합 연산에서 합집합은 **UNION(합)** 을 사용한다. + +특이점은 두 테이블의 중복을 제거한뒤 결과값을 출력한다. 중복을 허용하려면 **UNION ALL** 을 사용한다. + +**INTERSECT로 교집합 구하기** + +AND에 해당하는 교집합을 구하기 위한 연산자는 INTERSECT로, UNION과 마찬가지로 중복된 것이 있다면 해당 레코드는 제외된다. + +**EXCEPT로 차집합 구하기** + +주의사항: UNION과 INTERSECT는 순서에 관계없이 결과 값이 같지만 EXCEPT는 순서에 따라 결과가 다르다. 이는 사칙연산과 같은 것이다. (5 - 1과 1 - 5는 다르다!) + +### 윈도우 함수 + +**책의 주제인 성능과 관련이 있어 굉장히 중요한 기능.** + +윈도우 함수의 특징을 한마디로 정리하면 ‘**집약 기능이 없는 GROUP BY 구**’ 이다. + ++ GROUP BY: 자르기 / 집약 | 윈도우 함수: 자르기만 존재. + +GROUP BY 구는 필드로 테이블을 자르고, 이어서 잘라진 조각 개수만큼의 레코드 수를 더해서 출력한다. + +반면에 윈도우함수는 ‘PARTITION BY’구로 수행 한다. 차이점은 테이블을 자른 후에 집약하지 않으므로 출력 결과의 레코드 수가 입력되는 테이블의 레코드 수와 같다는 것이다. + +```sql +**SELECT** address, **COUNT**(*****) + **FROM** Address +**GROUP** **BY** address; + +*-- 결과* +address **|** **count** +--------+------ +**서울시 **|** 3 +인천시 **|** 2 +부산시 **|** 2 +속초시 | 1 +서귀포시 | 1 +``` + +GROUP BY와의 차이점은 자른 후에 집약하지 않으므로 출력 결과의 레코드 수가 입력되는 테이블의 레코드 수와 같다는 것이다. + +윈도우 함수로 주소별 사람수를 계산하는 아래 SQL을 보자 (파티션 분할 결과를 쉽게 이해하기 위한 구분선 존재. 실제 출력에는 해당하지 X) + +```sql +**SELECT** address, + **COUNT**(*****) OVER(PARTITION **BY** address) +**FROM** Address; + +*-- 결과* +address **|** **count** +--------+------ +**속초시 **|** 1 +--------------- +인천시 **|** 2 +인천시 **|** 2 +--------------- +서울시 **|** 3 +서울시 **|** 3 +서울시 **|** 3 +--------------- +부산시 **|** 2 +부산시 **|** 2 +--------------- +서귀포시 **|** 2 +``` + +지역별 사람의 수는 양쪽 모두 똑같지만 출력되는 레코드의 수가 다르다. → 집약 작업이 수행되지 않았기 때문! + +윈도우 함수로 사용할 수 있는 함수는 COUNT 또는 SUM같은 일반함수 이외에도 RANK, ROW_NUMBER등이 있다. + +### 트랜잭션과 갱신 + +SQL은 ‘Structured Query Language’의 약자이다. 즉 데이터 검색을 중심으로 수행하기 위한 언어이다. 따라서 **데이터를 갱신하는것은 부가적인 기능이다.** + +SQL의 갱신 작업은 3종류로 분류된다. + +1. 삽입 (Insert) +2. 제거 (Delete) +3. 갱신 (Update) + +이외에도 1과 3을 합친 머지(Merge)도 존재한다. + +**INSERT 로 데이터 삽입** + +RDB는 데이터를 테이블에 보관한다. 테이블은 데이터를 보관하는 상자일 뿐으므로 내부에 데이터가 없으면 사용하는 의미가 없다. RDB에서 데이터를 등록하는 단위는 레코드(또는 행)다. 삽입할 때 INSERT 구문을 사용한다. + +```sql +-- 기본적인 INSERT 구문 +**INSERT** **INTO** [테이블 명] ([필드1], [필드2], [필드3] ... ) + **VALUES** ([값1, [값2], [값3] ...); +``` + +INSERT 구문은 기본적으로 레코드를 하나씩 삽입한다. 만약 100개의 레코드를 삽입해야 한다면 여러개의 레코드를 한 개의 INSERT 구문으로 삽이하는 multi-row insert 기능을 사용하면 된다.(일부 DBMS에서만 지원한다.) + +그러나 오류가 발생했을때 어떤 레코드가 문제인지 확인하기 어렵다. (이런 방법도 있구나~ 정도로만 생각하기) + +**DELETE 로 데이터 제거** + +데이터를 삭제할 때는 하나의 레코드 단위가 아니라, 한 번에 여러개의 레코드 단위로 처리하게 된다. + +```sql +--- 기본적인 DELETE 구문 +DELETE FROM [테이블 이름] +``` + +부분적으로만 레코드를 제거하고 싶을 때는 SELECT 구문에서 사용했던 WHERE 구로 제거 대상을 선별한다. + +```sql +--- 기본적인 DELETE 구문 +DELETE FROM Address + WHERE address = '인천시'; +``` + +잘못된 구문 + +```sql +DELETE name FROM Address +``` + +- DELETE 구문의 삭제 대상은 필드가 아니라 레코드이므로, 일부 필드만 삭제할 수 없다. + +```sql +DELETE * FROM Address +``` + +- *기호를 사용할 경우도 마찬가지로 오류가 발생한다. 레코드의 필드 일부만 지우고 싶다면 UPDATE 구문을 사용할 것. + +**UPDATE 로 데이터 갱신** + +등록된 데이터를 변경하기 위해 사용하는 구문으로, 테이블의 데이터를 갱신한다. + +```sql +--- 기본적인 UPDATE 구문 +UPDATE [테이블 이름] + SET [필드 이름] = [식]; +``` + +UPDATE 구문도 일부 레코드만 갱신하고 싶을 경우 WHERE 구로 필터링한다. + +```sql +-- 빛나래의 전화번호를 갱신하는 구문 +UPDATE Address + SET phone_nbr = '080-5849-XXXX' +WHERE name = '빛나래' +``` + +UPDATE 구문의 SET 구에 여러 개의 필드를 입력하면 한 번에 여러개의 값을 변경할 수 있다. + +```sql +-- UPDATE 구문을 한 번 사용해서 갱신 +-- 1. 필드를 쉼표로 구분해서 나열 +UPDATE Address + SET phone_nbr = '080-5848-XXXX', + age = 20 + WHERE name = '빛나래'; + +-- 2. 필드를 괄호로 감싸서 나열 +UPDATE Address + SET (phone_nbr, age) = ('080-5848-XXXX',20) + WHERE name = '빛나래'; +``` +## Leet Code 풀이 + +### 1731. The Number of Employees Which Report to Each Employee + +[](https://leetcode.com/problems/the-number-of-employees-which-report-to-each-employee/description/) + +``` +# Write your MySQL query statement below +select e.employee_id, e.name, + count(m.reports_to) as reports_count, + round(avg(m.age*1),0) as average_age +from Employees e +join Employees m +on e.employee_id=m.reports_to +group by e.employee_id, e.name +order by e.employee_id +``` + +### 1934. Confirmation Rate +아직 못풀었습니다🥲 (아래는 풀던 틀린 답입니다) +``` +SELECT s.user_id, ROUND(SUM(c.action = 'confirmed') / COUNT(*) * 100, 2) AS confirmation_rate +FROM Signups AS s +LEFT JOIN Confirmations AS c ON s.user_id = c.user_id +GROUP BY s.user_id; + +``` \ No newline at end of file diff --git a/Document/2023/1112/yubin/1112_yubin.md b/Document/2023/1112/yubin/1112_yubin.md new file mode 100644 index 0000000..14fdf90 --- /dev/null +++ b/Document/2023/1112/yubin/1112_yubin.md @@ -0,0 +1,192 @@ +> 1105 김유빈 디비디비딥 정리 + +# 3장. SQL의 조건 분기 + +> 구문에서 식으로 + +## Union을 사용한 쓸데없이 긴 표현 + +UNION은 외부적으로 하나의 SQL 구문을 실행하는 것처럼 보이지만, 내부적으로는 여러 개의 SELECT 구문을 실행하는 실행 계획으로 해석된다. -> 따라서 테이블에 접근하는 횟수가 많아지고, I/O 비용이 크게 증가한다. +UNION을 사용해도 좋을지 여부는 신중히 검토해야한다. + +```sql +SELECT item_name, year, price_tax_ex AS price + FROM Items + WHERE year <= 2001 +UNION ALL +SELECT item_name, year, price_tax_ex AS price + FROM Items + WHERE year >= 2002; +``` + +1. 쓸데없이 길다: 거의 같은 쿼리를 두 번이나 실행 +2. 성능적으로 문제가 된다. + UNION을 사용 했을 때의 실행 계획에서 Item 테이블에 2회 접근한다. + -> TABLE ACCESS FULL(index없이 테이블을 모두 스캔하는 것)도 2번 발생한다. 읽어오는 비용도 테이블의 크기에 따라 선형적으로 증가하게 된다. + +UNION은 간단하게 레코드를 합칠 수 있다는 점에서 편리하지만, 물리 자원과 SQL의 성능을 나쁘게 만드므로 정확한 판단 하에 사용해야 한다. +**WHERE 구에서 조건 분기를 하는 사람은 초보자** -> SELECT구 만으로 조건 분기를 하자. + +개선된 쿼리 + +```sql +SELECT item_name, year, +CASE WHEN year <= 2001 THEN price_tax_ex +WHEN year >= 2002 THEN price_tax_in END AS price +FROM Items; +``` + +UNION을 사용한 쿼리와 같은 결과를 출력하지만 성능적으로 CASE를 쓴 쿼리가 훨씬 좋다. +Items 테이블 접근 횟수 : 1회 +TABLE ACCESS FULL : 1회 +-> UNION을 사용한 구문보다 성능이 2배 좋아졌으며 sql 구문 자체의 가독성도 크게 증가. + +SQL구문의 성능이 좋은지 나쁜지는 반드시 실행 계획 레벨에서 판단해야. - SQL구문에는 어떻게 데이터를 검색할지 나타내는 접근 경로가 쓰여 있지 않기 때문. + +UNION의 기본 단위는 SELECT '구문'을 기본 단위로 함. 이는 아직 절차 지향형의 발상을 벗어나지 못한 방법이다. 반면, CASE의 기본 단위는 '식'이다. 이렇게 '구문'에서 '식'으로 사고를 변경하는 것이 SQL을 마스터하는 열쇠 중 하나이다. + +## 집계와 조건 분기 + +### 1. 집계 대상으로 조건 분기 + +아래 인구 테이블에서 성별 1은 남성, 2는 여성을 의미. 지역에 따른 남 / 녀 인구의 합을 추출하고 싶을때 + +| 지역 이름 | 성별 | 인구 | +| --------- | ---- | ---- | +| 성남 | 1 | 60 | +| 성남 | 2 | 40 | +| 수원 | 1 | 30 | +| 수원 | 2 | 40 | +| 광명 | 1 | 50 | +| 광명 | 2 | 60 | +| 일산 | 1 | 20 | +| 일산 | 2 | 15 | + +### UNION을 사용한 방법 + +```sql +SELECT prefecture, SUM(pop_men) AS pop_men, SUM(pop_wom) AS pop_wom + FROM ( SELECT prefecture, pop AS pop_men, null AS pop_wom + FROM Population + WHERE sex = '1' + UNION + SELECT prefecture, null AS pop_men, pop AS pop_wom + FROM Population + WHERE sex = '2' +GROUP BY prefecture; +``` + +남성의 인구를 지역별로 구하고, 여성의 인구를 지역별로 구한 뒤 UNION을 활용해 합치는 방법은 절차지향적인 방식이다. +또한 테이블에 접근하는 횟수가 늘어나 성능적으로 문제가 됨을 앞에서 학습했다. + +CASE식을 집약 함수 내부에 포함시켜 해결할 수 있다. + +```sql +SELECT prefecture, + SUM(CASE WHEN sex='1' THEN pop ELSE 0 END) AS pop_men, + SUM(CASE WHEN sex='2' THEN pop ELSE 0 END) AS pop_wom + FROM Population + GROUP BY prefecture; +``` + +외관이 간단해질 뿐만 아니라 성능도 캐시를 고려하지 않았을 때 2배로 증가한다. + +'WHERE'구와 'HAVING'구에서 조건 분기를 하는 사람은 초보자이다. + +### 1. 집약 결과로 조건 분기 + +직원과 직원이 소속된 팀을 관리하는 테이블 +| 직원ID | 팀ID | 직원이름 | 팀 | +|--------|------|----------|------------| +| 201 | 1 | Joe | 상품기획 | +| 201 | 2 | Joe | 개발 | +| 201 | 3 | Joe | 영업 | +| 202 | 2 | Jim | 개발 | +| 203 | 3 | Carl | 영업 | +| 204 | 1 | Bree | 상품기획 | +| 204 | 2 | Bree | 개발 | +| 204 | 3 | Bree | 영업 | +| 204 | 4 | Bree | 관리 | +| 205 | 1 | Kim | 상품기획 | +| 205 | 2 | Kim | 개발 | + +위 테이블을 UNION으로 조건 분기한 코드 + +``` +SELECT emp_name, MAX(team) AS team +FROM Employees +GROUP BY emp_name +HAVING count(_)=1 +UNION +SELECT emp_name, '2개를 겸무' AS team +FROM Employees +GROUP BY emp_name +HAVING count(_)=2 +UNION +SELECT emp_name, '3개 이상을 겸무' AS team +FROM Employees +GROUP BY emp_name +HAVING count(\*)>=3; +``` + +3번의 TABLE ACCESS FULL이 발생한다. emp_name으로 그룹화한 집합의 개수를 구하므로 HAVING 구를 사용한다. + +이를 CASE 식을 사용할 경우, 아래와 같다. + +``` +SELECT emp*name, +CASE WHEN COUNT(*)=1 THEN MAX(team) +WHEN COUNT(\_)=2 THEN '2개를 겸무' +WHEN COUNT(\*)=3 THEN '3개 이상을 겸무' +END AS team +FROM Employees +GROUP BY emp_name; +``` + +이렇게 CASE 식을 사용하여 테이블 접근 비용을 3분의 1로 줄일 수 있다. 이는 집약 결과를 CASE식의 입력으로 사용했기 때문이다. +**HAVING 구에서 조건 분기를 하는 사람도 초보자** + +## 그래도 UNION이 필요한 경우 + +### 1. UNION을 사용할 수밖에 없는 경우 + +여러 개의 서로 다른 테이블에서 검색한 결과를 머지하는 경우 + +``` +SELECT col_1 +FROM Table_A +WHERE col_2='A' +UNION +SELECT col_3 +FROM Table_B +WHERE col_4='B' +``` + +### 2. UNION을 사용하는 것이 성능적으로 좋은 경우 + +이 경우는 인덱스와 관련된 경우이다. 테이블의 크기가 커 TABLE FULL SCAN보다 INDEX RANGE SCAN이 효율적일 경우 인덱스와 UNION 조합이 더 성능이 좋을 수 있다. + +OR, IN 사용 시 WHERE 구문에서 해당 필드에 부여된 인덱스를 사용할 수 없다 -> 테이블이 크고 WHERE 조건으로 선택되는 레코드 수가 충분히 작다면 UNION이 더 빠르다. + +## 절차 지향형과 선언형 + +조건 분기는 조건 분기를 위해 만들어진 CASE 식을 사용하는 것이 UNION을 사용하는 것보다 좋다. + +- SQL 구문 내부에는 식(expression)을 작성 -> 선언적인 식에 적응 필요. +- 절차 지향형 세계에서 선언형 세계로 도약하는 것이 중요. + +4장은 완벽히 정리하지 않아 제외했습니다! + +# Leet Code 풀이 + +### 대여 기록이 존재하는 자동차 리스트 구하기 + +[](https://school.programmers.co.kr/learn/courses/30/lessons/157341) + +``` +-- 수정 예정 +SELECT DISTINCT CAR_ID FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY +WHERE CAR_ID IN (SELECT CAR_ID FROM CAR_RENTAL_COMPANY_CAR +WHERE CAR_TYPE = "세단") && (START_DATE BETWEEN DATE("2022-10-01") and DATE("2022-10-31")) +ORDER BY CAR_ID DESC +``` \ No newline at end of file diff --git a/Document/2023/1203/yubin/1203_yubin.md b/Document/2023/1203/yubin/1203_yubin.md new file mode 100644 index 0000000..a0592e7 --- /dev/null +++ b/Document/2023/1203/yubin/1203_yubin.md @@ -0,0 +1,649 @@ +> 1203 김유빈 디비디비딥 정리 + +# 7장 서브쿼리 + +## 21강. 서브쿼리가 일으키는 폐해 + +### 1\. 서브쿼리의 문제점 + +서브쿼리의 성능적 문제는 실체적인 데이터를 저장하고 있지 않다는 점에서 기인 + +**연산 비용 추가** +서브쿼리에 접근할 때마다 SELECT 구문을 실행해서 데이터를 만들어야. → 구문 실행에 발생하는 비용 추가. (복잡할수록 실행 비용 높아짐) + +**데이터 I/O 비용 발생** +연산 결과의 데이터양이 큰 경우 DBMS가 저장소에 있는 파일을 쓸 때도 존재. (ex. Microsoft SQL Server에서 서브쿼리 결과 tempdb에 저장) 이는 TEMP 탈락 현상의 일종. → 저장소 성능에 따라 접근 속도가 떨어진다. + +**최적화를 받을 수 없음** +서브쿼리로 만들어지는 데이터는 구조적으로 테이블과 차이가 없다. but 명시적 제약, 인덱스가 작성되어 있는 테이블과 달리 서브쿼리에는 메타 정보가 존재하지 않는다 → 옵티마이저가 쿼리를 해석하기 위해 필요한 정보를 서브쿼리에서는 얻을 수 없다. +→ 내부적으로 복잡한 연산 수행 / 결과 크기가 큰 서브쿼리 사용시 성능 리스크를 고려해야. + +**"해당 내용이 정말 서브쿼리를 사용하지 않으면 구현할 수 없는 것인가?"** + +### 2\. 서브쿼리 의존증 + +고객의 구입 명세 정보를 저장하는 테이블(Receipts)에 순번(Seq) 필드는 오래전에 구입했을수록 낮은 값을 가진다. + +이때 고객별 최소 순번을 구하는 상황을 가정하자. (고객들이 구매했던 가장 오래된 구입 이력을 찾는 것.) + +``` +user_id | seq | price +--------*-----*------ +A | 1| 500 +B | 5| 100 +C | 10| 600 +``` + +_구하고자 하는 답_ + +처음 직면하는 문제점은 최소 순번이 고객마다 다르다는 것이다. 만약 구하고자 하는 최소값이 1이라는 비지니스 규칙이 있다면 seq = 1 조건을 통해 답을 도출할 수 있다. 하지만 예제는 이러한 규칙이 없으므로 동적으로 구해야 한다. + +- **서브쿼리를 사용한 방법** + 간단하게 고객들의 최소 순번 값을 저장하는 서브쿼리(R2)를 만들고, 기존의 Receipts 테이블과 결합하는 방법이 있다.이 방법은 간단하지만 두 가지의 단점을 가지고 있다. + +``` +SELECT R1.cust_id, R1.seq, R1.price +FROM Receipts R1 + INNER JOIN + (SELECT cust_id, MIN(seq) AS min_seq + FROM Receipts + GROUP BY cust_id) R2 + ON R1.cust_id=R2.cust_id + AND R1.seq=R2.min_seq; +``` + +1\. 코드가 복잡해서 읽기 어렵다. +2\. 성능이 떨어진다. + +\- 서브쿼리는 대부분 일시적인 영역(메모리 또는 디스크)에 확보되므로 오버헤드가 생긴다. +\- 서브쿼리는 인덱스 또는 제약 정보가 없기 때문에 최적화되지 못한다. +\- 이 쿼리는 결합을 필요로 하기 때문에 비용이 높고 실행 계획 변동 리스크가 발생한다. +\- Receipts 테이블에 스캔이 두 번 필요하다. + +이는 실행 계획에서도 확인할 수 있다. Oracle과 PostgreSQL 모두 R1, R2 각각에 대해 스캔이 이루어지며, 결합(Hash Join)이 이루어진다. Receipts 테이블에 2회의 접근이 필요하다는 것이다. + +**\- 상관 서브쿼리는 답이 될 수 없다** + +``` +SELECT user_id, seq, price + FROM Receipts R1 +WHERE seq = (SELECT MIN(seq) + FROM Receipts R2 + WHERE R1.user_id = R2.userId); +``` + +상관 서브쿼리를 사용한 동치변환 방법 + +상관 서브쿼리를 사용하더라도 Receipts 테이블에 두 번 접근하게 된다. +Receipts 테이블에 접근 1회와 기본 키의 인덱스 접근 1회가 필요하다. 결국 성능적인 장점이 없다. + +**\- 윈도우 함수로 결합을 제거** +일단 개선해야 하는 부분은 Receipts 테이블에 대한 접근을 1회로 줄이는 것이다. (SQL 튜닝에서 가장 중요하나 것이 I/O를 줄이는 것이다.) 접근을 줄이기 위해 ROW_NUMBER()함수를 아래와 같이 사용한다. + +``` +SELECT cust_id, seq, price + FROM (SELECT cust_id, seq, price, + ROW_NUMBER() + OVER (PARTITION BY cust_id + ORDER BY seq) AS row_seq + FROM Receipts ) WORK + WHERE WORK.row_seq = 1; +``` + +ROW_NUMBER 함수로 각 사용자의 구매 이력에 번호를 붙이고 그 번호를 조건을 걸어 조회 하는 쿼리이다. 이 쿼리를 통해 Receipts 테이블에 한번만 접근하게 된다. (이전 seq 필드의 최솟값이 불확실해 쿼리를 한 번 더 사용해야 했던 문제 해결) + +**\- 장기적 관점에서의 리스크 관리** +최초의 쿼리와 상관 서브쿼리를 사용한 쿼리에 비해 윈도우 함수를 사용한 쿼리가 얼마나 성능이 좋은지는 여러 환경에 의해 단언하기 어렵다. + +하지만 저장소의 I/O 양을 감소시키는 것이 SQL 튜닝의 가장 기본 원칙이다. 처음 사용한 쿼리와 비교해보면 결합을 제거했다. 따라서 단순 성능 향상뿐만 아니라 성능의 안정성 확보도 기대 가능하다. + +결합을 사용한 쿼리는 두 개의 불안정 요소가 있다. + +- 결합 알고리즘의 변동 리스크 +- 환경 요인에 의한 지연 리스크(인덱스, 메모리, 매개변수 등) + 상관 서브쿼리를 사용한 쿼리도 앞의 리스크에 해당된다. + +**\- 알고리즘 변동 리스크** +결합 알고리즘에는 크게 Nested Loops, Sort Merge, Hash 세 가지 종류가 있다. 어떤 것을 선택할지는 테이블의 크기 등을 고려하여 옵티마이저가 자동으로 결정한다. 레코드 수가 적은 테이블은 Nested Loops가 선택되기 쉽고, 큰 테이블의 경우에는 Sort Merge, Hash가 선택되기 쉽다. + +따라서 처음에는 레코드가 적어 Nested Loops를 사용하다가 어느 역치를 넘어서면 실행계획에 변동이 생긴다. 이 경우 성능이 좋아지는 경우도 있겠지만, 나빠지는 경우도 많다.(실행 계획의 안정성을 확보하고 싶다면 Hint 구문을 사용하는 것이 좋다.) → 결합을 사용하면 이러한 변동 리스크를 안을 수 밖에 없다. + +또한 데이터양이 많아지면서 Sort Merge, Hash에 필요한 메모리가 부족해지면 일시적으로 저장소를 사용한다. 그 시점에는 성능이 대폭 떨어진다. (TEMP 탈락 현상) + +**\- 환경 요인에 의한 지연 리스크** +Netsed Loops의 내부 테이블 결합 키에 인덱스가 존재하면 성능이 크게 개선된다. 또한 Sort Merge, Hash가 선택되어 TEMP 탈락이 발생하는 경우에 작업 메모리를 늘려주면 성능을 개선할 수 있다. + +하지만 항상 결합 키에 인덱스가 존재하는 것이 아니고, 메모리 튜닝은 한정된 리소스 내부에서 트레이드 오프를 발생시킨다. +즉 장기적으로 고려해야할 리스크를 늘리게 된다는 뜻이다. + +따라서 옵티마이저가 이해하기 쉽게 쿼리를 단순하게 작성해야 한다. + +꼭 기억할 두 가지 사항 + +- 실행 계획이 단순할수록 성능이 안정적이다. +- 엔지니어는 기능뿐만이 아니라 비기능적인 부분도 보장해야한다 + +![문서 12_1](https://github.com/yubin21/db-db-deep/assets/80163835/442534cf-7648-454c-a404-ca3e4479ca3c) + +### 4\. 서브쿼리 의존증 - 응용편 + +Receipts 테이블에서 최댓값을 가지는 레코드와 price 필드의 최대, 최소 차이를 구해보자. + +``` +cust_id | diff +--------+------- +A | -200 +B | -900 +C | 550 +D | 0 +``` + +_구하고자 하는 답_ + +**- 다시 서브쿼리 의존증** + +최솟값의 집합과 최댓값의 집합을 고객 ID를 키로 결합한다. + +``` +SELECT TMP_MIN.cust_id, + TMP_MIN.price - TMP_MAX.price AS diff +FROM (SELECT R1.cust_id, R1.seq, R1.price + FROM Receipts R1 + INNER JOIN + (SELECT cust_id, MIN(seq) AS min_seq + FROM Receipts + GROUP BY cust_id) R2 + ON R1.cust_id=R2.cust_id + AND R1.seq=R2.min_seq) TMP_MIN + INNER JOIN + (SELECT R3.cust_id, R3.seq, R3.price + FROM Receipts R3 + INNER JOIN + (SELECT cust_id, MAX(seq) AS min_seq + FROM Receipts + GROUP BY cust_id) R4 + ON R3.cust_id=R4.cust_id + AND R3.seq=R4.min_seq) TMP_MAX + ON TMP_MIN.cust_id=TMP_MAX.cust_id; +``` + +TMP_MIN: 최솟값의 집합, TMP_MAX: 최댓값의 집합이댜. 쿼리가 매우 길며 가독성도 좋지 않다. 서브쿼리 계층이 깊어서 서쿼리를 확인하기도 힘들다.이전의 쿼리를 두 번 붙여 넣어 테이블 접근 4회 발생한다. + +**- 레코드간 비교에서도 결합은 불필요** + +쿼리의 개선 포인트: 테이블 접근과 결합을 얼마나 줄일 수 있는가 + +``` +SELECT cust_id, + SUM(CASE WHEN min_seq=1 THEN price ELSE 0 END) + - SUM(CASE WHEN max_seq=1 THEN price ELSE 0 END) AS diff +FROM (SELECT cust_id, price, + ROW_NUMBER() OVER (PARTITION BY cust_id ORDER BY seq) AS min_seq, + ROW_NUMBER() OVER (PARTITION BY cust_id ORDER BYP seq DESC) AS max_seq + FROM Receipts) WORK +WHERE WORK.min_seq=1 + OR WORK.max_seq=1 +GROUP BY cust_id; +``` + +최댓값을 뽑고자 내림차순으로 정렬한다. → 내림차순 순번 max_seq가 1인 레코드가 seq의 최댓값을 가지고 있을 것이 보증된다. min_seq과 max_seq에서 가장 윗 번호인 1번 레코드만 가져와 차이를 구한다. + +CASE 식으로 최솟값과 최댓값을 다른 필드에 할당한다. 테이블의 스캔 횟수가 1회가 되며 테이블의 크기가 커질수록 스캔 횟수가 적어지는 의미가 커진다. +윈도우 함수로 정렬이 2회 발생(ORDER BY seq/ORDER BY seq DESC)하는 데에서 비용이 들지만, 결합을 반복하는 것보다 저렴/실행 계획의 안정성도 확보할 수 있다. + +### 5. 서브쿼리는 정말 나쁠까? + +- 쿼리를 처음 고민할 때 서브쿼리를 사용해 문제를 분할하면 생각하기 쉬워진다. **생각의 보조 도구** +- 집합을 세세한 부분으로 나누는 기술으로 bottom-up 타입의 사고방식과 좋은 상성을 가진다. +- 효율적인 코드가 되지는 않는다. + +## 22강 서브쿼리 사용이 더 나은 경우 + +### 1. 결합과 집약 순서 + +결합할 때 사람이 직접 연산 순서를 명시해주면 성능 개선 가능 + +여러 사업소가 한 회사에 속하는 두개의 일대다 부모자식 관계 테이블이 존재한다. + +문제: 회사(district)마다 주요 사업소의 총 직원수(sum_emp)를 구하기 + +1. 결합 후 집약하는 방법 + +``` +SELECT C.co_cd, MAX(C.district), + SUM(emp_nbr) AS sum_emp +FROM Companies C + INNER JOIN + Shops S + ON C.co_cd=S.co_cd +WHERE main_flg='Y' +GROUP BY C.co_cd; +``` + +회사 테이블과 사업소 테이블의 결합 수행 후 결과에 GROUP BY를 적용해 집약한다. + +2. 집약 후 결합하는 방법 + +``` +SELECT C.co_cd, C.district, sum_emp +FROM Companies C + INNER JOIN + (SELECT co_cd, SUM(emp_nbr) AS sum_emp + FROM Shops + WHERE main_flg='Y' + GROUP BY co_cd) CSUM + ON C.co_cd=CSUM.co_cd; +``` + +사업소 테이블 집약해 직원수 구한 후 회사 테이블과 결합한다. + +**- 겷합 대상 레코드 수** +결합 대상 레코드 수를 판단했을때, 위 두 방법은 성능적으로 큰 차이가 있다. + +- 회사 테이블: 레코드 4개 +- 사업소 테이블: 레코드 10개 + +- 회사 테이블: 레코드 4개 +- 사업소 테이블(CSUM): 레토드 4개 + +CSUM 뷰가 회사 코드로 집약되어 4개로 압축되었다. → 첫번째보다 결합 비용을 낮출 수 있다. + +이는 데이터가 많을수록 더 큰 차이를 보인다. 회사 테이블의 규모에 비해 사업소 테이블의 규모가 매우 크다면 결합 대상 레코드수를 집약하는 편이 I/O 비용을 줄일 수 있다. (비록 두번째 방법에서 집약 비용이 더 크긴하나, TEMP 탈락이 발생하지 않는 다면 괜찮은 트레이드 오프) + +환경 역시 의존적이다. 테이블 레코드 개수뿐만 아니라 HW, MW, 결합 알고리즘 등 요소를 모두 포함 → 실제 개발시 요인을 고려할 것. + +선택 지 중 하나로 "사전에 결합 레코드 수를 합축한다" 를 알면 좋다. + +# 8장 SQL의 순서 + +## 23강 레코드에 순번 붙이기 + +### 1. 기본키가 한 개의 필드일 경우 + +**- 윈도우 함수를 사용** + +``` +SELECT student_id, + ROW_NUMBER() OVER (ORDER BY student_id) AS Seq + FROM Weights; +``` + +ROW_NUMBER 함수를 사용한다. + +**- 상관 서브쿼리를 사용** + +``` +SELECT student_id, + (SELECT COUNT(*) + FROM Weight W2 + WHERE W2.student_id <= W1.student_id) AS Seq + FROM Weights W1; +``` + +재귀 집합을 만들고 요소 수를 COUNT 함수로 센다. 기본키 student_id를 비교 키로 사용 → 재귀 집합의 요소가 한 개씩 증가한다. (순번 생성시 자주 사용하는 트릭) + +기능은 동일하지만 첫번째 방법의 성능이 좋다. 스캔 횟수 1회 / 2회 + +### 2. 기본 키가 여러 개의 필드로 구성되는 경우 + +기본 키가 두 개인 경우의 테이블 +**- 윈도우 함수를 사용** + +``` +SELECT class, student_id, + ROW_NUMBER() OVER (ORDER BY class, student_id) AS seq + FROM Weights2; +``` + +**- 상관 서브쿼리를 사용** + +``` +SELECT class, student_id, + (SELECT COUNT(*) + FROM Weights2 W2 + WHERE (W2.class, W2.student_id) <= (W1.class, W1.student_id) ) AS seq + FROM Weights2 W1; +``` + +**다중 필드** 비교하기(문자, 숫자, 3개 비교, 기본 키 인덱스도 사용 가능) + +**- 그룹마다 순번을 붙이는 경우** + +테이블을 그룹으로 나누고 그룹마다 내부 레코드에 순번을 붙인다. + +**- 윈도우 함수를 사용** + +``` + SELECT class, student_id, + ROW_NUMBER() OVER(PARTITION BY class ORDER BY student_id) AS Seq + FROM Weight2 +``` + +class 필드에 PARTITION BY 를 적용한다. + +**- 상관서브쿼리를 사용** + +``` +SELECT class, student_id, + (SELECT COUNT(*) + FROM Weights2 W2 + WHERE W2.class = W1.class AND W2.student_id <= W1.student_id) AS seq + FROM Weights2 W1; +``` + +### 4. 순번과 갱신 + +검색이 아닌 갱신에서 순번을 매기는 방법 - 테이블에 순번 필드 (seq)를 만들고, 순번을 갱신하는 UPDATE 구문을 만든다. + +**-윈도우 함수를 사용** + +``` +UPDATE Weights3 + SET seq = (SELECT seq + FROM ( SELECT class, student_id, + ROW_NUMBER() OVER (PARTITION BY class ORDER BY student_id) AS seq + FROM Weights3) SeqTbl + WHERE Weights3.class = SeqTbl.class + AND Weights3.student_id = SeqTbl.student_id); +``` + +SeqTbl라는 서브쿼리로 테이블을 만들어 class 그룹마다 순번 매긴 값을 seq 컬럼에 업데이트한다. + +**- 상관 서브쿼리를 사용** + +``` +UPDATE Weights3 +SET seq = (SELECT COUNT(*) + FROM Weights3 W2 + WHERE W2.class = Weights3.class AND W2.student_id <= Weights3.student_id); +``` + +## 24강. 레코드에 순번 붙이기 응용 + +순번의 성질 **연속성**, **유일성**을 이용하여 다양한 테크닉을 사용할 수 있다. + +### 1. 중앙값 구하기 + +중앙값: 숫자를 정렬하고 양쪽 끝부터 수를 셀 때 정중앙에 오는 값, 평균값에 비해 outlier에 영향 받지 않는다는 장점이 존재한다. 단순 평균(mean)과 다르게 아웃라이어에 영향을 받지 않는다. + +\+ 중앙값 구하는 방법 + +레코드 개수가 홀수일 때: 중앙의 값을 사용 +레코드 개수가 짝수일 때: 중앙의 두 값을 평균내어 사용 + +**- 집합 지향적 방법** + +``` +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; +``` + +테이블을 상위 집합과 하위 집합으로 분할하고 공통 부분을 검색하는 방법이다. 집합 지향적인 발상에 기반한 _SQL스러운_ 방법이다. +![문서 13_1](https://github.com/yubin21/db-db-deep/assets/80163835/07046853-9daf-4bbd-ae46-8e816908923b) + +1. 코드가 복잡해서 무엇을 하고 있는 것인지 한 번에 이해하기 어렵다. +2. 자기 **결합**(w1과 w2간)을 수행하여 성능이 나쁘다. + +**- 절차 지향적 방법 1 - 세계의 중심을 향해** + +sql에서 자연수의 특징을 활용하면 ‘양쪽 끝부터 숫자 세기’를 할 수 있다 + +``` +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); +``` + +만약 홀수일 경우 hi=lo가 될 것이고 짝수일 경우 hi 값은 lo-1, lo+1 중 하나가 될 것이므로 IN 연산자로 한꺼번에 비교한다. RANK 또는 DENSE_RANK를 사용해서는 안된다. 순위가 겹치거나 빌 수 있다. +테이블 접근 1회로 감소, 대신 정렬이 2회로 늘었다. ROW_NUMBER에서 사용하는 정렬이 오름/내림차순 2개라서 그렇다. + +주의 1) RANK, DENSE_RANK 대신 ROW_NUMBER 함수를 사용해야 레코드 집합에 자연수 할당해서 연속성과 유일성 가질 수 있음 +주의 2) ORDER BY 정렬 키에 weight 뿐만 아니라 student_id도 포함해야 정확한 결과 나옴 + +집합 지향적 방법과 비교할 때 **결합이 제거**되었고 **정렬이 1회 늘어**났으므로 성능 개선되었다고 볼 수 있다. +![문서 13_4](https://github.com/yubin21/db-db-deep/assets/80163835/6c7ec4ef-d7f7-4966-abf8-6f59b3962dcc) + +**- 절차 지향적 방법 2 - 2빼기 1은 1** +성능적으로 개선하기 + +``` +SELECT AVG(weight) AS median +FROM (SELECT weight, + 2 * ROW_NUMBER() OVER (ORDER BY weight) - COUNT(*) OVER() AS diff + FROM Weights) TMP +WHERE diff BETWEEN 0 AND 2; +``` + +ROW_NUMBER() == (모든 레코드 개수의 절반±1)이 될 때 중간 값이라고 볼 수 있다. + +절차 지향적 방법 (1)과 비교할 때 **정렬이 1회 줄어들었다.** 이 방법이 SQL 표준으로 중앙값을 구하는 가장 빠른 방법이다. + +### 2. 순번을 사용한 테이블 분할 + +테이블을 여러 개의 그룹으로 분할하는 문제 + +**- 단절 구간 찾기** +비어있는 숫자를 다음처럼 출력하기 + +``` +gap_start ~ gap_end +--------- - ------- + 2 ~ 2 + 5 ~ 6 + 10 ~ 11 +``` + +**- 집합 지향적 방법 - 집합의 경계선** + +``` +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); +``` + +N2.num을 사용해 현재 레코드 값 N1.num 보다 큰 숫자의 집합을 조건으로 지정한다. + +min(N2.num)으로 N1.num의 바로 다음 숫자를 지정해 차이가 1보다 클 경우 비어있는 숫자로 간주한다. + +집합 지향적 방법은 반드시 **자기 결합**을 사용하므로 리스크가 있다. + +**- 절차 지향적 방법 - '다음 레코드'와 비교** +'현재 레코드와 다음 레코드를 비교해 차이가 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; +``` + +레코드의 순서를 활용하여 현재 레코드 값(num)과 다음 레코드의 숫자 차이(diff)를 비교한다. + +테이블 접근 1회, 정렬 1회이며 정렬이 사용되지만 결합을 사용하지 않아 성능이 안정적이다. + +### 3. 테이블에 존재하는 시퀀스 찾기 + +지금까지와는 반대로 테이블에 존재하는 수열을 그룹화한다. + +**- 집합 지향적 방법 - 다시, 집합의 경계선** + +``` +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; +``` + +자기 결합으로 num 필드 조합 만든 후 MIN, MAX 값으로 경계를 구하는 방식이다. +자기 결합 수행 후 극치 함수(MIN, MAX)로 집약을 수행한다. + +**- 집합 지향적 방법 - 다시, 다음 레코드 하나와 비교** + +``` +SELECT low, high +FROM (SELECT low, + CASE WHEN high IS NULL + THEN MIN(high) OVER(ORDER BY seq + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + ELSE high END AS high + FROM (SELECT CASE WHEN COALESCE(prev_diff,0) <>1 + THEN num ELSE NULL END AS low, + CASE WHEN COALESCE(next_diff,0) <>1 + THEN num ELSE NULL END AS high, + seq + FROM (SELECT num, + MAX(num) OVER (ORDER BY num ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) - num AS next_diff, + num-MAX(num) OVER (ORDER BY num ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) - num AS prev_diff, + ROW_NUMBER() OVER (ORDER BY num) As seq + FROM numbers) TMP1) TMP2) TMP3 +WHERE low IS NOT NULL; +``` + +1. TMP 1 서브쿼리 + +현재 레코드와 전후 레코드의 차이를 구해 prev_diff, next_diff 저장한다. +차이가 1보다 크면 비어있는 부분이 존재한다. + +2. TMP 2 서브쿼리 + +CASE 식으로 차이가 1보다 큰지 확인해 각 시퀀스의 양쪽 지점이 되는 값(low, high 필드)을 계산한다. + +3. TMP 3 서브쿼리 + +high값이 없는 레코드에 현재 레코드 이후의 레코드들을 돌며 가장 작은 high값을 가져온다. +low값이 없는 레코드는 무시한다. + +서브쿼리의 크기에 따라 중간 결과를 메모리에 유지할지 저장소를 사용할지 결정되므로 성능 측면에서 집합 지향 쿼리에 비해 좋은지 알 수 없다. + +## 25강. 시퀀스 객체, IDENTIFY 필드, 채번 테이블 + +표준 SQL에는 순번을 다루는 기능으로 시퀀셜 객체나 IDENTIFY 필드가 존재한다. 하지만 **최대한 사용하지 않는다**. 사용한다면 시퀀스 객체를 사용한다. + +### 1. 시퀀스 객체 + +``` +CREATE SEQUENCE testseq +START WITH 1 +INCREMENT BY 1 +MAXVALUE 100000 +MINVALUE 1 +CYCLE; +``` + +테이블 또는 뷰처럼 스키마 내부에 존재하는 객체 중 하나로 CREATE문으로 생성한다. INSERT 구문에서 흔히 사용된다. + +초깃값, 증가값, 최댓값, 최솟값, 최댓값에 도달했을 때 순환 유무 등의 옵션을 지정할 수 있으며, 시퀀스 객체가 생성하는 순번은 유일성, 연속성, 순서성을 가진다. + +**- 시퀀스 객체의 문제점** + +- 표준화가 늦어서, 구현에 따라 구문이 달라 이식성이 없고, 사용할 수 없는 구현도 있다. +- 시스템에서 자동으로 생성되는 값이므로 실제 엔티티 속성이 아니다. +- 성능적인 문제를 일으킨다 + +**- 시퀀스 객체로 발생하는 성능 문제** + +시퀀스 객체가 생성하는 순번은 세 가지 특성을 가진다. +**순서성(순번의 대소 관계가 유지됨), 유일성, 연속성** + +사용자가 시퀀스 객체에서 NEXT VALUE를 검색할 때 처리 과정은 아래와 같음 + +1. 시퀀스 객체에 배타 락 적용 +2. NEXT VALUE 검색 +3. CURRENT VALUE 1만큼 증가시킴 +4. 시퀀스 객체에 배타 락 해제 + 위와 같은 과정으로 인해 동시에 여러 사용자가 시퀀스 객체에 접근할 경우 락 충돌으로 인한 성능 저하 문제 발생 + +대처법: CACHE, NOORDER 객체로 성능 문제 완화 가능 + +**- 시퀀스 객체로 발생하는 성능 문제의 대처** + +(1) CACHE +읽어들일 변수를 메모리에 설정하는 것. 값이 클수록 접근 비용을 줄일 수 있다. 다만 시스템 장애시 정상동작을 담보할 수 없다. + +(2) NOORDER +순서성을 담보하지 않음으로써 오버 헤드를 줄인다. + +**- 순번을 키로 사용할 때의 성능 문제** +DBMS의 저장 방식으로 인해 순번처럼 비슷한 데이터 연속으로 INSERT 시 물리적으로 같은 영역에 저장되어 특정 물리적 블록에만 I/O 부하 커져 성능 저하가 발생한다. = Hot spot, Hot block + +시퀀스 객체를 사용해 INSERT를 반복하는 경우 발생하고, 대처가 불가능하다. + +\* 핫 스팟: I/O 부하가 몰리는 특정 물리적 블록 +![문서 13_2](https://github.com/yubin21/db-db-deep/assets/80163835/45a18015-051b-4eb3-92ba-2bece84e0371) + +**- 순번을 키로 사용할 때의 성능 문제에 대처** + +(1) Oracle의 열 키 인덱스 +연속된 값을 도입하는 경우라도 DBMS 내부에서 변화를 주어 제대로 분산할 수 있는 구조를 사용한다. +\- I/O양이 늘어나 SELECT 구문 성능이 나빠질 수 있으며 구현의존적 방법이다. + +(2) 인덱스에 복잡한 필드를 추가해서 데이터의 분산도를 높인다. +\- 복잡한 필드 추가할 경우 불필요한 의미를 생성하므로 다른 개발자가 이해하기 어려울 수 있어 논리적으로 좋은 설계가 아니다. + +**→ 시퀀스 객체는 최대한 사용하지 말아야 하며, 리스크를 확실하게 인지하고 사용하자** + +### 2. IDENTIFY 필드 + +‘자동 순번 필드’라고도 한다. 테이블의 필드로 정의하고, INSERT 발생할 때마다 자동을 순번을 붙여주는 기능이다. + +시퀀스 객체에 비해 단점이 많다. + +- 시퀀스 객체는 여러 테이블에서 사용 가능하지만, IDENTIFY 필드는 특정 테이블에 국한된다. +- CACHE, NOORDER를 지정할 수도 없거나 제한적으로만 사용할 수 있다. + +**→ 이점이 거의 없다.** + +### 3. 채번 테이블 + +순번을 부여하기 위해 어플리케이션에서 채번 테이블이라는 것을 만들어 사용했었다. 테이블을 활용해 유사적으로 시퀀스 객체 락 메커니즘을 구현한 것이다. + +구시대 유물이며 문제가 안생기기를 바라는 것이 최선이다.(바틀넥이 걸려도 튜닝할 방법도 없다) + +## 추가) TEMP 탈락 현상 + +**SQL 레벨업 - “4장 집약과 자르기”에서 이미 나온 개념. 이 교재에서만 정의된 용어!** + +→ "인 메모리 부족 현상으로 인한 스왑현상" 정도로 이해할 수 있을 것 같다 + +💡 142p. + +--- + +하지만 정렬과 해시 모두 메모리를 많이 사용하므로, 충분한 해시용 워킹 메모리가 확보되지 않으면 스왑이 발생한다. 따라서 저장소 위의 파일이 사용되면서 굉장히 느려진다. + +예를 들어, 오라클에서 정렬 또는 해시를 위해 PGA라는 메모리 영역을 사용한다. 이때 PGA 크기가 집약 대상 데이터양에 비해 부족하면 일시 영역(저장소)을 사용해 부족한 만큼 채운다. + +위 현상을 TEMP 탈락이라 한다. 이 현상이 발생하면 메모리만으로 처리가 끝나는 경우와 비교해 극단적으로 성능이 떨어지게 된다. 메모리와 저장소(일반적으로 디스크)의 접근속도가 굉장히 차이나기 때문이다. + +### TEMP 영역을 자동으로 확장하게 만들 수 있는 DBMS도 존재한다. + +**1. Oracle의 `AUTOEXTEND`** + +**`TABLESAPCE_AUTOEXTEND` : 테이블스페이스에 연결되어 있는 데이터 파일의 용량이 자동으로 확장하도록 설정** + +```sql +▶ 용량 부족 시 테이블스페이스가 자동으로 ~MB씩 증가하며 최대 ~MB까지 확장하도록 설정 +[ SQL> alter database datafile '경로/파일명.dbf' +autoextend on next nm(증가치) maxsize nm(최대용량); ] +``` + +**2. Microsoft SQL Server의 `SSMS & T-SQL`** + +TEMPDB 데이터베이스를 사용하여 TEMP 테이블스페이스 역할을 수행. TEMPDB의 크기를 동적으로 관리하려면 SQL Server Management Studio(SSMS) 또는 T-SQL을 사용하여 TEMPDB 설정을 변경할 수 있음. [공식 문서](https://learn.microsoft.com/ko-kr/azure/azure-sql/managed-instance/tempdb-configure?view=azuresql&tabs=ssms) 참조