สิ่งที่ใครสักคนควรบอกเกี่ยวกับ Postgres
(challahscript.com)- เอกสารทางการของ Postgres ยอดเยี่ยมมาก แต่ Postgres 17 PDF ยาวถึง 3,200 หน้า ทำให้ผู้เริ่มต้นเรียนรู้ทั้งการออกแบบสคีมา การทำงานของ SQL และกับดักด้านการปฏิบัติการจากเอกสารอย่างเดียวก่อนใช้งานจริงได้ยาก
- ถ้าไม่มีเหตุผลพิเศษ ควร ทำข้อมูลให้เป็น normalized และการ denormalize เพื่อเพิ่มประสิทธิภาพการอ่านด้วยการเก็บข้อมูลซ้ำ ต้องยอมรับต้นทุนเรื่องความไม่สอดคล้องกันของข้อมูลและความซับซ้อนในการเขียนที่เพิ่มขึ้น
- คีย์เวิร์ด SQL ไม่สนตัวพิมพ์ใหญ่เล็ก แต่ NULL ใกล้เคียงกับ “ไม่ทราบค่า” มากกว่า การเปรียบเทียบแบบเดียวกับ
nullในภาษาโปรแกรมทั่วไปจึงอาจให้ผลลัพธ์ไม่ตรงคาด - ถ้าใช้
psqlเป็น ก็แค่ใช้ pager,\x,.psqlrc,\pset null, การเติมคำอัตโนมัติ, คำสั่ง backslash และ\copyให้ดี ก็ช่วยให้การอ่านผลลัพธ์ การสำรวจข้อมูล และการส่งออก CSV สะดวกขึ้นมาก - index, lock, transaction และ JSONB ทรงพลังมาก แต่ถ้าไม่เข้าใจ query plan และข้อจำกัดด้านการปฏิบัติการ ก็อาจนำไปสู่ปัญหาด้านประสิทธิภาพหรือความพร้อมใช้งานได้
บริบทที่ควรรู้ก่อนลุยเอกสารทางการชุดใหญ่
- เอกสารทางการของ Postgres สำหรับเวอร์ชันปัจจุบันคือ 17 ถ้าพิมพ์เป็น PDF ขนาด US letter จะมี 3,200 หน้า และถ้าพิมพ์เป็น A4 จะมี 3,024 หน้า
- มีความรู้เชิงปฏิบัติเกี่ยวกับ Postgres หลายอย่างที่รู้ไว้ก่อนใช้งานจะช่วยได้ และบางส่วนก็ใช้กับ SQL DBMS อื่นได้ด้วย แต่ขอบเขตการใช้ไม่ได้ชัดเจนเสมอไป
โดยพื้นฐานแล้วควร normalize ข้อมูล
- Normalization คือกระบวนการลบข้อมูลซ้ำซ้อนหรือข้อมูลที่ไม่จำเป็นออกจากสคีมาฐานข้อมูล
- ถ้าเก็บ
user_emailไว้ตรง ๆ ในตารางdocumentsเมื่อผู้ใช้เปลี่ยนอีเมล ก็ต้องอัปเดตทุกแถวเอกสารของผู้ใช้นั้น- ทางเลือกคือให้แต่ละแถวใน
documentsอ้างอิงแถวในตารางอื่น เช่นusersด้วย foreign keyuser_id
- ทางเลือกคือให้แต่ละแถวใน
- ไม่จำเป็นต้องท่องจำ normal form แต่ละระดับอย่าง “1st normal form” ทั้งหมด แต่กระบวนการ normalize โดยทั่วไปมักนำไปสู่สคีมาที่ดูแลรักษาง่ายกว่า
- Denormalization คือการเก็บข้อมูลซ้ำเพื่อให้อ่านได้เร็ว โดยไม่ต้องคำนวณข้อมูลบางอย่างใหม่ทุกครั้ง
- เช่น ในแอปจัดกะพนักงาน อาจไม่คำนวณชั่วโมงทำงานสะสมของปีนี้ทุกครั้งจากผลรวม shift duration ทั้งหมด แต่คำนวณและเก็บไว้เป็นระยะ ๆ หรือเมื่อมีการเปลี่ยนแปลงเวลาทำงาน
- ข้อมูลนี้อาจเก็บไว้ใน Postgres เอง หรือเก็บไว้ในชั้น cache อย่าง Redis ก็ได้
- การ denormalize แทบจะมีต้นทุนเสมอ โดยต้นทุนหลักคือ ความเป็นไปได้ที่ข้อมูลจะไม่สอดคล้องกัน และความซับซ้อนของการเขียนที่เพิ่มขึ้น
คำแนะนำแบบ “อย่าทำแบบนี้” จากโปรเจกต์ Postgres
- ในวิกิทางการของ Postgres มีรายการ “Don’t do this”
- ไม่เป็นไรถ้ายังไม่เข้าใจทุกข้อ และข้อที่ยังไม่เข้าใจก็มักเป็นข้อผิดพลาดที่มีโอกาสทำพลาดน้อยกว่า
- โดยเฉพาะคำแนะนำต่อไปนี้ควรจำไว้
- ใช้
texttype สำหรับเก็บข้อความ - ใช้
timestampz/time with time zoneสำหรับเก็บเวลา - ตั้งชื่อตารางเป็น snake_case
- ใช้
พฤติกรรมของ SQL ที่ทำให้งงได้ง่าย
-
คีย์เวิร์ด SQL ไม่จำเป็นต้องเป็นตัวพิมพ์ใหญ่
- คีย์เวิร์ด SQL ไม่แยกตัวพิมพ์ใหญ่เล็ก
- คิวรีต่อไปนี้มีความหมายเหมือนกัน
SELECT * FROM my_table WHERE x = 1 AND y > 2 LIMIT 10; select * from my_table where x = 1 and y > 2 limit 10; SELECT * from my_table WHERE x = 1 and y > 2 LIMIT 10;- คุณสมบัตินี้ไม่ได้จำกัดเฉพาะ Postgres
-
NULL ไม่เหมือน null/nil ในภาษาทั่วไป
NULLของ SQL ใกล้เคียงกับ “ไม่ทราบค่า” มากกว่าnullหรือnilในภาษาโปรแกรมทั่วไปNULL = NULLไม่ได้คืนค่าtrueแต่คืนค่าNULL- การเปรียบเทียบที่มีฝั่งหนึ่งเป็น
NULLส่วนใหญ่ก็จะได้ผลลัพธ์เป็นNULLด้วย - สำหรับการเปรียบเทียบ
NULLควรใช้โอเปอเรชันต่อไปนี้x IS NULL: ถ้าxเป็นNULLจะได้truex IS NOT NULL: ถ้าxไม่เป็นNULLจะได้truex IS NOT DISTINCT FROM y: คล้ายx = yแต่ปฏิบัติต่อNULLเหมือนค่าแบบปกติx IS DISTINCT FROM y: คล้ายx != y/x <> yแต่ปฏิบัติต่อNULLเหมือนค่าแบบปกติ
- ในเงื่อนไข
WHEREจะคืนแถวเฉพาะเมื่อเงื่อนไขเป็นtrueเท่านั้นSELECT * FROM users WHERE title != 'manager'จะไม่คืนแถวที่titleเป็นNULL- เพราะผลของ
NULL != 'manager'คือNULL
COALESCEจะคืนค่าตัวแรกที่ไม่ใช่NULLจากหลายอาร์กิวเมนต์
COALESCE(NULL, 5, 10) = 5 COALESCE(2, NULL, 9) = 2 COALESCE(NULL, NULL) IS NULL
ใช้ psql ให้มีประโยชน์มากขึ้น
-
ปรับให้อ่านผลลัพธ์ง่ายขึ้น
- ถ้า query ตารางที่มีหลายคอลัมน์หรือค่าที่ยาวแล้วผลลัพธ์อ่านยาก อาจเป็นเพราะปิด pager อยู่
- pager ของเทอร์มินัลช่วยให้เลื่อนดูข้อความยาวหรือผลลัพธ์ตารางจาก
psqlได้ภายใน viewport - ตารางที่มีหลายคอลัมน์สามารถเปิด expanded mode ด้วย
\pset expandedหรือ\x - ถ้าอยากใช้เป็นค่าเริ่มต้น ก็เพิ่ม
\xใน~/.psqlrcในโฮมไดเรกทอรีได้
-
ทำให้การแสดงผล NULL ชัดเจนขึ้น
- ค่าเริ่มต้นไม่ได้ทำให้เห็นชัดนักว่าค่าไหนเป็น
NULLในผลลัพธ์ - ใน
psqlสามารถกำหนดสตริงสำหรับแสดงNULLได้
\pset null '[NULL]'- ใช้สตริง Unicode ก็ได้ และถ้าอยากให้เป็นค่าเริ่มต้นก็เพิ่มคำสั่งเดียวกันใน
~/.psqlrc
- ค่าเริ่มต้นไม่ได้ทำให้เห็นชัดนักว่าค่าไหนเป็น
-
ใช้การเติมคำอัตโนมัติและคำสั่ง backslash ให้คุ้ม
psqlรองรับ การเติมคำอัตโนมัติ แบบ interactive console- พิมพ์บางส่วนของคีย์เวิร์ดหรือชื่อตารางแล้วกด Tab เพื่อเติมส่วนที่เหลือได้
- คำสั่ง backslash ที่มีประโยชน์มีดังนี้
\?: รายการ shortcut ทั้งหมด\d: แสดงรายการ relation หรือก็คือตารางและ sequence พร้อม owner\d+: เหมือน\dแต่เพิ่มขนาดและ metadata บางส่วน\d table_name: แสดงสคีมาตาราง ชนิดคอลัมน์ การเป็น nullable ค่า default index และ foreign key constraint\e: แก้ไข query ใน editor เริ่มต้นที่กำหนดไว้ในตัวแปรแวดล้อม$EDITOR\h SQL_KEYWORD: แสดงไวยากรณ์ของคีย์เวิร์ด SQL นั้นและลิงก์ไปเอกสาร
-
ส่งออก CSV และใช้ alias ใน SELECT
- ใช้
\copyเพื่อบันทึกผลลัพธ์ query เป็น CSV ได้
\copy (select * from some_table) to 'my_file.csv' CSV- ถ้าต้องการให้มีชื่อคอลัมน์ในบรรทัดแรก ให้เพิ่มตัวเลือก
HEADER
\copy (select * from some_table) to 'my_file.csv' CSV HEADER\copyช่วยเลี่ยงการต้องใช้สิทธิ์ที่สูงขึ้นแบบที่คำสั่งCOPYมาตรฐานต้องการ- คอลัมน์ผลลัพธ์ของ
SELECTสามารถตั้ง alias ด้วยASได้
SELECT vendor, COUNT(*) AS number_of_backpacks FROM backpacks GROUP BY vendor ORDER BY number_of_backpacks DESC;- ใน
GROUP BYและORDER BYสามารถอ้างอิงหมายเลขคอลัมน์ตามลำดับที่ปรากฏหลังSELECTได้
SELECT vendor, COUNT(*) AS number_of_backpacks FROM backpacks GROUP BY 1 ORDER BY 2 DESC;- รูปแบบย่อนี้มีประโยชน์ แต่ควรหลีกเลี่ยงใน query ที่จะ deploy ไป production
- ใช้
เพิ่ม index แล้วไม่ได้แปลว่าจะถูกใช้งานเสมอไป
-
index และ query plan
- index คือโครงสร้างข้อมูลที่ทำหน้าที่เป็น ไดเรกทอรีทางลัด สำหรับค้นหาแถวในตารางตามฟิลด์ที่กำหนด
- index ที่พบบ่อยที่สุดคือ B-tree และใช้ได้กับทั้งเงื่อนไขเท่ากันแบบตรงตัว เช่น
WHERE a = 3และเงื่อนไขช่วง เช่นWHERE a > 5 - ไม่สามารถสั่ง Postgres โดยตรงให้ใช้ index ตัวใดตัวหนึ่งได้
- Postgres จะอิงจากสถิติที่เก็บไว้ของแต่ละตารางเพื่อคาดการณ์ว่า index จะเร็วกว่าการอ่านทั้งตารางแบบ sequential scan หรือไม่
- ถ้าเติม
EXPLAINไว้หน้าSELECT ... FROM ...ก็จะเห็น query plan ว่า Postgres จะรัน query อย่างไร - เวลาอ่าน query plan สามารถดู คู่มือ EXPLAIN ANALYZE ของ thoughtbot, เอกสาร pganalyze, เอกสารทางการ, และ explain.depesz.com ได้
-
ตารางเล็กและ index หลายคอลัมน์
- ในตารางที่มีแถวน้อย เช่นฐานข้อมูลสำหรับพัฒนาในเครื่อง local, index อาจไม่ได้ช่วยมากนัก
- ถ้ามีราว 100 แถว Postgres อาจตัดสินว่า sequential scan เร็วกว่า index
- Postgres รองรับ index แบบหลายคอลัมน์
CREATE INDEX CONCURRENTLY ON tbl (a, b);- เงื่อนไขอย่าง
WHERE a = 1 AND b = 2อาจเร็วกว่าเมื่อใช้งาน index แยกกันคนละตัวสำหรับaและb - เพราะสามารถเดิน B-tree เดียวและรวมเงื่อนไขการค้นหาได้อย่างมีประสิทธิภาพ
- index
(a, b)ยังช่วยให้ query ที่กรองแค่aเร็วได้พอ ๆ กับ index เดี่ยวของa - สำหรับ query อย่าง
WHERE b = 5ก็อาจเร็วขึ้นได้ แต่ไม่จำเป็นต้องดีที่สุด- เพราะ key ของ index เรียงตาม
aก่อนแล้วค่อยbจึงต้องไล่ผ่านค่าของaทั้งหมดเพื่อหาค่าb
- เพราะ key ของ index เรียงตาม
- ถ้าต้อง query หลายชุดของคอลัมน์ร่วมกัน ก็มักจะมีทั้ง
(a, b)และ index เดี่ยวของb - หรืออาจพึ่ง index เดี่ยวของ
aและbแยกกันตามความเหมาะสม
-
ใช้ text_pattern_ops กับ prefix match
- อาจเก็บไดเรกทอรีแบบลำดับชั้นด้วยวิธี materialized path และต้องหาลูกหลานทั้งหมดที่ขึ้นต้นด้วย prefix บางตัว
SELECT * FROM directories WHERE path LIKE '/1/2/3/%'- แม้จะสร้าง B-tree index ปกติบนคอลัมน์
pathแล้ว query นี้ก็อาจไม่ใช้มัน
CREATE INDEX CONCURRENTLY ON directories (path);- ถ้าต้องการลำดับระดับอักขระที่จำเป็นสำหรับ prefix match หรือ pattern match ต้องกำหนด operator class
CREATE INDEX CONCURRENTLY ON directories (path text_pattern_ops);
ปัญหาด้านการปฏิบัติการที่เกิดจาก lock และ transaction
-
lock ของ Postgres
- lock หรือ mutex เป็นกลไกที่ทำให้งานที่เสี่ยงอันตรายทำได้พร้อมกันครั้งละไคลเอนต์เดียว
- ในฐานข้อมูล การอัปเดต object อย่าง row, table, view ต้องสำเร็จทั้งหมดหรือไม่สำเร็จเลย และเพื่อป้องกันไม่ให้การทำงานพร้อมกันทำให้สำเร็จแค่บางส่วน จึงต้องขอ lock กับ object ที่เกี่ยวข้อง
- ระดับ table lock ของ Postgres มีหลายระดับ ตั้งแต่จำกัดน้อยไปจนถึงจำกัดมาก
ACCESS SHARE:SELECTROW SHARE:SELECT ... FOR UPDATEROW EXCLUSIVE:UPDATE,DELETE,INSERTSHARE UPDATE EXCLUSIVE:CREATE INDEX CONCURRENTLYSHARE:CREATE INDEXที่ไม่ใช้CONCURRENTLYACCESS EXCLUSIVE:ALTER TABLE,ALTER INDEXหลายรูปแบบ
- บนตารางเดียวกัน การทำงานต่อไปนี้อาจทำได้พร้อมกันหรือจำเป็นต้องรอ
UPDATEระหว่างSELECT: ทำได้UPDATEระหว่างCREATE INDEX CONCURRENTLY: ทำได้SELECTระหว่างCREATE INDEX: ทำได้SELECTระหว่างALTER TABLE: โดยทั่วไปต้องรอALTER TABLEระหว่างSELECT: โดยทั่วไปต้องรอ
ALTER TABLEบางรูปแบบอาจต้องการ lock ที่อ่อนกว่านี้ และดูข้อมูลทั้งหมดได้จาก เอกสาร explicit locking ทางการ และ คู่มือ lock conflict ตาม operation
-
ALTER TABLE ที่ช้าและคิวของ lock
- ถ้า
ALTER TABLEใช้เวลานาน แม้แต่SELECTที่อ่านตารางเดียวกันก็อาจถูกบล็อก - ถ้าเป็นตารางสำคัญอย่าง
usersที่ทุก request ของเว็บแอปอ้างอิง คำขออาจค้างจน timeout และคืน 503 - สาเหตุที่ทำให้
ALTER TABLEช้าได้บ่อยมีดังนี้- การเพิ่มคอลัมน์ที่มี default แบบ non-constant
- การเปลี่ยนชนิดของคอลัมน์
- การเพิ่ม uniqueness constraint
- หลัง Postgres 11 ปัญหาที่การเพิ่มคอลัมน์พร้อม default ทุกแบบทำให้ช้าถูกแก้ไปแล้ว และที่ยังเป็นปัญหาได้คือ default แบบ non-constant
- แม้ตัว
ALTER TABLEเองจะเป็นงานที่เร็ว ก็จะยังไม่เริ่มจนกว่าจะได้ lock- เช่น ถ้ามี
SELECTช้าจากแดชบอร์ดภายในที่มีมานานกำลังรันอยู่ALTER TABLEก็ต้องรอ
- เช่น ถ้ามี
- lock ของ Postgres สร้าง คิวรอ ได้ ดังนั้น query ถัด ๆ มาบนตารางเดียวกันที่เข้ามาหลัง
ALTER TABLEซึ่งกำลังรออยู่ ก็อาจต้องรอต่อด้วย - อ่านสถานการณ์แบบเดียวกันเพิ่มเติมได้ที่ Migrations and exclusive locks
- ถ้า
-
transaction ที่เปิดนานก็อันตราย
- transaction คือวิธีรวมหลายคำสั่งฐานข้อมูลให้เป็น all-or-nothing โดยเริ่มด้วย
BEGINและจบด้วยCOMMIT - การเปลี่ยนแปลงระหว่าง transaction จะยังไม่เห็นจากไคลเอนต์อื่น และจะถูกเผยแพร่สู่ฐานข้อมูลเมื่อ
COMMIT - เหมาะกับงานอย่างการโอนเงิน ที่การลดเงินจากบัญชีหนึ่งและเพิ่มอีกบัญชีหนึ่งต้องสำเร็จพร้อมกันหรือยกเลิกพร้อมกัน
- ถ้า transaction ได้ lock มาแล้ว ก็จะถือ lock นั้นไว้จนถึง
COMMIT - ถ้า
BEGINแล้วUPDATErow บางแถวและลุกไปทำอย่างอื่นDELETEของ row เดียวกันจากไคลเอนต์อื่นก็จะหยุดรอจนกว่า transaction จะ commit - transaction ที่เปิดค้างนานเกินจำเป็นอาจบล็อก query หรือการอัปเดตจากไคลเอนต์อื่นได้
- transaction คือวิธีรวมหลายคำสั่งฐานข้อมูลให้เป็น all-or-nothing โดยเริ่มด้วย
JSONB เป็นเครื่องมือที่คมมาก
-
ปัญหาด้านประสิทธิภาพและสคีมาของ JSONB
- JSONB ยืดหยุ่นมาก แต่ถ้าใช้ผิดทางก็มีข้อเสียมากเช่นกัน
- Postgres ไม่ติดตามสถิติของคอลัมน์ JSONB ดังนั้น query แบบเท่ากันตรงตัวบนคอลัมน์ JSONB เดียวอาจช้ากว่ามากเมื่อเทียบกับ query บนคอลัมน์ปกติหลายคอลัมน์
- มีกรณีตัวอย่างหนึ่งที่ ช้าลง 2000 เท่าเพราะ JSONB
- คอลัมน์ JSONB ใส่อะไรก็ได้แทบทุกอย่าง จึงทรงพลังมาก แต่ก็แทบไม่มีการรับประกันเรื่องโครงสร้าง
- กับตารางปกติ เราดูสคีมาแล้วพอคาดเดาผลลัพธ์ของ query ได้ แต่กับ JSONB เราอาจไม่แน่ใจว่าชื่อ key เป็น camelCase หรือ snake_case หรือสถานะเป็น boolean หรือ enum
- คุณสมบัติแบบ static type ที่ข้อมูล Postgres ปกติมี จะไม่ใช้กับ JSONB ในลักษณะเดียวกัน
-
ความแปลกของการเปรียบเทียบชนิดใน JSONB
- ถ้าต้องการหาแถวที่ฟิลด์
brandในคอลัมน์ JSONBdataของตารางbackpacksมีค่าเป็นJanSportquery ต่อไปนี้จะใช้ไม่ได้
select * from backpacks where data['brand'] = 'JanSport';- Postgres คาดว่าชนิดของค่าด้านขวาในการเปรียบเทียบต้องตรงกับค่าด้านซ้าย และค่าด้านขวาต้องเป็นเอกสาร JSON ที่ถูกต้อง
- เอกสาร JSON ต้องเป็น object, array, string, number, boolean หรือ null ดังนั้น
JanSportเดี่ยว ๆ จึงไม่ใช่ JSON ที่ถูกต้อง - query ที่ถูกต้องคือต้องเปรียบเทียบกับ JSON string หรือแปลงค่าด้านซ้ายให้เป็น
textของ Postgres
select * from backpacks where data['brand'] = '"JanSport"'; select * from backpacks where data['brand'] = '"JanSport"'::jsonb; select * from backpacks where data->>'brand' = 'JanSport';NULLของ SQL และnullของ JSONB ทำงานต่างกัน'null'::jsonb = 'null'::jsonbให้ค่าtrueแต่NULL = NULLให้ค่าNULL
- JSONB มี โอเปอเรเตอร์และฟังก์ชันเฉพาะ อยู่มาก จึงจำทั้งหมดในครั้งเดียวได้ยาก
- ใน Postgres มีทั้ง
JSONซึ่งเก็บค่า JSON เป็นข้อความ และJSONBซึ่งแปลงเป็นรูปแบบไบนารีที่มีประสิทธิภาพ - JSONB มีข้อดีอย่างการทำ index ได้ ส่วนรูปแบบ JSON อาจมองว่าเหมาะกับกรณีพิเศษ
- ถ้าต้องการหาแถวที่ฟิลด์
2 ความคิดเห็น
สิ่งที่ไม่ควรทำ สักวันหนึ่งคงต้องหาเวลาอ่านดูสักครั้ง
ความเห็นจาก Hacker News
โดยทั่วไป PostgreSQL แยกตัวพิมพ์เล็ก-ใหญ่ แต่การเขียนคีย์เวิร์ด SQL เป็นตัวพิมพ์ใหญ่ก็มักเป็นความพยายามเพิ่มความอ่านง่ายด้วยการจับแพตเทิร์นด้วยสายตา
ไม่ได้จำเป็นเสมอไป แต่ถ้าต้องดีบักคิวรีของคนอื่น ก็น่าจะโยนเข้า prettifier เพื่อไล่อ่านนิยามได้เร็วโดยไม่สะดุดกับรูปแบบไวยากรณ์จุกจิก
เหมือนกับการจัดระเบียบโค้ดในภาษาอื่น โครงสร้างทางสายตาอย่างการเยื้องที่สม่ำเสมอช่วยลดเวลาที่ใช้ทำความเข้าใจส่วนที่เดาง่ายอยู่แล้ว และทำให้โฟกัสกับจุดสำคัญได้
แต่ส่วนตัวเกลียดการผสมตัวพิมพ์เล็ก-ใหญ่ใน identifier แบบ
actuallyUsingCaseInIdentifiersมาก และไม่อยากเห็น คอลัมน์ที่ต้องใส่เครื่องหมายอัญประกาศคู่ เวลาเช็กผ่าน CLIถ้าเป็นคิวรีชั่วคราวที่พิมพ์เร็ว ๆ แล้วทิ้ง ไม่มีใครเห็น ก็ไม่สนตัวพิมพ์เล็ก-ใหญ่ แต่ SQL ที่จะ commit เข้า repository จะเขียนคำสั่งเป็น ALL CAPS
ตอนนี้มีสีแล้วเลยไม่จำเป็นอีกต่อไป แต่เป็นความทรงจำเก่า ๆ เลยไม่มีแหล่งอ้างอิง
ถึงอย่างนั้นก็ไม่ควรผสม identifier ที่ใส่เครื่องหมายอัญประกาศกับแบบไม่ใส่ และการดูโครงสร้างภายในก็แทบไม่ได้เป็นมาตรฐานอยู่แล้ว จึงไม่ได้มีความหมายมากนัก
เพิ่งเคยเห็นหัวข้อ “don’t do this” ในวิกิของ PostgreSQL แต่มีประโยชน์มาก: https://wiki.postgresql.org/wiki/Don%27t_Do_This
เช่น ในสคีมาใหม่อาจปิดฟีเจอร์อย่าง table inheritance ไปเลย และถ้าจะเปิดใหม่ก็ควรต้องตั้งค่าแบบจงใจให้ยุ่งยากหน่อย
หลายอย่างที่พูดถึงที่นี่ไม่ได้ใช้เฉพาะกับ PostgreSQL
เช่น พฤติกรรมแปลก ๆ ของ
NULLหรือเรื่องลำดับคอลัมน์ในดัชนี และโดยเฉพาะปฏิสัมพันธ์ระหว่าง NULL กับดัชนี/ข้อกำหนด unique ที่แม้แต่ใน MySQL ก็ไม่ตรงสัญชาตญาณตัวอย่างเช่น ถ้ามีตารางผู้ใช้ที่
emailห้ามเป็น NULL แต่usernameเป็น NULL ได้ แล้วตั้งข้อกำหนด unique ที่(email, username)คุณก็ยังใส่emailเดิมซ้ำได้หลายครั้งเมื่อusernameเป็น NULL เพราะ NULL ไม่เท่ากับ NULL อื่นhttps://www.postgresql.org/docs/devel/sql-createtable.html#S...
use case ที่ต้องการพฤติกรรมตรงกันข้ามมีน้อยกว่ามาก
ถ้าพูดแค่ว่า “ถ้าไม่มีเหตุผลที่ดี ก็ควรทำข้อมูลให้เป็น normalized” แล้วจบ แบบนั้นไม่ค่อยดี
แม้แต่หน้าที่ผู้เขียนลิงก์ไว้ก็ยังมี รูปแบบ normalization รวม non-normal form ด้วยถึง 11 แบบ ซึ่งคนส่วนใหญ่ไม่รู้ด้วยซ้ำว่าคืออะไร และในนั้น 7 แบบก็แทบไม่มีโอกาสได้ใช้
ไม่ควรทำให้คนไปไล่หาว่าต้องใช้ normal form ที่สูงขึ้นเรื่อย ๆ
แม้แต่ในโปรเจกต์ที่เพิ่งย้ายไปไม่นาน ก็ยังต้องแก้ปัญหาแบบนี้อยู่หลายจุด เพราะแทบไม่มีเหตุผลให้เก็บข้อมูลซ้ำ
ทิปข้อแรกคือให้ทำ VACUUM ทุกวัน
ตอนเริ่มต้นใหม่ ๆ ไม่รู้เรื่องนี้เลย จึงไม่ได้ทำ VACUUM ให้ฐานข้อมูลของ reddit เลย และวันหนึ่งพอจำเป็นต้องรันขึ้นมาก็ทำให้ reddit ล่มไปเกือบทั้งวันระหว่างรอมันทำเสร็จ
ถ้าเป็นขนาดของ reddit ก็ยังน่าแปลกใจที่ transaction ID ไม่หมดไปก่อน
อยากให้เหล่านักพัฒนาใส่ใจกับ การทำให้เป็นบรรทัดฐาน (normalization) มากกว่านี้ และเลิกยัดทุกอย่างลงในคอลัมน์ JSONB
ส่วนนักพัฒนาที่มีประสบการณ์มากกว่าจะรู้ว่าคำตอบที่ถูกคือ อย่าทำข้อมูลซ้ำเลยนอกจากคีย์ และทำ denormalization เฉพาะเมื่อจำเป็นจริง ๆ เท่านั้น
ต่อมาฐานข้อมูลอย่าง Mongo ก็ปรากฏขึ้นและนำเสนอ “สิ่งคล้ายฐานข้อมูล” ที่ทำ normalization ได้ยากหรือแทบไม่มีความหมาย ซึ่งยิ่งไปส่งเสริมคนกลุ่มนั้น และผลลัพธ์ก็คือช่วงเวลาสั้น ๆ ที่งานออกแบบฐานข้อมูลอันเลวร้ายกับกองขยะที่บำรุงรักษาไม่ได้เติบโตงอกงาม
ตอนนี้ลูกตุ้มเริ่มแกว่งกลับมา ผู้คนกลับมาค้นพบข้อดีของฐานข้อมูลแบบ normalized อีกครั้ง แต่ คอลัมน์ JSON ก็ยังคงเป็นช่องทางหนีที่เปิดโอกาสให้แนวปฏิบัติแย่ ๆ เติบโตได้
ข้อแรกคือเพื่อเก็บ JSON เอง หากเว็บเซิร์ฟเวอร์เรียกใช้ API ของบุคคลที่สามแล้วบันทึก API response ดิบลงใน คอลัมน์ JSONB ก่อนจะประมวลผลจากตรงนั้น ก็จะมีบันทึกที่ตรวจสอบย้อนหลังได้ไว้ใช้ตอนดีบักปัญหาที่มาจาก API นั้น
ข้อสองคือเพื่อเก็บ sum type การที่ SQL ไม่รองรับ sum type อาจถือเป็นข้อบกพร่องใหญ่ที่สุดอย่างหนึ่งของการทำโมเดลข้อมูลในฐานข้อมูล SQL
วิธีแก้เลี่ยงมีอยู่หลายแบบ และ “ก็แค่ใส่มันลงในคอลัมน์ JSONB แล้วตรวจสอบความถูกต้องในแอปพลิเคชัน” ก็เป็นหนึ่งในนั้น แต่ไม่มีวิธีไหนที่ยอดเยี่ยมเป็นพิเศษ
ตราบใดที่คุณไม่ได้เขียนคิวรีแย่ ๆ ลงไปในนั้นแทนที่จะดึงค่าใน JSONB ออกมาเป็นคอลัมน์แยก ผมก็ไม่คิดว่านั่นเป็นปัญหาใหญ่อะไรในตัวมันเอง
เพราะถ้าตอบโจทย์ด้าน persistence ได้สำเร็จ ก็แทบไม่มีแรงกดดันให้ต้องคิดเรื่องการออกแบบที่ดีอย่างจริงจัง
จะเหมาะหรือไม่ที่จะสร้าง DBMS ซ้อนบน DBMS อีกชั้นก็ยังน่าสงสัย แต่อย่างน้อยตอนนี้สภาพก็เป็นแบบนั้น
ถ้าคอลัมน์ใหม่ทำให้ประสิทธิภาพพังหรือก่อปัญหา ก็ควรย้อนกลับได้
หากมีเครื่องมือ CLI เข้ามาเกี่ยวข้อง ก็ต้องจัดการด้วยว่ารับ downtime ได้มากแค่ไหน จะทำการอัปเดตเวอร์ชันแบบซิงก์พร้อมกันทั้งบริษัทได้หรือไม่ หรือจะต้องรองรับทั้งสคีมาเก่าและใหม่ไปอีกระยะหนึ่ง
ถ้าฐานข้อมูลไม่ได้เป็นส่วนหนึ่งของผลิตภัณฑ์หลักของทีม เรื่องเหล่านี้อาจหายไปทั้งหมด
ผมเขียนบทความนี้ขึ้นมาเพื่อช่วยมือใหม่: https://tomcam.github.io/postgres/
บทความดีมากจริง ๆ และผมไม่เคยรู้มาก่อนว่าเอกสารของ PostgreSQL มีถึง 3200 หน้า
ผมใช้มันมาสักพักแล้วและเรียนรู้ไปตามความจำเป็นเรื่อย ๆ ผมเองก็ชอบเอกสารทางการมาก และก็ชอบอ่านบทความเฉพาะเรื่องเมื่อถึงเวลาที่ต้องใช้หัวข้อนั้น
ถ้าผู้เขียนเพิ่มไว้ที่ https://challahscript.com/what_i_wish_someone_told_me_about_... ว่าอินเด็กซ์คอลัมน์
(b, a)ก็ทำงานได้ดีเวลา query ด้วยbอย่างเดียว ก็น่าจะเป็นประโยชน์กับผู้อ่านตอนพูดถึงการ query ด้วย
aอย่างเดียวก็มีการสื่อไว้ประมาณหนึ่งแล้ว แต่ถ้าจะเขียนให้ชัดกว่านี้ก็คงไม่เสียหายส่วนเรื่อง JSON/JSONB ผมแทบไม่ได้ใช้เลย เลยไม่ค่อยได้เห็นมากนัก
พอนึกถึง SQL ชวนขำที่เคยเห็นในงานจริง ผมคิดว่าควรเริ่มจากการอ่าน บทความของ Codd และทำความเข้าใจว่ารูปแบบเชิงสัมพันธ์คืออะไร
มันยาวแค่ 11 หน้า และแค่อ่านสิ่งนั้นก็จะช่วยลดความทุกข์ของโลกนี้ลงได้
เกือบทั้งหมดของบทความนี้ใช้ได้กับ ฐานข้อมูล MVCC อื่น ๆ อย่าง MySQL ด้วย
รายละเอียดอาจต่างกัน แต่ MySQL ก็เจอปัญหาคล้าย ๆ กัน เช่น ทรานแซ็กชันยาว ๆ และการถือ metadata lock ระหว่าง
ALTERรวมถึงปัญหาสนุก ๆ อื่น ๆ แบบเดียวกัน