Django'da select_related ve prefetch_related kullanımı

Bir proje geliştirirken dikkat ettiğimiz konulardan birtanesi de kuşkusuz sistemin olabildiğince performanslı çalışmasını sağlamaktır.

Performansı artırmanın birden fazla yöntemi olduğu gibi bir yöntemi de ihtiyacımız olan veriyi DB’den olabildiğince az sorgu ile çekmektir. [1]


Şu şekilde modellerimiz olduğunu varsayalım.

class Category(models.Model):
    name = models.CharField(max_length=30)

class Product(models.Model):
    name = models.CharField(max_length=100)
    category = models.ForeignKey(Category, related_name='products')

Şimdi ise bu modeller üzerinden hangi kod nekadar db sorgusu attığını ve nasıl daha optimize edebiliriz göstermeye çalışacağım.

Mesela bir ürünü çekip ürünün ismini ve bağlı olduğu kategorinin ismini yazdırmak istiyoruz diyelim.

product = Product.objects.first()
print(product.name, product.category.name)

Burda ürünü çektiğimizde bir query, kategorinin ismine erişmeye çalıştığımızda ekstra bir query daha atılır.

Yukarıdaki örnekte olduğu gibi kullanacağımız kesin olan yerlerde objeyi çekerken ilişkili diğer objeyi select_related ile çekebiliriz.

product = Product.objects.select_related('category').first()
print(product.name, product.category.name)

Burda ürünü getirirken bağlı olduğu kategoriyide beraberinde çektiğimizden kategorinin fieldlarına erişmeye çalıştığımızda ekstra bir query atmaz.

prefetch_related, many-to-many ve reverse relation ile birbirine bağlı objeleri tek seferde çekmemizi sağlar.

categories = Category.objects.prefetch_related('products').all()

for category in categories:
    print(category.products.all())

Burada kategoriyi çekmek için bir query, kategorilere bağlı ürünleri çekmek için ayrı bir query atar. Daha sonra python tarafında çektiği ürünleri ilgili kategori ile ilişkilendirir. Binlerce kategori, binlerce ürün bile olsa toplamda 2 query atar.

Mesela kategorileri çekerken, kategorilere bağlı ürünleri ve her ürünün sahibini çekmek istiyoruz diyelim.

Performanssız yontem:

Category.objects.prefetch_related('products__owner').all()

Bu query DB’ye 3 query atar. Birincisi kategorileri çekmek için, ikincisi kategorilere bağlı ürünleri çekmek için, üçüncüsü ise her bir ürünün sahibini çekmek için.

Performanslı yöntem:

Category.objects.all().prefetch_related(
    Prefetch('products', queryset=Product.objects.select_related('owner'))
)

Bu 2 query atar. 1. kategorileri, 2. ise ürünleri çekmek için. Ürünleri getirirken select_related ile ürünün sahibinide çeker(iki işlem tek query’de).

1 - prefeth_related ile çekilen objeler üzerinde filter(), exists(), count(), first(), last() gibi methodlar kullanıldığında bunlar ekstra query atar.

category = Category.objects.prefetch_related('products').get(pk=1) # 2 query
category.products.filter(active=True) # Ekstra query
category.products.exists() # Ekstra query
category.products.count() # Ekstra query

Diyelimki kategoriye ait ürünleri alt alta yazdırmak istiyoruz ama kategoriye ait ürün yoksa farklı bir işlem yapmak istiyoruz diyelim.

Bu örnekte toplamda 3 query atar:

category = Category.objects.prefetch_related('products').get(pk=1)

if category.products.exists(): # Ekstra query
    for product in category.products.all():
        print(product.name)
else:
    print('Kategoride urun yok')

Bu örnekte ise toplamda 2 query atar:

category = Category.objects.prefetch_related('products').get(pk=1)

if category.products.all():
    for product in category.products.all():
        print(product.name)
else:
    print('Kategoride urun yok')

2- Aşağıdaki örnek 3 query atar. Biri ürünü çekmek için, biri kategoriyi çekmek için diğeri ise kategoriye bağlı ürünleri çekmek için:

Product.objects.prefetch_related('category__products').all()

Ürünü çekerken beraberinde bağlı olduğu kategoriyide çekersek, kategoriye bağlı olan ürünleri erişmeye çalıştığımızda kategori için ayrı bir query daha atmaz. Bu örnekte sadece 2 query atar:

Product.objects.select_related('category').prefetch_related('category__products').all()

[1]: Bazı durumlarda (4-5 tabloyu birbirine joinlemek gibi), herseyi tek queryde getirmek yerine ayrı ayrı queryler atmak daha performanslı olabiliyor.

comments powered by Disqus