Skip to content
Ameen Abozaid
PublishedBEG-051-L03

Databases, schemas, and users

  • Lesson 3 of 6
  • 11 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

You can now connect and look around. In this lesson you start organizing and securing what you build. PostgreSQL arranges things in layers, and the library picture fits them well: one server is the building; inside it, each database is a room; inside a room, each schema is a shelf unit that groups tables; and each user (the manual calls it a role) is a person with a key that opens some doors and not others.

You will create a user and a database for the Library application, connect as that user, and see why you should not do daily work as the all-powerful postgres account.

Why this matters: BEG-051's common-mistake list begins with "working as the postgres superuser". Setting up a dedicated user takes two commands, and it is the habit that keeps a coding slip from becoming a disaster.

This lesson builds on Lessons 1 and 2 (connecting with psql, \l, \dn, \du) and on BEG-050 (DDL and the idea of a table).

Learning Objectives

By the end of this lesson you will be able to:

  • Describe the layers server, database, schema, and table, and where each lives.
  • Create a user with CREATE USER and a database with CREATE DATABASE ... OWNER.
  • Explain what a role, a superuser, and the public schema are.
  • Write a schema-qualified table name and explain the search path in one sentence.
  • Connect as the application user and check who you are with \conninfo or SELECT current_user;.
  • Explain why an application must not connect as postgres.

Written Explanation

The layers

text
server (one installation)
  database "library"
    schema "public"
      tables: authors, books, members, loans
    schema "staging"
      tables: ...
  database "postgres"
  ...

According to the PostgreSQL manual, a schema is a named container inside one database that holds tables and other objects. Two different schemas can contain tables with the same name, and schemas cannot be nested. Every new database starts with a schema named public, and an unqualified table name goes there. So CREATE TABLE books (...) is the same as CREATE TABLE public.books (...).

A database is a bigger boundary than a schema. When you connect with psql -d library, you connect to that one database; to work in another one you reconnect (in psql, \c name). That makes a database a natural unit for one application: Library data in library, something else in its own database.

Qualified names and the search path

You can name a table with its schema: staging.tmp_books. If you write just books, PostgreSQL looks through a list of schemas called the search path. The default setting is "$user", public. The first entry means "a schema named after the current user, if one exists", and since it normally does not, public is where plain names are found. SELECT current_schema(); tells you where new tables would be created.

For this course, you will keep to the public schema and use a second schema only to see how it works. Two schemas are useful when one database holds several areas, such as staging data separate from live data.

One recent change to know about: from PostgreSQL 15 on, ordinary users no longer get the right to create objects in public by default. The manual says databases upgraded from older versions keep the old behavior. In our setup the user who owns the database can create tables, which is why Lesson 6 works. If you ever get permission denied for schema public as a non-owner, that is the cause.

Roles, users, and superusers

PostgreSQL uses one concept, the role, for both individual people and groups. A role that can log in is what we usually call a user. CREATE USER is the same as CREATE ROLE plus the LOGIN attribute. Roles belong to the whole server, not to one database, so a name is unique across the installation.

Two attributes matter here. By default a new role is not a superuser (NOSUPERUSER) and cannot create databases (NOCREATEDB). A superuser bypasses all permission checks, which is why the manual advises granting it only when necessary. The postgres role created at installation is a superuser. You can see the difference with \du:

text
                              List of roles
  Role name  |                         Attributes
-------------+------------------------------------------------------------
 library_app |
 postgres    | Superuser, Create role, Create DB, Replication, Bypass RLS

library_app has no special attributes: it can log in (it was created with CREATE USER) and own what it creates, and nothing more.

Creating the Library user and database

Connect as postgres once, for setup only:

sql
CREATE USER library_app WITH PASSWORD 'change-me';
CREATE DATABASE library OWNER library_app;
  • The user is created first, because the database names it as its owner.
  • OWNER library_app makes that user the owner of the new database. The manual says you can only name another role as owner if you may switch to that role, which a superuser can.
  • 'change-me' is a placeholder. A password typed into SQL like this travels to the server in plain text and may be saved in your client history or in the server log, as the manual for CREATE ROLE warns. In real work, set the password with \password library_app in psql, which sends it encrypted. For a local learning server a throwaway value is acceptable, but never reuse a real one.

CREATE DATABASE cannot be run inside a transaction block (Lesson 5 explains what that is), so run it by itself.

