본문 바로가기

잡다/SQL

[SQL] SELECT 문

SELECT 문은 테이블로부터 특정 정보를 얻어내기 위해 사용하는 명령으로, 대략 다음과 같은 형식을 가진다.

SELECT col1, col2 ....
FROM table1
WHERE condition1 { AND condition2 }
GROUP BY selected_col1, ... [ WTIH ROLLUP ]
HAVING condition1 { AND condition2 }
ORDER BY selected_col1 [ASC|DESC], ...
LIMIT limit_num;
  • SELECT : 테이블로부터 얻어낼 열을 나열
  • FROM : 정보를 얻어낼 대상인 테이블
  • WHERE : 열을 선택하는 조건을 서술
  • GROUP BY : 특정 열을 기준으로 묶어 나타내고 싶은 경우 사용.
  • HAVING : 집계 함수 ( COUNT, SUM, MIN, MAX ... ) 을 포함하는 조건을 서술
  • ORDER BY : 특정 열을 기준으로 전체 정보를 정렬
  • LIMIT : 추출된 정보에서 보여줄 행의 개수를 제한

 MYSQL에서는 대소문자를 구분하지 않으므로 어떻게 작성해도 상관 없으며, 각 명령은 일정한 순서대로 적용된다. 이런 특성에 의해 특정 상황에서는 의도와는 다른 결과가 발생할 수 있다는 점을 고려해야 한다.

 각 구문의 실행 순서는 SQL마다 조금씩 다를 수 있으나, 대략적으로 다음과 같은 순서를 가지는 것으로 보인다.

https://towardsdatascience.com/the-6-steps-of-a-sql-select-statement-process-b3696a49a642

https://sqlbolt.com/lesson/select_queries_order_of_execution

  1. FROM & JOIN : 전체 데이터 셋에서 특정 데이터를 가져온다.
  2. WHERE : 제약조건을 만족하는 데이터 셋만 남긴다.
  3. GROUP BY [ WITH ROLLUP ] : 지정된 열에 기반하여 각 데이터들을 그룹화한다.
  4. HAVING : GROUP BY 절에 등장한 행들에 대해 제약조건을 만족하는 데이터만 남긴다.
  5. SELECT : 표현식을 최종 계산한다.
  6. DISTINCT : 중복을 제거한다.
  7. ORDER BY : 지정된 순서 ( ASC | DESC + 등장 순서 ) 를 기준으로 정렬한다.
  8. LIMIT / OFFSET : 해당 조건을 만족하는 데이터만 남긴다.

 

SELECT

SELECT 문은 특정 테이블로부터 특정 열의 정보를 조회하기 위한 명령문이다. 통상적으로 데이터를 다룰 때 조회의 비중이 대부분을 차지하는 것을 생각해보면 SELECT 명령은 중요한 지위를 가지고 있다고 생각할 수 있다.

위 이미지는 MYSQL에서 선택적으로 제공하는 World 데이터 베이스의 구조를 나타낸 것이다.

country 테이블에서 모든 정보를 가져오면 다음과 같다. 모든 열 정보는 ' * ' 을 통해 가져올 수 있다.

SELECT * FROM country;

 

특정 열의 정보만을 조회할 수도 있다. 이 경우 각 열을 쉼표를 기준으로 나눠 쿼리를 작성한다.

SELECT Code, Name, Continent FROM Country;

WHERE

데이터가 아무리 많아도, 그냥 데이터 자체로는 의미가 없을 수 있다. 예를 우리가 Country 테이블에서 Code가 A로 시작하는 행의 정보가 필요하다고 생각해보자. 

SELECT COUNT(*) FROM Country;

 COUNT 함수를 이용하면 테이블에서 특정 열에 해당하는 데이터 개수를 얻을 수 있다. 이때 해당 쿼리에 따르면 239개의 데이터가 존재함을 알 수 있다. 현재 테이블에는 데이터가 얼마 없어 일일이 분석하면 되겠지만, 만약 데이터가 백만개를 넘어선다면? 사람 수준으로 분석하기는 벅찰 것이다.

 SQL 문법 중 WHERE을 이용하면 이러한 데이터들을 특정 조건으로 필터링할 수 있다. 예를 들어 Code가 A로 시작하도록 조건을 설정할 때는 WHERE문에서 LIKE을 이용할 수 있다.

SELECT Code, Name, Continent FROM Country Where Code LIKE 'A%';

이를 통해 A로 시작하는 코드를 가진 나라들만을 조회할 수 있다.

