Skip to content

Commit 5976a78

Browse files
poi1649the9kimparkmuhyeun
authored
[무민] 조인(Join) 한방 쿼리와 여러 개 쿼리 초안 작성 #790 (#791)
* docs: 초안 작성 * Update src/content/post/2023-10-09-join-query-vs-multiple-quries.md Co-authored-by: DEOKWOO KIM <[email protected]> * Update 2023-10-09-join-query-vs-multiple-quries.md * 무민 초안 피드백 반영 * 무민 두 번째 피드백 반영 * 무민 세 번째 피드백 반영 * 무민 네 번째 피드백 반영 * 썸네일 추가 --------- Co-authored-by: DEOKWOO KIM <[email protected]> Co-authored-by: parkmuhyeun <[email protected]>
1 parent 88dbef7 commit 5976a78

10 files changed

+239
-1
lines changed

src/content/author.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,4 +398,11 @@
398398
github: nuyh99
399399
website: https://github.com/nuyh99
400400
location: Busan
401-
profile_image: profile/salmone.jpeg
401+
profile_image: profile/salmon.jpg
402+
- id: 5기_무민
403+
avatar: avatars/moomin.png
404+
bio: 끊임없이 생각하고 나아가는 개발자 무민(박무현)입니다
405+
github: parkmuhyeun
406+
website: https://github.com/parkmuhyeun
407+
location: Seoul
408+
profile_image: profile/moomin.png

src/content/avatars/moomin.png

