Optimize database query ด้วย :include
August 7th, 2009 Posted in My Project, Programming | No Comments »ในการเขียนโปรแกรมบน Ruby on Rails นั้น เรามักที่จะใช้ ActiveRecord ในการทำหน้าที่เป็น ORM ระหว่างตัว Application กับ database ซื่งทำให้การเรียก Record นั้น สามารถทำได้อย่างง่ายดาย เช่น ถ้าผมจะเรียกดู post ทั้งหมดที่มีอยู่ในระบบ ผมแค่สั่ง
Post.find(:all) # หรือว่า Post.all ก็ได้ ใน Rails 2.x
ซึ่งตรงนี้ ถ้าเราไปดูใน Log file จะพบว่า ActiveRecord นั้น จะใช้คำสั่งค้นหาข้อมูลประมาณนี้ครับ
Post Load (0.1ms) SELECT * FROM "posts"
(ผมใช่ sqlite3 เพราะฉะนั้น table name/field name จะถูกใส่ไว้ใน quote ครับ)
ถ้าสมมุติในโปรแกรมนั้น เราได้ทำ Association ระหว่าง Post และ Comment (Post has many comments) และระหว่าง Comment กับ User (comment belongs to user)
class Post < ActiveRecord::Base has_many :comments # ... end class Comment < ActiveRecord::Base belongs_to :post belongs_to :user # ... end
ถ้าเราต้องการจะแสดงผล comment แต่ละอันด้วย เราก็สามารถทำได้โดยเรียกเมธอด #comments ที่ถูกสร้างขึ้นมาอัตโนมัติโดยการทำ association และเช่นเดียวกัน ถ้าเราต้องการแสดงด้วยว่า comment นั้นถูกเขียนโดยใคร เราก็สามารถเรียกเมธอด #user บน comment เช่นกัน
<% Post.find(:all).each do |post| %> <!-- display post --> <% post.comments.each do |comment| %> By: <%= comment.user.username %> <!-- display comments --> <% end %> <% end %>
คราวนี้ สมมุติว่าบล็อกเรามีทั้งหมด 10 Post แล้วแต่ละอันมี 5 comment … SQL ที่ออกมานั้น จะเป็นประมาณนี้ครับ
Post Load (0.1ms) SELECT * FROM "posts"
Comment Load (0.5ms) SELECT * FROM "comments" WHERE ("comments".
"post_id" = '1')
User Load (0.2ms) SELECT * FROM "users" WHERE ("user"."id" = '5')
User Load (0.2ms) SELECT * FROM "users" WHERE ("user"."id" = '24')
User Load (0.2ms) SELECT * FROM "users" WHERE ("user"."id" = '30')
User Load (0.2ms) SELECT * FROM "users" WHERE ("user"."id" = '4')
CACHE (0.0ms) SELECT * FROM "users" WHERE ("user"."id" = '5')
Comment Load (0.3ms) SELECT * FROM "comments" WHERE ("comments".
"post_id" = '2')
User Load (0.2ms) SELECT * FROM "users" WHERE ("user"."id" = '38')
User Load (0.2ms) SELECT * FROM "users" WHERE ("user"."id" = '14')
User Load (0.2ms) SELECT * FROM "users" WHERE ("user"."id" = '40')
User Load (0.2ms) SELECT * FROM "users" WHERE ("user"."id" = '2')
User Load (0.2ms) SELECT * FROM "users" WHERE ("user"."id" = '9')
...จะเห็นได้ว่า ในสถานการณ์ที่แย่ที่สุดนั้น (user ที่มา comment แต่ละ post นั้น ไม่ตรงกันเลย เป็นต้น) ActiveRecord จำเป็นต้องทำการ Query ทั้งหมด 1 + 10 + (10 * 5) = 61 ครั้ง ซึ่งไม่มีประสิทธิภาพเลยครับ เพราะเปลือง Query มากมาย
ดังนั้น เพื่อให้ Query ทั้งหมดนี่มีประสิทธิภาพมากขึ้น ActiveRecord จึงมี key หนึ่งชื่อว่า :include เอาไว้สำหรับสั่งว่าให้ ActiveRecord นั้นทำการโหลด Model ที่ associates กับ object นี้ขึ้นมาด้วยพร้อมๆ กันเลย เพื่อประหยัด Query ครับ เพราะฉะนั้นโค้ดในการค้นหาของเราจะเปลี่ยนเป็น
Post.find(:all, :include => {:comments => :user}).each do |post|
# ... display post
post.comments.each do |comment|
By: <%= comment.user.username %>
# ... display comments
end
endแล้วผลของมันน่ะหรอครับ? 61 query -> 3 queries ครับ!
Post Load (0.1ms) SELECT * FROM "posts"
Comment Load (0.3ms) SELECT * FROM "comments" WHERE ("comments".
"post_id" IN (1, 2, 3, 4, 5))
User Load (1.3ms) SELECT * FROM "users" WHERE ("user"."id" IN (5, 24,
30, 4, 2, 38, 14, 40, 2, 9, 23, 41, 48, 50, 32, 10, 48)เพราะฉะนั้นการใช้ :include นั้น เป็นการ optimize query อย่างได้ผลทีเดียวละครับ โดยจะเห็นได้ว่าเรายังสามารถโหลด model แบบ nested ได้โดยการใช้ Hash และโหลดโมเดลหลายๆ อันพร้อมกันโดยใช้ Array ครับ อย่างเช่นถ้าเราต้องการโหลด Attachments จาก Comment และโหลด Tags จาก Post ด้วย เราก็สามารถใช้คำสั่งอย่างนี้ได้ครับ
Post.find(:all, :include => [{:comments => [:user, :attachments]}, :tags])
คำเตือน: Use it, but don’t abuse it!
ในบางครั้ง การใช้ :include นั้น อาจจะทำให้เวลาในการ query นั้นลดลงได้ถ้าเทียบกับการ query object เล็กๆ หลายๆ ครั้งแทน เพราะฉะนั้นมันไม่ใช่สิ่งที่เวิร์คที่สุดครับ ต้องปรับใช้ให้เข้ากับงานซะมากกว่า โดยที่ผมแนะนำให้ใช้ #find method ตามปกติก่อน แล้วจึงค่อยเพิ่ม :include เข้าไปถ้าเราเห็นว่ามีการ query record จำนวนมากๆครับ … ถือซะว่าการใช้ :include นั้นเป็นการ refactor code ครับ และไม่ใช่สิ่งที่ต้องมาคิดตั้งแต่แรกว่าตรงนี้ต้องใช้มันหรือไม่












