236 lines
5.8 KiB
Markdown
236 lines
5.8 KiB
Markdown
# PostgreSQL Rules
|
|
|
|
Guidelines for designing schemas specific to PostgreSQL.
|
|
|
|
---
|
|
|
|
## Data Types
|
|
|
|
| Use case | Type | Notes |
|
|
|----------|------|-------|
|
|
| Primary key | `BIGSERIAL` | Auto-increment BIGINT |
|
|
| Primary key (small) | `SERIAL` | Auto-increment INT |
|
|
| Foreign key | Match with PK type | |
|
|
| Money/Price | `NUMERIC(18,2)` | Precise |
|
|
| Boolean | `BOOLEAN` | TRUE/FALSE/NULL |
|
|
| Short text | `VARCHAR(n)` or `TEXT` | TEXT has no limit |
|
|
| JSON | `JSONB` | **Queryable**, prefer over JSON |
|
|
| Timestamp | `TIMESTAMPTZ` | **Always use with timezone** |
|
|
| Date | `DATE` | |
|
|
| UUID | `UUID` | Native type |
|
|
| Array | `TEXT[]`, `INT[]` | |
|
|
| IP Address | `INET` | |
|
|
|
|
### TIMESTAMPTZ vs TIMESTAMP
|
|
**IMPORTANT**: Always use `TIMESTAMPTZ` to avoid timezone issues.
|
|
|
|
---
|
|
|
|
## Comments (NO metadata tables needed)
|
|
|
|
PostgreSQL supports `COMMENT ON`:
|
|
|
|
### Table comment
|
|
```sql
|
|
COMMENT ON TABLE orders IS 'Table storing customer orders';
|
|
```
|
|
|
|
### Column comments
|
|
```sql
|
|
COMMENT ON COLUMN orders.id IS 'Primary key, auto increment';
|
|
COMMENT ON COLUMN orders.order_number IS 'Unique order code, format: ORD-YYYYMMDD-XXXXX';
|
|
COMMENT ON COLUMN orders.status IS 'Status: pending|confirmed|shipped|cancelled';
|
|
```
|
|
|
|
### Query comments
|
|
```sql
|
|
-- Table comment
|
|
SELECT obj_description('orders'::regclass);
|
|
|
|
-- Column comments
|
|
SELECT
|
|
c.column_name,
|
|
pgd.description
|
|
FROM information_schema.columns c
|
|
LEFT JOIN pg_catalog.pg_statio_all_tables st
|
|
ON c.table_schema = st.schemaname AND c.table_name = st.relname
|
|
LEFT JOIN pg_catalog.pg_description pgd
|
|
ON pgd.objoid = st.relid AND pgd.objsubid = c.ordinal_position
|
|
WHERE c.table_name = 'orders';
|
|
```
|
|
|
|
---
|
|
|
|
## Index Types
|
|
|
|
### B-Tree (default)
|
|
```sql
|
|
CREATE INDEX idx_orders_status ON orders(status);
|
|
CREATE INDEX idx_orders_user_status_created ON orders(user_id, status, created_at DESC);
|
|
```
|
|
|
|
### Partial Index (very useful)
|
|
```sql
|
|
-- Only index non-deleted records
|
|
CREATE INDEX idx_orders_active ON orders(user_id, status)
|
|
WHERE deleted_at IS NULL;
|
|
|
|
-- Only index pending orders
|
|
CREATE INDEX idx_orders_pending ON orders(created_at)
|
|
WHERE status = 'pending';
|
|
```
|
|
|
|
### GIN (for JSONB, Arrays, Full-text)
|
|
```sql
|
|
CREATE INDEX idx_orders_metadata ON orders USING GIN(metadata);
|
|
CREATE INDEX idx_products_tags ON products USING GIN(tags);
|
|
|
|
-- Full-text search
|
|
CREATE INDEX idx_products_search ON products
|
|
USING GIN(to_tsvector('english', name || ' ' || description));
|
|
```
|
|
|
|
### BRIN (for large tables with ordered data)
|
|
```sql
|
|
CREATE INDEX idx_logs_created ON logs USING BRIN(created_at);
|
|
```
|
|
|
|
---
|
|
|
|
## Sequences (Auto Increment)
|
|
|
|
```sql
|
|
-- SERIAL/BIGSERIAL auto-creates sequence
|
|
CREATE TABLE users (id BIGSERIAL PRIMARY KEY);
|
|
|
|
-- Identity columns (PostgreSQL 10+)
|
|
CREATE TABLE users (
|
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY
|
|
);
|
|
|
|
-- Custom sequence
|
|
CREATE SEQUENCE order_number_seq START 1000;
|
|
```
|
|
|
|
---
|
|
|
|
## Auto update timestamp (requires trigger)
|
|
|
|
PostgreSQL **does not have** `ON UPDATE CURRENT_TIMESTAMP`. Need to create trigger:
|
|
|
|
```sql
|
|
-- Function
|
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Apply to table
|
|
CREATE TRIGGER trg_orders_updated_at
|
|
BEFORE UPDATE ON orders
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_updated_at_column();
|
|
```
|
|
|
|
---
|
|
|
|
## ENUM Type
|
|
|
|
```sql
|
|
-- Create enum type
|
|
CREATE TYPE order_status AS ENUM (
|
|
'pending', 'confirmed', 'shipped', 'cancelled', 'completed'
|
|
);
|
|
|
|
-- Use in table
|
|
CREATE TABLE orders (
|
|
status order_status NOT NULL DEFAULT 'pending'
|
|
);
|
|
|
|
-- Add new value
|
|
ALTER TYPE order_status ADD VALUE 'refunded' AFTER 'completed';
|
|
```
|
|
|
|
**Note:** Cannot remove values from ENUM.
|
|
|
|
---
|
|
|
|
## Partitioning
|
|
|
|
### Range partitioning
|
|
```sql
|
|
CREATE TABLE fact_orders (
|
|
id BIGSERIAL,
|
|
order_date DATE NOT NULL,
|
|
...
|
|
) PARTITION BY RANGE (order_date);
|
|
|
|
CREATE TABLE fact_orders_2024 PARTITION OF fact_orders
|
|
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');
|
|
|
|
CREATE TABLE fact_orders_default PARTITION OF fact_orders DEFAULT;
|
|
```
|
|
|
|
### Hash partitioning
|
|
```sql
|
|
CREATE TABLE logs PARTITION BY HASH (user_id);
|
|
CREATE TABLE logs_0 PARTITION OF logs FOR VALUES WITH (MODULUS 4, REMAINDER 0);
|
|
CREATE TABLE logs_1 PARTITION OF logs FOR VALUES WITH (MODULUS 4, REMAINDER 1);
|
|
```
|
|
|
|
---
|
|
|
|
## Example DDL
|
|
|
|
```sql
|
|
-- Create enum
|
|
CREATE TYPE order_status AS ENUM ('pending', 'confirmed', 'shipped', 'cancelled');
|
|
|
|
-- Create table
|
|
CREATE TABLE orders (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
order_number VARCHAR(50) NOT NULL,
|
|
user_id BIGINT NOT NULL,
|
|
status order_status NOT NULL DEFAULT 'pending',
|
|
subtotal NUMERIC(18,2) NOT NULL DEFAULT 0,
|
|
discount_amount NUMERIC(18,2) NOT NULL DEFAULT 0,
|
|
total_amount NUMERIC(18,2) NOT NULL DEFAULT 0,
|
|
metadata JSONB DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT uk_orders_number UNIQUE (order_number),
|
|
CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id)
|
|
);
|
|
|
|
-- Indexes
|
|
CREATE INDEX idx_orders_user ON orders(user_id);
|
|
CREATE INDEX idx_orders_status ON orders(status);
|
|
CREATE INDEX idx_orders_user_status_created ON orders(user_id, status, created_at DESC);
|
|
CREATE INDEX idx_orders_metadata ON orders USING GIN(metadata);
|
|
|
|
-- Comments
|
|
COMMENT ON TABLE orders IS 'Table storing order information';
|
|
COMMENT ON COLUMN orders.order_number IS 'Unique order code';
|
|
COMMENT ON COLUMN orders.status IS 'Order status';
|
|
|
|
-- Trigger for updated_at
|
|
CREATE TRIGGER trg_orders_updated_at
|
|
BEFORE UPDATE ON orders
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
```
|
|
|
|
---
|
|
|
|
## Checklist
|
|
|
|
- [ ] Use `TIMESTAMPTZ` instead of `TIMESTAMP`
|
|
- [ ] Use `JSONB` instead of `JSON`
|
|
- [ ] `COMMENT ON` for tables and columns
|
|
- [ ] Trigger for `updated_at`
|
|
- [ ] Partial indexes for soft delete / filtered queries
|
|
- [ ] GIN index for JSONB columns
|