Technology

SELECT count(id) เร็วกว่า count(*) จริง ๆ เหรอ

By Arnon Puitrakul - 04 ตุลาคม 2024

SELECT count(id) เร็วกว่า count(*) จริง ๆ เหรอ

เราไปเจอความเชื่อนึงเกี่ยวกับ SQL Query มาว่า เนี่ยนะ ถ้าเราจะ Count หรือนับแถว เราอย่าไปใช้ count(*) นะ ให้เราใช้ count(id) หรือด้านในเป็น Primary Key ใน Table นั้น ๆ จะทำให้ Query ได้เร็วกว่า วันน้ีเรามาทดลองกันดีกว่า ว่ามันเป็นแบบนั้นจริง ๆ หรือไม่ และเดี๋ยวจะมาบอกว่า ทำไมด้วย

การทดลอง

เราเริ่มจากทดลองให้ดูกันก่อน โดยเราจะใช้ Database ขนาดใหญ่ ๆ หน่อย เป็น Database ที่เก็บ State ของ Home Assistant ในบ้านเราเอง น่าจะใหญ่เต็มไม้เต็มมือสำหรับการทดลองได้ดีเลยละ ใน Table นั้นมีข้อมูลอยู่ทั้งหมด 29,403,762 rows

CREATE TABLE `states` (
  `state_id` int NOT NULL AUTO_INCREMENT,
  `domain` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `entity_id` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `state` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `attributes` longtext COLLATE utf8mb4_unicode_ci,
  `event_id` int DEFAULT NULL,
  `last_changed` datetime(6) DEFAULT NULL,
  `last_updated` datetime(6) DEFAULT NULL,
  `created` datetime(6) DEFAULT NULL,
  `old_state_id` int DEFAULT NULL,
  `attributes_id` int DEFAULT NULL,
  `origin_idx` int DEFAULT NULL,
  `context_id` varchar(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `context_user_id` varchar(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `context_parent_id` varchar(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `last_updated_ts` double DEFAULT NULL,
  `last_changed_ts` double DEFAULT NULL,
  `context_id_bin` tinyblob,
  `context_user_id_bin` tinyblob,
  `context_parent_id_bin` tinyblob,
  `metadata_id` int DEFAULT NULL,
  PRIMARY KEY (`state_id`),
  KEY `ix_states_old_state_id` (`old_state_id`),
  KEY `ix_states_attributes_id` (`attributes_id`),
  KEY `ix_states_last_updated_ts` (`last_updated_ts`),
  KEY `ix_states_context_id_bin` (`context_id_bin`(16)),
  KEY `ix_states_metadata_id_last_updated_ts` (`metadata_id`,`last_updated_ts`),
  CONSTRAINT `states_ibfk_2` FOREIGN KEY (`old_state_id`) REFERENCES `states` (`state_id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE=InnoDB AUTO_INCREMENT=1631572343 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

ด้านบนนี้เป็น DDL ของ Table ชื่อว่า State ซึ่งเป็น Table ที่ Home Assistant สร้างขึ้นมาเพื่อเก็บ State หรือค่าจาก Sensor ต่าง ๆ ในบ้านของเราเอง ถ้าเราอ่าน DDL ออก เราจะเห็นว่า เขามีการสร้าง Key ทั้งหมด 6 ตัวด้วยกัน คือ Primary Key เป็น state_id ก็ Make Sense ใช่มะ และอีก 5 Secondary Index ที่เหลือ เขาสร้างเป็น Index Key เอาไว้ ดูจากการใช้งาน เราก็พอจะเข้าใจนะว่า ทำไมต้องสร้างเช่น ix_states_last_attributes_id น่าจะสำหรับเวลาเราจะดึง Attributes ที่อยู่ในแต่ละ State ออกมาเรียงเป็นกราฟแสดงผล Index ชุดนี้จะช่วยทำให้การ Query มันทำได้เร็วขึ้นนั่นเอง

1. select count(state_id) from states;
2. select count(*) from states;

งั้นเรามาลอง Query กันดีกว่า เราขอแบ่งการทดลองออกเป็น 2 ส่วนตามที่เราสงสัย อันแรกเป็นอันที่ทุกคนบอกให้เราทำ คือ การใส่ Primary Key ของ Table ลงไปใน Count และอีกอันเป็นอันที่เราบอกว่า ใช้ไปเถอะคือ การใส่ Star ลงไปใน Count เลย ในการทดลอง เราจะทำการสั่ง Query แต่ละ Expression ทั้งหมด 10 ครั้ง และ เอามาหาค่าเฉลี่ยกัน มาลองดูกันว่ามันจะเป็นอย่างไร

เอาไปหามาละ หากเราใช้คำสั่งแบบที่ 1 เวลาที่ใช้โดยเฉลี่ยอยู่ที่ 4.0296 วินาที และ วิธีที่ 2 อยู่ที่ 3.8275 วินาที อ้าวไหงกลายเป็นว่าแบบที่ 2 เร็วกว่าละ แต่ ๆๆๆๆ มันห่างกัน 5.28% เท่านั้นเอง ทำให้เอาเข้าจริงแล้ว เราเรียกว่า มันไม่มีนัยสำคัญขนาดนั้นเลย

มันเกิดอะไรขึ้น ?

explain select count(state_id) from states;

เพื่อให้เข้าใจที่มา เราขอให้ MySQL มันบอกวิธีการหน่อยว่า มันจะสั่ง Query ยังไง ผ่านคำสั่งชื่อว่า EXPLAIN

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE states null index null ix_states_old_state_id 5 null 29308619 100 Using index

ผลด้านบน เกิดจากการสั่งให้ EXPLAIN Query ตัวที่ 1 นั่นคือ ตัวที่ใส่ Primary Key เข้าไปใน Count เราจะเห็นว่า ถึงเราจะให้มันนับ Key ที่ชื่อว่า state_id แต่พอ Explain ออกมา อ้าว กลายเป็นไปนับจาก Key ที่ชื่อว่า ix_states_old_state_id แทนแล้วละ

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE states null index null ix_states_old_state_id 5 null 29308619 100 Using index

เปรียบเทียบกับ Query 2 ที่เราใส่ Star เข้าไปแทน จะเห็นว่า เมื่อเราสั่ง Explain ออกมา ตัว Key ที่มันใช้ มันใช้ตัวเดียวกันเลยนิหว่า ใช่ฮะ นั่นคือ เหตุว่าทำไมเราถึงบอกว่า แก ๆๆๆๆ มันเหมือนกัน แต่เดี๋ยวก่อน ๆๆ ใจเย็น ๆ ทุกคน อ่านต่อไปก่อน มันอาจจะไม่เหมือนกันก็เป็นได้ ~

ทำไมถึงเป็นแบบนี้ ?

สมมุติว่า ถ้าเรามีข้อมูลอยู่ตารางนึง เราอยากรู้ว่า ในตารางนี้ประกอบด้วยข้อมูลทั้งหมดกี่ Row เราจะทำอย่างไร

วิธีการที่ง่ายที่สุดคือ การไล่นับ ๆ ลงไปเรื่อยจากบนลงล่าง มันดูเป็นวิธีที่ง่ายมาก ๆ ไม่น่ามีปัญหาอะไร แต่ข้อเสียคือ มันช้าไปใช่มั้ย เช่น ถ้าเรามีสัก 10 rows ชิว ๆ มันจบ แต่หากเรามาเล่นกับเคสที่เราทำกันในบทความนี้หลัก 29M rows ยังไม่นับพวก Transactional Database ขนาดใหญ่ ๆ พวกนั้นหลัก 1,000M rows ได้เลย การนับแบบนี้ไม่น่าจะรอด วิธีนี้เราจะเรียกว่า Full-Table Scan เป็นสิ่งที่พวก DBMS พยายามหลีกเลี่ยงให้มากที่สุดเท่าที่จะเป็นไปได้ เช่นใน MySQL เขาจะมีบอกอยู่ว่า ตัวมันเองจะเริ่มใช้ Full-Table Scan เมื่อไหร่ใน Documentation ของ MySQL

งั้นเราเปลี่ยนวิธีใหม่ เรามาใช้ประโยชน์ของ Key กันดีกว่า เริ่มจากใช้ Primary Key ในเมื่อเราแบ่งข้อมูลเป็น Block เราเรียกวิธีนี้ว่า Clustered Index งั้น เราก็ไล่นับทีละ Block แน่นอนว่า จำนวน Block มันค่อย ๆ เยอะขึ้นเรื่อย ๆ เมื่อเราเพิ่มจำนวนข้อมูลเข้าไปเรื่อย ๆ

ความอร่อยมันเพิ่มขึ้นเมื่อ เราจำเป็นต้องเข้าถึงข้อมูลหลาย Block มากขึ้นเรื่อย ๆ เนื่องจากมันจำเป็นต้องอ่านข้อมูลหลายรอบมากขึ้น เช่น เรามี 10 Blocks การจะนับ เราจะต้องค่อย ๆ เอาทีละ Block เข้ามานับไปเรื่อย ๆ และ แต่ละ Block เราต้องหยิบมาทุก Column อีก ใหญ่มาก ๆ ยิ่งถ้าเราใช้พวก Magnetics Disk อย่าง HDD การอ่านในลักษณะนี้ไม่น่าจะดีเท่าไหร่

เมื่อเทียบกับการใช้ Secondary Key ทั้งหลาย มันเก็บแค่ Key ที่มันต้องเก็บเท่านั้น จำนวน Block ไม่เท่ากับพวก Primary Key ทำให้การไล่ไปตามพวก Secondary Key ทำได้เร็วกว่าการไล่ใน Primary Key มาก ๆ

เมื่อเป็นแบบนี้ ทำให้ใน MySQL มันป้องกันพวกสู่รู้ หรือมั่วทำพลาดด้วยการที่ ถ้ามันรู้ว่า เราจะทำการนับจำนวน Row เฉย ๆ มันจะ Ignore สิ่งที่อยู่ใน Count แล้วเปลี่ยนเป็น Star ให้หมด เพราะยังไงผลก็เหมือนกัน แถมทำงานได้เร็วกว่าแน่นอนด้วย ใช่แล้วฮะ ภายใน DBMS สมัยใหม่ นอกจากที่เราจะต้องมาทำ Query Optimisation เองแล้ว ภายในตัวมันเองยังมีกลไกที่เข้ามาช่วยด้วยเช่นกัน

สรุป

ไม่ว่า เราจะ Count Star หรือ Primary Key ก็ตาม ยังไง ๆ ผลที่เราจะได้มันเหมือนกันหมด เพราะ MySQL หรือ DBMS บางตัวเขาเขียนมาดัก และ Optimise Query ให้เราอยู่เบื้องหลังอยู่แล้ว และในทางทฤษฏีเอง การที่เรานับจาก Secondary Key เอง ยังไง ๆ ก็เร็วกว่าการที่เรานั่งนับผ่าน Primary Key ในเคสนี้อย่างแน่นอน เลยไม่แปลกเท่าไหร่ที่ DBMS จะทำแบบนี้ เพื่อให้เราได้ Performance ที่ดีที่สุดนั่นเอง

Read Next...

ซื้อ NAS สำเร็จรูปหรือจะประกอบเครื่องเองเลยดี

ซื้อ NAS สำเร็จรูปหรือจะประกอบเครื่องเองเลยดี

ในปัจจุบันเราพึ่งพาการเก็บข้อมูลต่าง ๆ บนเครื่องคอมพิวเตอร์มากขึ้นเรื่อย ๆ สำหรับบางคนมากจนไม่สามารถเก็บได้ในเครื่องของเราอีกแล้ว ทำให้อาจจะต้องมองหาระบบจัดเก็บข้อมูลอื่น ๆ เข้ามาใช้งาน หนึ่งในนั้นคือการใช้ NAS หลายคนมีคำถามว่า แล้วเราควรจะเลือกซื้อ NAS สำเร็จรูป หรือ DIY ดี...

SELECT count(id) เร็วกว่า count(*) จริง ๆ เหรอ

SELECT count(id) เร็วกว่า count(*) จริง ๆ เหรอ

เราไปเจอความเชื่อนึงเกี่ยวกับ SQL Query มาว่า เนี่ยนะ ถ้าเราจะ Count หรือนับแถว เราอย่าไปใช้ count(*) นะ ให้เราใช้ count(id) หรือด้านในเป็น Primary Key ใน Table นั้น ๆ จะทำให้ Query ได้เร็วกว่า วันน้ีเรามาทดลองกันดีกว่า ว่ามันเป็นแบบนั้นจริง ๆ หรือไม่...

การเบลอรูปภาพ มันทำได้อย่างไร ทำไมภาพถึงเบลอได้

การเบลอรูปภาพ มันทำได้อย่างไร ทำไมภาพถึงเบลอได้

เคยสงสัยกันมั้ยว่า Filter ที่เราใช้เบลอภาพ ไม่ว่าจะเพื่อความสวยงาม หรืออะไรก็ตาม แท้จริงแล้ว มันทำงานอย่างไร วันนี้เราจะพาไปดูคณิตศาสตร์และเทคนิคเบื้องหลังกันว่า กว่าที่รูปภาพจะถูกเบลอได้ มันเกิดจากอะไร...

AI Watermark กับความรับผิดชอบต่อการใช้ AI

AI Watermark กับความรับผิดชอบต่อการใช้ AI

หลังจากดูงาน Google I/O 2024 ที่ผ่านมา เรามาสะดุดเรื่องของการใส่ Watermark ลงไปใน Content ที่ Generate จาก AI วันนี้เราจะมาเล่าให้อ่านกันว่า วิธีการทำ Watermark ใน Content ทำอย่างไร...