티스토리 뷰
Ruby on Rails 코드를 짤 때 있어서 남들한테 홈페이즈 결과(프론트)가 보여지는거도 중요하지만, 내부 퍼포먼스도 중요합니다.
사실 레일즈에서는 SQL 문법으로 안쓰고 ORM 방식, 즉 모델 참조 방식으로 데이터 조작을 하다보니 SQL 문법에 관심이 없을 수도 있는데 이는 먼 미래에 있어선 큰 치명적일 수 있습니다.
결국 최종적으로는 Rails에서 작성된 ORM이 SQL로 변환되서 데이터 탐색이 이루어지기 때문입니다.
이번에는 SQL N+1 문제에 대해 맛보기 형식으로 한번 글을 써나아가보고자 합니다.
테이블 쿼리 탐색에 있어, 테이블 및 데이터는 위 테이블 내 자료를 기반으로 진행합니다.
-
SQL N+1 문제 발생
N+1 문제란?
SQL 구문 한번의 입력만으로 결과가 나와야 하지만 추가적으로 N번을 더 SQL 탐색을 진행하는 문제
말로 설명하는것 보다 직접 눈으로 보여드리겠습니다.
1) N+1 문제
N+1 문제 발생 사례 : 데이터 결과를 도출하기 전에 항상 User 테이블을 참조합니다.
2) N+1 문제가 완화된 쿼리
N+1문제는 주로 다른 테이블을 참조해서 데이터 결과를 보여줄 때 많이 발생합니다.
이러한 해결을 위해 SQL 정석상으로는 다음 2가지의 해결 방법이 존재합니다 :
사전 테이블 참조(Eager Load), LEFT JOIN(Left Outer Join)
※ 읽을 거리 : https://zetawiki.com/wiki/N%2B1_%EC%BF%BC%EB%A6%AC_%EB%AC%B8%EC%A0%9C
하지만 이번 시간에는 Ruby on Rails 시선으로서 해결법에 대해 알아보겠습니다.
(정석이랑 Rails 방식이랑 용어가 다릅니다.)
1. Preload : 사전 테이블 참조
@data = Post.preload(:user).to_a
@data.each do |t|
puts "[#{t.id}] 제목 : #{t.title} / 작성자 : #{t.user.email}"
end
기존의 SQL 정석으로 치면 사전 데이터 참고로서, 데이터 탐색 전에 사전에 테이블을 참조하는 방식입니다.
SQL 쿼리는 총 2번 쓰입니다.
하지만 :preload 속성은 타 테이블을 참고해서 조건을 표현하는 where, find_by와 같은 조건절을 사용할 수 없습니다.
2. Eager_load : LEFT OUTER JOIN
@data = Post.eager_load(:user).to_a
@data.each do |t|
puts "[#{t.id}] 제목 : #{t.title} / 작성자 : #{t.user.email}"
end
Left Outer Join 방식은 2개의 테이블이 있다 할 때, 주체가 될 왼쪽 테이블을 기준으로 두 테이블간에 서로 맵핑을 하는 방식입니다.
SQL 쿼리는 총 1번 쓰입니다.
더불어 eager_load 메소드는 타 테이블을 참조하는 조건절 사용이 가능합니다.
@data = Post.eager_load(:user).where("users.email LIKE ?", "%.com%").to_a
@data.each do |t|
puts "[#{t.id}] 제목 : #{t.title} / 작성자 : #{t.user.email}"
end
3. includes
Preload(사전 테이블 참조)와 Eagerload(Left Outer Join) 2가지 속성을 모두 가지고 있는 메소드입니다.
1) :includes 를 :preload 처럼
includes의 Default는 preload입니다.
@data = Post.includes(:user).to_a
@data.each do |t|
puts "[#{t.id}] 제목 : #{t.title} / 작성자 : #{t.user.email}"
end
2) :includes 를 :eager_load 처럼
where절을 사용하면 LEFT OUTER JOIN 방식으로 SQL 탐색을 하게 됩니다.
@data = Post.includes(:user).where(:users => { "email": "lakeshia@torphy.info" }).to_a
@data.each do |t|
puts "[#{t.id}] 제목 : #{t.title} / 작성자 : #{t.user.email}"
end
where 뿐만 아니라 references 메소드를 통해서도 LEFT OUTER JOIN을 하게됩니다.
@data = Post.includes(:user).references(:user).to_a
4. 속도 밴치마킹
www.chrisrolle.com/en/blog/benchmark-preload-vs-eager_load
어떤분이 밴치마킹 테스트를 하셨었는데, 조인보다 preload가 빠르단 결과가 나왔습니다.
-
조인만으로 테이블 퍼포먼스가 해결될까?
꼭 그렇진 않습니다.
테이블 검색 성능을 향상을 시키기 위해선 색인(Index) 이라는 기능을 도입하는게 좋습니다.
색인은 Original Table의 Key-Value 를 가지고 있는 별도의 테이블이며, Key를 받아내면, Key에 매칭되는 Value값을 추적해내어 테이블 내 데이터 검색 속도를 높입니다.
' 오 그러면 모든 테이블마다 색인을 적용하면 되겠네! '
위 생각같이 저희 뜻대로 이루어지면 좋겠지만.. 융통성 없이 모든 테이블에 이런 짓을 행해서는 안됩니다.
-
장점 색인은 테이블 내 데이터 탐색 속도를 높인다는 장점이 있습니다.
-
장점 색인은 JOIN이 자주 쓰이는 작업에 쓰이는게 좋습니다.
-
단점 테이블 내 내용이 삽입(INSERT)/수정(UPDATE)가 될 경우 삽입(INSERT)/수정(UPDATE) 에 대해 Indexing 함에 있어서 성능이 떨어질 수 있다는 문제점이 발생합니다.
-
단점 데이터 변경 작업이 너무 잦다면 오버헤드 이슈 때문에 오히려 색인(INDEX)을 안쓰는게 나을 수 있습니다.
-
단점 인덱스도 결국 데이터다보니 DB 내 공간을 사용합니다.
-
단점 색인(INDEX)은 테이블 내 데이터가 많을 때 그 힘이 발휘되나, 데이터 양이 너무 적으면 오히려 성능이 떨어집니다.
-
강제로 조인이 된 상황에선 어떻게 인덱싱을 해야할까?
Order.includes(:user).where(order_status: "Cancelled")
만약 위와같이 includes 메소드를 통해 강제로 LEFT JOIN이 된 상황인 경우이고, order_status 에트리뷰트를 조건으로 사용을 해낸다면
add_index :orders, :order_status
다음과 같이 where 절에 있어 기준이 되는 에트리뷰트를 인댁싱을 해주면 됩니다.
-
연결고리
-
자료 참고
1. 제타위키 : N+1 문제 정의 [클릭]
2. Rails Compare :includes, :preload, :eager_load, Benchmark [클릭]
3. Rails Compare :includes, :preload, :eager_load 2 [클릭]
4. DB INDEX란? [클릭]
5. Rails에 테이블 색인 적용하기 [클릭]
6. Rails : How to table Index? [클릭]
7. 색인 테이블 제작에 있어 Attribute 순서의 중요성 [클릭]
8. Rails : includes 메소드를 쓴 상황에서 테이블 색인을 적용해야 한다면? [클릭]
'프로그래밍 공부 > Ruby on Rails : 이론' 카테고리의 다른 글
검색 기능 구현의 고민 : 공백처리 (1) | 2019.12.14 |
---|---|
SQLite, PostgreSQL 차이 : Like / iLike (2) | 2019.12.13 |
Ruby on Rails : SQL Performance의 고민 (서론) (0) | 2019.11.06 |
Ruby on Rails : 여러가지 메소드 (일부는 깊게 파보기) (0) | 2019.11.06 |
Ruby on Rails : 비동기(Ajax) 이벤트 구현 (4) | 2019.11.03 |