SQL LIMIT and Pagination

SQL Tutorial › Querying Data › LIMIT & Pagination

SQL LIMIT and Pagination

BeginnerLesson 12 of 699 min read6 queries run on MySQL 8.4

LIMIT returns only the first N rows of a result: the five newest orders, the three most expensive products, page 2 of a list. Combined with ORDER BY it answers “top N” questions, and with OFFSET it powers pagination in almost every web app.

In this lesson
  • Return the first N rows with LIMIT
  • Skip rows with OFFSET to build pages
  • Find the Nth highest value
  • Know the equivalents in other databases and the cost of big offsets

LIMIT syntax

MySQL
SELECT columnsFROM table_nameORDER BY columnLIMIT row_count [OFFSET skip];

LIMIT is the last step of a query: MySQL builds and sorts the whole result first, then keeps the requested rows.

Sample data

products · 9 rows
product_idproduct_namecategorypricestock
1Wireless MouseElectronics799120
2Mechanical KeyboardElectronics349945
3USB-C HubElectronics19990
4Office ChairFurniture899912
5Standing DeskFurniture189995
6Notebook PackStationery249300
7Gel PensStationery199250
8Desk LampFurniture149930
9Monitor ArmFurniture279918

Top N rows

The three most expensive products:

SQL
SELECT product_name, priceFROM productsORDER BY price DESCLIMIT 3;
ResultMySQL 8.4
product_nameprice
Standing Desk18999
Office Chair8999
Mechanical Keyboard3499
3 rows in set
How this query works, step by step
  1. FROM products reads 9 rows.
  2. ORDER BY price DESC sorts them from the highest price.
  3. LIMIT 3 keeps the first three rows of the sorted result.
Watch out

LIMIT without ORDER BY returns some N rows, not the first or best ones. Always sort when the choice of rows matters.

OFFSET: skip rows

OFFSET skips rows before LIMIT starts counting. Products 4 to 6 by price:

SQL
SELECT product_name, priceFROM productsORDER BY price DESCLIMIT 3 OFFSET 3;
ResultMySQL 8.4
product_nameprice
Monitor Arm2799
USB-C Hub1999
Desk Lamp1499
3 rows in set

MySQL also accepts the short form LIMIT offset, count, so LIMIT 3, 3 means the same. The order of the two numbers is easy to mix up; LIMIT 3 OFFSET 3 is clearer.

Pagination

For page p with n rows per page: LIMIT n OFFSET (p - 1) * n. Page 2 of the employee list, 5 per page:

SQL
SELECT emp_id, first_name, last_nameFROM employeesORDER BY emp_idLIMIT 5 OFFSET 5;
ResultMySQL 8.4
emp_idfirst_namelast_name
105VikramSingh
106NehaGupta
107ArjunDas
108KavyaIyer
109RohanJoshi
5 rows in set
How this query works, step by step
  1. Rows are sorted by emp_id so every page is stable.
  2. OFFSET 5 skips page 1 (employees 100 to 104).
  3. LIMIT 5 returns employees 105 to 109.
Tip

Always sort pages by a unique column (or add one as a tie-breaker). If two rows have the same sort value, they can swap places between pages, and a user sees one row twice and misses another.

Nth highest value

A classic interview question: find the second highest salary. Sort the distinct salaries descending, skip one, take one:

SQL
SELECT DISTINCT salaryFROM employeesORDER BY salary DESCLIMIT 1 OFFSET 1;
ResultMySQL 8.4
salary
125000
1 row in set
How this query works, step by step
  1. DISTINCT removes duplicate salaries, so a tie cannot be counted twice.
  2. ORDER BY salary DESC puts 250,000 first.
  3. OFFSET 1 skips it, and LIMIT 1 returns the next one: 125,000.

To get the 3rd highest, use OFFSET 2. DENSE_RANK solves the same problem and also returns who earns it.

LIMIT with ties

Which employees are the two lowest paid in Engineering?

SQL
SELECT first_name, salaryFROM employeesWHERE dept_id = 1ORDER BY salaryLIMIT 2;
ResultMySQL 8.4
first_namesalary
Amit25000
Priya72000
2 rows in set

The answer is Amit (25,000) and one of the two people earning 72,000. LIMIT cuts strictly after 2 rows and does not care that Priya and Karan are tied. If ties matter, use RANK().

LIMIT in other databases

DatabaseFirst 3 rowsRows 4-6
MySQL, PostgreSQL, SQLiteLIMIT 3LIMIT 3 OFFSET 3
SQL ServerSELECT TOP 3 ...OFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY
Oracle 12c+, standard SQLFETCH FIRST 3 ROWS ONLYOFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY

Performance: big offsets are slow

To answer LIMIT 20 OFFSET 100000, MySQL still finds and throws away 100,000 rows. For deep pages, use keyset pagination: remember the last value of the previous page and continue after it:

SQL
SELECT emp_id, first_nameFROM employeesWHERE emp_id > 104ORDER BY emp_idLIMIT 5;
ResultMySQL 8.4
emp_idfirst_name
105Vikram
106Neha
107Arjun
108Kavya
109Rohan
5 rows in set

With an index on emp_id, MySQL jumps straight to the right place, no matter how deep the page.

Exercise: Latest three orders

Show the three most recent orders: order_id, order_date and status.

Expected resultMySQL 8.4
order_idorder_datestatus
10102025-06-01Delivered
10092025-05-15Delivered
10082025-04-28Pending
3 rows in set
Hint 1

Sort by order_date descending, then limit.

Exercise: Third cheapest price

Find the third lowest distinct product price.

Expected resultMySQL 8.4
price
799
1 row in set
Hint 1

DISTINCT, ascending sort, skip two rows.

Quick quiz

1. What does LIMIT 5 OFFSET 10 return?

2. In MySQL, LIMIT 10, 5 means…

3. Why add ORDER BY to a query with LIMIT?

4. What is the SQL Server equivalent of LIMIT 3?

LIMIT FAQ

How do I get the first 10 rows in SQL?

In MySQL, PostgreSQL and SQLite use ORDER BY ... LIMIT 10. In SQL Server use SELECT TOP 10, and in standard SQL FETCH FIRST 10 ROWS ONLY.

How do I implement pagination in SQL?

Use ORDER BY on a unique column plus LIMIT page_size OFFSET (page - 1) * page_size. For very deep pages, keyset pagination (WHERE id > last_id) is much faster.

How do I find the second highest salary in SQL?

SELECT DISTINCT salary FROM employees ORDER BY salary DESC LIMIT 1 OFFSET 1; Alternatively, use DENSE_RANK() in a subquery and pick rank 2.

Does LIMIT make a query faster?

It reduces the rows sent back, and MySQL can stop early when an index provides the order. But with an ORDER BY on an unindexed column, MySQL still has to sort the whole result first.

SQL ORDER BY

SQL Tutorial › Querying Data › ORDER BY

SQL ORDER BY

BeginnerLesson 11 of 699 min read9 queries run on MySQL 8.4

Without ORDER BY, a database returns rows in whatever order is cheapest for it, and that order can change from one run to the next. ORDER BY sorts the result so it is predictable: highest salary first, newest order first, names A to Z.

In this lesson
  • Sort ascending (ASC) and descending (DESC)
  • Sort by several columns to break ties
  • Sort by an expression or a SELECT alias
  • Control where NULLs go and define a custom order

ORDER BY syntax

MySQL
SELECT columnsFROM table_nameORDER BY column1 [ASC | DESC], column2 [ASC | DESC], ...;

ASC (ascending: A to Z, small to large, old to new) is the default. DESC reverses it.

Sample data

employees · 12 rows (some columns)
emp_idfirst_namedept_idsalaryhire_date
100MeeraNULL2500002015-01-05
101Asha11250002018-04-12
102Rahul1980002019-07-01
103Priya1720002021-01-18
104Karan1720002022-08-29
105Vikram21050002017-11-20
106Neha2540002022-03-14
107Arjun2480002023-06-05
108Kavya3880002020-09-09
109Rohan3450002024-01-22
110Sneha4780002019-02-25
111Amit1250002025-06-02

Sort ascending

SQL
SELECT first_name, salaryFROM employeesORDER BY salary;
ResultMySQL 8.4
first_namesalary
Amit25000
Rohan45000
Arjun48000
Neha54000
Priya72000
Karan72000
Sneha78000
Kavya88000
Rahul98000
Vikram105000
Asha125000
Meera250000
12 rows in set
How this query works, step by step
  1. FROM employees reads 12 rows.
  2. SELECT keeps two columns.
  3. ORDER BY salary sorts from the lowest salary (25,000) to the highest (250,000). ASC is implied.

Sort descending

SQL
SELECT first_name, hire_dateFROM employeesORDER BY hire_date DESC;
ResultMySQL 8.4
first_namehire_date
Amit2025-06-02
Rohan2024-01-22
Arjun2023-06-05
Karan2022-08-29
Neha2022-03-14
Priya2021-01-18
Kavya2020-09-09
Rahul2019-07-01
Sneha2019-02-25
Asha2018-04-12
Vikram2017-11-20
Meera2015-01-05
12 rows in set
How this query works, step by step
  1. DESC puts the largest value first. For dates that means the most recent.
  2. Amit, hired in June 2025, is first; Meera, hired in 2015, is last.

Sort by several columns

Sort by department, and inside each department by salary from high to low:

SQL
SELECT dept_id, first_name, salaryFROM employeesORDER BY dept_id, salary DESC;
ResultMySQL 8.4
dept_idfirst_namesalary
NULLMeera250000
1Asha125000
1Rahul98000
1Priya72000
1Karan72000
1Amit25000
2Vikram105000
2Neha54000
2Arjun48000
3Kavya88000
3Rohan45000
4Sneha78000
12 rows in set
How this query works, step by step
  1. Rows are first sorted by dept_id ascending. The CEO’s NULL department comes first, because MySQL treats NULL as smaller than any value.
  2. Rows with the same dept_id are then sorted by salary DESC.
  3. Each column has its own direction: dept_id ascending, salary descending.

