In 09-01 we looked at what to read and in 09-02 at where to study and practice. The third vertex is missing, and it is the one that turns the rest into real work: what with. This lesson is the inventory of the tools you actually work with on databases — engines on your own machine, clients, design, migrations, test data, diagnostics and backups — and, above all, the judgment about which ones you need now and which ones you do not need yet.

Because the dominant mistake in this field is not using too few tools, it is accumulating them. It is easy to end up with three GUI clients installed, two diagramming tools, a migration tool that never gets used and no backup configured at all. The final section proposes a minimum toolkit of five pieces you can start with tomorrow, and an explicit list of what you can ignore without remorse.

Everything here is aimed at letting you do what the last lesson of module 8 asked of you: take one of the VallBici cases or the BiblioRed schema, set it up on your machine and break it.

Important warning. The versions of engines and tools move on constantly; command-line options change; and the licenses and business models of tools change too — there are projects that have gone from free to commercial and back again. The commands in this lesson are correct in their general form, but always check the syntax and the package names in the documentation for your version. And the rule that governs everything else: the official documentation overrides whatever any course says, this one included. No prices and no free-tier conditions are quoted either, because they change without notice: check at the official source before signing up for anything.

Contents

  1. Engines on your machine: installation and startup
  2. Docker and Docker Compose: the 08-03 environment in one command
  3. Managed databases in the cloud
  4. Console clients: psql and mongosh
  5. GUI clients
  6. Design and diagrams
  7. Migrations and schema version control
  8. Test data
  9. Performance and diagnostics
  10. Backups, monitoring and administration
  11. The minimum toolkit
  12. Common Mistakes and Tips
  13. Exercises
  14. Conclusion: closing the course

  1. Engines on your machine: installation and startup

Having the engine installed locally is not negotiable. You can learn a lot against a managed database, but you cannot shut it down halfway through a transaction to see what happens, and that kind of experiment is exactly what you are missing.

Engine When to install it Weight Follows on from
PostgreSQL Always. It is the course's reference engine and the one that solves 90% of cases Medium The whole course
SQLite Always; you almost certainly have it already. For quick tests, prototypes and file analysis None 01-02, 01-04
MongoDB If you are going to work with documents or want to redo 08-02 Medium 03-03, 08-02
Redis If you are going to redo 08-03 or work with caching and ephemeral data Low 03-02, 08-03
Elasticsearch Only if you are going to redo the search part of 08-03. It is the heaviest High 08-03
Neo4j / Cassandra Only out of curiosity or specific need. Do not install them "just in case" High 03-02

PostgreSQL

# Debian / Ubuntu
sudo apt update
sudo apt install postgresql postgresql-contrib

# macOS with Homebrew
brew install postgresql@16
brew services start postgresql@16

# Check that the service is alive (Linux with systemd)
sudo systemctl status postgresql
sudo systemctl enable --now postgresql

After installing on Linux there is a system user postgres, who is the engine's superuser. The sensible first step is to create your own role and your own database, instead of always working as the superuser — exactly what was argued in 06-04:

# Log in as the engine superuser
sudo -u postgres psql

# Inside psql: create a role and a database for the project
CREATE ROLE bibliored_app WITH LOGIN PASSWORD 'change-me';
CREATE DATABASE bibliored OWNER bibliored_app;
\q
# Now connect with the application role
psql -h localhost -U bibliored_app -d bibliored

The postgresql-contrib package deserves a note: it brings extensions you are going to want, among them pg_stat_statements (section 9), pg_trgm (similarity search, the one compared with Elasticsearch in 08-03) and btree_gist (needed for exclusion constraints over ranges).

SQLite

# Debian / Ubuntu
sudo apt install sqlite3

# macOS: it ships with the system; for the most recent version
brew install sqlite

# Usage: the database is a file, there is no server and no service
sqlite3 test.db
-- Inside sqlite3
.databases
.tables
.schema loans
.mode box          -- readable output in columns
.headers on
.quit

SQLite is the most underrated tool in the inventory. For trying out a schema idea, for analyzing a CSV with SQL or for carrying sample data in a repository, there is nothing faster. Remember what 01-02 said: its typing is dynamic and its concurrency model is single-writer, so it is not a substitute for PostgreSQL in an application with several users writing at the same time.

MongoDB and Redis

# MongoDB: the official packages are installed by adding the vendor's
# repository; check the installation guide for your system at
# https://www.mongodb.com/docs/
sudo systemctl enable --now mongod
mongosh

# Redis
sudo apt install redis-server        # Debian / Ubuntu
brew install redis                   # macOS
sudo systemctl enable --now redis-server
redis-cli ping                       # should answer PONG

For MongoDB and Redis, and even more so for Elasticsearch, the advisable route on a work machine is not installing them as system services but bringing them up in containers. That is what comes next.

  1. Docker and Docker Compose: the 08-03 environment in one command

Installing four engines as system services means four services always starting up, four configurations to maintain and a painful uninstall the day you want to clean up. With containers, the whole environment is a text file you can version, share and destroy without a trace.

# Bring up a standalone PostgreSQL in 30 seconds
docker run --name pg-test \
  -e POSTGRES_PASSWORD=secret \
  -p 5432:5432 \
  -d postgres:16

