404 Not Found

404 Not Found


nginx

PostgreSQL Data Types: The Complete Guide

Choosing the right data type is the foundation of database design—it determines storage efficiency, query performance, and data precision.

1. What You'll Learn


2. A Developer's Real Story

(1) The Pain: Confused About Data Types

Bob ran into a series of tough choices when designing the tables for an e-commerce database:

(2) The Solution: Precise Type Selection

PostgreSQL offers a rich set of dedicated data types; choosing precisely saves storage while guaranteeing precision:

SQL
-- Correct type choices for an e-commerce database
CREATE TABLE smart_products (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,  -- future-proof ID
    name VARCHAR(200) NOT NULL,                          -- bounded string
    description TEXT,                                    -- unbounded text
    price DECIMAL(10, 2) NOT NULL,                       -- exact money
    weight_kg REAL,                                      -- approximate is OK
    is_available BOOLEAN DEFAULT true,                   -- yes/no flag
    created_at TIMESTAMPTZ DEFAULT NOW(),                -- timezone-aware
    source_ip INET,                                      -- IP address type
    attributes JSONB DEFAULT '{}'::jsonb                 -- flexible schema
);

(3) The Payoff


3. Data Type Decision Flow

100%
graph TB
    START[What kind of data?] --> NUM{Numeric?}
    NUM -->|Yes| INT{Need exact<br/>precision?}
    INT -->|Yes, money/rates| DEC[DECIMAL / NUMERIC]
    INT -->|No, measurements| FLOAT[REAL / DOUBLE PRECISION]
    NUM -->|Whole numbers| RANGE{Value range?}
    RANGE -->|-32768 to 32767| SMALL[SMALLINT]
    RANGE -->|-2.1B to 2.1B| INT2[INTEGER]
    RANGE -->|Larger| BIG[BIGINT]
    NUM -->|Auto-increment ID| AUTO[SERIAL / IDENTITY]
    START --> STR{Text?}
    STR -->|Fixed length| CHAR[CHAR]
    STR -->|Bounded variable| VAR[VARCHAR n]
    STR -->|Unbounded| TEXT2[TEXT]
    START --> TIME{Date/Time?}
    TIME -->|Date only| DATE2[DATE]
    TIME -->|Time only| TIME2[TIME]
    TIME -->|Timestamp| TZ{Timezone?}
    TZ -->|Yes, global app| TSTZ[TIMESTAMPTZ]
    TZ -->|No, local only| TS[TIMESTAMP]
    TIME -->|Duration| IV[INTERVAL]
    START --> SPEC{Special?}
    SPEC -->|Yes/No| BOOL[BOOLEAN]
    SPEC -->|UUID| UUID2[UUID]
    SPEC -->|IP address| IP[INET / CIDR]
    SPEC -->|Enum list| ENUM2[ENUM]
    SPEC -->|JSON doc| JSON[JSONB]

4. Integer Types

Type Storage Range Typical use
SMALLINT 2 bytes -32,768 ~ 32,767 age, rating, status code
INTEGER (INT) 4 bytes -2,147,483,648 ~ 2,147,483,647 general integers, primary-key IDs
BIGINT 8 bytes ±9,223,372,036,854,775,807 large-table IDs, amounts (in cents)

▶ Example: Choosing Integer Types

SQL
-- SMALLINT for small-range values
CREATE TABLE ratings (
    user_id INTEGER REFERENCES users(id),
    product_id INTEGER REFERENCES products(id),
    score SMALLINT CHECK (score BETWEEN 1 AND 5),  -- 1-5 is well within SMALLINT range
    PRIMARY KEY (user_id, product_id)
);

-- INTEGER for most IDs
CREATE TABLE categories (
    id SERIAL PRIMARY KEY,  -- SERIAL = INTEGER + auto-increment sequence
    name VARCHAR(100) NOT NULL
);

