Django ORM: เพิ่มประสิทธิภาพคิวรีให้ทำงานเต็มขีดความสามารถ

คู่มือฉบับสมบูรณ์สำหรับเพิ่มประสิทธิภาพคิวรีของ Django ORM ทั้ง select_related, prefetch_related, ดัชนี การวิเคราะห์ปัญหา N+1 และเทคนิคขั้นสูงสำหรับแอปพลิเคชันที่ต้องการประสิทธิภาพสูง

การปรับแต่งคิวรี Django ORM เพื่อประสิทธิภาพสูงสุด

Django ORM มอบเลเยอร์การเข้าถึงฐานข้อมูลที่เรียบง่ายและสง่างาม แต่ความเรียบง่ายดังกล่าวอาจซ่อนปัญหาประสิทธิภาพที่สำคัญเอาไว้ แอปพลิเคชัน Django ที่ไม่ได้รับการปรับแต่งสามารถสร้างคิวรีนับร้อยรายการในที่ที่คิวรีเดียวก็เพียงพอ คู่มือนี้นำเสนอเทคนิคหลักสำหรับตรวจหาและแก้ไขปัญหาเหล่านั้น

กฎทองของการปรับแต่ง

ก่อนปรับแต่งจะต้องวัดผล การใช้ django-debug-toolbar ในขั้นพัฒนาช่วยให้เห็นทุกคิวรี SQL ที่สร้างขึ้นและระบุจุดคอขวดได้อย่างรวดเร็ว

เข้าใจปัญหา N+1

ปัญหา N+1 คือกับดักที่พบบ่อยที่สุดเวลาทำงานกับ ORM โดยจะเกิดขึ้นเมื่อคิวรีแรกดึงวัตถุ N รายการ จากนั้นจึงต้องมีคิวรีเพิ่มเติมอีก N ครั้งเพื่อเข้าถึงความสัมพันธ์ของแต่ละวัตถุ การทวีจำนวนคิวรีนี้ทำให้ประสิทธิภาพลดลงอย่างมีนัยสำคัญ

python
# models.py
from django.db import models

class Author(models.Model):
    """โมเดลแทนผู้แต่งของหนังสือ"""
    name = models.CharField(max_length=200)
    email = models.EmailField(unique=True)
    bio = models.TextField(blank=True)

    def __str__(self):
        return self.name

class Book(models.Model):
    """โมเดลแทนหนังสือพร้อมผู้แต่ง"""
    title = models.CharField(max_length=300)
    author = models.ForeignKey(
        Author,
        on_delete=models.CASCADE,
        related_name='books'
    )
    published_date = models.DateField()
    isbn = models.CharField(max_length=13, unique=True)

    def __str__(self):
        return self.title

โมเดลเรียบง่ายเหล่านี้ช่วยให้แสดงปัญหา N+1 ได้อย่างเป็นรูปธรรม

python
# views.py - ตัวอย่างที่มีปัญหา
def list_books_bad(request):
    """❌ วิวนี้ก่อให้เกิด N+1 คิวรี"""
    books = Book.objects.all()  # 1 คิวรีสำหรับหนังสือ

    for book in books:
        # ทุกการเข้าถึง book.author จะกระตุ้นคิวรี SQL
        print(f"{book.title} โดย {book.author.name}")

    # ถ้ามีหนังสือ 100 เล่ม = 101 คิวรี SQL!
    return render(request, 'books/list.html', {'books': books})

โค้ดดูธรรมดาแต่กลับสร้างคิวรีหนึ่งครั้งต่อหนังสือหนึ่งเล่มเพื่อดึงผู้แต่งที่เกี่ยวข้อง

เมธอด select_related จะทำ SQL JOIN และดึงข้อมูลที่สัมพันธ์กันมาในคิวรีเดียว ใช้ได้กับความสัมพันธ์ ForeignKey และ OneToOneField

python
# views.py - โซลูชันที่ปรับแต่งแล้วด้วย select_related
def list_books_optimized(request):
    """✅ วิวนี้สร้างคิวรีเดียวที่มี JOIN"""
    # select_related ทำ SQL JOIN
    books = Book.objects.select_related('author').all()

    for book in books:
        # ไม่มีคิวรีเพิ่มเติม: ผู้แต่งถูกโหลดมาแล้ว
        print(f"{book.title} โดย {book.author.name}")

    # รวม: 1 คิวรี SQL เท่านั้นไม่ว่าจะมีหนังสือกี่เล่ม
    return render(request, 'books/list.html', {'books': books})

