สารบัญ
การจัดรูปแบบ/ความอ่านง่าย
- ใช้ comma นำหน้าเมื่อแยกฟิลด์
- ใช้ค่า dummy ใน
WHERE
- จัดย่อหน้าโค้ดให้เหมาะสม
- พิจารณาใช้ CTE เมื่อเขียนคิวรีที่ซับซ้อน
ความสามารถที่มีประโยชน์
- ใช้โอเปอเรเตอร์
:: เพื่อแปลงชนิดข้อมูล
- ใช้ anti join
- ใช้
QUALIFY เพื่อกรอง window function
- สามารถใช้
GROUP BY ด้วยตำแหน่งคอลัมน์ได้
ข้อควรระวังที่ควรหลีกเลี่ยง
- ระวังเมื่อใช้
NOT IN ร่วมกับค่า NULL
- เปลี่ยนชื่อฟิลด์ที่คำนวณแล้วเพื่อหลีกเลี่ยงความกำกวม
- ระบุให้ชัดว่าแต่ละคอลัมน์มาจากตารางใด
- ทำความเข้าใจลำดับการประมวลผล
- ใส่คอมเมนต์ในโค้ด
- อ่านเอกสารทั้งหมด
การจัดรูปแบบ/ความอ่านง่าย
ใช้ comma นำหน้าเมื่อแยกฟิลด์
- ใน
SELECT สามารถใช้ comma นำหน้าเพื่อให้เห็นการแยกคอลัมน์ใหม่ได้ชัดเจน
- comma นำหน้าเป็นสัญญาณทางสายตาที่ช่วยให้ตรวจพบได้ง่ายว่ามี comma หายไปหรือไม่
SELECT
employee_id,
employee_name,
job,
salary
FROM employees;
ใช้ค่า dummy ใน WHERE
- ใช้ค่า dummy ใน
WHERE เพื่อเพิ่มหรือลบเงื่อนไขแบบไดนามิกได้
SELECT *
FROM employees
WHERE 1=1 -- 더미 값
AND job IN ('Clerk', 'Manager')
AND dept_no != 5;
จัดย่อหน้าโค้ดให้เหมาะสม
- จัดย่อหน้าโค้ดเพื่อให้อ่านง่ายขึ้น และช่วยให้เพื่อนร่วมงานรวมถึงตัวเราในอนาคตเข้าใจโค้ดได้ง่าย
-- ตัวอย่างที่ไม่ดี:
SELECT
timeslot_date,
timeslot_channel,
overnight_fta_share,
IFF(DATEDIFF(DAY, timeslot_date, CURRENT_DATE()) > 7,
LAG(overnight_fta_share, 1) OVER (PARTITION BY timeslot_date, timeslot_channel ORDER BY timeslot_activity),
NULL) AS C7_fta_share,
IFF(DATEDIFF(DAY, timeslot_date, CURRENT_DATE()) >= 29,
LAG(overnight_fta_share, 2) OVER (PARTITION BY timeslot_date, timeslot_channel ORDER BY timeslot_activity),
NULL) AS C28_fta_share
FROM timeslot_data;
-- ตัวอย่างที่ดี:
SELECT
timeslot_date,
timeslot_channel,
overnight_fta_share,
IFF(DATEDIFF(DAY, timeslot_date, CURRENT_DATE()) > 7,
LAG(overnight_fta_share, 1) OVER (PARTITION BY timeslot_date, timeslot_channel ORDER BY timeslot_activity),
NULL) AS C7_fta_share,
IFF(DATEDIFF(DAY, timeslot_date, CURRENT_DATE()) >= 29,
LAG(overnight_fta_share, 2) OVER (PARTITION BY timeslot_date, timeslot_channel ORDER BY timeslot_activity),
NULL) AS C28_fta_share
FROM timeslot_data;
พิจารณาใช้ CTE เมื่อเขียนคิวรีที่ซับซ้อน
- แทนที่จะซ้อน inline view หลายชั้น สามารถใช้ Common Table Expression (CTE) เพื่อเพิ่มความอ่านง่ายและความเป็นระเบียบของโค้ดได้
-- ใช้ inline view:
SELECT
vhs.movie,
vhs.vhs_revenue,
cs.cinema_revenue
FROM
(SELECT
movie_id,
SUM(ticket_sales) AS cinema_revenue
FROM tickets
GROUP BY movie_id) AS cs
INNER JOIN
(SELECT
movie,
movie_id,
SUM(revenue) AS vhs_revenue
FROM blockbuster
GROUP BY movie, movie_id) AS vhs
ON cs.movie_id = vhs.movie_id;
-- ใช้ CTE:
WITH cinema_sales AS (
SELECT
movie_id,
SUM(ticket_sales) AS cinema_revenue
FROM tickets
GROUP BY movie_id
),
vhs_sales AS (
SELECT
movie,
movie_id,
SUM(revenue) AS vhs_revenue
FROM blockbuster
GROUP BY movie, movie_id
)
SELECT
vhs.movie,
vhs.vhs_revenue,
cs.cinema_revenue
FROM cinema_sales AS cs
INNER JOIN vhs_sales AS vhs
ON cs.movie_id = vhs.movie_id;
ความสามารถที่มีประโยชน์
ใช้โอเปอเรเตอร์ :: เพื่อแปลงชนิดข้อมูล
- ใน RDBMS บางตัว สามารถใช้โอเปอเรเตอร์
:: เพื่อแปลงค่าไปเป็นชนิดข้อมูลอื่นได้
SELECT CAST('5' AS INTEGER); -- ใช้ฟังก์ชัน CAST
SELECT '5'::INTEGER; -- ใช้ไวยากรณ์ ::
ใช้ anti join
- anti join มีประโยชน์มากเมื่อเราต้องการคืนค่าเฉพาะแถวที่มีอยู่ในตารางหนึ่งแต่ไม่มีในอีกตารางหนึ่ง
- แม้จะใช้ subquery ได้เช่นกัน แต่โดยทั่วไป anti join มักเร็วกว่า
-- anti join:
SELECT
video_content.*
FROM video_content
LEFT JOIN archive
ON video_content.series_id = archive.series_id
WHERE archive.series_id IS NULL;
-- subquery:
SELECT
*
FROM video_content
WHERE series_id NOT IN (SELECT DISTINCT series_id FROM archive);
-- correlated subquery:
SELECT
*
FROM video_content
WHERE NOT EXISTS (
SELECT 1
FROM archive a
WHERE a.series_id = vc.series_id
);
-- EXCEPT:
SELECT series_id
FROM video_content
EXCEPT
SELECT series_id
FROM archive;
ใช้ QUALIFY เพื่อกรอง window function
- สามารถใช้
QUALIFY เพื่อกรองผลลัพธ์ของ window function ได้
- มีประโยชน์ในการลดจำนวนบรรทัดของโค้ด
-- ใช้ QUALIFY:
SELECT
product,
market,
SUM(revenue) AS market_revenue
FROM sales
GROUP BY product, market
QUALIFY DENSE_RANK() OVER (PARTITION BY product ORDER BY SUM(revenue) DESC) <= 10
ORDER BY product, market_revenue;
-- ไม่ใช้ QUALIFY:
SELECT
product,
market,
market_revenue
FROM (
SELECT
product,
market,
SUM(revenue) AS market_revenue,
DENSE_RANK() OVER (PARTITION BY product ORDER BY SUM(revenue) DESC) AS market_rank
FROM sales
GROUP BY product, market
)
WHERE market_rank <= 10
ORDER BY product, market_revenue;
สามารถใช้ GROUP BY ด้วยตำแหน่งคอลัมน์ได้
- สามารถใช้ตำแหน่งคอลัมน์แทนชื่อคอลัมน์ใน
GROUP BY หรือ ORDER BY ได้
- มีประโยชน์กับคิวรีชั่วคราว แต่ในโค้ด production ควรอ้างอิงชื่อคอลัมน์เสมอ
SELECT
dept_no,
SUM(salary) AS dept_salary
FROM employees
GROUP BY 1 -- dept_no คือคอลัมน์แรกใน SELECT
ORDER BY 2 DESC;
ข้อควรระวังที่ควรหลีกเลี่ยง
ระวังเมื่อใช้ NOT IN ร่วมกับค่า NULL
NOT IN จะไม่ทำงานตามคาดเมื่อมีค่า NULL
- ควรใช้
NOT EXISTS แทน
INSERT INTO departments (id)
VALUES (1), (2), (NULL);
-- ไม่ทำงานเพราะมีค่า NULL
SELECT *
FROM employees
WHERE department_id NOT IN (SELECT DISTINCT id FROM departments);
-- วิธีแก้
SELECT *
FROM employees e
WHERE NOT EXISTS (
SELECT 1
FROM departments d
WHERE d.id = e.department_id
);
เปลี่ยนชื่อฟิลด์ที่คำนวณแล้วเพื่อหลีกเลี่ยงความกำกวม
- หากตั้งชื่อฟิลด์ที่คำนวณแล้วซ้ำกับชื่อคอลัมน์เดิม อาจทำให้เกิดพฤติกรรมที่ไม่คาดคิด
INSERT INTO products (product, revenue)
VALUES ('Shark', 100), ('Robot', 150), ('Alien', 90);
-- window function จัดอันดับสินค้า 'Robot' เป็นอันดับ 1
SELECT
product,
CASE product WHEN 'Robot' THEN 0 ELSE revenue END AS revenue,
RANK() OVER (ORDER BY revenue DESC)
FROM products;
ระบุให้ชัดว่าแต่ละคอลัมน์มาจากตารางใด
- ในคิวรีที่ซับซ้อน การระบุให้ชัดว่าแต่ละคอลัมน์มาจากตารางใดจะช่วยให้ตามหาปัญหาได้ง่ายขึ้น
SELECT
vc.video_id,
vc.series_name,
metadata.season,
metadata.episode_number
FROM video_content AS vc
INNER JOIN video_metadata AS metadata
ON vc.video_id = metadata.video_id;
ทำความเข้าใจลำดับการประมวลผล
- สำหรับผู้ที่กำลังเรียน SQL คำแนะนำที่สำคัญที่สุดคือการเข้าใจลำดับการประมวลผล
- เมื่อเข้าใจลำดับนี้แล้ว วิธีเขียนคิวรีจะเปลี่ยนไปอย่างสิ้นเชิง
ใส่คอมเมนต์ในโค้ด
- เวลาที่เขียนโค้ด ควรใส่คอมเมนต์อธิบายเหตุผลไว้ด้วย
- เพื่อนร่วมงานและตัวเราในอนาคตจะขอบคุณมาก
SELECT
video_content.*
FROM video_content
LEFT JOIN archive -- 새로운 CMS는 아카이브 비디오 형식을 처리할 수 없음
ON video_content.series_id = archive.series_id
WHERE archive.series_id IS NULL;
อ่านเอกสารทั้งหมด
- การอ่านเอกสารอย่างครบถ้วนช่วยให้หลีกเลี่ยงปัญหาที่คาดไม่ถึงได้
- ใช้เวลาเพียงไม่กี่นาที แต่ช่วยแก้ปัญหาที่ไม่คาดคิดได้มาก
-- หากอ่านเอกสารมากกว่านี้ก็จะแก้ปัญหา NULL ได้
SELECT COALESCE(GREATEST(signup_date, consumption_date), signup_date, consumption_date);
-- สามารถใช้ฟังก์ชัน GREATEST_IGNORE_NULLS ได้
SELECT GREATEST_IGNORE_NULLS(signup_date, consumption_date);
สรุปของ GN⁺
- บทความนี้รวบรวมเคล็ดลับและเทคนิคหลากหลายอย่างสำหรับการเขียน SQL ให้มีประสิทธิภาพและอ่านง่ายขึ้น
- ไม่ว่าจะเป็นผู้เริ่มต้นเรียน SQL หรือ data analyst ที่มีประสบการณ์ ก็มีข้อมูลที่เป็นประโยชน์อยู่มาก
- โดยเฉพาะเรื่องการใช้ CTE กับคิวรีซับซ้อน การใช้ anti join และการใช้
QUALIFY ล้วนช่วยงานจริงได้มาก
- การทำความเข้าใจลำดับการประมวลผลของ SQL การใส่คอมเมนต์ในโค้ด และการอ่านเอกสารอย่างละเอียด เป็นนิสัยที่สำคัญ
- เครื่องมืออื่นที่มีความสามารถใกล้เคียงกัน เช่น PostgreSQL, MySQL, Oracle
2 ความคิดเห็น
ในโพสต์นี้ เครื่องหมายจุลภาคนำหน้าทั้งหมดถูกเขียนเป็นเครื่องหมายจุลภาคตามหลังนะครับ ในต้นฉบับใส่ไว้เป็นแบบนำหน้า
ความเห็นบน Hacker News
ความจำเป็นในการเข้าใจเซิร์ฟเวอร์ DB ให้ดีและตรวจสอบ query plan อยู่เสมอ
EXISTSมักเร็วกว่าINNOT EXISTSและEXCEPTทำงานต่างกันในการจัดการค่าNULLUNION ALLอาจเร็วกว่าORJOINเคล็ดลับสำหรับงาน stored procedure ที่ซับซ้อน
ความเห็นเกี่ยวกับความอ่านง่ายของโค้ด
ข้อเสนอให้ใช้ไวยากรณ์แบบ FROM-first และ piping ของ SQL
เคล็ดลับเกี่ยวกับ Anti Join
EXISTSเพราะได้เปรียบเมื่อใช้ subquery ตามเงื่อนไขเพื่อตรวจสอบการมีอยู่ของแถวข้อดีของการใช้ comma นำหน้าในคำสั่ง
SELECTใน MSSQL แนะนำให้ใช้
/* */แทน--เมื่อต้องใส่คอมเมนต์แนะนำให้ใช้ window function
ประเด็นถกเถียงเรื่องการใช้
1=1ในWHEREclauseแนะนำ AI2sql