13. INSERT, UPDATE และ DELETE

วัตถุประสงค์การเรียนรู้

เมื่อเรียนจบบทนี้ คุณจะสามารถ

  1. เพิ่มระเบียนสินค้าใหม่ลงในตาราง menus โดยใช้คำสั่ง INSERT แบบแถวเดียวและหลายแถว
  2. เติมข้อมูลในตารางจาก query ด้วย INSERT … SELECT สำหรับการดำเนินการจำนวนมากและการ archive ข้อมูล
  3. แก้ไขระเบียนที่มีอยู่อย่างแม่นยำด้วย UPDATE … SET … WHERE รวมถึง UPDATE แบบมีเงื่อนไขโดยใช้ CASE
  4. ลบแถวออกถาวรด้วย DELETE … WHERE และอธิบายได้ว่าเมื่อใดควรใช้ DELETE เมื่อใดควรใช้รูปแบบ soft-delete แทน

บทนำ

ฐานข้อมูลเป็นองค์ประกอบสำคัญของการดำเนินธุรกิจ เพราะต้องรองรับทั้งการเติบโตของข้อมูล การเปลี่ยนแปลงของรายการธุรกรรม และการกำจัดข้อมูลที่ล้าสมัยอย่างเป็นระบบ บทนี้จึงนำเสนอคำสั่งในกลุ่ม Data Manipulation Language (DML) ซึ่งใช้ในการเพิ่ม แก้ไข และลบข้อมูล เพื่อให้ข้อมูลในฐานข้อมูลถูกต้องและทันสมัยอยู่เสมอ ได้แก่ INSERT, UPDATE และ DELETE

จนถึงจุดนี้ คำสั่ง SQL ที่ปรากฏในตำราเล่มนี้ยังจำกัดอยู่ที่ SELECT ซึ่งใช้เพื่อการสืบค้นข้อมูลเป็นหลัก แต่ฐานข้อมูลในองค์กรธุรกิจจริงไม่ได้มีหน้าที่เพียงเก็บและแสดงผลข้อมูลเท่านั้น หากยังต้องรองรับการเพิ่ม การปรับปรุง และการยกเลิกรายการข้อมูลอย่างต่อเนื่อง ตัวอย่างเช่น เมื่อมีการพัฒนาเครื่องดื่มใหม่อย่าง Lychee Rose Milk Tea สำหรับโปรโมชันเทศกาลสงกรานต์ ธุรกิจก็ต้องใช้ INSERT เพื่อบันทึกเมนูใหม่ลงในตาราง menus หากต้นทุนวัตถุดิบเพิ่มขึ้นและมีนโยบายปรับราคาสินค้ากลุ่ม seasonal ขึ้น 5% ก็ต้องใช้ UPDATE เพื่อแก้ไขราคาที่เกี่ยวข้องทั้งหมด และเมื่อ Osmanthus Milk Tea ถูกยกเลิกหลังสัปดาห์ที่ 4 ก็ต้องตัดสินใจให้เหมาะสมว่าจะใช้ DELETE เพื่อลบข้อมูลออก หรือจะคงข้อมูลไว้ในสถานะปิดใช้งานเพื่อประโยชน์ด้านการควบคุมและการตรวจสอบย้อนหลัง

INSERT

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

ก่อนที่จะ INSERT ควรตรวจสอบสถานะปัจจุบันของตารางก่อนเสมอ โดยตรวจสอบจำนวนแถวข้อมูลในตาราง menus ก่อน INSERT

-- มี SKU กี่รายการในวันนี้?
SELECT 
 COUNT(*) AS total_skus,
 MAX(sku_id) AS highest_sku_id
FROM menus;
total_skus highest_sku_id
22 22

sku_id ที่ว่างถัดไปคือ 23 ตอนนี้ INSERT เครื่องดื่มใหม่

INSERT แบบแถวเดียว

คำสั่ง INSERT มีไวยากรณ์พื้นฐานดังนี้

INSERT INTO table_name (col1, col2, col3, ...)
VALUES (val1, val2, val3, ...);

คุณไม่จำเป็นต้องระบุทุกคอลัมน์ แต่ควรระบุชื่อคอลัมน์ที่ให้ค่าอย่างชัดเจนเสมอ คอลัมน์ที่ไม่ระบุจะได้ค่า DEFAULT (หรือ NULL หากไม่กำหนด default) การระบุ column list ทำให้โค้ดปลอดภัยต่อการเปลี่ยนแปลง schema ในอนาคต

หากทีมผลิตภัณฑ์ของ PiCha Tea House ได้อนุมัติเครื่องดื่ม seasonal ใหม่สำหรับเทศกาลสงกรานต์ ได้แก่ Lychee Rose Milk Tea (ขนาด Large), ราคา 145 บาท เป็นสินค้าในหมวด milk_tea ขนาด Large (700 มล.) คาเฟอีนระดับกลาง เป็นสินค้า seasonal และเปิดตัวเฉพาะที่สาขา ICONSIAM