คิวรี SQL ที่สร้างขึ้นใช้ LEFT OUTER JOIN เพื่อดึงผู้แต่งมาพร้อมกับหนังสือ

python
# ต่อ select_related สำหรับความสัมพันธ์ซ้อนกัน
# models.py
class Publisher(models.Model):
    name = models.CharField(max_length=200)
    country = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=300)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)

# views.py
def list_books_with_details(request):
    """ดึงหนังสือ ผู้แต่ง และสำนักพิมพ์ด้วยคิวรีเดียว"""
    books = Book.objects.select_related(
        'author',      # ForeignKey ไปยัง Author
        'publisher'    # ForeignKey ไปยัง Publisher
    ).all()

    return render(request, 'books/list.html', {'books': books})

หลายความสัมพันธ์สามารถปรับแต่งพร้อมกันได้โดยระบุไว้ใน select_related

สำหรับความสัมพันธ์ ManyToMany หรือความสัมพันธ์ย้อนกลับ (ForeignKey จากอีกฝั่ง) prefetch_related จะรันคิวรีแยกที่ปรับแต่งไว้แล้ว เพื่อหลีกเลี่ยง JOIN ขนาดใหญ่

python
# models.py
class Tag(models.Model):
    """แท็กสำหรับจัดหมวดหมู่หนังสือ"""
    name = models.CharField(max_length=50, unique=True)

    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=300)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    tags = models.ManyToManyField(Tag, related_name='books')

ความสัมพันธ์ ManyToMany ระหว่าง Book และ Tag ต้องใช้ prefetch_related เพื่อให้การปรับแต่งมีประสิทธิภาพ

python
# views.py - การปรับแต่ง ManyToMany
def list_books_with_tags(request):
    """✅ ดึงหนังสือพร้อมแท็กอย่างมีประสิทธิภาพ"""
    books = Book.objects.prefetch_related('tags').all()

    for book in books:
        # แท็กถูกโหลดล่วงหน้าแล้ว ไม่ต้องคิวรีเพิ่ม
        tag_names = [tag.name for tag in book.tags.all()]
        print(f"{book.title}: {', '.join(tag_names)}")

    # รวม: 2 คิวรี (หนังสือ + แท็ก) ไม่ว่าจะมีกี่รายการ
    return render(request, 'books/list.html', {'books': books})

prefetch_related รันคิวรีแยกสำหรับแท็กแล้วจึงรวมข้อมูลใน Python

select_related vs prefetch_related

ควรใช้ select_related กับ ForeignKey และ OneToOne (SQL JOIN) ใช้ prefetch_related กับ ManyToMany และความสัมพันธ์ย้อนกลับ (คิวรีแยก) สามารถผสมทั้งสองได้บน QuerySet เดียวกัน

ปรับแต่งการพรีโหลดด้วยอ็อบเจ็กต์ Prefetch

อ็อบเจ็กต์ Prefetch ให้ความยืดหยุ่นในการควบคุมข้อมูลที่ถูกโหลดล่วงหน้า ทั้งการกรอง การเรียง และการจำกัดจำนวนผลลัพธ์

python
# views.py
from django.db.models import Prefetch

def list_authors_with_recent_books(request):
    """ดึงผู้แต่งพร้อมเฉพาะหนังสือที่ใหม่"""
    # Prefetch แบบกำหนดเอง: เฉพาะหนังสือตั้งแต่ปี 2025
    recent_books_prefetch = Prefetch(
        'books',
        queryset=Book.objects.filter(
            published_date__year__gte=2025
        ).order_by('-published_date'),
        to_attr='recent_books'  # เก็บใน attribute ที่กำหนดเอง
    )

    authors = Author.objects.prefetch_related(
        recent_books_prefetch
    ).all()

    for author in authors:
        # เข้าถึงผ่าน attribute ที่กำหนดเอง
        for book in author.recent_books:
            print(f"{author.name}: {book.title}")

    return render(request, 'authors/list.html', {'authors': authors})

