บทเรียน 113 · Rails · Day 2

Query — ใบเบิกของที่ยังไม่ยื่น จนกว่าจะใช้ข้อมูลจริง

~20–25 นาที (ถ้าลองใน console ตามทุกก้อน) · ควรผ่าน บทเรียน 106 (Active Record · SQL ภาษาคน) และ บทเรียน 107 (ลอง where · first ใน console) มาก่อน

เรื่องที่ทำให้มือใหม่งงที่สุดเรื่องหนึ่งของ Rails: พิมพ์ Post.where(...) แล้ว Rails ยังไม่ได้ไปเอาข้อมูลจากฐานข้อมูลเลย
อ่านจบบทนี้ คุณจะบอกได้ว่าบรรทัดไหน "ไปตู้จริง" บรรทัดไหนยังไม่ไป · เปิดดู SQL ที่แอปยิงจริงจาก log ได้ · และเลือกใช้ count · size · length · exists? · pluck · sum ได้ถูกตัว โดยให้ฐานข้อมูลทำงานให้ แทนที่จะขนข้อมูลมาทั้งหมด

🔁 ทวนก่อนเริ่ม — คำจากบทก่อน ๆ ที่บทนี้ใช้

🧰 ตรวจด้วย Ruby 4.0.9 · Rails 8.1.4 · PostgreSQL 17 (สไลด์ใช้ Ruby 4.0.6 · Rails 8.1.3 — รุ่นแก้บั๊กในสายเดียวกัน ใช้แทนกันได้)

🗂 ตัวอย่างในบทนี้ใช้ตาราง users / posts / comments และข้อมูลตัวอย่าง (seeds) ชุดเดียวกับที่สร้างไว้ใน บท 107 — บทความ 6 ชิ้น (published 4 · draft 2) ของสมชาย สมหญิง และมานะ

ผลทุกก้อนรันจริงแล้วก็อปมา · เวลา (ms) · วันที่ · เลข 0x… ของคุณจะไม่ตรงกับในบท และจอคุณจะมีสี · เลข 001 002 ใน prompt ของ console นับตามบรรทัดที่พิมพ์ในรอบนั้น บางก้อนในบทรันแยกรอบ เลขเลยเริ่มใหม่ — ไม่ต้องสนใจ

ทวน SQL ภาษาคน — คำที่จะเห็นทั้งบท

ทั้งบทนี้เราจะอ่าน SQL ที่ Rails ยิงออกไป · ไม่ต้องเขียน SQL เป็น แค่อ่านออกว่ามันขออะไรจากตู้ก็พอ
นี่คือคำทั้งหมดที่จะเจอ (ทวนจากบท 106 และเพิ่มอีกนิด):

คำใน SQLแปลเป็นภาษาคน
SELECT "posts".*ขอทุกช่องของแฟ้ม (* = ทุกช่อง)
SELECT "posts"."title"ขอแค่ช่อง title ช่องเดียว
FROM "posts"จากลิ้นชัก posts
WHERE …เฉพาะแผ่นที่ตรงเงื่อนไขนี้
… AND …"และ" — ต้องตรงทั้งสองเงื่อนไข
ORDER BY … DESC / ASCเรียงตามช่องนี้ · DESC = มากไปน้อย · ASC = น้อยไปมาก
LIMIT 10เอามาไม่เกิน 10 แผ่น
COUNT(*)ไม่ต้องหยิบแฟ้มมา แค่นับให้หน่อยว่ามีกี่แผ่น
SUM(…) · AVG(…)บวกตัวเลขในช่องนี้ของทุกแผ่น · หาค่าเฉลี่ย
GROUP BY …แยกกองตามช่องนี้ก่อน แล้วค่อยนับ/บวกทีละกอง
… AS "ชื่อ"ตั้งชื่อเล่นให้ผลลัพธ์ช่องนั้น (Rails ใส่ไว้ใช้เอง ข้ามได้)
UPDATE … SET …แก้ค่าในช่องนี้ ของทุกแผ่นที่ตรงเงื่อนไข

ลองแปลหนึ่งประโยคเต็ม ๆ:

SELECT "posts".* FROM "posts" WHERE "posts"."published" = TRUE ORDER BY "posts"."views" DESC LIMIT 10

= "ขอแฟ้มทุกช่อง จากลิ้นชัก posts · เฉพาะแผ่นที่ช่อง published เป็นจริง · เรียงตามยอดวิวมากไปน้อย · เอามาไม่เกิน 10 แผ่น"
เครื่องหมาย "…" รอบชื่อเป็นแค่วิธีเขียนชื่อของ PostgreSQL · "posts"."views" อ่านว่า "ช่อง views ของลิ้นชัก posts"

Post.where(...) ไม่ใช่ข้อมูล — มันคือใบเบิกของ

ตอนพิมพ์ Post.where(published: true) หลายคนคิดว่า "Rails ไปเอาบทความที่ published มาให้แล้ว" · ความจริงคือยังไม่ได้ไปเลย
สิ่งที่ where คืนมาคือคำขอที่จดไว้ว่า "อยากได้อะไร" · Rails จะไปตู้จริงก็ต่อเมื่อเราเริ่มใช้ข้อมูล เช่น วนลูปโชว์ หรือนับจำนวน

ประโยชน์ของการออกแบบแบบนี้: เราต่อเงื่อนไขเพิ่มได้ทีละบรรทัด (where แล้ว order แล้ว limit) · Rails รวมทุกอย่างเป็น SQL ประโยคเดียว แล้วไปตู้ครั้งเดียวตอนท้าย · ไม่ใช่ไปตู้ทุกบรรทัด

📝 เทียบให้เห็นภาพ — ใบเบิกของที่ยังเขียนไม่เสร็จ

Post.where(published: true) เหมือนหยิบใบเบิกของมาหนึ่งใบ แล้วเขียนว่า "เอาแฟ้มที่ published แล้ว" · .order(views: :desc) = เขียนต่อว่า "เรียงตามยอดวิว" · .limit(10) = "ขอแค่ 10 แผ่น"
ตลอดเวลาที่ยังเขียนอยู่ ไม่มีใครเดินไปตู้เลย · คนดูแลสต็อกจะเดินไปก็ต่อเมื่อเรา "ยื่นใบ" — คือตอนที่เราต้องการของจริง เช่น จะเอาแฟ้มมาวางเรียงบนจาน (each)