57.4 KB
Loading
16.5 KB
Loading
14 KB
Loading
51.7 KB
Loading
52 KB
Loading
297 KB
Loading
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
---
2+
layout: post
3+
title: 어떤 경우에 한 방 조인 쿼리와 여러 개 쿼리 분리를 고려할 수 있을까?
4+
author:
5+
- 5기_무민
6+
tags:
7+
- query
8+
- sub-query
9+
- join
10+
date: 2023-10-09T12:00:00.000Z
11+
draft: false
12+
image: ../teaser/join-query-vs-multiple-quries.png
13+
---
14+
15+
16+
최근에 프로젝트에서 리팩토링과 동시에 쿼리 개선을 하고 있는데요. 조인 한 방 쿼리로 **한 번에 끝내는 게 여러 번 네트워크 타는 것보다 좋을 것** 같아서 한 방 쿼리로 수정하고 있다가 다음과 같이 모든 쿼리가 조인이 덕지 덕지 붙어있는 코드를 보고 뭔가 이상함을 감지했습니다.
17+
18+
19+
```sql
20+
select * from pet_food p1_0
21+
join brand b1_0 on b1_0.id=p1_0.brand_id
22+
join pet_food_primary_ingredient p2_0 on p1_0.id=p2_0.pet_food_id
23+
join primary_ingredient p3_0 on p3_0.id=p2_0.primary_ingredient_id
24+
where p1_0.id=1
25+
```
26+
27+
28+
```sql
29+
select * from review r1_0
30+
join pet p1_0 on p1_0.id=r1_0.pet_id
31+
join breed b1_0 on b1_0.id=p1_0.breed_id
32+
join pet_size p2_0 on p2_0.id=b1_0.pet_size_id
33+
left join helpful_reaction h1_0 on r1_0.id=h1_0.review_id
34+
join adverse_reaction a1_0 on r1_0.id=a1_0.review_id
35+
where r1_0.id=?
36+
```
37+
38+
39+
`뭔가 아닌 거 같은데..?` 물론 한 방 쿼리가 좋을 때도 있지만 항상 한 방 쿼리가 좋진 않을 것인데 **언제 한 방 쿼리를 사용하고 언제 여러 개 쿼리를 사용**할 수 있을지 궁금증이 생겼습니다.
40+
41+
42+
---
43+
44+
## 조인(Join)으로 한 방 쿼리 vs 쿼리 여러 개
45+
46+
47+
> No Silver Bullet - 만병통치약은 없다.
48+
49+
50+
**어떤 성능 시나리오에서든 어느 솔루션이 더 빠른지 확인하기 위해 직접 솔루션을 테스트하고 측정해야 합니다.** 즉, 최선의 경우는 없는데요. 하지만, 일반적으로 어떤 상황에서 쓰면 좋을지는 통계가 있을 것입니다. 테이블 관계를 기준으로 한번 살펴봅시다.
51+
52+
53+
### 일대일 관계
54+
55+
56+
일반적으로 일대일 관계 같은 **많은 외부 레코드를 가리키지 않는 경우** 다음과 같은 성능을 나타냅니다.
57+
58+
59+
![](../images/2023-10-09-join-vs-multiple-performance_graph.png)
60+
61+
62+
조인이 가장 빠른데 그 이유는 여러 개 쿼리가 발생하면 데이터베이스에 대한 각 쿼리에 **고정 비용**이 발생하기 때문입니다. 일대일 관계 같은 경우 중복도 발생하지 않아서 부담 없이 조인을 사용할 수 있을 것 같습니다.
63+
64+
65+
그렇다고 해서, 일대다 관계에서는 절대 조인을 쓰지 말란 것이 아닙니다. 위에서 말한 것에 핵심 포인트는 일대일 관계뿐 아니라 일대다 관계에서도 많은 외부 레코드를 가리키지 않는 경우입니다. 한 방 조인으로 결괏값 주기 vs 여러 번 통신 + 애플리케이션에서 조합의 **트레이드오프**를 잘 고려해 봐야 됩니다.
66+
67+
68+
### 일대다 관계
69+
70+
71+
하지만, 참조된 레코드 중 다수가 동일할 수 있는 **일대다 관계** 같은 경우 조인 시에 중복이 엄청나게 발생할 수 있으니 잘 생각해 봐야 됩니다.
72+
73+
74+
예를 들어봅시다.
75+
76+
77+
![](../images/2023-10-09-join-vs-multiple-entities.png)
78+
79+
80+
위와 같은 관계에서 1개의 게시물이 있고 게시물에는 2개의 댓글과 2개의 태그가 있다고 가정해 봅시다.
81+
82+
83+
```sql
84+
SELECT post.id, comment.id, tag.id
85+
FROM post
86+
LEFT JOIN comment on post.id = comment.post_id
87+
LEFT JOIN tag on tag.post_id = post.id;
88+
89+
-- post_id comment_id tag_id
90+
-- 1 1 1
91+
-- 1 1 2
92+
-- 1 2 1
93+
-- 1 2 2
94+
```
95+
96+
97+
위와 같은 쿼리를 실행하면 2 * 2로 4개의 결과가 나왔습니다. 여기서 댓글과 태그가 4개로 늘어난다면 어떻게 될까요?
98+
99+
100+
```sql
101+
SELECT post.id, comment.id, tag.id
102+
FROM post
103+
LEFT JOIN comment on post.id = comment.post_id
104+
LEFT JOIN tag on tag.post_id = post.id;
105+
106+
-- post_id comment_id tag_id
107+
-- 1 1 1
108+
-- 1 1 2
109+
-- 1 1 3
110+
-- 1 1 4
111+
-- 1 2 1
112+
-- 1 2 2
113+
-- 1 2 3
114+
-- 1 2 4
115+
-- 1 3 1
116+
-- 1 3 2
117+
-- 1 3 3
118+
-- 1 3 4
119+
-- 1 4 1
120+
-- 1 4 2
121+
-- 1 4 3
122+
-- 1 4 4
123+
```
124+
125+
126+
각 행의 곱인 4 * 4 = 16개의 행이 나오게 됩니다. 그럼 만약에 각 데이터의 수가 1000개씩만 되어도 나오는 행의 수는 몇 개일까요..? 여기서 더 **많은 테이블과 레코드들을 추가하면 수많은 중복된 데이터가 생겨날 것입니다.**
127+
128+
실제 테스트를 해봅시다. 실험 환경은 다음과 같습니다.
129+
- Mac M1 Pro 16G 512GB
130+
- 실험 데이터 수는 게시글 1개, 댓글 1000개, 태그 1000개
131+
132+
133+
먼저 조인을 이용하여 100만(1000 * 1000)개의 테이블을 생성해 결괏값 100만 개를 내어줄 때는 **12초**가 걸렸습니다.
134+
135+
136+
![](../images/2023-10-09-join-vs-multiple-log1.png)
137+
138+
139+
하지만, 두 개의 쿼리(1000 + 1000)로 분리하여 결괏값 100만 개를 내어 줄 때는 고작 **0.2초**밖에 안 걸린 걸 볼 수 있습니다.
140+
141+
142+
![](../images/2023-10-09-join-vs-multiple-log2.png)
143+
144+
145+
여러 개의 테이블을 조인하는 경우도 아래의 벤치마크를 한번 봅시다. 두 경우 모두 동일한 결과(6 x 50 x 7 x 12 x 90 = 2268000)를 얻지만 여러 개의 쿼리가 훨씬 더 빠릅니다.
146+
147+
148+
1. **5개의 조인을 사용한 단일 쿼리**
149+
150+
쿼리: **8.074508초**
151+
152+
결과 크기: 2268000
153+
154+
2. **연속 쿼리 5개**
155+
156+
결합된 쿼리 시간: **0.00262초**
157+
158+
결과 크기: 165(6 + 50 + 7 + 12 + 90)
159+
160+
161+
또한, 중복 데이터로 인해 더 많은 **메모리**를 사용합니다. 적은 테이블의 수를 조인하는 경우 메모리 사용량이 적겠지만, 테이블이 점점 늘어날수록 차지하는 메모리가 기하급수적으로 늘기 때문에 잘 고려하면 좋을 것 같습니다.
162+
163+
164+
### 조인이 적합한 지 여부 판단
165+
166+
167+
조인을 사용해야 하는지는 조인이 **적합한지**에 따라 판단해 볼 수도 있습니다. 위의 예시로 사용한 테이블에서 댓글과 태그는 게시글과는 관련이 있지만 서로는 관련이 없습니다. 이 경우 두 개의 별도 쿼리를 사용하는 것이 더 좋습니다. 태그와 댓글을 결합하려고 하면 둘 사이에는 직접적인 관계가 없는데도 각 테이블행의 곱만큼 가능한 모든 조합이 생성됩니다. 또한, 두 쿼리를 병렬로 수행하여 이득을 얻을 수도 있습니다.
168+
169+
170+
하지만, 여기서 만약에 다음과 같은 상황을 생각해 봅시다.
171+
172+
173+
![](../images/2023-10-09-join-vs-multiple-entities-modified.png)
174+
175+
176+
게시물에 댓글을 달고 댓글 작성자의 이름까지 원한다면? 조인을 고려해 볼 수 있습니다. 훨씬 더 자연스러운 쿼리일 뿐 아니라 데이터베이스 시스템에서 이와 같은 쿼리를 [최적화](https://dev.mysql.com/doc/refman/8.0/en/optimization.html)하려고 노력하고 있다고 합니다.
177+
178+
179+
## 서브쿼리(SubQuery)
180+
181+
182+
조인 대신에 **서브 쿼리**를 이용하는 방법도 있습니다. 서브 쿼리와 조인의 성능을 비교해 봅시다. 우선 MySQL **5.5** 버전에서 서브 쿼리는 못쓸 정도라고 평할 정도로 제대로 수행되지 않았습니다.
183+
184+
185+
테스트 데이터
186+
- 메인 테이블 100만 건
187+
- 서브 테이블1 (Index O) 1000건
188+
- 서브 테이블2 (Index X) 1000건
189+
190+
191+
수행 시간
192+
- MySQL 5.5 + 서브 쿼리 + No Index에서 100만 건 & 1천 건 조회에 **3분**이 소요
193+
- MySQL 5.5 + 서브 쿼리 + Index에서 100만 건 & 1천 건 조회에 **1.8초** 소요
194+
- MySQL 5.5 + 조인 + No Index에서 100만 건 & 1천 건 조회에 **11초** 소요
195+
- MySQL 5.5 + 조인 + Index에서 100만 건 & 1천 건 조회에 **0.139초** 소요
196+
197+
MySQL 특정 버전(5.5이하)에서는 서브 쿼리 대신 조인이 압도적으로 빠른 것을 볼 수 있습니다.
198+
199+
200+
MySQL에서는 무슨 일이 있었기에 5.5 버전에서는 서브 쿼리 성능이 좋지 않을까요?
201+
1. 최적화 부재: 초기 버전의 MySQL에서는 서브 쿼리에 대한 충분한 최적화가 이루어지지 않았습니다. 그 결과 서브 쿼리는 복잡한 연산을 필요로 하는 경우에 매우 비효율적으로 동작하게 되었습니다.
202+
2. 재귀적 처리: MySQL 5.5에서 서브 쿼리는 종종 재귀적으로 처리되었고, 이는 성능 문제를 야기했습니다. 특히, 상위 쿼리에서 서브 쿼리의 결과를 여러 번 재참조해야 할 때 이 문제가 두드러졌습니다.
203+
204+
205+
**5.6**에서는 **서브 쿼리 성능 개선**이 많이 이루어졌습니다.
206+
207+
208+
수행 시간
209+
- MySQL 5.6 + 서브 쿼리 + No Index에서 100만 건 & 1천 건 조회에 **19초** 소요
210+
- MySQL 5.6 + 서브 쿼리 + Index에서 100만 건 & 1천 건 조회에 **0.18초** 소요
211+
212+
213+
3분에서 19초, 11초에서 0.18초 개선될 정도로 5.6에서는 최적화가 이루어졌지만 아직 **모든 서브 쿼리가 다 최적화가 된 것은 아니라고 합니다.**
214+
215+
216+
즉, 웬만하면 **최대한 조인을 이용**하고 조인을 이용하기 어렵다면 5.6 이상은 서브 쿼리를 사용합니다. 5.5 이하는 절대 사용하지 않고 차라리 쿼리를 나눠서 2번(메인 쿼리, 서브 쿼리) 실행하고 애플리케이션에서 조립하면 좋을 것 같습니다. 물론, 스칼라 서브 쿼리 캐싱 등의 최적화 기술로 때때로 서브 쿼리가 조인보다 더 효율적일 때도 있습니다.
217+
218+
219+
## 결론
220+
221+
222+
사실 위의 내용은 어느 정도 통계에 기반에서 이렇다 할 뿐이지 자신의 상황에서는 전혀 다르게 동작(고려해야 할 변수가 많기 때문에)할 수도 있습니다. 그러므로 **참고만 하고** EXPLAIN 같은 실행 계획이나 **직접 테스트하여 측정해 사용**하도록 합시다.
223+
224+
225+
> Don't guess, measure!
226+
227+
---
228+
참고:
229+
- [JOIN queries vs multiple queries](https://stackoverflow.com/questions/1067016/join-queries-vs-multiple-queries)
230+
- [MySQL where in (서브쿼리) vs 조인 조회 성능 비교 (5.5 vs 5.6)](https://jojoldu.tistory.com/520)
231+

src/content/profile/moomin.png

57.4 KB
Loading
31.8 KB
Loading

0 commit comments

Comments
 (0)