attribute to_attr เก็บผลลัพธ์ไว้ใน list ของ Python แทน manager ตามปกติ

python
# การผสมขั้นสูง: select_related + Prefetch
def list_authors_complete(request):
    """ตัวอย่างการปรับแต่งหลายระดับแบบครบถ้วน"""
    authors = Author.objects.prefetch_related(
        Prefetch(
            'books',
            queryset=Book.objects.select_related(
                'publisher'  # ปรับแต่งสำนักพิมพ์ของแต่ละเล่มไปด้วย
            ).prefetch_related(
                'tags'       # และแท็กของแต่ละเล่ม
            ).filter(published_date__year=2026)
        )
    ).all()

    return render(request, 'authors/complete.html', {'authors': authors})

แนวทางนี้ช่วยลดจำนวนคิวรีในโครงสร้างข้อมูลที่ซับซ้อนได้อย่างมาก

พร้อมที่จะพิชิตการสัมภาษณ์ Django แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

จำกัดคอลัมน์ด้วย only() และ defer()

โดยปกติ Django จะดึงทุกคอลัมน์ของตาราง เมื่อโมเดลมีฟิลด์จำนวนมากหรือมีฟิลด์ขนาดใหญ่ การจำกัดคอลัมน์จะช่วยเพิ่มประสิทธิภาพ

python
# views.py
def list_books_minimal(request):
    """ดึงเฉพาะคอลัมน์ที่จำเป็น"""
    # only() ระบุคอลัมน์ที่ต้องการ
    books = Book.objects.only(
        'id',
        'title',
        'published_date'
    ).select_related('author')

    # ระวัง: การเข้าถึงฟิลด์ที่ไม่ได้รวมไว้จะกระตุ้นคิวรี
    for book in books:
        print(book.title)  # OK, รวมไว้
        # print(book.isbn)  # จะทำให้เกิดคิวรีเพิ่ม

    return render(request, 'books/list.html', {'books': books})

เมธอด only() สร้างอ็อบเจ็กต์แบบ "deferred" ที่โหลดเฉพาะคอลัมน์ที่ระบุ

python
# defer() เพื่อลดคอลัมน์เฉพาะบางตัว
def list_authors_without_bio(request):
    """ตัดฟิลด์ขนาดใหญ่ที่ไม่ค่อยใช้ออก"""
    # defer() ตัดคอลัมน์ที่ระบุออก
    authors = Author.objects.defer(
        'bio'  # TextField จะไม่ถูกโหลด
    ).all()

    for author in authors:
        print(author.name)   # OK
        print(author.email)  # OK
        # author.bio จะโหลดฟิลด์เมื่อจำเป็น

    return render(request, 'authors/list.html', {'authors': authors})

defer() คือสิ่งตรงข้ามของ only() คอลัมน์ที่ระบุไว้จะไม่ถูกโหลดตั้งแต่ต้น

ปรับแต่งด้วย values() และ values_list()

เมื่อต้องการเฉพาะบางค่าโดยไม่ต้องใช้อ็อบเจ็กต์โมเดลครบถ้วน values() และ values_list() จะคืนผลเป็น dictionary หรือ tuple ที่เบากว่า

python
# views.py
def get_book_titles(request):
    """ดึงเฉพาะชื่อหนังสือเป็นรายการ"""
    # values_list คืนค่าเป็น tuple
    titles = Book.objects.values_list('title', flat=True)
    # ผลลัพธ์: ['หนังสือ 1', 'หนังสือ 2', ...]

    # values คืนค่าเป็น dictionary
    book_data = Book.objects.values('title', 'published_date')
    # ผลลัพธ์: [{'title': 'หนังสือ 1', 'published_date': ...}, ...]

    return render(request, 'books/titles.html', {'titles': titles})

เมธอดเหล่านี้หลีกเลี่ยงการสร้างอ็อบเจ็กต์โมเดลและช่วยลดการใช้หน่วยความจำ

python
# ผสมกับการ aggregate
from django.db.models import Count, Avg

