# Django ORM: เพิ่มประสิทธิภาพคิวรีให้ทำงานเต็มขีดความสามารถ > คู่มือฉบับสมบูรณ์สำหรับเพิ่มประสิทธิภาพคิวรีของ Django ORM ทั้ง select_related, prefetch_related, ดัชนี การวิเคราะห์ปัญหา N+1 และเทคนิคขั้นสูงสำหรับแอปพลิเคชันที่ต้องการประสิทธิภาพสูง - Published: 2026-02-15 - Updated: 2026-04-28 - Author: SharpSkill - Tags: django, django orm, python, ปรับแต่งฐานข้อมูล, ประสิทธิภาพ - Reading time: 14 min --- 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}) ``` โค้ดดูธรรมดาแต่กลับสร้างคิวรีหนึ่งครั้งต่อหนังสือหนึ่งเล่มเพื่อดึงผู้แต่งที่เกี่ยวข้อง ## แก้ปัญหา N+1 ด้วย select_related เมธอด `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 ด้วย prefetch_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}) ``` แนวทางนี้ช่วยลดจำนวนคิวรีในโครงสร้างข้อมูลที่ซับซ้อนได้อย่างมาก ## จำกัดคอลัมน์ด้วย 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 ยังคงเป็นเครื่องมือที่ทรงพลังเมื่อเข้าใจรายละเอียดต่าง ๆ ของมัน --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/django/django-orm-optimizing-queries