ที่ไม่เหมือนกัน: ใบเบิกของจริงยื่นแล้วก็หมดหน้าที่ · แต่ใบเบิกของ Rails ยื่นซ้ำได้ และบางคำสั่ง (เช่น count) จะเดินไปตู้ใหม่ทุกครั้งที่เรียก แม้เคยยื่นไปแล้ว — เดี๋ยวเห็นจริงในหัวข้อ count vs size

เทียบกับ Go (GORM): GORM ก็ต่อเงื่อนไขทีละท่อนได้เหมือนกัน แต่ต้องปิดท้ายด้วยคำสั่งที่สั่งให้ไปตู้เอง เช่น Find · ตามเอกสาร GORM เรียกคำสั่งพวกนี้ว่า Finisher Methods และบอกว่ามันยิง SQL ทันที:

// Go + GORM — บรรทัดนี้ไปตู้ทันที เพราะลงท้ายด้วย Find
var posts []Post
db.Where("published = ?", true).Order("views desc").Find(&posts)
# Ruby + Rails — บรรทัดนี้ยังไม่ไปตู้ ไม่มีคำว่า Find ท้าย
posts = Post.where(published: true).order(views: :desc)
🗣 ศัพท์ที่เพิ่งเรียนไป
ActiveRecord::Relation (relation)
ชนิดของสิ่งที่ where · order · limit คืนมา = "ใบเบิกของ" ที่ยังไม่ยื่น · ใน console จะเห็นชื่อเต็มว่า Post::ActiveRecord_Relation · พิมพ์บอก AI ได้ว่า “this returns an ActiveRecord::Relation, when does it actually hit the database?”
lazy loading · lazy evaluation
"ขี้เกียจไว้ก่อน" — ยังไม่ทำงานจริงจนกว่าจะต้องใช้ผล · คำว่า hit the database = ไปตู้จริง · ถาม senior ได้ว่า “relation นี้ถูก load ตรงไหน ใน controller หรือใน view”
query chaining · finisher
การต่อคำสั่ง .where.order.limit เป็นสาย · ฝั่ง GORM คำสั่งปิดท้ายที่ไปตู้ (Find · First) เรียกว่า finisher · พิมพ์บอก AI ได้ว่า “in GORM I call Find to execute, what triggers execution in ActiveRecord?”

ลองใน console — แล้วเจอกับดักทันที

มาพิสูจน์ด้วยตาตัวเอง · เปิด bin/rails console แล้วพิมพ์ทีละบรรทัดตามสไลด์หน้า 31
ถ้า where ไม่ไปตู้จริง เราไม่ควรเห็น SQL โผล่เลยสักบรรทัด… ผลจริง:

wongnok-blog(dev):001> q = Post.where(published: true)
  Post Load (1.7ms)  SELECT "posts".* FROM "posts" WHERE "posts"."published" = TRUE /* loading for pp */ LIMIT 11 /*application='WongnokBlog'*/