def get_author_statistics(request):
    """สถิติรายผู้แต่งโดยไม่ต้องโหลดอ็อบเจ็กต์"""
    stats = Author.objects.values('name').annotate(
        book_count=Count('books'),
        avg_year=Avg('books__published_date__year')
    ).order_by('-book_count')

    # ผลลัพธ์: [{'name': 'ผู้แต่ง', 'book_count': 5, 'avg_year': 2024}, ...]
    return render(request, 'authors/stats.html', {'stats': stats})

การใช้ annotation ทำให้คำนวณได้ในระดับฐานข้อมูลโดยตรง

สร้างดัชนีเพื่อเร่งคิวรี

ดัชนีของฐานข้อมูลช่วยเร่งการค้นหาได้อย่างเด่นชัด Django อนุญาตให้นิยามดัชนีในโมเดลโดยตรง

python
# models.py
from django.db import models

class Book(models.Model):
    title = models.CharField(max_length=300, db_index=True)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    published_date = models.DateField()
    isbn = models.CharField(max_length=13, unique=True)
    status = models.CharField(max_length=20, default='available')

    class Meta:
        # ดัชนีรวมสำหรับคิวรีที่ใช้บ่อย
        indexes = [
            # ดัชนีบนวันเผยแพร่ (เรียงบ่อย)
            models.Index(
                fields=['published_date'],
                name='book_pub_date_idx'
            ),
            # ดัชนีรวมสำหรับกรองตามผู้แต่ง + สถานะ
            models.Index(
                fields=['author', 'status'],
                name='book_author_status_idx'
            ),
            # ดัชนีบางส่วน: เฉพาะหนังสือที่ว่าง
            models.Index(
                fields=['published_date'],
                name='book_available_idx',
                condition=models.Q(status='available')
            ),
        ]
        # ลำดับเริ่มต้นที่ใช้ดัชนี
        ordering = ['-published_date']

ดัชนีเหล่านี้ช่วยเพิ่มประสิทธิภาพคิวรีที่กรองตามคอลัมน์ดังกล่าว

ดัชนีกับการเขียนข้อมูล

ดัชนีช่วยให้การอ่านเร็วขึ้นแต่ทำให้การเขียน (INSERT, UPDATE) ช้าลงเล็กน้อย ควรสร้างดัชนีเฉพาะคอลัมน์ที่ใช้บ่อยใน WHERE, ORDER BY หรือ JOIN เท่านั้น

หันไปใช้คิวรี SQL ดิบเมื่อจำเป็น

สำหรับคิวรีซับซ้อนหรือการปรับแต่งที่ขึ้นอยู่กับฐานข้อมูล คิวรี SQL ดิบให้การควบคุมที่สมบูรณ์

python
# views.py
from django.db import connection

def get_books_with_raw_sql(request):
    """คิวรีดิบสำหรับกรณีพิเศษ"""
    # วิธีที่ 1: raw() เพื่อคืนเป็นอ็อบเจ็กต์โมเดล
    books = Book.objects.raw('''
        SELECT b.*, a.name as author_name
        FROM library_book b
        INNER JOIN library_author a ON b.author_id = a.id
        WHERE b.published_date > %s
        ORDER BY b.published_date DESC
    ''', ['2025-01-01'])

    return render(request, 'books/list.html', {'books': books})

def execute_custom_query(request):
    """การรันโดยตรงสำหรับคิวรีที่ไม่ใช่ SELECT"""
    with connection.cursor() as cursor:
        # คิวรีที่มี aggregate ซับซ้อน
        cursor.execute('''
            SELECT
                a.name,
                COUNT(b.id) as book_count,
                AVG(EXTRACT(YEAR FROM b.published_date)) as avg_year
            FROM library_author a
            LEFT JOIN library_book b ON b.author_id = a.id
            GROUP BY a.id, a.name
            HAVING COUNT(b.id) > 2
            ORDER BY book_count DESC
        ''')
        results = cursor.fetchall()

    return render(request, 'stats.html', {'results': results})

คิวรีดิบข้าม ORM ไป แต่จะสูญเสียความสามารถในการพอร์ตข้ามฐานข้อมูล

วิเคราะห์คิวรีด้วย django-debug-toolbar

เครื่องมือ django-debug-toolbar ช่วยให้เห็นทุกคิวรี SQL ที่วิว Django สร้างขึ้น