INSERT INTO menus (
 sku_id, sku_name, family, tea_base,
 milk_flag, fruit_flag, cold_brew_flag, blended_flag,
 size_code, size_ml, list_price_thb,
 sweetness_min_pct, sweetness_max_pct,
 caffeine_level, seasonal_flag, signature_flag, launch_branch_flag
) VALUES (
 23, 'Lychee Rose Milk Tea L', 'milk_tea', 'lychee_rose', -- sku_id, sku_name, family, tea_base,
 1, 0, 0, 0,        -- milk: ใช่, fruit/cold-brew/blended: ไม่ใช่
 'L', 700, 145,     -- ขนาด large, 700 มล., 145 บาท
 0, 100,            -- รองรับความหวาน 0–100%
 'medium', 1, 0, 1  -- คาเฟอีนปานกลาง, seasonal, ไม่ใช่ signature, เปิดตัวที่ flagship
);
Note

การระบุชื่อคอลัมน์ในคำสั่ง INSERT INTO มีบทบาทสำคัญในการกำหนดให้ค่าข้อมูลแต่ละค่าถูกจับคู่กับคอลัมน์ปลายทางได้อย่างชัดเจน ช่วยลดความเสี่ยงของความผิดพลาดที่อาจเกิดจากการอาศัยลำดับคอลัมน์เพียงอย่างเดียว และยังเปิดโอกาสให้สามารถละเว้นคอลัมน์ที่มีค่าเริ่มต้น หรือคอลัมน์ที่ระบบสร้างขึ้นโดยอัตโนมัติได้

SELECT เพื่อยืนยันหลัง INSERT

SELECT sku_id, sku_name, family, size_code, list_price_thb,
       seasonal_flag, signature_flag
FROM menus
WHERE sku_id = 23;
sku_id sku_name family size_code list_price_thb seasonal_flag signature_flag
23 Lychee Rose Milk Tea L milk_tea L 145 1 0

จะเห็นได้ว่ามีแถวข้อมูลเมนูใหม่ Lychee Rose Milk Tea L เพิ่มเข้าในตาราง menus

SELECT COUNT(*) AS total_skus FROM menus;
total_skus
23

เมื่อนับจำนวนเมนูทั้งหมดจะอยู่ที่ 23 เมนู

INSERT แบบหลายแถว

การเพิ่มหลายแถวพร้อมกันทำได้โดยระบุ column list ครั้งเดียวใน INSERT INTO แล้วตามด้วย VALUES ที่มีหลายชุดคั่นด้วย , โดยแต่ละชุดค่าอยู่ในวงเล็บ (...) แยกกัน เช่น

INSERT INTO menus (col1, col2, ...)   -- ระบุ column ครั้งเดียว
VALUES
 (ค่าแถวที่ 1a, ค่าแถวที่ 1b, ...),  -- แถวที่ 1 (จบแต่ละแถวข้อมูลที่เพิ่มด้วย ,)
 (ค่าแถวที่ 2a, ค่าแถวที่ 2b, ...),  -- แถวที่ 2 (จบแต่ละแถวข้อมูลที่เพิ่มด้วย ,)
 (ค่าแถวที่ 3a, ค่าแถวที่ 3b, ...);  -- แถวที่ 3 (ปิดด้วย ;)

หากทีมผลิตภัณฑ์ได้อนุมัติเครื่องดื่มเพิ่มเติมอีกสองรายการสำหรับการเปิดตัวในฤดูกาลถัดไป ได้แก่ Strawberry Matcha Latte (Large, 155 บาท) และ Cold Brew Jasmine Pure Tea (Large, 120 บาท) แทนที่จะรันคำสั่ง INSERT แยกกันสองครั้ง เราสามารถเขียนคำสั่ง INSERT เดียวที่เพิ่มข้อมูลหลายแถวพร้อมกันได้

-- เพิ่มเมนูใหม่ 2 รายการ: Strawberry Matcha Latte L และ Cold Brew Jasmine Pure Tea L
INSERT INTO menus (
    sku_id, sku_name, family, tea_base,
    milk_flag, fruit_flag, cold_brew_flag, blended_flag,
    size_code, size_ml, list_price_thb,
    sweetness_min_pct, sweetness_max_pct,
    caffeine_level, seasonal_flag, signature_flag, launch_branch_flag
) VALUES
    (24, 'Strawberry Matcha Latte L', 'milk_tea', 'matcha',
     1, 1, 0, 0, 'L', 700, 155, 0, 100, 'high', 1, 0, 0),
    (25, 'Cold Brew Jasmine Pure Tea L', 'pure_tea', 'jasmine',
     0, 0, 1, 0, 'L', 700, 120, 0, 75, 'medium', 0, 0, 0);

ตรวจสอบรายการที่เพิ่ม:

SELECT sku_id, sku_name, family, size_code, list_price_thb, seasonal_flag
FROM menus
WHERE sku_id IN (24, 25);
sku_id sku_name family size_code list_price_thb seasonal_flag
24 Strawberry Matcha Latte L milk_tea L 155 1
25 Cold Brew Jasmine Pure Tea L pure_tea L 120 0
SELECT COUNT(*) AS total_skus FROM menus;
total_skus
25

INSERT … SELECT

INSERT ... SELECT เป็นรูปแบบที่ขยายความสามารถของคำสั่ง INSERT โดยเปิดให้ผู้ใช้เพิ่มข้อมูลลงในตารางเป้าหมายจากผลลัพธ์ของ query SELECT ได้โดยตรง แทนที่จะป้อนข้อมูลทีละแถว วิธีนี้มีประโยชน์อย่างยิ่งในงานจัดการข้อมูลระดับธุรกิจ เช่น การจัดเก็บข้อมูลย้อนหลังใน archive table การสร้าง summary table เพื่อการวิเคราะห์ และการถ่ายโอนข้อมูลระหว่างตาราง

