Database

효율적 쿼리란? - 서브쿼리와 조인, 뷰, 인덱스로 쿼리 최적화

readyoun 2025. 4. 7. 15:20

https://www.flaticon.com/free-icon/sql_9543826?related_id=9543826&origin=search

효율적 쿼리란 무엇일까?

데이터베이스는 수많은 데이터를 저장하고, 이 데이터에 대해 빠르고 정확한 응답을 제공해야 합니다. 하지만 단순히 작동하는 SQL 문을 작성하는 것만으로는 부족합니다. "어떻게 하면 더 빠르게, 더 적은 비용으로 원하는 결과를 가져올 수 있을까?" 가 바로 효율적인 쿼리를 고민해야 하는 이유입니다.

📖 참고: 이 글은 『이것이 취업을 위한 컴퓨터 과학이다』 CHAPTER 06, p.573 ~ p.592의 내용을 기반으로 한 설명입니다.

 

더보기

[참고] 쿼리 실행 과정 

 

SQL 문은 단순한 명령어처럼 보이지만, DB 내부에서는 복잡한 과정을 거칩니다. 실행 과정을 크게 요약하면 다음과 같습니다.

  1. 파싱(Parsing) – SQL 문법 분석 및 내부 표현으로 변환
  2. 최적화(Optimization) – 실행 계획 수립 (인덱스, 조인 순서 고려)
  3. 실행(Execution) – 결정된 계획에 따라 쿼리 수행

이 중 "최적화" 단계는 성능을 좌우하는 핵심입니다. 어떤 쿼리는 수천 배의 성능 차이를 만들 수 있습니다.

 

쿼리 최적화기(Query Optimizer) 는 가능한 실행 경로 중 비용이 가장 적은 계획을 선택합니다.

 

이때 비용은 CPU 연산, 디스크 I/O, 네트워크 트래픽 등을 기준으로 평가되며, 

통계 정보(statistics)를 바탕으로 실행 계획을 세웁니다.

 

따라서 데이터 양이 급변했거나, 통계 정보가 오래된 경우, 예상보다 비효율적인 계획이 선택될 수 있습니다. 

 


1. 서브쿼리와 조인 - 여러 테이블에 질의하려면

1.1 서브 쿼리 - 쿼리 안에 쿼리?

서브쿼리는 하나의 쿼리 안에서 또 다른 쿼리를 사용하는 방식입니다. 특히 WHERE, FROM, SELECT절에서 활용할 수 있죠.

 

자세한 종류 : [SQL] 서브쿼리란? 서브쿼리 종류 - 스칼라, 인라인 뷰, 중첩 

 

책에서 다루는 대표 두 가지 유형이 있습니다.

  1. SELECT문 안에 SELECT문이 포함된 서브 쿼리
  2. DELETE문 안에 SELECT문이 포함된 서브 쿼리

p.575 예제 - "사용자 테이블의 각 사용자별 게시글 수를 함께 출력하려면?"

SELECT users.username,
       (SELECT COUNT(*)
        FROM posts
        WHERE posts.user_id = users.user_id) AS post_count
FROM users;

 

결과:

+----------+-------------+
| username | post_count  |
+----------+-------------+
| kim      | 2           |
| lee      | 1           |
| park     | 1           |
+----------+-------------+

 

또는 특정 사용자 이메일로 조건을 걸어 그 사람의 게시글을 모두 삭제하고 싶다면?

DELETE FROM posts
WHERE user_id = (
  SELECT user_id
  FROM users
  WHERE email = 'kim@example.com'
);

서브쿼리는 비상관 서브쿼리와 상관 서브쿼리로 나뉩니다. 비상관 서브쿼리는 외부 쿼리와 독립적으로 한 번만 실행되지만, 상관 서브쿼리는 외부 쿼리의 각 행에 대해 반복 실행되므로 성능에 악영향을 줄 수 있습니다. 예를 들어 다음과 같은 형태는 성능상 좋지 않을 수 있습니다.

SELECT name FROM student s 
WHERE kor > (SELECT AVG(kor) FROM student WHERE class_id = s.class_id);

상관 쿼리 vs 비상관 쿼리 

1.2 조인 - 테이블을 연결하는 더 효율적인 방법은?

조인은 여러 테이블 간의 연관 데이터를 결합하는 방식입니다.

 

