← Back to Index MySQL 8.0 Learning Guide Part 1 of 10

Part 1: SQL Basics

Section 1 — The foundation of everything: querying, modifying, and structuring data.

Table of Contents

SELECT

SELECT is how you read data from a table. Think of it like asking the database a question: "Show me this information." You pick which columns you want to see.

Select All Columns

The asterisk * means "give me every column."

SQLSELECT * FROM employees;
Output:
idnamedepartmentsalaryhire_date
1John SmithEngineering75000.002020-03-15
2Jane DoeMarketing65000.002021-07-22
3Bob WilsonEngineering80000.002019-11-08
4Alice JohnsonSales55000.002022-01-30
5Charlie BrownMarketing70000.002021-05-12

Select Specific Columns

List only the columns you care about, separated by commas.

SQLSELECT name, department, salary FROM employees;
Output:
namedepartmentsalary
John SmithEngineering75000.00
Jane DoeMarketing65000.00
Bob WilsonEngineering80000.00
Alice JohnsonSales55000.00
Charlie BrownMarketing70000.00

INSERT

INSERT adds new rows into a table. You specify which columns get which values. It's like filling out a new employee registration form.

Single Row Insert

SQLINSERT INTO employees (name, department, salary, hire_date) VALUES ('Diana Prince', 'Sales', 60000.00, '2023-04-10');
Query OK, 1 row affected
Resulting table now contains:
idnamedepartmentsalaryhire_date
1John SmithEngineering75000.002020-03-15
2Jane DoeMarketing65000.002021-07-22
3Bob WilsonEngineering80000.002019-11-08
4Alice JohnsonSales55000.002022-01-30
5Charlie BrownMarketing70000.002021-05-12
6Diana PrinceSales60000.002023-04-10

Multi-Row Insert

Add several rows at once by separating each set of values with a comma.

SQLINSERT INTO products (product_name, category, price) VALUES ('Tablet', 'Electronics', 499.99), ('Bookshelf', 'Furniture', 249.99), ('Monitor', 'Electronics', 379.99);
Query OK, 3 rows affected
New rows added to products:
idproduct_namecategoryprice
6TabletElectronics499.99
7BookshelfFurniture249.99
8MonitorElectronics379.99

UPDATE

UPDATE changes data that already exists in a table. Always use a WHERE clause so you don't accidentally change every row!

Update Single Column

SQLUPDATE employees SET salary = 78000.00 WHERE id = 1;
Query OK, 1 row affected
Employee id=1 after update:
idnamedepartmentsalary
1John SmithEngineering78000.00

Update Multiple Columns

SQLUPDATE orders SET status = 'shipped', quantity = 3 WHERE id = 2;
Query OK, 1 row affected
Order id=2 after update:
idcustomer_idproduct_idorder_datestatusquantity
2212026-02-21shipped3

Update with WHERE Condition

Update all rows that match a condition — here, marking every pending order as shipped.

SQLUPDATE orders SET status = 'shipped' WHERE quantity = 1 AND status = 'pending';
Query OK, 1 row affected (order id=5 updated)
Affected rows:
idcustomer_idstatusquantity
55shipped1

DELETE

DELETE removes rows from a table permanently. Like UPDATE, always use WHERE unless you truly want to wipe every row.

Delete Specific Row

SQLDELETE FROM orders WHERE id = 5;
Query OK, 1 row affected
orders after deletion:
idcustomer_idproduct_idorder_datestatusquantity
1112026-02-20delivered2
2212026-02-21pending1
3122026-02-22processing1
4312026-02-22delivered3

Delete with Conditions

Remove all delivered single-quantity orders.

SQLDELETE FROM orders WHERE status = 'delivered' AND quantity = 2;
Query OK, 1 row affected (order id=1 deleted)

WHERE

WHERE filters rows so you only get the ones that match your condition. It's like saying "but only show me the ones where..."

Equals ( = )

SQLSELECT * FROM employees WHERE department = 'Engineering';
Output:
idnamedepartmentsalaryhire_date
1John SmithEngineering75000.002020-03-15
3Bob WilsonEngineering80000.002019-11-08