ไวยากรณ์ ชอง INSERT ... SELECT

INSERT INTO target_table (col1, col2, ...)
SELECT col1, col2, ...
FROM source_table
WHERE condition;

ในการใช้คำสั่ง INSERT ... SELECT รายการคอลัมน์ของตารางเป้าหมายต้องตรงกับคอลัมน์ที่ SELECT ส่งคืนทั้งจำนวนและลำดับ เพื่อให้การนำข้อมูลเข้าสู่ตารางเป็นไปอย่างถูกต้อง ทั้งนี้ หากตารางเป้าหมายถูกสร้างจาก CREATE TABLE AS SELECT ... ซึ่งทำให้โครงสร้างคอลัมน์สอดคล้องกับผลลัพธ์ต้นทางอยู่แล้ว ผู้ใช้ก็สามารถละเว้นการระบุรายการคอลัมน์ในฝั่ง INSERT ได้

ตัวอย่างการใช้งาน INSERT ... SELECT ก่อนยุติการจำหน่ายเครื่องดื่ม seasonal เมื่อสิ้นฤดูกาล ทีม data ของ PiCha จะย้าย SKU ที่เกี่ยวข้องไปจัดเก็บในตาราง menus_archive เพื่อคงไว้ซึ่งข้อมูลสำหรับการรายงานย้อนหลังและการวิเคราะห์ในอนาคต

ขั้นตอนที่ 1 สร้าง archive table (DDL):

-- สร้างตาราง archive สำหรับเก็บประวัติเมนูที่ถูกยกเลิก โดยคัดลอก schema จาก menus
CREATE TABLE menus_archive AS
SELECT
 *, -- คัดลอกทุก column จากตาราง menus
 CURRENT_DATE AS archived_date -- เพิ่ม column วันที่เก็บ archive
FROM menus
WHERE 1=0; -- คัดลอกเฉพาะ schema ไม่มีแถวข้อมูล (เงื่อนไขเป็นเท็จเสมอ)

ขั้นตอนที่ 2 Archive สินค้า seasonal (INSERT … SELECT):

INSERT INTO menus_archive
SELECT *, CURRENT_DATE AS archived_date
FROM menus
WHERE seasonal_flag = 1;

ยืนยัน archive:

SELECT sku_id, sku_name, family, list_price_thb, seasonal_flag, archived_date
FROM menus_archive;
sku_id sku_name family list_price_thb seasonal_flag archived_date
18 Fresh Orange Jasmine Tea L fruit_tea 150 1 2026-04-26
21 Mango Sticky Rice Tea L fruit_tea 135 1 2026-04-26
22 Thai Tea Frappe L blended 125 1 2026-04-26

เมื่อเข้าใจการเพิ่มแถวใหม่ด้วย INSERT แล้ว ขั้นต่อไปคือการเปลี่ยนข้อมูลในแถวที่มีอยู่ด้วย UPDATE ซึ่งเทียบได้กับการเดินถือปืนยิงป้ายราคาไปตามเมนูที่มีอยู่เดิม

UPDATE

UPDATE เป็นคำสั่งในกลุ่ม DML ที่ใช้สำหรับแก้ไขข้อมูลในแถวที่มีอยู่แล้วภายในตาราง โดยผู้ใช้สามารถเปลี่ยนแปลงค่าของคอลัมน์หนึ่งคอลัมน์หรือหลายคอลัมน์พร้อมกันได้ตามเงื่อนไขที่กำหนด คำสั่งนี้จึงมีบทบาทสำคัญในการทำให้ฐานข้อมูลสะท้อนสภาพการดำเนินงานที่เป็นปัจจุบันอยู่เสมอ

ไวยากรณ์ของคำสั่ง UPDATE แบบทั่วไป

UPDATE table_name
SET column1 = new_value1,
    column2 = new_value2
WHERE condition;

คุณสามารถอัปเดตหลายคอลัมน์ในคำสั่งเดียว โดยระบุใน SET คั่นด้วยจุลภาค หากไม่มี WHERE ทุกแถวในตารางจะถูกอัปเดต ซึ่งในระบบ production มักเป็นสิ่งที่ไม่ต้องการ

ในบริบทธุรกิจ UPDATE มักถูกใช้เมื่อข้อมูลเดิมยังคงมีอยู่ แต่รายละเอียดบางส่วนต้องได้รับการปรับปรุง เช่น การขึ้นราคาสินค้า การเปลี่ยนสถานะคำสั่งซื้อ การแก้ไขข้อมูลลูกค้า หรือการปรับหมวดหมู่ของสินค้าใหม่ การใช้ UPDATE อย่างถูกต้องช่วยให้ธุรกิจรักษาความถูกต้อง ความสอดคล้อง และความทันสมัยของข้อมูลได้ดียิ่งขึ้น

อีกประเด็นที่สำคัญคือ คำสั่ง UPDATE มักทำงานร่วมกับ WHERE เพื่อระบุอย่างชัดเจนว่าต้องการแก้ไขแถวใดบ้าง หากละเว้น WHERE ระบบจะปรับค่าทุกแถวในตารางทันที ซึ่งอาจก่อให้เกิดผลกระทบอย่างมากในเชิงปฏิบัติ ดังนั้นการใช้ UPDATE จึงต้องอาศัยความระมัดระวังและการตรวจสอบเงื่อนไขก่อนดำเนินการเสมอ