Now connect as the new user:

bash
psql -U library_app -d library

Check who you are: SELECT current_user, current_database(); returns library_app and library.

Why not postgres?

The superuser can read, change, or drop everything: every database, every user. If your application connects as postgres, then:

  • a bug (or a deliberate attack such as SQL injection, which later modules cover) can drop any database on the server;
  • a leaked configuration file hands over total control, not just access to Library data;
  • you cannot tell from the user name which application did something.

Giving each application its own user, which can reach only its own database, is called least privilege: give only the rights the job needs. You can see it working. Logged in as library_app, try to create a user or a database:

text
library=> CREATE ROLE intruder LOGIN;
ERROR:  permission denied to create role
DETAIL:  Only roles with the CREATEROLE attribute may create roles.
library=> CREATE DATABASE other;
ERROR:  permission denied to create database

The prompt ends in => for an ordinary user and =# for a superuser. That small character is a handy reminder of how powerful your session is.

Note one limit on what this lesson claims: the database owner still has broad power inside its own database (it may create and drop tables there), which is exactly what the Lab needs. Finer-grained rights (granting only SELECT on some tables) come in later levels.

Examples

Example 1: listing rooms and keys (simple)

As postgres:

sql
\l library
\du library_app
text
  Name   |    Owner    | Encoding | ...
---------+-------------+----------+-----
 library | library_app | UTF8     | ...

The owner column shows that library_app owns the room. \du shows its empty attributes column. Both commands read information without changing anything.

Example 2: a second schema (practical)

Connected as library_app to library:

sql
CREATE SCHEMA staging;
CREATE TABLE staging.tmp_books (id INTEGER PRIMARY KEY);
\dt staging.*
SHOW search_path;
SELECT current_schema();
text
 Schema  |   Name    | Type  |    Owner
---------+-----------+-------+-------------
 staging | tmp_books | table | library_app

   search_path
-----------------
 "$user", public

 current_schema
----------------
 public

The table lives on the staging shelf, not public. A plain SELECT * FROM tmp_books; would fail, because staging is not on the search path; staging.tmp_books works. If a table is "missing" and you can see it in a GUI, check its schema first.

Example 3: a user who tries too much (realistic)

A new teammate connects to library as library_app and runs a cleanup script that someone pasted from the internet. The script contains DROP DATABASE postgres; and CREATE ROLE backdoor SUPERUSER;. As library_app, the server refuses both, because the role has no such privileges (the earlier errors are what you see for creating a role or a database). The same script run as postgres would be dangerous. The restricted user did not stop all mistakes, but it limited the blast radius to one database.

Note that this example describes the permission checks you saw, rather than the exact errors for DROP DATABASE and SUPERUSER, which we did not run.

Backend Insight

Production systems usually have at least two kinds of database users. An administrator role is used by humans or migration tools to create and change structure. An application role is used by the running service and has only what it needs. Many teams go further and make the application role unable to drop tables at all. Whatever the exact design, the rule is that the credentials stored in your service's configuration should open as few doors as possible.

Names also matter. A user called library_app and a database called library make logs and error messages readable (library_app failed to insert into library). When you configure a Java connection later, you will supply exactly these values: host, port, database library, user library_app, and the password.

Common Mistakes

Mistake 1: Working as postgres because it "just works"

It works because it ignores every permission check, including the ones that would have caught your mistake. Use postgres to create users and databases, then switch.

Mistake 2: Putting a real password in a SQL script in Git

CREATE USER ... PASSWORD 'secret' in a committed file leaks the password. Use placeholders in files you commit and set real passwords with \password.

Mistake 3: Creating the database before the user

CREATE DATABASE library OWNER library_app; fails if the role does not exist yet. Order: role first, then database.

Mistake 4: Confusing schemas with databases

A schema is a folder inside one database; connecting to a different database requires a new connection, but moving between schemas does not. Tables in different databases cannot be referenced with a simple schema-qualified name.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. Put in order, largest to smallest: table, database, schema, server.

Show solution A1

A1. Server, database, schema, table.

A2. What is the name of the default schema, and what does CREATE TABLE books (...) do with it?

Show solution A2

A2. The default schema is public. An unqualified CREATE TABLE books creates public.books.

A3. What is the difference between CREATE ROLE x; and CREATE USER x;?

Show solution A3

