SQL Tutorial for Beginners: Learn Queries, JOINS, and Filtering from Scratch

Learn SQL from scratch: SELECT, WHERE filtering, GROUP BY, JOINs explained, and INSERT/UPDATE/DELETE — with working examples you can run today.

This sql tutorial for beginners teaches you to write real database queries from scratch — no prior experience needed. By the end, you will be able to filter data, aggregate it, modify it, and combine tables with JOINs, all using a realistic sample database you can practice on today.

Every query in this guide is working SQL you can run yourself. Grab the free SQLite browser or use any online SQL playground, and follow along — SQL is learned by typing queries, not by reading about them.

What Is SQL and Why Learn It?

SQL (Structured Query Language, pronounced “sequel”) is the language used to talk to databases — the systems that store virtually all of the world’s data, from your bank balance to your social media feed. When an app needs to “find all orders from last month over $100,” it sends a SQL query to the database.

SQL has survived for 50 years because it is declarative: you describe what data you want, and the database figures out how to get it. And unlike most programming skills, SQL is genuinely forever — queries you learn today will work decades from now. If you are aiming at a data career, our guide on how to learn SQL for data analysis shows where this skill takes you.

Our Sample Database: Customers and Orders

Throughout this sql tutorial for beginners, we will use two tables — the classic customers-and-orders pair you will meet in every real business database:

customers table:

id | name          | country | signup_date
---+---------------+---------+------------
1  | Alice Smith   | USA     | 2024-01-15
2  | Bob Jones     | USA     | 2024-02-20
3  | Carlos Ruiz   | Spain   | 2024-03-10
4  | Diana Prince  | USA     | 2024-03-25
5  | Erik Johansson| Sweden  | 2024-05-01

orders table:

id  | customer_id | product     | amount | order_date
----+-------------+-------------+--------+-----------
101 | 1           | Laptop      | 1200   | 2024-06-01
102 | 1           | Mouse       | 25     | 2024-06-15
103 | 2           | Keyboard    | 80     | 2024-06-20
104 | 3           | Monitor     | 300    | 2024-07-02
105 | 2           | Laptop      | 1200   | 2024-07-10
106 | 4           | Headphones  | 150    | 2024-07-15

Notice that orders.customer_id refers to customers.id — this link between tables is what makes JOINs possible later.

SELECT: Reading Data from a Table (SQL Tutorial for Beginners)

SELECT is the heart of SQL — it retrieves data. Start simple:

-- Get every column (*) from every row in customers
SELECT * FROM customers;

-- Get only the columns you need (better practice)
SELECT name, country FROM customers;

Two habits to build now: name your columns explicitly instead of SELECT * (faster, clearer, and safer when tables change), and end statements with a semicolon. SQL keywords are conventionally written in UPPERCASE, though the language itself is case-insensitive.

WHERE: Filtering Rows

WHERE keeps only the rows that match a condition — this is how you learn to learn sql queries that answer real questions:

-- All customers from the USA
SELECT name, country FROM customers
WHERE country = 'USA';

-- Orders over $100
SELECT product, amount FROM orders
WHERE amount > 100;

-- Orders that are NOT laptops
SELECT * FROM orders
WHERE product != 'Laptop';

Comparison operators work as you would expect: =, != (or <>), >, <, >=, <=. Note that text values go in single quotes, while numbers do not.

Combining Conditions: AND, OR, NOT

-- USA customers who signed up after March 1, 2024
SELECT name FROM customers
WHERE country = 'USA' AND signup_date > '2024-03-01';

-- Orders that are laptops OR cost more than $500
SELECT product, amount FROM orders
WHERE product = 'Laptop' OR amount > 500;

-- Customers NOT from the USA
SELECT name, country FROM customers
WHERE NOT country = 'USA';

Watch out: AND binds tighter than OR, just like multiplication before addition. When mixing them, use parentheses to be explicit: WHERE (a = 1 OR a = 2) AND b = 3.

Pattern Matching and Lists: LIKE, IN, BETWEEN

-- Names starting with 'A' (% matches any sequence of characters)
SELECT name FROM customers
WHERE name LIKE 'A%';

-- Names with 'o' anywhere
SELECT name FROM customers
WHERE name LIKE '%o%';

-- Customers from a specific list of countries
SELECT name, country FROM customers
WHERE country IN ('USA', 'Spain');

-- Orders between $50 and $500 (inclusive)
SELECT product, amount FROM orders
WHERE amount BETWEEN 50 AND 500;

-- Orders placed in June 2024
SELECT * FROM orders
WHERE order_date BETWEEN '2024-06-01' AND '2024-06-30';

The _ wildcard matches exactly one character ('Sm_th' matches “Smith”). LIKE is case-insensitive in some databases (PostgreSQL needs ILIKE) — check your database’s docs if case matters.

ORDER BY and LIMIT: Sorting and Paging

-- Most expensive orders first
SELECT product, amount FROM orders
ORDER BY amount DESC;

-- Customers alphabetically by name (ASC is the default)
SELECT name FROM customers
ORDER BY name ASC;

-- Top 3 biggest orders
SELECT product, amount FROM orders
ORDER BY amount DESC
LIMIT 3;

-- Skip the top 2, show the next 3 (pagination)
SELECT product, amount FROM orders
ORDER BY amount DESC
LIMIT 3 OFFSET 2;

Sorting is a small but essential skill in any sql tutorial for beginners: ORDER BY always comes after WHERE, and LIMIT comes last. LIMIT/OFFSET is how every “page 2 of results” feature on the web works.

Aggregate Functions: COUNT, SUM, AVG, MIN, MAX

No sql tutorial for beginners is complete without aggregates — they collapse many rows into a single answer, the foundation of every report and dashboard:

-- How many customers do we have?
SELECT COUNT(*) FROM customers;

-- Total revenue and average order value
SELECT SUM(amount) AS total_revenue,
       AVG(amount) AS average_order,
       MAX(amount) AS biggest_order,
       MIN(amount) AS smallest_order
FROM orders;

AS renames a column in the output — always alias your aggregates so results are readable. Note COUNT(*) counts rows while COUNT(column) counts non-NULL values in that column.

GROUP BY and HAVING: Aggregates per Category

GROUP BY splits rows into groups and aggregates each one separately. This is where SQL gets powerful:

-- Revenue per product
SELECT product,
       COUNT(*) AS num_orders,
       SUM(amount) AS revenue
FROM orders
GROUP BY product
ORDER BY revenue DESC;

-- Which customers spent more than $500 in total?
SELECT customer_id,
       SUM(amount) AS total_spent
FROM orders
GROUP BY customer_id
HAVING SUM(amount) > 500;

The WHERE vs HAVING rule: WHERE filters individual rows before grouping; HAVING filters groups after aggregation. You cannot use WHERE SUM(amount) > 500 — aggregates are not allowed in WHERE, which is exactly why HAVING exists.

INSERT, UPDATE, DELETE: Changing Data

Reading data is only half the story in this sql tutorial for beginners. The basic sql commands for writing data are:

-- Add a new customer
INSERT INTO customers (name, country, signup_date)
VALUES ('Fatima Khan', 'USA', '2024-08-01');

-- Add a new order for her (assuming her id is 6)
INSERT INTO orders (customer_id, product, amount, order_date)
VALUES (6, 'Tablet', 450, '2024-08-10');

-- Raise the price of the mouse order (id 102) to $30
UPDATE orders
SET amount = 30
WHERE id = 102;

-- Remove test orders under $10
DELETE FROM orders
WHERE amount < 10;

Critical safety rule: never run UPDATE or DELETE without a WHERE clause unless you truly mean “every row in the table.” An UPDATE orders SET amount = 0 with no WHERE zeroes out the entire table. Professionals always write the WHERE first, or test with a SELECT using the same condition.

JOINs Explained: Combining Tables

Real databases split data across tables to avoid duplication. SQL joins explained simply: a JOIN matches rows from two tables using a shared column — here, orders.customer_id = customers.id.

INNER JOIN: Only Matching Rows

-- Each order with the customer's name
SELECT customers.name, orders.product, orders.amount
FROM orders
INNER JOIN customers
  ON orders.customer_id = customers.id;

Result: 6 rows — every order has a matching customer. INNER JOIN keeps only rows that match on both sides.

LEFT JOIN: Keep All Rows from the Left Table

-- ALL customers, with their orders where they exist
SELECT customers.name, orders.product, orders.amount
FROM customers
LEFT JOIN orders
  ON orders.customer_id = customers.id;

Result: 7 rows — Erik Johansson (customer 5) appears with NULL for product and amount because he has no orders. LEFT JOIN keeps every row from the table on the left (customers) even when there is no match. This is the most useful JOIN for questions like “which customers have never ordered?” — just add WHERE orders.id IS NULL.

RIGHT JOIN and a Visual Mental Model

RIGHT JOIN is the mirror image: keep all rows from the right table. (In practice, most developers just flip the table order and use LEFT JOIN.)

Picture two overlapping circles (a Venn diagram):

  • INNER JOIN = only the overlap (rows matching in both tables)
  • LEFT JOIN = the entire left circle plus the overlap
  • RIGHT JOIN = the entire right circle plus the overlap
  • FULL OUTER JOIN = both entire circles (supported in PostgreSQL; not in MySQL/SQLite)

One more practical JOIN example — total spending per customer including customers who spent nothing:

SELECT customers.name,
       COUNT(orders.id) AS num_orders,
       COALESCE(SUM(orders.amount), 0) AS total_spent
FROM customers
LEFT JOIN orders
  ON orders.customer_id = customers.id
GROUP BY customers.name
ORDER BY total_spent DESC;

COALESCE replaces NULL with 0 so Erik shows $0 instead of NULL. Note COUNT(orders.id) counts only matched orders (NULLs are skipped), while COUNT(*) would wrongly count Erik’s empty row.

Putting It All Together: A Real-World Query

Let’s answer a realistic business question using everything in this sql tutorial for beginners: “Which products generated more than $200 in revenue from USA customers in 2024, ranked by revenue?”

SELECT orders.product,
       COUNT(*) AS num_orders,
       SUM(orders.amount) AS revenue
FROM orders
INNER JOIN customers
  ON orders.customer_id = customers.id
WHERE customers.country = 'USA'
  AND orders.order_date BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY orders.product
HAVING SUM(orders.amount) > 200
ORDER BY revenue DESC;

Read it in SQL’s logical order: FROM + JOIN (gather the tables) → WHERE (filter rows) → GROUP BY (group what remains) → HAVING (filter groups) → SELECT (choose columns) → ORDER BY (sort). Memorizing this order makes even complex queries readable.

What’s Next

You now know the core of SQL: SELECT, WHERE, ORDER BY, LIMIT, aggregates with GROUP BY/HAVING, INSERT/UPDATE/DELETE, and INNER/LEFT/RIGHT JOINs. That covers roughly 90% of the queries working developers and analysts write daily.

For practice, download SQLite (free, serverless, runs anywhere) and recreate the customers/orders tables above. For reference, the official PostgreSQL tutorial is the gold standard for going deeper. Write one query a day for a month, and SQL will feel like a native language.

Leave a Reply

Your email address will not be published. Required fields are marked *