CREATE Schema และ Tables ใน PostgreSQL

Schema และ Table เป็นโครงสร้างพื้นฐานที่สำคัญที่สุดในระบบฐานข้อมูล PostgreSQL ก่อนที่จะเริ่มเก็บข้อมูลใด ๆ ได้ จำเป็นต้องออกแบบและสร้าง Schema เพื่อจัดกลุ่มตาราง แล้วจึงสร้างตารางพร้อมกำหนดคอลัมน์ ชนิดข้อมูล และ Constraints ให้เหมาะสม

บทความนี้จะอธิบายการสร้างและจัดการ Schema การสร้างตารางพร้อมตัวอย่างจริง การเลือกชนิดข้อมูลที่เหมาะสม การใช้ Constraints ต่าง ๆ รวมถึงเทคนิคขั้นสูงอย่าง Table Inheritance และ Partitioning

ทำความเข้าใจ Schema

Schema คือ Namespace ที่ใช้จัดกลุ่มวัตถุต่าง ๆ ในฐานข้อมูล เช่น ตาราง View Function และ Sequence ทุกฐานข้อมูลจะมี Schema เริ่มต้นชื่อ public ซึ่งเป็นที่เก็บวัตถุทั้งหมดหากไม่ได้ระบุ Schema เฉพาะ

การใช้ Schema แยกกันช่วยจัดระเบียบฐานข้อมูลได้ดีขึ้น เช่น แยกตามทีม แยกตามโมดูลของแอปพลิเคชัน หรือแยกข้อมูลระหว่างผู้เช่า (Multi-Tenant) ทำให้จัดการสิทธิ์และดูแลรักษาได้สะดวกขึ้น

การสร้างและจัดการ Schema

สร้าง Schema

-- สร้าง schema พื้นฐาน
CREATE SCHEMA inventory;

-- สร้าง schema พร้อมกำหนดเจ้าของ
CREATE SCHEMA sales AUTHORIZATION app_user;

-- สร้าง schema ถ้ายังไม่มี (ป้องกัน error)
CREATE SCHEMA IF NOT EXISTS analytics;

-- ดู schema ทั้งหมดใน database
SELECT schema_name FROM information_schema.schemata;

ตั้งค่า Search Path

Search Path กำหนดลำดับการค้นหา Schema เมื่อเรียกชื่อวัตถุโดยไม่ระบุ Schema นำหน้า

-- ดู search_path ปัจจุบัน
SHOW search_path;

-- ตั้ง search_path สำหรับ session ปัจจุบัน
SET search_path TO inventory, public;

-- ตั้ง search_path ถาวรสำหรับ role
ALTER ROLE app_user SET search_path = inventory, sales, public;

-- ตั้ง search_path ถาวรสำหรับ database
ALTER DATABASE mydb SET search_path = inventory, public;

แก้ไขและลบ Schema

-- เปลี่ยนชื่อ schema
ALTER SCHEMA old_name RENAME TO new_name;

-- เปลี่ยนเจ้าของ schema
ALTER SCHEMA inventory OWNER TO admin_user;

-- ลบ schema (ต้องว่างเปล่า)
DROP SCHEMA temp_schema;

-- ลบ schema พร้อมวัตถุทั้งหมดข้างใน
DROP SCHEMA temp_schema CASCADE;

ชนิดข้อมูล (Data Types) ที่ใช้บ่อย

การเลือกชนิดข้อมูลที่เหมาะสมมีผลต่อทั้งประสิทธิภาพและความถูกต้องของข้อมูล ด้านล่างเป็นชนิดข้อมูลหลักที่ควรรู้จัก

ตัวเลข (Numeric Types)

-- จำนวนเต็ม
SMALLINT          -- -32,768 ถึง 32,767 (2 bytes)
INTEGER           -- -2 พันล้าน ถึง 2 พันล้าน (4 bytes)
BIGINT            -- -9 ล้านล้าน ถึง 9 ล้านล้าน (8 bytes)

-- Auto-increment
SMALLSERIAL       -- SMALLINT + auto increment
SERIAL            -- INTEGER + auto increment
BIGSERIAL         -- BIGINT + auto increment

-- ทศนิยม
NUMERIC(10, 2)    -- ทศนิยมแม่นยำ (เหมาะกับเงิน)
REAL              -- ทศนิยม 6 หลัก (4 bytes)
DOUBLE PRECISION  -- ทศนิยม 15 หลัก (8 bytes)

ข้อความ (Text Types)

