아이티윌_데이터 분석 55기/강의내용 필기_SQL

#2) 2일차_SELECT문 : WHERE, GROUP BY, HAVING

ecosso 2026. 3. 8. 17:50

 2일차에 배운 내용을 정리하였다.

 목차는 다음과 같다. :

 

01. WHERE

 -01 비교연산자

 -02 between a and b

 -03 in 연산자

 -04 NULL 연산자

 -05 like 연산자

 

02. GROUP BY

 -01 집계 함수

 -02 group by

 

03. HAVING

 

* 데이터 적재하기


01. WHERE

 출력을 원하는 행을 선택하는 절로, 출력할 대상에 대한 조건을 작성해주어야 한다.

 예를 들어 A 열에 속하는 값 중 10보다 큰 값만 조회하고 싶다면, a >= 10 등 비교대상(컬럼) + 연산자 + 상수의 형태로 작성한다.

 또한 논리연산자(and, or, not) 등을 이용하여 여러 조건을 동시에 나열할 수 있다.

 

 연산자의 종류는 다음과 같다. :

 -01 비교연산자

     =, !=, <>, >, >=, <, <=  (!=를 보통 많이 사용한다.)

     emp 테이블에서 급여(SAL)가 3000 이상인 직원의 이름, 부서번호, 급여를 조회하길 원한다면 다음과 같이 작성한다.

 

select ename, deptno, sal
  from EMP
 where sal >= 3000;

 

 

    SCOTT, KING, FORD 3명이 3000 이상의 급여를 받는 것으로 확인되었다.


 -02 between a and b

    a 이상 b 이하의 범위에 속하는 데이터를 조회하며, 이상과 이하이므로 a와 b의 값 모두를 포함하는 범위에서의 데이터가 조회된다. 물론 논리연산자를 이용해서 작성할 수도 있고 결과는 동일하지만, between a and b를 이용한다면 좀 더 깔끔하게 작성 가능하다.

 

select ENAME, DEPTNO, SAL
  from emp
 where sal >=2000 and sal <=3000;

select ENAME, DEPTNO, SAL
  from emp
 where sal between 2000 and 3000;

 

 또한 a가 b보다 작아야한다. a가 b보다 큰 값을 가지게 된다면 에러는 일어나지 않지만, 그 어떤 값도 조회되지 않는다. 3000이상 1000 이하의 범위에 해당되는 값은 존재하지 않기 때문이다.

 

select ENAME, DEPTNO, SAL
  from emp
 where sal between 3000 and 2000;

 

 

 ENAME, DEPNO, SAL은 존재하는 열이지만, 3000보다 크고 2000보다 작은 수는 존재하지 않으니 그 어떤 행도 조회되지 않음을 확인할 수 있었다.

 

 

 또한 특정 년도에 입사한 직원에 대한 조회도 다음과 같이 작성 가능하다. 

 

select ENAME, HIREDATE, SAL
  from emp
 where HIREDATE between '1987/01/01' and '1987/12/31';

 

 

SCOTT과 ADAMS가 1987년에 입사하였음을 확인할 수 있다.

 


 -03  in 연산자

 in은 포함 연산자로 특정 값이 여러 값을 가진 목록 안에 포함되어 있는지 확인한다. 만약 emp 테이블에서 이름이 SMITH와 ALLEN인 사람을 찾고 싶은데 and를 사용하게 된다면 이름이 두 개인 사람을 찾아야하는 상황이 된다. 이름은 하나씩 가지기 때문에 마찬가지로 그 어떤 행도 조회되지 않는다.

 

select ename, sal, deptno
  from EMP
 where ename = 'SMITH' and ename= 'ALLEN';

 

 따라서 or를 이용하여 이름이 SMITH이거나, ALLEN인 사람을 다음과 같이 찾을 수 있다.

 

select ename, sal, deptno
  from EMP
 where ename = 'SMITH' or ename= 'ALLEN';

 

 

 하지만 지금은 두 명 정도의 사람을 찾으니 크게 문제가 되지 않겠지만, 예를 들어 20개 정도의 이름을 찾아야하는 상황이라면 읽기 힘든 쿼리가 작성되게 될 것이다.

 