ตัวอย่างนี้สาธิตการใช้คำสั่ง UPDATE เพื่อปรับราคาสินค้า โดยสมมติสถานการณ์ว่าทีม supply chain รายงานต่อทีม finance ว่าต้นทุนวัตถุดิบของสินค้ากลุ่ม seasonal เพิ่มขึ้น 5% ในไตรมาสปัจจุบัน ภายใต้นโยบายการตั้งราคาของบริษัท ต้นทุนที่เพิ่มขึ้นนี้จะถูกส่งผ่านไปยังราคาขายของเมนู seasonal ทั้งหมด

ขั้นตอนที่ 1 ยืนยันแถวเป้าหมายด้วย SELECT

SELECT sku_id, sku_name, list_price_thb, seasonal_flag
FROM menus
WHERE seasonal_flag = 1
ORDER BY sku_id;
sku_id sku_name list_price_thb seasonal_flag
18 Fresh Orange Jasmine Tea L 150 1
21 Mango Sticky Rice Tea L 135 1
22 Thai Tea Frappe L 125 1

สามแถวนี้คือแถวที่ต้องปรับราคาขึ้น

ทำการ UPDATE list_price_thb เพิ่มขึ้น 5%

-- ปรับขึ้นราคาเมนู seasonal ทุกรายการ 5%
UPDATE menus
SET list_price_thb = CAST(ROUND(list_price_thb * 1.05) AS INTEGER) -- คูณราคาเดิมด้วย 1.05 แล้วปัดเป็นจำนวนเต็ม
WHERE seasonal_flag = 1; -- อัปเดตเฉพาะเมนูที่เป็น seasonal

ตรวจสอบราคาหลังการ UPDATE

SELECT sku_id, sku_name, list_price_thb, seasonal_flag
FROM menus
WHERE seasonal_flag = 1
ORDER BY sku_id;
sku_id sku_name list_price_thb seasonal_flag
18 Fresh Orange Jasmine Tea L 158 1
21 Mango Sticky Rice Tea L 142 1
22 Thai Tea Frappe L 131 1

ลองดูอีกตัวอย่างของการ UPDATE โดยมีสถานการณ์ว่า จากการวิเคราะห์ข้อมูลย้อนหลัง 60 วันที่ผ่านมาแสดงให้เห็นว่า Pu’er Milk Tea (Regular และ Large) มีแนวโน้มสูงขึ้นทั้งในคะแนนความพึงพอใจของลูกค้าและความถี่ของออเดอร์ ทีม marketing ต้องการเพิ่มทั้งสองขนาดในรายการเครื่องดื่ม signature

ตรวจสอบก่อน UPDATE

SELECT sku_id, sku_name, signature_flag
FROM menus
WHERE sku_name ILIKE 'Pu''er%'; -- เนื่องจากชื่อ Pu'er มีเครื่องหมาย ' อยู่ จึงต้องใส่ ' ติดกัน 2 ครั้ง
sku_id sku_name signature_flag
7 Pu’er Milk Tea R 0
8 Pu’er Milk Tea L 0

ทั้งสองแถวมี signature_flag = 0 ดำเนินการยกระดับ

UPDATE เพื่อยกระดับสถานะ signature

UPDATE menus
SET signature_flag = 1
WHERE sku_name ILIKE 'Pu''er%'; -- เนื่องจากชื่อ Pu'er มีเครื่องหมาย ' อยู่ จึงต้องใส่ ' ติดกัน 2 ครั้ง

ยืนยันหลัง UPDATE

SELECT sku_id, sku_name, signature_flag
FROM menus
WHERE sku_name ILIKE 'Pu''er%'; -- เนื่องจากชื่อ Pu'er มีเครื่องหมาย ' อยู่ จึงต้องใส่ ' ติดกัน 2 ครั้ง
sku_id sku_name signature_flag
7 Pu’er Milk Tea R 1
8 Pu’er Milk Tea L 1

ทางทีมการตลาดแจ้งว่า เครื่องดื่มประเภท blended ต้องมีการบวก premium 10 บาท และ pure tea ต้องมีการบวก premium 5 บาท ขณะที่หมวดสินค้าอื่นไม่ต้องมีการเปลี่ยนแปลงราคา ในสถานการณ์เช่นนี้ แทนที่จะดำเนินการ UPDATE หลายครั้งแยกตามประเภทสินค้า ผู้ใช้สามารถอาศัย CASE expression ภายใน SET clause เพื่อจัดการเงื่อนไขทั้งหมดได้ใน statement เดียว

เริ่มจากการหาค่าเฉลี่ยของ family ก่อนทำการปรับราคาด้วย UPDATE

SELECT family,
       COUNT(*) AS skus,
       ROUND(AVG(list_price_thb), 1) AS avg_price
FROM menus
GROUP BY family
ORDER BY family;
family skus avg_price
blended 1 131.0
fruit_tea 5 121.0
milk_tea 14 107.5
pure_tea 2 97.5

ใช้ UPDATE แบบมีเงื่อนไขด้วย CASE