GROUP BY

 데이터는 자체만으로도 중요하지만, 해당 데이터들의 숫자가 중요한 경우도 존재한다. world 데이터베이스의 city 테이블에는 각 도시 관련 정보가 저장되어 있다. 이때, 특정 국가에 속한 도시의 개수를 알고 싶은 경우를 생각해보자. city 테이블 자체에는 도시의 수 관련 정보가 없기 때문에 여태까지의 방법으로는 각 도시에 대응되는 도시 수를 알 방법이 없다. 이 경우 GROUP BY 를 이용한다.

 GROUP BY 구문은 뒤에 따라오는 열들을 기준으로 데이터를 묶어주는 기능을 하는데, 해당 데이터들에 대해 집계함수를 적용하여 수치적인 계산을 수행할 수 있다.

집계함수에는 다음과 같은 함수등이 있다.

  • COUNT : 데이터의 개수를 반환
  • MIN : 데이터 중 최소값 반환
  • MAX : 데이터 중 최대값 반환
  • AVG : 데이터의 평균 반환
  • SUM : 데이터의 총합 반환
  • VAR_POP : 데이터의 분산 반환
  • STDDEV_POP : 데이터의 표준편차 반환

앞서 언급한 상황( 국가에 속한 도시의 개수 얻기 )에 대해서는 다음과 같은 쿼리를 작성할 수 있다.

SELECT CountryCode, COUNT(Name)
	FROM city
    GROUP BY CountryCode;

당연히 WHERE과 같이 사용할 수도 있다.

SELECT CountryCode, COUNT(Name)
	FROM city
    WHERE CountryCode LIKE 'A%'
    GROUP BY CountryCode;

 GROUP BY 구문 뒤에 따라오는 열들을 인덱스로 치환할 수도 있다. 이때 인덱스는 SELECT 문에서 해당 열이 등장한 순서와 대응되며, 1부터 시작한다.

 위 쿼리에서 GROUP BY 구문의 CountryCode는 SELECT 문에서 1번째로 등장했으므로, 간단하게 1로 치환할 수 있다.

SELECT CountryCode, COUNT(Name)
	FROM city
    WHERE CountryCode LIKE 'A%'
    GROUP BY 1;

WITH ROLLUP

  때로는 수치 데이터 전체에 대한 합, 평균 등을 얻고 싶을 수도 있다. SQL 문법에서는 GROUP BY 구문 뒤에 WITH ROLLUP 구문을 붙여 숫자 데이터에 대한 총계를 얻을 수 있다.

 WITH ROLLUP 구문은 이전까지의 쿼리를 통해 생성한 데이터에서 각각의 열을 대상으로 SELECT 구문에서 사용된 집계 함수를 적용한다. 만약 AVG( 데이터의 평균을 내는 집계 함수 ) 을 사용하면, 대응되는 열의 전체 데이터를 대상으로 평균을 내며, SUM( 데이터를 더하는 집계함수 ) 을 사용하면, 대응되는 열의 전체 데이터를 더하게 된다.

 위에서 적용한 쿼리에서 각 국가의 인구수 총합 및 지역별 평균 인구수와 관련된 열을 추가하고, WITH ROLLUP 구문을 이용하여 전체 데이터에 대한 총계를 얻어보자.

SELECT CountryCode,
		COUNT(Name) 'number of city',
        SUM(Population) 'sum of population',
        AVG(population) 'avg of population'
	FROM city
    WHERE CountryCode LIKE 'A%'
    GROUP BY CountryCode WITH ROLLUP;

 총계를 나타내는 행에서 GROUP BY의 기준이 되는 열인 CountryCode는 현재 테이블 상에서 NULL로 나타나고 있다. 현재 상황에서 이 NULL이 위치한 자리의 값을 총계에 대한 결과라는 의미로 "RESULT" 로 채우고 싶다면 어떻게 해야 할까? 일차원적으로는 IFNULL을 이용할 수 있다. 여기서는 비슷하게 생긴 NULLIF 및 IF도 다루고 가자.

  • IFNULL( target, value ) : target??value 에 대응된다. target이 null이면 value를 반환한다.
  • NULLIF( target, value ) : target이 value와 다르면 target을 반환, 같으면 NULL을 반환한다.
  • IF(condition, then_, else_) : condition을 만족하면 then_  을, 아니면 else_ 을 반환한다.

 현재 시점에서 IFNULL을 이용하여 다음과 같은 쿼리를 작성할 수 있다.