Not Equals ( != or <> )

SQLSELECT product_name, price FROM products WHERE category != 'Electronics';
Output:
product_nameprice
Desk349.99
Chair199.99

Greater Than ( > ) and Less Than ( < )

SQLSELECT product_name, price FROM products WHERE price > 300;
Output:
product_nameprice
Laptop999.99
Phone699.99
Desk349.99

AND

Both conditions must be true.

SQLSELECT * FROM orders WHERE status = 'delivered' AND customer_id = 1;
Output:
idcustomer_idproduct_idorder_datestatusquantity
1112026-02-20delivered2
4132026-02-22delivered1

OR

At least one condition must be true.

SQLSELECT * FROM orders WHERE status = 'pending' OR status = 'processing';
Output:
idcustomer_idproduct_idorder_datestatusquantity
2212026-02-21pending1
3122026-02-22processing1

NOT

Reverses a condition — "give me everything that is NOT this."

SQLSELECT name, department FROM employees WHERE NOT department = 'Engineering';
Output:
namedepartment
Jane DoeMarketing
Alice JohnsonSales
Charlie BrownMarketing

ORDER BY

ORDER BY sorts your results. By default it goes A→Z (ascending). Add DESC to reverse it.

Ascending (default)

SQLSELECT product_name, price FROM products ORDER BY price ASC;
Output:
product_nameprice
Headphones149.99
Chair199.99
Desk349.99
Phone699.99
Laptop999.99

Descending

SQLSELECT product_name, price FROM products ORDER BY price DESC;
Output:
product_nameprice
Laptop999.99
Phone699.99
Desk349.99
Chair199.99
Headphones149.99

Multiple Columns

Sort by category first (A→Z), then by price highest-first within each category.

SQLSELECT product_name, category, price FROM products ORDER BY category ASC, price DESC;
Output:
product_namecategoryprice
LaptopElectronics999.99
PhoneElectronics699.99
HeadphonesElectronics149.99
DeskFurniture349.99
ChairFurniture199.99

GROUP BY

GROUP BY lumps rows together based on a column's value, so you can run aggregate functions like COUNT(), SUM(), or AVG() on each group. Think of it as "give me a summary per category."

Basic Grouping with COUNT

SQLSELECT category, COUNT(*) AS total_products FROM products GROUP BY category;
Output:
categorytotal_products
Electronics3
Furniture2

HAVING

HAVING is like WHERE but for grouped results. You can't use WHERE to filter on COUNT() or SUM() — that's what HAVING is for.

SQLSELECT category, COUNT(*) AS total_products, AVG(price) AS avg_price FROM products GROUP BY category HAVING COUNT(*) > 2;
Output:
categorytotal_productsavg_price
Electronics3616.656667
Only Electronics has more than 2 products, so it's the only group that passes the HAVING filter.

LIMIT

LIMIT restricts how many rows the query returns. Great for previewing data or showing "Top N" results.

SQLSELECT product_name, price FROM products ORDER BY price DESC LIMIT 3;
Output (top 3 most expensive products):
product_nameprice
Laptop999.99
Phone699.99
Desk349.99

OFFSET

OFFSET skips the first N rows before starting to return results. By itself you still need LIMIT to cap how many rows come back.

SQLSELECT name, department FROM employees ORDER BY id LIMIT 3 OFFSET 2;
Output (skip first 2, then return 3):
namedepartment
Bob WilsonEngineering
Alice JohnsonSales
Charlie BrownMarketing

LIMIT + OFFSET (Pagination)

Combine them to build pagination — like showing page 1, page 2, etc. of results on a website. If each page shows 2 records:

Page 1

SQL-- Page 1: skip 0, show 2 SELECT id, name, department FROM employees ORDER BY id LIMIT 2 OFFSET 0;
Output (Page 1):
idnamedepartment
1John SmithEngineering
2Jane DoeMarketing

Page 2

SQL-- Page 2: skip 2, show 2 SELECT id, name, department FROM employees ORDER BY id LIMIT 2 OFFSET 2;
Output (Page 2):
idnamedepartment
3Bob WilsonEngineering
4Alice JohnsonSales