-- ปรับราคาแยกตามกลุ่มสินค้า: blended +10 บาท, pure_tea +5 บาท, กลุ่มอื่นคงเดิม
UPDATE menus
SET list_price_thb = CASE
    WHEN family = 'blended'  THEN list_price_thb + 10 -- เพิ่ม 10 บาทสำหรับเมนูปั่น
    WHEN family = 'pure_tea' THEN list_price_thb + 5  -- เพิ่ม 5 บาทสำหรับชาไม่มีนม
    ELSE list_price_thb                               -- milk_tea และ fruit_tea ไม่เปลี่ยนแปลง
END;

ค่าเฉลี่ยของ family หลัง UPDATE

SELECT family,
       COUNT(*) AS skus,
       ROUND(AVG(list_price_thb), 1) AS avg_price_after
FROM menus
GROUP BY family
ORDER BY family;
family skus avg_price_after
blended 1 141.0
fruit_tea 5 121.0
milk_tea 14 107.5
pure_tea 2 102.5

เมื่อเข้าใจการเพิ่มและเปลี่ยนแปลงข้อมูลด้วย INSERT และ UPDATE แล้ว คำสั่ง DML ตัวสุดท้ายคือ DELETE ซึ่งเกี่ยวข้องกับการนำแถวออกจากตาราง ทั้งในรูปแบบ hard delete และ soft delete ผ่าน flag

DELETE

DELETE ใช้สำหรับลบแถวข้อมูลออกจากตารางโดยตรง และเมื่อ transaction ถูก commit แล้ว ข้อมูลดังกล่าวจะถูกลบออกจากตารางอย่างถาวร ซึ่งเรียกว่า Hard Delete การลบลักษณะนี้มีความเสี่ยงสูงกว่าการแก้ไขข้อมูลทั่วไป เพราะโดยปกติไม่มีกลไกสำหรับย้อนคืนได้โดย่ง่าย ไม่มีปุ่ม Undo ให้กด ด้วยเหตุนี้ ระบบฐานข้อมูลในสภาพแวดล้อม production จำนวนมากจึงเลือกใช้แนวทาง Soft Delete แทนการลบจริง โดยเปลี่ยนสถานะของแถวผ่านคอลัมน์ เช่น active_flag จาก 1 เป็น 0 วิธีนี้ทำให้ข้อมูลยังคงอยู่ในตาราง แต่ถูกแยกออกจากการใช้งานตามปกติผ่านเงื่อนไขใน query

ในเชิงการจัดการข้อมูล Hard Delete เหมาะสำหรับกรณีที่ข้อมูลหมดความสำคัญ ไม่มีความจำเป็นในเชิงประวัติ และไม่เกี่ยวข้องกับความสัมพันธ์เชิงอ้างอิงของตารางอื่น ขณะที่ Soft Delete เหมาะกับสถานการณ์ที่ข้อมูลยังมีคุณค่าในด้านการตรวจสอบย้อนหลัง การอ้างอิงกับธุรกรรมเดิม หรือการกลับมาเปิดใช้งานในภายหลัง ดังนั้น การตัดสินใจเลือกระหว่างสองแนวทางนี้จึงไม่ใช่เพียงประเด็นทางเทคนิค แต่ยังเป็นประเด็นด้านการควบคุมภายใน ความเสี่ยง และการกำกับดูแลข้อมูลด้วย

ขอเริ่มจากตัวอย่างการทำ Soft Delete โดยสมมติสถานการณ์ว่า ทีมผลิตภัณฑ์ยืนยันว่า Osmanthus Milk Tea (SKU 9 และ 10) ถูกยกเลิกหลังสัปดาห์ที่ 4 ก่อนตัดสินใจลบหรือปิดการใช้งาน ให้ตรวจสอบว่ามีออเดอร์ประวัติที่อ้างอิง SKU เหล่านี้หรือไม่

การตรวจสอบข้อมูลก่อน DELETE

SELECT oi.sku_id,
       m.sku_name,
       COUNT(*) AS order_lines
FROM order_items oi
JOIN menus m ON oi.sku_id = m.sku_id
WHERE oi.sku_id IN (9, 10)
GROUP BY oi.sku_id, m.sku_name;
sku_id sku_name order_lines
9 Osmanthus Milk Tea R 1383
10 Osmanthus Milk Tea L 3432

สอง SKU ของ Osmanthus ปรากฏใน 4,815 order lines ประวัติ ถ้า hard-delete SKU 9 และ 10 ออกจาก menus แถว order_items ทั้งหมดนี้จะอ้างอิง sku_id ที่ไม่มีอยู่ รายงานรายได้ การวิเคราะห์ความสามารถในการทำกำไร และการแจกแจง product family จะเพี้ยนโดยเงียบ ๆ

ทำ soft delete ผ่าน active_flag

แทนที่จะลบแถวออก ให้เพิ่มคอลัมน์ active_flag ในตาราง menus และทำเครื่องหมาย SKU ที่ถูกยกเลิกว่าไม่ active

-- ขั้นตอนที่ 1: เพิ่ม flag column (DDL ทำครั้งเดียว)
ALTER TABLE menus ADD COLUMN active_flag INTEGER DEFAULT 1;

-- ขั้นตอนที่ 2: soft-delete SKU Osmanthus
UPDATE menus
SET active_flag = 0
WHERE sku_id IN (9, 10);

ยืนยันผลลัพธ์ soft delete:

SELECT active_flag,
       COUNT(*) AS sku_count
FROM menus
GROUP BY active_flag
ORDER BY active_flag DESC;
active_flag sku_count
1 20
0 2