select ename, sal, deptno
  from EMP
 where ename = 'SMITH' or ename= 'ALLEN' or ename= 'WARD' or ename= 'JONES' or ename= 'MARTIN'.......

 

 그래서 in 연산자를 이용해서 다음과 같이 작성하기도 좋고 읽기도 좋은 쿼리를 작성할 수 있다.

 

select ename, sal, deptno
  from EMP
 where ename in ('SMITH','ALLEN');

 

 

 결과도 or를 사용한 쿼리와 동일하다.


 -04 NULL 연산자

  DBMS에서 미정의 값을 뜻하며, 0과는 다른 개념이다. (0은 0이라는 값이 있으므로)

  따라서 NULL을 포함한 상태에서 산술연산(+, -, *, /)을 시도할 시 결과는 항상 NULL을 리턴한다.

  emp 테이블에서 이름, 급여, 보너스, 총급여를 다음의 쿼리를 통해 조회 가능하며, 보너스에 NULL값이 있는 행은 급여+보너스 역시 NULL로 조회되는 것을 볼 수 있다.

 

select ename, sal, comm, sal + comm as 총급여
  from emp;
 

 

  보너스를 받는 ALLEN, WARD, MARTIN, TURNER는 총급여가 계산되었다. TURNER의 경우 보너스가 NULL이 아닌 0이기에 1500+0으로 1500의 값이 정상적으로 계산되었다.

  또한 NULL을 포함하는 행만 조회하고 싶거나, NULL이 포함되지 않는 행만 조회하고 싶다면 다음과 같은 쿼리를 작성하면 된다.

 

  보너스 열에 NULL을 포함하는 행만 조회 

 

 select *
  from EMP
 where comm is null; 

 


  보너스 열에 값이 존재하는 행만 조회


select *
  from EMP
 where comm is not null; 

 


 -05 like 연산자

 

 like 연산자는 패턴 연산자로 완전히 일치하지 않아도 되는 일부 패턴에 대한 전달이 가능한 연산자이다. 예를 들어, 상품명에 '가' 라는 글자만 포함하는 모든 값을 조회하고 싶다면 이 연산자를 사용하면 된다.

 또한 패턴 정의 기호인 와일드카드는 다음과 같다. :

  1) % : 모든 문자열을 표현하며 길이가 정해져있지 않다. (zero length를 포함한다)

  2) _ : 모든 문자열을 표현하며 단 한 글자로 길이가 정해져있다.

 

 %는 길이가 정해져있지 않으므로 몇 개를 써도 상관없으나, _는 길이가 정해져있어서 중복해서 사용할 때 이 점을 유의해야한다.

emp 테이블에서 이름이 s로 시작하는 직원 정보를 출력한다면 다음과 같은 쿼리를 작성하며, 문자의 경우 작은따옴표 안에 작성한다.

 

select *
  from emp
  where ename like 'S%';
 

 

 SMITH, SCOTT이 조회되었다.

 

 이름의 '두 번째' 글자가 O인 직원의 정보를 조회한다면 다음과 같이 쿼리가 작성된다. 두 번째 위치에 오는 글자에 맞는 조건을 조회하려고 하니, 앞에 _ 하나를 넣어 두 번째 글자가 O이며 그 뒤에 따라오는 글자는 그 무엇이건 허용하기 위해 %를 추가한다. 필요한 것은 O가 두 번째 오기만 하면  되는 이름이기 때문이다.


select *
  from EMP
 where ename like '_O%';

 

 

 JONES, FORD가 조회되었다.

 앞의 그 어떤 글자도 상관없고, 두 번째만 O면 되는 것 아닌가? 라고 생각하고 _ 앞에 %를 넣어보니 다음과 같은 결과가 출력되었다.

 

