DEV Community

Cover image for SQL In One Go - Hands on Guide
Danish
Danish

Posted on

SQL In One Go - Hands on Guide

This is a practical, one sitting tour of SQL using a simple store: users, products, and orders. You will read, join, group, filter, and update data with queries you can run right now.

Use the online compiler to practice queries side by side:
onecompiler


What is a database

A database is an organized way to store information so you can ask questions and get reliable answers.
In our store:

  1. users: people who place orders
  2. products: things we sell
  3. orders: which user bought which product, and whether it was paid

Database design in plain words

  1. What data are we storing
    users, products, orders

  2. What properties do they have
    users: first name, last name
    products: name, department, price, weight
    orders: user id, product id, paid flag

  3. What types are those properties
    text: names and departments
    number: price and weight
    boolean: paid


Create the tables

We will use three tables and keep the design simple.

Why this design

  • Each table has a primary key called id so every row is unique.
  • orders.user_id points to users.id.
  • orders.product_id points to products.id.
  • paid says whether the order is completed.
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  first_name VARCHAR,
  last_name  VARCHAR
);

CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  name       VARCHAR,
  department VARCHAR,
  price      INTEGER,
  weight     INTEGER
);

CREATE TABLE orders (
  id SERIAL PRIMARY KEY,
  user_id    INTEGER REFERENCES users(id),
  product_id INTEGER REFERENCES products(id),
  paid       BOOLEAN
);
Enter fullscreen mode Exit fullscreen mode

Practice all queries side by side

  1. Open One Compiler or any other sql compiler of your own choice.

  2. Create the three tables

  3. Go to this link and copy the dummy data for tables we are going to use in our journey
    Dummy Data Docs

  4. Paste and run the queries as you learn.


Relationships you should know

  1. one to many
    One user can have many orders. users.id → many orders.user_id.

  2. many to one
    Many orders belong to one product. orders.product_idproducts.id.

  3. one to one
    Not in this design, but an example would be users and a user_profiles table with the same id.

  4. primary keys
    id in each table. They uniquely identify rows.

  5. foreign keys
    orders.user_id references users.id.
    orders.product_id references products.id.

  6. foreign key delete rules
    Default is restrict (Postgres uses NO ACTION), which prevents deleting a user if there are orders pointing to them.
    If you want to delete a user and also delete their orders:

-- Option A: define with cascade from the start
CREATE TABLE orders (
  id SERIAL PRIMARY KEY,
  user_id    INTEGER REFERENCES users(id) ON DELETE CASCADE,
  product_id INTEGER REFERENCES products(id),
  paid       BOOLEAN
);

-- Option B: add a named FK later with cascade
ALTER TABLE orders
ADD CONSTRAINT fk_orders_user
FOREIGN KEY (user_id) REFERENCES users(id)
ON DELETE CASCADE;
Enter fullscreen mode Exit fullscreen mode

If you want to keep orders but clear the link when a user is deleted:

ALTER TABLE orders
ADD CONSTRAINT fk_orders_user_setnull
FOREIGN KEY (user_id) REFERENCES users(id)
ON DELETE SET NULL;
Enter fullscreen mode Exit fullscreen mode

Insert data

You can paste bulk sample data to save time.

Go to this link and copy the dummy data for tables we are going to use in our journey
https://docs.google.com/document/d/1mA7tXgljt5bVGMMg2fCybyeon3PF3UJzPFz8iXfPzLw/edit?usp=sharing

After inserting, a quick sanity check:

Why: confirm rows exist.

SELECT COUNT(*) AS users, 
       (SELECT COUNT(*) FROM products) AS products, 
       (SELECT COUNT(*) FROM orders)   AS orders;
Enter fullscreen mode Exit fullscreen mode

Example result

users products orders
50 100 300

Update rows

Why: fix a typo or move a product to a new department.

UPDATE products
SET department = 'Home'
WHERE id = 11;  -- Fantastic Metal Chair
Enter fullscreen mode Exit fullscreen mode

