Basic SQL
This chapter covers the fundamental building blocks of every SQL query you will ever write. By the end you will be able to retrieve exactly the data you need, shape it with expressions and aliases, sort it, remove duplicates, and limit output to a manageable number of rows.
We will use this employees table for all examples:
| emp_id | name | dept | salary | hire_date |
|---|---|---|---|---|
| 1 | Alice | HR | 60000 | 2020-01-15 |
| 2 | Bob | IT | 85000 | 2019-03-10 |
| 3 | Carol | IT | 92000 | 2018-07-22 |
| 4 | David | HR | 55000 | 2021-09-01 |
| 5 | Eve | Finance | 78000 | 2020-11-30 |
| 6 | Frank | IT | 95000 | 2017-05-14 |
Comments in SQL
Before writing queries, it is important to know how to add comments โ notes for yourself and other developers that are ignored by the database.
-- This is a single-line comment. Everything after -- is ignored.
SELECT name, salary -- you can put comments at the end of a line too
FROM employees;
/*
This is a multi-line comment.
Useful for longer explanations
or for temporarily disabling a block of SQL.
*/
SELECT *
FROM employees;
Use comments to explain why a query does something, not just what it does. Future you will be grateful.
The SELECT Statement โ Retrieving Data
SELECT is the command that retrieves data from a table. It is the core of DQL (Data Query Language) and the command you will write most often.
Selecting All Columns with *
The asterisk * means "give me every column".
SELECT * FROM employees;
Result:
| emp_id | name | dept | salary | hire_date |
|---|---|---|---|---|
| 1 | Alice | HR | 60000 | 2020-01-15 |
| 2 | Bob | IT | 85000 | 2019-03-10 |
| 3 | Carol | IT | 92000 | 2018-07-22 |
| 4 | David | HR | 55000 | 2021-09-01 |
| 5 | Eve | Finance | 78000 | 2020-11-30 |
| 6 | Frank | IT | 95000 | 2017-05-14 |
SELECT * in production queries. Always list only the columns you actually need. It reduces data transfer, makes the query's intent clear, and prevents bugs when someone adds or reorders columns in the table.
Selecting Specific Columns
List the column names separated by commas:
SELECT name, salary
FROM employees;
Result:
| name | salary |
|---|---|
| Alice | 60000 |
| Bob | 85000 |
| Carol | 92000 |
| David | 55000 |
| Eve | 78000 |
| Frank | 95000 |
Column Aliases with AS
An alias renames a column in the output. It does not change the actual table โ it only affects what the column header looks like in the result set.
SELECT name AS employee_name,
salary AS annual_salary,
hire_date AS "Date Hired" -- use quotes for aliases with spaces
FROM employees;
Result:
| employee_name | annual_salary | Date Hired |
|---|---|---|
| Alice | 60000 | 2020-01-15 |
| Bob | 85000 | 2019-03-10 |
The AS keyword is optional in most databases โ SELECT name employee_name works too. However, always write AS for clarity.
"Date Hired" and "date hired" are different.
The FROM Clause
FROM tells SQL which table (or tables) to read data from. Every SELECT query needs a FROM clause (except in some databases that allow SELECT without FROM for expressions).
SELECT name
FROM employees; -- read from the employees table
Table Aliases
Just like columns, tables can have aliases. This is especially useful when:
- The table name is long
- You are joining multiple tables
- You need to self-join a table (a table joined to itself)
-- Give the employees table a short alias 'e'
SELECT e.name, e.salary
FROM employees e;
-- Using AS keyword (both styles are valid)
SELECT e.name, e.salary
FROM employees AS e;
When you use a table alias, you can prefix column names with the alias to make clear which table they come from: e.name means "the name column from the table aliased as e".
Arithmetic in SELECT
You can perform calculations directly in the SELECT clause. The result appears as a new column in the output.
SELECT name,
salary,
salary * 12 AS annual_salary, -- yearly total
salary * 0.10 AS bonus_estimate, -- 10% bonus
(salary + 5000) * 12 AS new_annual_salary, -- with raise
ROUND(salary / 22, 2) AS daily_rate -- approx daily rate
FROM employees;
Result (showing key columns):
| name | salary | annual_salary | bonus_estimate |
|---|---|---|---|
| Alice | 60000 | 720000 | 6000.00 |
| Bob | 85000 | 1020000 | 8500.00 |
| Frank | 95000 | 1140000 | 9500.00 |
Common arithmetic operators:
| Operator | Meaning | Example |
|---|---|---|
+ |
Addition | salary + 5000 |
- |
Subtraction | salary - tax |
* |
Multiplication | price * quantity |
/ |
Division | total / count |
% or MOD() |
Modulo (remainder) | 17 % 5 โ 2 |
MOD(17, 5) for modulo โ Oracle does not support the % operator. In PostgreSQL and MySQL, % works directly.
String Concatenation
To combine text values, use the concatenation operator.
-- Standard SQL / Oracle / PostgreSQL: use ||
SELECT name || ' works in ' || dept AS description
FROM employees;
-- MySQL / SQL Server: use CONCAT()
SELECT CONCAT(name, ' works in ', dept) AS description
FROM employees;
-- PostgreSQL also supports CONCAT()
SELECT CONCAT(name, ' (', dept, ')') AS name_dept
FROM employees;
Result:
| description |
|---|
| Alice works in HR |
| Bob works in IT |
| Carol works in IT |
-- Practical example: build a display label
SELECT emp_id,
name || ' - ' || dept AS display_label,
'Salary: $' || salary AS salary_label
FROM employees;
Useful Scalar Functions in SELECT
These functions transform a single value and are used directly in SELECT:
SELECT name,
UPPER(name) AS name_upper, -- 'alice' โ 'ALICE'
LOWER(name) AS name_lower, -- 'ALICE' โ 'alice'
LENGTH(name) AS name_length, -- number of characters
SUBSTR(name, 1, 3) AS first_3_chars, -- first 3 characters
TRIM(name) AS name_trimmed, -- remove leading/trailing spaces
ROUND(salary, -3) AS rounded_salary, -- round to nearest 1000
ABS(-500) AS absolute_val -- absolute value
FROM employees;
SUBSTR() while MySQL and PostgreSQL also support SUBSTRING(). Both do the same thing. Always check your database's function reference for exact names.
The WHERE Clause โ Filtering Rows
WHERE limits the output to only rows that match a condition. Only rows where the condition evaluates to TRUE are included in the result.
-- Only employees in the IT department
SELECT name, dept, salary
FROM employees
WHERE dept = 'IT';
Result:
| name | dept | salary |
|---|---|---|
| Bob | IT | 85000 |
| Carol | IT | 92000 |
| Frank | IT | 95000 |
Comparison Operators
These operators build the conditions used in WHERE:
| Operator | Meaning | Example |
|---|---|---|
= |
Equal to | dept = 'IT' |
<> or != |
Not equal to | dept <> 'HR' |
> |
Greater than | salary > 80000 |
< |
Less than | salary < 60000 |
>= |
Greater than or equal | salary >= 85000 |
<= |
Less than or equal | salary <= 70000 |
-- Salary exactly 85000
SELECT name FROM employees WHERE salary = 85000;
-- Not in HR department
SELECT name, dept FROM employees WHERE dept <> 'HR';
-- Hired on or after 2020
SELECT name, hire_date FROM employees WHERE hire_date >= '2020-01-01';
-- Salary between 70000 and 90000 (using basic operators)
SELECT name, salary FROM employees WHERE salary >= 70000 AND salary <= 90000;
String Comparisons
String values in WHERE must use single quotes. SQL is case-sensitive for string comparisons depending on the database collation.
-- Correct: single quotes
WHERE dept = 'IT'
-- Wrong: double quotes are for identifiers (column/table names), not values
WHERE dept = "IT" -- This will ERROR in most databases
-- Case-insensitive search using UPPER()
WHERE UPPER(dept) = 'IT' -- matches 'IT', 'it', 'It'
ORDER BY โ Sorting Results
ORDER BY sorts the output. Without ORDER BY, the database makes no guarantee about the order of rows returned.
-- Sort by salary, highest first (DESC = descending)
SELECT name, salary
FROM employees
ORDER BY salary DESC;
Result:
| name | salary |
|---|---|
| Frank | 95000 |
| Carol | 92000 |
| Bob | 85000 |
| Eve | 78000 |
| Alice | 60000 |
| David | 55000 |
-- Sort alphabetically by name (ASC = ascending, this is the default)
SELECT name, dept, salary
FROM employees
ORDER BY name ASC;
-- You can omit ASC since it is the default
ORDER BY name
-- Sort by department first, then by salary descending within each department
SELECT name, dept, salary
FROM employees
ORDER BY dept ASC, salary DESC;
Result of multi-column ORDER BY:
| name | dept | salary |
|---|---|---|
| Eve | Finance | 78000 |
| Alice | HR | 60000 |
| David | HR | 55000 |
| Frank | IT | 95000 |
| Carol | IT | 92000 |
| Bob | IT | 85000 |
NULL Ordering
When a column contains NULL values, their sort position depends on the database. You can control it explicitly:
-- Oracle / PostgreSQL: put NULLs at the end
SELECT name, manager_id
FROM employees
ORDER BY manager_id ASC NULLS LAST;
-- Put NULLs at the beginning
ORDER BY manager_id DESC NULLS FIRST;
Ordering by Column Position (Avoid in Production)
You can reference a column by its position number in the SELECT list:
-- Order by the 2nd column in SELECT (salary)
SELECT name, salary FROM employees ORDER BY 2 DESC;
This is fragile โ if you reorder the SELECT columns, the sort changes silently. Use column names instead.
DISTINCT โ Removing Duplicates
DISTINCT removes duplicate rows from the result. It applies to the combination of all listed columns.
-- See all unique departments (no duplicates)
SELECT DISTINCT dept
FROM employees;
Result:
| dept |
|---|
| HR |
| IT |
| Finance |
-- Without DISTINCT: shows 6 rows with repeats
SELECT dept FROM employees;
-- Result: HR, IT, IT, HR, Finance, IT (6 rows, IT and HR repeated)
-- With DISTINCT on two columns: unique (dept, salary) combinations
SELECT DISTINCT dept, salary
FROM employees
ORDER BY dept, salary;
DISTINCT applies to the entire row across all listed columns, not just the first one. SELECT DISTINCT dept, salary gives you unique (dept, salary) pairs, not just unique depts.
COUNT DISTINCT
A very common pattern is counting how many unique values exist:
-- How many different departments are there?
SELECT COUNT(DISTINCT dept) AS num_departments
FROM employees;
-- Result: 3
LIMIT / FETCH FIRST โ Restricting Row Count
Often you only want the top N rows โ the top 10 customers, the 5 most recent orders, etc.
Different databases have slightly different syntax:
-- MySQL / PostgreSQL / SQLite โ LIMIT
SELECT name, salary
FROM employees
ORDER BY salary DESC
LIMIT 3; -- only return the top 3
-- Oracle 12c+ / SQL Server 2012+ โ FETCH FIRST
SELECT name, salary
FROM employees
ORDER BY salary DESC
FETCH FIRST 3 ROWS ONLY;
-- SQL Server โ TOP (put it right after SELECT)
SELECT TOP 3 name, salary
FROM employees
ORDER BY salary DESC;
-- Oracle pre-12c โ use ROWNUM in a subquery
SELECT name, salary
FROM (
SELECT name, salary FROM employees ORDER BY salary DESC
)
WHERE ROWNUM <= 3;
Result of all the above (top 3 earners):
| name | salary |
|---|---|
| Frank | 95000 |
| Carol | 92000 |
| Bob | 85000 |
Pagination โ Skipping Rows
Pagination means showing "page 2", "page 3", etc. โ skipping some rows and showing the next batch.
-- MySQL / PostgreSQL: LIMIT + OFFSET
-- Skip first 3 rows, then return 3 (rows 4, 5, 6 = "page 2")
SELECT name, salary
FROM employees
ORDER BY emp_id
LIMIT 3 OFFSET 3;
-- Oracle / SQL Server: OFFSET ... FETCH NEXT
SELECT name, salary
FROM employees
ORDER BY emp_id
OFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY;
CASE Expression โ Conditional Logic in SELECT
The CASE expression lets you add if/then/else logic inside a query. It comes in two forms.
Simple CASE (compare one value to many)
-- Translate department codes into readable names
SELECT name,
CASE dept
WHEN 'HR' THEN 'Human Resources'
WHEN 'IT' THEN 'Information Technology'
WHEN 'Finance' THEN 'Finance Department'
ELSE 'Other'
END AS dept_full_name
FROM employees;
Searched CASE (evaluate different conditions)
-- Categorise employees by salary range
SELECT name,
salary,
CASE
WHEN salary >= 90000 THEN 'Senior'
WHEN salary >= 70000 THEN 'Mid-level'
WHEN salary >= 50000 THEN 'Junior'
ELSE 'Entry-level'
END AS seniority_band
FROM employees;
Result:
| name | salary | seniority_band |
|---|---|---|
| Alice | 60000 | Junior |
| Bob | 85000 | Mid-level |
| Carol | 92000 | Senior |
| David | 55000 | Junior |
| Eve | 78000 | Mid-level |
| Frank | 95000 | Senior |
-- CASE can also be used in ORDER BY to control custom sort order
SELECT name, dept
FROM employees
ORDER BY
CASE dept
WHEN 'IT' THEN 1 -- IT first
WHEN 'Finance' THEN 2
WHEN 'HR' THEN 3
ELSE 4
END;
The Mandatory Clause Order Rule
This is one of the most important rules to memorise. SQL clauses must appear in this exact order. The database will throw a syntax error if you put them in a different sequence:
SELECT ... -- 1st: which columns
FROM ... -- 2nd: which table
WHERE ... -- 3rd: filter rows (optional)
GROUP BY ... -- 4th: group rows (optional)
HAVING ... -- 5th: filter groups (optional)
ORDER BY ... -- 6th: sort result (optional)
LIMIT ... -- 7th: restrict row count (optional)
-- WRONG โ WHERE comes after ORDER BY (will error)
SELECT name FROM employees
ORDER BY salary
WHERE dept = 'IT';
-- CORRECT โ WHERE before ORDER BY
SELECT name FROM employees
WHERE dept = 'IT'
ORDER BY salary;
SQL Formatting Best Practices
Well-formatted SQL is much easier to read and debug. Here are the widely-accepted conventions:
-- BAD: everything on one line, hard to scan
select name,salary,dept from employees where salary>70000 order by salary desc;
-- GOOD: readable formatting
SELECT name,
salary,
dept
FROM employees
WHERE salary > 70000
ORDER BY salary DESC;
Formatting rules to follow:
- Write SQL keywords in UPPERCASE (
SELECT,FROM,WHERE, etc.) - Write table and column names in lowercase
- Put each major clause on its own line
- Indent column lists and conditions with consistent spacing
- Align the keywords vertically (many developers align
FROM,WHERE,ORDER BYwith SELECT) - Always end a statement with a semicolon
; - Use one clause per line for readability
-- Well-formatted example
SELECT e.name AS employee_name,
e.dept,
e.salary,
e.salary * 12 AS annual_salary
FROM employees e
WHERE e.dept IN ('IT', 'Finance')
AND e.salary > 70000
ORDER BY e.dept ASC,
e.salary DESC
LIMIT 10;
Putting It All Together โ Complete Examples
-- Example 1: Top-earning IT employees with formatted output
SELECT name AS employee,
salary,
salary * 12 AS annual_salary,
'IT Department' AS team
FROM employees
WHERE dept = 'IT'
ORDER BY salary DESC
FETCH FIRST 2 ROWS ONLY;
-- Example 2: Salary category for all employees
SELECT name,
dept,
salary,
CASE
WHEN salary >= 90000 THEN 'Top earner'
WHEN salary >= 70000 THEN 'Mid earner'
ELSE 'Below average'
END AS category
FROM employees
ORDER BY salary DESC;
-- Example 3: Unique departments with employee count (preview of aggregates)
SELECT DISTINCT dept
FROM employees
ORDER BY dept;
Common Errors
| Error | Cause | Fix |
|---|---|---|
| ORA-00942 | Table or view does not exist in the current schema | Prefix with schema name (HR.EMPLOYEES) or check USER_TABLES |
| ORA-00904 | Column name invalid โ misspelled or not in the table | Run DESCRIBE employees to list valid column names |
| ORA-00923 | FROM keyword not found where expected โ keyword missing or misplaced | Check for a missing comma between select-list items or a misspelled keyword |
| ORA-00933 | SQL command not properly ended โ extra keyword or punctuation after the statement | Remove trailing semicolons in JDBC/OCI; check for stray keywords after ORDER BY |
| ORA-01422 | Exact fetch returns more than one row โ used in a context expecting a scalar result | Add a WHERE clause to select a single row, or use aggregate/FETCH FIRST 1 ROW |
| ORA-00936 | Missing expression โ empty SELECT list or a trailing comma | Ensure every comma in the SELECT list is followed by a valid expression |
Interview Corner
SELECT * and listing individual columns, and why does listing columns matter in production code?โถ Show answer
SELECT * returns every column in the table at query time. While convenient for ad-hoc exploration, it causes problems in production:
- Schema drift โ if a column is added or reordered, downstream code that expects positional or named columns may break.
- Performance โ unnecessary columns are fetched from disk and sent over the network.
- Readability โ explicit column lists document the intent of the query and make maintenance easier.
Always list columns explicitly in production queries, views, and application code.
ORDER BY, and how can you control the sort position of NULLs?โถ Show answer
By default, Oracle sorts NULLs last in ascending order and first in descending order โ the opposite of some other databases (e.g. PostgreSQL sorts NULLs last in both directions by default).
You can override this with the NULLS FIRST / NULLS LAST clause:
SELECT last_name, commission_pct
FROM employees
ORDER BY commission_pct ASC NULLS LAST; -- NULLs at the bottom regardless
This is important for reports where NULLs represent missing data that should appear at the end of a sorted list.
Related Topics
- Filtering & Conditions โ narrow results with WHERE, BETWEEN, IN, LIKE, and NULL checks
- Aggregate Functions โ COUNT, SUM, AVG, GROUP BY for summarising result sets
- Joins โ combining columns from multiple tables with JOIN
- Subqueries โ embedding one SELECT inside another