SELECT IFNULL(CountryCode, "RESULT") as CountryCode,
		COUNT(Name) 'number of city',
        SUM(Population) 'sum of population',
        AVG(population) 'avg of population'
	FROM city
    WHERE CountryCode LIKE 'A%'
    GROUP BY CountryCode WITH ROLLUP;

 그러나 이 방법에는 문제점이 존재한다. 실제 데이터베이스 상에는 없지만, 어떤 국가에도 속하지 않는 도시가 있다고 가정해보자. 이 경우 도시에 대한 CountryCode는 존재하지 않으므로 NULL이 된다. 이때 존재하지 않는다는 의미의 NULL과 WITH ROLLUP에 의한 NULL은 IFNULL을 통해 구분할 수 없으므로, 두 값 모두 RESULT가 될 것이다. 물론, 이러한 결과는 우리의 의도와는 거리가 매우 멀다.

 GROUPING 함수는 GROUP BY 및 WITH ROLLUP 에 의해 그룹핑된 데이터와 기존의 데이터를 구분할 때 사용된다. 전달된 칼럼을 기준으로 특정 행이 그룹핑에 의한 것이라면 1을, 아니면 0을 반환한다.

SELECT CountryCode as CountryCode,
		GROUPING(CountryCode),
		COUNT(Name) 'number of city',
        SUM(Population) 'sum of population',
        AVG(population) 'avg of population'
	FROM city
    WHERE CountryCode LIKE 'A%'
    GROUP BY CountryCode WITH ROLLUP;

그룹핑에 의해 생성된 행은 1을 가지게 된다.

 이 함수를 이용하여 IFNULL을 사용한 구문을 변경해보자.

SELECT IF(GROUPING(CountryCode) = 0, CountryCode, "RESULT") as CountryCode,
		COUNT(Name) 'number of city',
        SUM(Population) 'sum of population',
        AVG(population) 'avg of population'
	FROM city
    WHERE CountryCode LIKE 'A%'
    GROUP BY CountryCode WITH ROLLUP;

결과가 정상적으로 출력된다. 현재 City 테이블에서는 큰 차이가 없어 보일 수 있으나, NULL 데이터가 포함되는 순간 완전히 다른 결과가 발생하므로, 그룹핑 결과를 지정하고 싶은 경우에는 반드시 GROUPING을 사용하자.

HAVING

 HAVING 구문은 집계함수를 이용하여 특정 데이터를 필터링할 때 사용된다. 이때 궁금증이 들 수 있다. SQL에서는 특정 데이터를 필터링하기 위한 목적으로 이미 WHERE 구문이 존재하기 때문이다. 그렇다면 왜 HAVING 구문이 따로 존재할 필요가 있는 걸까?

 이전에 데이터를 필터링하기 위한 조건을 설정할 수 있었던 WHERE 문은 SQL 문법상에서 GROUP BY 문보다 선행한다. 이때 SQL의 쿼리는 작성된 순서대로 평가된다는 특징이 있기 때문에, WHERE 구문을 실행하는 시점에서는 아직 데이터 집계가 발생하지 않는다. 따라서 WHERE 구문에서는 집계 함수가 포함된 조건을 처리할 수 없다. 이러한 이유로 집계 함수와 관련된 조건을 처리하기 위한 다른 구문이 필요했고, 이것이 HAVING이다.

 위 쿼리에서 국가 당 도시의 개수가 2개 이상이면서 인구가 10000명 이상인 국가만을 선택하고 싶다면, HAVING 구문을 이용하여 다음과 같이 쿼리를 작성할 수 있다.

SELECT CountryCode,
		COUNT(Name) 'number of city',
        SUM(Population) 'sum of population',
        AVG(population) 'avg of population'
	FROM city
    WHERE CountryCode LIKE 'A%'
    GROUP BY CountryCode
    HAVING Count(Name) >= 2 AND SUM(Population) >= 10000;

이때 위 상황에서 데이터의 총계를 구하기 위해 WITH ROLLUP을 사용해보자. 보통 이 상황에서 WITH ROLLUP을 통해 의도한 것은 "도시가 2개 이상 + 인구가 10000명 이상" 의 조건을 만족하는 행들에 대한 총계일 것이다.

SELECT CountryCode,
		COUNT(Name) 'number of city',
        SUM(Population) 'sum of population',
        AVG(population) 'avg of population'
	FROM city
    WHERE CountryCode LIKE 'A%'
    GROUP BY CountryCode WITH ROLLUP
    HAVING Count(Name) >= 2 AND SUM(Population) >= 10000;