Result idea: 1 row updated.


Delete rows

Why: remove a test user that has no orders.

DELETE FROM users
WHERE id = 51;  -- only if it exists and has no related orders
Enter fullscreen mode Exit fullscreen mode

If a user has orders and you did not set ON DELETE CASCADE, the delete will fail to protect data integrity.


Retrieve data with SELECT

1) Simple SELECT

SELECT id, first_name, last_name
FROM users
LIMIT 3;
Enter fullscreen mode Exit fullscreen mode

Example result

id first_name last_name
1 Iva Lindgren
2 Ignatius Johns
3 Jannie Boehm

2) Calculations in SELECT

Why: show price with a simple sales tax preview.

SELECT id, name, price,
       ROUND(price * 1.15) AS price_with_tax
FROM products
ORDER BY id
LIMIT 3;
Enter fullscreen mode Exit fullscreen mode

Example result

id name price price_with_tax
1 Practical Fresh Shirt 876 1007
2 Gorgeous Steel Towels 412 474
3 Rustic Plastic Bacon 10 12

3) String operations

Why: display a friendly full name.

SELECT id,
       first_name || ' ' || last_name AS full_name,
       UPPER(last_name)               AS last_upper
FROM users
LIMIT 3;
Enter fullscreen mode Exit fullscreen mode

Example result

id full_name last_upper
1 Iva Lindgren LINDGREN
2 Ignatius Johns JOHNS
3 Jannie Boehm BOEHM

4) WHERE filter

SELECT id, name, department, price
FROM products
WHERE department = 'Toys' AND price > 800
ORDER BY price DESC;
Enter fullscreen mode Exit fullscreen mode

Example result

id name department price
33 Handcrafted Concrete Bike Toys 748
31 Sleek Granite Towels Toys 797
9 Generic Fresh Computer Toys 926

5) Compound WHERE with OR

SELECT id, name, department, price
FROM products
WHERE department = 'Sports' OR department = 'Baby'
ORDER BY department, price DESC
LIMIT 5;
Enter fullscreen mode Exit fullscreen mode

6) WHERE with lists

SELECT id, name, department
FROM products
WHERE department IN ('Home','Garden','Electronics')
ORDER BY department, id
LIMIT 5;
Enter fullscreen mode Exit fullscreen mode

Joins

Why joins exist: data lives in separate tables. Joins bring them together.

1) INNER JOIN

Question: show paid orders with customer and product name.

SELECT o.id AS order_id,
       u.first_name || ' ' || u.last_name AS customer,
       p.name AS product,
       p.price,
       o.paid
FROM orders o
JOIN users u    ON u.id = o.user_id
JOIN products p ON p.id = o.product_id
WHERE o.paid = TRUE
ORDER BY o.id
LIMIT 5;
Enter fullscreen mode Exit fullscreen mode

Example result

order_id customer product price paid
1 Arely McGlynn Practical Steel Shoes 947 true
2 Ignatius Johns Generic Fresh Computer 926 true
3 Jannie Boehm Licensed Steel Towels 939 true
4 Iva Lindgren Gorgeous Concrete Towels 328 true
5 Kailee Jacobson Fantastic Soft Fish 10 true

(Your rows will differ. This is a shape preview.)

2) LEFT JOIN

Question: list users and whether they have any orders.

SELECT u.id,
       u.first_name || ' ' || u.last_name AS customer,
       COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, customer
ORDER BY order_count DESC, u.id
LIMIT 5;
Enter fullscreen mode Exit fullscreen mode

3) JOIN with extra WHERE

Question: paid orders for the Sports department.

SELECT u.first_name || ' ' || u.last_name AS customer,
       p.name AS product,
       p.department,
       p.price
FROM orders o
JOIN users u    ON u.id = o.user_id
JOIN products p ON p.id = o.product_id
WHERE o.paid = TRUE
  AND p.department = 'Sports'
ORDER BY p.price DESC;
Enter fullscreen mode Exit fullscreen mode