-- BIGINT for high-growth tables
CREATE TABLE audit_log (
    id BIGSERIAL PRIMARY KEY,  -- BIGSERIAL = BIGINT + auto-increment
    action VARCHAR(50),
    details JSONB,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

Output:

TEXT
CREATE TABLE
💡 Tip: Not sure whether to use INTEGER or BIGINT? For primary-key IDs, if you expect more than 1 billion rows, go straight to BIGINT. It costs only 4 extra bytes per row (just 4 MB per million rows), but spares you a costly ALTER TABLE rewrite of a large table later.


5. Auto-increment Sequences

(1) SERIAL vs IDENTITY

Method Syntax Standard Manual insert? Recommended
SERIAL id SERIAL PRIMARY KEY PG-specific Yes Legacy compatibility
BIGSERIAL id BIGSERIAL PRIMARY KEY PG-specific Yes Legacy compatibility
IDENTITY id INT GENERATED ALWAYS AS IDENTITY SQL standard Needs OVERRIDING ✅ New projects
SQL
-- SQL-standard identity column (PostgreSQL 10+)
CREATE TABLE orders_v2 (
    id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    total DECIMAL(12, 2) DEFAULT 0
);

-- Insert without specifying id (auto-generated)
INSERT INTO orders_v2 (user_id, total) VALUES (1, 99.99);

-- Attempt to manually insert id (will FAIL with GENERATED ALWAYS)
-- INSERT INTO orders_v2 (id, user_id, total) VALUES (100, 1, 50.00);

-- Use GENERATED BY DEFAULT if you need manual override sometimes
CREATE TABLE logs (
    id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    message TEXT
);

Output:

TEXT
INSERT 0 1

6. Floating-Point and Exact Numerics

(1) REAL / DOUBLE vs DECIMAL

Type Storage Precision Best for
REAL 4 bytes 6 significant digits scientific computing, sensors
DOUBLE PRECISION 8 bytes 15 significant digits scientific computing, statistics
DECIMAL(p,s) variable exact money, rates (recommended)
NUMERIC(p,s) variable exact same as DECIMAL

▶ Example: Floating-Point Precision Pitfalls

SQL
-- Float precision trap: 0.1 + 0.2 != 0.3
SELECT 0.1::REAL + 0.2::REAL = 0.3::REAL AS float_equal;
-- Output: f (false!)

-- DECIMAL has no precision loss
SELECT 0.1::DECIMAL + 0.2::DECIMAL = 0.3::DECIMAL AS decimal_equal;
-- Output: t (true!)

Output:

TEXT
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ Example: Using DECIMAL for Money

SQL
-- DECIMAL(precision, scale)
-- precision = total digits, scale = digits after decimal point
-- DECIMAL(10,2) = up to 99,999,999.99

CREATE TABLE products_precise (
    id SERIAL PRIMARY KEY,
    name VARCHAR(200),
    price DECIMAL(10, 2) NOT NULL,       -- max 99,999,999.99
    tax_rate DECIMAL(5, 4) DEFAULT 0.0875, -- 0.0875 = 8.75%
    discount NUMERIC(5, 2) DEFAULT 0.00   -- max 999.99%
);

-- Money calculations are exact
SELECT name,
       price,
       price * tax_rate AS tax_amount,
       price + (price * tax_rate) AS price_with_tax
FROM products_precise;

Output:

TEXT
 count 
-------
     5
(1 row)
Precision choice DECIMAL params Max value Best for
Small e-commerce DECIMAL(8,2) 999,999.99 daily volume < 1M
Large e-commerce DECIMAL(12,2) 99,999,999,999.99 global e-commerce
Crypto DECIMAL(20,8) very large BTC 8-decimal precision

7. String Types

Type Storage Max length Best for
CHAR(n) fixed-length, space-padded n hashes, ISO codes
VARCHAR(n) variable-length n text with a length limit
TEXT variable-length unlimited text without a length limit

▶ Example: String Type Comparison

SQL
-- CHAR: fixed-length (padded with spaces)
SELECT LENGTH('abc'::CHAR(5));
-- Output: 5 (padded to 5 chars)

-- VARCHAR: variable-length with limit
SELECT LENGTH('abc'::VARCHAR(5));
-- Output: 3 (stored as-is, max 5)

-- TEXT: variable-length, no limit
SELECT LENGTH('abc'::TEXT);
-- Output: 3 (no limit)

-- Performance test: VARCHAR vs TEXT are identical in PG
-- (Unlike MySQL where VARCHAR is faster than TEXT)

Output:

TEXT
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)
💡 Tip: In PostgreSQL, VARCHAR and TEXT have identical query and storage performance. PG does not slow down just because TEXT has no length limit. The only reason to choose one over the other is whether you need the database to enforce a length limit.


8. Date and Time Types

Type Storage Range Best for
DATE 4 bytes 4713 BC ~ 5874897 AD date only (birthday, holiday)
TIME 8 bytes 00:00:00 ~ 24:00:00 time only (business hours)
TIMESTAMP 8 bytes 4713 BC ~ 294276 AD date+time (no time zone)
TIMESTAMPTZ 8 bytes same as above date+time+timezone (✅ recommended)
INTERVAL 16 bytes ±178000000 years time span

▶ Example: TIMESTAMP vs TIMESTAMPTZ

SQL
-- Set timezone to UTC
SET timezone = 'UTC';

-- Insert the same moment in different types
INSERT INTO test_times (ts_no_tz, ts_with_tz) VALUES
    ('2026-07-13 10:00:00', '2026-07-13 10:00:00+00');