CHAR(n)           -- ความยาวคงที่ เติมช่องว่างให้ครบ
VARCHAR(n)        -- ความยาวจำกัด ไม่เติมช่องว่าง
TEXT              -- ความยาวไม่จำกัด (แนะนำใช้แทน VARCHAR)

วันที่และเวลา (Date/Time Types)

DATE              -- วันที่อย่างเดียว (2026-04-07)
TIME              -- เวลาอย่างเดียว (14:30:00)
TIMESTAMP         -- วันที่ + เวลา (ไม่มี timezone)
TIMESTAMPTZ       -- วันที่ + เวลา + timezone (แนะนำ)
INTERVAL          -- ช่วงเวลา ('1 year 2 months 3 days')

ชนิดข้อมูลพิเศษ

BOOLEAN           -- true / false / null
UUID              -- Universally Unique Identifier
JSONB             -- JSON แบบ binary (ค้นหาเร็ว แนะนำ)
JSON              -- JSON แบบ text (เก็บตามต้นฉบับ)
INET              -- IP address (IPv4/IPv6)
CIDR              -- IP network
MACADDR           -- MAC address
BYTEA             -- ข้อมูล binary
ARRAY             -- อาร์เรย์ เช่น INTEGER[], TEXT[]
MONEY             -- สกุลเงิน (ไม่แนะนำ ใช้ NUMERIC แทน)

การสร้างตาราง (CREATE TABLE)

โครงสร้างพื้นฐาน

CREATE TABLE inventory.products (
    id          SERIAL PRIMARY KEY,
    sku         VARCHAR(50) NOT NULL UNIQUE,
    name        VARCHAR(200) NOT NULL,
    description TEXT,
    price       NUMERIC(10, 2) NOT NULL CHECK (price >= 0),
    quantity    INTEGER NOT NULL DEFAULT 0 CHECK (quantity >= 0),
    category_id INTEGER,
    is_active   BOOLEAN NOT NULL DEFAULT true,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

ตัวอย่างข้างต้นสร้างตาราง products ใน schema inventory โดยมี Primary Key แบบ auto-increment, Constraints หลายตัว และค่า Default สำหรับบางคอลัมน์

ตารางที่มี Foreign Key

-- สร้างตาราง categories ก่อน
CREATE TABLE inventory.categories (
    id    SERIAL PRIMARY KEY,
    name  VARCHAR(100) NOT NULL UNIQUE,
    slug  VARCHAR(100) NOT NULL UNIQUE
);

-- สร้างตาราง orders ที่อ้างอิง customers
CREATE TABLE sales.orders (
    id           BIGSERIAL PRIMARY KEY,
    customer_id  INTEGER NOT NULL REFERENCES sales.customers(id),
    order_date   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    status       VARCHAR(20) NOT NULL DEFAULT 'pending'
                 CHECK (status IN ('pending', 'confirmed', 'shipped', 'delivered', 'cancelled')),
    total_amount NUMERIC(12, 2) NOT NULL CHECK (total_amount >= 0)
);

-- สร้างตาราง order_items ที่อ้างอิงทั้ง orders และ products
CREATE TABLE sales.order_items (
    id         BIGSERIAL PRIMARY KEY,
    order_id   BIGINT NOT NULL REFERENCES sales.orders(id) ON DELETE CASCADE,
    product_id INTEGER NOT NULL REFERENCES inventory.products(id) ON DELETE RESTRICT,
    quantity   INTEGER NOT NULL CHECK (quantity > 0),
    unit_price NUMERIC(10, 2) NOT NULL CHECK (unit_price >= 0),
    UNIQUE (order_id, product_id)
);

Foreign Key กำหนดความสัมพันธ์ระหว่างตาราง โดย ON DELETE CASCADE หมายถึงเมื่อลบแถวหลักจะลบแถวที่อ้างอิงด้วย ส่วน ON DELETE RESTRICT จะป้องกันไม่ให้ลบแถวหลักหากยังมีแถวอ้างอิงอยู่

Constraints ที่ควรรู้

PRIMARY KEY

-- Primary Key คอลัมน์เดียว
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL
);

-- Composite Primary Key (หลายคอลัมน์)
CREATE TABLE user_roles (
    user_id INTEGER NOT NULL REFERENCES users(id),
    role_id INTEGER NOT NULL REFERENCES roles(id),
    PRIMARY KEY (user_id, role_id)
);

UNIQUE

-- Unique คอลัมน์เดียว
CREATE TABLE users (
    id    SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE
);