# Connect from inside the container itself
docker exec -it pg-test psql -U postgres

# Destroy it without a trace
docker rm -f pg-test

And this is the file that brings up the environment of the polyglot case study in 08-03: PostgreSQL as the transactional source of truth, MongoDB for telemetry and profiles, and Redis for real-time availability.

# docker-compose.yml — environment for the VallBici polyglot case (08-03)
# Usage:  docker compose up -d      /  docker compose down -v  (deletes the data)
services:

  # --- PostgreSQL: source of truth for the transactional core ---------------
  postgres:
    image: postgres:16                 # pin the major version; do not use "latest"
    container_name: vallbici-postgres
    environment:
      POSTGRES_USER: vallbici
      POSTGRES_PASSWORD: development   # local only; never in production
      POSTGRES_DB: vallbici
    ports:
      - "5432:5432"                    # host:container
    volumes:
      - pgdata:/var/lib/postgresql/data          # persistent data
      - ./sql:/docker-entrypoint-initdb.d:ro     # .sql scripts that run
                                                 # only the first time
    healthcheck:                       # so other services can wait
      test: ["CMD-SHELL", "pg_isready -U vallbici"]
      interval: 5s
      retries: 10

  # --- MongoDB: telemetry, enriched profiles and incidents ------------------
  mongo:
    image: mongo:7
    container_name: vallbici-mongo
    environment:
      MONGO_INITDB_ROOT_USERNAME: vallbici
      MONGO_INITDB_ROOT_PASSWORD: development
    ports:
      - "27017:27017"
    volumes:
      - mongodata:/data/db

  # --- Redis: real-time availability, sessions and reservations -------------
  redis:
    image: redis:7
    container_name: vallbici-redis
    command: ["redis-server", "--appendonly", "yes"]   # AOF persistence
    ports:
      - "6379:6379"
    volumes:
      - redisdata:/data

volumes:
  pgdata:
  mongodata:
  redisdata:
docker compose up -d          # bring everything up
docker compose ps             # see status
docker compose logs -f postgres
docker compose down           # stop, keeping the volumes
docker compose down -v        # stop and DELETE the data

Four details of the file deserve attention, because they are what separates a toy compose file from a useful one:

  1. Pinned version (postgres:16, not postgres:latest). With latest, one day you update the image by accident and the data format is no longer compatible.
  2. Named volumes. Without them, docker compose down takes your data with it. With them, it survives until you explicitly ask for -v.
  3. docker-entrypoint-initdb.d. Any .sql you leave in ./sql runs when the database is created for the first time. It is the natural home for the BiblioRed or VallBici schema: cloning the repository and running docker compose up -d leaves you with the schema created.
  4. healthcheck. It lets your application — or a migrations container — wait until PostgreSQL is genuinely accepting connections, not merely started.

If you also want Elasticsearch to reproduce the station search, add it as one more service; bear in mind that it consumes considerably more memory than the other three together and usually needs memory tuning on the host machine.

  1. Managed databases in the cloud

A managed database is the same engine, operated by somebody else: backups, upgrades, high availability and monitoring come included. You do not need them in order to learn; for publishing a project of your own they are very convenient.

Type of offering Established examples When it makes sense
PostgreSQL managed by the big providers Amazon RDS and Aurora, Google Cloud SQL, Azure Database for PostgreSQL When you already work in that cloud
PostgreSQL from specialized providers Neon, Supabase, Crunchy Bridge, among others Personal projects and prototypes; they usually have a free tier
Managed MongoDB MongoDB Atlas Redoing 08-02 without installing anything; it includes sample data sets
Managed Redis Redis Cloud and each cloud's equivalents Caching in a published project
Managed search Elastic Cloud and equivalents When local Elasticsearch is too heavy for you

About free tiers. Several of these offerings have a free tier or initial credit, and they are perfectly suitable for a published learning project. But the conditions — storage limits, pauses for inactivity, expiry — change frequently, so check them on the provider's official website at the moment you are going to use them. And two practical cautions: first, always turn on spending alerts before creating anything, because pay-per-use billing can surprise you; second, a managed database does not exempt you from knowing how to administer: if you do not understand what a VACUUM is or what the isolation level implies, the pretty dashboard is not going to save you.

  1. Console clients: psql and mongosh

We start with the console clients and not the graphical ones, deliberately. psql is not the basic option: it is the most powerful tool of all the ones in this lesson. Everything that exists in a GUI client exists in psql, and quite a lot of what is in psql exists in no GUI client. Besides, it is always available: on the server, inside the container, over SSH, in a script.

The most used psql metacommands

They pick up what 01-04 called the system catalog: each one of these metacommands is, in reality, a query against the catalog written by you without realizing it.

Metacommand What it does
\l Lists the server's databases
\c bibliored Connects to another database
\dt Lists the tables in the current schema
\d loans Describes a table: columns, types, indexes, foreign keys
\d+ loans The same, with on-disk size, storage and descriptions
\di Lists the indexes
\dn Lists the schemas
\du Lists the roles and their attributes (a direct continuation of 06-04)
\df Lists the functions
\sf function_name Shows a function's source code
\x Toggles expanded output (one column per line); essential with wide tables
\timing Turns on the execution time of every query
\e Opens the last query in your editor
\i file.sql Runs a SQL file
\copy mytable FROM 'data.csv' CSV HEADER Imports/exports CSV from the client (needs no server permissions)
\watch 2 Repeats the last query every 2 seconds; excellent for watching counters
\? / \h CREATE INDEX Metacommand help / SQL syntax help
\q Quit