-- Change to Tokyo timezone
SET timezone = 'Asia/Tokyo';

-- TIMESTAMP (no timezone) shows the same literal
SELECT ts_no_tz FROM test_times;
-- Output: 2026-07-13 10:00:00 (unchanged, could be any timezone!)

-- TIMESTAMPTZ converts to local timezone
SELECT ts_with_tz FROM test_times;
-- Output: 2026-07-13 19:00:00+09 (10:00 UTC = 19:00 Tokyo)

Output:

TEXT
INSERT 0 1

▶ Example: Date/Time Functions

SQL
-- Current date and time
SELECT NOW();                    -- 2026-07-13 10:30:00.123456+00
SELECT CURRENT_DATE;             -- 2026-07-13
SELECT CURRENT_TIMESTAMP;        -- same as NOW()

-- Date arithmetic with INTERVAL
SELECT NOW() + INTERVAL '7 days';     -- one week from now
SELECT NOW() - INTERVAL '3 months';   -- 3 months ago

-- Age calculation
SELECT AGE(TIMESTAMP '1990-05-15');   -- 36 years 1 month 28 days

-- Extract parts
SELECT EXTRACT(YEAR FROM NOW());      -- 2026
SELECT EXTRACT(MONTH FROM NOW());     -- 7
SELECT EXTRACT(DOW FROM NOW());       -- 1 (Monday, 0=Sunday)

Output:

TEXT
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

9. Special Types

(1) UUID

SQL
-- Enable uuid-ossp extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- Generate different UUID versions
SELECT uuid_generate_v4();  -- random UUID (most common)
SELECT uuid_generate_v1();  -- time-based UUID
UUID version Generation Best for
v1 timestamp + MAC address time-ordered
v4 random most scenarios (recommended)
v7 timestamp + random (new standard) time-ordered + random

(2) INET / CIDR (Network Addresses)

▶ Example: Storing and Querying IP Addresses

SQL
-- INET: single IP or IP range
CREATE TABLE access_log (
    id BIGSERIAL PRIMARY KEY,
    source_ip INET NOT NULL,
    access_time TIMESTAMPTZ DEFAULT NOW()
);

INSERT INTO access_log (source_ip) VALUES
    ('192.168.1.100'),
    ('10.0.0.5'),
    ('2001:db8::1');

-- Query: find all IPs in a subnet (impossible with VARCHAR!)
SELECT source_ip FROM access_log
WHERE source_ip << '192.168.1.0/24'::INET;
-- << means "is contained within"

-- CIDR: network range
SELECT '192.168.1.0/24'::CIDR;

Output:

TEXT
INSERT 0 1
Operator Meaning Example
<< contained in 192.168.1.5' << '192.168.1.0/24' = true
>> contains '192.168.1.0/24' >> '192.168.1.5' = true
= equals '192.168.1.5'::INET = '192.168.1.5'::INET

(3) BOOLEAN

▶ Example: Three Representations of Boolean

SQL
-- Boolean accepts multiple representations
SELECT true, 't', 'true', 'yes', 'on', '1';   -- all = TRUE
SELECT false, 'f', 'false', 'no', 'off', '0';  -- all = FALSE

-- Boolean in WHERE clause
SELECT name FROM products WHERE is_available IS TRUE;
SELECT name FROM products WHERE is_available IS NOT FALSE;

Output:

TEXT
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

(4) ENUM

▶ Example: Custom Enum Types

SQL
-- Create an enum type (once, reusable across tables)
CREATE TYPE order_status AS ENUM (
    'pending', 'paid', 'shipped', 'delivered', 'cancelled'
);

-- Use in table definition
CREATE TABLE orders_enum (
    id SERIAL PRIMARY KEY,
    status order_status DEFAULT 'pending'
);

-- Enum values are validated automatically
INSERT INTO orders_enum (status) VALUES ('pending');   -- OK
-- INSERT INTO orders_enum (status) VALUES ('unknown');  -- ERROR!

Output:

TEXT
INSERT 0 1
⚠️ Note: Once a PG ENUM type is created, adding new values requires ALTER TYPE ... ADD VALUE (allowed inside a transaction since PG 9.1). If the values change often, a VARCHAR + CHECK constraint is more flexible.


10. Data Type Comparison Table

Need ❌ Not recommended ✅ Recommended Reason
Money REAL / FLOAT DECIMAL(p,s) floating-point loses precision
Timestamp TIMESTAMP TIMESTAMPTZ global apps need time zones
IP address VARCHAR INET INET supports range queries
Yes/No INTEGER (0/1) BOOLEAN clearer semantics
Long text VARCHAR(9999) TEXT TEXT has no limit and same performance
Status enum VARCHAR + CHECK ENUM or VARCHAR+CHECK ENUM if stable, CHECK if frequently changing
Flexible attributes many NULL columns JSONB one column for dynamic fields
Auto-increment ID SERIAL IDENTITY SQL-standard syntax

