- 특정 칼럼의 값으로 레코드를 그룹핑하고 각 그룹별로 집계된 결과를 하나의 레코드로 조회할 때 사용한다.
- 쿼리에 그룹바이가 사용되면 그룹바이에 명시된 그룹키가 아닌 칼럼은 일반적으로 집합 함수를 감싸서 사용해야 한다. 이 규칙을 지키지 않으면 다른 DBMS에서는 에러가 나지만, 마스에서는 그룹키가 아닌 칼럼이더라도 쿼리에서 집합 함수 없이 그냥 사용할 수 있다.
SELECT first_name FROM employees GROUP BY gender;
SELECT first_name, last_name, COUNT(*)
FROM employees
GROUP BY first_name ORDER BY last_name;
# GROUP BY gender로 해야할듯? => 아니고 아래의 한글 설명이 그냥 잘못 적혀있다.
- 두 쿼리 모두 gender칼럼으로 그룹바이하고 있지만 셀렉트나 오더바이에는 그룹바이에 명시되지 않은 칼럼이 집합함수로 감싸지지 않고 그대로 사용 됐다. 마스에서 허용은 되지만 문제는 이 쿼리가 실행되면 first_name 이라는 칼럼이 어떤 값을 가져올지 예측할 수 없다는 것이다.
- 첫 번째 쿼리는 gender로 GROUP BY를 수행했기 때문에 결과 레코드는 2건이 반환될 것이다. 하지만 셀렉트하는 칼럼은 젠더 칼럼이 아니라 first_name 을 조회한다. 이럴 경우에 반환되는 first_name 은 남녀 성별로 한 건씩만 가져오긴 할 것이다. 하지만 그 값이 제일 큰지, 작은지, 중간인지 알 수 없다.
- 두 번째 쿼리 또한 first_name 으로 그룹바이를 수행해서 last_name으로 정렬한다. 이 또한 first_name이 동일한 여러 사원들 중 어느 사원의 last_name을 가지고 정렬을 수행했는지 보장할 수 없는 쿼리다. 그래서 실제 조회 결과에 정렬되지 않은 채로 출력될 때도 있다.
- 그렇기에 가급적 GROUP BY 절에 명시되지 않은 칼럼은 반드시 집합함수로 감싸서 사용하자. 이를 강제하는 문법은 FULL GROUP-BY를 사용하면 된다. sql_mode 시스템 변수는 마스 서버의 여러가지 실행 방법상의 모드르 설정하는데 ONLY_FULL_GROUP_BY를 설정하면 된다.
- 마스는 그룹바이를 그루핑 칼럼 순서대로 정렬까지 수행한다. 하지만 실제로는 정렬이 필요하지 않은 경우가 있다. 이래서 성능이 느린 경우가 있다. 이럴 때에 정렬을 하지 않도록 처리하는 방법이 뒤에 ORDER BY NULL을 붙이는 것이다.
EXPLAIN
SELECT from_date
FROM salaries
GROUP BY from_date;
- Using filesort가 있기에 정렬이 동반됐음을 알 수 있다.
EXPLAIN
SELECT from_date
FROM salaries
GROUP BY from_date;
- 정렬이 안되었다.
- 잘 사용하지 않지만 마스는 그룹바이 절의 칼럼에 정렬 순서를 명시할 수 있다. 마스의 그룹바이가 정렬까지 수행하기에 이런 문법이 가능하다.
SELECT title, from_date
FROM titles
GROUP BY title DESC, from_date ASC;
- 이 방법도 오더바이에 명시되는 것 과 같이 각 칼럼의 정렬 순서가 혼용되면 인덱스를 사용할 수 없다. 그렇기에 이럴 경우에는 이 방식을 사용하기 보다는 별도의 ORDER BY 절을 사용하자.
- 그룹바이가 사용된 쿼리에서는 그루핑된 그룹별로 소계를 가져올 수 있는 롤업 기능이 있다. 롤업으로 출력되는 소계는 단순히 최종 합만 가져오는 것이 아니라 그룹바이에 사용된 칼럼의 개수에 따라 소계의 벨이 달라진다. 엑셀의 피벗 테이블이라고 생각하면 된다.
SELECT dept_no, COUNT(*) FROM dept_emp
GROUP BY dept_no
WITH ROLLUP;
- dept_emp 테이블을 부서 번호로 그루핑하는 예제 쿼리다.
- 롤업과 함께 사용되었기에 각 그룹별로 소계를 출력하는 레코드가 추가되어 표시된다. 소계 레코드의 칼럼 값은 항상 NULL임을 유의하자. 그룹바이 절에 dept_no 칼럼 1개만 있기 때문에 소계가 1개만 존재하고 이 칼럼 값은 널로 표기 됐다.
- 아래는 칼럼이 2개인 것이다.
SELECT first_name, last_name, COUNT(*)
FROM employees
GROUP BY first_name, last_name
WITH ROLLUP;
- 소계가 2단계로 표시된다. 우선 first_name 그룹별로 소계가 표시되고 마지막은 전체로 표시된다.
- 이 기능은 ORDER BY와 사용할 수 없다. 그리고 LIMIT와 사용할 경우에는 결과가 이상할 수 있다.
- 그룹바이나 집합 함수를 통해 레코드를 그룹핑할 수 있지만 하나의 레코드를 여러 개의 칼럼으로 나누거나 변환하는 SQL 문법은 없다. 하지만 SUM()이나 COUNT()와 같은 집합 함수와 CASE WHEN ... END 구문을 이요해 레코드를 칼럼으로 변환하거나 하나의 칼럼을 조건으로 구분해서 2개 이상의 칼럼으로 변환하는 것이 가능하다.
레코드를 칼럼으로 변환
- 부서별로 사원 수를 확인하는 쿼리다.
SELECT dept_no, COUNT(*) AS emp_count FROM dept_emp GROUP BY dept_no;
- 이렇게 결과를 만들었다. 그런데 이런 결과를 자주 만들어야 할 경우가 생긴다면 어떨까? 위의 결과를 SUM(CASE WHEN ...)으로 한번 더 변환하면 된다
SELECT
SUM(CASE WHEN dept_no='d001' THEN emp_count ELSE 0 END) AS count_d001,
SUM(CASE WHEN dept_no='d002' THEN emp_count ELSE 0 END) AS count_d002,
SUM(CASE WHEN dept_no='d003' THEN emp_count ELSE 0 END) AS count_d003,
SUM(CASE WHEN dept_no='d004' THEN emp_count ELSE 0 END) AS count_d004,
SUM(CASE WHEN dept_no='d005' THEN emp_count ELSE 0 END) AS count_d005,
SUM(CASE WHEN dept_no='d006' THEN emp_count ELSE 0 END) AS count_d006,
SUM(CASE WHEN dept_no='d007' THEN emp_count ELSE 0 END) AS count_d007,
SUM(CASE WHEN dept_no='d008' THEN emp_count ELSE 0 END) AS count_d008,
SUM(CASE WHEN dept_no='d009' THEN emp_count ELSE 0 END) AS count_d009,
SUM(emp_count) AS count_total
FROM (
SELECT dept_no, COUNT(*) AS emp_count FROM dept_emp GROUP BY dept_no
) tb_derived;
- 이러면 아까는 세로로 나오던 정보가 가로로 변환되는 것을 볼 수 있다.
- 원리는 부서별로 9개의 레코드를 한 건의 레코드로 만들어야 하기에 그룹바이된 결과를 서브 쿼리로 만든 후 SUM 함수를 사용 했다. 즉 9개의 레코드를 1건의 레코드로 변환한 것이다. 그리고 부서번호 순서대로 CASE WHEN ..구문을 이용해 각 칼럼에서 필요한 값만 선별해서 SUM 했다. 이럴 떄에 COUNT, MIN, MAX, AVG, SUM 을 잘 사용하면 된다. 단점은 부서 번호가 쿼리의 일부로 사용되기에 번호가 변경될 때에 쿼리도 변경된다는 것이다. 다만 동적으로 쿼리를 생산하는 방법을 사용하면 해결된다.
하나의 칼럼을 여러 칼럼으로 분리
- 위의 쿼리는 단순하게 부서별로 전체 사원의 수만 조회할 수 있는 쿼리였다. 우리는 SUM(CASE WHEN ...)문장을 특정 조건으로 소그룹을 나눠서 사원의 수를 구하는 용도로 사용할 수 있다.
SELECT de.dept_no,
SUM(CASE WHEN e.hire_date BETWEEN '1980-01-01' AND '1989-12-31' THEN 1 ELSE 0 END) AS cnt_1980,
SUM(CASE WHEN e.hire_date BETWEEN '1990-01-01' AND '1999-12-31' THEN 1 ELSE 0 END) AS cnt_1990,
SUM(CASE WHEN e.hire_date BETWEEN '2000-01-01' AND '2009-12-31' THEN 1 ELSE 0 END) AS cnt_2000,
COUNT(*) AS cnt_total
FROM dept_emp de, employees e
WHERE e.emp_no=de.emp_no
GROUP BY de.dept_no;
- dept_emp 테이블로만으로는 사원의 입사일자를 알 수 없기에 employees 테이블을 조인했고 그 결과를 dept_emp 테이블의 dept_no 별로 GROUP BY 를 실행했다. 그리고 그 결과인 부서별 사원의 정보를 CASE WHEN으로 사원의 입사 연도를 구분해서 각각 연도별로 SUM 을 해서 원하는 결과를 만든 것이다.
-
검색된 레코드를 어떤 순서로 정렬할지를 결정한다. 이게 사용되지 않을 경우에는 SELECT 쿼리는 어떤 순서로 결과를 정렬할까?
- 인덱스를 사용한 셀렉트는 인덱스의 정렬 순서대로 레코드를 가져온다.
- 인덱스를 못 쓰고 풀 테이블 스캔을 할 경우에는 MyISAM이라면 테이블에 저장된 순서로 가져온다. 그렇지만 이 순서가 INSERT된 순서는 아니다. 이노디비는 항상 프라이머리 키로 클러스터링 돼 있기에 기본적으로 프라이머리키 순서대로 레코드를 가져온다.
- 셀렉트 쿼리가 임시테이블을 거쳐서 조회되면 순서를 예측하기 어렵다.
-
이런 규칙이 있지만 DBMS는 ORDER BY없는 쿼리에 대해서는 정렬을 보장하지않는다. 그래서 정렬이 필요한 곳에는 항상 오더바이를 사용해야한다.
-
오더바이에서 인덱스를 사용하지 못할 경우에는 추가 정렬이 진행되는데 Using filesort라는 코멘트가 표시된다. File의 의미는 마스서버가 퀵 소트 정렬 알고리즘을 수행했다라고 생각하면 된다.
- 1개 또는 그 이상 여러개의 칼럼으로 정렬을 수행할 수 있다. 정렬 순서는 칼럼별로 다르게 명시할 수 있다. 보통 정렬할 대상은 칼럼명이나 표현식으로 명시하지만 SELECT되는 칼럼의 순번을 명시할 수 있다. 즉 ORDER BY 2라고 하면 SELECT 되는 칼럼들 중에서 2번째 칼럼으로 정렬하라는 의미가 된다.
SELECT first_name, last_name
FROM employees
ORDER BY last_name;
SELECT first_name, last_name
FROM employees
ORDER BY 2;
- 아래의 쿼리처럼 문자열 상수를 사용할 경우에는 옵티마이저가 ORDER BY 절 자체를 무시한다. 비록 칼럼명이라고 하더라도 아래의 쿼리같이 따옴표를 사용해서 문자 리터럴로 표시할 경우에는 상수값으로 정렬하라는 의미가 된다. 컬럼이 아니라 상수 값으로 정렬을 수행하라는 것은 아무 의미가 없기에 오더바이절을 무시해버린다.
SELECT first_name, last_name
FROM employees
ORDER BY "last_name";
- 다른 DBMS에서는 쌍따옴표를 식별자를 표현하기 위해 사용하지만 마스에서는 문자열 리터럴을 표현하는 데이 사용한다.
- 이벤트를 위해 추첨을 할경우에 이 방법을 사용할 수 있다. RAND() 함수로 발생되는 임의의 값을 각 레코드별로 부여하고, 그 임의값으로 정렬을 수행한다. 간단하게 랜덤 레코드를 가져올 수 있다. 하지만 이를 이용한 임의 정렬이나 조회는 절대 인덱스를 이용할 수 없다. 정렬해야 할 레코드가 적으면 문제가 되지 않지만 많을 경우에는 문제가 된다. 인덱스는 변수가 아닌 정해진 값을 순서대로 정렬해서 가지고 있기 때문에 인덱스를 이용한 임의 정렬은 구현할 수 없다. 하지만 반대로 테이블에 미리 임의 값을 별도의 칼럼으로 생성해두고 그 칼럼에 인덱스를 생성해두면 손쉽게 인덱스를 이용한 임의 정렬을 구현할 수 있다.
- 여러 칼럼을 조합해서 정렬시, 칼럼의 정렬 순서가 혼용되면 인덱스를 이용할 수 없다. 만약 ASC와 DESC가 혼용된 정렬이 인덱스를 사용하게 하려면 칼럼의 값 자체를 변형시켜 테이블에 저장하는 것이 유일한 해결책이다.
- 만약 혼용된 정렬이 인덱스를 사용하게 하려면 칼럼의 값 자체를 변형시켜 테이블에 저장하는 것이 유일한 해결책이다. 문자열 타입은 별다른 방법이 없지만 숫자나 날짜 타입은 아래처럼 변경해서 저장하면 된다.
- 숫자는 반대 부호로
- 날짜는 DATETIME, DATE을 타임스탬프로 변환하여 정수타입으로 만든 다음에 음수를 붙인다.
CREATE TABLE tb_member (
region VARCHAR(20),
age INT,
INDEX ix_age_region (age, region)
);
-- // 나이는 역순으로, 지역은 정순으로 조회
SELECT (age * -1) AS age, region
FROM tb_member
ORDER BY age ASC, region ASC;
-
위에서는 오더바이의 두 칼럼이 모두 오름차순이기에 인덱스 정렬 순서와 동일해서 사용가능하다.
-
마스의 정렬에서 NULL은 항상 최소의 값으로 간주하고 정렬을 수행한다. 오름차순의 경우에는 널이 항상 먼저 반환된다. 널에 대한 정렬 순서를 변경하려면 함수를 사용해야 하는데, 이 경우에는 인덱스를 이용할 수 없게 될 수도 있다. 왜냐하면 값이 변경될 수 있기에다.
-
칼럼의 값을 반대로 저장할 수 없을 때에는 여러개의 쿼리로 정렬작업을 진행할 수 있다. 우선 이름을 오름차순으로 정렬하고 다시 지역을 내림차순으로 정렬하려한다. 이 경우에도 역시 혼용되었기에 인덱스를 이용할 수 없다.
CREATE TABLE tb_member (
region VARCHAR(20),
name VARCHAR(20),
INDEX ix_name_region (name, region)
);
INSERT INTO tb_member VALUES ('경기', '홍길동');
INSERT INTO tb_member VALUES ('경기', '이철수');
INSERT INTO tb_member VALUES ('서울', '김영희');
SELECT * FROM tb_member
ORDER BY name ASC, region DESC;
- 위의 예제에서 두 칼럼 모두 문자열이라 반대 값으로 변환할 수 없다. 이때는 앱에서 정렬하려는 쿼리를 쪼개서 실행할 수 있다.
ResultSet rs1 = stmt.executeQuery {
"SELECT name FROM tb_member GROUP BY name ORDER BY name ASC");
while(rs1.next()){
int currentNAme = rs1.getString("name");
ResultSet rs2 = stmt1.executeQuery {
"SELECT name FROM tb_member WHERE name=" + currentName + " ORDER BY region DESC");
while(rs2.next()) P
//여기서 rs2의 레코드를 순서대로 출력시 그 결과가 최종 정렬된 순서다.)
System.out.println(rs2);
}
}
- 위에서는 정렬을 분리해서 실행했다. 그래서 인덱스로만 필요한 작업을 처리한다. 우선 조회된 name칼럼의 값 순서대로 두번째 쿼리를 실행한다. 두번째에서는 name에 대한 동등 비교를 실행하고 region 칼럼에 대해서 역순으로 정렬해서 결과를 가져온다.
- 이런 방법은 정렬해야 할 레코드 건수가 너무 많아서 디스크를 이용해야 할 경우
- 첫번째 정렬 칼럼에 중복된 값이 많아서 두 번째 쿼리의 반복 실행 횟수가 적은 경우에 사용하기 좋다.
- 하나 또는 여러 칼럼의 연산 결과를 이용해 정렬하는 것도 가능하다. 하지만 연산 결과에 의한 정렬은 인덱스를 사용할 수 없기에 가능하다면 피하는 것이 좋다.
SELECT * FROM employees ORDER BY emp_no;
SELECT * FROM employees ORDER BY emp_no+10;
- 첫번째 쿼리는 프라이머리 키 순서대로 읽기만 하면 된다.
- 하지 두 번째 쿼리는 동일한 순서를 만들어내는 쿼리임에도 불구하고 옵티마이저는 이를 최적화하지 못하고 별도의 정렬을 진행한다. 즉 오더바이 절에 인덱스에 명시된 칼럼의 값을 조금이라도 변형하면 인덱스를 이용하여 정렬이 불가능하다.
표현식의 결과 순서가 칼럼의 원본 값 순서와 동일할 때
- 위의 예제와 같이 오더바이 절의 표현식에 의해 변형된 값 자체가 변형 이전의 값과 순서가 동일하다면 변형되지 않은 칼럼을 그대로 사용해주는 것이 인덱스를 이용한 정렬을 사용하는 유일한 방법이다.
표현식의 정렬 순서가 칼럼의 원본 값과 다른 경우(연산의 결과가 칼럼의 값에만 의존적인 경우)
-
미리 표현식의 연산 결과를 위한 별도의 칼럼을 추가해서 레코드가 추가되거나 갱신될 때에 해당 칼럼을 계속 업데이트하는 방식이 최선이다. 마스는 함수를 이용한 인덱스가 없기에 표현식의 결과를 저장하는 별도의 칼럼을 생성해야 한다. 그리고 그 칼럼에 인덱스를 생성하고 표현식의 정렬이 필요할 때는 이미 연산 결과값이 저장된 칼럼으로 ORDER BY를 사용하는 것이다.
-
아래는 회원 테이블에서 나이가 30살을 기준으로 가까운 나이 순서대로 정렬하는 요건이다.
SELECT * FROM tb_member ORDER BY ABS(member_age - 30);
- 위에서 member_age 칼럼에 인덱스가 준비돼 있더라도 별도의 정렬을 거쳐야한다. 하지만 ABS(member_age-30)표현식의 결과는 member_age 칼럼값에만 의존적이다. 다음과 같이 연산의 결과를 저장하는 emeber_age_diff 칼럼에 인덱스를 생성하고 정렬을 수행하면 된다.
CREATE TABLE tb_member(
member_id VARCHAR(20) NOT NULL,
member_age SMALLINT NOT NULL,
member_age_diff SMALLINT NOT NULL
...
INDEX ix.agediff (member_age_diff)
);
SELECT * FROM tb_member ORDER BY member_age_diff;
- 위의 경우가 아니더라도 URL과 같이 긴 문자열에 프라이머리 키나 보조 인덱스를 만들어야 할 때도 있다. 하지만 마스에서 인데스키는 최대 765바이트 이상을 넘을 수 없다는 제약이 있기에 이럴 경우 앞 부분만 잘라서 인덱스를 생성한다. 비록 이렇더라도 765바이트는 크다. 그래서 MD5와 같은 방법을 사용해서 16바이트로 관리하기도 한다. 이렇게 원본 칼럼으로부터 어떤 연산의 결과를 따로 저장하는 칼럼을 추출 칼럼이라고도 한다.
표현식의 정렬 순서가 칼럼의 원본 값과 다른 경우(연산의 결과가 칼럼 이외의 값에 의존적인 경우)
- 이러한 경우는 어떤 방식을 사용해도 인덱스를 사용해서 정렬할 수 없다. 아까 쿼리가 가변적이라고 생각해보자.
SELECT * FROM tb_member ORDER BY ABS(member_age - ?);
- 이럴 경우에는 레코드가 추가되거나 갱신될 때에 미리 연산해두는 것이 불가능하다. 오더바이를 튜닝해서 인덱스를 이용할 수 없다. 그래서 WHERE절의 조건을 최적화해서 정렬해야할 레코드의 건수를 최대한 줄이는 형태로 튜닝하는 것이 좋다. 일반적으로 인ㄷ게슬르 이용해 정렬할 수 없을 때는 쿼리가 가져오는 값의 크기가 크면 클수록 정렬하는 데 더 많은 메모리가 필요하기 때문에 셀렉트되는 칼럼을 최소화하는 것이 좋다.
- 쿼리를 작성할 때 서브 쿼리를 사용하면 단위 처리별로 쿼리를 독립시킬 수 있다. 조인처럼 여러 테이블을 섞어두는 형태가 아니라서 쿼리의 가독성도 높아지고 복잡한 쿼리도 쉽게 작성할 수 있다. 그렇지만 마스 서버는 서브 쿼리를 최적으로 실행하지 못할 때가 많다. 대표적으로 FROM 절에서 사용되는 서브쿼리나 WHERE 절의 IN구문은 가장 최신 버전인 마스 5.5에서도 효율적이지 않다.
- 서브 쿼리는 외부 쿼리에서 정의된 칼럼을 참조하는지 여부에 따라 상관 서브 쿼리와 독립 서브 쿼리로 나눌 수 있다.
-
상관 서브 쿼리
- 서브 쿼리 외부에서 정의된 테이블의 칼럼을 참조해서 검색을 수행할 때 상관 서브쿼리라고 한다. 상관 서브쿼리는 독립적으로 실행되지 못하고 항상 외부 쿼리가 실행된 후 그 결과값이 전달돼야만 서브 쿼리가 실행될 수 있다. 다음 예제에서 EXISTS 이하의 서브 쿼리에서는 dept_emp 테이블에서 지정된 기간 내에 부서가 변경된 사원을 검색하고 있다. 상관 서브쿼리는 외부 쿼리보다 먼저 실행되지 못하기 때문에 일반적으로 상관 서브 쿼리를 포함하는 비교 조건은 범위 제한 조건이 아니라 체크 조건으로 사용된다.
SELECT * FROM employees e WHERE EXISTS (SELECT 1 FROM dept_emp de WHERE de.emp_no=e.emp_no AND de.from_date BETWEEN '2000-01-01' AND '2011-12-30')
-
독립 서브 쿼리
- 다음의 예제 쿼리와 같이 외부 쿼리의 칼럼을 사용하지 않고 서브쿼리에서 정의된 칼럼만 참조하는 경우다. 공식 표현은 self-contained subquery이다. 외부 쿼리와 상관 없이 항상 같은 결과를 반환한다. 그래서 외부 쿼리보다 먼저 실행되어 외부 쿼리의 검색을 위한 상수로 사용되는 편이 일반적이다. 독립 서브 쿼리가 포함된 비교 조건은 범위 제한 조건으로 사용될 수 있다. 하지만 마스에서는 독립 서브 쿼리라 하더라도 효율적으로 처리되지 못할 때가 많다. 아래를 보자.
SELECT de.dept_no, de.emp_no FROM dept_emp de WHERE de.emp_no=(SELECT e.emp_no FROM employees e WHERE e.first_name='Georgi' AND e.last_name='Facello' LIMIT 1)
-
- LIMIT 절과 LOAD DATA INFILE의 파일명에는 사용할 수 없다.
- IN 연산자와 함께 사용할 때에는 효율적으로 처리되지 못한다.
- IN 연산자 안에서 사용하는 서브 쿼리에는 ORDER BY와 LIMIT를 동시에 사용할 수 없다.
- FROM 절에 사용하는 서브 쿼리는 상관 서브 쿼리 형태로 사용할 수 없다. 다음 예제 쿼리에서는 FROM 절의 서브 쿼리가 바깥에서 정의된 departments테이블의 dept_no 를 참조하고 있다. 하지만 이런 형태는 칼럼을 인식할 수 없다는 오류를 남긴다.
SELECT *
FROM departments d,
(SELECT * FROM dept_emp de WHERE de.dept_no=d.dept_no) x
WHERE d.dept_no=x.dept_no LIMIT 10;
=> ERROR 1054 (42522): Unknown column 'd.dept_no' in where clause
- 서브 쿼리를 이용해 하나의 테이블에 대해 읽고 쓰기를 동시에 할 수 없다. 아래를 보라.
UPDATE departments
SET dept_name=(SELECT CONCAT(dept_name, '2') FROM departments WHERE dept_no='d009')
WHERE dept_no='d001';
=> ERROR 1093 (HY000) You can't specify target table 'dpartments' for update in FROM clause
- 아래는 서브 쿼리를 이용해 departments 테이블을 읽고. 조회된 값을 다시 departments 테이블에 업데이트하는 쿼리다. 실제 읽는 레코드와 변경하는 레코드는 다른 레코드지만 현재 모든 버전의 마스에서는 이를 허용하지 않는다. 하지만 이러한 형태의 구문이 꼭 필요하다면 간단히 마스를 속일 수는 있다. departments 테이블을 읽는 서브 쿼리의 결과를 임시 테이블로 저장하도록 쿼리를 변경하는 것이다. 그러면 마스 서버는 임시 테이블을 읽어서 departments 테이블을 변경하는 것으로 인식하기 때문에 문제없이 처리된다. 하지만 이런 방식은 임시테이블이 필요하기에 비추한다. 그리고 데드락의 원인이 되기도 하므로 주의해야 한다.
UPDATE departments
SET dept_name=(
SELECT dept_name
FROM (SELECT CONCAT(dept_name, '2')
FROM departments WHERE dept_no='d009') tab_temp
) WHERE dept_no='d001';
- SELECT 절에 사용된 서브 쿼리는 내부적으로 임시 테이블을 만든다거나 쿼리를 비효율적으로 실행하도록 만들지는 않기 때문에 서브 쿼리가 적절히 인덱스를 사용할 수 있다면 크게 주의할 사항은 없다. 일반적으로 셀렉트 절에 서브 쿼리를 사용하면 그 서브 쿼리는 항상 칼럼과 레코드가 하나인 결과를 반환해야 한다. 그 값이 널이든 아니든 관계없이 레코드가 1건이 존재해야 한다는 것인데 마스에서는 이 체크 조건이 조금 느슨하다.
SELECT emp_no, (SELECT dept_name FROM departments WHERE dept_name='Sales1')
FROM dept_emp LIMIT 10;
OK
SELECT emp_no, (SELECT dept_name FROM departments)
FROM dept_emp LIMIT 10;
ERROR : subquery returns more than 1row
SELECT emp_no, (SELECT dept_no, dept_name FROM departments WHERE dept_name='Sales1')
FROM dept_emp LIMIT 10;
ERROE : operand should contain 1 column
-
첫 번째 쿼리에서 사용된 서브쿼리의 결과는 항상 0건이다. 하지만 이 쿼리는 에러를 발생하지 않고 서브쿼리의 결과는 널로 채워져서 반환된다.
-
두 번째 쿼리에서 서브 쿼리가 두건 이상의 레코드를 반환하는 경우에는 에러가 나면서 쿼리가 종료된다.
-
세 번째 쿼리와 같이 SELECT 절에 사용된 서브 쿼리가 2개 이상의 칼럼을 가져오려고 할 때도 에러가 발생한다. 즉 셀렉트 절의 서브 쿼리에는 로우 서브 쿼리를 사용할 수 없고 오로지 스칼라 서브 쿼리만 사용할 수 있다.
-
가끔 조인으로 처리해도 되는 쿼리를 SELECT 절의 서브 쿼리를 사용해서 작성할 때도 있다. 하지만 서브 쿼리로 실행될 때보다 조인으로 처리할 때가 더 빠르기에 조인을 사용하자
SELECT SQL_NO_CACHE
COUNT(concat(e1.first_name,
(SELECT e2.first_name FROM employees e2 WHERE e2.emp_no=e1.emp_no) )
)
FROM employees e1;
SELECT SQL_NO_CACHE
COUNT(concat(e1.first_name, e2.first_name) )
FROM employees e1, employees e2
WHERE e1.emp_no=e2.emp_no;
- 두 예제 모두 employees 테이블을 두 번씩 프라이머리 키를 이용해 참조하는 쿼리다. 물론, 위 emp_no는 프라이머리 키라서 조인이나 서브 쿼리 중 어떤 방식을 사용해도 같은 결과를 가져온다. SQL_NO_CACE는 성능 비교를 위해 캐시를 사용하지 않는 힌트다. 전자는 0.73 후자는 0.42초가 걸렸다.
- 서브 쿼리가 WHERE 절에서 사용될 때 어떻게 처리되는지 살펴보자. 상관 서브 쿼리는 범위 제한 조건으로 사용되지 못하는데, 이는 마스를 포함한 일반적인 RDBMS 에서도 모두 같다. 그리고 독립 서브 쿼리일 때는 서브 쿼리를 먼저 실행한 후 상수로 변환하고, 그 조건을 범위 제한 조건으로 사용하는 것이 일반적이다. 하지만 마스에서 독립 서브 쿼리를 처리하는 방식은 조금 다르다. 다음 예제로 마스가 독립 서브 쿼리를 처리하는 방법을 살펴보자.
SELECT * FROM dept_emp de
WHERE de.emp_no =
(SELECT e.emp_no
FROM employees e
WHERE e.first_name='Georgi' AND e.last_name='Facello' LIMIT 1);
- dept_emp 테이블을 풀 테이블 스캔으로 레코드를 한 건씩 읽으면서, 서브 쿼리를 매번 실행해서 서브 쿼리가 포함된 조건이 참인지 비교한다. 간단하게 생각해봐도 서브 쿼리를 실행하고 그 결과값을 외부 쿼리의 조건에 상수로 적용하면 훨씬 효율적일 텐데 말이다. 이는 마스 서버에서 서브 쿼리의 최적화가 얼마나 부족한지 잘 보여준다. 외부 쿼리의 비교 조건이 동등 비교가 아니라 크다 또는 작다와 같이 범위 비교조건이더라도 결과는 마찬가지다.
- 실행계획의 첫번째 라인에서는 dept_emp테이블을 읽기 위해 Ix_empno_fromdate 인덱스를 필요한 부분만 레인지 스캔으로 읽었다는 것을 알 수 있다. 이는 두 번째 라인의 서브 쿼리가 먼저 실행되어 그 결과를 외부 쿼리 비교 조건의 입력으로 전달했음을 의미한다. 마스 5.5에 와서야 비로소 서브 쿼리가 조금은 최적화 된 것이라고 할 수 있다.
- WHERE 절에 IN연산자를 상수와 함께 사용할 때는 동등 비교와 독같이 처리되기에 상당히 최적화돼서 실행된다.
SELECT * FROM employees WHERE emp_no IN(10001, 10002, 10010);
- 하지만 IN의 입력으로 상수가 아니라 서브쿼리를 사용하면 처리 방식이 달라진다. 부서명이 Finance인 부서에 소속된 사원들의 사원번호를 조회하는 예제이다.
SELECT * FROM dept_emp de
WHERE de.dept_no IN
(SELECT d.dept_no FROM dpartments d WHERE d.dept_name='Finance');
- 위의 실행계획에서 주의해야할 점은 첫 줄이 ALL(풀테이블스캔)이라는 것과 두번째는 DEPENDENT SUBQUERY라는 것이다. 위의 쿼리의 서브쿼리는 외부 쿼리와 전혀 연관이 없는 독립된 서브쿼리인데 왜 이렇게 표시될까? 왜냐하면 마스의 옵티마이저에 의해 IN(서브쿼리) 부분이 EXISTS(서브쿼리) 형태로 변환되어 실행되기 때문이다. 아래의 서브쿼리에서 de.dept_no라는 칼럼이 조건으로 사용됐는데 이는 마스 옵티마이저가 독립 서브 쿼리를 상관 서브쿼리로 변경해서 실행했기 때문이다.
SELECT * FROM dept_emp de
WHERE EXISTS
(SELECT 1 FROM departments d WHERE d.dept_name='Finamce' AND d.dept_no=de.dept_no);
- 서브쿼리가 상관쿼리로 변경되었기에 외부 쿼리는 풀테이블 스캔을 사용할 수 밖에 없다. 그래서 위의 실행계획 첫 줄이 ALL인 것이다. 만일 최고로 최적화가 잘 되었을 경우는 아래와 같다.
SELECT * FROM dept_emp de WHERE de.dept_no IN('d002');
- 5.5버전까지는 최적화되지 못한다. 그렇기에 다른 형태로 변경해서 사용하는 것이 좋다. 변경시에 서브쿼리에서 사용하는 내부 테이블과 외부 쿼리에서 사용하는 외부 테이블의 관계에 따라 개선 방법이 다른데 이를 비교해보자.
바깥쪽테이블(dept_emp)과 서브쿼리 테이블(departments)의 관계가 1:1이거나 M:1 인 경우
- 조인으로 풀어서 작성한다
SELECT de.*
FROM dept_emp de INNER JOIN dpartments d
ON de.dept_name='Finance' AND d.dept_no=de.dept_no;
1:M인 경우
-
바깥쪽 쿼리와 서브 쿼리를 조인으로 풀어서 작성하면 최종 결과의 건수가 달라질 수 있기에 단순히 서브 쿼리를 조인으로 변경할 수 없다. 이럴 때는 다시 두 가지 방법으로 나눠서 개선할 수 있다. 첫 번째는 다음 쿼리와 같이 조인 후 조인 칼럼(de.dept_no)으로 그루핑해서 결과를 가져오는 것이다. 이럴 경우 GROUP BY를 추가해서 조인하기에 발생한 중복 레코드를 강제로 제거한다. 그리고 그룹바이가 인덱스를 이용해 처리되기 때문에 서브 쿼리보다 성능을 상당히 향상시킬 수 있다.
SELECT de.* FROM dept_emp de INNDER JOIN departments d ON d.dept_name='Finance' AND d.dept_no=de.dept_no GROUP BY de.dept_no;
-
위에도 좋지만 그룹바이 처리가 인덱스를 못 탈 수 있다. 이러면 성능이 더 안좋다. 이럴 경우에는 원본 쿼리에서 서브 쿼리를 분리하는 방법이 있다. 우선 서브 쿼리를 먼저 실행해서 그 연산 결과를 IN 연산자의 입력으로 사용하는 것이다.
ResultSet rs = statement.executeQuerty("SELECT d.dept_no FROM departments d WHERE d.dept_name='Finance'");
ResultSet rs1 = null;
StringBuffer inEnumBuffer = new StringBuffer();
while(rs.nest()){
inEnumBuffer.append(",' ").append(rs.getString("dept_no")).append("' ");
)
rs1 = statemnet.executeQuery("SELECT * FROM dept_emp WHERE dept_no IN ('" + inEnumBuffer.toString() + '")");
- IN보다 비효율적으로 처리되는게 NOT IN이다. 앞에서 IN 형태의 쿼리는 마스 옵티마이저가 EXISTS 패턴으로 변형해서 실행한다는 것을 배웠다. 마찬가지로 이 또한 NOT EXISTS로 변환한다. 아래는 부서가 Finance가 아닌 모든 레코드를 가져온다. 첫 번째는 원래, 두 번째는 옵티마이저가 최적화한 쿼리다.
SELECT * FROM dept_emp de
WHERE de.dept_no NOT IN
(SELECT d.dept_no FROM departments d WHERE d.dept_name='Finance');
SELECT * FROM dept_emp de
WHERE NOT EXISTS
(SELECT 1 FROM departments d WHERE d.dept_name='Finance' AND d.dept_no=de.dept_no);
-
다 좋아보이지만 NOT IN 서브쿼리의 de.dept_no가 NULL이 될 수 있다면 NOT EXISTS 형태로 변환할 수 없다. 그리고 쿼리의 실행 계획에서 Full scan on NULL key라는 메시지가 표시된다. SQL 표준에서는 NULL을 알 수 없는 값으로 정의하는데, 마스도 이런 해석을 따른다. 그러므로 de.dept_no가 NULL일 경우에는 아래의 두 가지 중 경우에 따라 작업을 수행한다.
- 서브 쿼리가 결과 레코드를 한 건이라도 가질 경우
- NULL IN(레코드 결과) ⇒ NULL
- 서브 쿼리가 결과 레코드를 한 건도 가지지 않을 경우
- NULL IN(빈 결과) ⇒False
- 서브 쿼리가 결과 레코드를 한 건이라도 가질 경우
-
결국 마스에서 NOT IN 형태의 최적화는 왼쪽 값이 NULL인지 아닌지에 따라 NOT EXISTS로 최적화를 적용할지 말지가 결정된다. 왼쪽값이 실제로 널이아니라면 NOT EXISTS로 최적화되어 적용되므로 특별히 문제가 되지 않는다. 하지만 널일 경우에 마스 서버는 NOT EXISTS로 최적화를 수행하지 못하고 위의 두 가지 경우 중 어떤 경우인지 판단하기 위해 NOT IN연산자의 오른쪽에 위치한 서브 쿼리가 결과를 한 건이라도 가지는지 판단해야 하낟. 이때는 절때 인덱스를 사용할 수 없다. 이는 옵티마이저가 NOT IN(seubquery)를 최적화하기 위해 trigcond라는 선택적인 최적화 방법을 사용하기 때문이다.
-
NOT IN(subquery)에서 왼쪽 값이 널이 되면 서브 쿼리는 항상 풀 테이블 스캔으로 처리되는데, 이미 위에서 언급한 것처럼 이대는 서브 쿼리의 실행 결과가 한 건이라도 존재하는지, 아니면 한 건도 존재하지 않는지만 판단하면 된다. 그래서 실제로 서브 쿼리가 아무런 조건을 가지지 않는다면 풀 테이블 스캔을 하지만 처음 레코드 한 건만 가져오면 되기 때문에 전혀 성능상 문제가 되지 않는다. 또한 서브 쿼리가 조회하는 테이블의 건수가 몇 건 되지 않는다면 이때도 문제가 되지 않는다. 그런데 서브 쿼리가 자체적인 조건을 가지고 있으면서 테이블의 건수가 많다면 상당히 많은 시간이 걸릴 수도 있다. 서브 쿼리는 풀 테이블 스캔을 통해 조건에 일치하는 레코드 한 건을 가져오는 방식으로 처리되므로 서브 쿼리의 자체 조건에 일치하는 레코드가 희박할수록 성능이 느려진다.
-
결론적으로 마스 옵티마이저는 NOT IN 조건으로 우리가 생각못했던 많은 부가 작업을 한다. 가급적 칼럼이 NULL 가지지 않게 NOT NULL 옵션을 사용하는 것이 좋다. 혹은 아래와 같이 아에 옵티마이져에게 널이아니라고 이야기해주는 방법도 잇다.
SELECT * FROM dept_emp de
WHERE de.dept_no IS NOT NULL
de.dept_no NOT IN(SELECT d.dept_no FROM departments d WHERE d.dept_name='Finance');
- 이제는 이 구문을 개선해보자. 조인으로 풀어서 할 수 있다. LEFT JOIN을 사용한다.
SELECT de.*
FROM dept_emp de
LEFT JOIN departments d ON d.dept_name='Finance' AND d.dept_no=de.dept_no
WHERE d.dept_no IS NULL;
- 서브 쿼리의 테이블을 바깥쪽 쿼리의 테이블에 아우터 조인으로 연결했고 그 결과에서 조인된테이블의 조인 칼럼이 널인 레코드만 가져오는 형태로 개선했다. 결론적으로 이 쿼리는 dept_emp 테이블에는 존재하지만 departments 테이블에는 존재하지 않는 레코드를 가져오는 쿼리로 개선한 것이다.
- 쿼리 튜닝시 가장 먼저 FROM 절의 서브 쿼리를 조인으로 바꾼다. FROM 절에 사용된 서브 쿼리는 항상 임시 테이블을 사용하기에 제대로 최적화 되지 못하고 비효율적일 때가 많다. 더구나 불필요하게 사용된 경우가 많다. 아래는 최적화가 얼마나 안되었는지를 보여준다. 임시 테이블의 사용 횟수를 보면 된다.
- 두번째에는 임시테이블을 1개, 세번째 에는 2개를 사용했다. 딱히 별거 없는 서브쿼리였는데에도 말이다. 위의 예제에서 두번째 SELECT 쿼리의 실행 계획을 보자. DERIVED 가 2번 표시되는 것을 볼 수 있다. 여기서 DERIVED는 FROM 절에 사용된 서브 쿼리에만 나타나며, 우리가 흔히 인라인 뷰라고 하는 것을 의미한다. 마스 옵티마이저는 인라인 뷰를 항상 메모리나 디스크에 임시 테이블 형태로 구체화한다.
- FROM 절에 사용된 서브 쿼리가 만들어 내는 데이터가 작거나, TEXT나 BLOB 등과 같은 대용량 칼럼이 없는 경우에는 메모리에 임시 테이블을 생성하기에 심각한 문제는 아니다. 하지만 너무 크다면 디스크에 임시 테이블을 만들고 그로 인해 병목이 생기게 된다. 물론 메모리에 임시 테이블이 생겨도 문제가 될 수 있다.
- 실제 튜닝을 하다보면 FROM절에는 서브 쿼리가 필요한 경우가 별로 없다. 어쩔 수 없이 사용하는 경우가 대부분이기 때문에 조인으로 잘 풀어서 사용하자.
- 조인이 여러 테이블의 칼럼을 연결하는 것이라면 집합은 여러 테이블의 레코드를 연결하는 방법이다. UNION, INTERSECT, MINUS가 있다.
- UNION : 두 개의 집합을 하나로 묶는 역할을 한다. 그리고 중복을 제거할지에 따라 UNION DISTINCT, UNION ALL로 나뉜다.
- INTERSECT는 교집합을 반환한다.
- MINUS는 첫 번쨰 집합에서 두 번째 집합을 뺀 나머지 결과만 반환한다.
- 집합 연산도 모두 임시 테이블이 필요하다. 그래서 앞선 문제가 그대로 발생한다.
- 마스에서는 자주 사용되는 UNION만 제공한다.
- 중복 레코드에 대해서는 어떻게 판단할까? UNION을 수행할 대상은 이미 임시 테이블로 만들어졌으며 이 임시 테이블에는 중복 체크의 기준이 될 프라이머리 키가 없다. 그래서 집합 연산에서 레코드가 똑같은지 비교하려면 임시 테이블의 모든 칼럼을 비교해야 하는 것이다. 그래서 비교해야 하는 칼럼 값의 길이가 길어지면 더 느려진다.
- ALL, DISTINCT 모두 두 집합의 합을 만들기 위해 버퍼 역할을 하는 임시 테이블을 사용한다. 하지만 ALL은 단순히 임시 테이블만 사용하지만 후자는 집합의 모든 칼럼을 이용해 유니크 인덱스를 생성한다. 두 차이는 단순히 유니크 인덱스를 가지느냐의 차이지만 실제로 이것에 의한 차이는 작지 않다.
-
위에서 알 수 있듯 거의 6배 이상의 차이가 난다. 위의 쿼리는 절대로 중복이 발생할 수 없는 구조임에도 그냥 사용한다면 엄청 느리다.
-
그래도 교집합이 존재한다면 어떻게 교집합을 제거하고나서 ALL로 처리할 수 있는 방법이 없을까를 생각해보고 그래도 없을 경우에 distinct를 사용하자. 그리고 작성한 쿼리에 UNION이 있다면 이를 기준으로 앞 뒤 쿼리를 각각 독립된 쿼리로 분리하자. 그리고 개별 쿼리를 별도로 실행하고, 그 결과를 애플리케이션에서 합치는 방법을 고려하자.
-
가끔 ORDER BY 가 사용된 쿼리를 유니온이나 유니온 올로 결합하는 경우에 Incorrect usage of UNION and ORDER BY라는 오류 메시지가 출력될 때도 있다. 이럴 때는 각 서브 쿼리를 괄호로 감싸고 그 결과를 유니온이나 유니온 올로 처리하면 된다.
- 교집합을 가져오는 행위는 INNER JOIN 과 동일하다.
SELECT emp_no FROM dept_emp WHERE dept_no='d001'
INTERSECT
SELECT emp_no FROM dept_emp WHERE dept_no='d002';
- 마스에서는 이를 지원하지 않는다. 그리고 이너조인으로 할 때가 더 빠르다.
SELECT de1.emp_no
FROM dept_emp de1
INNER JOIN dept_emp de2 ON de2.emp_no=de1.emp_no ADN de2.dept_no='d001'
WHERE de1.dept_no='d002';
- 차집합이다. 조인이나 EXISTS로 구현가능하다.
SELECT emp_no FROM dept_emp WHERE dept_no='d001'
MINUS
SELECT emp_no FROM dept_emp WHERE dept_no='d002';
- 마스는 지원하지 않는다.
SELECT de1.emp_no FROM dept_emp de1
WHERE de1.dept_no='d001'
AND NO EXISTS (
SELECT 1 FROM dept_emp de2
WHERE de2.emp_no=de1.emp_no AND de2.dept_no='d002');
- 처리대상이 많아질 경우는 위에보다 아래의 조인을 사용하는 것이 좋다.
SELECT de1.emo_no FROM dept_emp de1
LEFT JOIN dept_emp de2 ON de2.emp_no=de1.emp_no AND de2.dept_no='d002'
WHERE de1.dept_no='d001'
AND de2.dept_no IS NULL;
- 마케팅부서(d001)에 속한 적이 있는 사원과 재정부서(002)에 속한 적이 있는 사원을 레프트조인으로 한 결과에서 de2.dept_no I NULL조건으로 재정 부서에서 일했던 적이 없는 사원만을 뽑아낸것이다.
- 이노디비 테이블에 대해서는 레코드를 셀렉트할 때 레코드에 아무런 잠금도 걸지 않는다. 하지만 셀렉트 쿼리를 이용해 읽은 칼럼의 값을 애플리케이션에서 가공해서 다시 업데이트하고자 할 때는 다른 트랜잭션이 그 칼럼의 값을 변경하지 못하게 해야 할 때도 있다. 이럴 때는 레코드를 읽으면서 강제로 잠금을 걸어둘 필요가 있다. 이 때 사용하는 명령이다. 둘 다 AUTO-COMMIT이 비활성화된 상태나 BEGIN 명령이나 START TRANSACTION 명령으로 트랜잭션이 시작된 상태에서만 잠금이 유지된다.
- LOCK IN SHARE MODE는 SELECT된 레코드에 대해 읽기 잠금을 설정하고 다른 세션에서 해당 레코드를 변견하지 못하게 한다. 물론 읽기는 가능하다.
- FOR UPDATE는 쓰기 잠금을 설정하고 다른 트랜잭션에서 그 레코드를 변경하는 것뿐아니라 읽지도 못하게 한다.
- 둘은 SELECT 쿼리 문장의 마지막에 추가하면 된다. 아래 두개는 다른 DBMS에서 호환되도록 처리한 것이다.
SELECT * FROM employees WHERE emp_no=10001 LOCK IN SHARE MODE;
SELECT * FROM employees WHERE emp_no=10001 FOR UPDATE;
SELECT * FROM employees WHERE emp_no=10001 /*! LOCK IN SHARE MODE */;
SELECT * FROM employees WHERE emp_no=10001 /*! FOR UPDATE */;
- 위의 옵션이 추가될 경우 COMMIT, ROLLBACK 명령과 함께 잠금이 해제된다. 이외에 잠금만 해제하는 방법은 없다. 그래서 이 둘은 잠금 경합을 많이 발생시키고 데드락도 발생시키는 경우가 많았다.
- 이 명령어는 SELECT 쿼리의 결과를 화면으로 출력하는 것이 아니라 파일로 저장할 수 있다. 테이블 단위로 데이터를 덤프 받아서 적재하거나 엑셀파일이나 다른 DBMS로 옮길 때 유용하다.
- 다만 주의사항이 3가지가 있다.
- 결과는 마스 클라이언트가 아니라 마스 서버가 기동중인 장비의 디스크로 저장된다.
- 결과를 저장할 파일, 저장되는 디렉토리는 마스 서버를 기동중인 운영체제의 계정이 쓰기 권한을 가지고 있어야 한다.
- 이미 동일 디렉터리에 동일 이름의 파일이 있을 때 기존 파일을 덮어쓰지 않고 에러를 발생시키고 종료한다.
SELECT emp_no, firstName, last_name
INTO OUTFILE '/tmp/result.csv'
FIELDS TERMINATED BY ','
LINES TERMINATED BY ' \n'
FROM employees WHERE emp_no BETWEEN 10001 AND 1010
-
윈도우에서는 \r\n을 사용하면 된다. 그리고 디렉터리 구분은 \이다.
-
만약 SELECT된 문자열이 ,나 \n을 포함한다면 값을 나중에 제대로 읽을 수없다. 그럴 경우에는 OPTIONALLY ENCLOSED BY라는 옵션을 사용하면 좋다. 그러면 이 옵션으로 각 칼럼을 감싸서 파일에 저장한다. 이 옵션을 지정하는 경우에 ESCAPED BY 옵션도 같이 사용해주는 것이 좋다. 이 옵션은 앞의 옵션에 정의된 문자를 포함하고 있을 때 어떻게 이스케이프 처리를 할지 설정한다.
SELECT emp_no, firstName, last_name
INTO OUTFILE '/tmp/result.csv'
FIELDS TERMINATED BY ','
OPTIONALLY ENCLOSED BY '"' ESCAPED BY '"'
LINES TERMINATED BY ' \n'
FROM employees WHERE emp_no BETWEEN 10001 AND 1010
- 칼럼의 값을 쌍따옴표로 감싸서 저장했다.
- ESCAPED BY를 잘 줘서 만일 G'eo"r,gi가 있을 때도 잘 적용된다