SQL (Structured Query Language) is the language used to ask questions of relational databases, and it is one of the most useful skills in data work. Whether your organisation runs PostgreSQL, SQL Server, MySQL, BigQuery or Snowflake, the core of SQL is the same – and the core starts with two keywords: SELECT and WHERE.
This tutorial teaches you to retrieve exactly the rows and columns you need. We will create a small table, then work through selecting columns, filtering rows, combining conditions, handling missing values, sorting and limiting results. All examples run in SQLite, which needs no installation if you have Python, and they work with minimal or no changes in other databases.
How to follow along
The easiest option is the free DB Browser for SQLite, or any online SQLite playground. Alternatively, run SQL from Python using the built-in sqlite3 module – see Using SQLite with Python and Pandas. Create the sample table by running this script:
CREATE TABLE employees (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
department TEXT,
city TEXT,
salary INTEGER,
start_date TEXT
);
INSERT INTO employees VALUES
(1, 'Amira Hassan', 'Finance', 'London', 52000, '2019-03-01'),
(2, 'Tom Wright', 'Sales', 'Leeds', 38000, '2021-07-15'),
(3, 'Priya Shah', 'Data', 'London', 61000, '2018-11-05'),
(4, 'Owen Davies', 'Sales', 'Cardiff', 41000, '2022-01-10'),
(5, 'Grace Okafor', 'Data', 'Manchester', 57000, '2020-06-22'),
(6, 'Liam Murphy', 'Finance', 'Leeds', 47000, '2023-02-01'),
(7, 'Sophie Brown', NULL, 'London', 35000, '2024-04-08'),
(8, 'Jack Wilson', 'Marketing', 'Glasgow', NULL, '2021-09-30');
Dates are stored as text in ISO format (YYYY-MM-DD), which is the standard approach in SQLite and sorts correctly as text.
Step 1: Select all columns
SELECT *
FROM employees;
SELECT lists the columns you want, FROM names the table, and * means “every column”. A semicolon ends the statement. SQL keywords are not case-sensitive, but writing them in capitals is a widely used convention that makes queries easier to read.
SELECT * is fine for exploring a table, but avoid it in saved queries and reports. If someone adds a column to the table, your output changes unexpectedly, and on wide tables you pull far more data than you need.
Step 2: Select specific columns
SELECT name, department, salary
FROM employees;
Columns appear in the order you list them. You can also rename columns in the output with AS, and calculate new values:
SELECT name,
salary,
salary / 12 AS monthly_salary,
salary * 1.03 AS salary_after_3pc_rise
FROM employees;
Be careful with integer division: in SQLite and some other databases, dividing two integers gives an integer, so 7 / 2 returns 3. Use salary / 12.0 if you need decimals.
Step 3: Filter rows with WHERE
WHERE keeps only the rows where a condition is true:

SELECT name, city
FROM employees
WHERE city = 'London';
Text values go in single quotes. The comparison operators are:
=equal to<>or!=not equal to>,<,>=,<=greater/less than
SELECT name, salary
FROM employees
WHERE salary >= 50000;
Text comparison case-sensitivity depends on the database. In SQLite, = is case-sensitive, so 'london' would not match 'London'. In SQL Server and MySQL it usually is not, because of their default collations. When in doubt, compare LOWER(city) = 'london'.
Step 4: Combine conditions with AND, OR and NOT
-- Data team members in London
SELECT name, department, city
FROM employees
WHERE department = 'Data' AND city = 'London';
-- Anyone in Sales or Marketing
SELECT name, department
FROM employees
WHERE department = 'Sales' OR department = 'Marketing';
When you mix AND and OR, always use brackets. AND is evaluated before OR, which often gives surprising results:

-- Probably NOT what you mean: Sales staff (any salary) OR Finance staff over 50k
SELECT name, department, salary
FROM employees
WHERE department = 'Sales' OR department = 'Finance' AND salary > 50000;
-- What you probably mean: Sales or Finance staff, earning over 40k
SELECT name, department, salary
FROM employees
WHERE (department = 'Sales' OR department = 'Finance') AND salary > 40000;
Step 5: Use IN, BETWEEN and LIKE
These shortcuts make filters shorter and clearer.
-- IN: match any value in a list
SELECT name, city
FROM employees
WHERE city IN ('Leeds', 'Manchester', 'Glasgow');
-- BETWEEN: inclusive range
SELECT name, salary
FROM employees
WHERE salary BETWEEN 40000 AND 55000;
-- Dates between two values (ISO text compares correctly)
SELECT name, start_date
FROM employees
WHERE start_date BETWEEN '2021-01-01' AND '2021-12-31';
-- LIKE: pattern matching. % = any characters, _ = one character
SELECT name
FROM employees
WHERE name LIKE 'S%';
BETWEEN includes both ends. With full timestamps rather than dates this catches people out: BETWEEN '2021-01-01' AND '2021-12-31' excludes '2021-12-31 14:00'. For timestamps, use >= '2021-01-01' AND < '2022-01-01'.
Step 6: Handle NULL correctly
NULL means “unknown” or “missing”. It is not zero and not an empty string, and it does not equal anything – not even another NULL. This query returns no rows:
SELECT name FROM employees WHERE department = NULL;
Use IS NULL and IS NOT NULL instead:
SELECT name FROM employees WHERE department IS NULL;
SELECT name, salary FROM employees WHERE salary IS NOT NULL;
NULL also affects “not equal” filters. WHERE department <> 'Sales' does not return Sophie Brown, because comparing NULL to anything gives “unknown”, which WHERE treats as false. If you want rows with missing departments too, say so explicitly:
SELECT name, department
FROM employees
WHERE department <> 'Sales' OR department IS NULL;
To show a friendly value instead of NULL, use COALESCE, which returns the first non-null argument:
SELECT name, COALESCE(department, 'Unassigned') AS department
FROM employees;
Step 7: Sort with ORDER BY and limit results
SELECT name, salary
FROM employees
WHERE salary IS NOT NULL
ORDER BY salary DESC
LIMIT 3;
ORDER BY sorts ascending by default; add DESC for descending. You can sort by several columns: ORDER BY department, salary DESC. LIMIT restricts the number of rows returned. In SQL Server the equivalent is SELECT TOP 3 ..., and in standard SQL (supported by PostgreSQL and Oracle) it is FETCH FIRST 3 ROWS ONLY.
Without ORDER BY, a database does not guarantee any row order. If order matters, always specify it.
Step 8: Remove duplicate values with DISTINCT
SELECT DISTINCT city
FROM employees
ORDER BY city;
DISTINCT applies to the whole row of selected columns, so SELECT DISTINCT city, department returns unique city–department combinations.
The order SQL actually runs in
You write SELECT ... FROM ... WHERE ... ORDER BY, but the database logically processes FROM first, then WHERE, then SELECT, then ORDER BY. That explains a common error: you generally cannot use a column alias created in SELECT inside WHERE, because WHERE runs first. You can use it in ORDER BY, which runs last.
Common mistakes
- Using
= NULLinstead ofIS NULL. - Mixing AND/OR without brackets.
- Double quotes for text. Standard SQL uses single quotes for text values; double quotes are for identifiers such as column names.
- Relying on default row order. Use
ORDER BY. - BETWEEN on timestamps that excludes most of the last day.
- Integer division silently dropping decimals.
FAQ
Is SQL the same in every database?
The core (SELECT, WHERE, JOIN, GROUP BY) is shared. Differences appear in functions (dates especially), row limiting and some data types.
Does the order of conditions in WHERE affect speed?
Generally no. Modern query optimisers decide the order of evaluation. Indexes on filtered columns matter far more.
Can I filter on a calculated column?
Yes, by repeating the expression in WHERE (for example WHERE salary / 12 > 4000) or by using a CTE – see CTEs vs Subqueries.
Next steps
Next, learn to combine tables with SQL Joins Explained and summarise data with SQL GROUP BY and HAVING. Once you are comfortable, window functions open up rankings and running totals.