Page 3

SQL-- Page 3: skip 4, show 2 SELECT id, name, department FROM employees ORDER BY id LIMIT 2 OFFSET 4;
Output (Page 3):
idnamedepartment
5Charlie BrownMarketing

INNER JOIN

INNER JOIN returns only the rows where there is a match in BOTH tables. If a customer has no orders, they won't appear. If an order has no matching customer, it won't appear either.

customers orders INNER JOIN
Only the overlap is returned — customers who have at least one order.
SQLSELECT c.name, o.order_date, o.status FROM customers c INNER JOIN orders o ON c.id = o.customer_id;
Output:
nameorder_datestatus
Acme Corp2026-02-20delivered
TechStart Inc2026-02-21pending
Acme Corp2026-02-22processing
Global Trade2026-02-22delivered
CloudNet2026-02-23pending
Notice: DataFlow Ltd (id=4) does not appear because they have no orders.

LEFT JOIN

LEFT JOIN returns ALL rows from the left table (customers) and the matching rows from the right table (orders). If there's no match, the right side columns show NULL.

customers orders LEFT JOIN
All of the left table is returned, plus matching rows from the right. Unmatched right-side columns become NULL.
SQLSELECT c.name, o.order_date, o.status FROM customers c LEFT JOIN orders o ON c.id = o.customer_id;
Output:
nameorder_datestatus
Acme Corp2026-02-20delivered
Acme Corp2026-02-22processing
TechStart Inc2026-02-21pending
Global Trade2026-02-22delivered
DataFlow LtdNULLNULL
CloudNet2026-02-23pending
DataFlow Ltd now appears with NULL values for order columns because they have no orders — the LEFT JOIN keeps them anyway.

RIGHT JOIN

RIGHT JOIN is the mirror of LEFT JOIN. It returns ALL rows from the right table and matching rows from the left. In practice, most people just swap the table order and use LEFT JOIN instead.

departments employees RIGHT JOIN
All of the right table is returned, plus matching rows from the left.
SQLSELECT d.dept_name, d.budget, e.id AS emp_id, e.name FROM departments d RIGHT JOIN employees e ON d.dept_name = e.department;
Output:
dept_namebudgetemp_idname
Engineering500000.001John Smith
Marketing300000.002Jane Doe
Engineering500000.003Bob Wilson
Sales250000.004Alice Johnson
Marketing300000.005Charlie Brown
All 5 employees appear (right table). If there were an employee with a department not in the departments table, the department columns would be NULL.

CROSS JOIN

CROSS JOIN pairs every row from the first table with every row from the second table. If table A has 3 rows and table B has 2 rows, you get 3 × 2 = 6 rows. This is called a "Cartesian product."

departments (2 rows) × products (5 rows) = 10 rows
departments
Engineering
Marketing
×
products
Laptop
Phone
Desk
Chair
Headphones
=
10 combinations
Every department paired
with every product
SQLSELECT d.dept_name, p.product_name FROM departments d CROSS JOIN products p;
Output (showing all 10 rows):
dept_nameproduct_name
EngineeringLaptop
EngineeringPhone
EngineeringDesk
EngineeringChair
EngineeringHeadphones
MarketingLaptop
MarketingPhone
MarketingDesk
MarketingChair
MarketingHeadphones

UNION

UNION stacks the results of two queries on top of each other (vertically). It automatically removes duplicate rows. Both queries must have the same number of columns.

SQL-- Customers who have delivered orders, combined with customers in specific cities SELECT c.name, c.city FROM customers c INNER JOIN orders o ON c.id = o.customer_id WHERE o.status = 'delivered' UNION SELECT name, city FROM customers WHERE country = 'USA';
Output (duplicates removed):
namecity
Acme CorpNew York
Global TradeChicago
TechStart IncSan Francisco
DataFlow LtdBoston
CloudNetSeattle
Acme Corp appears in both queries (delivered order + USA-based). No duplicates in the final result because UNION removes them.

UNION ALL

UNION ALL works like UNION but keeps all rows, including duplicates. It's faster because it doesn't need to check for duplicates.