아쉽게도, 우리의 의도와는 전혀 다른 값이 나온다. 정확히 말하자면 HAVING을 통한 필터링이 총계에 영향을 주지 못했다. 총계 자체의 값은 위에서 HAVING 없이 WITH ROLLUP 만을 이용한 값과 다르지 않았다.

HAVING은 WITH ROLLUP에 영향을 주지 않는다.

 WITH ROLLUP은 GROUP BY 구문이 실행되는 시점에 함께 동작한다. 이 시점에서 그룹핑 데이터가 계산된다. 이후 HAVING 문에서 ROLLUP에 의한 새로운 행은 다른 행들과 완전히 동일한 데이터로 간주되어 처리될 뿐, 특수한 계산을 수행하거나 변경되지는 않는다. 즉 그룹핑이 GROUP BY에서 종결되므로 HAVING문에서는 필터링에 영향을 줄 수 없다.

 이러한 동작을 우리의 의도대로 바꾸려면 서브 쿼리 등 다른 방법이 필요하다.

ORDER BY

 HAVING 구문에 의해 데이터가 필터링된 이후, SELECT 문에서는 실제로 조회할 데이터를 계산한다. ORDER BY는 SELECT를 통해 계산된 데이터를 오름차순 혹은 내림차순으로 정렬할 때 사용되는 구문으로, 완성된 데이터를 대상으로 명령을 수행한다.

 위에서의 쿼리에서 도시 및 인구가 많은 순서에 따라 전체 데이터를 정렬하고 싶다면, 아래와 같이 쿼리를 작성한다.

SELECT CountryCode,
    COUNT(Name) 'number of city',
    SUM(Population) 'sum of population',
    AVG(population) 'avg of population'
FROM city
    WHERE CountryCode LIKE 'A%'
    GROUP BY CountryCode WITH ROLLUP
    HAVING Count(Name) >= 2 AND SUM(Population) >= 10000
    ORDER BY 2 DESC, 'sum of population' DESC;

ORDER BY에 전달되는 열은 인덱스, 별칭 혹은 원래 값의 형태를 가질 수 있다. 먼저 선언된 순서대로 정렬이 진행되며, 선언된 열 뒤에 ASC 혹은 DESC를 덧붙여 오름차순 혹은 내림차순 정렬이 가능하다.

LIMIT

 다양한 구문을 사용해 생성한 데이터를 조회하는 경우를 생각해보자. 만약 해당 조건을 만족하는 데이터의 수가 천만개를 넘어간다면, 이를 조회하는데만 꽤 긴 시간을 써야 할 것이다. 이때 LIMIT를 사용하면 한번에 보고 싶은 최대 데이터 개수를 제한할 수 있다. LIMIT 뒤에 단순히 숫자를 전달하면, 최대 해당 갯수만큼의 데이터만 보여준다.

 여태까지의 쿼리에서 최대 5개의 데이터만 보고싶다면, 다음과 같이 쿼리를 작성할 수 있다.

SELECT CountryCode,
		COUNT(Name) 'number of city',
        SUM(Population) 'sum of population',
        AVG(population) 'avg of population'
	FROM city
    WHERE CountryCode LIKE 'A%'
    GROUP BY CountryCode WITH ROLLUP
    HAVING Count(Name) >= 2 AND SUM(Population) >= 10000
    ORDER BY 2 DESC, 'sum of population' DESC
    LIMIT 5;

5개의 데이터만 조회되고 있다.

LIMIT는 최대 조회 갯수만을 지정한 것이므로 데이터가 해당 수치 이하로 존재하면 통상적인 상황과 동일하게 동작한다.

 

요약

 SELECT 구문은 테이블에서 특정 데이터를 조회할 때 사용된다. 각 구문은 특정 순서에 따라 실행되며, 바뀌지 않는다.

 WHERE 및 HAVING 구문은 특정 데이터를 필터링하는데 사용된다. 이때 HAVING은 집계함수를 처리한다.

 GROUP BY는 데이터를 그룹핑하는데 사용되며, WITH ROLLUP을 덧붙여 총계를 얻을 수 있다.

 ORDER BY는 데이터를 정렬하는데 사용되며, ASC 및 DESC를 명시하여 오름차순 혹은 내림차순 정렬을 수행한다.

 LIMIT는 조회할 데이터의 최대 갯수를 지정하는데 사용된다. 

'잡다 > SQL' 카테고리의 다른 글

[SQL] Entity Relationship Model  (0) 2022.01.17
[SQL] DB 및 테이블의 생성, 열람 및 삭제  (0) 2022.01.09
sql 데이터 형  (0) 2022.01.07