=>
[#<Post:0x0000000127a4e458
...
wongnok-blog(dev):002> q = q.where("views > ?", 100)
  Post Load (2.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."published" = TRUE AND (views > 100) /* loading for pp */ LIMIT 11 /*application='WongnokBlog'*/
=>
[#<Post:0x000000012657d5d8
...

เห็น SQL ทุกบรรทัด! ขัดกับที่เพิ่งพูดไปเลย · แต่สังเกตคำแปลก ๆ ใน SQL: /* loading for pp */ กับ LIMIT 11
ทั้งสองอย่างนี้ console ใส่เอง ไม่ใช่ของ where:

ในแอปจริง (controller · view) ไม่มีนิสัยนี้ — ไม่มีใครมาโชว์ผลทุกบรรทัด · กับดักนี้มีแค่ใน console
วิธีแก้: ต่อท้ายบรรทัดด้วย ; nil · ; คือ "จบคำสั่งแรก ขึ้นคำสั่งใหม่ในบรรทัดเดียวกัน" · คำสั่งสุดท้ายเป็น nil console เลยโชว์แค่ nil ไม่ต้องยื่นใบเบิกของ

🙋 เทียบให้เห็นภาพ — ผู้ช่วยขี้สงสัยในครัวทดลอง

ครัวทดลอง (console) มีผู้ช่วยคนหนึ่งที่อยากให้คุณเห็นผลทุกอย่างที่เขียน · คุณเพิ่งเขียนใบเบิกของบรรทัดแรก เขาก็วิ่งไปตู้ หยิบแฟ้มมาโชว์ 11 แผ่นทันที · เขียนต่ออีกบรรทัด ก็วิ่งไปอีกรอบ
; nil = บอกผู้ช่วยว่า "ไม่ต้องโชว์ ฉันยังเขียนไม่เสร็จ"

ที่ไม่เหมือนกัน: ผู้ช่วยจริงถ้าไปตู้มาแล้วก็คงวางแฟ้มไว้ให้เราใช้ต่อ · แต่ผู้ช่วยใน console ถ่ายสำเนาใบของเราไปเบิกเอง แล้วทิ้งแฟ้มที่ได้หลังโชว์เสร็จ — ใบตัวจริง (q) ในมือเรายังนับว่า "ไม่เคยยื่น" · พอเราเอาไปใช้จริง Rails ก็ต้องไปตู้อีกรอบอยู่ดี

เทียบกับ Go: Go ไม่มีครัวทดลองแบบนี้ (ต้อง go run ทั้งไฟล์) จึงไม่มีใครมาแอบโชว์ผลให้ · กับดักนี้เป็นของ console ของ Rails ล้วน ๆ

ลองใหม่ด้วย ; nil ทุกบรรทัด:

q = Post.where(published: true); nil
q = q.where("views > ?", 100); nil
q = q.order(created_at: :desc); nil
q = q.limit(10); nil

พิมพ์ 4 บรรทัดนี้ จะเห็นบรรทัด Post Load (…) SELECT … โผล่กี่ครั้ง?

ผลจริง — ไม่โผล่เลยสักครั้ง:

wongnok-blog(dev):001> q = Post.where(published: true); nil
=> nil
wongnok-blog(dev):002> q = q.where("views > ?", 100); nil
=> nil
wongnok-blog(dev):003> q = q.order(created_at: :desc); nil
=> nil
wongnok-blog(dev):004> q = q.limit(10); nil
=> nil

สี่บรรทัดนี้แค่เขียนเพิ่มลงใบเบิกของ · ยังไม่มีใครไปตู้ · นี่คือพฤติกรรมเดียวกับในแอปจริง

ใบเบิกของยังอยู่ในมือ ถามมันได้ว่าเป็นอะไร และเขียนว่าอะไรไว้บ้าง (ผลจริง ต่อจากเมื่อกี้):

wongnok-blog(dev):005> q.class
=> Post::ActiveRecord_Relation
wongnok-blog(dev):006> q.to_sql
=> "SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"published\" = TRUE AND (views > 100) ORDER BY \"posts\".\"created_at\" DESC LIMIT 10"

ทีนี้ "ยื่นใบ" จริงด้วย each (วนทีละแผ่น บท 93) แล้วลองยื่นซ้ำอีกครั้ง — ผลจริง ต่อจากเมื่อกี้:

wongnok-blog(dev):007> q.loaded?
=> nil
wongnok-blog(dev):008> q.each { |post| puts post.title }; nil
  Post Load (4.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."published" = TRUE AND (views > 100) ORDER BY "posts"."created_at" DESC LIMIT 10 /*application='WongnokBlog'*/
ผัดกะเพราหมูสับ
ส้มตำปูปลาร้า
ต้มยำกุ้งน้ำข้น
=> nil
wongnok-blog(dev):009> q.loaded?
=> true
wongnok-blog(dev):010> q.each { |post| puts post.title }; nil
ผัดกะเพราหมูสับ
ส้มตำปูปลาร้า
ต้มยำกุ้งน้ำข้น
=> nil
🗣 ศัพท์ที่เพิ่งเรียนไป
inspect · pp (pretty print)
สิ่งที่ console ทำกับผลของทุกบรรทัดเพื่อโชว์หลัง => · กับ relation มันทำให้ยิง SQL (loading for pp) · พิมพ์บอก AI ได้ว่า “in rails console, why does a where clause run SQL immediately? I see /* loading for pp */”
to_sql · loaded?
to_sql = ดู SQL ที่ relation จะยิง โดยไม่ยิง · loaded? = relation นี้ไปเอาข้อมูลมาแล้วหรือยัง · ถาม senior ได้ว่า “ก่อนส่ง relation นี้ไป view ผมเช็ค to_sql แล้ว SQL หน้าตาแบบนี้ ถูกไหม”

⚠️ พิมพ์ชื่อคอลัมน์ผิดใน where จะยังไม่ error ตอนเขียน — จะ error ตอนยื่นใบ ซึ่งอาจอยู่ไกลออกไปหลายบรรทัด (ผลจริง: สะกด publish ขาด ed):

wongnok-blog(dev):001> bad = Post.where(publish: true); nil
=> nil
wongnok-blog(dev):002> bad.class
=> Post::ActiveRecord_Relation
wongnok-blog(dev):003> bad.size
  Post Count (2.3ms)  SELECT COUNT(*) FROM "posts" WHERE "posts"."publish" = TRUE /*application='WongnokBlog'*/
(wongnok-blog):3:in '<main>': PG::UndefinedColumn: ERROR:  column posts.publish does not exist (ActiveRecord::StatementInvalid)
…
HINT:  Perhaps you meant to reference the column "posts.published".

error ชี้ :3 (บรรทัดที่ยื่นใบ) ไม่ใช่บรรทัด 1 ที่เขียนผิด · ในแอปจริงอาจชี้ไปที่ไฟล์ view ทั้งที่ผิดใน controller · ActiveRecord::StatementInvalid = ชื่อ error ของ Rails แปลว่า "ตู้ไม่รับ SQL ประโยคนี้" · HINT ท้ายสุดคือ PostgreSQL เดาให้ว่าน่าจะหมายถึง published

ยื่นใบเมื่อไหร่ — คำสั่งไหนไปตู้จริง

ตารางนี้สรุปจากผลรันจริงใน console ทุกบรรทัด (เริ่มจาก Post.where(published: true) ที่ยังไม่ยื่น) · ไม่ต้องจำทั้งหมด จำแค่หลัก: อะไรที่ต้องรู้ "ของจริง" = ไปตู้

พิมพ์ไปตู้ไหมSQL ที่เห็นจริง (ภาษาคน)
.where · .order · .limit❌ ไม่ไปแค่เขียนเพิ่มลงใบ
.class · .to_sql❌ ไม่ไปถามเรื่องตัวใบ ไม่ได้ถามเรื่องแฟ้ม
.each { … } · .to_a✅ ไปSELECT "posts".* … = หยิบทุกแผ่นที่ตรงมาวางบนโต๊ะ · to_a แปลงเป็น Array ธรรมดา
.first✅ ไป (ถ้ายังไม่โหลด)… ORDER BY "posts"."id" ASC LIMIT 1 = เรียงตามเลขที่แล้วหยิบแผ่นแรกแผ่นเดียว · ถ้าโหลดแล้วไม่ไปซ้ำ
.count✅ ไป ทุกครั้งSELECT COUNT(*) … = ให้ตู้นับ · เรียกสองครั้งก็ไปสองครั้ง
.any? · .empty? · .exists?✅ ไปSELECT 1 AS one … LIMIT 1 = "มีสักแผ่นไหม" เจอแผ่นแรกก็หยุด
<% @posts.each do |post| %> ใน view✅ ไปเหมือน each ข้างบน · เดี๋ยวเห็นใน log หัวข้อถัดไป

ใน controller เขียนบรรทัดเดียว @posts = Post.where(published: true).order(:views) · ตอนบรรทัดนี้ทำงานเสร็จ Rails ยิง SQL ไปแล้วกี่ครั้ง?

ตัวเลือกที่ 1 ถูก — where กับ order แค่เขียนเพิ่มลงใบเบิกของ · SQL จะโผล่ตอนที่ view เอา @posts ไปวน each

ตัวเลือกที่ 2 ผิดครึ่งเดียว — ถูกที่ "รวมเป็นประโยคเดียว" แต่ประโยคนั้นยังไม่ถูกส่งตอนนี้ · ตัวเลือกที่ 3 ผิด เพราะ Rails ไม่เคยยิงแยกทีละ method · ตัวเลือกที่ 4 ผิด เพราะจำนวนแถวไม่เกี่ยวกับว่าจะยิงหรือไม่ยิง

จุดที่ต้องจำ: where · order · limit = เขียนใบ · each · count · first = ยื่นใบ

เห็น SQL ที่แอปยิงจริง — สมุดบันทึกการเบิกของ

console ดีไว้ลองทีละบรรทัด · แต่เวลาแอปช้า เราอยากรู้ว่าหน้าเว็บจริงหนึ่งหน้าไปตู้กี่รอบ ด้วย SQL อะไร
คำตอบอยู่ในไฟล์ log/development.log · Rails จดทุกอย่างที่เห็นในเทอร์มินัลของ server ลงไฟล์นี้ด้วย รวมถึง SQL ทุกประโยค

📒 เทียบให้เห็นภาพ — สมุดบันทึกการเบิกของ

ทุกครั้งที่คนดูแลสต็อกเดินไปตู้ เขาจดไว้หนึ่งบรรทัด: "เวลาเท่าไหร่ · เบิกอะไร · ใช้เวลากี่วินาที · ใครสั่งให้มาเบิก" · อยากรู้ว่าจานนี้ทำไมช้า ก็เปิดสมุดนับดูว่าเดินไปตู้กี่รอบ

ที่ไม่เหมือนกัน: สมุดจริงเปิดอ่านย้อนหลังสบาย ๆ · แต่ development.log ยาวขึ้นเรื่อย ๆ ไม่มีวันหยุด (ทุกใบสั่งต่อท้ายไฟล์เดิม) · จึงนิยมเปิดแบบ "ดูสด" ด้วย tail -f แทนการเปิดไฟล์ทั้งไฟล์

เทียบกับ Go: GORM ค่าเริ่มต้นจดแค่ SQL ที่ช้าหรือที่ error · อยากเห็นทุกประโยคต้องเรียก db.Debug() หรือตั้ง logger ของมันเอง · Rails เปิดสมุดนี้ไว้ให้ตั้งแต่ rails new ในโหมด development

มาทำหน้า /posts จิ๋ว ๆ ให้มีของให้ดู — 3 ไฟล์ (ถ้ามี PostsController จาก Day 1 อยู่แล้ว แก้ action index ให้เป็นแบบนี้):

# config/routes.rb — เพิ่มบรรทัดนี้ใน block (บท 101) · ถ้ามี resources :posts อยู่แล้ว ไม่ต้องเพิ่ม
resources :posts, only: :index
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts = Post.where(published: true).order(views: :desc)
  end
end
<%# app/views/posts/index.html.erb %>
<h1>บทความ <%= @posts.count %> ชิ้น</h1>
<ul>
  <% @posts.each do |post| %>
    <li><%= post.title %> (<%= post.views %> views)</li>
  <% end %>
</ul>

เปิดเทอร์มินัลสองหน้าต่าง · หน้าต่างแรกเปิด server · หน้าต่างที่สองเปิดสมุดแบบดูสด:

# หน้าต่างที่ 1
bin/rails server

# หน้าต่างที่ 2 — อยู่ในโฟลเดอร์แอปเหมือนกัน
tail -f log/development.log

tail = โชว์ท้ายไฟล์ · -f = "follow" มีบรรทัดใหม่ต่อท้ายเมื่อไหร่ โชว์ทันที (กด Ctrl-C เพื่อเลิกดู) · จากนั้นเปิด http://localhost:3000/posts ในเบราว์เซอร์ หน้าต่างที่ 2 จะขึ้นแบบนี้ (ผลจริง · ผมยิงด้วย curl):

Started GET "/posts" for ::1 at 2026-11-04 18:49:47 +0700
Processing by PostsController#index as */*
  Rendering layout layouts/application.html.erb
  Rendering posts/index.html.erb within layouts/application
  Post Count (1.1ms)  SELECT COUNT(*) FROM "posts" WHERE "posts"."published" = TRUE /*action='index',application='WongnokBlog',controller='posts'*/
  ↳ app/views/posts/index.html.erb:3
  Post Load (0.8ms)  SELECT "posts".* FROM "posts" WHERE "posts"."published" = TRUE ORDER BY "posts"."views" DESC /*action='index',application='WongnokBlog',controller='posts'*/
  ↳ app/views/posts/index.html.erb:5
  Rendered posts/index.html.erb within layouts/application (Duration: 19.0ms | GC: 9.5ms)
  Rendered layout layouts/application.html.erb (Duration: 26.4ms | GC: 10.8ms)
Completed 200 OK in 80ms (Views: 21.6ms | ActiveRecord: 20.7ms (2 queries, 0 cached) | GC: 10.8ms)

อ่านทีละส่วน:

⚠️ เลขบรรทัดของไฟล์ .erb ใน log เกินจริง 1 — log บอก :3 กับ :5 แต่ในไฟล์ count อยู่บรรทัด 2 · each อยู่บรรทัด 4 · เปิดไฟล์ view แล้วดูบรรทัดที่ log บอก ลบหนึ่ง (ไฟล์ .rb ไม่โดน)

สาเหตุ: แอปใหม่เปิดค่าตั้งที่แอบแปะโน้ตชื่อไฟล์ไว้บนสุดของ view ทุกไฟล์ (annotate_rendered_view_with_filenames) · ผมลองปิดแล้วเลขกลับมาเป็น :2 กับ :4 · ไม่ต้องไปปิดเอง

บรรทัด ↳ กับป้าย /*action=…*/ มาจากค่าตั้งสองตัว (verbose_query_logs · query_log_tags_enabled) ที่ Rails 8.1 เปิดไว้ให้แล้ว ในไฟล์ config/environments/development.rb ของแอปที่สร้างใหม่ · ไม่ต้องแก้อะไร

อีกทางที่สไลด์แนะนำ (แค่รู้จัก): gem rack-mini-profiler ขึ้นกล่องเล็ก ๆ มุมจอทุกหน้า บอกว่าหน้านี้ไปตู้กี่รอบ · ไม่ได้มากับแอป และบทนี้ไม่ได้ติดตั้ง · ถ้าทีมใช้ จะเห็นตัวเลขเดียวกับบรรทัด (2 queries) ใน log

🗣 ศัพท์ที่เพิ่งเรียนไป
development.log · tail -f
ไฟล์ที่ Rails จดทุกใบสั่งและทุก SQL ในโหมด development · tail -f = เปิดดูบรรทัดใหม่แบบสด ๆ · พิมพ์บอก AI ได้ว่า “here is my development.log for one request to /posts, how many queries does it run?”
verbose query logs · query log tags
บรรทัด ↳ ไฟล์:บรรทัด ใต้ SQL = verbose query logs · ป้าย /*action='index'…*/ ท้าย SQL = query log tags · ถาม senior ได้ว่า “บน production เราเปิด query log tags ไว้ไหม จะได้รู้ว่า SQL ช้าตัวนี้มาจาก controller ไหน”

count · size · length — นับสามแบบ ไปตู้ไม่เท่ากัน

ทั้งสามคำตอบ "มีกี่แผ่น" เหมือนกัน แต่วิธีหาคำตอบต่างกัน · สไลด์หน้า 35 ยกเรื่องนี้มาเป็น "คำถามสัมภาษณ์ยอดนิยม" — ลองใน console ทีละบรรทัดกับใบเบิกของที่ยังไม่ยื่น (ผลจริง):

wongnok-blog(dev):001> posts = Post.where(published: true); nil
=> nil
wongnok-blog(dev):002> posts.size
  Post Count (2.6ms)  SELECT COUNT(*) FROM "posts" WHERE "posts"."published" = TRUE /*application='WongnokBlog'*/
=> 4
wongnok-blog(dev):003> posts.length
  Post Load (1.5ms)  SELECT "posts".* FROM "posts" WHERE "posts"."published" = TRUE /*application='WongnokBlog'*/
=> 4
wongnok-blog(dev):004> posts.size
=> 4
wongnok-blog(dev):005> posts.count
  Post Count (1.6ms)  SELECT COUNT(*) FROM "posts" WHERE "posts"."published" = TRUE /*application='WongnokBlog'*/
=> 4
📞 เทียบให้เห็นภาพ — นับแฟ้มสามวิธี

count = โทรถามคนดูแลสต็อกว่า "มีกี่แผ่น" เขานับในตู้แล้วบอกตัวเลขกลับมา · โทรทุกครั้งที่อยากรู้ แม้แฟ้มกองอยู่บนโต๊ะตรงหน้า
length = ขนแฟ้มทั้งหมดมาวางบนโต๊ะแล้วนับเอง · เปลืองแรงถ้าแค่อยากรู้ตัวเลข แต่ถ้ายังไงก็จะใช้แฟ้มพวกนั้นอยู่แล้วก็คุ้ม
size = คนฉลาด: ดูก่อนว่าบนโต๊ะมีแฟ้มกองอยู่ไหม · มี → นับบนโต๊ะ · ไม่มี → โทรถาม

ที่ไม่เหมือนกัน: คนจริงเห็นว่าแฟ้มบนโต๊ะเก่าแล้วก็โทรถามใหม่ได้ · แต่ size กับ length ไม่รู้ว่ามีคนเพิ่มแฟ้มเข้าตู้ไปหลังจากที่ขนมาแล้ว — นับจากกองบนโต๊ะเสมอ · อยากได้ตัวเลขล่าสุดจริง ๆ ต้อง count

เทียบกับ Go: count ≈ db.Model(&Post{}).Where(…).Count(&n) ให้ DB นับ · length ≈ db.Where(…).Find(&posts) แล้ว len(posts) · ฝั่ง Go ไม่มี size ที่เลือกให้เอง ต้องตัดสินใจเองทุกครั้ง

ทีนี้มาดูในหน้าเว็บจริง · แก้ view ให้โชว์จำนวนทั้งบนและล่างของรายการ:

<%# app/views/posts/index.html.erb %>
<h1>บทความ <%= @posts.count %> ชิ้น</h1>
<ul>
  <% @posts.each do |post| %>
    <li><%= post.title %> (<%= post.views %> views)</li>
  <% end %>
</ul>
<p>รวม <%= @posts.count %> ชิ้น</p>

หน้านี้ไปตู้กี่รอบ? แล้วถ้าเปลี่ยน count ทั้งสองที่เป็น size · หรือเป็น length ล่ะ?

ผลจริงจาก log ทีละแบบ (ตัดมาเฉพาะบรรทัด SQL กับบรรทัดสรุป · เลขบรรทัด view เกินจริง 1 ตามที่เตือนไว้):

# แบบ count ทั้งสองที่
  Post Count (1.6ms)  SELECT COUNT(*) FROM "posts" WHERE "posts"."published" = TRUE /*…*/
  ↳ app/views/posts/index.html.erb:3
  Post Load (1.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."published" = TRUE ORDER BY "posts"."views" DESC /*…*/
  ↳ app/views/posts/index.html.erb:5
  CACHE Post Count (0.0ms)  SELECT COUNT(*) FROM "posts" WHERE "posts"."published" = TRUE
  ↳ app/views/posts/index.html.erb:9
Completed 200 OK in 15ms (Views: 4.2ms | ActiveRecord: 8.8ms (3 queries, 1 cached) | GC: 0.0ms)

# แบบ size ทั้งสองที่
  Post Count (1.5ms)  SELECT COUNT(*) FROM "posts" WHERE "posts"."published" = TRUE /*…*/
  ↳ app/views/posts/index.html.erb:3
  Post Load (1.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."published" = TRUE ORDER BY "posts"."views" DESC /*…*/
  ↳ app/views/posts/index.html.erb:5
Completed 200 OK in 15ms (Views: 4.3ms | ActiveRecord: 8.1ms (2 queries, 0 cached) | GC: 0.0ms)

# แบบ length ทั้งสองที่
  Post Load (2.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."published" = TRUE ORDER BY "posts"."views" DESC /*…*/
  ↳ app/views/posts/index.html.erb:3
Completed 200 OK in 17ms (Views: 3.9ms | ActiveRecord: 11.4ms (1 query, 0 cached) | GC: 0.0ms)
  • count = 3 รอบ · ตัวที่สาม CACHE Post Count (0.0ms) = Rails จำได้ว่าใบสั่งนี้เพิ่งถามคำถามเดียวกันไป เลยตอบจากความจำ ไม่ได้ไปตู้จริง (ความจำนี้อยู่แค่ภายในใบสั่งเดียว ใบสั่งถัดไปเริ่มใหม่) · แต่ count ก็ยังพยายามไปตู้ทุกครั้ง
  • size = 2 รอบ · ตัวบน (บรรทัด 2) แฟ้มยังไม่มาบนโต๊ะ → ยังโทรถาม COUNT(*) · ตัวล่าง (บรรทัด 8) อยู่หลัง each แฟ้มมาแล้ว → นับบนโต๊ะ ไม่มี SQL
  • length = 1 รอบ · ตัวบนขนแฟ้มมาเลยตั้งแต่บรรทัด 2 · each กับตัวล่างใช้กองเดิมทั้งหมด

สรุปใช้จริง: ถ้าหน้านั้นจะวน each โชว์ทุกแผ่นอยู่แล้ว และแผ่นไม่เยอะ → length ไปตู้รอบเดียวคุ้มสุด · ถ้าแค่อยากรู้ตัวเลข ไม่ได้โชว์แฟ้ม → count ให้ตู้นับ อย่าขนมาทั้งลิ้นชัก · ไม่แน่ใจ → size เลือกให้เอง แต่ต้องรู้ว่ามันอยู่ก่อนหรือหลัง each

ถามแค่ว่า "มีไหม" — ใช้ exists?

บางทีไม่ได้อยากรู้ว่ากี่ชิ้น แค่อยากรู้ว่า "สมชายเคยเขียนบทความไหม" · ลองสองแบบ (ผลจริง ต่อจากก้อน count เมื่อกี้) · user_id = ช่อง "เป็นของผู้ใช้เลขที่ …" บนแฟ้มบทความ ที่สร้างไว้ในบท 107 (ละเอียดในบท 115) · สมชายคือเลขที่ 1:

wongnok-blog(dev):006> Post.where(user_id: 1).count > 0
  Post Count (2.8ms)  SELECT COUNT(*) FROM "posts" WHERE "posts"."user_id" = 1 /*application='WongnokBlog'*/
=> true
wongnok-blog(dev):007> Post.exists?(user_id: 1)
  Post Exists? (2.6ms)  SELECT 1 AS one FROM "posts" WHERE "posts"."user_id" = 1 LIMIT 1 /*application='WongnokBlog'*/
=> true
🗣 ศัพท์ที่เพิ่งเรียนไป
count vs size vs length
count = ให้ DB นับเสมอ · length = โหลดมาแล้วนับใน Ruby · size = โหลดแล้วนับใน Ruby ยังไม่โหลดให้ DB นับ · ถาม senior ได้ว่า “ตรงนี้ควรใช้ size แทน count ไหม เพราะเดี๋ยว view จะวน each อยู่แล้ว”
exists? · query cache
exists? = ถามแค่ว่ามีสักแถวไหม (LIMIT 1) · CACHE ใน log = query cache ที่ Rails จำคำตอบของ SQL เดิมไว้ภายในหนึ่ง request · พิมพ์บอก AI ได้ว่า “I only need to know if any row exists, should I use exists? instead of count > 0?”

ใน view วน <% @posts.each do |post| %> เสร็จแล้ว · ใต้รายการอยากเขียน "รวม N ชิ้น" โดยไม่ไปตู้เพิ่ม ควรใช้อะไร?

ตัวเลือกที่ 2 ถูก — หลัง each แฟ้มอยู่บนโต๊ะแล้ว (loaded? เป็นจริง) · size นับบนโต๊ะ ไม่มี SQL · ตรงกับผลจริงแบบ size ในเฉลย predict (ตัวล่างไม่มี SQL) · length ก็ได้ผลเดียวกันตรงนี้

ตัวเลือกที่ 1 ผิด เพราะ count ส่ง COUNT(*) ไปถามตู้ใหม่ทุกครั้ง แม้แฟ้มอยู่บนโต๊ะแล้ว (ถ้าใบสั่งนี้เพิ่งถามคำถามเดียวกันไป log จะขึ้น CACHE แต่ถ้ายังไม่เคยถาม ก็ไปตู้จริง) · ตัวเลือกที่ 3 ผิดสองชั้น — ไปตู้ และนับบทความ draft ด้วย ได้ 6 ไม่ใช่ 4 · ตัวเลือกที่ 4 ผิด เพราะ exists? ตอบแค่จริง/เท็จ ไม่ได้บอกจำนวน

จุดที่ต้องจำ: หลัง each ใช้ size · ก่อน each แม้แต่ size ก็ยังไปตู้

ให้ตู้คำนวณให้ — ขอแค่ของที่ใช้

หลักเดียวกับ count: ถ้าอยากได้แค่ ตัวเลข หรือแค่ บางช่อง · บอกตู้ให้ทำมาให้เสร็จ ดีกว่าขนแฟ้มทั้งแผ่นมาทำเองใน Ruby · ทุกตัวข้างล่างนี้ไปตู้ทันที (เป็นการยื่นใบ) — ผลจริงจากข้อมูลตัวอย่าง:

wongnok-blog(dev):001> Post.pluck(:title)
  Post Pluck (1.3ms)  SELECT "posts"."title" FROM "posts" /*application='WongnokBlog'*/
=>
["ต้มยำกุ้งน้ำข้น",
 "ส้มตำปูปลาร้า",
 "ข้าวมันไก่ประตูน้ำ",
 "ก๋วยเตี๋ยวเรืออยุธยา",
 "ผัดกะเพราหมูสับ",
 "แกงเขียวหวานไก่"]
wongnok-blog(dev):002> Post.ids
  Post Ids (0.8ms)  SELECT "posts"."id" FROM "posts" /*application='WongnokBlog'*/
=> [1, 2, 3, 4, 5, 6]
📋 เทียบให้เห็นภาพ — ขอแค่รายชื่อ ไม่ต้องขนแฟ้ม

อยากได้รายชื่อเมนูไปติดบอร์ด · ทางแรก: ขนแฟ้มสูตรทั้ง 6 แผ่นมาวางบนโต๊ะ แล้วนั่งลอกชื่อ (Post.all.map(&:title) — map กับ &: จากบท 92) · ทางที่สอง: บอกคนดูแลสต็อกว่า "จดแค่ชื่อเมนูมาให้กระดาษแผ่นเดียว" (pluck(:title))

ที่ไม่เหมือนกัน: กระดาษรายชื่อจริงยังเอาไปถามต่อได้ว่า "เมนูนี้ใส่อะไร" · แต่ผลของ pluck เป็นแค่ข้อความเปล่า ๆ — ไม่ใช่แฟ้ม เรียก .save หรือ .user ต่อไม่ได้ · ต้องใช้ทั้งแผ่นเมื่อไหร่ ต้องกลับไปเบิกแฟ้มจริง

ต่อไปเป็นการให้ตู้ "คิดเลข" (ผลจริง ต่อจากเมื่อกี้):

wongnok-blog(dev):003> Post.sum(:views)
  Post Sum (2.3ms)  SELECT SUM("posts"."views") FROM "posts" /*application='WongnokBlog'*/
=> 1055
wongnok-blog(dev):004> Post.average(:views)
  Post Average (2.7ms)  SELECT AVG("posts"."views") FROM "posts" /*application='WongnokBlog'*/
=> 0.1758333333333333333e3
wongnok-blog(dev):005> Post.where(published: true).average(:views)
  Post Average (2.6ms)  SELECT AVG("posts"."views") FROM "posts" WHERE "posts"."published" = TRUE /*application='WongnokBlog'*/
=> 0.26375e3
wongnok-blog(dev):006> Post.group(:user_id).count
  Post Count (0.5ms)  SELECT COUNT(*) AS "count_all", "posts"."user_id" AS "posts_user_id" FROM "posts" GROUP BY "posts"."user_id" /*application='WongnokBlog'*/
=> {3 => 2, 2 => 2, 1 => 2}
wongnok-blog(dev):007> Post.group(:user_id).sum(:views)
  Post Sum (2.5ms)  SELECT SUM("posts"."views") AS "sum_views", "posts"."user_id" AS "posts_user_id" FROM "posts" GROUP BY "posts"."user_id" /*application='WongnokBlog'*/
=> {3 => 510, 2 => 85, 1 => 460}

ขอบางช่องแต่ยังได้เป็น object Post ใช้ select — แต่ระวัง (ผลจริง):

wongnok-blog(dev):008> Post.select(:id, :title).first
  Post Load (1.2ms)  SELECT "posts"."id", "posts"."title" FROM "posts" ORDER BY "posts"."id" ASC LIMIT 1 /*application='WongnokBlog'*/
=> #<Post:0x000000012622a368 id: 1, title: "ต้มยำกุ้งน้ำข้น">
wongnok-blog(dev):009> Post.select(:id, :title).first.body
  Post Load (2.3ms)  SELECT "posts"."id", "posts"."title" FROM "posts" ORDER BY "posts"."id" ASC LIMIT 1 /*application='WongnokBlog'*/
(wongnok-blog):9:in '<main>': missing attribute 'body' for Post (ActiveModel::MissingAttributeError)

สุดท้าย ทวน update_all ที่เคยใช้ในภารกิจบท 111 — แก้หลายแถวในคำสั่งเดียว (ผลจริง):

wongnok-blog(dev):010> Post.where(published: false).update_all(views: 0)
  Post Update All (4.7ms)  UPDATE "posts" SET "views" = 0 WHERE "posts"."published" = FALSE /*application='WongnokBlog'*/
=> 2

เทียบกับ Go: GORM มี Pluck("title", &titles) · Count(&n) คล้ายกัน · ส่วน sum/average ฝั่ง GORM ต้องเขียน Select("sum(views)") เอง · Rails มี method ตั้งชื่อไว้ให้

🗣 ศัพท์ที่เพิ่งเรียนไป
pluck · select
pluck = ดึงค่าดิบของบางคอลัมน์ ไม่สร้าง object · select = สร้าง object แต่โหลดแค่บางคอลัมน์ · ถาม senior ได้ว่า “หน้านี้ใช้แค่ title ควร pluck หรือ select ดี เพราะยังต้องใช้ link_to กับ post อยู่”
aggregate (sum · average · group)
การให้ DB คำนวณสรุปจากหลายแถว (รวม · เฉลี่ย · นับแยกกลุ่ม) แทนการโหลดมาคำนวณใน Ruby · พิมพ์บอก AI ได้ว่า “rewrite this so the database does the aggregation instead of Ruby”
update_all
แก้หลายแถวด้วย UPDATE ประโยคเดียว · ข้าม validation และ callback · ถาม senior ได้ว่า “ใช้ update_all ตรงนี้ได้ไหม มี callback อะไรที่ต้องทำงานด้วยหรือเปล่า”

explain — แค่ให้เห็นว่ามีของแบบนี้

explain = ถาม PostgreSQL ว่า "ถ้าให้ทำใบเบิกนี้ จะไปหาแฟ้มยังไง" · ใส่ (:analyze) = ทำจริงแล้วจับเวลาด้วย · บทนี้แค่ให้เห็นหน้าตา ใช้จริงตอนเจอหน้าที่ช้า (ผลจริง):

wongnok-blog(dev):001> Post.where(published: true).explain(:analyze)
  Post Load (1.0ms)  SELECT "posts".* FROM "posts" WHERE "posts"."published" = TRUE /*application='WongnokBlog'*/
=>
EXPLAIN (ANALYZE) SELECT "posts".* FROM "posts" WHERE "posts"."published" = TRUE /*application='WongnokBlog'*/
                                             QUERY PLAN
----------------------------------------------------------------------------------------------------
 Seq Scan on posts  (cost=0.00..16.30 rows=315 width=101) (actual time=0.008..0.009 rows=4 loops=1)
   Filter: published
   Rows Removed by Filter: 2
 Planning Time: 0.038 ms
 Execution Time: 0.015 ms
(5 rows)

🎯 ภารกิจ · เขียนใบเบิกของของสมชาย (~10 นาที)

ใช้ bin/rails console กับข้อมูลตัวอย่างชุดเดียวกับบท 107 · ทุกบรรทัดที่สร้างใบเบิกของ ต่อท้ายด้วย ; nil

  1. สร้าง mine = บทความของสมชาย (user_id: 1) · เพิ่มเงื่อนไข published · เรียงยอดวิวมากไปน้อย — สามบรรทัด ต้องไม่เห็น SQL สักบรรทัด
  2. พิมพ์ mine.to_sql แล้วแปลเป็นภาษาคนในใจ
  3. ขอแค่ชื่อเรื่อง เรียงตามที่ตั้งไว้ — ใช้ method ที่ไม่สร้าง object Post
  4. ยอดวิวรวมของบทความใน mine
  5. เช็คว่าสมหญิง (user_id: 2) มีบทความที่ยังเป็น draft ไหม — ใช้วิธีที่ "เจอแผ่นแรกก็หยุด"

ควรเห็น: ข้อ 1 มีแต่ => nil · ข้อ 3 ได้ ["ส้มตำปูปลาร้า", "ต้มยำกุ้งน้ำข้น"] · ข้อ 4 ได้ 460 · ข้อ 5 ได้ true และ SQL มี LIMIT 1

ถ้าเจอ error นี้ แปลว่า…

ผลจริงทั้งหมด:

wongnok-blog(dev):001> mine = Post.where(user_id: 1); nil
=> nil
wongnok-blog(dev):002> mine = mine.where(published: true); nil
=> nil
wongnok-blog(dev):003> mine = mine.order(views: :desc); nil
=> nil
wongnok-blog(dev):004> mine.to_sql
=> "SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = 1 AND \"posts\".\"published\" = TRUE ORDER BY \"posts\".\"views\" DESC"
wongnok-blog(dev):005> mine.pluck(:title)
  Post Pluck (1.2ms)  SELECT "posts"."title" FROM "posts" WHERE "posts"."user_id" = 1 AND "posts"."published" = TRUE ORDER BY "posts"."views" DESC /*application='WongnokBlog'*/
=> ["ส้มตำปูปลาร้า", "ต้มยำกุ้งน้ำข้น"]
wongnok-blog(dev):006> mine.sum(:views)
  Post Sum (2.9ms)  SELECT SUM("posts"."views") FROM "posts" WHERE "posts"."user_id" = 1 AND "posts"."published" = TRUE /*application='WongnokBlog'*/
=> 460
wongnok-blog(dev):007> Post.where(user_id: 2, published: false).exists?
  Post Exists? (2.0ms)  SELECT 1 AS one FROM "posts" WHERE "posts"."user_id" = 2 AND "posts"."published" = FALSE LIMIT 1 /*application='WongnokBlog'*/
=> true
  • ข้อ 2 แปล: "ขอทุกช่องจากลิ้นชัก posts · เฉพาะแผ่นที่เจ้าของเลขที่ 1 และ published เป็นจริง · เรียงวิวมากไปน้อย" — สามบรรทัดกลายเป็น AND ในประโยคเดียว
  • ข้อ 3: pluck ใช้ใบเบิกของเดิม แต่เปลี่ยน SELECT "posts".* เป็น SELECT "posts"."title" ให้เอง · เรียงตาม ORDER BY ที่เขียนไว้
  • ข้อ 4: sum ตัด ORDER BY ทิ้งให้เอง เพราะการรวมเลขไม่ต้องเรียง · 340 + 120 = 460
  • ข้อ 5: where ใส่สองเงื่อนไขในวงเล็บเดียวได้ · exists? ได้ LIMIT 1 (ก๋วยเตี๋ยวเรืออยุธยาเป็น draft ของสมหญิง)

ลองต่อถ้ายังมีเวลา: พิมพ์ mine.count สองครั้งติดกัน แล้วพิมพ์ mine.size — นับดูว่าเห็น SQL กี่บรรทัด แล้วเทียบกับตารางในหัวข้อ count vs size

ผลของคุณไม่ตรงกับนี้ หรือเห็น SQL ในจังหวะที่ไม่คาดไว้ ถามกลับมาได้เลย พร้อมแปะทั้งก้อนที่ console ขึ้นมา

อยากรู้แค่ว่า "สมชายมีบทความบ้างไหม" (ไม่ต้องรู้ว่ากี่ชิ้น) · แบบไหนให้ตู้ทำงานน้อยที่สุด?

ตัวเลือกที่ 3 ถูก — SQL จริงคือ SELECT 1 AS one … LIMIT 1 เจอแผ่นแรกก็ตอบ ไม่ต้องหยิบแฟ้ม ไม่ต้องนับต่อ

ตัวเลือกที่ 1 กับ 4 ผิด เพราะหยิบทุกแผ่นของสมชายมาวางบนโต๊ะก่อน (SELECT "posts".*) แค่เพื่อตอบว่ามีหรือไม่ · ตัวเลือกที่ 2 ไม่ได้หยิบแฟ้ม (เป็น COUNT(*)) แต่ตู้ต้องนับให้ครบทุกแผ่นก่อน — ดีกว่า 1 และ 4 แต่ยังทำงานเกินจำเป็น

จุดที่ต้องจำ: ถามว่า "มีไหม" ใช้ exists? · ถาม "กี่ชิ้น" ใช้ count · อย่าขนแฟ้มมาเพื่อตอบคำถามที่ตอบเป็นตัวเลขได้

ใน console พิมพ์ q = Post.where(published: true) (ไม่มี ; nil) แล้วเห็น SQL ที่มี /* loading for pp */ LIMIT 11 · แปลว่าอะไร?

ตัวเลือกที่ 2 ถูก — console ต้องโชว์ผลหลัง => จึงไปตู้เพื่อ "pretty print" (pp) และขอแค่ 11 แผ่นกันตารางใหญ่ · ใน log ของหน้า /posts เห็นชัดว่า controller ที่มี where ไม่ได้ยิง SQL เลย

ตัวเลือกที่ 1 ผิด — ต่อท้าย ; nil แล้ว SQL หายไปเลย พิสูจน์ว่าไม่ใช่ where ที่ยิง · ตัวเลือกที่ 3 ผิด เพราะตารางเรามี 6 แถว แต่ยังได้ LIMIT 11 · ตัวเลือกที่ 4 ผิด เพราะในแอปจริงไม่มี LIMIT 11 โผล่ใน log

จุดที่ต้องจำ: ทดลองเรื่อง "ยิงตอนไหน" ใน console ต้องต่อ ; nil ไม่งั้นจะเห็นภาพผิด

สรุป 3 บรรทัด

ถ้าเปิดสไลด์ดูประกอบ

หน้าสไลด์เขียนของจริงต้องสนใจไหม
31พิมพ์ q = Post.where(…) ทีละบรรทัด "ยังไม่มี SQL วิ่งเลย"ใน console เห็น SQL ทุกบรรทัด (loading for pp) · ต้องต่อ ; nil ถึงจะเห็นว่าไม่ยิงสนใจ — พิมพ์ตามแล้วจะงง
31เรียกใช้ relation เดิมซ้ำ "ยิง SQL ใหม่ทุกครั้ง"จริงกับ count · แต่ each / first / size ครั้งที่สองบน relation ที่โหลดแล้วไม่ยิงซ้ำนิดหน่อย
32ต้องเพิ่ม verbose_query_logs = true เองRails 8.1 เปิดไว้ใน development แล้ว · เลขบรรทัดไฟล์ .erb เกินจริง 1สนใจ เรื่องเลขบรรทัด
35Post.where(user_id: 1).count > 0 = "โหลดมาทั้งหมด"ยิง COUNT(*) ไม่ได้โหลดแถว · exists? ยังดีกว่าเพราะหยุดที่แถวแรกนิดหน่อย — ข้อสรุปถูก เหตุผลเกินจริง
35"ใน view ที่วน each อยู่แล้ว ใช้ size"ถูกเฉพาะ size ที่อยู่หลัง each · ถ้าอยู่บน each ยังยิง COUNT(*)สนใจ
35Post.where(old: true).update_all(…)ตารางเราไม่มีช่อง old → PG::UndefinedColumn · บทนี้ใช้ published: false แทนไม่ต้อง — แค่ตัวอย่าง

อ้างอิง