← Back to course

Practical Project: Building a Small Shop Database

Practical Project: Building a Small Shop Database

Welcome back.

In the previous lesson, you learned about indexes and basic performance.

You learned:

Very good.

Now it is time to put many pieces together.

Today we will build a small practical PostgreSQL project.

Not a fake one-table example.

Not a mysterious database with names like table1 and data2.

No.

Today we will create a small shop database.

It will have:

customers
categories
products
orders
order_items

This is close to real database design.

Small enough to understand.

Real enough to be useful.

Like a training sword.

But for SQL.

Less dangerous.

Usually.

What You Will Learn

In this lesson, you will learn how to:

By the end of this lesson, you will have a working mini database project.

Not just random SQL commands.

A real structure.

A little database machine.

With customers.

Products.

Orders.

And enough relationships to make PostgreSQL feel important.

Project Idea

We are building a small shop database.

The shop sells products.

Each product belongs to a category.

Customers can create orders.

Each order can contain many products.

This means we need several tables.

The structure will be:

categories
products
customers
orders
order_items

Relationships:

categories -> products
customers  -> orders
orders     -> order_items
products   -> order_items

In simple words:

One category has many products.
One customer has many orders.
One order has many order items.
One product can appear in many order items.

This is where relational databases become useful.

Instead of storing everything in one giant messy table, we separate data into clean tables.

Because one giant table is easy at the beginning.

And terrible later.

Like putting all your tools, socks, documents, passwords, and pasta in one drawer.

Possible.

But not wise.

Prepare the Database

Open PostgreSQL:

sudo -iu postgres psql

Connect to your database:

\c learning_postgresql

If the database does not exist, create it:

CREATE DATABASE learning_postgresql;

Then connect:

\c learning_postgresql

Now remove old tables if they exist.

The order is important because some tables depend on others.

DROP TABLE IF EXISTS order_items;
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS customers;
DROP TABLE IF EXISTS categories;

We drop order_items first because it depends on orders and products.

PostgreSQL protects relationships.

Like a strict librarian.

But with more semicolons.

Create the Categories Table

A category groups products.

Examples:

Electronics
Furniture
Office
Kitchen

Create the table:

CREATE TABLE categories (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) UNIQUE NOT NULL
);

Important details:

We do not want two categories called Electronics.

One Electronics is enough.

Even for PostgreSQL.

Create the Products Table

Each product belongs to one category.

Create the table:

CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  name VARCHAR(150) NOT NULL,
  price NUMERIC(10, 2) NOT NULL CHECK (price >= 0),
  stock_quantity INTEGER NOT NULL CHECK (stock_quantity >= 0),
  category_id INTEGER NOT NULL REFERENCES categories(id)
);

This table has:

The important relationship is:

category_id INTEGER NOT NULL REFERENCES categories(id)

This means:

Each product must belong to an existing category.

PostgreSQL will not allow a product to point to a category that does not exist.

Good.

Because imaginary categories are not great for reports.

Or sanity.

Create the Customers Table

Customers place orders.

Create the table:

CREATE TABLE customers (
  id SERIAL PRIMARY KEY,
  email VARCHAR(255) UNIQUE NOT NULL,
  name VARCHAR(100) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Important details:

In real applications, email is often used for login or communication.

So duplicate emails are usually a bad idea.

Two Annas can exist.

Two identical emails should not.

PostgreSQL agrees.

Create the Orders Table

An order belongs to one customer.

Create the table:

CREATE TABLE orders (
  id SERIAL PRIMARY KEY,
  customer_id INTEGER NOT NULL REFERENCES customers(id),
  status VARCHAR(50) NOT NULL DEFAULT 'new',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

This table stores the order itself.

But notice something.

There is no total column.

Why?

Because the total can be calculated from order_items.

In many systems, totals are stored too for performance and history.

But for learning, calculating totals is better.

It teaches you how the data works.

And PostgreSQL likes teaching.

In a strict way.

Create the Order Items Table

An order can contain many products.

For example:

Order 1:
- Laptop x 1
- Mouse x 2

This is why we need order_items.

Create the table:

CREATE TABLE order_items (
  id SERIAL PRIMARY KEY,
  order_id INTEGER NOT NULL REFERENCES orders(id),
  product_id INTEGER NOT NULL REFERENCES products(id),
  quantity INTEGER NOT NULL CHECK (quantity > 0),
  unit_price NUMERIC(10, 2) NOT NULL CHECK (unit_price >= 0)
);

Important details:

Why store unit_price here?

Because product prices can change later.

Imagine a product costs 25 today.

Tomorrow it costs 30.

Old orders should still remember the old price.

Otherwise your reports become a historical crime scene.

So order_items.unit_price stores the price used in that order.

Very practical.

Very real.

Insert Categories

Add categories:

INSERT INTO categories (name)
VALUES
  ('Electronics'),
  ('Furniture'),
  ('Office'),
  ('Kitchen');

Check them:

SELECT * FROM categories;

You should see four categories.

Small beginning.

Database civilization starts here.

Insert Products

Add products:

INSERT INTO products (name, price, stock_quantity, category_id)
VALUES
  ('Laptop', 900.00, 5, 1),
  ('Mouse', 25.00, 30, 1),
  ('Keyboard', 70.00, 15, 1),
  ('Desk Chair', 150.00, 8, 2),
  ('Bookshelf', 120.00, 4, 2),
  ('Notebook', 5.00, 100, 3),
  ('Pen', 2.00, 200, 3),
  ('Whiteboard', 80.00, 3, 3),
  ('Coffee Mug', 12.00, 40, 4),
  ('Kitchen Knife', 35.00, 20, 4);

Check products:

SELECT * FROM products;

Now we have products connected to categories.

But we still see category_id.

Humans prefer names.

So soon we will use JOIN.

Because mysterious IDs are not a user interface.

They are a cry for help.

Insert Customers

Add customers:

INSERT INTO customers (email, name)
VALUES
  ('anna@example.com', 'Anna'),
  ('marco@example.com', 'Marco'),
  ('sofia@example.com', 'Sofia'),
  ('luca@example.com', 'Luca');

Check customers:

SELECT * FROM customers;

Now we have people who can buy things.

Very dangerous.

For wallets.

Insert Orders

Create some orders:

INSERT INTO orders (customer_id, status)
VALUES
  (1, 'paid'),
  (1, 'paid'),
  (2, 'paid'),
  (3, 'new'),
  (4, 'cancelled');

This means:

Anna has two orders.
Marco has one order.
Sofia has one new order.
Luca has one cancelled order.

Check orders:

SELECT * FROM orders;

The orders exist.

But they are empty.

An order without items is like a pizza box without pizza.

Technically a box.

Emotionally disappointing.

Insert Order Items

Now add items to orders:

INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES
  (1, 1, 1, 900.00),
  (1, 2, 2, 25.00),
  (1, 3, 1, 70.00),

  (2, 6, 10, 5.00),
  (2, 7, 20, 2.00),

  (3, 4, 1, 150.00),
  (3, 5, 1, 120.00),

  (4, 9, 2, 12.00),
  (4, 10, 1, 35.00),

  (5, 2, 1, 25.00);

Check order items:

SELECT * FROM order_items;

Now we have a real mini shop structure.

Customers.

Orders.

Products.

Items.

Prices.

Quantities.

Relationships.

Beautiful little database machinery.

Show Products with Category Names

Let us start with a simple JOIN.

SELECT
  p.id,
  p.name AS product_name,
  p.price,
  p.stock_quantity,
  c.name AS category_name
FROM products AS p
JOIN categories AS c
ON p.category_id = c.id
ORDER BY p.id;

This gives readable product data.

Instead of:

Laptop | category_id = 1

We get:

Laptop | Electronics

This is what real applications need.

Users should not need to decode IDs.

Users already suffer enough.

Especially when passwords require one symbol, one number, one ancient rune, and emotional stability.

Show Orders with Customer Names

Now show orders with customer names:

SELECT
  o.id AS order_id,
  c.name AS customer_name,
  c.email,
  o.status,
  o.created_at
FROM orders AS o
JOIN customers AS c
ON o.customer_id = c.id
ORDER BY o.id;

This tells us who placed each order.

Very useful.

A table with only customer_id is technically correct.

But readable data is better.

Readable data is what separates a good admin panel from a spreadsheet cave.

Show Full Order Details

Now let us show order items with product and customer information.

SELECT
  o.id AS order_id,
  c.name AS customer_name,
  p.name AS product_name,
  oi.quantity,
  oi.unit_price,
  oi.quantity * oi.unit_price AS item_total
FROM order_items AS oi
JOIN orders AS o
ON oi.order_id = o.id
JOIN customers AS c
ON o.customer_id = c.id
JOIN products AS p
ON oi.product_id = p.id
ORDER BY o.id, oi.id;

This is a serious query.

It connects:

order_items
orders
customers
products

Now we can see:

Which customer bought which product.
How many.
At what price.
With what total.

This is the power of relational databases.

Small tables.

Connected properly.

Useful answers.

No chaos drawer.

Calculate Total for Each Order

Now calculate the total value of each order.

SELECT
  o.id AS order_id,
  c.name AS customer_name,
  o.status,
  SUM(oi.quantity * oi.unit_price) AS order_total
FROM orders AS o
JOIN customers AS c
ON o.customer_id = c.id
JOIN order_items AS oi
ON o.id = oi.order_id
GROUP BY o.id, c.name, o.status
ORDER BY o.id;

This uses:

A very useful combination.

The database calculates totals from order items.

No calculator needed.

No nervous Excel file needed.

PostgreSQL handles it.

With dignity.

Calculate Only Paid Orders

Usually, cancelled orders should not count as revenue.

So we can filter by status.

SELECT
  o.id AS order_id,
  c.name AS customer_name,
  SUM(oi.quantity * oi.unit_price) AS order_total
FROM orders AS o
JOIN customers AS c
ON o.customer_id = c.id
JOIN order_items AS oi
ON o.id = oi.order_id
WHERE o.status = 'paid'
GROUP BY o.id, c.name
ORDER BY order_total DESC;

This shows only paid orders.

Very important.

Because counting cancelled orders as revenue is a great way to make beautiful reports and terrible business decisions.

Pretty numbers can lie.

SQL helps.

But only if you ask correctly.

Total Revenue

Now calculate total revenue from paid orders.

SELECT
  SUM(oi.quantity * oi.unit_price) AS total_revenue
FROM orders AS o
JOIN order_items AS oi
ON o.id = oi.order_id
WHERE o.status = 'paid';

This gives one number.

A very business-looking number.

The kind of number people put on dashboards.

Then stare at seriously.

Even if the dashboard has only three rows.

Orders Per Customer

Count how many orders each customer has.

SELECT
  c.name AS customer_name,
  COUNT(o.id) AS orders_count
FROM customers AS c
LEFT JOIN orders AS o
ON c.id = o.customer_id
GROUP BY c.name
ORDER BY orders_count DESC;

We use LEFT JOIN because we may want to show customers with zero orders too.

In our current data, every customer has at least one order.

But in real life, many customers register and buy nothing.

Very normal.

Very painful for marketing.

Paid Orders Per Customer

Now count only paid orders.

SELECT
  c.name AS customer_name,
  COUNT(o.id) AS paid_orders_count
FROM customers AS c
LEFT JOIN orders AS o
ON c.id = o.customer_id
AND o.status = 'paid'
GROUP BY c.name
ORDER BY paid_orders_count DESC;

Important detail:

The status condition is inside the ON clause:

AND o.status = 'paid'

Why?

Because we still want all customers.

If we put this condition in WHERE, customers without paid orders might disappear.

This is a classic LEFT JOIN detail.

Small detail.

Big difference.

Database trap number 247.

Collect them all.

Total Spending Per Customer

Now calculate how much each customer spent on paid orders.

SELECT
  c.name AS customer_name,
  COALESCE(SUM(oi.quantity * oi.unit_price), 0) AS total_spent
FROM customers AS c
LEFT JOIN orders AS o
ON c.id = o.customer_id
AND o.status = 'paid'
LEFT JOIN order_items AS oi
ON o.id = oi.order_id
GROUP BY c.name
ORDER BY total_spent DESC;

Here we use COALESCE.

Why?

Because if a customer has no paid orders, the sum is NULL.

We want 0.

This:

COALESCE(SUM(oi.quantity * oi.unit_price), 0)

means:

If the sum exists, show it.
If the sum is NULL, show 0.

Very practical.

Very dashboard-friendly.

Very “do not scare the accountant”.

Sales by Category

Now let us calculate sales by category.

SELECT
  cat.name AS category_name,
  SUM(oi.quantity * oi.unit_price) AS total_sales
FROM order_items AS oi
JOIN products AS p
ON oi.product_id = p.id
JOIN categories AS cat
ON p.category_id = cat.id
JOIN orders AS o
ON oi.order_id = o.id
WHERE o.status = 'paid'
GROUP BY cat.name
ORDER BY total_sales DESC;

This answers:

Which categories generate the most revenue?

Very useful for business.

Maybe Electronics wins.

Maybe Office wins.

Maybe Kitchen surprises everyone with mugs.

Never underestimate mugs.

Products Sold Count

Now count how many units of each product were sold.

SELECT
  p.name AS product_name,
  SUM(oi.quantity) AS units_sold
FROM products AS p
JOIN order_items AS oi
ON p.id = oi.product_id
JOIN orders AS o
ON oi.order_id = o.id
WHERE o.status = 'paid'
GROUP BY p.name
ORDER BY units_sold DESC;

This is useful for stock planning.

If pens sell a lot, order more pens.

If bookshelves do not sell, maybe stop building a wooden empire.

Data helps decisions.

Sometimes brutally.

Products Never Sold

Now find products that were never sold in paid orders.

SELECT
  p.name AS product_name
FROM products AS p
LEFT JOIN order_items AS oi
ON p.id = oi.product_id
LEFT JOIN orders AS o
ON oi.order_id = o.id
AND o.status = 'paid'
GROUP BY p.id, p.name
HAVING COUNT(o.id) = 0
ORDER BY p.name;

This query is a little more advanced.

It uses:

The idea is:

Show products where there are zero paid orders.

Very useful.

Very business.

Very “why did we buy 500 of these?”.

Low Stock Products

Show products with low stock.

SELECT
  name AS product_name,
  stock_quantity
FROM products
WHERE stock_quantity < 10
ORDER BY stock_quantity ASC;

This is a simple query.

But very practical.

In real systems, this could power an alert:

Stock is low. Order more.

Databases are not only for storing data.

They can help you notice problems.

Like a quiet assistant.

Who only speaks SQL.

Create Useful Indexes

Now let us add indexes that make sense for this project.

Primary keys already have indexes automatically.

But foreign keys are good candidates for indexes.

Create indexes:

CREATE INDEX idx_products_category_id
ON products(category_id);
CREATE INDEX idx_orders_customer_id
ON orders(customer_id);
CREATE INDEX idx_order_items_order_id
ON order_items(order_id);
CREATE INDEX idx_order_items_product_id
ON order_items(product_id);

These indexes can help with joins.

Now create indexes for common filtering:

CREATE INDEX idx_orders_status
ON orders(status);
CREATE INDEX idx_orders_created_at
ON orders(created_at);

These can help queries like:

WHERE status = 'paid'

and:

ORDER BY created_at DESC

Do not create indexes blindly.

Here they make sense because these columns are likely to appear in joins, filters, or sorting.

Indexes should follow real queries.

Not vibes.

PostgreSQL does not optimize vibes.

We already discussed this.

Still tragic.

Check a Query with EXPLAIN ANALYZE

Let us inspect a query.

EXPLAIN ANALYZE
SELECT
  o.id AS order_id,
  c.name AS customer_name,
  SUM(oi.quantity * oi.unit_price) AS order_total
FROM orders AS o
JOIN customers AS c
ON o.customer_id = c.id
JOIN order_items AS oi
ON o.id = oi.order_id
WHERE o.status = 'paid'
GROUP BY o.id, c.name
ORDER BY order_total DESC;

Look for words like:

Seq Scan
Index Scan
Hash Join
Nested Loop
Planning Time
Execution Time

Do not panic if the output looks complicated.

The goal is not to understand every detail yet.

The goal is to start reading the plan.

PostgreSQL is telling you how it works.

Like a mechanic opening the engine.

At first it looks scary.

Then slowly it starts making sense.

Then you become the scary one.

In a good way.

Update Stock After an Order

In real shops, when an order is paid, stock should decrease.

For example, if order 1 sold:

Laptop x 1
Mouse x 2
Keyboard x 1

We can update stock manually:

UPDATE products
SET stock_quantity = stock_quantity - 1
WHERE id = 1;
UPDATE products
SET stock_quantity = stock_quantity - 2
WHERE id = 2;
UPDATE products
SET stock_quantity = stock_quantity - 1
WHERE id = 3;

Then check:

SELECT
  name,
  stock_quantity
FROM products
WHERE id IN (1, 2, 3);

This works.

But manual updates are dangerous.

In real applications, this should be handled carefully.

Usually inside a transaction.

We will talk more about safe database changes later.

Because databases are powerful.

And power plus carelessness equals interesting disasters.

A Better Stock Update Query

We can update stock from order items.

For order 1:

UPDATE products AS p
SET stock_quantity = p.stock_quantity - oi.quantity
FROM order_items AS oi
WHERE p.id = oi.product_id
AND oi.order_id = 1;

This updates all products from order 1 in one query.

Very useful.

Very powerful.

Also dangerous if you use the wrong order_id.

PostgreSQL will obey.

It is not your mother.

It will not ask:

Are you sure, dear?

So always check your conditions.

Find Order Details for One Customer

Show all paid order items for Anna:

SELECT
  c.name AS customer_name,
  o.id AS order_id,
  p.name AS product_name,
  oi.quantity,
  oi.unit_price,
  oi.quantity * oi.unit_price AS item_total
FROM customers AS c
JOIN orders AS o
ON c.id = o.customer_id
JOIN order_items AS oi
ON o.id = oi.order_id
JOIN products AS p
ON oi.product_id = p.id
WHERE c.email = 'anna@example.com'
AND o.status = 'paid'
ORDER BY o.id;

This is the kind of query you might use for:

Customer order history.
Invoices.
Admin panels.
Customer support.

Very real.

Very useful.

Very “please tell me what this customer bought”.

Search Products by Category and Price

Show products from Electronics cheaper than 100:

SELECT
  p.name AS product_name,
  p.price,
  c.name AS category_name
FROM products AS p
JOIN categories AS c
ON p.category_id = c.id
WHERE c.name = 'Electronics'
AND p.price < 100
ORDER BY p.price ASC;

This query could power a filter in an online shop.

Category.

Price.

Sorting.

Very normal.

Very common.

Very good place to think about indexes if the table grows.

Add a Multi-Column Index

If you often filter products by category and sort by price, this index can help:

CREATE INDEX idx_products_category_price
ON products(category_id, price);

This index is useful for queries like:

WHERE category_id = 1
ORDER BY price ASC

Column order matters.

The index starts with category_id.

Then price.

It is not random.

It is based on how the query works.

Indexes are not stickers.

They are strategy.

Tiny database strategy.

Like chess, but with more invoices.

Common Mistakes in Small Database Projects

Storing Everything in One Table

Bad idea:

customer_name
customer_email
product_name
category_name
order_date
quantity
price

All in one table.

This looks easy.

At first.

Then customer email changes.

Then category name changes.

Then product price changes.

Then reports become strange.

Then someone opens Excel.

Then society collapses.

Use related tables.

That is the point of relational databases.

Forgetting Foreign Keys

Without foreign keys, your data can become inconsistent.

For example:

Order item points to product 999.
But product 999 does not exist.

Bad.

Foreign keys protect relationships.

PostgreSQL can guard the door.

Let it.

Forgetting Historical Prices

If order items only point to products and do not store unit_price, old orders may change when product prices change.

That is usually bad.

An old order should remember the price at the time of purchase.

History matters.

Especially when money is involved.

Counting Cancelled Orders as Revenue

This is a classic reporting mistake.

Always think about status.

WHERE o.status = 'paid'

Without this, your revenue report may include cancelled or unpaid orders.

The chart looks better.

Reality does not.

Reality usually wins.

Using INNER JOIN When You Need LEFT JOIN

If you want to show all customers, even customers without orders, use LEFT JOIN.

If you use INNER JOIN, customers without orders disappear.

Sometimes this is correct.

Sometimes it silently ruins your report.

SQL does not scream.

It just returns exactly what you asked.

Even if you asked badly.

Very honest.

Very dangerous.

Practice

Show all products with categories:

SELECT
  p.name AS product_name,
  p.price,
  c.name AS category_name
FROM products AS p
JOIN categories AS c
ON p.category_id = c.id
ORDER BY c.name, p.name;

Show all orders with customers:

SELECT
  o.id AS order_id,
  c.name AS customer_name,
  o.status,
  o.created_at
FROM orders AS o
JOIN customers AS c
ON o.customer_id = c.id
ORDER BY o.id;

Calculate totals for each order:

SELECT
  o.id AS order_id,
  c.name AS customer_name,
  SUM(oi.quantity * oi.unit_price) AS order_total
FROM orders AS o
JOIN customers AS c
ON o.customer_id = c.id
JOIN order_items AS oi
ON o.id = oi.order_id
GROUP BY o.id, c.name
ORDER BY order_total DESC;

Calculate total paid revenue:

SELECT
  SUM(oi.quantity * oi.unit_price) AS total_revenue
FROM orders AS o
JOIN order_items AS oi
ON o.id = oi.order_id
WHERE o.status = 'paid';

Show total spending per customer:

SELECT
  c.name AS customer_name,
  COALESCE(SUM(oi.quantity * oi.unit_price), 0) AS total_spent
FROM customers AS c
LEFT JOIN orders AS o
ON c.id = o.customer_id
AND o.status = 'paid'
LEFT JOIN order_items AS oi
ON o.id = oi.order_id
GROUP BY c.name
ORDER BY total_spent DESC;

Show sales by category:

SELECT
  cat.name AS category_name,
  SUM(oi.quantity * oi.unit_price) AS total_sales
FROM order_items AS oi
JOIN products AS p
ON oi.product_id = p.id
JOIN categories AS cat
ON p.category_id = cat.id
JOIN orders AS o
ON oi.order_id = o.id
WHERE o.status = 'paid'
GROUP BY cat.name
ORDER BY total_sales DESC;

Show low stock products:

SELECT
  name,
  stock_quantity
FROM products
WHERE stock_quantity < 10
ORDER BY stock_quantity ASC;

Run these queries.

Change the data.

Add customers.

Add orders.

Add order items.

Break something.

Fix it.

That is learning.

Very PostgreSQL.

Very practical.

Mini Challenge

Extend the shop database.

Add a new table called suppliers.

Rules:

Create the table:

CREATE TABLE suppliers (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  email VARCHAR(255) UNIQUE NOT NULL
);

Insert suppliers:

INSERT INTO suppliers (name, email)
VALUES
  ('Tech Supplier', 'tech@example.com'),
  ('Office Supplier', 'office@example.com'),
  ('Home Supplier', 'home@example.com');

Add a supplier column to products:

ALTER TABLE products
ADD COLUMN supplier_id INTEGER REFERENCES suppliers(id);

Update products:

UPDATE products
SET supplier_id = 1
WHERE category_id = 1;
UPDATE products
SET supplier_id = 2
WHERE category_id = 3;
UPDATE products
SET supplier_id = 3
WHERE category_id IN (2, 4);

Now write queries to:

Hint for sales by supplier:

SELECT
  s.name AS supplier_name,
  SUM(oi.quantity * oi.unit_price) AS total_sales
FROM suppliers AS s
JOIN products AS p
ON s.id = p.supplier_id
JOIN order_items AS oi
ON p.id = oi.product_id
JOIN orders AS o
ON oi.order_id = o.id
WHERE o.status = 'paid'
GROUP BY s.name
ORDER BY total_sales DESC;

This challenge adds another real-world layer.

Suppliers.

Products.

Sales.

Reports.

The database grows.

The brain suffers a little.

This is normal.

Growth has a price.

So do JOINs.

Summary

Today you built a practical PostgreSQL project.

You learned how to:

This is a big step.

Now PostgreSQL is no longer just theory.

You have used it like a real tool.

A database is not only tables.

It is structure.

Relationships.

Rules.

Questions.

Answers.

And sometimes strange errors that make you question your life choices.

But that is part of the journey.

Next Lesson

In the next lesson, we will learn how PostgreSQL works with applications.

We will discuss how backend applications connect to PostgreSQL.

We will look at connection strings.

Environment variables.

Basic security ideas.

And why putting database passwords directly in your code is a terrible idea.

A very terrible idea.

The kind of idea that wakes up security people at night.