select *
  from EMP
 where ename like '%_O%';

 

 

 JONES, FORD는 동일하게 조회되었지만 이 경우 SCOTT이 같이 조회되었다. 앞에 %이 하나 붙음으로써 어찌되었건 특정 글자 뒤에 O가 오기만 하면 되는 이름이 되어버린 것이다. 이처럼 원하는 조건에 적절한 쿼리를 작성해야함을 알 수 있었다.

 

 

 또한 찾고자하는 패턴에 % 혹은 _가 포함될 수도 있는데, 이럴 때 패턴 정의 기호의 의미를 무력화하여 기호를 있는 그대로 전달하기 위해 사용하는 이스케이프 문자(escape character)가 존재한다.

 보통 \를 사용하며, \ 뒤에 따라오는 기호는 무력화된다.

 

 상품명에 _ 가 포함되는 상품만을 조회하고 싶다면 \_ 를 사용하여 _가 가지고 있던 의미를 무력화하고 기호 그대로 전달된다.

 

 또한 like에 한정하여 이스케이프 문자를 원하는 문자로 지정 가능하다.

 

 

 만약 다음과 같은 like_test1이라는 테이블이 존재하고, ID의 두 번째 자리에 %가 존재하는 값을 조회하고 싶어 다음과 같은 쿼리를 작성한다면 이런 결과가 나오게 된다.

 

 select *
  from like_test1
 where id like '_%%';

 

 

 _는 한 글자, %는 몇 개를 사용해도 무관하니 결국은 한 글자 이상의 아이디를 모두 조회하게 되었다.

그러나 두 번째 자리가 %인 ID를 찾기 위해서 두 번째 %가 그저 단순한 문자로서 남기 위해 이스케이프 문자를 \로 지정 후 \%의 형태로 쿼리를 작성하게 되면 이러한 결과가 나오게 된다.

 

select *
  from like_test1
 where id like '_\%%' escape '\';

 

 

 두 번째 자리가 %인 ID만이 조회되었다.


 -06 논리 연산자

 논리 연산자는 여러 조건을 논리적으로 결합하거나 판단함으로써 참, 거짓을 판단한다. and, or, not이 있다.

  1) and :모든 조건이 참일 경우 참

  2) or : 한 조건만 참이여도 참

  3) not : 조건의 반대 집합, 여집합

 

  또한 논리 연산자의 우선 순위는 not > and > or 순이다.

 

  만약 student 테이블에서 키가 170 이상 180 이하가 아닌 학생의 이름, 학년, 키를 조회하고 싶다면 다음과 같은 쿼리를 작성한다.

 

select name, grade, height
  from student
 where height not between 170 and 180;

 

  not을 통해 170~180 사이의 키를 가진 학생들이 제외된 행의 값만 조회되었다.


02. GROUP BY

 group by 절은 그룹별 요약(집계 연산)을 위해 사용하는 절이다. 그룹을 묶는 용도로 사용하며, group by로 묶인 그룹 기준으로 select 절에서 그룹별 정보를 출력한다.

 

 -01 집계 함수

 또한 집계 함수의 경우 NULL을 세지 않기 때문에 연산대상에도 포함하지 않는다.

 집계 함수는 다음과 같으며 emp 테이블로 실습을 진행하였다. :

 총 14개의 행이 있으며, COMM 열에 NULL이 섞여있음을 알 수 있다.

 

  1) count

select count(*), count(empno), count(comm)
  from emp;

 

 

 count(*) 는 전체 행을 집계하기 때문에 14개의 행이 모두 집계되었다.

 count(empno) 역시 사번으로 모든 행에 값이 존재하기에 14개의 행이 모두 집계되었다.

 count(comm)의 경우 NULL이 섞여있기에 값이 존재하는 0, 300, 500, 1400 4개 행만을 집계되었다.

 

  2) sum

select sum(sal)
  from emp;

 

 

  행 전체의 급여(SAL)의 총합이 계산되었다.

 

 

 3) min / max

select min(sal), max(sal)
  from emp;

 

  급여(SAL) 열에서의 최솟값, 최댓값이 조회되었다.

 

 4) avg