Ties are common. Priya and Karan both earn 72,000; add a second key so their order is defined:

SQL
SELECT first_name, salaryFROM employeesWHERE dept_id = 1ORDER BY salary DESC, first_name;
ResultMySQL 8.4
first_namesalary
Asha125000
Rahul98000
Karan72000
Priya72000
Amit25000
5 rows in set

Sort by an expression or alias

ORDER BY runs after SELECT, so it can use a column alias:

SQL
SELECT first_name, salary, commission,       salary + IFNULL(commission, 0) AS total_payFROM employeesWHERE dept_id = 2ORDER BY total_pay DESC;
ResultMySQL 8.4
first_namesalarycommissiontotal_pay
Vikram10500012000117000
Neha54000600060000
Arjun48000450052500
3 rows in set
How this query works, step by step
  1. For each Sales employee, IFNULL(commission, 0) turns a missing commission into 0 (IFNULL).
  2. total_pay adds it to the salary.
  3. ORDER BY total_pay DESC sorts by the alias, which exists because SELECT has already run.

You can also sort by a column that is not in the SELECT list:

SQL
SELECT product_nameFROM productsORDER BY price DESCLIMIT 3;
ResultMySQL 8.4
product_name
Standing Desk
Office Chair
Mechanical Keyboard
3 rows in set

Where do NULLs go?

MySQL sorts NULL before all values in ASC and after them in DESC. To put NULLs last in an ascending sort, sort by column IS NULL first:

SQL
SELECT first_name, commissionFROM employeesORDER BY commission IS NULL, commission;
ResultMySQL 8.4
first_namecommission
Arjun4500
Neha6000
Vikram12000
MeeraNULL
AshaNULL
RahulNULL
PriyaNULL
KaranNULL
KavyaNULL
RohanNULL
SnehaNULL
AmitNULL
12 rows in set
How this query works, step by step
  1. commission IS NULL is 0 for rows with a commission and 1 for rows without.
  2. Sorting by it puts the three sales staff (0) before everyone else (1).
  3. Within the first group, commission sorts the amounts from low to high.

A custom sort order with FIELD() and CASE

Show orders by status in a business order: Pending first, then Shipped, Delivered, Cancelled:

MySQL
SELECT order_id, statusFROM ordersORDER BY FIELD(status, 'Pending', 'Shipped', 'Delivered', 'Cancelled'), order_id;
ResultMySQL 8.4
order_idstatus
1008Pending
1006Shipped
1001Delivered
1002Delivered
1003Delivered
1005Delivered
1007Delivered
1009Delivered
1010Delivered
1004Cancelled
10 rows in set
How this query works, step by step
  1. FIELD(status, ...) returns the position of the status in the list: Pending = 1, Shipped = 2, Delivered = 3, Cancelled = 4.
  2. Sorting by that number gives the business order; order_id breaks ties.

FIELD() is MySQL-specific. The portable way is a CASE expression:

SQL
SELECT order_id, statusFROM ordersORDER BY CASE status           WHEN 'Pending'   THEN 1           WHEN 'Shipped'   THEN 2           WHEN 'Delivered' THEN 3           ELSE 4         END,         order_id;
ResultMySQL 8.4
order_idstatus
1008Pending
1006Shipped
1001Delivered
1002Delivered
1003Delivered
1005Delivered
1007Delivered
1009Delivered
1010Delivered
1004Cancelled
10 rows in set
Watch out

ORDER BY 2 (sorting by column position) works, but it silently changes meaning when someone edits the SELECT list. Use column names.

Exercise: Cheapest first

List product_name, category and price, sorted by category A to Z and, within a category, from the cheapest product.

Expected resultMySQL 8.4
product_namecategoryprice
Wireless MouseElectronics799
USB-C HubElectronics1999
Mechanical KeyboardElectronics3499
Desk LampFurniture1499
Monitor ArmFurniture2799
Office ChairFurniture8999
Standing DeskFurniture18999
Gel PensStationery199
Notebook PackStationery249
9 rows in set
Hint 1

Two sort keys, both ascending.

Exercise: Newest customers

List customers from the newest joined_on date to the oldest.

Expected resultMySQL 8.4
customer_namejoined_on
Sara Thomas2025-03-02
Manoj Pillai2025-01-11
Ishita Bose2024-07-30
David Fernandes2024-05-09
Fatima Khan2024-03-21
Rohit Kumar2024-02-03
Ananya Sharma2024-01-15
7 rows in set
Hint 1

Use DESC.

Quick quiz

1. What is the default sort direction?

2. In ORDER BY dept_id, salary DESC, which column is sorted descending?

3. Where does MySQL put NULL in an ascending sort?

4. Can ORDER BY use an alias defined in SELECT?

ORDER BY FAQ

How do I sort in descending order in SQL?

Add DESC after the column: ORDER BY salary DESC. Use ASC (or nothing) for ascending.

How do I sort by multiple columns in SQL?

List them separated by commas: ORDER BY dept_id, salary DESC. Rows are sorted by the first column, and later columns break ties.

Is the row order guaranteed without ORDER BY?

No. Without ORDER BY the database may return rows in any order, and it can change when data, indexes or the query plan change.

How do I put NULL values last in MySQL?

Sort by column IS NULL first: ORDER BY commission IS NULL, commission. MySQL has no NULLS LAST clause.

SQL WHERE Clause

SQL Tutorial › Querying Data › WHERE

SQL WHERE Clause

BeginnerLesson 9 of 6910 min read8 queries run on MySQL 8.4

Most questions you ask a database are about some rows, not all of them: the orders from January, the products that are out of stock, the employees in Pune. The WHERE clause filters rows so that only the ones matching a condition reach the result.

In this lesson
  • Filter rows with comparison operators
  • Compare numbers, text and dates correctly
  • Use WHERE with UPDATE and DELETE safely
  • Spot the classic WHERE mistakes

WHERE syntax

MySQL
SELECT column1, column2FROM table_nameWHERE condition;

The condition is checked for every row. Rows where it is true are kept; rows where it is false or unknown (because of NULL) are dropped.

Sample data

products · 9 rows
product_idproduct_namecategorypricestock
1Wireless MouseElectronics799120
2Mechanical KeyboardElectronics349945
3USB-C HubElectronics19990
4Office ChairFurniture899912
5Standing DeskFurniture189995
6Notebook PackStationery249300
7Gel PensStationery199250
8Desk LampFurniture149930
9Monitor ArmFurniture279918

Filter on a number

Which products cost less than 1,000?

SQL
SELECT product_name, priceFROM productsWHERE price < 1000;
ResultMySQL 8.4
product_nameprice
Wireless Mouse799
Notebook Pack249
Gel Pens199
3 rows in set
How this query works, step by step
  1. FROM products reads the 9 products.
  2. WHERE price < 1000 tests each row: 799, 249 and 199 pass; the other six fail.
  3. SELECT returns the name and price of the 3 rows that passed.

Comparison operators

OperatorMeaningExample
=equalcategory = 'Furniture'
<> or !=not equalstatus <> 'Delivered'
>greater thanprice > 2000
<less thanstock < 10
>=greater than or equalsalary >= 100000
<=less than or equalquantity <= 2

Products that are out of stock:

SQL
SELECT product_id, product_name, stockFROM productsWHERE stock = 0;
ResultMySQL 8.4
product_idproduct_namestock
3USB-C Hub0
1 row in set

Orders that are not delivered yet:

SQL
SELECT order_id, customer_id, statusFROM ordersWHERE status <> 'Delivered';
ResultMySQL 8.4
order_idcustomer_idstatus
10043Cancelled
10065Shipped
10081Pending
3 rows in set
How this query works, step by step
  1. <> means “not equal to”.
  2. Seven orders are Delivered, so the other three come back: one Cancelled, one Shipped and one Pending.

Filter on text

Text values go in single quotes. With MySQL’s default collation the comparison ignores case:

SQL
SELECT first_name, last_name, cityFROM employeesWHERE city = 'Pune';
ResultMySQL 8.4
first_namelast_namecity
PriyaNairPune
KavyaIyerPune
RohanJoshiPune
3 rows in set

Filter on dates

Write dates as 'YYYY-MM-DD' text. MySQL compares them as dates, so > means “later than”:

SQL
SELECT first_name, hire_dateFROM employeesWHERE hire_date >= '2022-01-01'ORDER BY hire_date;
ResultMySQL 8.4
first_namehire_date
Neha2022-03-14
Karan2022-08-29
Arjun2023-06-05
Rohan2024-01-22
Amit2025-06-02
5 rows in set
How this query works, step by step
  1. hire_date >= '2022-01-01' keeps people hired on or after 1 January 2022.
  2. Five employees joined in 2022 or later.
  3. ORDER BY hire_date lists them from the earliest to the latest hire.

Filter on a calculated value

The condition can use expressions. Which order lines are worth more than 5,000?

MySQL
SELECT oi.order_id, p.product_name, oi.quantity, p.price,       oi.quantity * p.price AS line_totalFROM order_items oiJOIN products p ON p.product_id = oi.product_idWHERE oi.quantity * p.price > 5000;
ResultMySQL 8.4
order_idproduct_namequantitypriceline_total
1002Office Chair189998999
1008Office Chair2899917998
1004Standing Desk11899918999
3 rows in set
How this query works, step by step
  1. The JOIN pairs each order line with its product so the price is available (INNER JOIN).
  2. WHERE calculates quantity * price for each line and keeps the lines above 5,000.
  3. The alias line_total cannot be used in WHERE (it is created later, in SELECT), so the expression is repeated.

WHERE with UPDATE and DELETE

WHERE decides which rows an UPDATE or DELETE touches. Without it, every row changes:

SQL
UPDATE productsSET stock = 50WHERE product_id = 3; SELECT product_id, product_name, stock FROM products WHERE product_id = 3;
ResultMySQL 8.4
mysql> UPDATE products
Query OK, 1 row affected
mysql> SELECT product_id, product_name, stock FROM products WHERE product_id …
product_idproduct_namestock
3USB-C Hub50
1 row in set
Watch out

Before running an UPDATE or DELETE, run a SELECT with the same WHERE and check that it returns exactly the rows you mean to change.

Common WHERE mistakes

  • Comparing with NULL using =. WHERE commission = NULL never matches. Use IS NULL (NULL Values).
  • Forgetting quotes around text. WHERE city = Pune makes MySQL look for a column called Pune.
  • Quoting numbers. WHERE price < '1000' works, but MySQL has to convert the text first; keep numbers unquoted.
  • Using a SELECT alias in WHERE. WHERE line_total > 5000 fails, because WHERE runs before SELECT.
MySQL
SELECT first_name FROM employees WHERE city = Pune;
ResultMySQL 8.4
ERROR 1054 (42S22): Unknown column 'Pune' in 'where clause'

Predict the result

How many rows does this return?

SELECT first_name, salaryFROM employeesWHERE salary <> 72000;

Exercise: Recent orders

Show order_id, customer_id and order_date for orders placed on or after 1 April 2025.

Expected resultMySQL 8.4
order_idcustomer_idorder_date
100722025-04-10
100812025-04-28
100932025-05-15
101052025-06-01
4 rows in set
Hint 1

Compare order_date with the text '2025-04-01' using >=.

Exercise: Furniture under 5,000

List the product_name and price of products in the Furniture category. (Filtering on two conditions at once comes in the next lesson; here, just the category.)

Expected resultMySQL 8.4
product_nameprice
Office Chair8999
Standing Desk18999
Desk Lamp1499
Monitor Arm2799
4 rows in set
Hint 1

WHERE category = 'Furniture'

Quick quiz

1. Which operator means “not equal” in SQL?

2. How should a date be written in a MySQL WHERE clause?

3. What happens to a row when the WHERE condition is unknown because of NULL?

4. What does UPDATE products SET stock = 0; without WHERE do?

WHERE clause FAQ

What is the WHERE clause in SQL?

WHERE filters rows. It follows FROM and contains a condition; only rows for which the condition is true are returned, updated or deleted.

Can I use WHERE with GROUP BY?

Yes. WHERE filters rows before they are grouped. To filter the groups themselves, for example “departments with more than 3 employees”, use HAVING.

What is the difference between WHERE and HAVING?

WHERE filters individual rows before grouping and cannot use aggregate functions. HAVING filters groups after GROUP BY and can use aggregates such as COUNT(*).

Is the WHERE clause case sensitive in MySQL?

With the default utf8mb4_0900_ai_ci collation, text comparisons are case and accent insensitive, so 'pune' matches 'Pune'. Use a binary or _cs collation for case-sensitive matching.

SQL AND, OR and NOT Operators

SQL Tutorial › Querying Data › AND, OR, NOT

SQL AND, OR and NOT Operators

BeginnerLesson 10 of 6910 min read7 queries run on MySQL 8.4

A single condition is rarely enough. “Furniture and under 5,000″, “Pune or Delhi”, “not cancelled”: AND, OR and NOT combine conditions in a WHERE clause. The one rule that catches everyone is precedence: AND binds tighter than OR, so you need parentheses more often than you think.

In this lesson
  • Require several conditions with AND
  • Accept any of several conditions with OR
  • Negate a condition with NOT
  • Use parentheses to control precedence, and know how NULL affects logic

Sample data

products · 9 rows
product_idproduct_namecategorypricestock
1Wireless MouseElectronics799120
2Mechanical KeyboardElectronics349945
3USB-C HubElectronics19990
4Office ChairFurniture899912
5Standing DeskFurniture189995
6Notebook PackStationery249300
7Gel PensStationery199250
8Desk LampFurniture149930
9Monitor ArmFurniture279918

AND: every condition must be true

Furniture that costs less than 5,000:

SQL
SELECT product_name, category, priceFROM productsWHERE category = 'Furniture'  AND price < 5000;
ResultMySQL 8.4
product_namecategoryprice
Desk LampFurniture1499
Monitor ArmFurniture2799
2 rows in set
How this query works, step by step
  1. MySQL checks both conditions for each of the 9 products.
  2. A row is kept only when both are true: it is Furniture and its price is below 5,000.
  3. The Office Chair and Standing Desk are Furniture but too expensive, so only the Desk Lamp and Monitor Arm remain.

OR: at least one condition must be true

Employees based in Pune or Delhi:

SQL
SELECT first_name, cityFROM employeesWHERE city = 'Pune'   OR city = 'Delhi';
ResultMySQL 8.4
first_namecity
PriyaPune
KavyaPune
RohanPune
SnehaDelhi
4 rows in set
How this query works, step by step
  1. A row is kept when the city is Pune, or Delhi, or (impossible here) both.
  2. Three employees are in Pune and one in Delhi: 4 rows.
Tip

A list of ORs on the same column is easier to read with IN: WHERE city IN ('Pune', 'Delhi').

NOT: reverse a condition

All orders except the delivered ones:

SQL
SELECT order_id, statusFROM ordersWHERE NOT status = 'Delivered';
ResultMySQL 8.4
order_idstatus
1004Cancelled
1006Shipped
1008Pending
3 rows in set

NOT also combines with other operators: NOT IN, NOT BETWEEN, NOT LIKE, IS NOT NULL.

Precedence: AND before OR

Goal: Electronics or Stationery products that are in stock. This first attempt looks right but is wrong:

SQL
SELECT product_name, category, stockFROM productsWHERE category = 'Electronics'   OR category = 'Stationery'  AND stock > 0;
ResultMySQL 8.4
product_namecategorystock
Wireless MouseElectronics120
Mechanical KeyboardElectronics45
USB-C HubElectronics0
Notebook PackStationery300
Gel PensStationery250
5 rows in set
How this query works, step by step
  1. AND is evaluated before OR, so MySQL reads the condition as category = 'Electronics' OR (category = 'Stationery' AND stock > 0).
  2. Every Electronics product passes, whatever its stock, so the out-of-stock USB-C Hub (stock 0) slips in.

Parentheses say what you mean:

SQL
SELECT product_name, category, stockFROM productsWHERE (category = 'Electronics' OR category = 'Stationery')  AND stock > 0;
ResultMySQL 8.4
product_namecategorystock
Wireless MouseElectronics120
Mechanical KeyboardElectronics45
Notebook PackStationery300
Gel PensStationery250
4 rows in set
How this query works, step by step
  1. The parentheses group the two category tests into one condition.
  2. That group and stock > 0 must both be true.
  3. The USB-C Hub now fails the stock test: 4 rows instead of 5.
Watch out

Whenever a WHERE clause mixes AND and OR, add parentheses, even when the default order happens to be correct. The next person to read the query will thank you.

OrderOperator
1comparisons: =, <, >, LIKE, IN …
2NOT
3AND
4OR

AND, OR and NULL

SQL uses three-valued logic: a condition can be TRUE, FALSE or UNKNOWN (when NULL is involved). WHERE keeps only TRUE rows.

SQL
SELECT first_name, commissionFROM employeesWHERE commission > 5000 OR commission < 5000;
ResultMySQL 8.4
first_namecommission
Vikram12000
Neha6000
Arjun4500
3 rows in set
How this query works, step by step
  1. For the three sales staff, commission has a value, so one of the two comparisons is true: Vikram (12,000), Neha (6,000), Arjun (4,500).
  2. For the other nine employees commission is NULL. NULL > 5000 is UNKNOWN, and so is NULL < 5000.
  3. UNKNOWN OR UNKNOWN is still UNKNOWN, so those rows are dropped.
ABA AND BA OR B
TRUEUNKNOWNUNKNOWNTRUE
FALSEUNKNOWNFALSEUNKNOWN
UNKNOWNUNKNOWNUNKNOWNUNKNOWN

A realistic filter

Engineering or Sales employees hired before 2022 who earn at least 90,000:

SQL
SELECT first_name, dept_id, hire_date, salaryFROM employeesWHERE (dept_id = 1 OR dept_id = 2)  AND hire_date < '2022-01-01'  AND salary >= 90000ORDER BY salary DESC;
ResultMySQL 8.4
first_namedept_idhire_datesalary
Asha12018-04-12125000
Vikram22017-11-20105000
Rahul12019-07-0198000
3 rows in set
How this query works, step by step
  1. The parentheses keep departments 1 and 2.
  2. hire_date < '2022-01-01' removes recent hires.
  3. salary >= 90000 removes the lower salaries.
  4. Asha, Vikram and Rahul pass all three tests.

Exercise: Cheap or plentiful

List products that cost less than 500 or have more than 100 in stock. Show product_name, price and stock.

Expected resultMySQL 8.4
product_namepricestock
Wireless Mouse799120
Notebook Pack249300
Gel Pens199250
3 rows in set
Hint 1

Two conditions joined by OR.

Exercise: Delivered in the first quarter

List delivered orders placed in January, February or March 2025 (order_date from 2025-01-01 to 2025-03-31).

Expected resultMySQL 8.4
order_idorder_datestatus
10012025-01-05Delivered
10022025-01-17Delivered
10032025-02-02Delivered
10052025-03-08Delivered
4 rows in set
Hint 1

Three conditions: the status, the first date and the last date, joined with AND.

Quick quiz

1. Which is evaluated first?

2. WHERE a = 1 OR b = 2 AND c = 3 is the same as…

3. What is TRUE OR UNKNOWN?

4. Why does WHERE commission > 5000 OR commission < 5000 skip employees with a NULL commission?

AND / OR FAQ

What is the difference between AND and OR in SQL?

AND returns a row only when all conditions are true. OR returns a row when at least one condition is true.

Does AND or OR come first in SQL?

AND is evaluated before OR. Use parentheses to change the order or simply to make it obvious.

Can I use AND and OR in the same WHERE clause?