사용자와 게시글 테이블을 조인해 사용자 이름과 게시글 제목을 함께 출력하려면?

SELECT users.username, users.email, posts.title
FROM users, posts
WHERE users.user_id = posts.user_id;

 

결과:

+----------+------------------+-------------------------------+
| username | email            | title                         |
+----------+------------------+-------------------------------+
| kim      | kim@example.com  | One                           |
| kim      | kim@example.com  | Two                           |
| lee      | lee@example.com  | Three                         |
| park     | park@example.com | Four                          |
+----------+------------------+-------------------------------+

 

INNER JOIN을 명시적으로 사용해도 되는데, 보통 INNER 를 생략하기도 합니다. 

SELECT customers.name, customers.age, customers.email,
       orders.product_id, orders.quantity, orders.amount
FROM customers
INNER JOIN orders
ON customers.id = orders.customer_id;

조인에는 다양한 종류가 있습니다.

종류 설명
INNER JOIN 테이블 A와 B의 레코드 중 조인 조건을 모두 만족하는 레코드만 반환
LEFT OUTER JOIN 테이블 A의 모든 레코드 + 조인 조건 만족하는 B의 레코드. 만족 못 하면 B는 NULL
RIGHT OUTER JOIN 테이블 B의 모든 레코드 + 조인 조건 만족하는 A의 레코드. 만족 못 하면 A는 NULL
FULL OUTER JOIN A와 B 모두의 모든 레코드를 포함. 어느 한쪽이 없으면 상대는 NULL로 채워짐

 

❗ 유의사항:

  • 조인 조건 누락 시 카티션 곱(Cartesian Product) 발생 → 폭발적인 행 수 증가!
  • 항상 ON 조건 또는 USING을 명확히 지정해야 합니다.
더보기

카티션 곱(Cartesian Product) 발생 가능성과 그로 인한 성능 문제

 

카티션 곱은 조인 조건이 누락되었을 때 발생하며, 두 테이블의 모든 행을 조합하여 결과를 생성합니다.

 

예를 들어, 테이블 A에 1,000개의 행이 있고 테이블 B에 500개의 행이 있다면, 카티션 곱은 1,000 × 500 = 500,000개의 행을 반환하게 됩니다. 이러한 결과는 데이터베이스 서버에 과도한 부하를 주고 쿼리 실행 시간을 크게 증가시킬 수 있습니다.

 

SELECT * FROM table1, table2; 같은 쿼리는 조인 조건이 없으므로 카티션 곱을 생성합니다.

 

카티션 곱을 방지하기 위한 방법은 항상 조인 조건을 명확히 지정하는 것입니다. SQL에서 ON 또는 USING 절을 사용하여 테이블 간의 관계를 정의해야 합니다. 조건이 명확하지 않으면 SQL은 기본적으로 모든 행을 조합하는 CROSS JOIN으로 처리합니다.

LEFT OUTER JOIN 예제 (p.582)

SELECT customers.name, orders.id AS order_id, orders.product_id,
       orders.quantity, orders.amount
FROM customers
LEFT OUTER JOIN orders
ON customers.id = orders.customer_id;

 

RIGHT OUTER JOIN 예제 (p.582)

SELECT customers.name, orders.id AS order_id, orders.product_id,
       orders.quantity, orders.amount
FROM customers
RIGHT OUTER JOIN orders
ON customers.id = orders.customer_id;

 

❗ LEFT vs RIGHT OUTER JOIN 유의점

  • JOIN 순서가 바뀌면 결과도 바뀜 → 기준이 되는 테이블이 무엇인지 항상 확인할 것
  • 가독성, 명확한 의도를 위해 LEFT JOIN을 선호하고, RIGHT JOIN은 최소화하는 것이 일반적
항목 LEFT OUTER JOIN RIGHT OUTER JOIN
기준 테이블 왼쪽 테이블(LEFT)이 기준 오른쪽 테이블(RIGHT)이 기준
NULL 채워짐 오른쪽 테이블의 매칭 안 되는 값 왼쪽 테이블의 매칭 안 되는 값
사용 예 고객은 있지만 주문 없는 경우 포함 주문은 있지만 고객 정보 없는 경우 포함 (드묾)

 

FULL OUTER JOIN 구현 예시 (p.583)

