SQL Foundations  «Prev  Next»

Lesson 9 The SELECT Statement
Objective Learn How to Use a very Simple SELECT Statement.

SQL SELECT Statement (How to Query a Database Table)

Lesson 8 used INSERT to put rows into BasicTable. This lesson uses SELECT to get them back out. SELECT is the most frequently used statement in SQL — it is how every application, report, and query retrieves data from a relational database. This lesson covers the core SELECT syntax used throughout this module: SELECT *, named column lists, WHERE filtering, ORDER BY sorting, and DISTINCT. Lessons in modules 5 through 7 build on this foundation with JOIN, aggregation, subqueries, and advanced filtering.

Retrieving Data — The SELECT Statement

SELECT * FROM BasicTable — The Simplest Query

The simplest SELECT statement retrieves every row and every column from a table. After the three INSERT statements from lesson 8, BasicTable contains three rows. The following query retrieves all of them:
SELECT * FROM BasicTable;
The asterisk is shorthand for "all columns." The result is a complete listing of every row in the table, with all columns, in no guaranteed order:


Firstname Lastname PhoneNumber
Carl Gauss 555-0192
Bernhard Riemann 555-0317
David Hilbert 555-0437

This is the result set — the output returned by the SELECT query. It looks like a table and can be thought of as one, but it is not stored anywhere. It is a temporary virtual table computed at query execution time and discarded when the query is done.

The Result Set — A Temporary Virtual Table

The result set is the fundamental output unit of SQL. Every SELECT query produces a result set — a two-dimensional grid of rows and columns, like a table, but existing only for the duration of the query execution. The result set reflects the state of the data at the moment the query runs. Run the same query a minute later after another INSERT, and the result set will include the new row.

In DBeaver, Beekeeper Studio, and HeidiSQL, the result set appears in the results panel below the SQL editor — a scrollable grid showing exactly what the database returned. In an application, the result set is made available to the application code through the database driver, row by row, for processing or display.


The SELECT Statement — Keyword by Keyword

Like CREATE TABLE (lesson 4) and INSERT (lesson 8), SELECT has a fixed structure built from specific keywords. Understanding each component before writing complex queries makes the syntax predictable.

The SELECT Clause — What to Retrieve

The SELECT keyword opens the statement and is followed by a list of what to retrieve — either an asterisk for all columns, or a comma-separated list of specific column names. This clause defines the columns that will appear in the result set:
-- All columns
SELECT *

-- Specific columns only
SELECT Firstname, Lastname

The FROM Clause — Where to Find It

The FROM keyword tells the engine which table to read. It is the second required clause — SELECT and FROM together are the minimum structure for any SELECT statement:
FROM BasicTable

The Complete Minimal SELECT

SELECT and FROM together form the minimal complete SELECT statement. All other clauses — WHERE, ORDER BY, GROUP BY, HAVING — are optional and refine what the query returns:
-- Minimal: retrieve all columns from BasicTable
SELECT * FROM BasicTable;

-- Minimal with column list: retrieve two specific columns
SELECT Firstname, Lastname FROM BasicTable;
The only required clauses are SELECT and FROM. Every other clause narrows, sorts, or aggregates the result.

Selecting Specific Columns

Named Column List vs SELECT *

Replacing the asterisk with a column list retrieves only the specified columns. The result set contains only those columns, in the order they are listed in the SELECT clause — regardless of the order they were defined in CREATE TABLE:
-- Returns only Lastname and Firstname, in that order
SELECT Lastname, Firstname
FROM BasicTable;
Result:

Lastname Firstname
GaussCarl
RiemannBernhard
HilbertDavid

PhoneNumber is absent from the result because it was not listed in the SELECT clause. The column still exists in the table — it was simply not requested.

Column Aliases — Renaming Output Columns

The AS keyword assigns an alias — a display name — to a column in the result set. The alias appears as the column header in the output and does not affect the underlying table:

SELECT Firstname AS "First Name",
       Lastname  AS "Last Name",
       PhoneNumber AS "Phone"
FROM BasicTable;