Two habits worth a lot that cost little: turn on \timing always, so every query tells you how long it takes; and use \watch to observe a counter live while another session works — it is the most direct way to see the concurrency phenomena of 06-02 with no additional tooling.

A ~/.psqlrc file with your preferences does the rest:

-- ~/.psqlrc
\set QUIET 1
\timing on
\x auto
\set HISTSIZE 10000
\set PROMPT1 '%[%033[1;32m%]%n@%/%[%033[0m%]%R%# '
\pset null '(null)'
\set QUIET 0

That \pset null '(null)' is more useful than it looks: by default a NULL and an empty string look exactly the same in the output, and that confusion has cost a lot of people a lot of debugging hours.

mongosh

// Common mongosh metacommands and operations
show dbs
use vallbici
show collections

db.trips.countDocuments({ status: "closed" })
db.trips.findOne()
db.trips.find({ bicycle_id: 417 }).sort({ start_ts: -1 }).limit(5)

db.trips.getIndexes()
db.trips.createIndex({ bicycle_id: 1, start_ts: -1 })

// The equivalent of EXPLAIN: a direct continuation of 06-03
db.trips.find({ bicycle_id: 417 })
        .explain("executionStats")

db.stats()
db.trips.stats()

mongosh is a complete JavaScript interpreter, not just a client. You can write loops and functions and load files with load('script.js'), which makes it the natural tool for generating test data or for one-off document migrations.

  1. GUI clients

A GUI client gives you three real things: browsing an unfamiliar schema much faster, seeing results in a comfortable grid, and generating diagrams by reverse engineering. It does not give you — and it is best not to fool yourself — any capability the console does not have.

