# SQL ขั้นสูงสำหรับสัมภาษณ์ Data Analyst: Subquery, Pivot และการเพิ่มประสิทธิภาพ Query 2026 > คู่มือ SQL ขั้นสูงสำหรับเตรียมสัมภาษณ์ Data Analyst 2026: correlated subquery, pivot query ด้วย conditional aggregation, EXPLAIN ANALYZE, กลยุทธ์ indexing และ anti-pattern ที่ต้องหลีกเลี่ยง - Published: 2026-05-15 - Updated: 2026-05-15 - Author: Anthony Fillion-Maillet - Tags: sql, data-analytics, interview, query-optimization, subqueries - Reading time: 10 min --- การสัมภาษณ์งานในตำแหน่ง Data Analyst ในปัจจุบันไม่ได้วัดเพียงความสามารถในการเขียน SELECT พื้นฐานเท่านั้น แต่ยังต้องการทักษะ SQL ขั้นสูงที่สามารถจัดการกับข้อมูลที่ซับซ้อน วิเคราะห์ประสิทธิภาพของ Query และออกแบบโครงสร้างฐานข้อมูลที่เหมาะสมได้ ผู้สมัครที่เข้าใจเทคนิคอย่าง Correlated Subquery, Pivot Query, EXPLAIN Plan และ Indexing Strategy จะมีความได้เปรียบอย่างชัดเจนในกระบวนการคัดเลือก บทความนี้รวบรวมหัวข้อ SQL ขั้นสูงที่พบบ่อยที่สุดในการสัมภาษณ์ตำแหน่ง Data Analyst พร้อมตัวอย่างโค้ดที่นำไปใช้งานได้จริง > **เคล็ดลับสำหรับการสัมภาษณ์** > > ในการสัมภาษณ์งาน ผู้สัมภาษณ์มักไม่ได้ต้องการเพียงคำตอบที่ถูกต้อง แต่ต้องการเห็นกระบวนการคิดด้วย การอธิบายว่าเหตุใดจึงเลือกใช้ CTE แทน Correlated Subquery หรือเหตุใด EXISTS จึงเหมาะสมกว่า IN ในบางสถานการณ์ จะช่วยแสดงถึงความเข้าใจเชิงลึกที่ทำให้โดดเด่นจากผู้สมัครคนอื่น ## Correlated Subquery กับ Regular Subquery: ความแตกต่างที่สำคัญ Regular Subquery คือ Subquery ที่ทำงานอิสระจาก Query หลัก โดยจะถูกประมวลผลเพียงครั้งเดียวแล้วนำผลลัพธ์ไปใช้กับ Query ภายนอก ตัวอย่างเช่น การหาคำสั่งซื้อที่มีมูลค่าสูงกว่าค่าเฉลี่ยของทั้งหมด ```sql -- regular_subquery_threshold.sql -- Find all orders above the average order value SELECT order_id, customer_id, total_amount FROM orders WHERE total_amount > ( -- Executes once, returns a single scalar value SELECT AVG(total_amount) FROM orders ); ``` ในทางตรงกันข้าม Correlated Subquery จะอ้างอิงข้อมูลจาก Query ภายนอก ทำให้ต้องถูกประมวลผลซ้ำทุกครั้งสำหรับแต่ละแถวของ Query หลัก สิ่งนี้ส่งผลต่อประสิทธิภาพอย่างมากเมื่อทำงานกับข้อมูลจำนวนมาก ตัวอย่างต่อไปนี้แสดงการค้นหาพนักงานที่มีเงินเดือนสูงกว่าค่าเฉลี่ยของแผนกตนเอง ```sql -- correlated_subquery_department.sql -- Employees earning above their department average SELECT e.employee_id, e.name, e.department, e.salary FROM employees e WHERE e.salary > ( -- Re-evaluated for each employee's department SELECT AVG(e2.salary) FROM employees e2 WHERE e2.department = e.department ); ``` วิธีที่ดีกว่าสำหรับ Query ข้างต้นคือการใช้ Common Table Expression (CTE) เพื่อคำนวณค่าเฉลี่ยของแต่ละแผนกเพียงครั้งเดียว แล้วจึง JOIN กลับมาเปรียบเทียบ วิธีนี้ลดจำนวนการสแกนตารางลงอย่างมาก ```sql -- optimized_with_cte.sql -- Same result, single pass over the data WITH dept_avg AS ( SELECT department, AVG(salary) AS avg_salary FROM employees GROUP BY department ) SELECT e.employee_id, e.name, e.department, e.salary FROM employees e JOIN dept_avg d ON e.department = d.department WHERE e.salary > d.avg_salary; ``` การเปลี่ยนจาก Correlated Subquery เป็น CTE + JOIN เป็นเทคนิคที่ผู้สัมภาษณ์มักต้องการเห็น เพราะแสดงให้เห็นว่าผู้สมัครคำนึงถึงประสิทธิภาพของ Query ไม่ใช่เพียงแค่ความถูกต้องของผลลัพธ์ ## Subquery Patterns ที่ต้องรู้ ### EXISTS กับ IN: เลือกใช้อย่างไร หนึ่งในคำถามที่พบบ่อยในการสัมภาษณ์คือความแตกต่างระหว่าง EXISTS กับ IN เมื่อใช้กับ Subquery ในแง่ของผลลัพธ์ ทั้งสองให้คำตอบเดียวกัน แต่ในแง่ของประสิทธิภาพนั้นแตกต่างกันอย่างมีนัยสำคัญ โดยเฉพาะเมื่อ Subquery ส่งคืนข้อมูลจำนวนมาก EXISTS จะหยุดทำงานทันทีเมื่อพบแถวแรกที่ตรงเงื่อนไข ในขณะที่ IN ต้องประมวลผล Subquery ทั้งหมดก่อน ```sql -- exists_vs_in.sql -- Customers who placed at least one order in 2026 (EXISTS — preferred) SELECT c.customer_id, c.name FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id AND o.order_date >= '2026-01-01' ); -- Equivalent with IN (slower on large datasets) SELECT customer_id, name FROM customers WHERE customer_id IN ( SELECT customer_id FROM orders WHERE order_date >= '2026-01-01' ); ``` ### Scalar Subquery ใน SELECT อีกรูปแบบหนึ่งที่ควรรู้จักคือ Scalar Subquery ใน SELECT clause ซึ่งส่งคืนค่าเดียวสำหรับแต่ละแถว วิธีนี้มีประโยชน์เมื่อต้องการแสดงข้อมูลรวม (Aggregated Data) ควบคู่กับข้อมูลรายบุคคล แต่ควรระมัดระวังเรื่องประสิทธิภาพเพราะทำงานคล้าย Correlated Subquery ```sql -- scalar_subquery_select.sql -- Each product with its category's total revenue SELECT p.product_id, p.product_name, p.category, ( SELECT SUM(oi.quantity * oi.unit_price) FROM order_items oi JOIN products p2 ON oi.product_id = p2.product_id WHERE p2.category = p.category ) AS category_total_revenue FROM products p; ``` สำหรับกรณีนี้ ในสถานการณ์จริงควรพิจารณาใช้ Window Function หรือ CTE แทนเพื่อประสิทธิภาพที่ดีกว่า การเข้าใจทั้งวิธีที่ใช้งานได้และวิธีที่เหมาะสมที่สุดเป็นสิ่งที่ผู้สัมภาษณ์ให้ความสำคัญ ## Pivot Query ด้วย Conditional Aggregation Pivot Query เป็นเทคนิคที่ Data Analyst ต้องใช้บ่อยมาก เพราะการแปลงข้อมูลจากแถวเป็นคอลัมน์ (Row to Column) ช่วยให้ข้อมูลอ่านง่ายขึ้นและเหมาะสำหรับการนำเสนอในรายงาน วิธีที่นิยมที่สุดคือการใช้ CASE ร่วมกับ Aggregate Function ตัวอย่างต่อไปนี้แสดงการสร้างรายงานรายได้รายเดือนของแต่ละผลิตภัณฑ์ โดยแปลงเดือนจากแถวเป็นคอลัมน์ ```sql -- pivot_monthly_revenue.sql -- Monthly revenue pivot: one row per product, one column per month SELECT product_id, product_name, SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 1 THEN quantity * unit_price ELSE 0 END) AS jan_revenue, SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 2 THEN quantity * unit_price ELSE 0 END) AS feb_revenue, SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 3 THEN quantity * unit_price ELSE 0 END) AS mar_revenue, SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 4 THEN quantity * unit_price ELSE 0 END) AS apr_revenue, -- Repeat for remaining months SUM(quantity * unit_price) AS total_revenue FROM order_items oi JOIN orders o ON oi.order_id = o.order_id JOIN products p ON oi.product_id = p.product_id WHERE o.order_date >= '2026-01-01' GROUP BY product_id, product_name ORDER BY total_revenue DESC; ``` อีกตัวอย่างหนึ่งที่พบบ่อยในงาน Data Analyst คือการวิเคราะห์พฤติกรรมผู้ใช้ตามประเภทอุปกรณ์ Query ด้านล่างแสดงการนับจำนวน Session ตามประเภทอุปกรณ์และคำนวณสัดส่วนของ Mobile ```sql -- pivot_user_activity.sql -- User activity: sessions by day of week and device type SELECT user_id, COUNT(CASE WHEN device_type = 'mobile' THEN 1 END) AS mobile_sessions, COUNT(CASE WHEN device_type = 'desktop' THEN 1 END) AS desktop_sessions, COUNT(CASE WHEN device_type = 'tablet' THEN 1 END) AS tablet_sessions, ROUND( COUNT(CASE WHEN device_type = 'mobile' THEN 1 END) * 100.0 / COUNT(*), 1 ) AS mobile_pct FROM user_sessions WHERE session_date >= CURRENT_DATE - INTERVAL '30 days' GROUP BY user_id HAVING COUNT(*) >= 5 -- Only users with meaningful activity ORDER BY mobile_pct DESC; ``` ## Dynamic Pivot ด้วย CROSSTAB สำหรับผู้ที่ใช้ PostgreSQL ฟังก์ชัน CROSSTAB จาก Extension `tablefunc` เป็นอีกทางเลือกหนึ่งในการสร้าง Pivot Table โดยเฉพาะเมื่อต้องการโครงสร้างที่ชัดเจนและอ่านง่ายกว่าการใช้ CASE หลายบรรทัด ```sql -- crosstab_dynamic_pivot.sql -- Enable the extension (once per database) CREATE EXTENSION IF NOT EXISTS tablefunc; -- Revenue by product and quarter using CROSSTAB SELECT * FROM crosstab( $$ SELECT product_name, 'Q' || EXTRACT(QUARTER FROM order_date)::TEXT AS quarter, SUM(quantity * unit_price) AS revenue FROM order_items oi JOIN orders o ON oi.order_id = o.order_id JOIN products p ON oi.product_id = p.product_id WHERE EXTRACT(YEAR FROM o.order_date) = 2026 GROUP BY product_name, quarter ORDER BY product_name, quarter $$, $$ VALUES ('Q1'), ('Q2'), ('Q3'), ('Q4') $$ ) AS pivot_table( product_name TEXT, q1_revenue NUMERIC, q2_revenue NUMERIC, q3_revenue NUMERIC, q4_revenue NUMERIC ); ``` ข้อควรทราบคือ CROSSTAB เป็นฟีเจอร์เฉพาะของ PostgreSQL ไม่สามารถใช้ใน MySQL หรือ SQL Server ได้โดยตรง ในการสัมภาษณ์ ควรระบุให้ชัดเจนว่ากำลังใช้ฟีเจอร์เฉพาะของ Database ใด เพราะแสดงให้เห็นถึงความเข้าใจในความแตกต่างระหว่าง Database Engine ต่าง ๆ ## EXPLAIN Plan และ Cost Analysis ความสามารถในการอ่านและวิเคราะห์ EXPLAIN Plan ถือเป็นทักษะที่แยกระหว่าง Data Analyst ทั่วไปกับ Data Analyst ขั้นสูงได้อย่างชัดเจน คำสั่ง EXPLAIN ANALYZE จะแสดงแผนการทำงานจริงของ Query รวมถึงเวลาที่ใช้ในแต่ละขั้นตอนและจำนวน Buffer ที่อ่าน ```sql -- explain_analyze_example.sql -- Analyze a slow query to identify bottlenecks EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT c.customer_id, c.name, COUNT(o.order_id) AS order_count, SUM(o.total_amount) AS lifetime_value FROM customers c JOIN orders o ON c.customer_id = o.customer_id WHERE o.order_date >= '2025-01-01' GROUP BY c.customer_id, c.name HAVING SUM(o.total_amount) > 1000 ORDER BY lifetime_value DESC; ``` เมื่ออ่านผลลัพธ์ของ EXPLAIN ANALYZE มีจุดสำคัญที่ต้องสังเกตดังนี้ - **Seq Scan vs Index Scan**: Seq Scan บนตารางขนาดใหญ่มักเป็นสัญญาณของ Query ที่ช้า ควรตรวจสอบว่ามี Index ที่เหมาะสมหรือไม่ - **Actual Time**: เปรียบเทียบเวลาจริงกับ Estimated Cost เพื่อดูว่า Planner ประเมินได้แม่นยำหรือไม่ - **Rows**: จำนวนแถวจริงที่ประมวลผลเทียบกับที่ Planner คาดการณ์ ถ้าต่างกันมากอาจต้องอัปเดต Statistics - **Buffers**: จำนวน Shared Hit (อ่านจาก Cache) เทียบกับ Shared Read (อ่านจาก Disk) ยิ่งมี Hit มากยิ่งดี - **Sort Method**: ตรวจสอบว่าเป็น In-Memory Sort หรือ Disk Sort เพราะ Disk Sort ช้ากว่ามาก ## Indexing Strategies สำหรับ Query ที่ใช้บ่อย หลังจากระบุ Bottleneck จาก EXPLAIN Plan แล้ว ขั้นตอนถัดไปคือการสร้าง Index ที่เหมาะสม การออกแบบ Index ที่ดีสามารถเปลี่ยน Query ที่ใช้เวลาหลายวินาทีให้เหลือเพียงไม่กี่มิลลิวินาที ```sql -- indexing_strategies.sql -- Index on order_date: filters a small percentage of rows CREATE INDEX idx_orders_date ON orders (order_date); -- Composite index for queries filtering on both columns CREATE INDEX idx_orders_customer_date ON orders (customer_id, order_date); -- Covering index: includes columns needed in SELECT -- Enables Index Only Scan (no table access) CREATE INDEX idx_orders_covering ON orders (customer_id, order_date) INCLUDE (total_amount, status); ``` สิ่งสำคัญที่ต้องเข้าใจเกี่ยวกับ Composite Index คือ **ลำดับของคอลัมน์มีความสำคัญมาก** Index `(customer_id, order_date)` จะถูกใช้เมื่อกรองด้วย `customer_id` อย่างเดียว หรือกรองด้วยทั้ง `customer_id` และ `order_date` แต่จะไม่ถูกใช้เมื่อกรองด้วย `order_date` อย่างเดียว ให้นึกถึงหลัก "Leftmost Prefix" เสมอ ### Partial Index: Index เฉพาะข้อมูลที่ต้องการ Partial Index คือ Index ที่สร้างเฉพาะบางส่วนของตาราง โดยใช้เงื่อนไข WHERE ในการกำหนดขอบเขต วิธีนี้ทำให้ Index มีขนาดเล็กลง ใช้พื้นที่น้อยลง และค้นหาได้เร็วขึ้น ```sql -- partial_index.sql -- Only index active orders (smaller, faster index) CREATE INDEX idx_orders_active ON orders (customer_id, order_date) WHERE status = 'active'; -- Only index recent data for dashboard queries CREATE INDEX idx_orders_recent ON orders (order_date) WHERE order_date >= '2026-01-01'; ``` Partial Index เหมาะอย่างยิ่งสำหรับกรณีที่ Query ส่วนใหญ่เข้าถึงเฉพาะข้อมูลบางส่วนเท่านั้น เช่น Dashboard ที่แสดงเฉพาะข้อมูลปัจจุบัน หรือระบบที่ต้องค้นหาเฉพาะรายการที่ยังดำเนินการอยู่ > **Anti-Pattern ที่ทำให้ Index ไม่ทำงาน** > > หนึ่งในข้อผิดพลาดที่พบบ่อยที่สุดคือการใช้ฟังก์ชันกับคอลัมน์ที่มี Index ซึ่งจะทำให้ Database ไม่สามารถใช้ Index ได้เลย ต้องทำ Seq Scan แทน ตรวจสอบ Query ทั้งหมดให้แน่ใจว่าไม่มีการห่อคอลัมน์ที่มี Index ด้วยฟังก์ชัน ## Query Anti-Patterns ที่ต้องหลีกเลี่ยง ### การใช้ฟังก์ชันกับคอลัมน์ที่มี Index ข้อผิดพลาดนี้เกิดขึ้นบ่อยมากในการเขียน Query กรองข้อมูลตามวันที่ การใช้ EXTRACT หรือฟังก์ชันอื่น ๆ กับคอลัมน์ที่มี Index จะทำให้ Database ต้องคำนวณค่าของทุกแถวก่อนเปรียบเทียบ ส่งผลให้ Index ไม่ถูกใช้งาน ```sql -- anti_patterns.sql -- BAD: function on indexed column disables the index SELECT * FROM orders WHERE EXTRACT(YEAR FROM order_date) = 2026; -- GOOD: range comparison uses the index SELECT * FROM orders WHERE order_date >= '2026-01-01' AND order_date < '2027-01-01'; ``` ### กับดัก NOT IN กับ NULL อีกหนึ่ง Anti-Pattern ที่อันตรายมากคือการใช้ NOT IN กับ Subquery ที่อาจมีค่า NULL เนื่องจากการเปรียบเทียบใด ๆ กับ NULL ใน SQL จะได้ผลลัพธ์เป็น UNKNOWN ไม่ใช่ TRUE หรือ FALSE ดังนั้น หากมีค่า NULL แม้แต่ค่าเดียวใน Subquery ผลลัพธ์ของ NOT IN จะเป็นชุดว่างเสมอ ```sql -- not_in_null_trap.sql -- BAD: returns empty if any customer_id is NULL in subquery SELECT * FROM customers WHERE customer_id NOT IN ( SELECT customer_id FROM orders ); -- GOOD: NOT EXISTS handles NULLs correctly SELECT * FROM customers c WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id ); ``` ในการสัมภาษณ์ การรู้จัก Anti-Pattern เหล่านี้และอธิบายได้ว่าเหตุใดจึงเป็นปัญหาจะช่วยสร้างความน่าเชื่อถือได้อย่างมาก เพราะแสดงให้เห็นว่าผู้สมัครมีประสบการณ์จริงในการทำงานกับข้อมูล > **แนวทางการฝึกฝนสำหรับการสัมภาษณ์** > > การเตรียมตัวที่มีประสิทธิภาพที่สุดคือการฝึกเขียน Query จริงกับข้อมูลจริง ลองสร้างฐานข้อมูลทดสอบ สร้าง Index แล้วใช้ EXPLAIN ANALYZE ดูผลกระทบ การเห็นความแตกต่างของ Execution Time ด้วยตนเองจะช่วยให้เข้าใจได้ลึกซึ้งกว่าการอ่านทฤษฎีเพียงอย่างเดียว แนะนำให้เตรียมตัวในหัวข้อเหล่านี้ตามลำดับ: Subquery Patterns → Pivot Queries → EXPLAIN Plan → Indexing → Anti-Patterns ## สรุป หัวข้อ SQL ขั้นสูงที่กล่าวมาทั้งหมดล้วนเป็นทักษะสำคัญสำหรับการสัมภาษณ์ตำแหน่ง Data Analyst สามารถสรุปประเด็นหลักได้ดังนี้ - **Correlated Subquery กับ Regular Subquery** — ต้องเข้าใจความแตกต่างด้านประสิทธิภาพ และรู้วิธีเปลี่ยน Correlated Subquery เป็น CTE + JOIN เมื่อเหมาะสม - **EXISTS กับ IN** — EXISTS เหมาะสำหรับตรวจสอบการมีอยู่ของข้อมูลมากกว่า IN โดยเฉพาะกับชุดข้อมูลขนาดใหญ่ และ NOT EXISTS จัดการ NULL ได้ถูกต้องกว่า NOT IN - **Pivot Query** — เทคนิค Conditional Aggregation ด้วย CASE เป็นวิธีมาตรฐานที่ใช้ได้กับทุก Database ส่วน CROSSTAB เป็นทางเลือกเพิ่มเติมสำหรับ PostgreSQL - **EXPLAIN Plan** — ทักษะการอ่าน Execution Plan จะช่วยระบุ Bottleneck และตัดสินใจเลือกวิธีเพิ่มประสิทธิภาพได้อย่างแม่นยำ - **Indexing** — การออกแบบ Index ที่เหมาะสม ทั้ง Single Column, Composite, Covering และ Partial Index สามารถปรับปรุงประสิทธิภาพของ Query ได้อย่างมหาศาล - **Anti-Patterns** — การหลีกเลี่ยงการใช้ฟังก์ชันกับคอลัมน์ที่มี Index และการระวังกับดัก NOT IN กับ NULL เป็นความรู้ที่แสดงถึงประสบการณ์จริง การเตรียมตัวอย่างเป็นระบบในหัวข้อเหล่านี้จะช่วยเพิ่มความมั่นใจและโอกาสในการผ่านการสัมภาษณ์ตำแหน่ง Data Analyst ได้อย่างมีนัยสำคัญ --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/data-analytics/advanced-sql-data-analyst-interviews