Aliases are useful for making result sets more readable, for renaming columns that have cryptic or technical names, and for naming computed columns that have no natural name (such as the result of a function or arithmetic expression).

When to Use SELECT * and When to Avoid It

SELECT * is convenient for exploration and quick verification — it is the right tool when you want to see all the data in a table during development or debugging. It is acceptable in situations where the specific columns and their order are irrelevant, such as inside an EXISTS subquery.

In production queries — application code, stored procedures, and view definitions — always name the columns explicitly. A table's column set can change: new columns get added with ALTER TABLE, columns get renamed, columns get dropped. A SELECT * in application code silently changes behavior when the table changes. Explicit column names make the query's intent clear and protect against unintended results when the schema evolves. The same principle applies to view definitions — a view built on SELECT * will include new columns added to the base table, which may expose data the view was not intended to expose.


Filtering Rows with WHERE

The WHERE Clause Syntax

The WHERE clause filters the rows returned by the query. Only rows for which the WHERE condition evaluates to TRUE are included in the result set. WHERE is placed after the FROM clause:
SELECT Firstname, Lastname, PhoneNumber
FROM BasicTable
WHERE Lastname = 'Gauss';
Result: one row — Carl Gauss with phone number 555-0192. The Riemann and Hilbert rows are excluded because their Lastname values do not match the condition.

Equality and Comparison Operators

WHERE conditions support a full set of comparison operators. Each operator compares the column value to the condition value and includes the row if the comparison is TRUE:

-- Equality
WHERE Lastname = 'Gauss'

-- Not equal
WHERE Lastname != 'Gauss'
-- or: WHERE Lastname <> 'Gauss'   (older syntax, both standard)

-- Range comparison
WHERE PhoneNumber > '555-0200'

-- Range between two values (inclusive)
WHERE PhoneNumber BETWEEN '555-0100' AND '555-0400'

-- Pattern matching (% matches any sequence of characters)
WHERE Lastname LIKE 'G%'

-- NULL test — note: = NULL never works; IS NULL is required
WHERE PhoneNumber IS NULL
WHERE PhoneNumber IS NOT NULL
Multiple conditions can be combined with AND and OR:
-- Both conditions must be true
WHERE Lastname = 'Gauss' AND PhoneNumber IS NOT NULL

-- Either condition can be true
WHERE Lastname = 'Gauss' OR Lastname = 'Riemann'

WHERE and Index Usage

The WHERE clause is the primary driver of index usage, as covered in lessons 5 through 7. When the WHERE condition references an indexed column with a selective predicate — equality on a high-cardinality column like Lastname — the query optimizer uses the index to locate matching rows directly rather than scanning the entire table. The WHERE clause you write here determines which indexes are useful to create with CREATE INDEX.


Sorting Results with ORDER BY

ASC and DESC

The ORDER BY clause sorts the result set by one or more columns. Without ORDER BY, the row order in the result set is undefined — the engine returns rows in whatever order is most efficient for the chosen execution plan, which may vary between executions. If you need a specific order, always specify it explicitly:
-- Ascending order (default — A to Z, smallest to largest)
SELECT Firstname, Lastname
FROM BasicTable
ORDER BY Lastname ASC;

-- Descending order (Z to A, largest to smallest)
SELECT Firstname, Lastname
FROM BasicTable
ORDER BY Lastname DESC;
ASC is the default direction and can be omitted — ORDER BY Lastname is equivalent to ORDER BY Lastname ASC.

Multi-Column ORDER BY

Multiple sort columns can be specified, each with its own direction. The result is sorted by the first column, then by the second column within ties in the first, and so on:

-- Sort by Lastname ascending, then Firstname ascending within same Lastname
SELECT Firstname, Lastname
FROM BasicTable
ORDER BY Lastname ASC, Firstname ASC;

ORDER BY and Index Order

As covered in lesson 7, when an index exists on the ORDER BY column, the optimizer may read rows in index order to avoid an explicit sort operation. An index on Lastname means ORDER BY Lastname can be satisfied by scanning the index in key order rather than reading all rows and sorting in memory. The CREATE INDEX statements from lessons 6 and 7 directly support the ORDER BY clauses used in SELECT.