Aggregations

1) GROUP BY

Question: revenue by department for paid orders.

SELECT p.department,
       SUM(p.price) AS revenue,
       COUNT(*)     AS orders_count
FROM orders o
JOIN products p ON p.id = o.product_id
WHERE o.paid = TRUE
GROUP BY p.department
ORDER BY revenue DESC
LIMIT 5;
Enter fullscreen mode Exit fullscreen mode

Example result

department revenue orders_count
Home 4300 6
Sports 2850 4
Toys 2600 3
Beauty 1800 3
Baby 1700 3

(Numbers are illustrative. Run it to see your data.)

2) Aggregation functions

COUNT, SUM, AVG, MIN, MAX

SELECT
  COUNT(*)              AS total_products,
  AVG(price)            AS avg_price,
  MIN(price)            AS min_price,
  MAX(price)            AS max_price
FROM products;
Enter fullscreen mode Exit fullscreen mode

3) The COUNT with null gotcha

COUNT(column) ignores nulls. COUNT(*) counts rows.

SELECT
  COUNT(*)      AS rows_considered,
  COUNT(paid)   AS paid_not_null
FROM orders;
Enter fullscreen mode Exit fullscreen mode

If any paid is null, paid_not_null will be smaller. With our seed, paid is set to true or false, so both counts often match.

4) Filter groups with HAVING

Question: show only departments with at least 5 paid orders.

SELECT p.department,
       COUNT(*) AS paid_orders
FROM orders o
JOIN products p ON p.id = o.product_id
WHERE o.paid = TRUE
GROUP BY p.department
HAVING COUNT(*) >= 5
ORDER BY paid_orders DESC;
Enter fullscreen mode Exit fullscreen mode

Group by with joins: useful patterns

Top 5 customers by total spend on paid orders

SELECT u.id,
       u.first_name || ' ' || u.last_name AS customer,
       SUM(p.price) AS total_spent,
       COUNT(*)     AS orders_count
FROM orders o
JOIN users u    ON u.id = o.user_id
JOIN products p ON p.id = o.product_id
WHERE o.paid = TRUE
GROUP BY u.id, customer
ORDER BY total_spent DESC
LIMIT 5;
Enter fullscreen mode Exit fullscreen mode

Average price per department for paid orders

SELECT p.department,
       AVG(p.price) AS avg_paid_price
FROM orders o
JOIN products p ON p.id = o.product_id
WHERE o.paid = TRUE
GROUP BY p.department
ORDER BY avg_paid_price DESC
LIMIT 5;
Enter fullscreen mode Exit fullscreen mode

ORDER BY, LIMIT, OFFSET

Question: show the 5 most expensive products, then the next 5.

-- top 5
SELECT id, name, department, price
FROM products
ORDER BY price DESC
LIMIT 5;

-- next 5
SELECT id, name, department, price
FROM products
ORDER BY price DESC
LIMIT 5 OFFSET 5;
Enter fullscreen mode Exit fullscreen mode

UNION, INTERSECT, EXCEPT

Think in sets of user ids.

1) Users who ordered from Sports or Baby

SELECT DISTINCT o.user_id
FROM orders o JOIN products p ON p.id = o.product_id
WHERE p.department = 'Sports'
UNION
SELECT DISTINCT o.user_id
FROM orders o JOIN products p ON p.id = o.product_id
WHERE p.department = 'Baby';
Enter fullscreen mode Exit fullscreen mode

2) Users who ordered from Sports and also from Baby

SELECT DISTINCT o.user_id
FROM orders o JOIN products p ON p.id = o.product_id
WHERE p.department = 'Sports'
INTERSECT
SELECT DISTINCT o.user_id
FROM orders o JOIN products p ON p.id = o.product_id
WHERE p.department = 'Baby';
Enter fullscreen mode Exit fullscreen mode

3) Users who have orders but no paid orders