แสดงเมนูที่ถูกยกเลิกจำนวน 2 เมนู WHERE active_flag = 0

SELECT sku_id, sku_name, list_price_thb, active_flag
FROM menus
WHERE active_flag = 0
ORDER BY family, list_price_thb;

แสดงเมนูที่ถูกยกเลิกจำนวน 2 เมนู

sku_id sku_name list_price_thb active_flag
9 Osmanthus Milk Tea R 95 0
10 Osmanthus Milk Tea L 115 0

แสดงเมนูที่ถูกยกเลิกจำนวน 2 เมนู WHERE active_flag = 1

SELECT sku_id, sku_name, list_price_thb, active_flag
FROM menus
WHERE active_flag = 1
ORDER BY family, list_price_thb;
sku_id sku_name list_price_thb active_flag
22 Thai Tea Frappe L 125 1
17 Peach Jasmine Tea L 95 1
15 Jasmine Grapefruit Tea L 105 1
16 Lemon Glutinous Green Tea L 105 1
21 Mango Sticky Rice Tea L 135 1
18 Fresh Orange Jasmine Tea L 150 1
1 Glutinous Green Milk Tea R 85 1
7 Pu’er Milk Tea R 95 1
11 Jasmine Milk Tea R 95 1
13 White Peach Milk Tea R 95 1
2 Glutinous Green Milk Tea L 100 1
3 Tie Guan Yin Milk Tea R 110 1
5 Da Hong Pao Milk Tea R 110 1
8 Pu’er Milk Tea L 115 1
12 Jasmine Milk Tea L 115 1
14 White Peach Milk Tea L 115 1
4 Tie Guan Yin Milk Tea L 130 1
6 Da Hong Pao Milk Tea L 130 1
20 Glutinous Green Pure Tea R 85 1
19 Tie Guan Yin Pure Tea R 110 1

วิธีนี้ทำให้ 20 SKU ที่ยัง active ปรากฏในรายงานการดำเนินงานทั้งหมด ในขณะที่แถว Osmanthus ที่ถูกยกเลิกยังคงอยู่ในตารางสำหรับ query ประวัติและไม่ทำให้ความสัมพันธ์ foreign key เสียหาย

หาก PiCha hard-delete SKU ที่ถูกยกเลิก การวิเคราะห์เปรียบเทียบปีต่อปีทุกครั้งจะนับรายได้ต่ำกว่าความเป็นจริงสำหรับช่วงเวลาที่ SKU เหล่านั้นยังขายอยู่ เช่น เดือนไหนที่ Osmanthus เคยขายดีจะดูแย่ลงอย่างผิดจริง Soft delete จึงเป็นแนวปฏิบัติมาตรฐานของอุตสาหกรรมสำหรับสถานการณ์แบบนี้

Hard Delete เหมาะสมเมื่อใด: การลบระเบียนทดสอบ

Hard Delete เหมาะสำหรับกรณีที่ระเบียนถูกสร้างขึ้นโดยไม่ตั้งใจ และไม่ได้ถูกอ้างอิงหรือใช้งานต่อในระบบ downstream ตัวอย่างเช่น หากมีการ INSERT SKU ทดสอบที่มี sku_id = 99 และ sku_name = 'TEST ITEM DO NOT USE' เข้าไปในฐานข้อมูล production โดยผิดพลาด และระเบียนนั้นไม่เคยปรากฏในธุรกรรมการขายจริง การลบออกอย่างถาวรด้วย DELETE ก็ถือเป็นแนวทางที่เหมาะสม

ไวยกรณ์พื้นฐานของ DELETE

DELETE FROM table_name
WHERE condition;

หากไม่มี WHERE ทุกแถวในตารางจะถูกลบ แต่โครงสร้างตารางยังอยู่ หากต้องการลบทั้งโครงสร้างและข้อมูล ให้ใช้ DROP TABLE

เริ่มต้นจากการตรวจสอบแถวข้อมูลที่ต้องการลบ ก่อน DELETE

-- ยืนยันว่าแถวทดสอบมีอยู่และไม่มีประวัติออเดอร์
SELECT m.sku_id, m.sku_name,
       COUNT(oi.sku_id) AS order_lines
FROM menus m
LEFT JOIN order_items oi ON m.sku_id = oi.sku_id
WHERE m.sku_id = 99
GROUP BY m.sku_id, m.sku_name;
sku_id sku_name order_lines
99 TEST ITEM DO NOT USE 0

ไม่มี order lines จึงปลอดภัยที่จะลบ

SQL Hard DELETE:

DELETE FROM menus
WHERE sku_id = 99;

ยืนยันหลัง DELETE:

SELECT COUNT(*) AS test_row_count
FROM menus
WHERE sku_id = 99;
test_row_count
0

การทำ Hard Delete ถือว่าปลอดภัยได้เฉพาะในกรณีที่เงื่อนไขสองข้อเป็นจริงพร้อมกัน ได้แก่ แถวนั้นเป็นข้อมูลที่เกิดขึ้นจากความผิดพลาดหรือไม่มีคุณค่าในเชิงประวัติแล้ว และไม่ถูกอ้างอิงโดยตารางอื่นในระบบ ด้วยเหตุนี้ การตรวจสอบทั้งสองประเด็นด้วย LEFT JOIN ก่อนทำ Hard Delete ทุกครั้งจึงควรเป็นมาตรฐานในการปฏิบัติ

