티스토리 뷰

반응형

공부하면서 json field을 쓰는 것을 보고 과연 join과 비교했을 때 얼마나 성능에 대한 이점이 있을까 고민을 하다가 한 번 실험해보기로 했습니다.

 

framework: django
database: postgresql 13
python version : 3.9
computer: macbook-pro 16inch m1 max 

제가 테스트한 환경입니다.

 


 

1. Account

class Account(BaseModel):
    name = models.CharField(max_length=25)

    objects = models.Manager()

    def __repr__(self):
        return f'id : {self.id}, name: {self.name}'

정규화 모델로 쓸 유저 모델입니다. 단순하게 이름 필드만 추가했습니다.

 

2. Actor

class Actor(BaseModel):
    name = models.CharField(max_length=25)
    data = models.JSONField(default=dict(), null=True, blank=True)
    objects = models.Manager()

    def __repr__(self):
        return f'id : {self.id}, name: {self.name}'

account와 다르게 Json 필드를 쓰기 위해 만든 유저 모델입니다.

 

3. Orders

class Orders(BaseModel):
    customer = models.ForeignKey(Account, on_delete=models.PROTECT, related_name='orders', null=True)
    client = models.ForeignKey(Actor, on_delete=models.PROTECT, related_name='orders', null=True)
    status = models.CharField(max_length=30, choices=OrderStatus.choices, default=OrderStatus.PREPARE)
    objects = models.Manager()

    def add_items(self, items):
        from orderitems.models import OrderItem
        orders = self
        data = []
        for item in items:
            data.append(OrderItem(orders=orders, items=item))
        OrderItem.objects.bulk_create(data)
        orders.status = OrderStatus.INVOICE_PRINTING
        orders.save()

    def cancel_items(self, items):
        orders = self
        orders.order_items.filter(orders=orders, items__in=items).delete()

    def __repr__(self):
        return f'주문 아이디 : {self.id} | : 손님 :{self.customer.id}::{self.customer.name} 주문 상태 {self.status}'

손님이 주문했을 때 사용하는 테이블입니다. 

여기서 customer는 json 필드가 없는 account와 연관관계를 맺었고, client는 json 필드를 가지고 있는 actor와 연관관계를 맺었습니다.

 

밑의 메소드는 주문했을 때 어떤 아이템을 주문했는지 매핑하는 OrderItem을 생성, 삭제 하는 편의 메소드를 미리 만들어놨습니다.

 

4. OrderItem

class OrderItem(BaseModel):
    orders = models.ForeignKey(Orders, on_delete=models.CASCADE, related_name='order_items')
    items = models.ForeignKey(Item, on_delete=models.RESTRICT, related_name='items')

    def __repr__(self):
        return f'id : {self.id} :: 주문 id: {self.orders.id}:: item {self.items.id}:{self.items.name}'

주문과 one to many 관계 , 아이템과 one to many 관계를 가지고 있는 테이블입니다.

 

 

5. Item

class Item(BaseModel):
    name = models.CharField(max_length=50)

    def __repr__(self):
        return f'id: {self.id} , name: {self.name}'

 

 


TestCase

 

1. SetUp

간단하게 유저준비와 아이템 준비를 해봤다.

 

 

1. 저장 시간 비교

1. json 필드를 통해 저장했을 때

    def test_json_저장시간(self):
        with CaptureQueriesContext(connection) as cqc:
            actor = Actor.objects.get(name='유저0')
            order = Orders.objects.create(client=actor)
            items = Item.objects.filter(name__startswith='아이템1')    # 111개
            actor.data['order_item'] = [item.id for item in items]
            actor.save()
        queries = connection.queries
        time = 0    # 0.013000000000000001
        for query in queries:
            time += float(query.get('time', 0))
        pass

아이템 111개의 아이디들을 json 필드 key는 'order_item'으로 value는 list 형태로 두되 거기에 item id 값을 넣었다.

 

시간은 대략

0.013000000000000001

 

2. join을 통해 저장했을 때

    def test_조인_저장시간(self):
        with CaptureQueriesContext(connection) as cqc:
            account = Account.objects.get(name='유저0')
            items = Item.objects.filter(name__startswith='아이템1')    # 111개
            order = Orders.objects.create(customer=account)
            order.add_items(items)
        queries = connection.queries
        # 개별 create를 때릴 때는 0.1210000000000001   여러개의 아이템을 저장할 때 bulk_create를 썼더니 많은 시간을 절약할 수 있었다.
        time = 0    # 0.02
        for query in queries:
            time += float(query.get('time', 0))
        pass