Yes, as long as you group conditions with parentheses, for example WHERE (city = 'Pune' OR city = 'Delhi') AND salary > 50000.

How does NOT work with NULL?

NOT UNKNOWN is still UNKNOWN, so NOT (commission > 5000) does not return rows where commission is NULL. Test for NULL explicitly with IS NULL.

SQL SELECT DISTINCT

SQL Tutorial › Querying Data › SELECT DISTINCT

SQL SELECT DISTINCT

BeginnerLesson 8 of 698 min read7 queries run on MySQL 8.4

A plain SELECT returns one result row for every table row, so the same value often appears many times. SELECT DISTINCT removes the duplicates and returns each unique value, or each unique combination of values, only once.

In this lesson
  • List the unique values of a column
  • Get distinct combinations of several columns
  • Count unique values with COUNT(DISTINCT ...)
  • How DISTINCT handles NULL

Sample data

employees · 12 rows (some columns)
emp_idfirst_namedept_idjob_titlecity
100MeeraNULLCEOMumbai
101Asha1Engineering ManagerBengaluru
102Rahul1Senior DeveloperBengaluru
103Priya1QA EngineerPune
104Karan1DeveloperBengaluru
105Vikram2Sales ManagerMumbai
106Neha2Sales ExecutiveMumbai
107Arjun2Sales ExecutiveKolkata
108Kavya3Marketing LeadPune
109Rohan3Content WriterPune
110Sneha4HR ManagerDelhi
111Amit1InternBengaluru

Duplicates without DISTINCT

Which cities do our employees work in?

SQL
SELECT cityFROM employeesORDER BY city;
ResultMySQL 8.4
city
Bengaluru
Bengaluru
Bengaluru
Bengaluru
Delhi
Kolkata
Mumbai
Mumbai
Mumbai
Pune
Pune
Pune
12 rows in set

The result has 12 rows because there are 12 employees: Bengaluru and Pune appear several times.

SELECT DISTINCT on one column

SQL
SELECT DISTINCT cityFROM employeesORDER BY city;
ResultMySQL 8.4
city
Bengaluru
Delhi
Kolkata
Mumbai
Pune
5 rows in set
How this query works, step by step
  1. FROM employees reads all 12 rows.
  2. SELECT city keeps only the city column.
  3. DISTINCT compares the rows of the result and keeps one copy of each value: 12 rows shrink to 5 cities.
  4. ORDER BY city sorts them alphabetically.

DISTINCT on several columns

With more than one column, DISTINCT removes rows where all the listed columns are the same. It works on the combination, not on each column separately:

SQL
SELECT DISTINCT dept_id, cityFROM employeesORDER BY dept_id, city;
ResultMySQL 8.4
dept_idcity
NULLMumbai
1Bengaluru
1Pune
2Kolkata
2Mumbai
3Pune
4Delhi
7 rows in set
How this query works, step by step
  1. MySQL builds (dept_id, city) pairs for all 12 employees.
  2. Pairs that occur more than once, such as (1, Bengaluru) for four engineers, are kept once.
  3. Department 1 still appears twice, because it has two different cities: Bengaluru and Pune.
  4. The CEO’s pair (NULL, Mumbai) is a combination of its own.

DISTINCT and NULL

DISTINCT treats all NULLs as equal, so they collapse into one NULL row:

SQL
SELECT DISTINCT commissionFROM employeesORDER BY commission;
ResultMySQL 8.4
commission
NULL
4500
6000
12000
4 rows in set
How this query works, step by step
  1. Nine employees have no commission (NULL) and three sales staff have one.
  2. The nine NULLs become a single NULL row, and the three amounts are all different.
  3. In ascending order MySQL puts NULL first.

COUNT(DISTINCT column)

To count how many different values a column has, put DISTINCT inside COUNT:

SQL
SELECT COUNT(*)             AS employees,       COUNT(city)          AS rows_with_city,       COUNT(DISTINCT city) AS different_cities,       COUNT(DISTINCT dept_id) AS departments_usedFROM employees;
ResultMySQL 8.4
employeesrows_with_citydifferent_citiesdepartments_used
121254
1 row in set
How this query works, step by step
  1. COUNT(*) counts every row: 12.
  2. COUNT(city) counts rows where city is not NULL: also 12.
  3. COUNT(DISTINCT city) counts unique cities: 5.
  4. COUNT(DISTINCT dept_id) ignores the CEO’s NULL and counts 4 departments.

Which customers have placed at least one order? DISTINCT turns “one row per order” into “one row per customer”:

SQL
SELECT DISTINCT customer_idFROM ordersORDER BY customer_id;
ResultMySQL 8.4
customer_id
1
2
3
4
5
5 rows in set

DISTINCT vs GROUP BY

SELECT DISTINCT city FROM employees and SELECT city FROM employees GROUP BY city return the same rows. Use DISTINCT when you only want unique rows; use GROUP BY when you also want a count, sum or other aggregate per group:

SQL
SELECT city, COUNT(*) AS staffFROM employeesGROUP BY cityORDER BY staff DESC, city;
ResultMySQL 8.4
citystaff
Bengaluru4
Mumbai3
Pune3
Delhi1
Kolkata1
5 rows in set
Watch out

DISTINCT is not a fix for a wrong join. If a query returns unexpected duplicates, find out why (often a missing join condition) instead of hiding them with DISTINCT. Removing duplicates also costs time on large results.

Exercise: Unique job titles

List each distinct job_title in the employees table, sorted alphabetically.

Expected resultMySQL 8.4
job_title
CEO
Content Writer
Developer
Engineering Manager
HR Manager
Intern
Marketing Lead
QA Engineer
Sales Executive
Sales Manager
Senior Developer
11 rows in set
Hint 1

Put DISTINCT straight after SELECT.

Exercise: How many product categories?

Count how many different category values the products table has. Name the column categories.

Expected resultMySQL 8.4
categories
3
1 row in set
Hint 1

Use COUNT(DISTINCT category).

Quick quiz

1. SELECT DISTINCT dept_id, city removes a row when…

2. A column has the values 5, NULL, 5, NULL. What does SELECT DISTINCT return?

3. What does COUNT(DISTINCT city) count?

SELECT DISTINCT FAQ

What does SELECT DISTINCT do in SQL?

It removes duplicate rows from the result, so each unique value or combination of values appears once.

Does DISTINCT apply to all columns?

Yes. DISTINCT always applies to the whole row of selected columns. There is no way to make it apply to only the first column; use GROUP BY or a window function for that.

Is DISTINCT faster than GROUP BY?

For simply removing duplicates, MySQL usually runs both the same way. Choose the one that states your intent: DISTINCT for unique rows, GROUP BY when you aggregate.

How do I count distinct values in SQL?

Use COUNT(DISTINCT column). It counts the different non-NULL values of the column.

SQL Data Types

SQL Tutorial › Getting Started › SQL Data Types

SQL Data Types

BeginnerLesson 5 of 6914 min read7 queries run on MySQL 8.4

Every column has a data type that decides what values it accepts, how much space it takes and how it sorts and compares. Choosing well prevents bugs such as rounding errors in money or names cut short. This lesson shows the MySQL types you will actually use, and what MySQL 8.4 does when a value does not fit.

In this lesson
  • Pick the right numeric type, and why money needs DECIMAL
  • CHAR vs VARCHAR vs TEXT
  • Store dates and times with DATE, DATETIME and TIMESTAMP
  • What happens when a value is too big or too long

Numeric types

TypeStoresRange / precisionTypical use
TINYINT1 byte-128 to 127flags, small counts
SMALLINT2 bytes-32,768 to 32,767quantities
INT4 bytesabout ±2.1 billionids, counts, salaries in whole rupees
BIGINT8 bytesabout ±9.2 × 10^18very large ids, bytes
DECIMAL(p,s)exactp digits, s after the pointmoney, rates
FLOAT / DOUBLEapproximate7 / 15 significant digitsmeasurements, science

Add UNSIGNED to an integer type to allow only zero and positive numbers and double the upper limit.

DECIMAL vs FLOAT: why money must be exact

FLOAT and DOUBLE store numbers in binary, so most decimal fractions are only approximated. DECIMAL stores them exactly:

MySQL
SELECT 0.1e0 + 0.2e0 AS double_sum,       0.1 + 0.2     AS decimal_sum;
ResultMySQL 8.4
double_sumdecimal_sum
0.300000000000000040.3
1 row in set
How this query works, step by step
  1. 0.1e0 is written in scientific notation, so MySQL treats it as a DOUBLE. The binary sum is 0.30000000000000004.
  2. 0.1 without an exponent is an exact DECIMAL literal, so the sum is exactly 0.3.
  3. A tiny error like this, repeated over thousands of invoices, is why prices and balances use DECIMAL(10,2).

Values that do not fit

MySQL 8.4 runs in strict mode by default: a value that does not fit is an error, not a silent change.

MySQL
CREATE TABLE stock_check (qty TINYINT);INSERT INTO stock_check VALUES (300);
ResultMySQL 8.4
mysql> CREATE TABLE stock_check (qty TINYINT)
Query OK, 0 rows affected
mysql> INSERT INTO stock_check VALUES (300)
ERROR 1264 (22003): Out of range value for column 'qty' at row 1
How this query works, step by step
  1. TINYINT holds -128 to 127.
  2. 300 is out of range, so MySQL rejects the row with error 1264 instead of storing 127.

String types

TypeStoresNotes
CHAR(n)exactly n characterspadded with spaces; good for fixed codes like 'IN'
VARCHAR(n)up to n charactersthe usual choice for names, emails, titles
TEXTup to 64 KBlong descriptions, comments
ENUM('a','b')one value from a listsmall fixed sets such as statuses

Text longer than the column allows is rejected:

MySQL
CREATE TABLE users (username VARCHAR(8));INSERT INTO users VALUES ('sqatools_admin');
ResultMySQL 8.4
mysql> CREATE TABLE users (username VARCHAR(8))
Query OK, 0 rows affected
mysql> INSERT INTO users VALUES ('sqatools_admin')
ERROR 1406 (22001): Data too long for column 'username' at row 1