สรุป

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

ตารางต่อไปนี้สรุปแนวคิดสำคัญ syntax ที่ใช้บ่อย และตัวอย่างการประยุกต์ใช้ในบริบทของธุรกิจ

แนวคิด Syntax การใช้งานทางธุรกิจ
INSERT แถวเดียว INSERT INTO t (cols) VALUES (vals) เพิ่ม SKU ใหม่เข้าสู่เมนู
INSERT หลายแถว INSERT INTO t (cols) VALUES (r1), (r2) เปิดตัว SKU หลายรายการพร้อมกัน
INSERT ... SELECT INSERT INTO t SELECT ... FROM s ย้ายหรือ archive ข้อมูลสินค้า seasonal จากอีกตารางหนึ่ง
UPDATE ... WHERE UPDATE t SET col = val WHERE condition ปรับราคาเครื่องดื่ม seasonal หรือแก้ไขข้อมูลเฉพาะบางรายการ
CASE ใน SET SET col = CASE WHEN ... END กำหนดราคา premium หรือเงื่อนไขแบบ tier ตาม family ของสินค้า
Soft delete UPDATE t SET active_flag = 0 WHERE ... SKU ที่ยกเลิกขายโดยไม่ลบข้อมูลออกจริง
Hard delete DELETE FROM t WHERE condition ลบข้อมูลทดสอบหรือระเบียนที่บันทึกผิดพลาดออกจากระบบ

เมื่อมองภาพรวม คำสั่งเหล่านี้ช่วยให้เราจัดการข้อมูลได้ครบทั้งวงจร ตั้งแต่การเพิ่มข้อมูลใหม่ การปรับปรุงข้อมูลเดิม ไปจนถึงการนำข้อมูลที่ไม่ต้องการออกจากระบบ การเลือกใช้คำสั่งให้เหมาะกับสถานการณ์จึงมีความสำคัญมาก โดยเฉพาะการแยกให้ชัดว่าเมื่อใดควรใช้การลบแบบชั่วคราว และเมื่อใดจึงเหมาะกับการลบออกจริง

คำถามท้ายบท

  1. INSERT แถวเดียว: เพิ่ม SKU pure tea ใหม่ลงในเมนู เลือก sku_id, sku_name, tea_base และ list_price_thb ที่เหมาะสมในช่วง 90–120 บาท รัน pre-flight check ดำเนินการ INSERT แล้วยืนยันด้วย SELECT
คลิกเพื่อดูเฉลย
-- Pre-flight check: ดู pure_tea ที่มีอยู่และ max sku_id
SELECT sku_id, sku_name, tea_base, list_price_thb
FROM menus
WHERE family = 'pure_tea'
ORDER BY sku_id;

SELECT MAX(sku_id) AS next_id FROM menus; -- ได้ 22 → ใช้ 23

-- INSERT
INSERT INTO menus (
 sku_id, sku_name, family, tea_base,
 milk_flag, fruit_flag, cold_brew_flag, blended_flag,
 size_code, size_ml, list_price_thb,
 sweetness_min_pct, sweetness_max_pct,
 caffeine_level, seasonal_flag, signature_flag, launch_branch_flag
) VALUES (
 23, 'Da Hong Pao Pure Tea R', 'pure_tea', 'da_hong_pao',
 0, 0, 0, 0,
 'R', 500, 110,
 0, 100,
 'high', 0, 0, 0
);

-- Verification
SELECT sku_id, sku_name, family, tea_base, list_price_thb
FROM menus
WHERE sku_id = 23;
  1. UPDATE พร้อม WHERE: ทีม finance ต้องการเพิ่มค่าบรรจุภัณฑ์ 2 บาทสำหรับออเดอร์ delivery ทั้งหมดโดยการเพิ่มราคาเมนูของเครื่องดื่ม blended ขึ้น 2 บาท เขียน UPDATE และรวมทั้ง pre-flight SELECT และ post-flight verification แบบ GROUP BY
คลิกเพื่อดูเฉลย
-- Pre-flight: ดู blended SKU ที่จะได้รับผลกระทบ
SELECT sku_id, sku_name, list_price_thb, list_price_thb + 2 AS new_price
FROM menus
WHERE family = 'blended';
-- Thai Tea Frappe L: 125 → 127, สั่งผ่าน delivery 427 ครั้ง

-- UPDATE
UPDATE menus
SET list_price_thb = list_price_thb + 2
WHERE family = 'blended';

-- Post-flight verification
SELECT family, COUNT(*) AS sku_count,
 MIN(list_price_thb) AS min_price,
 MAX(list_price_thb) AS max_price
FROM menus
GROUP BY family
ORDER BY family;
  1. Soft delete: โดยใช้รูปแบบ active_flag เขียน SQL เพื่อปิดการใช้งาน SKU ทั้งหมดในหมวด pure_tea ก่อนดำเนินการ รัน LEFT JOIN check เพื่อวัดจำนวนแถวใน order_items ที่อ้างอิง pure tea SKU
คลิกเพื่อดูเฉลย
-- เพิ่มคอลัมน์ active_flag ก่อน (ถ้ายังไม่มี)
ALTER TABLE menus ADD COLUMN active_flag INTEGER DEFAULT 1;

