ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Django 에서 Chain 필터 시 추가적인 테이블 JOIN 방지하기 ( + _next_is_sticky )
    기술/Django 2021. 9. 24. 19:06

    Django 에서 역관계에 있는 모델을 필터할 때 필터를 Chain 하여 사용할 경우

    같은 테이블을 두고 필터하더라도 추가적으로 Join 하여 필터합니다. 이 때문에 예상과 결과가 달라질 수 있습니다.

     

    주로 API 를 작성할 때 Django-Filters 를 사용하게 되는데

    복잡한 QueryString 을 받는 경우, Chain Query 가 많이 일어나며 발생하였습니다.

     

    간단한 샘플 모델

    class Post(models.Model):
      pass
    
    class Comment(models.Model):
      post = models.ForeignKey(Post)

    Chain Filter 와 Chain Filter 를 하지 않은 것의 차이

    Chain Filter 를 하지 않았을 때 추가적으로 Join 하지 않고 하나의 Join에 2개의 Where 를 사용합니다.

    Post.objects.filter(comment_set__deleted__isnull=True, comment_set__author_id=1)
    
    # SELECT *
    # FROM post
    # inner join comment on post.id = comment.post_id
    # where comment.deleted IS NULL and comment.author_id=1

     

    Chain Filter 를 하게 되었을 때 추가적으로 같은 테이블을 Join 하고 각각 Where 를 사용합니다.

     

    Post.objects.filter(comment_set__deleted__isnull=True).filter(comment_set__author=1)
    
    # SELECT *
    # FROM post
    # inner join comment on post.id = comment.post_id
    # inner join comment T3 on post.id = T3.post_id
    # where comment.deleted IS NULL and T3.author_id=1

     

    두 결과는 당연히 다른 결과를 나타나게 됩니다.

    Chain Filter 의 추가적인 Inner Join 을 해결하기 위한 내부 동작

    이를 해결하기 위해 Queryset class 는 내부적으로 _sticky_filter 라는 Flag 값을 가지고 있습니다.

    해당 값을 가진 상태로 추가적인 Chain 필터를 실행하게 되면 inner join 을 추가적으로 하지 않고 필터를 실행합니다.

     

    해당 플래그를 True 로 변경하기 위해서는 _next_is_sticky 라는 메서드를 호출하면 됩니다.

    그리고 이후 두개의 필터가 같은 테이블을 가지고 Where 를 사용하는 방식입니다.

    Post.objects._next_is_sticky().filter(comment_set__deleted__isnull=True).filter(comment_set__author_id=1)
    
    # SELECT *
    # FROM post
    # inner join comment on post.id = comment.post_id
    # where comment.deleted IS NULL and comment.author_id=1

     

    Django 코드 내의 _next_is_sticky 의 설명은 이렇게 작성되어있습니다.

    다음 필터 호출과 그 다음은 단일 필터로 처리해야함을 나타냅니다.
    이는 다대다 필터에 대한 테이블 재사용할 시기를 결정할 때 중요합니다.
    관련 관리자의 결과를 자연스럽게 걸러내질 수 있도록 해야합니다.

    이것은 현재 쿼리셋의 클론을 반환하지 않습니다. ( 자신(‘self’)를 반환합니다. )
    이 방법은 내부적으로만 사용되며, 즉시, 클론을 생성하는 filter 메서드가 뒤따라야합니다.

     

    더보기

    Sticky Filter 를 이해하기 위한 삽질

    query = Merchant.objects.all()
    query = query._next_is_sticky().filter(virtual_account__status='pending').filter(virtual_account__account_number__icontains='22').values('id', 'virtual_account__id', 'virtual_account__status')
    
    print('필터 이전에 Sticky 를 사용해야한다. Reason: ', query._sticky_filter)
    print(query.query, end='\n\n')
    
    # Sticky 를 filter 들 맨 앞에 작성했을 대, T3 를 새로 만들지 아니한다.
    # SELECT `tm2s_merchant`.`id`, `virtual_account_virtualaccount`.`id`, `virtual_account_virtualaccount`.`status`
    # FROM `tm2s_merchant`
    #          INNER JOIN `virtual_account_virtualaccount`
    #                     ON (`tm2s_merchant`.`id` = `virtual_account_virtualaccount`.`merchant_id`)
    # WHERE (`virtual_account_virtualaccount`.`status` = pending AND
    #        `virtual_account_virtualaccount`.`account_number` LIKE % 22 %)
    # ORDER BY `tm2s_merchant`.`id` DESC;
    
    
    query = Merchant.objects.all()
    query = query.filter(virtual_account__status='pending').filter(virtual_account__account_number__icontains='22')._next_is_sticky().values('id', 'virtual_account__id', 'virtual_account__status')
    
    print('필터 이후에 Sticky 를 사용해야한다. Chaning 으로 생성된 queryset 은 sticky 가 매번 초기화된다 Reason: ', query._sticky_filter)
    print(query.query, end='\n\n')
    
    # Sticky 를 filter 맨 뒤에 작성했을 때, T3 를 새로 만든다.
    # SELECT `tm2s_merchant`.`id`, T3.`id`, T3.`status`
    # FROM `tm2s_merchant`
    #          INNER JOIN `virtual_account_virtualaccount`
    #                     ON (`tm2s_merchant`.`id` = `virtual_account_virtualaccount`.`merchant_id`)
    #          INNER JOIN `virtual_account_virtualaccount` T3 ON (`tm2s_merchant`.`id` = T3.`merchant_id`)
    # WHERE (`virtual_account_virtualaccount`.`status` = pending AND T3.`account_number` LIKE % 22 %)
    # ORDER BY `tm2s_merchant`.`id` DESC;
    
    query = Merchant.objects.all()
    query = query.filter(virtual_account__status='pending')._next_is_sticky().filter(virtual_account__account_number__icontains='22').values('id', 'virtual_account__id', 'virtual_account__status')
    
    print('필터 가운데에 Sticky 를 사용해야한다. Reason: ', query._sticky_filter)
    print(query.query, end='\n\n')
    
    query = Merchant.objects.all()
    query = query._next_is_sticky().all().filter(virtual_account__status='pending').filter(virtual_account__account_number__icontains='22').values('id', 'virtual_account__id', 'virtual_account__status')
    
    print('sticky 필터는 연속으로 사용해야한다. Reason: ', query._sticky_filter)
    print(query.query, end='\n\n')
    
    # Sticky 를 filter 사이에 작성했을 때, T3 를 새로 만든다.
    # SELECT `tm2s_merchant`.`id`, T3.`id`, T3.`status`
    # FROM `tm2s_merchant`
    #          INNER JOIN `virtual_account_virtualaccount`
    #                     ON (`tm2s_merchant`.`id` = `virtual_account_virtualaccount`.`merchant_id`)
    #          INNER JOIN `virtual_account_virtualaccount` T3 ON (`tm2s_merchant`.`id` = T3.`merchant_id`)
    # WHERE (`virtual_account_virtualaccount`.`status` = pending AND T3.`account_number` LIKE % 22 %)
    # ORDER BY `tm2s_merchant`.`id` DESC;
    
    query = Merchant.objects.all()
    query = query._next_is_sticky().all()._next_is_sticky().filter(virtual_account__status='pending').filter(virtual_account__account_number__icontains='22').values('id', 'virtual_account__id', 'virtual_account__status')
    
    print('sticky 이후에 all( filter 이외의 것) 이 오면 T3 를 새로 만든다. Reason: ', query._sticky_filter)
    print(query.query, end='\n\n')
    
    # Sticky 이후에 all( filter 이외의 것 ) 이 온 경우 T3가 생성된다.
    # SELECT `tm2s_merchant`.`id`, T3.`id`, T3.`status`
    # FROM `tm2s_merchant`
    #          INNER JOIN `virtual_account_virtualaccount`
    #                     ON (`tm2s_merchant`.`id` = `virtual_account_virtualaccount`.`merchant_id`)
    #          INNER JOIN `virtual_account_virtualaccount` T3 ON (`tm2s_merchant`.`id` = T3.`merchant_id`)
    # WHERE (`virtual_account_virtualaccount`.`status` = pending AND T3.`account_number` LIKE % 22 %)
    # ORDER BY `tm2s_merchant`.`id` DESC;
    
    query = Merchant.objects.all()
    query = query._next_is_sticky().all()._next_is_sticky().filter(virtual_account__status='pending').filter(virtual_account__account_number__icontains='22').values('id', 'virtual_account__id', 'virtual_account__status')
    
    print('연속으로 사용하지 못할 시 앞에 sticky 를 한번 더 붙여주어야한다. Reason: ', query._sticky_filter)
    print(query.query, end='\n\n')
    
    # sticky 이후에 all( filter 이외의 것) 이 온 경우 sticky 를 한번 더 명시한다.
    # SELECT `tm2s_merchant`.`id`, `virtual_account_virtualaccount`.`id`, `virtual_account_virtualaccount`.`status`
    # FROM `tm2s_merchant`
    #          INNER JOIN `virtual_account_virtualaccount`
    #                     ON (`tm2s_merchant`.`id` = `virtual_account_virtualaccount`.`merchant_id`)
    # WHERE (`virtual_account_virtualaccount`.`status` = pending AND
    #        `virtual_account_virtualaccount`.`account_number` LIKE % 22 %)
    # ORDER BY `tm2s_merchant`.`id` DESC;
    
    query = Merchant.objects.all()
    query = query._next_is_sticky().filter(virtual_account__status='pending').filter(virtual_account__account_number__icontains='22').filter(virtual_account__account_number__icontains='33').values('id', 'virtual_account__id', 'virtual_account__status')
    
    print('Sticky 는 뒤에 오는 필터의 2개를 연결하는데 사용한다.. Reason: ', query._sticky_filter)
    print(query.query, end='\n\n')
    
    # Sticky 는 바로오는 뒤의 두 필드만을 JOIN AND 한다.
    # SELECT `tm2s_merchant`.`id`, T3.`id`, T3.`status`
    # FROM `tm2s_merchant`
    #          INNER JOIN `virtual_account_virtualaccount`
    #                     ON (`tm2s_merchant`.`id` = `virtual_account_virtualaccount`.`merchant_id`)
    #          INNER JOIN `virtual_account_virtualaccount` T3 ON (`tm2s_merchant`.`id` = T3.`merchant_id`)
    # WHERE (`virtual_account_virtualaccount`.`status` = pending AND
    #        `virtual_account_virtualaccount`.`account_number` LIKE % 22 % AND T3.`account_number` LIKE % 33 %)
    # ORDER BY `tm2s_merchant`.`id` DESC;
    
    query = Merchant.objects.all()
    query = query._next_is_sticky().filter(virtual_account__status='pending').filter(virtual_account__account_number__icontains='22')._next_is_sticky().filter(virtual_account__account_number__icontains='33').values('id', 'virtual_account__id', 'virtual_account__status')
    
    print('그럼 Sticky 는 정말 두 필드만을 JOIN AND 할까? Reason: ', query._sticky_filter)
    print(query.query, end='\n\n')
    
    # Sticky 는 바로오는 뒤의 두 Queryset 만을 Join And 하므로 filtr, filter 를 JOIN AND 하고, 맨 마지막은 T3 를 만들어 AND 한다.
    # SELECT `tm2s_merchant`.`id`, T3.`id`, T3.`status`
    # FROM `tm2s_merchant`
    #          INNER JOIN `virtual_account_virtualaccount`
    #                     ON (`tm2s_merchant`.`id` = `virtual_account_virtualaccount`.`merchant_id`)
    #          INNER JOIN `virtual_account_virtualaccount` T3 ON (`tm2s_merchant`.`id` = T3.`merchant_id`)
    # WHERE (`virtual_account_virtualaccount`.`status` = pending AND
    #        `virtual_account_virtualaccount`.`account_number` LIKE % 22 % AND T3.`account_number` LIKE % 33 %)
    # ORDER BY `tm2s_merchant`.`id` DESC;
    
    query = Merchant.objects.all()
    query = query._next_is_sticky().filter(virtual_account__status='pending').filter(virtual_account__account_number__icontains='22')._next_is_sticky().filter(virtual_account__account_number__icontains='33').values('id', 'virtual_account__id', 'virtual_account__status')
    
    print('그럼 3개를 연결하고 싶으면? Reason: ', query._sticky_filter)
    print(query.query, end='\n\n')
    
    # SELECT `tm2s_merchant`.`id`, `virtual_account_virtualaccount`.`id`, `virtual_account_virtualaccount`.`status`
    # FROM `tm2s_merchant`
    #          INNER JOIN `virtual_account_virtualaccount`
    #                     ON (`tm2s_merchant`.`id` = `virtual_account_virtualaccount`.`merchant_id`)
    # WHERE (`virtual_account_virtualaccount`.`status` = pending AND
    #        `virtual_account_virtualaccount`.`account_number` LIKE % 22 % AND
    #        `virtual_account_virtualaccount`.`account_number` LIKE % 33 %)
    # ORDER BY `tm2s_merchant`.`id` DESC;

    댓글

Designed by Tistory.