IT/WEB

[Django] 테이블(모델)의 JOIN(1)

개발자 두더지 2020. 10. 21. 23:53
728x90

데이터 베이스 관계에 대한 설명


우선 잠시 '1대1', '1대다(多)', '다(多)대다(多)'에 대한 이야기부터 하고자 한다. 

1대1관계

 양쪽의 레코드가 1대1 대응하는 관계를 의미한다. 다른 말로 하자면, 양쪽의 PK(기본키)가 같다고 할 수있다. 이러한 설계를 하는 예로는 분할하지 않아도 괜찮은 테이블이 분할되는 경우이다. 기존 테이블 구성을 변경하지 않고 항목을 추가하고 싶은 경우 등을 제외하고, 실제로 사용하는 경우는 적을 것이라고 생각한다.

1대다(多)관계

 A의 레코드는 B의 복수의 레코드와 관계가 있을 가능성이 있으나, B의 레코드는 A의 레코드와 최대 1개 밖에 관련이 있는 경우이다.

조금 더 자세히 설명하자면, A의 입장에서 봤을 때, A의 1개의 레코드가 동시에 복수의 B의 레코드와 관련되어 있는 것이고, B의 입장에서 봤을 땐 B의 1개의 레코드가 A의 1개의 레코드만 관계가 있는 것이다 (물론 관련 없는 레코드도 존재한다).  

 사례로 살펴보자면 다음과 같다.

- 예1) A : 블로그 주인, B: 블로그 게시글

- 예2) A : 고객, B: 주문

- 예3) A: 주문, B: 주문 상세

- 예4) A: 부서, B: 종업원

 설계를 예로 들자면, A(관계에서 1인 쪽)은 정규화에 의해 B(관계에서 다(多)인 쪽)으로 분활된 테이블인 경우가 많다. 1대다 관계는 1인 쪽의 테이블의 PK(기본키)를 참고하는 외부키를 다(多)인 쪽의 테이블에 두어 대응하는 경우가 많다.

다(多)대다(多) 관계

 A의 레코드는 B의 복수의 레코드와 관련이 있는 가능성이 있고, 마찬가지로 B의 레코드도 A의 복수의 레코드와 관계가 있는 가능성이 있다.

 조금 더 풀어서 설명하자면, A의 입장에서 봤을 때, 1개의 레코드가 동시에 복수의 B의 레코드와 관계가 있으며, B에서 봤을 때도, B의 1개의 레코드가 동시에 A의 레코드와 관계가 있을 가능성이 있다는 것이다.

 구체적인 예는 다음과 같다.

- 예1) A : 블로그 태그, B: 블로그 게시글

- 예2) A: 유저, B: 권한

 설계로 예를 들자면 다대다 관계는 쌍방에 외부키를 두고 중간 테이블을 이용하는 케이스가 많다.

 

 

JOIN에 대해서


JOIN이란 데이터 베이스의 두 개 이상의 테이블을 결합하는 것을 의미한다. 테이블 JOIN형태에는 여러 종류가 있는데 그러한 테이블 JOIN을 다이어그램으로 각각 표현하면 다음과 같다.  

내부 조인
왼쪽 조인
오른쪽 조인
완전 외부 조인

 그리고 이와 관련된 SQL의 Query문은 다음의 이미지와 같다 (물론 사용하는 SQL에 따라 조금씩 다르겠지만 기본적인 틀은 비슷할 것 이다).

 

 

Dajngo ORM의 JOIN


select_related()  prefetch_related()에 대해

 Django에서는 JOIN을 할 때, "select_related()"나 "prefetch_related()"를 사용하는 듯 하다.

 selected_related는 JOIN문을 만들어 SELECT문에 관련된 객체의 필드를 포함하여 작동한다. 그러므로, selected_related은 1개의 데이터 베이스 쿼리에 관련 객체를 취득하는 것이 가능하다. 그러나 관계에서 다(多)가 되는 곳을 JOIN하여 대규모 결과 세트를 얻어 버리는 것을 피하기 위해 selected_related는foreign key나 one-to-one과 같은 "대1"이되는 관계에서만 JOIN한다.

 한편. prefetch_related 는 각각의 관계에 대해 별도의 따로 따로 참고하여 Python 코드에 JOIN(상당의 처리)을 실시한다. 이것에 의해 select_related에 의해 취득할 수 있는 foregin key나 one-to-one 관계의 객체뿐만 아니라, select_related에서는 취득 불가능했던 many-to-many나 one-to-many관계의 객체를 JOIN하여 다룰 수 있다. 

 각각의 사용법을 표로 나타내자면 아래와 같다.