SELECT customers.name, orders.id AS order_id, orders.product_id,
       orders.quantity, orders.amount
FROM customers
LEFT OUTER JOIN orders ON customers.id = orders.customer_id
UNION
SELECT customers.name, orders.id AS order_id, orders.product_id,
       orders.quantity, orders.amount
FROM customers
RIGHT OUTER JOIN orders ON customers.id = orders.customer_id;

FULL OUTER JOIN은 MySQL에서 직접 지원되지 않기 때문에
위와 같이 LEFT JOIN + RIGHT JOIN + UNION을 조합하여 구현합니다.

 

JOIN으로 서브쿼리 대체 (p.583)

SELECT users.username, COUNT(posts.post_id) AS post_count
FROM users
LEFT JOIN posts ON users.user_id = posts.user_id
GROUP BY users.username;

 

앞서 사용한 서브쿼리 기반 카운트 쿼리를 JOIN과 GROUP BY를 통해 동일하게 구현할 수 있습니다. 이 방식은 보다 효율적이며 명확한 실행 계획을 만들어주는 장점이 있습니다.


2. 뷰(View) - 뷰는 왜 필요할까?

뷰(View)는 테이블처럼 사용 가능한 가상의 테이블입니다.

복잡한 쿼리를 단순화하고, 보안을 강화할 수 있습니다.

 

사용자와 게시글을 조인한 결과를 뷰로 저장하고 활용하려면?

CREATE VIEW myview AS
SELECT users.username, users.email, posts.title
FROM users, posts
WHERE users.user_id = posts.user_id;

 

해당 뷰를 이용한 조회 예시:

SELECT username, email, title
FROM myview
WHERE username = 'kim';

 

뷰는 SHOW TABLES 명령어를 통해 확인 가능:

mysql> SHOW TABLES;
+----------------+
| Tables_in_mydb |
+----------------+
| myview         |
| posts          |
| users          |
+----------------+

 

❗ 유의사항:

뷰는 보안과 재사용성 관점에서 유용합니다.

  • 민감한 정보를 제외한 컬럼만 노출 가능
  • 복잡한 조인 쿼리를 캡슐화하여 코드의 가독성과 유지보수성 향상
  • GRANT를 통해 특정 사용자에게만 접근 권한 부여 가능

3. 인덱스 - 왜 빠른 걸까?

인덱스는 테이블의 특정 컬럼에 대한 검색 속도를 향상시키는 자료구조입니다. 일종의 책의 목차라고 생각할 수 있습니다.

3.1 인덱스의 성능 영향

아래 예제로 인덱스가 성능에 미치는 영향을 확인해보겠습니다.

-- 테이블에 70만 개의 행이 있는 상태
SELECT COUNT(*) FROM users;
-- 결과: 700000 rows in set (0.86 sec)

-- 인덱스 없이 조건 검색
SELECT * FROM users WHERE nickname='User290562U';
-- 결과: 1 row in set (0.21 sec)

-- 인덱스 생성
CREATE INDEX idx_user ON users(nickname);

-- 다시 실행
SELECT * FROM users WHERE nickname='User290562U';
-- 결과: 1 row in set (0.00 sec)

 

인덱스를 사용하면 전체 테이블 탐색(Full Table Scan) 대신 인덱스를 통해 바로 데이터 위치를 찾을 수 있습니다.

 

단, SELECT *와 같은 경우 인덱스만으로는 모든 데이터를 가져올 수 없어 테이블 접근이 추가로 필요합니다.

3.2 인덱스 종류와 구조

인덱스의 종류

클러스터형 인덱스(Clustered Index)는 기본 키(primary key)에 적합하며,

세컨더리 인덱스(Secondary Index, Non-Clustered Index)는 다양한 쿼리를 처리하기 위한 보조 도구로 활용됩니다.

구분 설명
클러스터형 인덱스 실제 데이터가 인덱스 순서대로 정렬됨 (MySQL InnoDB 기본값)
세컨더리 인덱스 별도 인덱스 구조에 실제 데이터 위치를 가리킴

 

인덱스 자료구조

  • B-Tree:
    • 균형 이진 트리의 일반화 형태
    • 범위 검색과 정렬이 가능
    • 루트 → 내부 노드 → 리프 노드 구조
    • 검색/삽입/삭제 모두 O(log n) 시간 복잡도
    • RDBMS의 기본 인덱스 구조