-- Pre-flight: LEFT JOIN check วัดผลกระทบ
SELECT
 m.sku_id,
 m.sku_name,
 COUNT(oi.sku_id) AS order_items_affected
FROM menus m
LEFT JOIN order_items oi ON m.sku_id = oi.sku_id
WHERE m.family = 'pure_tea'
GROUP BY m.sku_id, m.sku_name;
-- Tie Guan Yin Pure Tea R: 1,331 แถว
-- Glutinous Green Pure Tea R: 1,362 แถว → รวม 2,693 แถว historical

-- Soft delete (ไม่ลบข้อมูลจริง)
UPDATE menus
SET active_flag = 0
WHERE family = 'pure_tea';

-- Verification
SELECT sku_id, sku_name, family, active_flag
FROM menus
WHERE family = 'pure_tea';
  1. INSERT … SELECT archive: สร้างตาราง discontinued_skus และเติมข้อมูลด้วย SKU ทั้งหมดที่มี seasonal_flag = 1 โดยใช้ INSERT … SELECT เพิ่มคอลัมน์ discontinued_date ที่มีวันที่วันนี้ ยืนยันจำนวน archive
คลิกเพื่อดูเฉลย
-- สร้างตาราง discontinued_skus สำหรับเก็บเมนูที่เลิกขาย โดยคัดลอก schema จาก menus
CREATE TABLE discontinued_skus AS
SELECT
 sku_id,                             -- รหัสสินค้า
 sku_name,                           -- ชื่อสินค้า
 family,                             -- กลุ่มสินค้า
 tea_base,                           -- ชาหลักที่ใช้
 list_price_thb,                     -- ราคาขาย (บาท)
 seasonal_flag,                      -- flag เมนูตามฤดูกาล
 CURRENT_DATE AS discontinued_date   -- วันที่ปลดระวาง
FROM menus
WHERE 1 = 0; -- คัดลอกเฉพาะ schema ไม่มีแถวข้อมูล (เงื่อนไขเป็นเท็จเสมอ)

-- ย้ายเมนู seasonal ทั้งหมดไปยังตาราง discontinued_skus
INSERT INTO discontinued_skus
SELECT
 sku_id,           -- รหัสสินค้า
 sku_name,         -- ชื่อสินค้า
 family,           -- กลุ่มสินค้า
 tea_base,         -- ชาหลักที่ใช้
 list_price_thb,   -- ราคาขาย (บาท)
 seasonal_flag,    -- flag เมนูตามฤดูกาล
 CURRENT_DATE AS discontinued_date -- วันที่ปลดระวาง
FROM menus
WHERE seasonal_flag = 1; -- กรองเฉพาะเมนู seasonal

-- Verification
SELECT COUNT(*) AS archived_count FROM discontinued_skus;
-- ได้ 3 แถว: Fresh Orange Jasmine Tea L, Mango Sticky Rice Tea L, Thai Tea Frappe L

SELECT * FROM discontinued_skus;
  1. การตัดสินใจ DELETE: stakeholder ขอให้คุณลบ transaction ทั้งหมดที่มี satisfaction_score = 1 ออกจากตาราง transactions โดยอ้างว่า “นี่คือข้อมูลที่ไม่ดีอย่างชัดเจน” เขียน pre-flight query เพื่อประเมินว่านี่ส่งผลกระทบต่อกี่แถว คิดเป็นสัดส่วนเท่าไรของ transaction ทั้งหมด และแถวเหล่านี้มาจากสาขาไหนบ้าง จากนั้นเขียนคำแนะนำ 3 ประโยคว่าควรดำเนินการ DELETE หรือไม่ พร้อมอธิบายเหตุผล
คลิกเพื่อดูเฉลย
-- Pre-flight: ประเมินขนาดผลกระทบ
SELECT
 COUNT(*) AS low_score_rows,
 (SELECT COUNT(*) FROM transactions) AS total_rows,
 ROUND(100.0 * COUNT(*) /
     (SELECT COUNT(*) FROM transactions), 2) AS pct_of_total
FROM transactions
WHERE satisfaction_score = 1;
-- 13 แถว จาก 53,323 = 0.02%

-- Pre-flight: แยกตามสาขา
SELECT
 s.store_name,
 COUNT(*) AS cnt,
 MIN(t.order_date) AS earliest,
 MAX(t.order_date) AS latest
FROM transactions t
JOIN stores s ON t.store_id = s.store_id
WHERE t.satisfaction_score = 1
GROUP BY s.store_name
ORDER BY cnt DESC;
-- Exchange Tower: 10 แถว, Park Silom: 3 แถว

คำแนะนำ: ไม่ควร DELETE

คะแนน 1 ดาวจาก 13 แถว (0.02%) คือ ข้อมูลที่ถูกต้อง ไม่ใช่ข้อผิดพลาด — ลูกค้าที่ไม่พอใจมีอยู่จริงและเป็นสัญญาณสำคัญที่ควรวิเคราะห์ การลบออกจะทำให้ Exchange Tower ดูดีกว่าความเป็นจริง (10 จาก 13 แถวมาจากสาขานี้) ซึ่งอาจทำให้ทีมบริหารมองข้ามปัญหาด้านคุณภาพบริการ หากต้องการแยกข้อมูลออกจากการวิเคราะห์บางส่วน ให้ใช้ WHERE satisfaction_score > 1 ใน query แทน ไม่ใช่การลบถาวร