관계 사용 메소드
one to one select_related()
many to one select_related()
one to many prefetch_related()
many to many prefetch_related()

 구체적으로 아래와 같이 되어 있다고 할 수 있다.

- select_relate() ; SQL쪽에서 관계를 이을 수 있는 JOIN이 된다. 1개의 SQL이 된다.

- prefetch_relate() ; python쪽에서 관계를 잇는다. 내부적으로 각 객체의 pk(대체로 id)를 IN에 전달하여 범위를 축소한다. 내부에서 cache된 것이 있다면 사용할 수 있다.

 즉 자기자신으로부터 시작하여 JOIN을 쓸 수 있는 것은 selected_related()를 사용하면 좋고, 불가능하다면 prefetch_related()를 사용하는 편이 좋다. 여기서 "자기자신으로 부터" 라는 것이 익숙하지 않아 고생을 했다. 

 

간략한 분별법

 어떤 모델끼리 관계가 복잡하다면 먼저 one to many 인지 many to one인지 확인하는 것이 좋다.  아래에 다양한 모델이 있는데 User과 UserKarma가 1:1관계 User과 Comment가 1:N의 관계로 되어있다.

class User(models.Model):
    name = models.CharField(max_length=32, default="foo", blank=False)

class UserKarma(models.Model):
    user = models.OneToOneField(User, related_name="karma")
    point = models.IntegerField(null=False, default=0)

class Comment(models.Model):
    user = models.ForeignKey(User, related_name="comments")
    content = models.CharField(max_length=140, null=False, default="")

 Comment의 모델에 ForeignKey()의 지정이 있으므로, 이 클래스가 query의 기점이 되었을 때는 many to one로 하고 싶다.

 여기에 각각의 select_related()를 하였을 때의 관계는 아래와 같다.

관계 자기자신 join대상 query
one to one' User UserKarma LEFT OUTER JOIN
one' to one User User INNER JOIN
many to one Comment User INNER JOIN
one to many User Comment x

 플랫한 레코가 리턴된다는 것을 전제로하는 듯하므로 one to many는 무리이다. DB에 정의로서는 1:1의 관계와 1:N의 관계 등을 신경쓰지 않으므로 one to one'의 부분도 마찬가지이다. User에 데이터가 있지만 UserKarma에는 데이터가 없는 없는 경우가 있을 수 있기 때문에 User를 키로 가정하고 LEFT OUTER JOIN으로 된다. 

 many to many관계에 대해서는 여기서 다루지 않는다. 아무튼 select_related()로는 JOIN하기에 무리인 경우는 prefetch_related()의 사용을 고려해보는 것이 좋다.

추가 설명

 한편 User을 Comment의 수로 내림차순 정렬하고 싶을 때 등 아래의 같은 코드를 작성한다. 

User.objects.annotate(c=Count('comment__user_id')).order_by("-c")
# SELECT "user"."id", "user"."name", COUNT("comment"."user_id") AS "c" FROM "user" LEFT OUTER JOIN "comment" ON ("user"."id" = "comment"."user_id") GROUP BY "user"."id", "user"."name" ORDER BY "c" DESC

 

+) 대부분 일본어 자료를 번역해서 정리했지만, 솔직히 이해가 되지 않아 조금 더 자료를 찾아보고 다음 포스팅을 통해 더욱 알기 쉬운 내용으로 작성할 예정입니다.


참고자료

medium.com/deliverytechkorea/django-queryset-1-14b0cc715eb7

www.w3schools.com/sql/sql_join.asp

akiyoko.hatenablog.jp/entry/2016/07/31/232754

oniondev.egloos.com/9869119

akiyoko.hatenablog.jp/entry/2016/08/03/080941

 

728x90