select avg(comm) as avg1,
       sum(comm)/count(*) as avg2, 
       avg(nvl(comm, 0)) as avg3
  from emp;

 

  avg1의 경우 NULL은 집계되지 않기 때문에 값이 존재하는 4개의 행만을 대상으로 계산된 값을 보여준다. 

   => (0 + 300 + 500 + 1400) / 4 = 550

  avg2의 경우 전체금액을 전체 행 개수로 나누는 것이기에 평균 금액을 보여준다.

   => (0 + 300 + 500 + 1400) / 14 = 157.1428...

  avg3의 경우 NULL값을 0으로 처리했기 때문에 avg2와 마찬가지로 평균 금액을 보여준다.

   => (0+ 300 + 500 + 1400 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0) / 14 = 157.1428...

     * 'nvl'은 해당 열에 null이 있을 경우 0으로 처리

  

 ** 집계 함수 사용 시 주의사항

 1) NULL만 구성된 컬럼 (이하 col1)

  - count(*) : 항상 모든 행의 수가 리턴 (행 전체의 수)

  - count(col1) : 항상 모든 행의 수가 리턴 (0, 값이 존재하는 행이 없으므로_ 수강신청하지 않는다해서 NULL명이다라고 세지 않는 것처럼))

  - sum, min, max, avg : NULL로 리턴 (값이 미정인 값은 집계하지 않는다.)

 

select count(*), count(comm), sum(comm), min(comm), max(comm), avg(comm)
  from emp
  where comm is null;

 

 

 2) WHERE절 조건에 성립하는 행이 없을 경우(공집합)

  - count(col1) : 0

  - sum, min, max, avg : NULL로 리턴

 

select count(*), count(comm), sum(comm), min(comm), max(comm), avg(comm)
  from EMP
 where deptno = 40;

 


 -02 group by

  group by는 같은 값끼리 모아주지만, 자동으로 정렬해주지는 않는다.

  자동정렬을 원한다면 order by를 통해 정렬해야 하는데, 정렬과정에서 메모리를 많이 사용한다고 한다. (정렬 코스트가 높다.) 따라서 불필요한 group by, order by를 피해야 성능에 문제가 없다. 

 

  student 테이블에서 group by와 집계 함수를 이용해 각 학년별 몸무게의 최댓값, 최소값을 구해보겠다.

 

select *
  from student;

 

 

  student table은 다음과 같이 학번, 이름, ID, 학년, 주민등록번호, 생년월일, 연락처, 키, 체중, 제1전공, 제2전공, 지도교수 열로 구성되어 있다.

 

select grade, max(WEIGHT), min(WEIGHT)
  from student
  group by GRADE;

 

 

  학년별로 몸무게의 최댓값, 최소값이 구해졌다.

 

  또한 group by는 행을 축소하는 것이기 때문에 축소 가능한 열을 지정해야 한다. 예를 들어 같은 부서끼리는 그룹화될 수 있는데, 급여나 주민등록번호같은 경우는 같은 값을 가지기 어렵기 때문이다. 급여 같은 경우도 sum(sal), avg(sal), max(sal), min(sal) 등을 통해 특정히 지정해주어야 출력이 가능하다.

 

select deptno, sal
  from emp
 group by deptno;

 

 

  group by 표현식이 아닙니다. 라는 에러가 발생하였다.

 

select deptno, max(sal)
  from emp
 group by deptno;

 

 

  max(sal)을 통해 부서별 급여의 최댓값을 지정해주니 정상적으로 결과가 출력되었다.

 또한 group by절에 언급되지 않은 행을 order by 혹은 select에서 언급하면 오류가 생긴다. 이미 group by를 통해 그룹화했기 때문이다. 

 

select deptno, max(sal)    
  from emp
 group by deptno
having sal = 3000;

 이미 부서번호(deptno)로 묶였기  때문에 급여에 대한 조건을 지정할 수 없다. 오류가 발생한다. 

select deptno, max(sal) 
  from emp
 group by deptno

 order by ename;

  이 역시 이미 부서번호(deptno)로 묶였기 때문에 개개인의 이름으로 정렬할 수 없다. 오류가 발생한다.

 