ENUM accepts only the listed values:

MySQL
CREATE TABLE tickets (id INT, status ENUM('open', 'in_progress', 'closed'));INSERT INTO tickets VALUES (1, 'open');INSERT INTO tickets VALUES (2, 'pending');
ResultMySQL 8.4
mysql> CREATE TABLE tickets (id INT, status ENUM('open', 'in_progress', 'clos…
Query OK, 0 rows affected
mysql> INSERT INTO tickets VALUES (1, 'open')
Query OK, 1 row affected
mysql> INSERT INTO tickets VALUES (2, 'pending')
ERROR 1265 (01000): Data truncated for column 'status' at row 1
How this query works, step by step
  1. The table is created and the first insert succeeds, because 'open' is in the list.
  2. 'pending' is not one of the allowed values, so the second insert fails.
Tip

Many teams prefer a VARCHAR column plus a CHECK constraint or a lookup table instead of ENUM, because adding a value to an ENUM needs an ALTER TABLE.

Date and time types

TypeFormatExample
DATEYYYY-MM-DD'2026-01-31'
TIMEHH:MM:SS'09:30:00'
DATETIMEYYYY-MM-DD HH:MM:SS'2026-01-31 09:30:00'
TIMESTAMPlike DATETIME, stored in UTCconverted to the session time zone
YEARYYYY2026

Date arithmetic understands the calendar:

MySQL
SELECT DATE '2026-01-31' + INTERVAL 1 MONTH AS next_month,       DATE '2024-02-28' + INTERVAL 1 DAY   AS leap_day,       DATEDIFF('2026-12-25', '2026-01-01') AS days_between;
ResultMySQL 8.4
next_monthleap_daydays_between
2026-02-282024-02-29358
1 row in set
How this query works, step by step
  1. One month after 31 January 2026 is 28 February, because February has no 31st.
  2. 2024 is a leap year, so the day after 28 February is the 29th.
  3. DATEDIFF() counts the days between two dates: 358.

Learn more in Date & Time Functions.

BOOLEAN

MySQL has no separate boolean type: BOOLEAN is a synonym for TINYINT(1), TRUE is 1 and FALSE is 0.

SQL
CREATE TABLE features (name VARCHAR(20), enabled BOOLEAN);INSERT INTO features VALUES ('dark_mode', TRUE), ('beta_api', FALSE);SELECT name, enabled FROM features;
ResultMySQL 8.4
mysql> CREATE TABLE features (name VARCHAR(20), enabled BOOLEAN)
Query OK, 0 rows affected
mysql> INSERT INTO features VALUES ('dark_mode', TRUE), ('beta_api', FALSE)
Query OK, 2 rows affected
mysql> SELECT name, enabled FROM features
nameenabled
dark_mode1
beta_api0
2 rows in set

JSON

A JSON column stores validated JSON documents, and you can read values from them in queries:

SQL
CREATE TABLE settings (user_id INT, prefs JSON);INSERT INTO settings VALUES (1, '{"theme": "dark", "font_size": 14}');SELECT user_id,       prefs->>'$.theme'     AS theme,       prefs->>'$.font_size' AS font_sizeFROM settings;
ResultMySQL 8.4
mysql> CREATE TABLE settings (user_id INT, prefs JSON)
Query OK, 0 rows affected
mysql> INSERT INTO settings VALUES (1, '{"theme": "dark", "font_size": 14}')
Query OK, 1 row affected
mysql> SELECT user_id,
user_idthemefont_size
1dark14
1 row in set
How this query works, step by step
  1. MySQL checks that the text is valid JSON before storing it.
  2. ->> extracts a value by path and returns it as plain text: $.theme is the theme key.

How to choose a data type

  • Ids and counts: INT (or BIGINT for very large tables).
  • Money: DECIMAL(10,2), never FLOAT.
  • Names, emails, codes: VARCHAR with a sensible limit.
  • Long text: TEXT.
  • Dates: DATE; date and time: DATETIME; “when was this row changed”: TIMESTAMP.
  • Yes/no: BOOLEAN.
  • Choose the smallest type that safely fits the data: smaller rows mean faster queries and smaller indexes.

Quick quiz

1. Which type should store product prices?

2. What is the difference between CHAR(10) and VARCHAR(10)?

3. In strict mode, what happens when you insert 300 into a TINYINT column?

4. What does BOOLEAN become in MySQL?

Data types FAQ

What are the main data types in SQL?

Numeric (INT, DECIMAL, FLOAT), string (CHAR, VARCHAR, TEXT), date and time (DATE, DATETIME, TIMESTAMP) and others such as BOOLEAN and JSON.

What is the difference between DATETIME and TIMESTAMP in MySQL?

DATETIME stores the date and time exactly as given. TIMESTAMP converts the value to UTC for storage and back to the session time zone when read, and its range ends in 2038.

Should I use VARCHAR(255) for every text column?

No. Choose a limit that matches the data, such as VARCHAR(100) for an email. Realistic limits act as validation and keep indexes smaller.

Why is FLOAT bad for money?

FLOAT and DOUBLE store binary approximations, so values like 0.1 cannot be stored exactly and totals drift. DECIMAL stores exact decimal values.

The SQL Sample Database

SQL Tutorial › Getting Started › The Sample Database

The SQL Sample Database

BeginnerLesson 6 of 698 min read1 queries run on MySQL 8.4

Every lesson in this tutorial queries the same small company database, so you can focus on SQL instead of new data each time. It has six related tables: a company’s departments and employees, and its online shop’s customers, products, orders and order items. This page shows every row and gives you the script to create it on your own MySQL server.

In this lesson
  • What each table holds and how the tables are linked
  • Which rows make interesting edge cases (NULLs, unmatched rows, ties)
  • How to create the database on your own server

How the tables are linked

TablePrimary keyLinks toRows
departmentsdept_id5
employeesemp_iddept_id → departments, manager_id → employees12
customerscustomer_id7
productsproduct_id9
ordersorder_idcustomer_id → customers10
order_itemsorder_id + product_idorder_id → orders, product_id → products16
  • HR side: each employee belongs to a department and reports to a manager, who is also an employee (manager_id points back to employees). The chain goes four levels deep: CEO → manager → senior developer → developer.
  • Shop side: a customer places orders; each order has one or more order items; each item is a product with a quantity. Line amount = quantity × price.

departments

departments · 5 rows
dept_iddept_namelocation
1EngineeringBengaluru
2SalesMumbai
3MarketingPune
4HRDelhi
5FinanceMumbai

Finance (dept 5) has no employees: useful for LEFT JOIN examples.

employees

employees · 12 rows
emp_idfirst_namelast_namedept_idmanager_idjob_titlesalarycommissionhire_datecity
100MeeraKapoorNULLNULLCEO250000NULL2015-01-05Mumbai
101AshaRao1100Engineering Manager125000NULL2018-04-12Bengaluru
102RahulMehta1101Senior Developer98000NULL2019-07-01Bengaluru
103PriyaNair1101QA Engineer72000NULL2021-01-18Pune
104KaranShah1102Developer72000NULL2022-08-29Bengaluru
105VikramSingh2100Sales Manager105000120002017-11-20Mumbai
106NehaGupta2105Sales Executive5400060002022-03-14Mumbai
107ArjunDas2105Sales Executive4800045002023-06-05Kolkata
108KavyaIyer3100Marketing Lead88000NULL2020-09-09Pune
109RohanJoshi3108Content Writer45000NULL2024-01-22Pune
110SnehaKulkarni4100HR Manager78000NULL2019-02-25Delhi
111AmitVerma1101Intern25000NULL2025-06-02Bengaluru

Rows worth noticing:

  • Meera Kapoor (100), the CEO, has no department and no manager (NULL).
  • Priya Nair and Karan Shah both earn 72,000: a tie, which matters for RANK and DENSE_RANK.
  • Only the three Sales staff have a commission; it is NULL for everyone else (NULL Values).

customers

customers · 7 rows
customer_idcustomer_nameemailcityjoined_on
1Ananya Sharmaananya@example.comMumbai2024-01-15
2Rohit Kumarrohit.k@example.comDelhi2024-02-03
3Fatima Khanfatima@example.comHyderabad2024-03-21
4David FernandesNULLGoa2024-05-09
5Ishita Boseishita@example.comKolkata2024-07-30
6Manoj Pillaimanoj.p@example.comChennai2025-01-11
7Sara Thomassara.t@example.comBengaluru2025-03-02

David Fernandes has no email, and Manoj Pillai and Sara Thomas have never ordered.

products

products · 9 rows
product_idproduct_namecategorypricestock
1Wireless MouseElectronics799120
2Mechanical KeyboardElectronics349945
3USB-C HubElectronics19990
4Office ChairFurniture899912
5Standing DeskFurniture189995
6Notebook PackStationery249300
7Gel PensStationery199250
8Desk LampFurniture149930
9Monitor ArmFurniture279918

The USB-C Hub is out of stock (stock = 0), and the Monitor Arm has never been ordered.

orders

orders · 10 rows
order_idcustomer_idorder_datestatus
100112025-01-05Delivered
100222025-01-17Delivered
100312025-02-02Delivered
100432025-02-14Cancelled
100542025-03-08Delivered
100652025-03-22Shipped
100722025-04-10Delivered
100812025-04-28Pending
100932025-05-15Delivered
101052025-06-01Delivered

order_items

order_items · 16 rows
order_idproduct_idquantity
100121
100165
100241
100311
100321
100451
1005610
100574
100632
100681
100711
100842
100882
100921
101063
101072

A first look across the tables

How much did each order cost? This joins three tables. You will learn every part of it in the coming lessons:

SQL
SELECT o.order_id,       c.customer_name,       o.status,       SUM(oi.quantity * p.price) AS order_totalFROM orders oJOIN customers   c  ON c.customer_id = o.customer_idJOIN order_items oi ON oi.order_id   = o.order_idJOIN products    p  ON p.product_id  = oi.product_idGROUP BY o.order_id, c.customer_name, o.statusORDER BY o.order_id;
ResultMySQL 8.4
order_idcustomer_namestatusorder_total
1001Ananya SharmaDelivered4744
1002Rohit KumarDelivered8999
1003Ananya SharmaDelivered4298
1004Fatima KhanCancelled18999
1005David FernandesDelivered3286
1006Ishita BoseShipped5497
1007Rohit KumarDelivered799
1008Ananya SharmaPending20996
1009Fatima KhanDelivered3499
1010Ishita BoseDelivered1145
10 rows in set
How this query works, step by step
  1. FROM orders o starts from the 10 orders (o is a short alias).
  2. The three JOINs attach the customer, the order’s items and each item’s product.
  3. quantity * price is the amount of one line; SUM(...) adds the lines of each order.
  4. GROUP BY makes one result row per order, and ORDER BY sorts by order number.

Create it on your own server

Create the database, select it, then run the script below (copy it into the mysql client, MySQL Workbench or any SQL tool):

MySQL
CREATE DATABASE company;USE company;
company.sql
-- sqatools.in SQL tutorial: the "company" sample database (runs unchanged on MySQL 8.4 and SQLite)CREATE TABLE departments (  dept_id   INT PRIMARY KEY,  dept_name VARCHAR(50) NOT NULL,  location  VARCHAR(50));INSERT INTO departments VALUES(1, 'Engineering', 'Bengaluru'),(2, 'Sales', 'Mumbai'),(3, 'Marketing', 'Pune'),(4, 'HR', 'Delhi'),(5, 'Finance', 'Mumbai'); CREATE TABLE employees (  emp_id     INT PRIMARY KEY,  first_name VARCHAR(30) NOT NULL,  last_name  VARCHAR(30) NOT NULL,  dept_id    INT,  manager_id INT,  job_title  VARCHAR(40),  salary     INT,  commission INT,  hire_date  DATE,  city       VARCHAR(30),  FOREIGN KEY (dept_id) REFERENCES departments (dept_id));INSERT INTO employees VALUES(100, 'Meera', 'Kapoor', NULL, NULL, 'CEO', 250000, NULL, '2015-01-05', 'Mumbai'),(101, 'Asha', 'Rao', 1, 100, 'Engineering Manager', 125000, NULL, '2018-04-12', 'Bengaluru'),(102, 'Rahul', 'Mehta', 1, 101, 'Senior Developer', 98000, NULL, '2019-07-01', 'Bengaluru'),(103, 'Priya', 'Nair', 1, 101, 'QA Engineer', 72000, NULL, '2021-01-18', 'Pune'),(104, 'Karan', 'Shah', 1, 102, 'Developer', 72000, NULL, '2022-08-29', 'Bengaluru'),(105, 'Vikram', 'Singh', 2, 100, 'Sales Manager', 105000, 12000, '2017-11-20', 'Mumbai'),(106, 'Neha', 'Gupta', 2, 105, 'Sales Executive', 54000, 6000, '2022-03-14', 'Mumbai'),(107, 'Arjun', 'Das', 2, 105, 'Sales Executive', 48000, 4500, '2023-06-05', 'Kolkata'),(108, 'Kavya', 'Iyer', 3, 100, 'Marketing Lead', 88000, NULL, '2020-09-09', 'Pune'),(109, 'Rohan', 'Joshi', 3, 108, 'Content Writer', 45000, NULL, '2024-01-22', 'Pune'),(110, 'Sneha', 'Kulkarni', 4, 100, 'HR Manager', 78000, NULL, '2019-02-25', 'Delhi'),(111, 'Amit', 'Verma', 1, 101, 'Intern', 25000, NULL, '2025-06-02', 'Bengaluru'); CREATE TABLE customers (  customer_id   INT PRIMARY KEY,  customer_name VARCHAR(50) NOT NULL,  email         VARCHAR(60),  city          VARCHAR(30),  joined_on     DATE);INSERT INTO customers VALUES(1, 'Ananya Sharma', 'ananya@example.com', 'Mumbai', '2024-01-15'),(2, 'Rohit Kumar', 'rohit.k@example.com', 'Delhi', '2024-02-03'),(3, 'Fatima Khan', 'fatima@example.com', 'Hyderabad', '2024-03-21'),(4, 'David Fernandes', NULL, 'Goa', '2024-05-09'),(5, 'Ishita Bose', 'ishita@example.com', 'Kolkata', '2024-07-30'),(6, 'Manoj Pillai', 'manoj.p@example.com', 'Chennai', '2025-01-11'),(7, 'Sara Thomas', 'sara.t@example.com', 'Bengaluru', '2025-03-02'); CREATE TABLE products (  product_id   INT PRIMARY KEY,  product_name VARCHAR(50) NOT NULL,  category     VARCHAR(30),  price        INT,  stock        INT);INSERT INTO products VALUES(1, 'Wireless Mouse', 'Electronics', 799, 120),(2, 'Mechanical Keyboard', 'Electronics', 3499, 45),(3, 'USB-C Hub', 'Electronics', 1999, 0),(4, 'Office Chair', 'Furniture', 8999, 12),(5, 'Standing Desk', 'Furniture', 18999, 5),(6, 'Notebook Pack', 'Stationery', 249, 300),(7, 'Gel Pens', 'Stationery', 199, 250),(8, 'Desk Lamp', 'Furniture', 1499, 30),(9, 'Monitor Arm', 'Furniture', 2799, 18); CREATE TABLE orders (  order_id    INT PRIMARY KEY,  customer_id INT NOT NULL,  order_date  DATE NOT NULL,  status      VARCHAR(20) NOT NULL,  FOREIGN KEY (customer_id) REFERENCES customers (customer_id));INSERT INTO orders VALUES(1001, 1, '2025-01-05', 'Delivered'),(1002, 2, '2025-01-17', 'Delivered'),(1003, 1, '2025-02-02', 'Delivered'),(1004, 3, '2025-02-14', 'Cancelled'),(1005, 4, '2025-03-08', 'Delivered'),(1006, 5, '2025-03-22', 'Shipped'),(1007, 2, '2025-04-10', 'Delivered'),(1008, 1, '2025-04-28', 'Pending'),(1009, 3, '2025-05-15', 'Delivered'),(1010, 5, '2025-06-01', 'Delivered'); CREATE TABLE order_items (  order_id   INT,  product_id INT,  quantity   INT NOT NULL,  PRIMARY KEY (order_id, product_id),  FOREIGN KEY (order_id) REFERENCES orders (order_id),  FOREIGN KEY (product_id) REFERENCES products (product_id));INSERT INTO order_items VALUES(1001, 2, 1), (1001, 6, 5),(1002, 4, 1),(1003, 2, 1), (1003, 1, 1),(1004, 5, 1),(1005, 6, 10), (1005, 7, 4),(1006, 3, 2), (1006, 8, 1),(1007, 1, 1),(1008, 4, 2), (1008, 8, 2),(1009, 2, 1),(1010, 6, 3), (1010, 7, 2);
Tip

The script uses only standard types (INT, VARCHAR, DATE), so it also runs unchanged on SQLite and needs only small changes for PostgreSQL or SQL Server.

Sample database FAQ

Where can I get a sample database to practise SQL?

Copy the script on this page into MySQL to get the six-table company database used in every lesson of this tutorial. It is small enough to check every result by hand.

Why does the sample data contain NULLs and unmatched rows?

Real data is messy. An employee without a department, customers without orders and a product never sold are exactly the cases that make joins, NULL handling and aggregates behave differently, so every lesson can show them.

Can I change the sample data?

Yes. Run INSERT, UPDATE and DELETE freely; to start over, drop the database and run the script again. The Try it editors in the lessons always start from a fresh copy.

Create Database and Create Table in SQL

SQL Tutorial › Getting Started › Create Database & Table

Create Database and Create Table in SQL

BeginnerLesson 4 of 6912 min read11 queries run on MySQL 8.4

Before you can store data you need a database to hold your tables and a table with the right columns. This lesson creates a school database and a students table from scratch, inspects the result, and shows the shortcuts every developer uses: IF NOT EXISTS and CREATE TABLE ... AS SELECT.

In this lesson
  • Create and select a database with CREATE DATABASE and USE
  • Define a table with columns, data types and a primary key
  • Inspect a table with DESCRIBE and SHOW TABLES
  • Copy query results into a new table with CREATE TABLE AS SELECT

CREATE DATABASE

A database (MySQL also calls it a schema) is a named container for tables. Create one and check that it exists:

MySQL
CREATE DATABASE school;SHOW DATABASES;
ResultMySQL 8.4
mysql> CREATE DATABASE school
Query OK, 1 row affected
mysql> SHOW DATABASES
Database
information_schema
mysql
performance_schema
school
sys
5 rows in set
How this query works, step by step
  1. CREATE DATABASE school creates an empty database called school.
  2. MySQL answers Query OK, 1 row affected: it reports one database created.
  3. SHOW DATABASES lists it next to the four system databases that every server has.

Running it twice fails, because the name is taken:

MySQL
CREATE DATABASE school;CREATE DATABASE school;
ResultMySQL 8.4
mysql> CREATE DATABASE school
Query OK, 1 row affected
mysql> CREATE DATABASE school
ERROR 1007 (HY000): Can't create database 'school'; database exists

Add IF NOT EXISTS to make the statement safe to re-run. MySQL then skips it with a warning instead of an error:

MySQL
CREATE DATABASE school;CREATE DATABASE IF NOT EXISTS school;
ResultMySQL 8.4
mysql> CREATE DATABASE school
Query OK, 1 row affected
mysql> CREATE DATABASE IF NOT EXISTS school
Query OK, 1 row affected, 1 warning
Tip

Set the character set when you create a database that stores names or text in several languages: CREATE DATABASE school CHARACTER SET utf8mb4; In MySQL 8.4 utf8mb4 is already the default.

USE: select the database

Tables always live inside a database. USE makes one the current database, so you can write students instead of school.students:

MySQL
CREATE DATABASE school;USE school;SELECT DATABASE() AS current_db;
ResultMySQL 8.4
mysql> CREATE DATABASE school
Query OK, 1 row affected
mysql> USE school
Query OK, 0 rows affected
mysql> SELECT DATABASE() AS current_db
current_db
school
1 row in set
How this query works, step by step
  1. USE school switches the session to the school database.
  2. DATABASE() returns the name of the current database, so the result is school.

CREATE TABLE syntax

MySQL
CREATE TABLE table_name (  column1 datatype [constraints],  column2 datatype [constraints],  ...  [table constraints]);

Each column needs a name and a data type (see SQL Data Types). Constraints such as NOT NULL and PRIMARY KEY add rules the data must follow.

Create a table

MySQL
CREATE DATABASE school;USE school;CREATE TABLE students (  student_id INT PRIMARY KEY,  full_name  VARCHAR(60) NOT NULL,  email      VARCHAR(100) UNIQUE,  birth_date DATE,  grade      INT DEFAULT 1);DESCRIBE students;
ResultMySQL 8.4
mysql> CREATE DATABASE school
Query OK, 1 row affected
mysql> USE school
Query OK, 0 rows affected
mysql> CREATE TABLE students (
Query OK, 0 rows affected
mysql> DESCRIBE students
FieldTypeNullKeyDefaultExtra
student_idintNOPRINULL
full_namevarchar(60)NONULL
emailvarchar(100)YESUNINULL
birth_datedateYESNULL
gradeintYES1
5 rows in set
How this query works, step by step
  1. CREATE TABLE students (...) creates the table with five columns.
  2. student_id INT PRIMARY KEY: a whole number that must be unique and not empty. It identifies each student.
  3. full_name VARCHAR(60) NOT NULL: text up to 60 characters that must always be filled.
  4. email VARCHAR(100) UNIQUE: two students cannot share an email.
  5. birth_date DATE stores a calendar date, and grade INT DEFAULT 1 gets the value 1 when an insert leaves it out.
  6. DESCRIBE students shows each column’s type, whether it accepts NULL (Null), its key (PRI = primary key, UNI = unique) and its default.

Insert rows and read them back

A new table is empty. Add three rows with INSERT and read them:

SQL
CREATE TABLE students (  student_id INT PRIMARY KEY,  full_name  VARCHAR(60) NOT NULL,  grade      INT DEFAULT 1);INSERT INTO students (student_id, full_name, grade) VALUES  (1, 'Aarav Patel', 3),  (2, 'Diya Menon', 2);INSERT INTO students (student_id, full_name) VALUES (3, 'Kabir Ali');SELECT * FROM students;
ResultMySQL 8.4
mysql> CREATE TABLE students (
Query OK, 0 rows affected
mysql> INSERT INTO students (student_id, full_name, grade) VALUES
Query OK, 2 rows affected
mysql> INSERT INTO students (student_id, full_name) VALUES (3, 'Kabir Ali')
Query OK, 1 row affected
mysql> SELECT * FROM students
student_idfull_namegrade
1Aarav Patel3
2Diya Menon2
3Kabir Ali1
3 rows in set
How this query works, step by step
  1. CREATE TABLE makes an empty students table.
  2. The first INSERT adds two rows in one statement: 2 rows affected.
  3. The second INSERT leaves out grade, so Kabir gets the default value 1.
  4. SELECT * shows all three rows.

CREATE TABLE IF NOT EXISTS

Creating a table that already exists is an error:

MySQL
CREATE TABLE departments (dept_id INT);
ResultMySQL 8.4
ERROR 1050 (42S01): Table 'departments' already exists

Setup scripts use IF NOT EXISTS so they can run again safely. The existing table is left untouched, and MySQL raises a note you can read with SHOW WARNINGS:

MySQL
CREATE TABLE IF NOT EXISTS departments (dept_id INT);SHOW WARNINGS;
ResultMySQL 8.4
mysql> CREATE TABLE IF NOT EXISTS departments (dept_id INT)
Query OK, 0 rows affected, 1 warning
mysql> SHOW WARNINGS
LevelCodeMessage
Note1050Table 'departments' already exists
1 row in set

SHOW TABLES and DESCRIBE

MySQL
SHOW TABLES;
ResultMySQL 8.4
Tables_in_company
customers
departments
employees
order_items
orders
products
6 rows in set
MySQL
DESCRIBE products;
ResultMySQL 8.4
FieldTypeNullKeyDefaultExtra
product_idintNOPRINULL
product_namevarchar(50)NONULL
categoryvarchar(30)YESNULL
priceintYESNULL
stockintYESNULL
5 rows in set
How this query works, step by step
  1. SHOW TABLES lists every table in the current database (company).
  2. DESCRIBE products (or DESC products) shows the columns of one table. product_id is the primary key, and product_name does not allow NULL.

CREATE TABLE AS SELECT

CREATE TABLE ... AS SELECT creates a new table and fills it with the result of a query. It is a quick way to save a report or make a backup copy before you change data:

MySQL
CREATE TABLE high_earners ASSELECT emp_id, first_name, salaryFROM employeesWHERE salary >= 100000; SELECT * FROM high_earners;
ResultMySQL 8.4
mysql> CREATE TABLE high_earners AS
Query OK, 3 rows affected
mysql> SELECT * FROM high_earners
emp_idfirst_namesalary
100Meera250000
101Asha125000
105Vikram105000
3 rows in set
How this query works, step by step
  1. The SELECT finds employees earning 100,000 or more: three rows.
  2. CREATE TABLE high_earners AS creates a table with the same three columns and copies those rows in.
  3. The new table is independent: later changes to employees do not change it.
Watch out

CREATE TABLE ... AS SELECT copies columns and data, but not the primary key, indexes or foreign keys. To copy the structure exactly, use CREATE TABLE new_table LIKE old_table; and then INSERT INTO new_table SELECT * FROM old_table;.

Exercise: Create a courses table

Create a table courses with course_id (whole number, primary key), title (text up to 80 characters, required) and fee (whole number, default 0). Insert two courses, one without a fee, and show all rows.

Expected resultMySQL 8.4
mysql> CREATE TABLE courses (
Query OK, 0 rows affected
mysql> INSERT INTO courses (course_id, title, fee) VALUES (1, 'SQL for Tester…
Query OK, 1 row affected
mysql> INSERT INTO courses (course_id, title) VALUES (2, 'Intro to Databases')
Query OK, 1 row affected
mysql> SELECT * FROM courses
course_idtitlefee
1SQL for Testers4999
2Intro to Databases0
2 rows in set
Hint 1

Use INT PRIMARY KEY, VARCHAR(80) NOT NULL and INT DEFAULT 0.

Hint 2

Leave fee out of the column list in the second insert.

Quick quiz

1. What does USE school; do?

2. A column is defined as grade INT DEFAULT 1. What does an insert that leaves out grade store?

3. Which statement copies a query result into a new table?

4. What does IF NOT EXISTS change?

CREATE TABLE FAQ

What is the difference between a database and a schema in MySQL?

In MySQL they are the same thing: CREATE SCHEMA is a synonym for CREATE DATABASE. In PostgreSQL and SQL Server a schema is a namespace inside a database.

How do I see the structure of a table in MySQL?

Use DESCRIBE table_name; for a column list, or SHOW CREATE TABLE table_name; for the full CREATE TABLE statement including indexes and foreign keys.

How do I copy a table in SQL?

CREATE TABLE copy AS SELECT * FROM original; copies columns and data. CREATE TABLE copy LIKE original; copies the exact structure with indexes but no rows.

How do I create a table only if it does not exist?

Write CREATE TABLE IF NOT EXISTS table_name (...). If the table exists, MySQL leaves it unchanged and issues a note instead of an error.

Install MySQL 8.4 and Run Your First Query

SQL Tutorial › Getting Started › Install MySQL

Install MySQL 8.4 and Run Your First Query

BeginnerLesson 2 of 6912 min read3 queries run on MySQL 8.4

You can follow most of this tutorial in the browser with the Try it buttons, but every SQL developer needs a real database server. This lesson shows the quickest way to install MySQL 8.4 LTS on Windows, macOS, Linux or Docker, how to connect to it, and how to load the sample company database used in every lesson.

In this lesson
  • Choose the right MySQL version and package
  • Install MySQL on Windows, macOS, Linux or with Docker
  • Connect with the mysql command-line client and MySQL Workbench
  • Create the sample database and run a first query

Which MySQL version?

Install MySQL 8.4 LTS (Long-Term Support). LTS releases get bug and security fixes for years without changing behaviour, which is what you want for learning and for production. The “Innovation” releases (9.x) add new features faster but are supported only until the next release. All queries in this tutorial were run on MySQL 8.4.

Note

MySQL Community Server is free. Download it only from the official site, dev.mysql.com/downloads, or from your operating system’s package manager.

Install on Windows

  1. Download the MSI installer for MySQL Community Server 8.4 (Windows x86, 64-bit) from dev.mysql.com/downloads/mysql/.
  2. Run it and choose the Typical setup.
  3. At the end, the MySQL Configurator opens. Keep the defaults (Development Computer, port 3306), set a strong root password, and let it install MySQL as a Windows service.
  4. Optionally install MySQL Workbench (a graphical editor) from dev.mysql.com/downloads/workbench/.

Check the install from a new Command Prompt:

Command Prompt
mysql --versionmysql -u root -p
Tip

If Windows says mysql is not recognized, add C:\Program Files\MySQL\MySQL Server 8.4\bin to your PATH, or open the MySQL 8.4 Command Line Client from the Start menu.

Install on macOS

Either download the DMG archive for macOS from dev.mysql.com and run the installer, or use Homebrew:

Terminal
brew install mysql@8.4brew services start mysql@8.4mysql_secure_installation

mysql_secure_installation sets the root password and removes the test database and anonymous users.

Install on Linux (Ubuntu / Debian)

Terminal
sudo apt updatesudo apt install mysql-serversudo systemctl status mysqlsudo mysql

Ubuntu’s own package may be an older 8.0 release. For 8.4, first add the official MySQL APT repository (the mysql-apt-config package from dev.mysql.com) and choose mysql-8.4-lts when it asks.

Run MySQL in Docker

If you already use Docker, this is the fastest clean install, and it is easy to throw away:

Terminal
docker run --name mysql84 -e MYSQL_ROOT_PASSWORD=Learn_SQL_2026 -p 3306:3306 -d mysql:8.4docker exec -it mysql84 mysql -u root -p

Connect and check the server

Once you are at the mysql> prompt (or in a Workbench query tab), ask the server which version it runs:

MySQL
SELECT VERSION();
ResultMySQL 8.4
VERSION()
8.4.13
1 row in set
How this query works, step by step
  1. VERSION() is a built-in function that returns the server version as text.
  2. There is no FROM, so MySQL evaluates the function once and returns one row.

List the databases on the server:

MySQL
SHOW DATABASES;
ResultMySQL 8.4
Database
company
information_schema
mysql
performance_schema
sys
5 rows in set
How this query works, step by step
  1. SHOW DATABASES lists every database your account can see.
  2. information_schema, mysql, performance_schema and sys are system databases. Do not create tables in them.
  3. company is the sample database used in this tutorial (you create it in the next section).

Load the sample database

Every lesson uses the same small company database: departments, employees, customers, products, orders and order items. The Sample Database lesson lists all its rows and has the full script to copy. In short:

MySQL
CREATE DATABASE company;USE company;-- then paste the CREATE TABLE and INSERT statements from the Sample Database lesson

Check that it worked:

SQL
SELECT COUNT(*) AS employees FROM employees;
ResultMySQL 8.4
employees
12
1 row in set
How this query works, step by step
  1. COUNT(*) counts rows. You will learn it in Aggregate Functions.
  2. The sample database has 12 employees, so the result is one row with the value 12.

mysql client commands you will use

CommandWhat it does
mysql -u root -pConnect as root; it asks for the password
SHOW DATABASES;List databases
USE company;Make company the current database
SHOW TABLES;List tables in the current database
DESCRIBE employees;Show a table’s columns and types
SOURCE C:/path/company.sqlRun every statement in a script file
exitLeave the client

Quick quiz

1. Which MySQL release type should a beginner install?

2. What does USE company; do?

3. Which of these is a system database you should not put your tables in?

Installation FAQ

Is MySQL free?

Yes. MySQL Community Server is open source and free for learning and most commercial use. Oracle also sells an Enterprise edition with extra tools and support.

What is the default MySQL port?
  1. If another program already uses it, choose a different port during setup and connect with mysql -u root -p -P 3307.
I forgot the MySQL root password. What now?

Stop the server, start it once with the --skip-grant-tables option, connect without a password and set a new one with ALTER USER 'root'@'localhost' IDENTIFIED BY 'new password';, then restart normally. On a fresh learning install it is often quicker to reinstall.

Should I use MySQL Workbench or the command line?

Learn both. Workbench is comfortable for writing and saving queries and shows results in a grid. The command-line client is available on every server and is what you will use over SSH.

Can I learn SQL without installing anything?

Yes. Every lesson here has Try it buttons that run the queries in your browser. Install MySQL when you want to build your own databases.

SQL Syntax and Comments

SQL Tutorial › Getting Started › SQL Syntax & Comments

SQL Syntax and Comments

BeginnerLesson 3 of 699 min read9 queries run on MySQL 8.4

SQL has a small set of syntax rules, and once you know them you can read almost any query. This lesson covers how a statement is built, when case matters, how to quote text and names, and how to write comments, with every rule shown on a real MySQL 8.4 query.

In this lesson
  • The parts of a SQL statement: keywords, identifiers, literals and clauses
  • When SQL is and is not case sensitive
  • Single quotes for text, backticks for names
  • Single-line and multi-line comments

Anatomy of a SQL statement

SQL
SELECT first_name, salaryFROM employeesWHERE dept_id = 2;
ResultMySQL 8.4
first_namesalary
Vikram105000
Neha54000
Arjun48000
3 rows in set
PartExampleMeaning
KeywordsSELECT, FROM, WHEREReserved words with a fixed meaning
Identifiersfirst_name, employeesNames of columns, tables, databases
Literals2, 'Mumbai'Fixed values: numbers, text, dates
Operators=, >, ANDCompare or combine values
ClausesWHERE dept_id = 2A keyword plus what follows it
Terminator;Ends the statement
How this query works, step by step
  1. FROM employees names the table.
  2. WHERE dept_id = 2 keeps the Sales department rows (dept 2 is Sales).
  3. SELECT first_name, salary shows two columns: three rows come back.

Rule 1: keywords are not case sensitive

SELECT, select and SeLeCt all work. Writing keywords in UPPER CASE and names in lower case is the usual style because it makes queries easier to scan:

SQL
select first_name from employees where emp_id = 101;
ResultMySQL 8.4
first_name
Asha
1 row in set

Text comparisons are also case insensitive by default in MySQL, because the default collation (utf8mb4_0900_ai_ci) ends in ci, meaning case-insensitive:

MySQL
SELECT first_name FROM employees WHERE city = 'PUNE';
ResultMySQL 8.4
first_name
Priya
Kavya
Rohan
3 rows in set
Note

Other databases differ. In PostgreSQL 'PUNE' = 'Pune' is false. Table names are case sensitive in MySQL on Linux but not on Windows, so always write names exactly as created.

Rule 2: whitespace and line breaks do not matter

You may put a whole query on one line or spread it over many. Spread long queries one clause per line, so they are easy to read and to compare in code reviews:

SQL
SELECT   first_name,         job_titleFROM     employeesWHERE    salary >= 100000;
ResultMySQL 8.4
first_namejob_title
MeeraCEO
AshaEngineering Manager
VikramSales Manager
3 rows in set

Rule 3: end statements with a semicolon

The semicolon separates statements. In the mysql client a statement does not run until you type ;. When you send several statements at once, each needs one:

SQL
SELECT COUNT(*) AS departments FROM departments;SELECT COUNT(*) AS customers FROM customers;
ResultMySQL 8.4
mysql> SELECT COUNT(*) AS departments FROM departments
departments
5
1 row in set
mysql> SELECT COUNT(*) AS customers FROM customers
customers
7
1 row in set

Rule 4: quotes

QuoteUse forExample
Single quotesText and date valuesWHERE city = 'Pune'
Backticks (the key left of 1)Names with spaces or reserved wordssee the example below
Double quotesText in MySQL (names in standard SQL)avoid; use single quotes

To put a single quote inside text, double it:

SQL
SELECT 'It''s a test' AS message, 'O''Brien' AS surname;
ResultMySQL 8.4
messagesurname
It's a testO'Brien
1 row in set
How this query works, step by step
  1. Inside single-quoted text, '' (two single quotes) stands for one '.
  2. MySQL returns the text with one quote, as the result shows.

Names normally need no quotes. Backticks are only needed when a name is a reserved word or contains spaces or special characters:

SQL
SELECT dept_name AS `Department Name`, location AS `where`FROM departmentsWHERE dept_id <= 2;
ResultMySQL 8.4
Department Namewhere
EngineeringBengaluru
SalesMumbai
2 rows in set

Rule 5: numbers, text and NULL

MySQL
SELECT 42 AS whole_number, 3.75 AS decimal_number, 'hello' AS text_value, '2026-01-31' AS date_text, NULL AS nothing;
ResultMySQL 8.4
whole_numberdecimal_numbertext_valuedate_textnothing
423.75hello2026-01-31NULL
1 row in set
  • Numbers are written without quotes.
  • Text and dates use single quotes. MySQL dates are written 'YYYY-MM-DD'.
  • NULL means “no value” and has its own lesson: NULL Values.

Comments

Comments are ignored by the database. Use them to explain why a query does something.

SQL
-- single-line comment: everything after "-- " is ignoredSELECT first_name,      -- the employee's first name       salary           /* monthly salary in rupees */FROM employees/* a multi-line comment   can span several lines */WHERE emp_id IN (101, 102);
ResultMySQL 8.4
first_namesalary
Asha125000
Rahul98000
2 rows in set
How this query works, step by step
  1. -- followed by a space starts a comment that runs to the end of the line. MySQL requires the space after the two dashes.
  2. /* ... */ can sit inside a line or span many lines.
  3. MySQL also accepts # as a single-line comment, but -- works in every database, so prefer it.
Tip

Commenting out a line is a quick way to test a query without one condition: put -- in front of the WHERE line and run it again.

Naming conventions

  • Use lower case with underscores: order_items, hire_date.
  • Make names describe the data: salary, not s1.
  • Pick singular or plural table names and stay consistent. This tutorial uses plural (employees).
  • Avoid reserved words like order, group or select as names; you would have to quote them everywhere.

Quick quiz

1. Which quotes should you use for a text value in MySQL?

2. Is select * from employees; valid?

3. How do you write the text It’s in SQL?

4. What must follow -- to start a comment in MySQL?

SQL syntax FAQ

Is SQL case sensitive?

SQL keywords are not case sensitive. In MySQL, text comparisons are case insensitive with the default collation, and table names are case sensitive on Linux but not on Windows.

Is the semicolon required in SQL?

It is required to separate statements and to run a statement in the mysql command-line client. A single statement sent by a tool or program usually works without it, but adding it is good practice.

How do I write comments in SQL?

Use -- for a single-line comment and /* ... */ for a comment that spans several lines. MySQL also supports #.

What is the difference between single and double quotes in SQL?

Standard SQL uses single quotes for text and double quotes for identifiers. MySQL accepts double quotes for text by default, but single quotes work everywhere, so use them for text and backticks for names that need quoting.