python
# settings.py - การตั้งค่า debug toolbar
INSTALLED_APPS = [
    # ... แอปอื่น ๆ
    'debug_toolbar',
]

MIDDLEWARE = [
    'debug_toolbar.middleware.DebugToolbarMiddleware',
    # ... middleware อื่น ๆ
]

# แสดง toolbar สำหรับคำขอภายในเครื่อง
INTERNAL_IPS = ['127.0.0.1']

DEBUG_TOOLBAR_PANELS = [
    'debug_toolbar.panels.sql.SQLPanel',      # คิวรี SQL
    'debug_toolbar.panels.timer.TimerPanel',  # เวลาในการทำงาน
    'debug_toolbar.panels.cache.CachePanel',  # แคช
]

การตั้งค่าเช่นนี้จะเปิดใช้งานพาเนลที่มีประโยชน์ที่สุดสำหรับการปรับแต่ง

python
# บันทึกล็อกคิวรี SQL ระหว่างพัฒนา
# settings.py
LOGGING = {
    'version': 1,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'level': 'DEBUG',
            'handlers': ['console'],
        },
    },
}

การล็อกแบบนี้แสดงทุกคิวรี SQL ในคอนโซล ใช้ตรวจหาปัญหา N+1 ได้สะดวก

แคช QuerySet

สำหรับข้อมูลที่ถูกเรียกบ่อยและไม่เปลี่ยนแปลง การแคชช่วยหลีกเลี่ยงคิวรีซ้ำ

python
# views.py
from django.core.cache import cache
from django.views.decorators.cache import cache_page

def list_featured_books(request):
    """ดึงหนังสือเด่นโดยใช้แคช"""
    cache_key = 'featured_books_list'

    # พยายามดึงจากแคช
    books = cache.get(cache_key)

    if books is None:
        # Cache miss: รันคิวรี
        books = list(
            Book.objects.select_related('author')
            .filter(featured=True)
            .order_by('-published_date')[:10]
        )
        # เก็บไว้ในแคชนาน 5 นาที
        cache.set(cache_key, books, timeout=300)

    return render(request, 'books/featured.html', {'books': books})

# Decorator แคชทั้งวิว
@cache_page(60 * 15)  # แคช 15 นาที
def list_all_tags(request):
    """แสดงแท็กทั้งหมด (ข้อมูลที่เปลี่ยนน้อย)"""
    tags = Tag.objects.annotate(
        book_count=Count('books')
    ).order_by('-book_count')

    return render(request, 'tags/list.html', {'tags': tags})

แคชช่วยลดภาระของฐานข้อมูลสำหรับข้อมูลที่ค่อนข้างคงที่

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

บทสรุป

การปรับแต่งคิวรีใน Django ORM ตั้งอยู่บนหลักสำคัญเพียงไม่กี่ข้อ:

วัดผลก่อนปรับแต่ง: ใช้ django-debug-toolbar เพื่อระบุปัญหาที่แท้จริง

ขจัดปัญหา N+1: select_related สำหรับ ForeignKey, prefetch_related สำหรับ ManyToMany

จำกัดข้อมูล: only(), defer(), values() โหลดเฉพาะที่จำเป็น

ทำดัชนีอย่างชาญฉลาด: สร้างดัชนีบนคอลัมน์ที่กรองหรือเรียงบ่อย

แคชอย่างมีกลยุทธ์: ข้อมูลที่ไม่ค่อยเปลี่ยนได้ประโยชน์จากแคชของ Django

คิวรีดิบ: ใช้เป็นทางเลือกสุดท้ายสำหรับการปรับแต่งเฉพาะฐานข้อมูล

เมื่อใช้อย่างเป็นระบบ เทคนิคเหล่านี้จะเปลี่ยนแอปพลิเคชัน Django ที่ช้าให้กลายเป็นระบบที่มีประสิทธิภาพและรองรับข้อมูลปริมาณมากได้ Django ORM ยังคงเป็นเครื่องมือที่ทรงพลังเมื่อเข้าใจรายละเอียดต่าง ๆ ของมัน

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

#django
#django orm
#python
#ปรับแต่งฐานข้อมูล
#ประสิทธิภาพ

แชร์

บทความที่เกี่ยวข้อง