SELECT DISTINCT user_id
FROM orders
EXCEPT
SELECT DISTINCT user_id
FROM orders
WHERE paid = TRUE;
Enter fullscreen mode Exit fullscreen mode

Subqueries

What is a subquery

A query inside another query. You can use its result as a value, a list, or a table.

Three shapes:

  1. single value: one row one column
  2. list: many rows one column
  3. table: many rows many columns

Tip: always give subqueries an alias when used as a table.

1) Subquery in SELECT (single value)

Question: show price and how it compares to the average price.

SELECT id, name, price,
       (SELECT ROUND(AVG(price)) FROM products) AS avg_price_all
FROM products
ORDER BY id
LIMIT 3;
Enter fullscreen mode Exit fullscreen mode

2) Subquery in WHERE (list)

Question: users who bought Toys.

SELECT DISTINCT u.id, u.first_name || ' ' || u.last_name AS customer
FROM users u
WHERE u.id IN (
  SELECT o.user_id
  FROM orders o
  JOIN products p ON p.id = o.product_id
  WHERE p.department = 'Toys'
)
ORDER BY u.id
LIMIT 5;
Enter fullscreen mode Exit fullscreen mode

3) Subquery as a table in FROM or JOIN

Question: top 3 departments by paid revenue, then list their orders.

WITH top_depts AS (
  SELECT p.department, SUM(p.price) AS revenue
  FROM orders o
  JOIN products p ON p.id = o.product_id
  WHERE o.paid = TRUE
  GROUP BY p.department
  ORDER BY revenue DESC
  LIMIT 3
)
SELECT td.department,
       u.first_name || ' ' || u.last_name AS customer,
       p.name AS product,
       p.price
FROM top_depts td
JOIN products p ON p.department = td.department
JOIN orders   o ON o.product_id = p.id AND o.paid = TRUE
JOIN users    u ON u.id = o.user_id
ORDER BY td.department, p.price DESC
LIMIT 10;
Enter fullscreen mode Exit fullscreen mode

4) Correlated subquery

Question: for each user, count their paid orders without a GROUP BY on users.

SELECT u.id,
       u.first_name || ' ' || u.last_name AS customer,
       (
         SELECT COUNT(*)
         FROM orders o
         WHERE o.user_id = u.id AND o.paid = TRUE
       ) AS paid_orders
FROM users u
ORDER BY paid_orders DESC, u.id
LIMIT 5;
Enter fullscreen mode Exit fullscreen mode

DISTINCT

Question: list all unique product departments.

SELECT DISTINCT department
FROM products
ORDER BY department;
Enter fullscreen mode Exit fullscreen mode

Utility operators: GREATEST and LEAST

Question: simple shipping rule

  • base fee is 100
  • weight fee is weight × 10
  • charge the greater of the two
SELECT id, name, weight,
       GREATEST(100, weight * 10) AS shipping_fee
FROM products
ORDER BY id
LIMIT 5;
Enter fullscreen mode Exit fullscreen mode

Question: cap a discount at 20

SELECT id, name, price,
       LEAST(price * 0.10, 20) AS discount_preview
FROM products
ORDER BY id
LIMIT 5;
Enter fullscreen mode Exit fullscreen mode

CASE

Question: bucket products by price.

SELECT id, name, price,
       CASE
         WHEN price >= 800 THEN 'Premium'
         WHEN price >= 300 THEN 'Standard'
         ELSE 'Budget'
       END AS price_band
FROM products
ORDER BY price DESC
LIMIT 10;
Enter fullscreen mode Exit fullscreen mode

How queries are executed

Logical order in most SQL engines:

  • Always remember source - From where the data is coming (table/tables) - is always made first
  1. FROM
  2. JOIN
  3. WHERE
  4. GROUP BY
  5. HAVING
  6. SELECT
  7. DISTINCT
  8. ORDER BY
  9. LIMIT and OFFSET

This order explains many surprises. For example, WHERE runs before grouping, while HAVING filters after grouping.

If you have any questions, comment down.

Happy Learning.

Top comments (0)