처음에 bulk_create로 하지 않고 각 orderitem 별로 create를 날렸을 때는 json 시간 대비 10배 정도 차이가 났었다.

하지만 bulk_create를 썼을 때는 큰 차이가 안 났었다.

 

그러면 십만개 넣었을 때는 시간차이가 얼마나 날까?

 

  json join
10_000개 0.967 8.986

확실히 join 즉 테이블을 여러개로 나눴을 때 생성하는 시간은 더 걸리는 것을 확인할 수 있다.

 

 

 

2. 삭제 시간 비교

1. json

    def test_json_삭제시간(self):
        actor = Actor.objects.get(name='유저0')
        order = Orders.objects.create(client=actor)
        items = Item.objects.filter(name__startswith='아이템1')  # 111
        actor.data['order_item'] = [item.id for item in items]
        actor.save()
        with CaptureQueriesContext(connection) as cqc:
            item = Item.objects.get(name='아이템1')
            """
            'SELECT "items_item"."id", "items_item"."created_at", "items_item"."modified_at", "items_item"."name" FROM "items_item" WHERE "items_item"."name" = \'아이템1\'
            """
            order_item: list = actor.data['order_item']
            order_item.remove(item.id)
            """
            'UPDATE "actor_actor" SET "created_at" = \'2022-01-09T07:12:46.623940+00:00\'::timestamptz, 
            "modified_at" = \'2022-01-09T07:12:48.235329+00:00\'::timestamptz,
             "name" = \'유저0\',
              "data" = \'{"order_em": [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200]}\' 
              WHERE "actor_actor"."id" = 1'
            """
            actor.save()
        queries = connection.queries
        time = 0    # 0.004, query 2개
        for query in queries:
            time += float(query.get('time', 0))
        pass

 

2. join

    def test_조인_삭제시간(self):
        # given
        account = Account.objects.get(name='유저0')
        items = Item.objects.filter(name__startswith='아이템1')  # 111
        order = Orders.objects.create(customer=account)
        order.add_items(items)
        # when
        with CaptureQueriesContext(connection) as cqc:
            item = Item.objects.filter(name='아이템1')
            order.cancel_items(item)
        queries = connection.queries
        time = 0  # 0.006  query 1개
        """
        'DELETE FROM "orderitems_orderitem" WHERE 
        (
            "orderitems_orderitem"."orders_id" = 1 
             AND "orderitems_orderitem"."items_id" 
                IN 
                (SELECT U0."id" FROM "items_item" U0 WHERE U0."name" = \'아이템1\')
             AND
             "oritems_orderitem"."orders_id" = 1
        )'
        """
        for query in queries:
            time += float(query.get('time', 0))
        pass

우선 결과만 보면 삭제할 때에도 json 필드가 좀 더 빠른 것을 알 수가 있다.

 

그럼 다량으로 삭제할 때는 얼마나 차이가 날까?

 

  json join
111 0.555 0.382
100000 0.722 0.795

놀라운 사실이다. 갯수가 늘어나도 성능 상 큰 차이가 없다.

 

 

3. 수정시간 비교( 추가)

1. json

    def test_json필드_하나수정할_때(self):
        actor = Actor.objects.get(name='유저0')
        order = Orders.objects.create(client=actor)
        items = Item.objects.filter(name__startswith='아이템1')  # 111
        actor.data['order_item'] = [item.id for item in items]
        actor.save()
        with CaptureQueriesContext(connection) as cqc:
            item = Item.objects.get(name='아이템2')
            order_item: list = actor.data['order_item']
            order_item.append(item.id)
            """
            하나 추가 되어도 전체를 다시 업데이트하는 것과 같다.
            """
            actor.save()
        queries = connection.queries
        time = 0    # 0.004
        for query in queries:
            time += float(query.get('time', 0))
        pass

2. join

    def test_join필드_하나_수정할_때(self):
        account = Account.objects.get(name='유저0')
        items = Item.objects.filter(name__startswith='아이템1')  # 111
        order = Orders.objects.create(customer=account)
        order.add_items(items)
        with CaptureQueriesContext(connection) as cqc:
            item = Item.objects.get(name='아이템2')
            order.add_items([item])
            """
            'INSERT INTO "orderitems_orderitem" ("created_at", "modified_at", "orders_id", "items_id") VALUES (\'2022-01-09T07:29:51.659504+00:00\'::timestamptz, \'2022-01-09T07:29:51.659519+00:00\'::timestamptz, 1, 3) RETURNING "orderitems_orderitem"."id"'
            """
        queries = connection.queries
        time = 0    # 0.004
        for query in queries:
            time += float(query.get('time', 0))
        pass

 