A3. CREATE USER is CREATE ROLE with LOGIN implied; a plain CREATE ROLE cannot log in unless you add LOGIN.

Level B: Application

B1. Write the two statements that create a user shop_app with a placeholder password and a database shop owned by it. Which one goes first and why?

Show solution B1

B1. CREATE USER shop_app WITH PASSWORD 'change-me'; then CREATE DATABASE shop OWNER shop_app;. The user goes first because the database statement names it as owner and fails if it does not exist.

B2. After connecting, the prompt is shop=#. What does the # tell you, and should you be doing daily work in this session?

Show solution B2

B2. # indicates a superuser session. No: use an ordinary user for daily work and keep the superuser for setup.

B3. A table staging.old_members exists, but SELECT * FROM old_members; fails with "does not exist". Why, and what are two ways to fix it?

Show solution B3

B3. The table is in staging, which is not on the search path. Fix: write staging.old_members, or add the schema to the search path (SET search_path TO staging, public;) for the session.

Level C: Challenge

C1. A manager says "the app keeps getting permission errors, just connect it as postgres". Give a reasoned reply with two risks and one alternative.

Show solution C1

C1. Risks: a bug or SQL injection could drop any database; a leaked configuration exposes the whole server. Alternative: find which privilege is missing (for example a table owned by someone else) and grant just that, or make the app user the owner of its own database as in this lesson.

C2. Design (in words) the database users for a small project with a developer who changes tables and a web application that only runs the service. State which attributes each needs and what you would avoid.

Show solution C2

C2. Developer role: can log in and owns or may change the application's tables; no superuser. Application role: can log in and works only on the application's database, ideally with only the rights to read and change data, not to drop tables. Avoid superuser for both, shared passwords, and real passwords in committed files. Detailed granting is a later topic, so this is a design goal rather than a recipe.

Mini Challenge

On your own server, as postgres, create library_app and the library database owned by it. Then:

  1. Reconnect as library_app and show SELECT current_user, current_database();.
  2. Try CREATE ROLE intruder LOGIN; and copy the error.
  3. Create a schema staging with one table in it and list it.
  4. Write down in two sentences what the owner column of \l shows and why that is better than postgres.

Check your work: you ran steps 1 to 3 as library_app, you set the password through the prompt or a throwaway placeholder, and no real password appears in your notes.

Summary & Next Lesson

Key takeaways

  • A server contains databases; a database contains schemas; a schema contains tables.
  • public is the default schema, and plain names are resolved through the search path.
  • A role that can log in is a user; the superuser (postgres) bypasses all permission checks.
  • Create the user first, then CREATE DATABASE ... OWNER that user, and connect as it.
  • Least privilege limits damage from bugs and leaks; a prompt ending in # means superuser.

Next lesson

The next lesson, "Constraints that protect data", moves into the library database and teaches the rules on the door: NOT NULL, UNIQUE, CHECK, and foreign keys, and what happens when someone tries to break them.

Self-check

  1. Which comes first: CREATE USER or CREATE DATABASE ... OWNER?
  2. What is the default schema called?
  3. What does \du show?
  4. Name one reason not to connect the application as postgres.
  5. How can you tell from the prompt that you are a superuser?
Show answers
  1. CREATE USER, because the database statement names the owner.
  2. public.
  3. The roles (users) on the server and their attributes, such as Superuser.
  4. A bug or a leaked password would give control of every database on the server.
  5. The prompt ends with # (an ordinary user sees >).

References

  • PostgreSQL documentation, Schemas. Supports: schema definition, public, qualified names, the default "$user", public search path, and the PostgreSQL 15 change to CREATE on public.
  • PostgreSQL documentation, Database Roles. Supports: roles are cluster-wide, a role may be a user or a group, superusers bypass checks.
  • PostgreSQL documentation, CREATE ROLE. Supports: CREATE USER implies LOGIN, defaults of NOSUPERUSER and NOCREATEDB, and the warning about plaintext passwords in history and logs.
  • PostgreSQL documentation, CREATE DATABASE. Supports: the OWNER option and that CREATE DATABASE cannot run in a transaction block.
  • The permission denied errors, \du, and \dn outputs were produced on a local PostgreSQL 18.3 test server. The => versus =# prompt convention is standard psql behavior that I did not verify on the pages I read or in my test session (which ran from scripts without a visible prompt), so treat it as a common convention.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Continue to next lesson

Type to search levels, phases, modules and lessons.