PostgreSQL Schema Layering

Schemas do more work in PostgreSQL than many teams admit. They are naming boundaries, ownership boundaries, search-path boundaries, and the first permission boundary inside one database. That makes schema design the simplest way to keep a layered warehouse legible without splitting into more databases than the workload actually needs.

Live Baseline

PostgreSQL | current schema footprint | verify the database already carries layer meaning

Read the current object layout before inventing a new taxonomy

Run this before proposing a new schema model so you do not redesign around assumptions that the database has already outgrown. It is typically triggered during platform review or medallion cleanup. The query is read-only. Its purpose is to show where real objects already live.

SELECT schemaname, tablename
FROM pg_tables
WHERE schemaname NOT IN ('pg_catalog','information_schema')
ORDER BY schemaname, tablename
LIMIT 20;
schemanametablename
bronzedim_country
bronzedim_index
bronzeeurostoxx50_ohlcv
dbopowershell_automation_chain_stage
demo_stcemployee
goldindex_performance

The shape is already visible without interpretation tricks: bronze, silver, and gold are meaningful pipeline layers; demo_stc is clearly sandbox/demo territory; dbo is legacy or compatibility debt rather than a PostgreSQL-native naming decision.

PostgreSQL | schema ownership and ACLs | inspect the current boundary honestly

Read ownership and grants together

Run this when reviewing cross-schema security or deciding whether public is still too open. It is typically triggered by permission design or least-privilege review. The query is read-only. Its purpose is to show which schemas are owner-only today and where default grants still exist.

SELECT nspname AS schema_name,
       nspowner::regrole AS owner_name,
       COALESCE(array_to_string(nspacl, ', '), '(owner only)') AS acl
FROM pg_namespace
WHERE nspname IN ('public','bronze','silver','gold','demo_stc','dbo')
ORDER BY nspname;
schema_nameowner_nameacl
bronzepostgres(owner only)
dbopostgres(owner only)
demo_stcpostgres(owner only)
goldpostgres(owner only)
publicpg_database_ownerpg_database_owner=UC/pg_database_owner, =U/pg_database_owner
silverpostgres(owner only)

This is the key PostgreSQL security read: most application schemas are owner-only, but public still carries its default grant posture. If a team is serious about namespace discipline, public should be a deliberate choice, not an ambient fallback.

Schema-Per-Layer As The Default

PostgreSQL | medallion layering | keep bronze, silver, and gold readable

Use schemas when one database still makes operational sense

Schema-per-layer is the default recommendation when the same database can safely host raw, refined, and serving surfaces without requiring different recovery, HA, or lifecycle policies. It keeps joins simple and makes ownership obvious without multiplying database-level administration.

Current stoxx already validates the pattern:

Layer schemaCurrent role in the database
bronzeraw and lightly normalized ingestion surfaces
silvercleaned and modeled analytical tables
goldsmall serving or score surfaces

Schema-Per-Domain And Schema-Per-Source

PostgreSQL | alternate layering patterns | use them only when the boundary is real

Add more schemas only when they carry real operational meaning

Use schema-per-domain when ownership, permissions, or deployment cadence differ by business domain. Use schema-per-source when raw ingestion from many source systems would otherwise become unreadable inside one staging layer. Do not add schemas simply to make the diagram look advanced.

Good reasons to add schemas:

PatternGood trigger
schema-per-domainteams truly own different table sets and permission boundaries
schema-per-sourcemany raw feeds would clutter a single bronze namespace
dedicated control or meta schemapipeline state, watermarks, and operational metadata need isolation

Control, Audit, And Contract Schemas

PostgreSQL | control-plane schemas | separate system state from business tables

Keep operational tables out of the core analytical layers

Control-plane objects such as watermarks, run history, schema-version markers, and data-quality state deserve their own schema because they are neither bronze nor gold. The same is true for explicit audit/history tables and stable contract views that shield consumers from physical table churn.

Recommended pattern:

SchemaPurpose
control or metaETL state, watermarks, run metadata
audit or historyappend-only business or operational history where needed
contract or curated views in serving schemastable consumer-facing projections

Cross-Schema Security

PostgreSQL | schema grants | prove how USAGE scopes access

Use schema-level USAGE as the first access gate

Run this when designing least-privilege access for readers or service roles. It is typically triggered during role design or onboarding a new consumer. The demo is transactional and rolled back. Its purpose is to show that schema access is explicit: a role with USAGE on one schema does not automatically see the others.

BEGIN;
 
CREATE ROLE note05_reader NOLOGIN;
GRANT USAGE ON SCHEMA bronze TO note05_reader;
 
SELECT nspname AS schema_name,
       has_schema_privilege('note05_reader', nspname, 'USAGE') AS note05_reader_can_use
FROM pg_namespace
WHERE nspname IN ('bronze','silver','gold')
ORDER BY nspname;
 
ROLLBACK;
schema_namenote05_reader_can_use
bronzet
goldf
silverf

This is the behavior you want for layered systems: explicit access to the namespace you meant to expose, and nothing ambient beyond that.

Decision Guide

PostgreSQL | choose the smallest boundary that solves the real problem

Prefer schemas until a stronger boundary is required

If you need…Prefer…
simple layer separation with shared recovery and HAschemas in one database
different backup, failover, or lifecycle policies by layerseparate databases
stable source-scoped raw ingestionsource-specific bronze schemas
shared control-plane state across layersdedicated control schema

Anti-Patterns

PostgreSQL | layering mistakes | avoid boundaries that confuse more than they protect

Keep the model operationally legible

Anti-patternWhy it hurts
dumping everything into publicdestroys namespace and permission clarity
creating many schemas with no ownership or grant distinctionsadds ceremony without real boundary value
copying SQL Server’s dbo default mentally into PostgreSQLhides the fact that PostgreSQL namespaces are more explicit and customizable
mixing control metadata into analytical schemasblurs operational and business contracts

Current Recommendation For stoxx

Clean up the edge cases, not the core model

The live database already supports schema-per-layer as the default design. The immediate improvement is not to redesign the whole namespace map. It is to tighten the exceptions:

ObservationRecommendation
bronze, silver, gold are already legiblekeep them as the main layer boundary
demo_stc is clearly non-productionkeep it isolated from core analytical flows
dbo exists in a PostgreSQL databasetreat it as compatibility or migration debt and avoid expanding it
public still has default visibility posturedecide explicitly whether to keep, restrict, or empty it

Next: 06-postgresql-index-types-and-strategy moves from namespace design to access-path design: B-tree, GIN, GiST, BRIN, partial and expression indexes, and how PostgreSQL index families map to real query patterns.