Eliminating Duplicates with DISTINCT

The DISTINCT keyword eliminates duplicate rows from the result set. When a query returns the same combination of column values in multiple rows, DISTINCT collapses them into a single row. DISTINCT is placed immediately after SELECT:
-- Without DISTINCT: returns one row per contact, may have duplicate lastnames
SELECT Lastname FROM BasicTable;

-- With DISTINCT: each unique Lastname appears only once
SELECT DISTINCT Lastname FROM BasicTable;
DISTINCT applies to the entire row of selected columns — two rows are considered duplicates only if all selected columns have identical values. SELECT DISTINCT Lastname, Firstname keeps rows with the same Lastname if their Firstname values differ.

DISTINCT has a performance cost: the engine must sort or hash the result set to identify and eliminate duplicates. For large result sets, this adds execution time. Use DISTINCT when the query genuinely needs unique values — not as a workaround for a missing WHERE condition or a misunderstood join.

Limiting the Number of Rows Returned

For large tables, returning all rows is impractical. All major database engines provide a mechanism to limit the result set to the first N rows:

LIMIT (MySQL / MariaDB / PostgreSQL / SQLite)

-- Return only the first 2 rows
SELECT Firstname, Lastname
FROM BasicTable
ORDER BY Lastname
LIMIT 2;

TOP (SQL Server)

-- SQL Server syntax — TOP goes inside the SELECT clause
SELECT TOP 2 Firstname, Lastname
FROM BasicTable
ORDER BY Lastname;

FETCH FIRST (SQL Standard)

-- Standard SQL syntax — supported in PostgreSQL, SQL Server, Oracle, DB2
SELECT Firstname, Lastname
FROM BasicTable
ORDER BY Lastname
FETCH FIRST 2 ROWS ONLY;
Always combine a row limit with ORDER BY — without ORDER BY, the "first" rows are undefined and may vary between executions.

Optional SELECT Clauses — A Preview

The full SELECT statement supports additional optional clauses beyond WHERE and ORDER BY. This lesson introduces the concept; later modules cover each in depth.

GROUP BY and HAVING

GROUP BY collapses multiple rows sharing the same value in a column into a single summary row, enabling aggregate functions — COUNT, SUM, AVG, MIN, MAX. HAVING filters the grouped result, similar to how WHERE filters individual rows. These are covered in detail in module 5.

-- Count of contacts per unique Lastname (contrived example on a small table)
SELECT Lastname, COUNT(*) AS ContactCount
FROM BasicTable
GROUP BY Lastname
HAVING COUNT(*) > 0;

JOIN — Combining Tables

JOIN combines rows from two or more tables based on a related column — typically a foreign key relationship. All the CREATE TABLE, INSERT, and index work in this module builds the foundation for JOIN: you need tables with data and indexed join columns before JOIN queries are meaningful. Module 7 covers inner joins, outer joins, and multi-table queries in full.
-- Preview: joining Contacts to Orders on ContactID
SELECT c.Lastname, c.Firstname, o.OrderDate
FROM Contacts c
JOIN Orders o ON c.ContactID = o.ContactID
WHERE o.OrderDate >= '2026-01-01';

What Modules 5 Through 7 Cover

This lesson's SELECT foundation — SELECT clause, FROM, WHERE, ORDER BY, DISTINCT, LIMIT — is the core that every subsequent SQL lesson builds on. Module 5 introduces table querying patterns. Module 6 covers filtering with WHERE in depth: the INTO statement, ORDER BY, LIKE, and complex filter conditions. Module 7 covers JOIN operations: inner joins, outer joins, left and right joins, and the UNION operator. Every concept in those modules is an extension of the SELECT statement introduced here.

Select Insert - Quiz

Click the Quiz link below to take a brief quiz about the SELECT and INSERT statements.
Select Insert - Quiz

SEMrush Software 8 SEMrush Banner 8