11. Complete Example: A Table Using Many Types

SQL
-- ============================================
-- Complete example: table with all major types
-- A sensor data collection system
-- ============================================

-- Create enum type
CREATE TYPE sensor_status AS ENUM ('active', 'inactive', 'maintenance');

-- Create table with diverse data types
CREATE TABLE sensors (
    -- Identity
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    uuid UUID DEFAULT uuid_generate_v4() UNIQUE,

    -- Text
    name VARCHAR(100) NOT NULL,
    description TEXT,
    location_code CHAR(10),

    -- Numeric
    latitude DECIMAL(9, 6) CHECK (latitude BETWEEN -90 AND 90),
    longitude DECIMAL(9, 6) CHECK (longitude BETWEEN -180 AND 180),
    altitude REAL,

    -- Network
    ip_address INET,
    subnet CIDR,

    -- Status
    status sensor_status DEFAULT 'active',
    is_online BOOLEAN DEFAULT false,

    -- Time
    installed_at DATE,
    last_reading_at TIMESTAMPTZ,
    reading_interval INTERVAL DEFAULT INTERVAL '5 minutes',

    -- Flexible data
    metadata JSONB DEFAULT '{}'::jsonb,

    -- Audit
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Insert sample data
INSERT INTO sensors (name, latitude, longitude, ip_address, status, installed_at, metadata)
VALUES (
    'Temperature Sensor A1',
    35.6762, 139.6503,
    '192.168.1.50',
    'active',
    '2025-01-15',
    '{"model": "TX-200", "unit": "celsius", "range_min": -40, "range_max": 85}'::jsonb
);

-- Query with type-specific operators
SELECT name, ip_address, metadata->>'model' AS model
FROM sensors
WHERE ip_address << '192.168.1.0/24'::INET
  AND status = 'active';

❓ FAQ

Q What's the difference between DECIMAL and NUMERIC?
A In PostgreSQL, DECIMAL and NUMERIC are completely equivalent and interchangeable. The SQL standard makes a subtle distinction, but PG's implementation is identical. DECIMAL is recommended (more intuitive).
Q What if a SERIAL column's sequence skips numbers?
A SERIAL uses a sequence object; after a failed or rolled-back INSERT, the sequence value is not rolled back (this is by design, to guarantee concurrency safety). Gaps are normal and don't affect functionality. If you must have consecutive values, handle it in the application layer rather than relying on the sequence.
Q Does changing VARCHAR(50) to VARCHAR(100) lock the table?
A In PostgreSQL, increasing VARCHAR length does not lock the table (no data rewrite) and completes instantly. Shrinking the length or changing the type is what locks the table.
Q Which should I choose, JSON or JSONB?
A Almost always JSONB. JSONB is stored as binary and supports indexed queries, so it's fast. JSON is stored as text and must be re-parsed on every query. The only case for JSON: when you need to preserve the input's whitespace/key order (e.g., audit logs).
Q Why is TIMESTAMPTZ recommended over TIMESTAMP?
A TIMESTAMPTZ converts to UTC on storage and back to the client's time zone on read. That means global users see their own local time with no confusion. TIMESTAMP stores and returns exactly what you give it, which is ambiguous for cross-time-zone applications.
Q Does mixing multiple data types in one table hurt performance?
A No. PG stores type information per column and only reads the columns it needs. Using dedicated types appropriately (e.g., INET for IPs) actually improves query performance, because the type's operators can use indexes.

📖 Summary


📝 Exercises

  1. Basic (★): Create a countries table with: id (SERIAL primary key), name (VARCHAR(100)), iso_code (CHAR(2)), population (BIGINT), gdp DECIMAL(15,2), is_developed (BOOLEAN). Insert 3 test rows.

  2. Intermediate (★★): Create a server_logs table with: id (BIGINT IDENTITY primary key), source_ip (INET), request_time (TIMESTAMPTZ), response_time_ms (INTEGER), is_error (BOOLEAN), metadata (JSONB). Insert 2 log records, then query all logs from the 10.0.0.0/8 network.

  3. Challenge (★★★): Design a financial_transactions table that handles multiple currencies (USD/EUR/JPY/CNY) with different amount precisions (USD/EUR: 2 decimals, JPY: 0 decimals, CNY: 2 decimals). Implement it using DECIMAL + CHECK constraints so different currencies require different decimal scales. Write the CREATE TABLE SQL plus 3 INSERT examples for different currencies.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