TIL.0319 lazy

Django의 ORM은 다른 ORM과 마찬가지로 Lazy-loading방식을 사용한다. Lazy-loading
이란 ORM에서 명령을 실행할 때마다 데이터베이스에 접근하여 데이터를 가져오는 것이 아닌 모든 명령처리가 끝나고 실제 데이터를 불러 와야할 때 데이터베이스 Query문을 실행하는 방식

  • on-demand loading으로도 불린다. 여기서 on-demand란 ''라는 의미이다. 요구가 있을 때는 언제든지
  • 온라인 콘텐츠(웹앱이나 웹사이트)의 기술 최적화이다.
  • 전체 웹페이지를 로딩하는 것이 아니라 유저가 필요할 때까지 미루고 남겨두었다가 원하는 부분에 대해서 요구하는 순간에 로딩하는 것이다.
  • 대표적인 예로 무한 스크롤이 있다. 웹 페이지를 유저가 내릴 때 로딩된다.

Query문을 실행하는 시점

  1. Iteration 객체일때
    1. QuerySet은 반복이 가능하며 처음 반복할때 쿼리를 실행한다.
  2. 슬라이싱(Slicing)
    1. QuerySet은 앞서 SQL쿼리 문에서 설명했듯 iterable한 object로서 슬라이싱 구문을 사용하여 슬라이싱을 할수있다.
  3. Pickling/Caching
    1. Pickle = 모듈 / Python 객체 구조를 직렬화 및 역 직렬화 하기위한 바이너리 프로토콜을 구현한다.
      1. 추후 추가설명 필요
  4. repr()
    1. reper()를 사용할때 쿼리를 실행
    2. reper = 객체의 표준 문자열 표현을 반환합니다
  5. len()
    1. len()을 호출했을때 쿼리를 실행한다.
  6. list()
    1. QuerySet을 호출하여 강제로 평가한다.
    2. ex) \ entry_list = list(Entry.object.all())
  7. bool()
    1. if 문을 사용해 boolean값을 확인하게 될때 포함
    2. 찾는값의 존재 여부만 파악 할때는 if 문사용 보다는 .exists()를 이용하여 확인하는 것이 성능면에서 효율적

Eager-loading

: 즉시로딩

Eager-loading ↔ Lazy-loading

Lazy-loading은 Query문을 하나하나 실행하여 데이터를 가져온다면

Earger-loading은 지금당장 사용하지 않을 데이터도 포함하여 Query 문을 실행 하기 때문에

Lazy-loading의 N+1문제의 해결책으로 많이 사용하게 된다.

Django에서 실행방법

  • select_related 메소드와 prefetch_related 메소드를 사용 select_related : 셀렉트할 객체가 역참조 하는 single object(oto or mtm)이거나 정참조의 foreign key일때 사용 select_related는 각각의 lookup마다 SQL의JOIN을 실행하여 테이블의 일부를 가져오고, select .. from에서 관련된 필드들을 가져온다. 형식 parameter에는 참조하는 class 의 이름을 소문자로 쓰고 ‘ ’에 감싼다 여러개의 parameter를 가질수있다. 예) Store.objects.filter(id=1).select_related('gungu','city').values('id','name','city_name') prefetch_related :구하려는 객체가 정참조 multiple objects(many-to-many or one-to-many)이거나, 또는 역참조 Foreign Key일때 사용한다. selected_related와 달리, prefetch_related는 SQL의 JOIN을 실행하지 않고, python에서 joining을 실행한다. 형식 <lowercase 모델이름>_set : 장고가 지어주는 default 역참조 메서드 이름 ForeignKey 필드에 이름을 정하면 default이름 대신 given 이름을 쓸 수 있다.

N+1 Query 문제

Lazy-loading의 성능이슈인 N+1 Query문제는 외래키(Foreign Key)를 참조해서 데이터를 가져올 때 발생한다.

아래와 같이 restaurant와 owner가 1:1 관계인 모델이 있다고 가정하자

mysql> select * from restaurants;
+----+--------------+--------------+----------+
| id | name         | place        | owner_id |
+----+--------------+--------------+----------+
|  1 | 얌얌피자     | 서울         |        1 |
|  2 | 굿굿피자     | 인천         |        2 |
|  3 | 좋아좋아     | 부산         |        3 |
|  4 | 배고파아     | 제주         |        4 |
|  5 | 정통스       | 이탈리아     |        5 |
+----+--------------+--------------+----------+

mysql> select * from owners;
+----+------+-----+
| id | name | age |
+----+------+-----+
|  1 | 김   |  21 |
|  2 | 이   |  22 |
|  3 | 박   |  30 |
|  4 | 안   |  40 |
|  5 | 전   |  50 |
+----+------+-----+
>>> restaurants = Restaurant.objects.all()
>>>
>>> for restaurant in restaurants:
...     restaurant.owner.name

이때 쿼리를 확인해보면 5개가 아닌 5+1인 6개의 쿼리가 날아간것을 확인할수있다.

{'sql': 'SELECT `restaurants`.`id`, `restaurants`.`name`, `restaurants`.`owner_id`, `restaurants`.`place` FROM `restaurants`', 'time': '0.003'},
 {'sql': 'SELECT `owners`.`id`, `owners`.`name`, `owners`.`age` FROM `owners` WHERE `owners`.`id` = 1 LIMIT 21', 'time': '0.004'},
 {'sql': 'SELECT `owners`.`id`, `owners`.`name`, `owners`.`age` FROM `owners` WHERE `owners`.`id` = 2 LIMIT 21', 'time': '0.001'},
 {'sql': 'SELECT `owners`.`id`, `owners`.`name`, `owners`.`age` FROM `owners` WHERE `owners`.`id` = 3 LIMIT 21', 'time': '0.001'}, 
{'sql': 'SELECT `owners`.`id`, `owners`.`name`, `owners`.`age` FROM `owners` WHERE `owners`.`id` = 4 LIMIT 21', 'time': '0.001'}, 
{'sql': 'SELECT `owners`.`id`, `owners`.`name`, `owners`.`age` FROM `owners` WHERE `owners`.`id` = 5 LIMIT 21', 'time': '0.001'}]

맨 첫번 째 Query문은 전체 restaurant를 가져오고 그뒤 owner에서 5번 따로 가져오게 된다. 이는 가져오는 데이터가 많으면 많을수록 비효율적인 코드가 되기 때문에 위에서 언급한 Eager-loading을 통해 해결해야 한다.

좋은 웹페이지 즐겨찾기