Tool Engines supported License Strong point Who for
DBeaver Community (https://dbeaver.io/) Very many, via JDBC: PostgreSQL, MySQL, SQLite, Oracle, SQL Server, and NoSQL in the commercial edition Free (Community edition) The universal browser: a single client for everything, with reverse-engineered diagrams Anyone touching several different engines
pgAdmin (https://www.pgadmin.org/) PostgreSQL only Free Total PostgreSQL coverage, administration included: roles, backups, statistics Anyone who lives in PostgreSQL and does administration
MongoDB Compass (from https://www.mongodb.com/) MongoDB only Free from the vendor Exploring documents with no known schema, building aggregation pipelines visually and seeing a graphical explain() Anyone working with MongoDB
TablePlus (https://tableplus.com/) Several, relational and some NoSQL Commercial, with a limited trial mode Speed and a very polished interface Anyone who spends the day in a client and values ergonomics
Beekeeper Studio (https://www.beekeeperstudio.io/) Several relational ones Free community + commercial edition A lightweight, simple alternative Anyone wanting something simple and free
DataGrip (JetBrains) Many Commercial Integration with the rest of the JetBrains tools, excellent refactoring and autocompletion Anyone already using that ecosystem
Code editor extensions (for example, database extensions for VS Code) Depends on the extension Varies Not leaving the editor to fire off a query Anyone who just needs quick queries next to the code

An honest recommendation. Install one. If you touch several engines, DBeaver. If only PostgreSQL and you do administration, pgAdmin. If you work with MongoDB, Compass on top of the previous one because it does things no other tool does. Having three relational clients installed is an almost infallible sign that none of them has been learned properly.

And a warning that recurs over time: tool licenses change. Projects that started out free have moved to commercial models and some the other way round. Check the current license before resting a team workflow on a particular tool.

  1. Design and diagrams

In 04-02 you drew entity-relationship diagrams and in 04-03 you transformed them into schemas. These are the tools with which that is done outside a course.

Tool Approach License / model Strong point Who for
Mermaid (https://mermaid.js.org/) Diagram as text, inside the repository Free The diagram lives next to the code, gets versioned and renders on repository platforms Everyone; it is what has been used in this course
dbdiagram.io (https://dbdiagram.io/) Diagram as text in a language of its own, in the browser Commercial with a free level Extremely fast for sketching and exporting the creation SQL Sketches and design discussions
DrawSQL (https://drawsql.app/) Visual editor in the browser Commercial with a free level Presentable diagrams for sharing with non-technical people Documentation facing the team
pgModeler (https://pgmodeler.io/) PostgreSQL-specific desktop modeler Open source; paid binaries Complete modeling with schema generation and synchronization Serious, sustained modeling on PostgreSQL
SchemaSpy (https://schemaspy.org/) Reverse engineering: HTML documentation from an existing database Free Documenting a legacy schema nobody understands Anyone landing on a project with no documentation
DBeaver / pgAdmin (diagrams included) Built-in reverse engineering See section 5 Seeing the diagram of what already exists without installing anything else Daily use

The criterion. For a diagram that has to live over time — in the repository, reviewed on every change — use Mermaid: it is text, it gets versioned and it renders in the README. For an afternoon's sketch, dbdiagram.io. To understand somebody else's sixty-table schema, reverse engineering with DBeaver or SchemaSpy, and you decide from there.

This is the Mermaid erDiagram you can start documenting your own schema with; it is exactly the format you have seen throughout the course:

erDiagram
    MEMBER ||--o{ LOAN : "makes"
    COPY ||--o{ LOAN : "is the object of"
    MATERIAL ||--o{ COPY : "has"
    BRANCH ||--o{ COPY : "holds"
    LOAN ||--o| FINE : "may generate"

    MEMBER {
        int member_id PK
        text first_name
        text email UK
        date join_date
        bool active
    }
    MATERIAL {
        int material_id PK
        text title
        text isbn UK
        int publication_year
    }
    COPY {
        int copy_id PK
        int material_id FK
        int branch_id FK
        text status
    }
    LOAN {
        int loan_id PK
        int member_id FK
        int copy_id FK
        timestamptz loan_date
        date due_date
        timestamptz return_date
    }
    FINE {
        int fine_id PK
        int loan_id FK
        numeric amount
        bool paid
    }

Save that block in your project's README.md and update it in the same commit in which you change the schema. It is the literal application of "document and version the schema" from 04-01, and it costs two minutes.

  1. Migrations and schema version control

This section is the most important of the lesson, and the one most people skip.

In 04-01 it was said that the schema has to be documented and versioned. A migration is the professional way of doing it: every schema change is a file, numbered and kept in the repository next to the code, applied in order and recorded by the database. The consequence is that the schema stops being the result of a series of stray ALTER TABLEs in somebody's console and becomes a reproducible sequence.

Why this is not optional. Four reasons, all verifiable on your first day working in a team:

  1. Reproducibility. Anyone clones the repository, runs the migrations and obtains exactly your schema. Without migrations, "setting up the environment" means asking a colleague.
  2. The schema and the code travel together. The commit that adds the return_date column also contains the code that uses it. Going back one version reverts both things.
  3. Repeatability across environments. What was applied in development is applied identically in staging and in production, without anyone typing anything by hand under pressure.
  4. History and auditing. "When was this index added and why?" has an answer: the commit, its date and its message.
Tool Format Ecosystem Strong point
Flyway (https://flywaydb.org/) Numbered plain SQL (V1__...sql) JVM, but usable from the command line with any language Simplicity: they are .sql files and they run in order
Liquibase (https://www.liquibase.org/) XML, YAML, JSON or SQL JVM and command line Changes described abstractly, with automatic rollback
Alembic Python SQLAlchemy Generates migrations from the difference against the models
Migrations built into frameworks Depends on the framework Django, Rails, Laravel, Entity Framework, Prisma, Ecto… You already have them: use them and do not add another tool
Lightweight tools (golang-migrate, dbmate, sqitch…) Plain SQL Agnostic One binary and SQL files, no dependencies

How to choose. If your framework already ships migrations, use those and that is that. If you use no framework, or you want explicit, controlled SQL, Flyway or an equivalent lightweight tool. Liquibase pays off in environments with several different engines and a need for formal rollback.

A minimal example with the numbered SQL file format, applied to BiblioRed:

-- V3__loans_index_member_date.sql
--
-- Context: the query for a member's loan history was doing a sequential
-- scan over 500,000 rows (see the EXPLAIN in ticket #142).
-- The column order is not arbitrary: member_id is the equality filter
-- and goes first; loan_date provides the ordering and goes after.
-- See lesson 06-03 and "SQL Performance Explained".

CREATE INDEX CONCURRENTLY idx_loans_member_date
    ON loans (member_id, loan_date DESC);
-- V4__room_reservations_no_overlaps.sql
--
-- Replaces the overlap check that lived in the application with an
-- engine constraint. Reason: under concurrency the check in the
-- application fails (lesson 06-02, write skew).

CREATE EXTENSION IF NOT EXISTS btree_gist;

ALTER TABLE room_reservations
    ADD COLUMN period tstzrange;

UPDATE room_reservations
   SET period = tstzrange(start_time, end_time, '[)');

ALTER TABLE room_reservations
    ALTER COLUMN period SET NOT NULL,
    ADD CONSTRAINT room_reservations_no_overlaps
        EXCLUDE USING gist (room_id WITH =, period WITH &&);

Three golden rules about migrations, all learned the hard way:

  1. An applied migration is never modified. If it was wrong, it gets corrected with a new migration. Changing an already applied file breaks the tool's checksum and desynchronizes the environments.
  2. Every migration must be runnable against real data. Adding a NOT NULL column with no default to a table with two million rows fails; it has to be done in steps.
  3. CREATE INDEX CONCURRENTLY in production. A normal CREATE INDEX blocks writes on the table for as long as it lasts. It is the perfect example of how what you learned in 06-02 about locking translates into a concrete operational decision.

  1. Test data

A schema with twelve rows teaches you nothing. The query planner will do a sequential scan every time, any index will look useless and no concurrency problem will show up. To learn about performance you need volume, and for that there are three routes.

Route 1: generate_series, installing nothing

It is the fastest route and it requires no external tooling. It generates half a million plausible loans for BiblioRed:

-- 500,000 loans spread across 20,000 members and 80,000 copies,
-- over the last three years
INSERT INTO loans (member_id, copy_id, loan_date,
                   due_date, return_date)
SELECT
    1 + floor(random() * 20000)::int                        AS member_id,
    1 + floor(random() * 80000)::int                        AS copy_id,
    ts                                                      AS loan_date,
    (ts + interval '21 days')::date                         AS due_date,
    CASE WHEN random() < 0.9                                -- 90% returned
         THEN ts + (random() * interval '35 days')
         ELSE NULL
    END                                                     AS return_date
FROM generate_series(
        now() - interval '3 years',
        now(),
        interval '3 minutes'
     ) AS ts;

-- Essential after a bulk load: without fresh statistics,
-- the planner makes decisions on false information
ANALYZE loans;

-- Check
SELECT count(*), min(loan_date), max(loan_date) FROM loans;

That final ANALYZE is not decoration. It is the number one cause of "I created the index and it still is not being used" in home-made tests: the planner from 06-03 decides using statistics, and after a bulk load the statistics are stale.

A warning about realism: random() produces a uniform distribution, and real data is never uniform. In BiblioRed a few titles account for the majority of loans, and that skew is exactly what makes indexes and plans interesting. If you want realistic tests, skew the distribution on purpose.

Route 2: fake data generators

Tool What it is When to use it
Faker (libraries for Python, JavaScript, PHP, Ruby…) Generates plausible names, addresses, emails, dates and text, with localization When you need data that looks real for screenshots or demos
Mockaroo (https://mockaroo.com/) Browser generator that exports CSV, JSON or SQL Quick prototypes without writing code
pgbench (ships with PostgreSQL) Generates a test schema and runs concurrent workloads Measuring the engine and observing real concurrency

pgbench deserves a separate mention because it does something the others do not: concurrent load. With pgbench -c 20 -T 60 you have twenty sessions writing at the same time for a minute, which is the way to really see what you saw in 06-02 with two terminals.

Route 3: public sample databases

Sometimes you do not want to set anything up: you want to practice queries against a non-trivial schema that already exists.

Sample database Domain Usual engine Good for
Pagila Film rental (the PostgreSQL version of Sakila) PostgreSQL The de facto standard for practicing SQL on PostgreSQL; a rich, well-normalized schema
Sakila Film rental MySQL The same, in the MySQL world
Chinook Music store PostgreSQL, SQLite, and others Very portable; excellent with SQLite for practicing without a server
Northwind Food distributor Several The veteran classic; useful for the number of published exercises that use it
MongoDB Atlas sample data sets Various (restaurants, flights, cinema) MongoDB Practicing aggregation pipelines without creating data

You find them by searching for their name; they are usually distributed as a SQL file loaded with psql -f. With Chinook on SQLite you have query practice in under a minute and without installing any server.

  1. Performance and diagnostics

A direct continuation of 06-03, where you read your first execution plan.

Tool Engine What it solves
EXPLAIN / EXPLAIN (ANALYZE, BUFFERS) PostgreSQL The estimated and the real plan, with times and disk accesses
Plan visualizers (for example https://explain.depesz.com/ and the graphical visualizers built into pgAdmin and DBeaver) PostgreSQL Turning a 200-line plan into something readable, pointing at where the time goes
pg_stat_statements PostgreSQL The most important query of all: which statements consume the most accumulated time on your server
auto_explain PostgreSQL Automatically logging the plan of queries that exceed a threshold
pgBadger PostgreSQL Analysis of the server logs with reports of slow queries, errors and waits
pg_stat_activity PostgreSQL What each session is running right now, and who is blocking whom
.explain("executionStats") MongoDB The equivalent of EXPLAIN ANALYZE: which index was used and how many documents were examined
MongoDB's database profiler MongoDB Logging slow operations to analyze them afterwards
mongostat / mongotop MongoDB Live server activity

pg_stat_statements deserves its own profile because it changes the way you work. Without it, you optimize the query somebody has complained is slow. With it, you optimize the one consuming the most total time, which is often a fast query run a hundred thousand times a day that nobody complains about.

-- Enabling it: it needs adding to shared_preload_libraries and a restart
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

-- The ten queries consuming the most total time on the server
SELECT
    substring(query, 1, 80)          AS query_text,
    calls,
    round(total_exec_time::numeric, 1) AS total_ms,
    round(mean_exec_time::numeric, 2)  AS mean_ms,
    rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
-- And the live diagnostic query: who is blocking whom
SELECT pid, state, wait_event_type, wait_event,
       now() - query_start AS duration,
       substring(query, 1, 60) AS query_text
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY duration DESC;

That second query is the one you will run the day the application "has hung". Almost always the answer is right there: a transaction open for twenty minutes that nobody has committed, blocking everyone else. It is 06-02 in real life.

  1. Backups, monitoring and administration

A continuation of 06-04. There is only one rule here that matters: a backup that has never been restored is not a backup, it is a hope.

Tool What for Level
pg_dump / pg_restore Logical backup of one database; portable across versions Essential
pg_dumpall Includes roles and cluster-wide global objects Essential if you administer
pg_basebackup Physical backup of the whole cluster Medium
pgBackRest (https://pgbackrest.org/) Incremental physical backups, retention, verification and point-in-time recovery Production
Barman An established alternative for the same thing Production
mongodump / mongorestore Logical backups of MongoDB Essential with MongoDB
Metric exporters + Prometheus + Grafana Dashboards of connections, size, slow queries, replica lag Production
pgwatch and equivalents PostgreSQL-specific monitoring with ready-made dashboards Production

The minimum, which you can have running this afternoon:

# Compressed logical backup in custom format (allows restoring
# individual tables, unlike the plain-text dump)
pg_dump -h localhost -U bibliored_app -d bibliored \
        -Fc -f bibliored_$(date +%F).dump

# Restore onto a NEW database: the proof that the backup works
createdb bibliored_test
pg_restore -d bibliored_test bibliored_2026-08-02.dump

# Minimum check: are the rows there?
psql -d bibliored_test -c "SELECT count(*) FROM loans;"
# MongoDB
mongodump  --uri="mongodb://localhost:27017/vallbici" --out=./backup
mongorestore --uri="mongodb://localhost:27017/vallbici_test" ./backup/vallbici

Automating it is a couple more lines — a scheduled task that runs the dump, compresses it and copies it somewhere else — but the part people omit is not the automation, it is the test restore. Put a quarterly restore test in the calendar. It is the only way to know the backup exists.

  1. The minimum toolkit

Everything above is the full inventory. This is what you actually need to start tomorrow.

# Tool Why it is essential
1 Local PostgreSQL (native or in a container) Without an engine of your own no experimentation is possible
2 psql It is the most powerful tool on the list and it is always available
3 Docker + a docker-compose.yml Reproducible, disposable, versioned environments
4 One GUI client (DBeaver or pgAdmin, one) Browsing other people's schemas and viewing results comfortably
5 A migration tool (your framework's, or Flyway) A versioned schema is not optional
6 EXPLAIN + pg_stat_statements Without measuring, optimizing is guessing

And this is what you do not need yet, said explicitly so you do not waste time:

Optional for now When it will stop being so
A second and a third GUI client Never
Elasticsearch, Neo4j or Cassandra installed When you have a specific problem that calls for them
A desktop modeling tool (pgModeler) When you model large schemas on a sustained basis
pgBackRest, Barman, Prometheus and Grafana When you operate a database somebody depends on
A subscription to a commercial client When the free one genuinely gets in your way, not before
Data generation tools with an interface generate_series covers almost everything for you

The tip that sums up the section: do not accumulate tools before you need them. Every installed tool has a maintenance, upgrade and attention cost. A tool is adopted when you have a problem that hurts and it solves it, not when you see it recommended in a list, this one included.

Common Mistakes and Tips

Mistake 1: not having a local engine. Working only against a shared or managed database means not being able to experiment: you cannot kill the process, fill the disk, provoke a deadlock or restore a backup over it. Everything interesting in this course requires a database you can destroy.

Mistake 2: using latest in container images. One day you update and the data format is no longer compatible with the previous version. Always pin the major version.

Mistake 3: changing the schema by hand in the console. The ALTER TABLE you typed straight into production is nowhere: not in the repository, not in your colleague's environment, and not in your memory three weeks from now. Every schema change, a migration.

Mistake 4: testing performance with two hundred rows. At that volume the sequential scan always wins and you will learn nothing about indexes. Generate hundreds of thousands of rows and run ANALYZE.

Mistake 5: forgetting ANALYZE after a bulk load. It is the most frequent cause of "the index is created but it is not being used". The planner decides using statistics, and after inserting half a million rows they are stale.

Mistake 6: trusting a backup that has not been restored. Restore onto a new database and count the rows. Quarterly. No exceptions.

Mistake 7: leaving transactions open in the GUI client. Several clients work in manual transaction mode: you open a tab, run an UPDATE, go for lunch, and the lock sits there stopping everyone. Check which mode your client is in and look at pg_stat_activity when something gets stuck.

Mistake 8: installing four engines and using none. The enthusiasm of module 3 leads to installing Cassandra and Neo4j "to try them out". Install them when you are going to do something specific with them, and delete them if within a month you have not.

Tip 1: version everything around the database. The docker-compose.yml, the migrations, the README with the Mermaid erDiagram, the test data scripts and the backup script. Making "clone the repository and run two commands" leave a working environment is an achievable and very profitable goal.

Tip 2: learn psql before any GUI client. The graphical one is learned in twenty minutes once you know what you are doing; the other way round, the graphical one hides what is happening and slows the learning down.

Tip 3: always keep a diagnostic query file to hand. The ones for pg_stat_activity, pg_stat_statements, table and index sizes and index usage. The day of the incident is not the moment to write them.

Tip 4: and once again, the official documentation rules. Versions move on, options get renamed and defaults get revised from one version to the next. Faced with any doubt between what this lesson says and what your version's manual says, the manual is right.

Exercises

These exercises are about real practice: they are solved with the machine switched on, not from memory. There is no single correct answer; the solutions are reasoned model answers and your version may be different and better.

Exercise 1: set up the polyglot environment and break it

Use Docker Compose to bring up the 08-03 environment (PostgreSQL + MongoDB + Redis), load the VallBici or BiblioRed schema into PostgreSQL and then:

  1. Check that the three engines answer from their console client.
  2. Insert at least 300,000 rows into the trips or loans table with generate_series, and run ANALYZE.
  3. Break it on purpose: stop the Redis container with the application running and document what stops working and what keeps working. Then run docker compose down (without -v) and bring it up again: check that the data is still there.
  4. Run docker compose down -v and bring it up again: check that it is not there, and explain why.
  5. Leave the result in a repository with docker-compose.yml, the initialization .sql files and a README that lets somebody else reproduce it in under ten minutes.

Exercise 2: version your schema with migrations

Take the BiblioRed schema as you left it in module 5 and turn it into a sequence of migrations:

  1. Choose a tool and justify the choice in two lines.
  2. Write V1 with the base schema and, at minimum, three later migrations representing real changes: a new index justified by an EXPLAIN, an integrity constraint that today lives in the application, and a new column on a table that already has data.
  3. The new-column migration must be runnable against the 300,000 rows from exercise 1 without leaving the table locked for an unacceptable amount of time. Explain how you achieve that.
  4. Document in each file why the change is made, not just what is done.
  5. Check the essential thing: drop the database, run the migrations from scratch and verify that you obtain the same schema.

Exercise 3: choose your toolkit and justify it

Without installing anything new yet, write out your personal toolkit for the next six months. For each piece:

  1. Which tool you choose and for which specific task.
  2. Against which alternative you chose it and why — a real reason, not "it is the most popular".
  3. Which tool from the lesson you explicitly rule out and under what condition you would reconsider it.
  4. Add an acid test: describe the specific task with which you will verify in a month that the choice was right.

Solutions

Model answers, not the only correct ones. What is assessed is the reasoning and that the machine really does what you say.

Solution 1 (model answer)

Points 1 and 2. Checking the three engines and loading volume:

docker compose up -d
docker compose exec postgres psql -U vallbici -d vallbici -c "SELECT version();"
docker compose exec mongo mongosh --quiet --eval "db.adminCommand({ping:1})"
docker compose exec redis redis-cli ping

The load is done with the generate_series from section 8, always followed by ANALYZE. Verification: EXPLAIN (ANALYZE) of a query filtered by member should go from a sequential scan to an index scan after creating the index and running ANALYZE; if nothing changes, the ANALYZE is almost certainly missing.

Point 3 — breaking it. With docker compose stop redis:

What happens Why
The real-time availability screen stops updating Redis is the source of the fast per-station bike counter
Active sessions are lost and people have to authenticate again Sessions live in Redis
Unlocking and charging for a trip keep working They are resolved against PostgreSQL, which is the source of truth
The history and the invoices remain queryable They are in PostgreSQL

That table is the empirical check of the thesis of 08-03: "that Redis can go down without preventing a single charge is the proof that the split is well made". If stopping Redis made charging impossible, the split would be wrong and would need revisiting.

Points 4 and 5. docker compose down stops the containers but keeps the named volumes (pgdata, mongodata, redisdata), so when you bring it back up the data is there. down -v removes those volumes and, with them, the data; furthermore, when the PostgreSQL volume is recreated empty, the docker-entrypoint-initdb.d scripts run again, since they only run when the data directory is uninitialized. Understanding that asymmetry is precisely the point of the exercise: the container is disposable, the volume is not.

Solution 2 (model answer)

1. Choice: numbered SQL files with Flyway or an equivalent lightweight tool. Reason: BiblioRed uses no framework with migrations of its own, the schema is thought out in SQL and I want what gets applied to be exactly what I read, with no intermediate abstraction layer.

2 and 3. The delicate migration — adding a NOT NULL column to a table with 300,000 rows — is done in three steps, not one:

-- V5__loans_origin_channel.sql
--
-- Adds the channel through which the loan was made (desk, web, app).
-- It is done in three steps so as not to lock the table: adding the column
-- as nullable is a metadata operation and is instantaneous; backfilling and
-- only then imposing NOT NULL avoids rewriting the whole table under a lock.

-- Step 1: nullable column (instantaneous, metadata only)
ALTER TABLE loans ADD COLUMN origin_channel text;

-- Step 2: backfill in batches (simplified here; in production, in blocks
-- of N rows with intermediate commits so as not to create a long transaction)
UPDATE loans SET origin_channel = 'desk' WHERE origin_channel IS NULL;

-- Step 3: with no null rows left, impose the constraint
ALTER TABLE loans
    ALTER COLUMN origin_channel SET NOT NULL,
    ADD CONSTRAINT loans_origin_channel_valid
        CHECK (origin_channel IN ('desk', 'web', 'app'));

The index justified by EXPLAIN is the V3 from section 7, with CONCURRENTLY and with a reasoned column order. The constraint that moves up from the application to the engine is the V4 of the non-overlapping room reservations.

4 and 5. The header comment of each file explains the why — the ticket, the execution plan that motivated it, the course lesson that grounds it — because a year from now the "what" is read in the SQL and the "why" is nowhere. The final verification is what gives the whole exercise its point: dropdb bibliored && createdb bibliored, apply the migrations from scratch and compare the resulting schema (pg_dump --schema-only) with the original database's. If they do not match, there is some change that was made by hand and is in no migration: exactly the problem this section exists to solve.

Solution 3 (model answer)

Piece Choice Against Why One-month acid test
Local engine PostgreSQL 16 in a container Native installation I can have two versions at once and destroy it without residue Bring up the environment on a new machine in under ten minutes
Main client psql with my own .psqlrc A GUI client It is on the server, in the container and in the deployment script Resolve an incident without opening a graphical interface
GUI client DBeaver pgAdmin I also open SQLite and MongoDB, and DBeaver covers them with a single client Explore somebody else's 40-table schema and understand it in one afternoon
Migrations Flyway with plain SQL Liquibase I need neither automatic rollback nor several engines; I want readable SQL Rebuild the database from scratch and have the schema match
Diagrams Mermaid in the README dbdiagram.io The diagram must be versioned with the schema, not live on another website The diagram still being correct after three schema changes
Diagnostics EXPLAIN + pg_stat_statements A commercial monitoring tool They come with the engine and answer 90% of the questions Identify the query consuming the most total time and improve it

Explicit exclusions. Elasticsearch and Neo4j: I do not install them until I have a typo-tolerant search or graph traversal requirement that PostgreSQL with pg_trgm or recursive queries does not cover. pgBackRest: not until I administer a database somebody other than me depends on; in the meantime, automated pg_dump restored quarterly. A paid commercial client: not until DBeaver genuinely gets in my way.

Conclusion: closing the course

Let us start small and end big.

The small part: of the whole inventory in this lesson, six pieces are enough — local PostgreSQL, psql, Docker Compose, one GUI client, one migration tool and EXPLAIN with pg_stat_statements. With those you can design, measure, version and recover, which is everything you need in order to work well. Add tools when a real problem calls for them, and not before. And remember the two warnings that govern the whole of module 9: versions, licenses, prices and free tiers change without notice, and the official documentation overrides any course, this one included.

And now the big part, because this lesson closes the entire course.

It has been nine modules and thirty-six lessons. It is worth looking back at the full route, because from the inside you do not always see it.

You started in 01-01 with a question that seemed naive: what is a database and why is a spreadsheet not enough. The answer took up the whole of module 1 — types of databases, fifty years of history from hierarchical systems to today, and the inside architecture of a database engine. In module 2 you learned the relational model and SQL for real: not just SELECT, but joins across several tables, aggregation, correlated subqueries and referential integrity, with the idea that holds it all together: if the rule is about integrity, it lives in the database. Module 3 pulled you out of there on purpose to teach you NoSQL, its four families and its modeling, and above all so you would see the relational model from the outside and understand that it is a choice and not a law of nature.

Modules 4 and 5 were the ones about the silent craft: designing. Entity-relationship diagrams, transformation into schemas, data types and constraints, functional dependencies, normal forms up to Boyce-Codd, and — the part many courses do not tell you — denormalizing on purpose and knowing how to justify why. Module 6 put the system under pressure: transactions and ACID, two terminals open watching a lost update with your own eyes, execution plans read line by line, permissions and backups. Module 7 was four lessons of exercises without a safety net. And module 8 made you build from scratch: BiblioRed in relational, VallBici in document form, and finally four engines sharing a municipal service with a table declaring, field by field, who rules over what.

What you can do now. You can sit down in front of a domain you do not know, ask the right questions, draw the model, transform it into tables with their types and their constraints, normalize it, decide where breaking the normalization is worthwhile and defend it. You can write multi-table queries with aggregation without looking up the syntax. You can read an execution plan, decide on an index and justify its column order. You can reason about what isolation level an operation needs and which anomaly you are accepting. You can decide whether a problem calls for a document, a key-value pair, a graph or a table, and — more importantly — when it does not. And you can set up the environment, version it and recover it.

That is not a small thing. It is, quite precisely, what is expected of someone who works with data with judgment.

What is missing, said honestly: experience. No lesson gives what having had a database in production with people depending on it gives. Nobody fully understands why backups get tested until they have needed one; nobody internalizes isolation levels until they have seen two duplicate charges on a Tuesday morning. That comes with time, and all this course has done — which is already a lot — is prepare you to recognize it when it happens, instead of suffering it without understanding it.

So the final invitation is the same one that closed 08-03, and now you have all the tools to accept it: choose one of the cases — the relational BiblioRed of modules 4 and 5, the document-based VallBici of 08-02, or the polyglot one of 08-03 — set it up on your machine with the docker-compose.yml from this lesson, load it with half a million rows, and break it. Kill the Redis container and see what keeps working. Open two terminals and provoke a deadlock. Drop the whole database and restore it from your backup. Create an index that is good for nothing and find out with EXPLAIN why it is good for nothing. Each of those things will teach you more than rereading the corresponding lesson, because the knowledge that sticks is the one you pay for with a mistake of your own.

Thank you for making it this far. Thirty-six lessons are a lot, and finishing a complete course — not abandoning it in module 3, which is what happens to most people with most courses — says something real about how you work. Databases are one of the few areas of computing where what you learn ages slowly: Codd's paper is more than fifty years old and it is still the basis of what you have studied, and the decisions you now know how to make will keep serving you when the languages, the frameworks and the fashions change.

There is no next lesson now. There is an empty database waiting for you to decide what goes inside. Good luck with it.

© Copyright 2026. All rights reserved