ชนิดข้อมูล
data type คือการตัดสินใจที่สำคัญที่สุดเพียงอย่างเดียวที่คุณทำเกี่ยวกับ column ชนิดข้อมูลควบคุมว่าค่าแบบไหนเข้าได้ ใช้พื้นที่เท่าไร เรียงลำดับและเปรียบเทียบกันอย่างไร และ operator ตัวไหนใช้ได้บ้าง PostgreSQL มาพร้อมชุดชนิดข้อมูลในตัวที่หลากหลาย และยังให้คุณนิยามชนิดของตัวเองได้ด้วย บทเรียนนี้จะพาทัวร์ชนิดที่คุณจะหยิบมาใช้บ่อยที่สุด พร้อมตัวอย่างสั้น ๆ ที่คุณรันได้
สำหรับจำนวนเต็ม integer (สี่ไบต์ ได้ถึงราว ๆ สองพันล้าน) ครอบคลุมตัวนับและ id ส่วนใหญ่ ขณะที่ bigint (แปดไบต์) จัดการกับช่วงที่ใหญ่กว่าได้ สำหรับค่าที่ความแม่นยำสำคัญ — เงินเป็นอันดับแรก — ให้ใช้ numeric ซึ่งเก็บทศนิยมได้อย่างแม่นยำ ชนิดทศนิยมแบบ floating-point อย่าง real นั้นเร็วแต่เป็นค่าโดยประมาณ ดังนั้นเก็บไว้ใช้กับข้อมูลทางวิทยาศาสตร์หรือการวัดที่ยอมรับการปัดเศษเล็กน้อยได้
CREATE TABLE measurements ( count_int integer, count_big bigint, price numeric(10, 2), reading real);
INSERT INTO measurements VALUES (42, 9000000000, 19.99, 3.14);SELECT * FROM measurements; count_int | count_big | price | reading-----------+------------+-------+--------- 42 | 9000000000 | 19.99 | 3.14(1 row)PostgreSQL มีชนิด string ที่ใช้ในชีวิตประจำวันสองแบบ text เก็บ string ความยาวเท่าใดก็ได้ และ varchar(n) เก็บ string ได้ถึง n ตัวอักษร ไม่มีบทลงโทษด้านประสิทธิภาพสำหรับ text ขีดจำกัดความยาวบน varchar คือความแตกต่างที่แท้จริงเพียงอย่างเดียว และคุณสามารถบังคับความยาวด้วย constraint แบบ CHECK แทนได้เสมอ
CREATE TABLE notes ( title varchar(120), body text);
INSERT INTO notes VALUES ('Release plan', 'Ship the new dashboard on Friday.');SELECT title, body FROM notes; title | body--------------+----------------------------------- Release plan | Ship the new dashboard on Friday.(1 row)Boolean
หัวข้อที่มีชื่อว่า “Boolean”column แบบ boolean เก็บ true, false หรือ null PostgreSQL ยอมรับการเขียนได้หลายแบบตอน input — true, 't', 'yes', 1 และค่า false ที่เทียบเท่ากัน — แต่จะแสดงค่าเป็น t หรือ f เสมอ
SELECT true AS yes, false AS no, null::boolean AS unknown; yes | no | unknown-----+----+--------- t | f |(1 row)วันที่และเวลา
หัวข้อที่มีชื่อว่า “วันที่และเวลา”สำหรับวันตามปฏิทินที่ไม่มีเวลา ให้ใช้ date สำหรับช่วงเวลาขณะหนึ่ง ให้ใช้ timestamptz — ย่อมาจาก timestamp with time zone — ซึ่งเก็บช่วงเวลานั้นเป็น UTC และแปลงไปและกลับจาก time zone ของ session ของคุณโดยอัตโนมัติ ชนิด timestamp ธรรมดาที่ไม่มี zone นั้นดูคล้ายกันแต่สูญเสียบริบทนั้นไป และเป็นแหล่งของบั๊กที่พบได้บ่อย
SELECT current_date AS today, now() AS moment, date '2026-01-31' AS a_day; today | moment | a_day------------+-------------------------------+------------ 2026-06-25 | 2026-06-25 09:14:22.51+00 | 2026-01-31(1 row)uuid เก็บตัวระบุที่ไม่ซ้ำกันในระดับสากลขนาด 128 บิต UUID เป็น primary key ที่ดีเมื่อ client ต้องเป็นคนสร้าง id เอง หรือเมื่อ id ต้องคาดเดาไม่ได้ เพราะไม่เปิดเผยจำนวน row อย่างที่ sequence ทำ
SELECT gen_random_uuid() AS id; id-------------------------------------- 6f3a1b2c-9d4e-4f70-8a1b-2c3d4e5f6a7b(1 row)JSON ด้วย jsonb
หัวข้อที่มีชื่อว่า “JSON ด้วย jsonb”ชนิด jsonb เก็บ JSON ในรูปแบบ binary ที่ถูกแยกองค์ประกอบไว้แล้ว ซึ่ง query และทำ index ได้เร็ว เลือกใช้แทนชนิด json ธรรมดาที่เก็บข้อความดิบและ re-parse ทุกครั้งที่เข้าถึง คุณสามารถดึงค่าออกมาได้ด้วย arrow operator
CREATE TABLE events ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, payload jsonb);
INSERT INTO events (payload)VALUES ('{"kind": "signup", "plan": "pro"}');
SELECT payload ->> 'plan' AS plan FROM events; plan------ pro(1 row)ชนิดใดก็ตามสามารถกลายเป็น array ได้ด้วยการเติมวงเล็บเหลี่ยม ดังนั้น int[] จึงเป็น column ของ array ของจำนวนเต็ม array เหมาะกับ list ขนาดเล็กที่มีลำดับและเป็นของ row เดียว เช่น tag ค่าคงที่ของ array เขียนด้วยปีกกาภายใน string ที่อยู่ในเครื่องหมายคำพูด
CREATE TABLE articles ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, tags text[]);
INSERT INTO articles (tags) VALUES ('{"sql", "postgres", "intro"}');SELECT tags, tags[1] AS first_tag FROM articles; tags | first_tag------------------------+----------- {sql,postgres,intro} | sql(1 row)ชนิดแบบ enumerated
หัวข้อที่มีชื่อว่า “ชนิดแบบ enumerated”เมื่อ column อาจเก็บได้เฉพาะชุดป้ายชื่อที่ตายตัว enum จะทำให้ค่าที่อนุญาตเป็นส่วนหนึ่งของชนิด คุณนิยามครั้งเดียวด้วย CREATE TYPE แล้วใช้เหมือนชนิดในตัวทั่วไป ค่าต่าง ๆ จะเรียงลำดับตามที่คุณไล่รายการไว้
CREATE TYPE order_status AS ENUM ('pending', 'paid', 'shipped', 'cancelled');
CREATE TABLE orders ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, status order_status NOT NULL DEFAULT 'pending');
INSERT INTO orders (status) VALUES ('paid');SELECT id, status FROM orders; id | status----+-------- 1 | paid(1 row)ใน pgAdmin: ชนิดที่กำหนดเองซึ่งสร้างด้วย CREATE TYPE จะปรากฏใต้ Schemas แล้ว public แล้ว Types ในแผนผัง browser ข้าง ๆ โหนด Tables คุณสามารถตรวจดูป้ายชื่อที่อนุญาตของ enum ที่นั่นได้โดยไม่ต้องเขียน query
เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”- ใช้
timestamptzไม่ใช่timestampสำหรับช่วงเวลาขณะหนึ่งใด ๆ การเก็บเป็น UTC และแปลงตาม session ช่วยหลีกเลี่ยงบั๊กเรื่อง time-zone ได้ทั้งหมวดหมู่ หยิบtimestampธรรมดามาใช้เฉพาะเมื่อค่าหนึ่งไม่มี zone จริง ๆ เท่านั้น เช่น นาฬิกาปลุก local ที่เกิดซ้ำ ๆ - ใช้
textเป็นค่าเริ่มต้น และใช้varchar(n)เฉพาะเมื่อความยาวสูงสุดที่แท้จริงเป็นส่วนหนึ่งของกฎ ไม่มีความแตกต่างด้านความเร็ว และ constraint แบบCHECKสามารถบังคับขีดจำกัดบนtextได้หากคุณต้องการ - เก็บเงินใน
numericไม่ใช่ในrealหรือdouble precisionfloating point แบบ binary ไม่สามารถแทนค่าอย่าง 0.10 ได้อย่างแม่นยำ ซึ่งนำไปสู่ข้อผิดพลาดในการปัดเศษที่สะสมขึ้น - เลือก
jsonbมากกว่าjsonรูปแบบ binary นั้น query ได้เร็วกว่าและทำ index ได้ ชนิดjsonแบบข้อความมีประโยชน์เป็นหลักเมื่อคุณต้องรักษารูปแบบของ input เดิมไว้ทุกประการ enumเยี่ยมมากสำหรับชุดป้ายชื่อที่ตายตัวจริง ๆ แต่การเพิ่มหรือจัดลำดับค่าใหม่ในภายหลังต้องใช้ALTER TYPEหากชุดนั้นเปลี่ยนบ่อย table lookup เล็ก ๆ ที่มี foreign key จะยืดหยุ่นกว่า
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
column ชนิดตายตัว (เช่น integer, text, timestamptz) | บังคับความถูกต้องของข้อมูล ใช้พื้นที่น้อยกว่า เปรียบเทียบและทำ index ได้เร็ว | ต้องรู้โครงสร้างข้อมูลล่วงหน้า เปลี่ยนชนิดทีหลังมีต้นทุน |
jsonb สำหรับข้อมูลรูปร่างไม่แน่นอน | ยืดหยุ่นสูง ไม่ต้อง migrate schema ทุกครั้งที่โครงสร้างเปลี่ยน | validate และทำ index ยากกว่า ค้นหาลึก ๆ ช้ากว่า column ปกติ |
numeric สำหรับเงิน | แม่นยำแบบ exact ไม่มีปัญหาปัดเศษสะสม | ช้ากว่า floating point เล็กน้อยเมื่อคำนวณจำนวนมาก |
float/double precision | คำนวณเร็ว เหมาะกับงานวิทยาศาสตร์ | เป็นค่าประมาณ ไม่เหมาะกับเงินเพราะปัดเศษผิดพลาดสะสมได้ |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- ใช้ชนิด
money— มีปัญหาเรื่อง locale และความแม่นยำที่ควบคุมยาก ทีมส่วนใหญ่จึงเลี่ยงใน schema ใหม่ แล้วใช้numericแทน - เก็บเวลาโดยไม่มี time zone (
timestampแทนtimestamptz) — ทำให้ค่าเวลาสูญเสียบริบทว่าเป็น time zone ไหน กลายเป็นบั๊กที่ปรากฏเฉพาะตอนระบบมีผู้ใช้ข้าม time zone - ใช้
float/double precisionกับจำนวนเงิน — floating point แบบ binary แทนค่าอย่าง 0.10 ไม่ได้อย่างแม่นยำ ต้องใช้numericหรือเก็บเป็นจำนวนเต็มหน่วย cent แทน
💡 ตัวอย่างจากของจริง
ระบบธนาคารและ ledger ทางการเงิน — ใช้
numericหรือเก็บจำนวนเงินเป็น integer หน่วยเล็กสุด (เช่น cent) แทบทุกที่ ไม่มีทีมไหนเก็บยอดเงินเป็นfloatเพราะ error สะสมจากการปัดเศษยอมรับไม่ได้ในงานบัญชีNotion — ใช้ JSONB สำหรับส่วนของ schema ที่โครงสร้างยืดหยุ่น (เนื้อหา block) แต่ยังคง column แบบ typed สำหรับ metadata ที่ต้อง query และบังคับ constraint บ่อย ๆ เป็นตัวอย่างของการผสมสองแนวทางในระบบเดียว