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

Django ORM มอบเลเยอร์การเข้าถึงฐานข้อมูลที่เรียบง่ายและสง่างาม แต่ความเรียบง่ายดังกล่าวอาจซ่อนปัญหาประสิทธิภาพที่สำคัญเอาไว้ แอปพลิเคชัน Django ที่ไม่ได้รับการปรับแต่งสามารถสร้างคิวรีนับร้อยรายการในที่ที่คิวรีเดียวก็เพียงพอ คู่มือนี้นำเสนอเทคนิคหลักสำหรับตรวจหาและแก้ไขปัญหาเหล่านั้น
ก่อนปรับแต่งจะต้องวัดผล การใช้ django-debug-toolbar ในขั้นพัฒนาช่วยให้เห็นทุกคิวรี SQL ที่สร้างขึ้นและระบุจุดคอขวดได้อย่างรวดเร็ว
เข้าใจปัญหา N+1
ปัญหา N+1 คือกับดักที่พบบ่อยที่สุดเวลาทำงานกับ ORM โดยจะเกิดขึ้นเมื่อคิวรีแรกดึงวัตถุ N รายการ จากนั้นจึงต้องมีคิวรีเพิ่มเติมอีก N ครั้งเพื่อเข้าถึงความสัมพันธ์ของแต่ละวัตถุ การทวีจำนวนคิวรีนี้ทำให้ประสิทธิภาพลดลงอย่างมีนัยสำคัญ
# 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 ได้อย่างเป็นรูปธรรม
# 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})โค้ดดูธรรมดาแต่กลับสร้างคิวรีหนึ่งครั้งต่อหนังสือหนึ่งเล่มเพื่อดึงผู้แต่งที่เกี่ยวข้อง
แก้ปัญหา N+1 ด้วย select_related
เมธอด select_related จะทำ SQL JOIN และดึงข้อมูลที่สัมพันธ์กันมาในคิวรีเดียว ใช้ได้กับความสัมพันธ์ ForeignKey และ OneToOneField
# 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 เพื่อดึงผู้แต่งมาพร้อมกับหนังสือ
# ต่อ 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 ด้วย prefetch_related
สำหรับความสัมพันธ์ ManyToMany หรือความสัมพันธ์ย้อนกลับ (ForeignKey จากอีกฝั่ง) prefetch_related จะรันคิวรีแยกที่ปรับแต่งไว้แล้ว เพื่อหลีกเลี่ยง JOIN ขนาดใหญ่
# 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 เพื่อให้การปรับแต่งมีประสิทธิภาพ
# 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 กับ ForeignKey และ OneToOne (SQL JOIN) ใช้ prefetch_related กับ ManyToMany และความสัมพันธ์ย้อนกลับ (คิวรีแยก) สามารถผสมทั้งสองได้บน QuerySet เดียวกัน
ปรับแต่งการพรีโหลดด้วยอ็อบเจ็กต์ Prefetch
อ็อบเจ็กต์ Prefetch ให้ความยืดหยุ่นในการควบคุมข้อมูลที่ถูกโหลดล่วงหน้า ทั้งการกรอง การเรียง และการจำกัดจำนวนผลลัพธ์
# 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 ตามปกติ
# การผสมขั้นสูง: 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 จะดึงทุกคอลัมน์ของตาราง เมื่อโมเดลมีฟิลด์จำนวนมากหรือมีฟิลด์ขนาดใหญ่ การจำกัดคอลัมน์จะช่วยเพิ่มประสิทธิภาพ
# 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" ที่โหลดเฉพาะคอลัมน์ที่ระบุ
# 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 ที่เบากว่า
# 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})เมธอดเหล่านี้หลีกเลี่ยงการสร้างอ็อบเจ็กต์โมเดลและช่วยลดการใช้หน่วยความจำ
# ผสมกับการ 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 อนุญาตให้นิยามดัชนีในโมเดลโดยตรง
# 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 ดิบให้การควบคุมที่สมบูรณ์
# 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 สร้างขึ้น
# 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', # แคช
]การตั้งค่าเช่นนี้จะเปิดใช้งานพาเนลที่มีประโยชน์ที่สุดสำหรับการปรับแต่ง
# บันทึกล็อกคิวรี SQL ระหว่างพัฒนา
# settings.py
LOGGING = {
'version': 1,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django.db.backends': {
'level': 'DEBUG',
'handlers': ['console'],
},
},
}การล็อกแบบนี้แสดงทุกคิวรี SQL ในคอนโซล ใช้ตรวจหาปัญหา N+1 ได้สะดวก
แคช QuerySet
สำหรับข้อมูลที่ถูกเรียกบ่อยและไม่เปลี่ยนแปลง การแคชช่วยหลีกเลี่ยงคิวรีซ้ำ
# 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 5.2 Custom Middleware และ Signal Handling: คู่มือเตรียมสัมภาษณ์เชิงเทคนิค
คู่มือเชิงลึก Django 5.2 custom middleware และ signal handling สำหรับการสัมภาษณ์เชิงเทคนิค ครอบคลุม request pipeline, async middleware, post_save, pre_save, custom signals และแนวทางปฏิบัติที่ดีสำหรับ production

คำถามสัมภาษณ์งาน Django และ Python: 25 คำถามยอดนิยมประจำปี 2026
25 คำถามสัมภาษณ์งาน Django และ Python ที่พบบ่อยที่สุด ORM, views, middleware, DRF, signals และการปรับแต่งประสิทธิภาพ พร้อมคำตอบละเอียดและตัวอย่างโค้ด

Django 5: สร้าง REST API ด้วย Django REST Framework
คู่มือฉบับสมบูรณ์สำหรับการสร้าง REST API ระดับมืออาชีพด้วย Django 5 และ DRF ครอบคลุม Serializers, ViewSets, การยืนยันตัวตน JWT และแนวทางปฏิบัติที่ดีที่สุด