select deptno, job, max(sal)
  from EMP
 group by deptno, job;

 

 

  같은 부서번호로 그룹화된 후 부서번호별 직업, 최대수익이 정상적으로 조회되었다.

 


03. HAVING

 group by를 수행한 후 각 그룹별 집계 결과에서 특정 그룹을 선택 혹은 제한할 용도로 사용된다. where는 사용할 수 없다. group by가 where보다 이후에 처리되기 때문이다.

 

 having을 통해 professor 테이블에서 학과별 급여평균이 300인 학과의 학과번호와 급여평균을 조회해보겠다.

 

select deptno, avg(pay)
  from professor
 group by deptno
having avg(pay) >= 300;

 

 

 professor 테이블에서 학과번호(deptno)가 같은 행끼리 그룹화한 뒤, 평균 급여가 300을 초과하는 행만 선택하였다. 만약 where를 통해 조건을 걸어주면 다음과 같은 에러가 발생한다.

 

select deptno, avg(pay)
  from professor
 where avg(pay) >= 300  
 group by deptno;

 

 

 전체를 집계하는 함수를 where절에서는 전달할 수 없음을 확인할 수 있다.

 이 부분이 조금 헷갈렸는데 강사님이 where는 한 행씩 거르는 조건, having은 그룹을 거르는 조건으로 이해하라는 조언을 주셨다. 커다란 도움이 되었다.

 

 그렇다면 미리 필요없는 행을 정리한 후 그룹화하는 것이 나을까? 그룹화한 후 필요없는 정보를 걸러내는 것이 나을까?

professor 테이블에서 101번과 102번이 아닌 학과에서의 최대 급여를 조회해야 할 때, 두 쿼리 모두 다 동일한 결과를 보여준다.

 

select deptno, max(pay)
  from professor
  where deptno not in (101, 102)
  group by deptno;
  
select deptno, max(pay)
  from professor
 group by deptno
having deptno not in (101, 102);

 

 

 where, having절 모두 사용 가능한 상황이어도 where절이 성능이 더 좋다고 배웠다. 불필요한 데이터를 미리 정리한 다음에 연산을 하는 것이 효율적이기 때문이다.

 무작정 having으로 그룹 단위에서 조건을 제한하기 전에 한 번 더 생각해볼 필요가 있음을 알 수 있었다.

 


* 데이터 적재하기

Tools > Load Tool > 로그인 유저(scott) 선택 > table에 table명 입력 혹은 선택 > 적재할 파일 선택 > 1st row is column name 체크 > RUN (사람모양, F5)


 

 

  > 그런데 만약 어떤 물류의 코드가 12자리이고, 7번째 자리가 특정 지역의 코드라고 가정해보자. 그렇다면 7번째 자리에 숫자 3이 있는 물류코드만 조회하고 싶다면 어떻게 해야할까? 이 포스팅을 작성하다 문득 궁금해졌다.

 

더보기

select *
  from product_list
 where id like '______3%' ;

 

 분명 이렇게 입력하는 사람도 어렵고 확인하는 사람도 _가 몇 개인지 세기 어려운 일이 분명히 발생할텐데 말이다. 두 개, 세 개까지야 괜찮겠지만 더 자릿수가 길어지기 시작하면 필요할 때마다 _를 일일히 입력하는 것도 분명 비효율적인 일일 것이다. 1일차 필기를 확인해보다보니 강사님이 dual table 예시를 보여주시면서 다음과 같은 쿼리를 작성해주셨었다. :

 

select substr('ABCD', 3, 1)

  from dual;
  

 

  3번째 자리의 1글자를 반환하는 것을 확인할 수 있었다.

 

 주말에 작성하는 포스팅이라 어디 물어볼 곳이 없어 당황해하다가 강사님이 작성해주셨던 쿼리를 조금 수정해서 다음과 같은 테이블을 하나 만들어 보았다.

 

 

 그리고 7번째 번호가 5인 ID만 조회하는 쿼리를 작성했다.

 

