Database/MS SQL

[MS SQL Server] #11_ IN / EXISTS / NOT IN / NOT EXISTS 비교

Tigercow.Door 2019. 5. 16. 18:00



안녕하세요 문범우입니다.

이번 포스팅에서는 IN, EXISTS, NOT IN, NOT EXISTS 에 대해서 보다 상세하게 알아보려고 합니다.

해당 내용은 꼭 SQL Server 뿐만 아니라 MySQL 등에서도 포괄적으로 적용되는 내용입니다.



0. 데이터 세팅


먼저 각 구문에 대해서 비교를 할 때 보다 쉽게 확인할 수 있도록 가상 데이터를 세팅해보도록 하겠습니다. 총 2개의 테이블을 생성하며 각 테이블의 이름과 데이터는 아래와 같습니다.


SELECT * FROM TB_FOOD;




SELECT * FROM TB_COLOR;




1. IN


SELECT * FROM TB_FOOD f

WHERE f.number IN (SELECT c.number FROM TB_COLOR c);


위와 같은 쿼리를 실행하면 어떤 결과가 나올까요?

먼저 결과를 살펴보면 다음과 같습니다.



이는 우리가 어느정도 예상할 수 있는 결과입니다. 하지만 실제로 IN 을 포함한 쿼리가 어떻게, 어떤 식으로 작동되는지 알아야 이후에 EXISTS 또는 NOT IN / NOT EXISTS와 헷갈리지 않습니다.


위의 쿼리에서는 제일먼저 TB_COLOR 테이블에 접근하게 됩니다.

즉, IN 뒤에 있는 괄호의 서브쿼리를 먼저 실행해서 그에 대한 요소를 가져오는 것이죠.

따라서 사실 IN뒤에 괄호안에는 서브쿼리 이외에도 직접 요소값을 적어줄 수 있습니다.


이후에는 TB_FOOD에서 하나의 레코드를 가져오며 그 레코드의 number 값이 앞에서 가져온 IN 이하의 요소들에 포함되어 있는지를 체크합니다. 그리고 IN 이하의 요소들 중 하나라도 일치한다면 그 레코드를 출력하게 되는 것이죠.


여기서 중요한 것은, 쿼리에서 TB_COLOR에 먼저 접근하여, number 값들을 가져와 리스트로 IN 이하에 뿌려주고, 그 이후에 TB_FOOD에서 하나의 레코드씩 IN 이하의 요소들과 일치하는지 비교한다는 것 입니다.



2. EXISTS


그럼 EXISTS는 어떻게 동작하는지 쿼리와 그 결과를 보도록 합시다.


SELECT * FROM TB_FOOD f

WHERE EXISTS (SELECT c.number FROM TB_COLOR c);



무언가 이상합니다. 우리가 기대했던 결과와는 달리 TB_FOOD 테이블이 그대로 출력되었습니다. 왜 이럴까요? 이는 EXISTS 구문에 대해서 정확히 알지 못하고 잘못 사용하였기 때문에 나온 결과입니다.


위의 쿼리를 기준으로 DB가 어떻게 동작하는지 한번 알아보겠습니다.

IN구문에서는 IN 이후에 나오는 소괄호 내부의 서브쿼리에 대해서 먼저 접근하였습니다. 하지만 EXISTS 구문에서는 다릅니다. 먼저 TB_FOOD에 접근하여 하나의 레코드를 가져오고 그 레코드에 대해서 EXISTS 이하의 서브쿼리를 실행하고 서브쿼리에 대한 결과가 '존재하는지'를 확인합니다.


예시를 들어 생각해보면, 제일 처음에 [ 1 / 치킨 ] 이라는 레코드를 가져왔을 것이고, 해당 레코드에 대해서 SELECT c.number FROM TB_COLOR c 쿼리를 통해 결과가 나오는지 확인합니다. 이때 서브쿼리에 대해 어떠한 결과라도 존재하기만 한다면 참이 되어서 [ 1 / 치킨 ] 레코드가 출력됩니다.

그런데 SELECT c.number FROM TB_COLOR c 쿼리는 사실 TB_FOOD의 어떠한 레코드하고도 연관이 없이 항상 결과값을 가지는 쿼리입니다. 따라서 TB_FOOD의 모든 레코드가 출력되는 것 이죠.


그럼 이를 우리가 기대하는 결과대로 출력하도록 하기 위해서는 다음과 같이 쿼리를 수정하면 됩니다.


SELECT * FROM TB_FOOD f

WHERE EXISTS (SELECT c.number FROM TB_COLOR c WHERE c.number = f.number);



이렇게 나온 결과는 사실 IN 구문과 같은 결과를 출력합니다. 하지만 내부적으로 쿼리가 동작하는 방식은 아예 다르다는 것에 주의하시길 바랍니다. 그러한 내부 로직에 따라서 성능차이도 크게 발생하기 때문입니다.



3. NOT IN


이번에는 NOT IN 구문입니다.

먼저 쿼리와 그 결과를 보고 함께 생각해보겠습니다.


SELECT * FROM TB_FOOD f

WHERE f.number NOT IN (SELECT c.number FROM TB_COLOR c);



위의 쿼리를 실행하니 위의 사진과 같이 아무런 결과도 출력되지 않았습니다. 왜 그럴까요?

