9.2 Schemas as Namespaces Inside a Database
Right, so you’ve got a database. It’s a big, empty warehouse. You could just start throwing tables in there willy-nilly, but you’d end up with a mess, like a garage where your car shares space with holiday decorations, a broken washing machine, and that one weird-smelling box you’re afraid to open. This is where schemas come in. Think of a schema as a logical namespace, a way to put up walls and create separate rooms inside your database warehouse. It’s how you impose order on the chaos.
The public schema is the default room everyone gets. It’s where your tables land if you don’t specify otherwise. And look, I get the appeal of just dumping everything in public—it’s right there, it’s easy. But public is a trap. It’s the database equivalent of using “password” as your password. It works, but it shows a profound lack of imagination and will bite you later. We can do better.
Why Bother with Schemas?
Why go through the trouble? Three big reasons: organization, access control, and, my personal favorite, avoiding name-punch-ups.
First, organization. You can group related tables together. finance.receivables, finance.payables, hr.employees, hr.benefits. It’s so much cleaner. When you \dt in psql or browse in your GUI, you don’t get a single, soul-crushing list of 200 tables; you get a nice, logical folder structure.
Second, access control. This is the killer feature. You can grant permissions on a per-schema basis. You can say “The reporting user can only SELECT from anything in the analytics schema” without giving them a peek at your user_passwords table (you’re not storing plaintext passwords, right? Right?!). This is infinitely more granular and sensible than trying to manage table-level permissions for everything.
Third, avoiding naming collisions. Let’s say you’re integrating data from two third-party systems, and they both have a generic users table. Instead of creating abominations like third_party_a_users and third_party_b_users, you can just create two schemas: vendor_a and vendor_b. Each can have its own users table, and everyone lives in peace. The database manages this separation perfectly.
Creating and Using Schemas
Creating a schema is stupidly simple. The designers got this one right.
CREATE SCHEMA finance;
CREATE SCHEMA hr;
Boom. You now have two new, empty rooms. To put something in that room, you use dot notation:
-- This table goes into the 'finance' schema, not 'public'
CREATE TABLE finance.invoices (
id SERIAL PRIMARY KEY,
amount DECIMAL NOT NULL,
due_date DATE NOT NULL
);
-- This one goes into 'hr'
CREATE TABLE hr.employees (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
salary DECIMAL NOT NULL
);
To query them, you just use the full name:
SELECT * FROM finance.invoices WHERE due_date < CURRENT_DATE;
Now, you can change your search_path—which is the order in which PostgreSQL looks for unqualified object names—to avoid typing the schema name every time. But use this power wisely. Setting it for a session is fine:
SET search_path TO finance, public;
Now, SELECT * FROM invoices; will look in finance first. But be careful; if you create a table now without specifying, it’ll land in finance, not public. This is a common “oops” moment. The real best practice is to explicitly name the schema 99% of the time. It makes your code clear and unambiguous, which is something the next person (probably future you) will thank you for.
The public Schema Landmine
I need to call out the public schema again because it’s weird. By default, every user has the ability to create objects in the public schema. That’s… a choice. In any sort of multi-user environment, this is a hilariously bad idea. It’s like leaving your front door open with a sign that says “Please Don’t Make a Mess.” The first thing you should do on a new database is revoke that ability.
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
Yes, the capitalization is absurd. CREATE is the privilege, SCHEMA public is the object, FROM PUBLIC is the special role that represents all users. It reads like a bad tongue twister, but it’s vital. This command doesn’t break anything that already exists; it just stops new users from cluttering up the place.
Dropping Schemas (And the Carnage Within)
Dropping a schema is a definitive act. You’re declaring that room and everything in it is no longer needed. PostgreSQL, rightly, won’t let you drop a schema that contains objects unless you tell it to nuke everything from orbit.
-- This will fail if the schema contains even a single table
DROP SCHEMA finance;
-- This is the "I know what I'm doing" version
DROP SCHEMA finance CASCADE;
The CASCADE option is terrifyingly powerful. It will drop the schema, every table in it, every view, every function, every everything. It will then recursively drop any objects in other schemas that depend on objects in the one you’re dropping. There is no undo. There is no confirmation dialog. It’s the database equivalent of saying “hold my beer.” Always, always, always double and triple-check your schema name before hitting enter on a DROP ... CASCADE. I’m not kidding. I’ve seen grown men weep over this.
The sane alternative is to drop the individual objects first, then the empty schema. Or better yet, use a migration tool or infrastructure-as-code so this is all scripted and reviewed, not typed live into a prompt while you’re on your third coffee.