select *

  from product_list
 where ID like '______5%';

 

 

 

 결과는 원하던 대로 조회되었지만 어딘지 모르게 찜찜하다. 

 

select *
  from product_list
 where substr(ID, 7, 1) = '5';
  

 

 훨씬 더 깔끔하게 동일한 결과가 출력되었다! 물론 결과만 정확하다면야 문제가 없겠지만 배운 것만 사용하려고 너무 집착하지 말고, 상황에 맞추어 적절한 구문을 활용해야 했다는 것을 알 수 있었다.

 

 그리고 이 과정을 통해 질문이 생겼다. :

 

1. 두 쿼리에 성능 차이가 존재하는가?

2. 만약 첫 번째 쿼리가 성능이 좀 더 좋다면, 실무에서는 수정/관리가 편한 쿼리를 선호할까? 아니면 성능이 좋은 쿼리를 선호할까?

 

 강사님이 매 강의마다 메모리, 성능에 대한 이야기를 여러번 강조하셨다.

 특히 같은 구문 안에 있는 연산자라 하더라도 상황에 따라 적절한 사용을 하지 않으면 성능이 크게 떨어지는 것 같았는데, SQL은 성능에 굉장히 민감하다는 느낌을 지속적으로 받고 있다.


 * 2026년 03월 09일

 강사님께 여쭈어보았다.

 

1. 두 쿼리에 성능 차이가 존재하는가?

  ▶ 존재한다. 함수를 사용하는 식으로 변형을 하는 것보다는 like 문처럼 가급적 그대로 읽는 것이 더 성능면에서 좋다.

 

2. 만약 첫 번째 쿼리가 성능이 좀 더 좋다면, 실무에서는 수정/관리가 편한 쿼리를 선호할까? 아니면 성능이 좋은 쿼리를 선호할까?

  ▶ SQL은 성능을 우선으로 생각한다.

 

 +) 그렇다면 혹시 7번째 자리에 오는 숫자가 지역코드라면, 지역코드만 따로 분류한 컬럼을 만들어서 조회하는 것이 쿼리도 깔끔하고 성능면에서도 더 우수할까?

  ▶ 이미 물류코드 안에 지역코드가 들어가있는데 굳이 테이블이나 열을 분리하는 것은 권장되지 않는다. 이후 모델링에 대한 이야기가 나올텐데, 그 단계에서 이미 고려되었을 부분이며 몇 가지의 규칙이 존재한다.

 

배운 것을 잘 쓰는 것도 중요하지만 먼저 무엇을 어떻게 하는 것이 중요한지를 잘 생각해보는 습관을 들여야겠다.

  > 이스케이프 문자에는 숫자도 알파벳도 들어가던데, 한글은 안들어가더라.

더보기

 like문에서는 내가 원하는대로 이스케이프 문자를 지정할 수 있다는 말에 !도 넣어보고 @도 넣어보고 알파벳도 넣어보았다. 통상적으로 \를 사용할 것이고 괜한 숫자나 영문자를 이스케이프 문자로 지정해 혼선을 만들 이유는 없겠지만, 일단 작은 따옴표 안에 집어넣어 문자로 인지시키다보니 신기한 마음에 이것저것 다 입력해보았다.

 그런데 숫자 혹은 영문자가 2글자를 초과하거나, 어떤 특수문자들이나 한글은 이스케이프 문자로 지정할 수가 없었다. 

 

 

 당연하지만 1자리 문자니까 바이트 문제겠지싶었지만 원하는대로 쓸 수 있다는 점이 즐거웠다.

 

 * 2026년 03월 09일

 강사님께 여쭈어보았다.

 

  ▶ '1자리 문자' 만 인정하는 구조이므로 한글과 같은 문자는 1자리 문자가 아니기에 이스케이프 문자로 사용할 수 없다. (오라클에서는 한글을 2바이트로 할당한다고 한다.) 그리고 통상적으로는 '\'를 이스케이프 문자로 사용한다.