SQLSELECT name, department FROM employees WHERE department = 'Engineering' UNION ALL SELECT name, department FROM employees WHERE id < 4;
Output (duplicates kept):
namedepartment
John SmithEngineering
Bob WilsonEngineering
John SmithEngineering
Jane DoeMarketing
Bob WilsonEngineering
John Smith and Bob Wilson appear twice — once from each query. UNION ALL keeps both copies. The highlighted rows come from the second query.

DISTINCT

DISTINCT removes duplicate values from your results. If 3 products are in Electronics, you'll still only see 'Electronics' once.

SQLSELECT DISTINCT category FROM products;
Output:
category
Electronics
Furniture
SQL-- Compare: without DISTINCT, Electronics appears 3 times SELECT category FROM products;
Output (without DISTINCT):
category
Electronics
Furniture
Electronics
Furniture
Electronics

AS (Aliases)

AS gives a temporary nickname to a column or table. It makes your output cleaner and your queries easier to read.

Column Alias

SQLSELECT name AS 'Employee Name', department AS 'Department', hire_date AS 'Start Date' FROM employees;
Output:
Employee NameDepartmentStart Date
John SmithEngineering2020-03-15
Jane DoeMarketing2021-07-22
Bob WilsonEngineering2019-11-08
Alice JohnsonSales2022-01-30
Charlie BrownMarketing2021-05-12

Table Alias

Shorten table names so joins are less repetitive to type.

SQLSELECT c.name, o.status FROM customers AS c INNER JOIN orders AS o ON c.id = o.customer_id WHERE o.status = 'delivered';
Output:
namestatus
Acme Corpdelivered
Global Tradedelivered

IN

IN lets you check if a value matches any item in a list. It's a cleaner alternative to writing multiple OR conditions.

SQLSELECT * FROM products WHERE product_name IN ('Laptop', 'Phone', 'Headphones');
Output:
idproduct_namecategoryprice
1LaptopElectronics999.99
2PhoneElectronics699.99
5HeadphonesElectronics149.99

BETWEEN

BETWEEN filters values within a range (inclusive on both ends). Works with numbers, dates, and strings.

SQLSELECT product_name, price FROM products WHERE price BETWEEN 200 AND 700;
Output:
product_nameprice
Phone699.99
Desk349.99
SQL-- BETWEEN works with dates too SELECT id, order_date, status FROM orders WHERE order_date BETWEEN '2026-02-21' AND '2026-02-22';
Output:
idorder_datestatus
22026-02-21pending
32026-02-22processing
42026-02-22delivered

LIKE

LIKE searches for patterns in text. Use % to match any number of characters and _ to match exactly one character.

% Wildcard (any characters)

SQL-- Names starting with 'J' SELECT name, department FROM employees WHERE name LIKE 'J%';
Output:
namedepartment
John SmithEngineering
Jane DoeMarketing
SQL-- Product names containing 'ph' anywhere SELECT product_name FROM products WHERE product_name LIKE '%ph%';
Output:
product_name
Headphones

_ Wildcard (single character)

SQL-- Product names with exactly 4 letters SELECT product_name, price FROM products WHERE product_name LIKE '____';
Output:
product_nameprice
Desk349.99

IS NULL / IS NOT NULL

NULL means "no value" or "unknown." You can't check for it with = NULL — you must use IS NULL or IS NOT NULL.

IS NULL

SQLSELECT name, email, city FROM customers WHERE email IS NULL;
Output:
nameemailcity
DataFlow LtdNULLBoston

IS NOT NULL

SQLSELECT name, email, city FROM customers WHERE email IS NOT NULL;
Output:
nameemailcity
Acme Corpinfo@acme.comNew York
TechStart Inchello@techstart.comSan Francisco
Global Tradecontact@globaltrade.comChicago
CloudNetsupport@cloudnet.comSeattle

EXISTS

EXISTS checks if a subquery returns any rows at all. It's like asking "is there at least one matching row?" It returns TRUE or FALSE.