참고
[MySQL] Index의 구조 : B-Tree, B+Tree
- [MySQL] B-tree, B+tree란? (인덱스와 연관지어서)
- [MySQL] B-Tree로 인덱스(Index)에 대해 쉽고 완벽하게 이해하기

 

  • Hash Table:
    • 정확히 일치하는 키 검색에 특화
    • 범위 검색 불가
    • NoSQL에서 주로 활용
잼씀: DB 인덱스에서는 왜 HashTable을 사용하지 않을까?

3.3 인덱스 사용 시 고려사항

인덱스가 적용되는 조건:

  • WHERE, JOIN, ORDER BY 절에 사용된 컬럼
  • LIKE 검색은 접두어 기준일 때만 (LIKE 'A%')
  • 함수 적용 컬럼에는 적용 안 됨 (WHERE UPPER(name) = 'ROBIN' ❌)

주의사항:

  • INSERT/UPDATE/DELETE 시 인덱스도 갱신 필요 → 쓰기 성능 저하
  • 인덱스가 많으면 Optimizer가 최적 인덱스 선택을 못할 수 있음
  • 자주 변경되는 컬럼(예: 로그인 실패 횟수)은 인덱스 설정 신중히 고려
  • 다중 컬럼 인덱스는 컬럼 순서가 성능에 큰 영향
  • EXPLAIN으로 인덱스 사용 여부 확인 필수

EXPLAINSQL 쿼리의 실행 계획을 미리 확인하는 도구다. 인덱스가 쓰였는지, 조인이 어떻게 수행되는지, 몇 행을 탐색하는지 알 수 있다. 성능 개선(쿼리 튜닝)에서는 무조건 확인해야 할 1순위 도구다.

 

결론: 자주 조회되지만 변경이 적은 컬럼 위주로 선택적 인덱스 생성이 중요합니다.


정리하며

효율적인 쿼리 작성은 데이터베이스 성능을 최적화하고 애플리케이션의 응답 시간을 줄이는 데 매우 중요합니다. 잘 설계된 쿼리는 불필요한 데이터 검색과 연산을 최소화하여 서버의 부하를 줄이고, 시스템 자원을 효율적으로 활용할 수 있게 합니다. 

 

반면, 비효율적인 쿼리는 과도한 I/O 작업과 CPU 사용을 초래하며, 데이터베이스의 성능 저하와 비용 증가로 이어질 수 있습니다. 이를 방지하기 위해 인덱스 활용, 필요한 컬럼만 조회, 조인 및 서브쿼리 최적화, 실행 계획 분석 등 다양한 최적화 기법을 적용해야 합니다. 

 

따라서, 단지 SQL이 동작하는 것에 만족하지 말고, 동시에 얼마나 빠르고 효율적으로 동작하는가를 고민해야 합니다. 데이터가 적을 때는 문제가 없어 보여도, 데이터가 많아질수록 설계 미비는 곧바로 성능 저하로 나타납니다.

더보기

요약

 

✅ 효율적 쿼리란?

  • 같은 결과를 더 빠르고, 더 적은 비용으로 얻는 SQL
  • 실행 계획 최적화가 핵심 (쿼리 옵티마이저, 통계 정보)

🔍 서브쿼리 vs 조인

  • 상관 서브쿼리는 행마다 서브쿼리 실행 → 느림
  • JOIN + GROUP BY는 집합 기반 연산 → 더 효율적

🧩 OUTER JOIN 종류

  • LEFT JOIN: 왼쪽 테이블 기준, 없는 값은 NULL
  • RIGHT JOIN: 오른쪽 테이블 기준 (덜 선호됨)
  • FULL OUTER JOIN: MySQL은 UNION으로 구현

👁‍🗨 VIEW 사용법

  • 복잡한 SELECT를 재사용 가능한 가상 테이블로
  • 보안과 쿼리 간결화에 유용하지만 갱신 제약 있음

⚡ 인덱스 최적화

  • B-Tree 기반, 검색 속도 비약적 향상
  • SELECT, WHERE, ORDER BY 등에서 유리
  • 너무 많은 인덱스 = 오히려 비효율 (쓰기 성능 저하)

💡 팁

  • EXPLAIN으로 쿼리 실행 계획을 꼭 확인하자!

참고자료