Learning SQL by reading is like learning to swim by watching videos: necessary, but not enough. For this course to be genuinely useful you need an environment where you can write queries, get them wrong, read the error message and try again. This lesson walks you from "I have nothing installed" to "I have PostgreSQL 16 running, I know how to connect and I've created the greenstore database". We'll cover installation on Linux, macOS and Windows, the Docker alternative (the one we recommend), an emergency exit with SQLite if you can't install anything on your machine, the essential commands of the psql client and the most widely used graphical clients.
Contents
- What exactly you need
- Option A (recommended): PostgreSQL with Docker
- Option B: native installation on Linux, macOS and Windows
- Option C (minimal): SQLite
- Connecting with
psqland its basic commands - Graphical clients: pgAdmin, DBeaver and VS Code
- Creating the
greenstoredatabase and a user of your own - Checking that everything works
- Common Mistakes and Tips
- Exercises
- Conclusion
- What exactly you need
To follow the course you need three pieces:
| Piece | What it is | Course recommendation |
|---|---|---|
| The server | The process that stores the data and runs the queries | PostgreSQL 16 |
| A command-line client | To write queries and see results | psql (ships with PostgreSQL) |
| A graphical client (optional) | To browse tables comfortably | DBeaver or pgAdmin |
And you need to decide how you're going to install the server. These are the three routes, ordered by what we recommend:
graph TD
A[Can you install software<br/>on your machine?] -->|Yes| B[Do you have Docker<br/>or can you install it?]
A -->|No| E[Option C: SQLite<br/>or PostgreSQL in the cloud]
B -->|Yes| C[Option A: Docker<br/>RECOMMENDED]
B -->|No| D[Option B: native<br/>system installation]
| Option | Advantages | Drawbacks |
|---|---|---|
| A. Docker | Exact, reproducible version; deletes without a trace; identical on all three operating systems | Requires Docker installed |
| B. Native | Starts with the system; no intermediate layer | Depends on the system; uninstalling cleanly is more awkward |
| C. SQLite | Zero server installation; a single file | A more limited dialect: you won't see users, strict types or a good part of modules 8, 9 and 10 |
- Option A (recommended): PostgreSQL with Docker
Docker lets you spin up PostgreSQL 16 in an isolated container, with exactly the version the course uses, without touching the rest of your system.
2.1. Starting the container
docker run --name pg-course \
-e POSTGRES_PASSWORD=course2026 \
-e POSTGRES_USER=postgres \
-e POSTGRES_DB=postgres \
-p 5432:5432 \
-v pgdata-course:/var/lib/postgresql/data \
-d postgres:16It's worth understanding each option, because you'll see them again:
| Option | What it does |
|---|---|
--name pg-course |
Gives the container a name so you can refer to it later |
-e POSTGRES_PASSWORD=... |
Superuser password. Mandatory: without it the container won't start |
-e POSTGRES_USER=postgres |
Superuser name (the default is already postgres) |
-p 5432:5432 |
Publishes the container's port on your machine, so external clients can connect |
-v pgdata-course:/var/lib/postgresql/data |
Persistent volume: without this you'd lose all your data when you delete the container |
-d |
Runs in the background (detached) |
postgres:16 |
Official image, version 16 |
2.2. Checking that it's running
In the logs you should see a line similar to database system is ready to accept connections.
2.3. Container life cycle
docker stop pg-course # Shut down (the data is still there)
docker start pg-course # Turn it back on
docker rm -f pg-course # Delete the container (the pgdata-course volume survives)
docker volume rm pgdata-course # Delete the data TOO. Be careful with this one.Note: if port 5432 is already taken by another PostgreSQL installation, change the publication to
-p 5433:5432and use-p 5433when connecting.
- Option B: native installation on Linux, macOS and Windows
| System | Installation commands | How it starts | Notes |
|---|---|---|---|
| Debian / Ubuntu | sudo apt updatesudo apt install postgresql-16 postgresql-client-16 |
sudo systemctl start postgresqlsudo systemctl enable postgresql |
The postgres system user is created; you get in with sudo -u postgres psql |
| Fedora / RHEL | sudo dnf install postgresql16-server postgresql16sudo /usr/pgsql-16/bin/postgresql-16-setup initdb |
sudo systemctl enable --now postgresql-16 |
The cluster has to be initialised explicitly |
| Arch Linux | sudo pacman -S postgresqlsudo -u postgres initdb -D /var/lib/postgres/data |
sudo systemctl enable --now postgresql |
|
| macOS (Homebrew) | brew install postgresql@16 |
brew services start postgresql@16 |
Creates a database named after your user |
| macOS (Postgres.app) | Download from postgresapp.com and drag to Applications | "Start" button | The simplest way on a Mac; you have to add the binary to your PATH |
| Windows (EDB installer) | Download the installer from enterprisedb.com and follow the wizard | Windows service, starts on its own | Includes pgAdmin 4 and the Stack Builder |
| Windows (winget) | winget install PostgreSQL.PostgreSQL.16 |
Windows service | The quick route with no graphical wizard |
Details that tend to cause trouble:
- Linux/Debian: after installing, local access goes through peer authentication. The right way in the first time is
sudo -u postgres psql, not plainpsql. - macOS with Homebrew: if
psqlisn't found, add the path to yourPATH:echo 'export PATH="/opt/homebrew/opt/postgresql@16/bin:$PATH"' >> ~/.zshrc source ~/.zshrc - Windows: the installer doesn't always add
psqlto thePATH. If PowerShell tells you the command isn't recognised, addC:\Program Files\PostgreSQL\16\binto thePathenvironment variable by hand, or use the "SQL Shell (psql)" shortcut in the Start menu.
- Option C (minimal): SQLite
If you work on a machine where you can't install services, SQLite will let you follow a good part of the course. A SQLite database is a single file and there's no server to start.
# Linux (Debian/Ubuntu)
sudo apt install sqlite3
# macOS: already installed
sqlite3 --version
# Windows (winget)
winget install SQLite.SQLiteCreating a database and opening it are the same thing:
Its client commands start with a dot instead of a backslash:
| Task | psql (PostgreSQL) | sqlite3 |
|---|---|---|
| List tables | \dt |
.tables |
| Describe a table | \d products |
.schema products |
| Quit | \q |
.quit |
| Run a file | \i file.sql |
.read file.sql |
| Help | \? |
.help |
Honest limitations of SQLite for this course: it has no users or permissions (module 11), its types are dynamic and very permissive (modules 1 and 5), it doesn't support
RIGHT/FULL OUTER JOINin versions before 3.39, itsEXPLAINis different (module 8) and it has no equivalent stored procedures or triggers (module 10). Use it as a last resort; if at any point you can switch to PostgreSQL, do it.
- Connecting with
psql and its basic commands
psql and its basic commandspsql is the official command-line client. It's the tool we'll use in the explanations because it's fast, it's always available and it shows errors exactly as the server produces them.
5.1. Connecting
# Native installation on Linux, the first time
sudo -u postgres psql
# Explicit connection specifying server, port, user and database
psql -h localhost -p 5432 -U postgres -d postgres
# If you use Docker, you can go straight into the container
docker exec -it pg-course psql -U postgres -d postgres
# A full connection string (URI) works too
psql "postgresql://postgres:course2026@localhost:5432/postgres"The options repeat across almost every PostgreSQL tool:
| Option | Meaning | Usual value |
|---|---|---|
-h |
Server host | localhost |
-p |
Port | 5432 |
-U |
User | postgres |
-d |
Database to connect to | greenstore |
-f |
Run a .sql file and quit |
-f greenstore.sql |
-c |
Run a single statement and quit | -c "SELECT version();" |
Once you're inside, the prompt tells you the state you're in:
postgres=# -- connected to the "postgres" DB as superuser greenstore=> -- connected to "greenstore" as a normal user greenstore-# -- UNFINISHED statement: the semicolon is missing greenstore'# -- you have an open single quote
That third prompt (-#) is the one that puzzles people most at the start: it means psql is waiting for you to close the statement with ;.
5.2. The metacommands you can't do without
Commands that start with \ are not SQL: the client itself interprets them, which is why they take no semicolon.
| Command | What it does |
|---|---|
\l |
Lists every database on the server |
\c greenstore |
Connects to the greenstore database |
\dt |
Lists the tables in the current schema |
\d products |
Describes the products table: columns, types, keys and indexes |
\dn |
Lists the schemas |
\du |
Lists the users and roles |
\df |
Lists the functions |
\i file.sql |
Runs a SQL file |
\x |
Toggles expanded view (one column per line): a lifesaver with wide tables |
\timing |
Turns the execution time of each query on/off |
\e |
Opens the last query in your text editor |
\? |
Help for the metacommands |
\h SELECT |
Syntax help for a specific SQL statement |
\q |
Quit |
Example of a typical session:
postgres=# \l postgres=# \c greenstore You are now connected to database "greenstore" as user "postgres". greenstore=# \dt greenstore=# \d products greenstore=# \timing Timing is on. greenstore=# \q
- Graphical clients: pgAdmin, DBeaver and VS Code
A graphical client doesn't replace psql, but it speeds up exploring an unfamiliar schema enormously.
| Client | Engines it supports | Strong point | Weak point | Ideal for |
|---|---|---|---|---|
| pgAdmin 4 | PostgreSQL only | Full server administration; ships with the Windows installer | Heavy web interface; not nimble for writing queries | Administering PostgreSQL in depth |
| DBeaver Community | PostgreSQL, MySQL, SQLite, Oracle, SQL Server and dozens more | One client for everything; good editor with autocompletion and ER diagrams | Uses a fair amount of memory (it's Java) | The course's general recommendation |
| VS Code SQL extension | PostgreSQL, MySQL, SQLite (depending on the extension) | Queries inside the editor where you already code; .sql files versionable in Git |
Fewer administration features | Anyone who already lives in VS Code |
| TablePlus / DataGrip | Multiple | Very polished and fast | Paid | Daily professional use |
Connection details any of them will ask you for (with the Docker setup from section 2):
| Field | Value |
|---|---|
| Host | localhost |
| Port | 5432 |
| Database | greenstore |
| User | postgres (or sql_course, see section 7) |
| Password | course2026 |
- Creating the
greenstore database and a user of your own
greenstore database and a user of your ownLet's prepare the course workspace. Connect first as superuser:
And run:
-- 1. Create a user of your own for the course, instead of working as superuser
CREATE USER sql_course WITH PASSWORD 'course2026';
-- 2. Create the course database, with that user as its owner
CREATE DATABASE greenstore
OWNER sql_course
ENCODING 'UTF8'
TEMPLATE template0
LC_COLLATE 'en_US.UTF-8'
LC_CTYPE 'en_US.UTF-8';What each part does:
CREATE USERcreates a role that can log in. Working as superuser every day is a bad habit: an accidentalDROPwould meet no barrier at all.OWNER sql_coursemakes that user the owner of the database so it can create tables inside without extra permissions.ENCODING 'UTF8'guarantees that the accented characters in our data (Castellón, Lucía Martínez Soler) are stored correctly.LC_COLLATEdefines the language's alphabetical order: with the English setting,ORDER BYwill place "apple" before "Banana", as you'd expect, instead of grouping all the capitals first.
If
LC_COLLATEgives you an error: in the PostgreSQL Docker image that locale may not have been generated. In that case, create the database without those two clauses:CREATE DATABASE greenstore OWNER sql_course ENCODING 'UTF8';It affects nothing in the course beyond small nuances of alphabetical ordering.
Now connect to the new database with your user:
And grant permissions on the public schema (necessary since PostgreSQL 15, which tightened the default permissions). This runs as superuser:
- Checking that everything works
The canonical check is to ask the server for its version:
Expected result (the exact text will vary depending on your system):
| version |
|---|
| PostgreSQL 16.4 on x86_64-pc-linux-gnu, compiled by gcc 12.2.0, 64-bit |
A couple more checks, useful for confirming you're where you think you are:
| current_database | current_user | now |
|---|---|---|
| greenstore | sql_course | 2026-02-25 10:14:07.412+01 |
And a quick test that you can create objects in the database:
CREATE TABLE test_table (id INTEGER, content TEXT);
INSERT INTO test_table VALUES (1, 'Hello GreenStore');
SELECT * FROM test_table;
DROP TABLE test_table;| id | content |
|---|---|
| 1 | Hello GreenStore |
If the four statements run without error, your environment is ready. With DROP TABLE test_table; you leave the database clean for the real script, which you'll load in lesson 01-06.
Checklist
| Check | Command | Expected result |
|---|---|---|
| The server responds | SELECT version(); |
A line with "PostgreSQL 16" |
| The database exists | \l |
greenstore appears in the list |
| You're connected to it | SELECT current_database(); |
greenstore |
| You can create tables | CREATE TABLE test_table (...) |
CREATE TABLE |
| The graphical client connects | Test the connection in DBeaver/pgAdmin | Connection successful |
Common Mistakes and Tips
psql: error: connection to server ... failed: Connection refused. The server isn't running or the port is a different one. With Docker, checkdocker ps; with a native installation,sudo systemctl status postgresql.FATAL: password authentication failed for user "postgres". Wrong password, or you're using the one from another environment. In Docker, the password is the one you passed inPOSTGRES_PASSWORDthe first time the volume was created: changing it in a laterdocker runhas no effect if the volume already exists.FATAL: role "your_user" does not exist. On Linux,psqltries to connect with your system user. Use-U postgresexplicitly or get in withsudo -u postgres psql.FATAL: database "greenstore" does not exist. You haven't created it yet, or you created it on a different server from the one you're querying.- Forgetting the semicolon. If the prompt goes from
=#to-#,psqlis waiting for the;. Type it and press Enter. - Losing your data when recreating the container. Without
-vthere's no volume, and deleting the container deletes the whole database. It's the most painful Docker mistake. - Tip: save your queries in
.sqlfiles. Create asql-course/folder with one file per module. You'll be able to run them with\iand you'll keep a record of your progress. - Tip: turn
\timingon from day one. You'll get used to looking at how long each query takes, something that will feel natural by the time you reach module 8. - Tip: use
\xwith wide tables. When a row doesn't fit on screen and the result is unreadable,\xturns it into a vertical list of fields.
Exercises
Exercise 1
Start PostgreSQL 16 with Docker on port 5433 instead of 5432 (useful if you already have another PostgreSQL on your machine), with the container name pg-greenstore and a volume called gs-data. Then connect with psql and check the server version.
Exercise 2
From psql, and without leaving the client, answer these three questions using metacommands (not SQL):
- Which databases exist on your server?
- Which users/roles are defined?
- What is the full syntax of the
CREATE DATABASEstatement?
Exercise 3
Create a database called sql_tests owned by the sql_course user, connect to it, create a notes(id, content) table, insert two rows, query them and finally drop the whole database.
Solutions
Solution 1
docker run --name pg-greenstore \
-e POSTGRES_PASSWORD=course2026 \
-p 5433:5432 \
-v gs-data:/var/lib/postgresql/data \
-d postgres:16The key detail is -p 5433:5432: the number on the left is the port on your machine and the one on the right the container's (which is always 5432, because inside the container PostgreSQL listens there). To connect:
Solution 2
\l (for list) shows databases with their owner and encoding; \du (display users) shows roles and their attributes; \h gives SQL syntax help, whereas \? gives the metacommand help. Remember: none of them takes a semicolon, because they aren't SQL.
Solution 3
CREATE TABLE notes (id INTEGER, content TEXT);
INSERT INTO notes VALUES (1, 'First note'), (2, 'Second note');
SELECT * FROM notes;| id | content |
|---|---|
| 1 | First note |
| 2 | Second note |
To drop the database you can't be connected to it, so first go back to postgres:
If you try the DROP while inside, PostgreSQL answers ERROR: cannot drop the currently open database. It's a very frequent error and the fix is always the same: switch database first.
Conclusion
You now have a real working environment:
- You know the three routes to having PostgreSQL 16: Docker (reproducible and recommended), native installation per operating system and SQLite as a last resort.
- You know the
psqlclient, its connection options (-h,-p,-U,-d,-f,-c) and its key metacommands (\l,\c,\dt,\d,\x,\timing,\?,\q). - You can choose a graphical client on solid grounds: DBeaver as the general option, pgAdmin for administration, VS Code if you already work there.
- You've created the
greenstoredatabase and thesql_courseuser, and you've verified the installation withSELECT version();.
In the next lesson, Basic SQL Syntax, we'll stop installing and start writing: you'll see how a SQL statement is built on the inside (keywords, identifiers, literals, operators), what role the semicolon plays, how PostgreSQL treats upper and lower case, how to comment your code, what formatting conventions professionals use and —very importantly— how to read an error message so you know exactly where you went wrong.
SQL Course
Module 1: Introduction to SQL
- What is SQL?
- Setting up your SQL environment
- Basic SQL syntax
- Understanding databases and tables
- The relational model: primary and foreign keys
- The course database: GreenStore
Module 2: Basic SQL queries
- The SELECT statement
- Aliases, expressions and calculated columns
- Filtering data with WHERE
- DISTINCT and removing duplicates
- Sorting data with ORDER BY
- Limiting results with LIMIT
Module 3: Working with multiple tables
- JOIN operations
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN and CROSS JOIN
- Set operations: UNION, INTERSECT and EXCEPT
Module 4: Advanced data filtering
- Using LIKE for pattern matching
- The IN and BETWEEN operators
- NULL values and IS NULL
- Aggregate functions: COUNT, SUM, AVG, MIN and MAX
- Aggregating data with GROUP BY
- The HAVING clause
Module 5: Data manipulation
- Creating tables and constraints with CREATE TABLE
- The INSERT statement
- The UPDATE statement
- The DELETE statement
- The UPSERT (MERGE) statement
- Changing the schema: ALTER TABLE and safe migrations
Module 6: Advanced SQL functions
- String functions
- Numeric functions
- Date and time functions
- Type conversion and handling NULL: CAST and COALESCE
- Conditional expressions
Module 7: Subqueries and nested queries
- Introduction to subqueries
- Correlated subqueries
- EXISTS and NOT EXISTS
- Using subqueries in SELECT, FROM and WHERE
- Subquery or JOIN: which one to choose
Module 8: Indexes and performance tuning
- Understanding indexes
- Creating and managing indexes
- Index types and when not to index
- Query optimization techniques
- Analyzing query performance
Module 9: Transactions and concurrency
- Introduction to transactions
- ACID properties
- Transaction control statements
- Isolation levels and concurrency anomalies
- Handling concurrency: locks and deadlocks
Module 10: Advanced topics
- Views
- Common table expressions (CTEs)
- Window functions
- Stored procedures
- Triggers
- JSON and semi-structured data
Module 11: SQL in practice
- Real-world use cases
- Best practices
- Security: SQL injection, permissions and roles
- SQL for data analysis
- SQL in web development