갯수가 많아지면 어떻게 될까?

사실 추가하는 것과 별 다른 게 없어서 생성과 유사한 차이가 있다는 것을 알 수가 있다.

  json join
11111 0.186 1.0959999999999999

 

 

4. 조회

1. json

    def test_json필드_조회(self):
        actor = Actor.objects.get(name='유저0')
        order = Orders.objects.create(client=actor)
        items = Item.objects.filter(name__startswith='아이템1')  # 111
        actor.data['order_item'] = [item.id for item in items]
        actor.save()
        with CaptureQueriesContext(connection) as cqc:
            Actor.refresh_from_db(actor)
            data = actor.data
        queries = connection.queries
        time = 0    # 0.002
        for query in queries:
            time += float(query.get('time', 0))
        pass

 

2. join

    def test_join필드_조회(self):
        # given
        account = Account.objects.get(name='유저0')
        items = Item.objects.filter(name__startswith='아이템1')  # 111
        order = Orders.objects.create(customer=account)
        order.add_items(items)
        # when
        with CaptureQueriesContext(connection) as cqc:
            item_ids = OrderItem.objects.filter(orders__customer=account).values_list('items_id', flat=True)
            """
            'SELECT "orderitems_orderitem"."items_id" FROM "orderitems_orderitem" INNER JOIN "orders_orders" ON ("orderitems_orderitem"."orders_id" = "orders_orders"."id") WHERE "orders_orders"."customer_id" = 1 
            """
            print(item_ids)
        queries = connection.queries
        time = 0    # 0.003
        for query in queries:
            time += float(query.get('time', 0))
        pass

우선 갯수가 100개 정도 되었을 때는 큰 차이가 없었다.

  json join
100000 0.086 0.017

갯수가 많을 때 솔직히 json이 더 높을 거라 생각했는데 이걸 그렇지 않았다.

물론 특정 필드만 조회한 결과이긴 하지만 그렇기엔 json data 안에 있는 필드 또한 아이디 값만 저장되어 있기에 공평한 과정이라 생각한다.

 


정리

  json join
생성
수정
삭제 무승부 무승부
조회

 

제가 테스트 한 결과가 다음 위의 표와 같습니다.

 

우리는 일반적으로 join이 시간이 더 많이 걸려서 꺼려진다. 그렇다고 json 필드로 처리하는 것이 맞을까? 라는 궁금증에서 다음과 같이 테스트를 해봤습니다.

우선 우리는 보통 생성,삭제, 수정 보다는 조회를 훨씬 많이 합니다. 

 

이 때 일정 갯수 임계점을 넘어간다면 join이 더 나은 판단이라는 것을 알 수 있습니다.(전제: 특정 필드만 골라서 조회한다고 가정했을 때이다. 만약에 테이블 모든 컬럼을 조회한다면 json 으로 처리하는 것을 더 빠를 것이다. [ 이것에 대한 테스트는 추후에 올릴 예정])

 

만일 당신이 조회 성능에 대해서 걱정이 되서 json필드를 쓴다면 좀 더 고려해보는 것을 추천합니다.( 물론 특정 row에 대해서 트랜잭션이 걸렸을 때 조회성능은 테스트를 못해봤습니다.)

 

저는 json필드를 자제하는 것을 추천하는 이유는 다음과 같습니다.

1. 애플리케이션 코드 상에서 json 필드 안에서 어떻게 구성되어 있는지 파악할 수가 없다. ( 따로 문서로 남겨야 한다는 단점이 명확)

2. json 필드를 많은 것을 처리했을 때 오히려 성능 상에서 많이 떨어질 수가 있다. (특정 row에 대해서 락이 걸렸을 때 수정, 삭제 같은 것이 불가능)

3. 하나의 객체에서 많은 역할을 하게 된다.( 유저에서 주문의 아이템 현황을 가지고 있는 것은 객체의 역할과 책임에서 많은 것을 가지게 된다.)

4. json field가 비즈니스의 변동에 대해서 대응하고자 추가한 것인데 오히려 비즈니스 변동에 대해서 처리하는 것이 더 힘들 수가 있다. 그 이유는 현재 아이템이 하나만 팔린다고 고려하고 처리를 하였는데 복수개를 고를 수가 있다고 하자. 이 때 어떤 것이 더 처리하기 쉬울까? 독자들한테 판단을 맡기겠습니다.

 

조인도 잘 쓰면 충분히 json field 정도의 성능이 나오며 오히려 더 나은 결과를 가져올 수 있습니다.

 

 

반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함