SQLMentor // learn sql

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
โš 
Avoid 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.

โœ“
Use double quotes for aliases that contain spaces or match SQL reserved words. In Oracle, quoted aliases are case-sensitive: "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
โš 
In Oracle, use 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;
โ„น
Oracle uses 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;
โ„น
In Oracle, NULLs sort as if they are larger than any other value (so they appear last in ASC, first in DESC) unless you use NULLS FIRST / NULLS LAST. MySQL sorts NULLs first in ASC order.

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:

  1. Write SQL keywords in UPPERCASE (SELECT, FROM, WHERE, etc.)
  2. Write table and column names in lowercase
  3. Put each major clause on its own line
  4. Indent column lists and conditions with consistent spacing
  5. Align the keywords vertically (many developers align FROM, WHERE, ORDER BY with SELECT)
  6. Always end a statement with a semicolon ;
  7. 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;
โœ“
Consistent formatting is not just cosmetic. When you are debugging a complex 50-line query at midnight, good formatting is what lets you quickly spot the mistake. Develop the habit now while queries are short.

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;
โ„น
The next chapter covers more powerful filtering with AND/OR, BETWEEN, IN, LIKE, and handling NULL values. The chapter after that covers GROUP BY and aggregate functions for summarising data.

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

IQ ยท Basic SQL
What is the difference between 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:

  1. Schema drift โ€” if a column is added or reordered, downstream code that expects positional or named columns may break.
  2. Performance โ€” unnecessary columns are fetched from disk and sent over the network.
  3. 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.

IQ ยท Basic SQL
How does Oracle handle NULL values in 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