-- Unique หลายคอลัมน์ (composite unique)
CREATE TABLE subscriptions (
    id          SERIAL PRIMARY KEY,
    user_id     INTEGER NOT NULL,
    plan_id     INTEGER NOT NULL,
    UNIQUE (user_id, plan_id)
);

-- Partial Unique (unique เฉพาะเงื่อนไข)
CREATE UNIQUE INDEX idx_active_email
    ON users (email) WHERE is_active = true;

CHECK

CREATE TABLE employees (
    id     SERIAL PRIMARY KEY,
    name   VARCHAR(100) NOT NULL,
    age    INTEGER CHECK (age >= 18 AND age <= 100),
    salary NUMERIC(10, 2) CHECK (salary > 0),
    email  VARCHAR(255) CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
    CONSTRAINT valid_hire_date CHECK (hire_date <= CURRENT_DATE)
);

NOT NULL และ DEFAULT

CREATE TABLE articles (
    id          SERIAL PRIMARY KEY,
    title       VARCHAR(200) NOT NULL,
    content     TEXT NOT NULL,
    status      VARCHAR(20) NOT NULL DEFAULT 'draft',
    view_count  INTEGER NOT NULL DEFAULT 0,
    published_at TIMESTAMPTZ,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

การใช้ Generated Columns

Generated Column คือคอลัมน์ที่คำนวณค่าอัตโนมัติจากคอลัมน์อื่นในตารางเดียวกัน ไม่ต้องคำนวณเองทุกครั้งที่ query

CREATE TABLE products (
    id          SERIAL PRIMARY KEY,
    name        VARCHAR(200) NOT NULL,
    price       NUMERIC(10, 2) NOT NULL,
    tax_rate    NUMERIC(4, 2) NOT NULL DEFAULT 0.07,
    -- Stored generated column
    price_with_tax NUMERIC(10, 2) GENERATED ALWAYS AS (price * (1 + tax_rate)) STORED
);

-- ตัวอย่าง: full_name จากชื่อ + นามสกุล
CREATE TABLE contacts (
    id         SERIAL PRIMARY KEY,
    first_name VARCHAR(100) NOT NULL,
    last_name  VARCHAR(100) NOT NULL,
    full_name  TEXT GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED
);

การแก้ไขตาราง (ALTER TABLE)

-- เพิ่มคอลัมน์
ALTER TABLE inventory.products ADD COLUMN weight NUMERIC(8, 2);

-- ลบคอลัมน์
ALTER TABLE inventory.products DROP COLUMN weight;

-- เปลี่ยนชนิดข้อมูล
ALTER TABLE inventory.products ALTER COLUMN sku TYPE VARCHAR(100);

-- เปลี่ยน default value
ALTER TABLE inventory.products ALTER COLUMN quantity SET DEFAULT 10;

-- เพิ่ม NOT NULL
ALTER TABLE inventory.products ALTER COLUMN description SET NOT NULL;

-- ลบ NOT NULL
ALTER TABLE inventory.products ALTER COLUMN description DROP NOT NULL;

-- เพิ่ม CHECK constraint
ALTER TABLE inventory.products ADD CONSTRAINT check_sku_format
    CHECK (sku ~ '^[A-Z]{2,4}-[0-9]{4,8}$');

-- ลบ constraint
ALTER TABLE inventory.products DROP CONSTRAINT check_sku_format;

-- เปลี่ยนชื่อคอลัมน์
ALTER TABLE inventory.products RENAME COLUMN name TO product_name;

-- เปลี่ยนชื่อตาราง
ALTER TABLE inventory.products RENAME TO items;

-- ย้ายตารางไป schema อื่น
ALTER TABLE inventory.items SET SCHEMA sales;

Index สำหรับเพิ่มประสิทธิภาพ

Index ช่วยเร่งความเร็วในการค้นหาข้อมูล แต่ใช้พื้นที่เพิ่มและทำให้การเขียนข้อมูลช้าลงเล็กน้อย ควรสร้าง Index เฉพาะคอลัมน์ที่ใช้ค้นหาบ่อย

-- B-tree Index (ค่าเริ่มต้น เหมาะกับ =, <, >, BETWEEN)
CREATE INDEX idx_products_category ON inventory.products (category_id);

-- Unique Index
CREATE UNIQUE INDEX idx_products_sku ON inventory.products (sku);

-- Composite Index (หลายคอลัมน์)
CREATE INDEX idx_orders_customer_date
    ON sales.orders (customer_id, order_date DESC);

-- Partial Index (เฉพาะเงื่อนไข)
CREATE INDEX idx_active_products
    ON inventory.products (name) WHERE is_active = true;

-- GIN Index สำหรับ JSONB
CREATE INDEX idx_products_metadata
    ON inventory.products USING GIN (metadata);

-- GIN Index สำหรับ Full-Text Search
CREATE INDEX idx_products_search
    ON inventory.products USING GIN (to_tsvector('english', name || ' ' || description));

-- Expression Index
CREATE INDEX idx_products_lower_name
    ON inventory.products (LOWER(name));

Table Partitioning

Partitioning แบ่งตารางขนาดใหญ่ออกเป็นส่วนย่อย ช่วยเพิ่มประสิทธิภาพการ query และจัดการข้อมูลเก่าได้สะดวก

Range Partitioning

-- สร้างตารางหลักแบบ partitioned
CREATE TABLE sales.transactions (
    id           BIGSERIAL,
    customer_id  INTEGER NOT NULL,
    amount       NUMERIC(12, 2) NOT NULL,
    created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW()
) PARTITION BY RANGE (created_at);

-- สร้าง partition สำหรับแต่ละเดือน
CREATE TABLE sales.transactions_2026_01
    PARTITION OF sales.transactions
    FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');

CREATE TABLE sales.transactions_2026_02
    PARTITION OF sales.transactions
    FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');

CREATE TABLE sales.transactions_2026_03
    PARTITION OF sales.transactions
    FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');

-- สร้าง default partition (จับข้อมูลที่ไม่ตรงเงื่อนไขใด)
CREATE TABLE sales.transactions_default
    PARTITION OF sales.transactions DEFAULT;

List Partitioning

-- แบ่งตามค่าที่กำหนด
CREATE TABLE sales.orders_by_region (
    id        BIGSERIAL,
    region    VARCHAR(20) NOT NULL,
    total     NUMERIC(12, 2) NOT NULL,
    order_date TIMESTAMPTZ NOT NULL DEFAULT NOW()
) PARTITION BY LIST (region);

CREATE TABLE sales.orders_north
    PARTITION OF sales.orders_by_region
    FOR VALUES IN ('north', 'northeast');

CREATE TABLE sales.orders_central
    PARTITION OF sales.orders_by_region
    FOR VALUES IN ('central', 'east', 'west');

CREATE TABLE sales.orders_south
    PARTITION OF sales.orders_by_region
    FOR VALUES IN ('south');

Temporary Tables

Temporary Table เป็นตารางชั่วคราวที่อยู่เฉพาะใน session หรือ transaction ปัจจุบัน เหมาะสำหรับเก็บผลลัพธ์ระหว่างการประมวลผล

-- Temporary table (หายเมื่อจบ session)
CREATE TEMPORARY TABLE temp_results (
    id      INTEGER,
    score   NUMERIC(5, 2),
    status  VARCHAR(20)
);

-- Temporary table ที่หายเมื่อจบ transaction
CREATE TEMPORARY TABLE temp_batch (
    batch_id  INTEGER,
    data      JSONB
) ON COMMIT DROP;

-- Temporary table ที่ลบข้อมูลเมื่อจบ transaction แต่โครงสร้างยังอยู่
CREATE TEMPORARY TABLE temp_staging (
    row_id INTEGER,
    value  TEXT
) ON COMMIT DELETE ROWS;

ตัวอย่างการออกแบบฐานข้อมูลจริง

ด้านล่างเป็นตัวอย่างการสร้าง Schema และตารางสำหรับระบบ E-Commerce แบบครบวงจร

-- ============================================
-- E-Commerce Database Schema
-- ============================================

-- สร้าง schemas
CREATE SCHEMA IF NOT EXISTS store;
CREATE SCHEMA IF NOT EXISTS payment;

-- ตาราง customers
CREATE TABLE store.customers (
    id          BIGSERIAL PRIMARY KEY,
    email       VARCHAR(255) NOT NULL UNIQUE,
    first_name  VARCHAR(100) NOT NULL,
    last_name   VARCHAR(100) NOT NULL,
    phone       VARCHAR(20),
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- ตาราง addresses
CREATE TABLE store.addresses (
    id          BIGSERIAL PRIMARY KEY,
    customer_id BIGINT NOT NULL REFERENCES store.customers(id) ON DELETE CASCADE,
    address     TEXT NOT NULL,
    city        VARCHAR(100) NOT NULL,
    province    VARCHAR(100) NOT NULL,
    postal_code VARCHAR(10) NOT NULL,
    is_default  BOOLEAN NOT NULL DEFAULT false
);

-- ตาราง categories
CREATE TABLE store.categories (
    id        SERIAL PRIMARY KEY,
    name      VARCHAR(100) NOT NULL,
    slug      VARCHAR(100) NOT NULL UNIQUE,
    parent_id INTEGER REFERENCES store.categories(id)
);

-- ตาราง products
CREATE TABLE store.products (
    id          BIGSERIAL PRIMARY KEY,
    sku         VARCHAR(50) NOT NULL UNIQUE,
    name        VARCHAR(200) NOT NULL,
    description TEXT,
    price       NUMERIC(10, 2) NOT NULL CHECK (price >= 0),
    stock       INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0),
    category_id INTEGER REFERENCES store.categories(id),
    metadata    JSONB DEFAULT '{}',
    is_active   BOOLEAN NOT NULL DEFAULT true,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- ตาราง orders
CREATE TABLE store.orders (
    id          BIGSERIAL PRIMARY KEY,
    customer_id BIGINT NOT NULL REFERENCES store.customers(id),
    address_id  BIGINT NOT NULL REFERENCES store.addresses(id),
    status      VARCHAR(20) NOT NULL DEFAULT 'pending'
                CHECK (status IN ('pending','confirmed','shipped','delivered','cancelled')),
    total       NUMERIC(12, 2) NOT NULL DEFAULT 0,
    note        TEXT,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- ตาราง order_items
CREATE TABLE store.order_items (
    id         BIGSERIAL PRIMARY KEY,
    order_id   BIGINT NOT NULL REFERENCES store.orders(id) ON DELETE CASCADE,
    product_id BIGINT NOT NULL REFERENCES store.products(id),
    quantity   INTEGER NOT NULL CHECK (quantity > 0),
    unit_price NUMERIC(10, 2) NOT NULL,
    UNIQUE (order_id, product_id)
);

-- ตาราง payments
CREATE TABLE payment.transactions (
    id         BIGSERIAL PRIMARY KEY,
    order_id   BIGINT NOT NULL REFERENCES store.orders(id),
    method     VARCHAR(30) NOT NULL CHECK (method IN ('credit_card','bank_transfer','promptpay')),
    amount     NUMERIC(12, 2) NOT NULL CHECK (amount > 0),
    status     VARCHAR(20) NOT NULL DEFAULT 'pending',
    paid_at    TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- สร้าง Indexes
CREATE INDEX idx_products_category ON store.products (category_id);
CREATE INDEX idx_products_active ON store.products (is_active) WHERE is_active = true;
CREATE INDEX idx_orders_customer ON store.orders (customer_id);
CREATE INDEX idx_orders_status ON store.orders (status);
CREATE INDEX idx_order_items_order ON store.order_items (order_id);
CREATE INDEX idx_payments_order ON payment.transactions (order_id);
CREATE INDEX idx_products_metadata ON store.products USING GIN (metadata);

สรุป

การสร้าง Schema และ Table ใน PostgreSQL เป็นพื้นฐานที่สำคัญของการออกแบบฐานข้อมูล Schema ช่วยจัดกลุ่มวัตถุให้เป็นระเบียบ ขณะที่ตารางต้องออกแบบให้มีชนิดข้อมูลและ Constraints ที่เหมาะสมตั้งแต่แรก การใช้ Index เร่งประสิทธิภาพ Partitioning จัดการข้อมูลขนาดใหญ่ และ Generated Columns ลดการคำนวณซ้ำ ล้วนเป็นเครื่องมือที่ช่วยให้ฐานข้อมูลทำงานได้อย่างมีประสิทธิภาพและดูแลรักษาง่าย

แนะนำบริการ DE

การออกแบบและสร้างฐานข้อมูลต้องอาศัยเซิร์ฟเวอร์ที่ให้คุณควบคุมการติดตั้งและตั้งค่าได้อย่างอิสระ Cloud VPS ของ DE รองรับการติดตั้ง PostgreSQL ทุกเวอร์ชัน พร้อม root access เต็มรูปแบบ สามารถปรับแต่ง Schema, Partitioning, Index และ Extension ต่าง ๆ ได้ตามต้องการ เหมาะสำหรับงาน Production ที่ต้องการประสิทธิภาพสูง

สำหรับผู้ที่ต้องการโฮสต์เว็บไซต์หรือแอปพลิเคชันที่ใช้ฐานข้อมูลโดยไม่ต้องจัดการเซิร์ฟเวอร์เอง Cloud Hosting ของ DE มีระบบจัดการฐานข้อมูลผ่าน Control Panel ที่ใช้งานง่าย พร้อมรองรับเว็บไซต์ได้ทันที