PostgreSQL Object Handbook
PostgreSQL Object Handbook

Table of Contents
I. The Core: Storage & Data Structure
- Standard Tables: Beyond
CREATE TABLE– Constraints & Defaults. - Partitioned Tables: Scaling horizontally with
PARTITION BY. - Tablespaces: Managing physical storage locations for heavy I/O.
- Temporary & Unlogged Tables: When performance beats persistence.
II. Data Flow & Automation
- Sequences: Managed auto-incrementing counters.
- Views vs. Materialized Views: Logical abstraction vs. Physical snapshots.
- Triggers & Event Triggers: Automating DML and DDL level actions.
- Rules: The hidden Postgres rewrite system.
III. Logic & Programmability
- Functions (UDFs): Computation, logic, and
STRICTvsIMMUTABLEvolatile types. - Stored Procedures: Handling Transactions (
COMMIT/ROLLBACK) inside the DB. - Operators & Operator Classes: Customizing how PG compares your data.
IV. Performance & Search
- The Index Family: B-Tree, GIN (for JSONB), GiST, BRIN, and Hash.
- Statistics Objects: Helping the Query Planner make better choices.
- Full Text Search Objects: Configurations, Dictionaries, and Parsers.
V. Global & Security Objects (Cluster-wide)
- Roles & Users: Grouping permissions and login management.
- Resource Groups: (If using forks) or GUC (Grand Unified Configuration) settings.
- Databases: How the cluster isolates its children.
VI. The Modern Postgres Objects
- Custom Data Types:
CREATE TYPE(Enums, Composites, Domains). - Foreign Data Wrappers (FDW): Mapping external data as local tables.
- Extensions: The power of
pg_stat_statements,PostGIS, andpg_vector.
I. The Core: Storage & Data Structure
If PostgreSQL were a house, this section is the foundation, the load-bearing walls, and the basement. Most people think they know how to create a table, but in Postgres, just a table is rarely the best answer for a high-performance system.
1. Standard Tables: Beyond CREATE TABLE
We all know the basic syntax, but a professional-grade table isn’t just a collection of columns; it’s a contract for data integrity. When you’re defining a table, you aren’t just choosing types like INT or TEXT. You are setting the rules of the game using Constraints and Defaults.
- Constraints: Use
CHECKconstraints religiously. Why handle price cannot be negative in your application code when the database can guarantee it at the disk level? - Smart Defaults: Don’t just settle for
NULL. UseDEFAULTvalues to handle timestamps (CURRENT_TIMESTAMP) or UUIDs automatically. It keeps yourINSERTstatements clean and your data consistent.
CREATE TABLE users (
user_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username TEXT NOT NULL CHECK (char_length(username) > 3),
balance NUMERIC DEFAULT 0 CHECK (balance >= 0),
created_at TIMESTAMPTZ DEFAULT now()
);
2. Partitioned Tables: Scaling Horizontally with PARTITION BY
Imagine a logs table with 500 million rows. Even with an index, a simple SELECT starts to feel heavy. This is where Table Partitioning saves your life.
Instead of one giant God Table, you split the data into smaller, manageable chunks (partitions) based on a key — usually a date or an ID range. Postgres’s query planner is smart enough to perform partition pruning, meaning it only looks at the specific table it needs and ignores the rest.
-- The 'Parent' table
CREATE TABLE orders (
order_id INT,
order_date DATE NOT NULL,
amount DECIMAL
) PARTITION BY RANGE (order_date);
-- A specific 'Child' partition for 2024
CREATE TABLE orders_2024 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');
3. Tablespaces: Managing Physical Storage for Heavy I/O
By default, Postgres stores everything in one place. But what if you have a massive archive table that you rarely touch, and a hot_users table that is hammered with requests every second?
Tablespaces allow you to map PostgreSQL objects to different physical locations on your server. You can put your hot indexes on a lightning-fast NVMe drive and move your cold history tables to a cheaper, slower HDD.
CREATE TABLESPACE fast_ssd_space LOCATION '/mnt/nvme/postgres_data';
CREATE TABLE high_priority_logs (
id SERIAL PRIMARY KEY,
data JSONB
) TABLESPACE fast_ssd_space;
4. Temporary & Unlogged Tables: Performance Over Persistence
Sometimes, you just don’t care if the data survives a crash. You just need speed.
- Temporary Tables: These live only for the duration of your session. They are perfect for complex ETL (Extract, Transform, Load) processes where you need a scratchpad to hold intermediate results.
- Unlogged Tables: These are the wild child of Postgres. They don’t write to the Write-Ahead Log (WAL). This makes them incredibly fast — up to 3x faster for writes — but there’s a catch: if the database crashes, the data in an unlogged table is wiped clean. Use these for transient caches or staging data where the source is elsewhere.
-- Speed without the safety net
CREATE UNLOGGED TABLE web_session_cache (
session_id TEXT PRIMARY KEY,
data TEXT,
expiry TIMESTAMPTZ
);
II. Data Flow & Automation
In a modern database, you don’t want to do everything manually. You want the system to handle the heavy lifting — whether that’s generating unique IDs, caching complex reports, or reacting to data changes in real-time.
1. Sequences: The Pulse of Your Primary Keys
We often use SERIAL or IDENTITY columns and forget they exist, but behind the scenes, there is a Sequence. A Sequence is a global object that provides a thread-safe way to generate unique numbers.
Sometimes, you need a sequence that isn’t tied to a specific table — perhaps for generating Global Transaction IDs across multiple services.
CREATE SEQUENCE global_order_id_seq
START WITH 100000
INCREMENT BY 5
CACHE 20; -- Pre-calculates numbers for extreme performance
Pro-Tip: Using CACHE on a sequence can drastically reduce I/O contention in high-concurrency environments. Just remember: if the DB crashes, you might lose the cached numbers, leaving a gap in your ID sequence.
2. Views vs. Materialized Views: Logic vs. Physics
This is the most common architectural decision you’ll make.
- Standard Views: Think of these as Saved Queries. They don’t store data; they just provide a clean window into your complex joins. They are perfect for security (hiding sensitive columns) or simplifying app code.
- Materialized Views (MV): These are the heavy hitters. An MV runs the query and saves the result to disk. If you have a dashboard query that takes 10 seconds to run, put it in an MV. It will load in milliseconds.
-- The Always Fresh View
CREATE VIEW active_users_summary AS
SELECT count(*), department FROM users WHERE status = 'active' GROUP BY department;
-- The High Performance Snapshot
CREATE MATERIALIZED VIEW monthly_financial_report AS
SELECT vendor, sum(amount) FROM massive_transactions_table GROUP BY vendor;
-- Don't forget to refresh it!
REFRESH MATERIALIZED VIEW monthly_financial_report;
3. Triggers & Event Triggers: The Database Hooks
Triggers allow you to say: When X happens, automatically do Y.
- DML Triggers: These react to data changes (
INSERT,UPDATE,DELETE). They are the gold standard for creating Audit Logs. If someone changes a user's salary, a trigger can automatically write the old and new values into a separateaudit_table. - Event Triggers: These are more Global. They react to DDL changes — like someone dropping a table or creating a new index. They are essential for database-level monitoring and enforcing strict schema standards.
-- Step 1: The Function
CREATE FUNCTION log_price_change() RETURNS trigger AS $$
BEGIN
INSERT INTO price_audit(old_price, new_price, changed_at)
VALUES (OLD.price, NEW.price, now());
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Step 2: The Trigger
CREATE TRIGGER trg_price_watch
AFTER UPDATE OF price ON products
FOR EACH ROW EXECUTE FUNCTION log_price_change();
4. Rules: The Hidden Rewrite System
Rules are the black sheep of the PostgreSQL family. While Triggers are like a side-car that runs after or before an event, a Rule actually rewrites the query before it even executes.
If you send a DELETE command to a table with a Rule, the Rule could rewrite it into an UPDATE status = 'deleted' instead.
Warning: Rules are incredibly powerful but can be very hard to debug because they change the user’s intent invisibly. Use them sparingly, mostly for creating Updatable Views where standard SQL logic isn’t enough.
CREATE RULE soft_delete_rule AS ON DELETE TO products
DO INSTEAD
UPDATE products SET is_active = false WHERE id = OLD.id;
III. Logic & Programmability
The true power of PostgreSQL lies in its extensibility. You aren’t limited to the built-in logic; you can teach the database new tricks, define how it handles transactions, and even change how it compares two pieces of data.
1. Functions (UDFs): The Logic Engine
User-Defined Functions (UDFs) are the bread and butter of database logic. But to write a good function, you need to understand Function Volatility. This tells the query planner how the function behaves, which can make or break your performance.
- IMMUTABLE: The function always returns the same result for the same arguments (e.g.,
2 + 2). The planner can pre-calculate these and cache them. - STABLE: The function returns the same result within a single table scan (e.g., looking up a value based on a fixed ID).
- VOLATILE: The value can change even within a single query (e.g.,
random()ornow()). - STRICT: A huge performance booster. If any input is
NULL, the function returnsNULLimmediately without even running the logic.
CREATE OR REPLACE FUNCTION calculate_tax(price numeric)
RETURNS numeric AS $$
BEGIN
RETURN price * 0.20;
END;
$$ LANGUAGE plpgsql IMMUTABLE STRICT;
2. Stored Procedures: Taking Control of Transactions
For years, Postgres only had Functions. But Functions have a major limitation: they run inside a single transaction. You can’t say Commit this part and Rollback that part inside a function.
Procedures (introduced in PG 11) changed the game. They allow you to manage transactions manually. This is essential for long-running batch jobs where you want to commit every 1,000 rows so you don’t bloat the Undo log or lock the entire table for an hour.
CREATE PROCEDURE bulk_archive_data(cutoff_date date)
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO archive_table SELECT * FROM active_table WHERE created_at < cutoff_date;
COMMIT; -- Save progress!
DELETE FROM active_table WHERE created_at < cutoff_date;
COMMIT; -- Finalize the cleanup
END;
$$;
-- You don't SELECT a procedure, you CALL it:
CALL bulk_archive_data('2023-01-01');
3. Operators & Operator Classes: Teaching PG New Rules
Ever wondered how PostgreSQL knows that 1 < 2 or that one JSONB object contains another? It uses Operators.
Postgres lets you define your own operators (like @@, !!, or ~>) and, more importantly, Operator Classes. If you create a custom data type—say, a ComplexNumber type—Postgres won't know how to sort it or index it by default. By defining an Operator Class, you tell the B-Tree or GIN index exactly how to compare your custom data.
-- Creating a custom &amp;#x27;cube&amp;#x27; equality operator (simplified logic)
CREATE OPERATOR === (
LEFTARG = box,
RIGHTARG = box,
PROCEDURE = area_equal_check,
COMMUTATOR = ===
);
Note : This is God Tier Postgres. Most devs will never touch this, but knowing it exists means you can optimize the database for literally any data type — from chemical structures to specialized geometric shapes.
IV. Performance & Search
A database is only as good as its retrieval speed. If your queries are crawling, your application is dying. In this section, we look at the internal mechanics that make PostgreSQL one of the fastest search engines on the planet.
1. The Index Family: Choosing the Right Weapon
Most developers just throw a B-Tree index at everything and hope for the best. While B-Tree is the Swiss Army Knife, PostgreSQL offers a specialized toolkit for different data shapes:
- B-Tree: The default. Best for unique values, ranges (
<,>,BETWEEN), and sorting. - GIN (Generalized Inverted Index): The king of multi-value data. If you are indexing JSONB arrays or full-text search vectors, GIN is your best friend. It points multiple keys to a single row.
- GiST (Generalized Search Tree): Essential for nearest neighbor searches or geometric data (e.g., find all coffee shops within 5km).
- BRIN (Block Range Index): The hidden gem for massive, naturally sorted tables (like time-series logs). It’s incredibly tiny (kilobytes instead of gigabytes) because it only stores the min/max values for a block of pages.
- Hash: Best for simple equality (
=) checks. It’s faster than B-Tree for exact matches but can’t handle ranges.
-- GIN Index for lightning-fast JSONB lookups
CREATE INDEX idx_user_metadata ON users USING GIN (metadata);
-- BRIN Index for a 1-Terabyte log table
CREATE INDEX idx_logs_created_at ON large_logs USING BRIN (created_at);
2. Statistics Objects: Guiding the Query Planner
The PostgreSQL Query Planner is like a GPS. It tries to find the fastest route to your data. To do this, it looks at statistics (histograms, most common values).
However, the planner can get confused by correlated columns. For example, if you filter by City = 'London' and Country = 'UK', the planner might think these are independent events and guess the row count wrong. Statistics Objects allow you to tell the database: Hey, these two columns are related, calculate their density together!
CREATE STATISTICS stts_city_country (dependencies)
ON city, country FROM addresses;
ANALYZE addresses; -- Updates the planner&amp;#x27;s brain
3. Full Text Search Objects: Beyond LIKE %word%
If you’re still using LIKE for searching through blog posts or product descriptions, stop. PostgreSQL has a built-in search engine that rivals dedicated tools. It relies on three main objects:
- Parsers: They break raw text into tokens (words, email addresses, hostnames).
- Dictionaries: They normalize tokens. They remove stop words (a, an, the) and perform Stemming (turning running and ran into the root word run).
- Configurations: This ties the Parser and Dictionaries together for a specific language.
-- Converting text to a searchable vector using the &amp;#x27;english&amp;#x27; configuration
SELECT to_tsvector(&amp;#x27;english&amp;#x27;, &amp;#x27;The quick brown fox jumped over the lazy dog&amp;#x27;);
-- Creating a searchable index
CREATE INDEX idx_post_content ON posts USING GIN (to_tsvector(&amp;#x27;english&amp;#x27;, content));
This is the Engine Room of the database. You’ve just shown your readers how to make their data fly.
Now we’re zooming out. While the previous sections were about what happens inside a database, this section is about the Cluster itself. In PostgreSQL, a Cluster is the entire instance — the engine that can house multiple databases and thousands of users.
Understanding these Global objects is critical because they don’t live inside a specific database; they exist at the top level of the server.
V. Global & Security Objects (Cluster-wide)
In the Postgres world, some things are bigger than a single database. If you drop a database, these objects remain. They are the God-level settings and identities that govern who can log in and how the server breathes.
1. Roles & Users: The Identity Matrix
In modern PostgreSQL, there is no technical difference between a User and a Group. They are both Roles.
- A User is just a Role with the
LOGINattribute. - A Group is a Role without
LOGIN, designed to hold a collection of permissions.
The pro-way to manage a cluster is to never grant permissions to individuals. Instead, create a Group Role (e.g., read_only_access), grant permissions to that group, and then grant the user to the group. It makes onboarding and offboarding as simple as a single command.
-- Create a group role
CREATE ROLE marketing_team NOLOGIN;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO marketing_team;
-- Create a user and add them to the group
CREATE ROLE alice WITH LOGIN PASSWORD &amp;#x27;secure_pass&amp;#x27;;
GRANT marketing_team TO alice;
2. GUC (Grand Unified Configuration) Settings
PostgreSQL doesn’t just run; it adapts. The GUC system is the collection of all settings that control the cluster’s behavior — from memory allocation (shared_buffers) to how aggressive the autovacuum should be.
What many developers forget is that you can set these configurations at different levels:
- Cluster Level: In the
postgresql.conffile. - Database Level: Using
ALTER DATABASE. - User Level: Using
ALTER ROLE.
Imagine a specific user who runs heavy analytical reports. You can increase the memory specifically for their session without affecting the rest of the web app.
-- Give the analyst role more memory for complex sorts
ALTER ROLE analyst SET work_mem = &amp;#x27;64MB&amp;#x27;;
3. Databases: The Great Wall of Isolation
A PostgreSQL Cluster can contain multiple Databases. It is important to remember that these are strictly isolated. You cannot run a single SQL query that joins a table from Database_A with a table from Database_B (unless you use Foreign Data Wrappers, which we will cover later).
This isolation is perfect for Multi-tenancy. If you are building a SaaS, giving each client their own database ensures that a bad query or a security leak in one doesn’t immediately compromise the others.
-- Creating a new isolated environment
CREATE DATABASE client_alpha OWNER alpha_admin;
4. Tablespaces (Revisited as Global Objects)
While we touched on these in the storage section, it’s vital to remember that Tablespaces are Global. They are defined at the cluster level. This means a single physical disk (Tablespace) can be shared across multiple databases within the same cluster, allowing you to manage hardware resources centrally.
This gives your article the Systems Engineering depth it needs.
VI. The Modern Postgres Objects
PostgreSQL is often called extensible for a reason. You aren’t stuck with what comes in the box. You can define your own data structures, reach out to other servers, and plug in massive new features using the extension system.
1. Custom Data Types: Tailor-Made Data
Why force your data into a VARCHAR or INT when it doesn't fit? Postgres allows you to create your own types to ensure your schema matches your business domain perfectly.
- Enums: Perfect for fixed sets like
order_status('pending', 'shipped', 'delivered'). It’s more efficient than a lookup table and cleaner than plain text. - Composite Types: Essentially a struct in the database. You can define a
full_addresstype that contains street, city, and zip, and use it as a single column in any table. - Domains: These are Types with Rules. You can create a
us_zip_codedomain based onTEXTbut with a built-in check to ensure it’s always 5 digits.
-- Creating an Enum
CREATE TYPE mood AS ENUM (&amp;#x27;happy&amp;#x27;, &amp;#x27;productive&amp;#x27;, &amp;#x27;tired&amp;#x27;);
-- Creating a Domain with validation
CREATE DOMAIN valid_email AS TEXT
CHECK (VALUE ~* &amp;#x27;^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$&amp;#x27;);
2. Foreign Data Wrappers (FDW): The SQL Mediator
Imagine you have data in a MySQL database, a CSV file on a server, and a MongoDB instance. With Foreign Data Wrappers, you can mount those external sources inside Postgres.
To the user, they look and act like local tables. You can even JOIN a local Postgres table with a remote MySQL table in a single query. It is the ultimate tool for data integration without the headache of a full ETL pipeline.
CREATE EXTENSION postgres_fdw;
CREATE SERVER remote_server
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host &amp;#x27;192.168.1.10&amp;#x27;, dbname &amp;#x27;foreign_db&amp;#x27;);
CREATE FOREIGN TABLE remote_orders (
id INT,
amount NUMERIC
) SERVER remote_server;
3. Extensions: The Superpowers
This is the Secret Sauce. PostgreSQL’s architecture allows developers to write plugins (Extensions) that add entirely new capabilities to the core engine.
- PostGIS: Turns Postgres into the world’s most powerful Geographic Information System (GIS). It adds types like
GEOMETRYand functions to calculate the distance between two GPS coordinates. - pg_stat_statements: A must-have for performance. It tracks every query run on the server and tells you exactly which ones are slow and why.
- pg_vector: The hottest extension right now. It allows Postgres to store and search Vector Embeddings, turning your database into a Vector Database for AI and LLM applications.
-- Enabling a superpower
CREATE EXTENSION postgis;
CREATE EXTENSION pg_vector;
-- Now you can store AI embeddings!
CREATE TABLE ai_documents (
id SERIAL PRIMARY KEY,
content TEXT,
embedding vector(1536) -- Specialized type for OpenAI embeddings
);
Conclusion
We’ve traveled from the physical bytes on the disk (Tablespaces) to the cutting edge of AI (pg_vector). PostgreSQL isn’t just a place to put your data — it’s an incredibly flexible, programmable, and scalable environment that can grow with your application.
By mastering these objects, you aren’t just writing SQL; you are architecting a system that is robust, automated, and fast. The next time you start a project, don’t just CREATE TABLE. Think about the Indexes, the Partitioning, the Roles, and the Extensions that will make your database a masterpiece.
← PostgreSQL Blog