SQL-- Find customers who have at least one order SELECT name, city FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id );
Output:
namecity
Acme CorpNew York
TechStart IncSan Francisco
Global TradeChicago
CloudNetSeattle
DataFlow Ltd is excluded because the subquery found zero matching orders for their customer_id.

CREATE TABLE

CREATE TABLE builds a brand-new table in your database. You define each column's name, data type, and any constraints (like NOT NULL, PRIMARY KEY, DEFAULT, etc.).

SQLCREATE TABLE order_items ( id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, product_name VARCHAR(100) NOT NULL, added_at DATETIME DEFAULT CURRENT_TIMESTAMP, added_by VARCHAR(100) DEFAULT NULL, unit_price DECIMAL(10,2) DEFAULT NULL, is_returned TINYINT(1) DEFAULT 0, notes TEXT, FOREIGN KEY (order_id) REFERENCES orders(id) );
Query OK, 0 rows affected
Table structure created:
ColumnTypeNullKeyDefault
idintNOPRIauto_increment
order_idintNOMULNULL
product_namevarchar(100)NONULL
added_atdatetimeYESCURRENT_TIMESTAMP
added_byvarchar(100)YESNULL
unit_pricedecimal(10,2)YESNULL
is_returnedtinyint(1)YES0
notestextYESNULL

ALTER TABLE

ALTER TABLE modifies an existing table's structure — adding columns, removing them, changing data types, or renaming columns.

ADD COLUMN

SQLALTER TABLE employees ADD COLUMN email VARCHAR(255) DEFAULT NULL AFTER hire_date;
Query OK, 0 rows affected
employees table now has:
idnamedepartmentsalaryhire_dateemail
1John SmithEngineering75000.002020-03-15NULL
2Jane DoeMarketing65000.002021-07-22NULL

DROP COLUMN

SQLALTER TABLE employees DROP COLUMN email;
Query OK, 0 rows affected

MODIFY COLUMN (change data type)

SQLALTER TABLE employees MODIFY COLUMN name VARCHAR(150) NOT NULL;
Query OK, 0 rows affected
DESCRIBE employees (name column changed):
FieldTypeNullDefault
namevarchar(150)NONULL

RENAME COLUMN

SQLALTER TABLE employees RENAME COLUMN salary TO annual_salary;
Query OK, 0 rows affected
Column renamed from "salary" to "annual_salary":
idnamedepartmentannual_salaryhire_date
1John SmithEngineering75000.002020-03-15

DROP TABLE

DROP TABLE permanently deletes an entire table and all its data. Use IF EXISTS to avoid an error if the table doesn't exist.

SQLDROP TABLE IF EXISTS order_items;
Query OK, 0 rows affected
The order_items table is now completely gone — structure and all data deleted permanently. IF EXISTS prevents an error if the table was already deleted.

CREATE DATABASE

CREATE DATABASE makes a new empty database on your MySQL server. Then you use USE to switch into it.

SQLCREATE DATABASE my_company CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Query OK, 1 row affected
SQLUSE my_company;
Database changed
SHOW DATABASES (partial):
Database
information_schema
my_company
mysql
performance_schema
sys

INSERT INTO...SELECT

This combines INSERT with a SELECT query, letting you copy data from one table (or a filtered/transformed version of it) into another table.

SQL-- First, create an archive table with the same structure CREATE TABLE delivered_orders_archive ( id INT PRIMARY KEY, customer_id INT, product_id INT, order_date DATE, status VARCHAR(20), quantity INT );
Query OK, 0 rows affected
SQL-- Now copy all delivered orders into the archive INSERT INTO delivered_orders_archive (id, customer_id, product_id, order_date, status, quantity) SELECT id, customer_id, product_id, order_date, status, quantity FROM orders WHERE status = 'delivered';
Query OK, 2 rows affected (2 rows copied)
delivered_orders_archive now contains:
idcustomer_idproduct_idorder_datestatusquantity
1112026-02-20delivered2
4332026-02-22delivered1
Only the 2 orders with status = 'delivered' were copied. The original orders table is unchanged.
← Back to MySQL 8.0 Learning Guide Index

Part 1: SQL Basics — MySQL 8.0 Learning Guide