ข้ามไปยังเนื้อหา

ชนิดข้อมูล

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)

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)

ชนิด 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)

เมื่อ 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 precision floating point แบบ binary ไม่สามารถแทนค่าอย่าง 0.10 ได้อย่างแม่นยำ ซึ่งนำไปสู่ข้อผิดพลาดในการปัดเศษที่สะสมขึ้น
  • เลือก jsonb มากกว่า json รูปแบบ binary นั้น query ได้เร็วกว่าและทำ index ได้ ชนิด json แบบข้อความมีประโยชน์เป็นหลักเมื่อคุณต้องรักษารูปแบบของ input เดิมไว้ทุกประการ
  • enum เยี่ยมมากสำหรับชุดป้ายชื่อที่ตายตัวจริง ๆ แต่การเพิ่มหรือจัดลำดับค่าใหม่ในภายหลังต้องใช้ ALTER TYPE หากชุดนั้นเปลี่ยนบ่อย table lookup เล็ก ๆ ที่มี foreign key จะยืดหยุ่นกว่า
ตัวเลือกBenefitCost
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 บ่อย ๆ เป็นตัวอย่างของการผสมสองแนวทางในระบบเดียว

คุณควรใช้ชนิดใดเพื่อเก็บจำนวนเงินอย่างแม่นยำ?
เหตุใด timestamptz จึงเป็นที่นิยมกว่า timestamp ธรรมดาสำหรับช่วงเวลาขณะหนึ่ง?
ชนิด JSON ใดที่เก็บข้อมูลในรูปแบบ binary ที่ทำ index และ query ได้อย่างมีประสิทธิภาพ?