우리가 처음에 알아본 IN의 방식에 대해서 알아봅시다. IN은 먼저 소괄호의 서브쿼리를 실행합니다. 그럼 SELECT c.number FROM TB_COLOR c 의 쿼리가 실행되고 그 결과로 다음의 리스트가 반환됩니다.


( 1, 2, 3, 4, 5, 6, NULL )


즉, 초기의 쿼리는 다음과 같은 쿼리인 것입니다.


SELECT * FROM TB_FOOD f

WHERE f.number NOT IN ( 1, 2, 3, 4, 5, 6, NULL );


그럼 이제 TB_FOOD에서 하나의 레코드씩 가져올 것이고 IN이 아니라 NOT IN 구문이기 때문에 소괄호의 요소들과 일치하지 않아야 결과로 반환됩니다.


TB_FOOD의 레코드들 중에서 [ 7 / 사탕 ] 레코드를 예로 들어서 생각해보면 해당 레코드의 number 값인 7이 NOT IN 이하의 소괄호에 있는지 확인하면 됩니다. 분명히 7이라는 요소는 존재하지 않습니다. 따라서 우리의 생각대로 라면 해당 레코드는 결과로 출력되어야 하는데 위에서 본 것 처럼 출력되지 않았습니다. 왜 일까요?


이는 DB 에서 해당 요소가 NOT IN 이하의 소괄호의 요소들에 대한 포함여부를 어떻게 판단하는지를 알면 쉽게 이해할수 있습니다. 사실 위의 쿼리는 아래의 쿼리와 같이 동작함으로써 NOT IN 이하의 소괄호에 대한 포함 여부를 판단하게 됩니다.


SELECT * FROM TB_FOOD f

WHERE f.number != 1

AND f.number != 2

AND f.number != 3

AND f.number != 4

AND f.number != 5

AND f.number != 6

AND f.number != NULL;

즉, NOT IN 구문은 TB_FOOD에서 가져온 레코드의 number 값이 소괄호의 모든 요소들과 일치하지 않는지를 체크하는 것입니다. 그런데 위에서는 number 값이 NULL과 연산을 진행하게 되는데, 이때 NULL과의 비교연산은 항상 UNKNOWN 값을 반환하게 됩니다. 따라서 WHERE 절 이하가 TRUE가 아니므로 해당 레코드가 출력되지 않게 되는 것이죠.


이렇게 NOT IN이 어떻게 동작하는지를 알고보니 결국 TB_COLOR에 존재하는 NULL 때문에 우리가 기대하던 결과가 나오지 않음을 알 수 있었습니다. 그럼 우리가 기대한 결과가 나오게 하려면 다음과 같이 쿼리를 수정하면 됩니다.


SELECT * FROM TB_FOOD f

WHERE f.number NOT IN (SELECT c.number FROM TB_COLOR c WHERE c.number IS NOT NULL);


 



4. NOT EXISTS


그럼 마지막으로 NOT EXISTS에 대해서 알아보도록 하겠습니다.

오히려 NOT EXISTS는 위에서 EXISTS에 대해서 이해했다면 크게 어려운 점이 없습니다. 하지만 그 결과가 NOT IN과 약간 다르죠. 쿼리와 결과를 먼저 보도록 하겠습니다.


SELECT * FROM TB_FOOD f

WHERE NOT EXISTS (SELECT c.number FROM TB_COLOR c WHERE c.number = f.number);



위에서 NOT IN을 사용했을 때에는 number 값이 NULL인 레코드는 출력되지 않았습니다. 그 이유를 다시 생각해보자면, IN 구문은 요소간에 비교 연산으로 레코드가 출력되는데 NULL 값에 대한 비교연산은 항상 UNKNOWN을 반환하기 때문이었습니다. 

하지만 앞에서 알아볼 때 EXISTS 구문은 다르게 동작했습니다. 위 쿼리를 기준으로 한다면 먼저 TB_FOOD에서 레코드를 가져오고 해당 레코드의 number를 NOT EXISTS 이하의 서브쿼리에 전달하여 해당 서브쿼리에서 값이 존재하는지를 확인합니다. EXISTS 구문이었다면 값이 존재할 때 해당 레코드를 출력하지만, NOT EXISTS 구문이기에 해당 서브쿼리의 값이 존재하지 않으면 해당 레코드를 출력합니다.


여기서 NULL이 출력하는 과정을 한번 더 자세하게 알아보자면, [ NULL / 타코 ] 레코드를 예로 들었을 떄, number = NULL 입니다. 따라서 NOT EXISTS 이하의 쿼리를 확인해보면 다음과 같을 것 입니다.


SELECT c.number FROM TB_COLOR c WHERE c.number = NULL;


이때 우리가 NOT IN에서 알아본 것과 같이 NULL에 대한 비교연산은 항상 UNKNOWN 값을 반환하므로 해당 쿼리의 결과가 존재하지 않게 되고, 이에 따라서 [ NULL / 타코 ] 레코드가 출력되는 것 입니다.



이렇게 IN, EXISTS, NOT IN, NOT EXISTS 에 대해서 알아보았습니다.

저 또한 많이 헷갈리던 개념들아었는데 다시 한번 정리하고 나니 확실히 개념이 잡히는 것 같습니다. IN과 EXISTS는 성능에 대해서도 많은 이슈를 가져오는 구문들이니 그 차이를 확실히 짚고 넘어가는 것을 추천드립니다.


혹시 내용이 잘못되었거나 더 궁금하신 점이 있다면 언제든지 말씀해주시면 감사하겠습니다.

728x90