Oracle Database
SELECT Queries
The SELECT statement is the foundation of everything you do with data in Oracle. Every report, every dashboard, every application screen that displays data is built on a SELECT. This lesson covers the full structure of a SELECT statement — choosing columns, writing expressions, controlling output order, eliminating duplicates, and working with Oracle-specific syntax like DUAL, ROWNUM, and FETCH FIRST. All examples draw from the DataplexaStore and DataplexaHR schemas.
To follow along with the examples in this lesson, use the course dataset from Lesson 8.
Basic SELECT Structure
A SELECT statement has a mandatory SELECT and FROM clause. Every other clause — WHERE, GROUP BY, HAVING, ORDER BY — is optional. Oracle evaluates clauses in a fixed internal order regardless of how they appear: FROM first, then WHERE, then GROUP BY, then HAVING, then SELECT, then ORDER BY last. Understanding this order explains why you cannot reference a column alias defined in SELECT inside a WHERE clause — the WHERE runs before SELECT has produced the alias.
The asterisk * selects all columns in the order they were defined at table creation. It is useful for exploration but should never appear in production queries or application code — when a column is added to the table, SELECT * returns it automatically, which can break applications that expect a fixed column count or order.
-- Select all columns — useful for exploration, avoid in production
SELECT * FROM customers;
-- Select specific columns — always preferred in application queries
SELECT customer_id, full_name, email, country
FROM customers;
-- Column aliases — AS is optional but always include it for readability
SELECT customer_id AS id,
full_name AS name,
UPPER(country) AS country_upper,
LOWER(email) AS email_lower
FROM customers
ORDER BY customer_id;
-- DUAL — Oracle's built-in single-row dummy table
-- Use it when you need to evaluate an expression without reading from a real table
SELECT SYSDATE AS today,
USER AS current_user,
'DataplexaStore' AS schema_label,
ROUND(1234.5678, 2) AS rounded
FROM DUAL;CUSTOMER_ID FULL_NAME EMAIL COUNTRY
─────────── ─────────────── ─────────────────────── ───────
1 Sarah Mitchell sarah.m@example.com USA
2 James Okafor james.o@example.com Nigeria
3 Priya Sharma priya.s@example.com India
-- Aliases and functions:
ID NAME COUNTRY_UPPER EMAIL_LOWER
── ─────────────── ───────────── ────────────────────────
1 Sarah Mitchell USA sarah.m@example.com
2 James Okafor NIGERIA james.o@example.com
3 Priya Sharma INDIA priya.s@example.com
-- DUAL:
TODAY CURRENT_USER SCHEMA_LABEL ROUNDED
─────────── ──────────── ────────────── ───────
28-MAR-2024 DATAPLEXA DataplexaStore 1234.57
- Oracle requires a
FROMclause on everySELECT— unlike some databases there is noSELECT 1 + 1without a table; useFROM DUALwhen no real table is needed - Column aliases defined with
AScan be used inORDER BYbut not inWHERE,GROUP BY, orHAVING— those clauses execute before the alias is assigned UPPER()andLOWER()do not change the stored value — they only affect the output of that query
Column Expressions and Arithmetic
SELECT can compute new values from existing columns using arithmetic operators, string functions, date functions, and conditional expressions. The result appears as a derived column in the output — nothing is written back to the table. Expressions are evaluated row by row, so every row in the result set gets its own computed value.
-- Arithmetic on numeric columns
SELECT product_name,
unit_price,
unit_price * 1.2 AS price_inc_vat,
unit_price * 0.9 AS discounted_10pct,
ROUND(unit_price * 1.2, 2) AS vat_rounded,
stock_qty * unit_price AS stock_value
FROM products
WHERE active_flag = 'Y'
ORDER BY unit_price DESC;
-- String expressions using Oracle's concatenation operator ||
SELECT employee_id,
first_name || ' ' || last_name AS full_name,
INITCAP(first_name) AS first_initcap,
LENGTH(last_name) AS last_name_length,
SUBSTR(email, 1, INSTR(email,'@') - 1) AS email_username
FROM employees
ORDER BY last_name;
-- Date expressions
SELECT order_id,
order_date,
TRUNC(order_date) AS date_only,
order_date + 14 AS due_date,
ROUND(SYSDATE - order_date, 0) AS days_old,
TO_CHAR(order_date, 'Month YYYY') AS month_label
FROM orders
ORDER BY order_date;PRODUCT_NAME UNIT_PRICE PRICE_INC_VAT DISCOUNTED_10PCT VAT_ROUNDED STOCK_VALUE
─────────────────────────────── ────────── ───────────── ──────────────── ─────────── ───────────
Standing Desk 499.99 599.99 449.99 599.99 24999.50
Ergonomic Office Chair 299.99 359.99 269.99 359.99 20999.30
Wireless Noise-Cancelling Headp… 149.99 179.99 134.99 179.99 10499.30
Running Shoes Pro 119.99 143.99 107.99 143.99 8399.30
-- Employee strings:
EMPLOYEE_ID FULL_NAME FIRST_INITCAP LAST_NAME_LENGTH EMAIL_USERNAME
─────────── ───────────────── ───────────── ──────────────── ──────────────
104 Fatima Al-Hassan Fatima 8 fatima
101 James Okafor James 6 james.o
103 Priya Sharma Priya 5 priya.s
-- Order dates:
ORDER_ID ORDER_DATE DATE_ONLY DUE_DATE DAYS_OLD MONTH_LABEL
──────── ─────────── ─────────── ─────────── ──────── ───────────
5001 10-JAN-2024 10-JAN-2024 24-JAN-2024 78 January 2024
5002 22-JAN-2024 22-JAN-2024 05-FEB-2024 66 January 2024
SUBSTR(str, start, length)extracts a substring — in Oracle positions are 1-based, not 0-based;SUBSTR(email, 1, 5)returns the first 5 characters starting from position 1INSTR(str, substr)returns the position of the first occurrence ofsubstrinstr— combining it withSUBSTRallows dynamic string parsing without knowing the exact position in advance- Any arithmetic involving a
NULLcolumn returnsNULL— ifstock_qtywereNULLthenstock_qty * unit_pricewould produceNULL, not zero; useNVL(stock_qty, 0)to substitute a default before arithmetic
DISTINCT and Eliminating Duplicates
SELECT DISTINCT removes duplicate rows from the result set. It operates on the entire row — if two rows have identical values in every selected column they are collapsed into one. DISTINCT applies to all columns in the SELECT list, not just the first one.
DISTINCT has a cost — Oracle must sort or hash the result to identify duplicates. On large result sets this can be slow. If you find yourself using DISTINCT to remove duplicates from a join result, investigate whether the join itself is producing duplicates due to a missing or incorrect join condition.
-- All countries customers come from — without DISTINCT duplicates appear
SELECT country FROM customers ORDER BY country;
-- DISTINCT removes duplicate country values
SELECT DISTINCT country
FROM customers
ORDER BY country;
-- DISTINCT across multiple columns — unique combinations of country and loyalty_tier
SELECT DISTINCT country,
loyalty_tier
FROM customers
ORDER BY country, loyalty_tier;
-- Count of distinct values — DISTINCT inside COUNT
SELECT COUNT(*) AS total_customers,
COUNT(DISTINCT country) AS unique_countries,
COUNT(DISTINCT loyalty_tier) AS unique_tiers
FROM customers;COUNTRY
───────
India
India
Nigeria
USA
USA
USA
-- DISTINCT country:
COUNTRY
───────
India
Nigeria
UK
USA
-- DISTINCT country + tier:
COUNTRY LOYALTY_TIER
─────── ────────────
India gold
India standard
Nigeria standard
UK gold
USA standard
-- COUNT:
TOTAL_CUSTOMERS UNIQUE_COUNTRIES UNIQUE_TIERS
─────────────── ──────────────── ────────────
12 5 3
DISTINCTis placed immediately afterSELECTand applies to the entire row —SELECT DISTINCT a, bdeduplicates on the combination ofaandb, not onaaloneCOUNT(DISTINCT column)counts unique non-null values in a column — a useful pattern for data profiling and cardinality checks- If duplicates appear in a query result unexpectedly, the cause is almost always a one-to-many join — check the join conditions before reaching for
DISTINCT
ORDER BY and Sorting
ORDER BY controls the sequence in which rows appear in the result. Without ORDER BY, Oracle returns rows in no guaranteed order — the physical storage order, index access order, or parallel execution order may all produce different sequences across runs. If row order matters, always specify it explicitly.
Oracle sorts NULL values to the end in ascending order and to the beginning in descending order by default. This can be overridden with NULLS FIRST or NULLS LAST.
-- Single column sort — ascending is the default
SELECT product_name, unit_price, stock_qty
FROM products
ORDER BY unit_price;
-- Descending sort
SELECT product_name, unit_price, stock_qty
FROM products
ORDER BY unit_price DESC;
-- Multi-column sort — primary sort by category, secondary sort by price within each category
SELECT category, product_name, unit_price
FROM products
ORDER BY category ASC, unit_price DESC;
-- Sort by column position (1-based) — works but reduces readability
SELECT product_name, unit_price, stock_qty
FROM products
ORDER BY 2 DESC;
-- NULL handling in sort — manager_id is nullable
-- By default NULLs sort last in ASC, first in DESC
SELECT employee_id, full_name, manager_id
FROM (SELECT employee_id,
first_name || ' ' || last_name AS full_name,
manager_id
FROM employees)
ORDER BY manager_id ASC NULLS LAST;
-- Sorting by a column alias defined in SELECT
SELECT product_name,
unit_price * stock_qty AS stock_value
FROM products
ORDER BY stock_value DESC;PRODUCT_NAME UNIT_PRICE STOCK_QTY
─────────────────────── ────────── ─────────
Cotton Casual T-Shirt 19.99 200
Yoga Mat Premium 29.99 150
Bluetooth Speaker Mini 34.99 180
-- Multi-column sort:
CATEGORY PRODUCT_NAME UNIT_PRICE
──────────── ───────────────────────── ──────────
Accessories Standing Desk 499.99
Accessories Laptop Stand Aluminium 59.99
Electronics Wireless Noise-Cancelling… 149.99
Electronics USB-C Hub 7-Port 39.99
-- NULL handling (NULLS LAST):
EMPLOYEE_ID FULL_NAME MANAGER_ID
─────────── ─────────────── ──────────
103 Priya Sharma 101
104 Fatima Al-Hassan 101
101 James Okafor (null)
- Sorting by column position (
ORDER BY 2) is fragile — if the SELECT column list is reordered, the sort column changes silently; always sort by name in production code NULLS LASTis often the right choice for nullable columns in ascending sorts — it pushes unknown values to the end rather than letting them appear first- Column aliases defined in SELECT can be used in
ORDER BY— this is one of the few places an alias is available, becauseORDER BYis the last clause Oracle evaluates
Limiting Rows — ROWNUM and FETCH FIRST
Oracle provides two ways to limit the number of rows returned. ROWNUM is the traditional approach — it assigns a row number to each row as it is retrieved, before sorting. This means WHERE ROWNUM <= 5 on a query with an ORDER BY does not return the top 5 sorted rows — it returns 5 arbitrary rows and then sorts them. To correctly limit after sorting, wrap the sorted query in a subquery and apply ROWNUM to the outer query.
FETCH FIRST n ROWS ONLY, introduced in Oracle 12c, applies the limit after sorting and is the correct modern approach. Always prefer it over ROWNUM for new code.
-- ROWNUM — traditional approach, applied BEFORE sorting
-- This does NOT return the 3 most expensive products
SELECT product_name, unit_price
FROM products
WHERE ROWNUM <= 3
ORDER BY unit_price DESC;
-- Correct ROWNUM pattern — sort in a subquery, limit in the outer query
SELECT product_name, unit_price
FROM (SELECT product_name, unit_price
FROM products
ORDER BY unit_price DESC)
WHERE ROWNUM <= 3;
-- FETCH FIRST — Oracle 12c syntax, correct and readable
-- Returns the 3 most expensive products after sorting
SELECT product_name, unit_price
FROM products
ORDER BY unit_price DESC
FETCH FIRST 3 ROWS ONLY;
-- OFFSET — skip rows and return the next n
-- Page 2 of results: skip the first 3, return the next 3
SELECT product_name, unit_price
FROM products
ORDER BY unit_price DESC
OFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY;
-- FETCH FIRST with PERCENT
SELECT product_name, unit_price
FROM products
ORDER BY unit_price DESC
FETCH FIRST 25 PERCENT ROWS ONLY;PRODUCT_NAME UNIT_PRICE
─────────────────────── ──────────
Standing Desk 499.99
Ergonomic Office Chair 299.99
Waterproof Hiking Jacket 189.99
-- Correct ROWNUM subquery:
PRODUCT_NAME UNIT_PRICE
─────────────────────── ──────────
Standing Desk 499.99
Ergonomic Office Chair 299.99
Waterproof Hiking Jacket 189.99
-- FETCH FIRST 3:
PRODUCT_NAME UNIT_PRICE
─────────────────────── ──────────
Standing Desk 499.99
Ergonomic Office Chair 299.99
Waterproof Hiking Jacket 189.99
-- OFFSET 3 FETCH NEXT 3:
PRODUCT_NAME UNIT_PRICE
─────────────────────── ──────────
Wireless Headphones 149.99
Running Shoes Pro 119.99
Yoga Mat Premium 89.99
ROWNUMis assigned beforeORDER BY— this is the most common beginner mistake in Oracle; the subquery pattern orFETCH FIRSTmust be used to get correct top-N resultsFETCH FIRST ... ROWS ONLYrequires anORDER BYto be deterministic — without it Oracle can return any n rows; always pair the two togetherOFFSET n ROWS FETCH NEXT m ROWS ONLYis Oracle's pagination syntax — use it for API endpoints or UI pages that return results in pages
Summary
| Feature | Syntax | Key Point |
|---|---|---|
| Column alias | col AS alias | Available in ORDER BY only — not in WHERE or GROUP BY |
| DUAL | SELECT expr FROM DUAL | Required when no real table is needed — Oracle mandates FROM |
| DISTINCT | SELECT DISTINCT col | Applies to the whole row — deduplicates on all selected columns |
| ORDER BY | ORDER BY col ASC|DESC | Without it row order is not guaranteed |
| NULLS LAST / FIRST | ORDER BY col ASC NULLS LAST | Controls where NULLs appear in sorted output |
| ROWNUM | WHERE ROWNUM <= n | Applied before ORDER BY — wrap in subquery for correct top-N |
| FETCH FIRST | ORDER BY col FETCH FIRST n ROWS ONLY | 12c+ — applied after ORDER BY — preferred over ROWNUM |
| OFFSET / FETCH NEXT | OFFSET n ROWS FETCH NEXT m ROWS ONLY | Pagination — skip n rows, return next m |
Practice Questions
Practice 1. Why should SELECT * be avoided in production application queries?
Practice 2. A query uses ORDER BY unit_price DESC and WHERE ROWNUM <= 3. Will it return the 3 most expensive products?
Practice 3. Write a query that returns the second page of products sorted by unit_price descending, with 4 products per page.
Practice 4. Why does SELECT DISTINCT country, loyalty_tier deduplicate differently from SELECT DISTINCT country?
Practice 5. A column alias total_value is defined in SELECT. In which clauses can it be referenced, and in which can it not?
Quiz
Quiz 1. Oracle requires a FROM clause on every SELECT. Which built-in table is used when no real table is needed?
Quiz 2. In Oracle, what is the default sort order for NULL values in an ascending ORDER BY?
Quiz 3. Which clause must always accompany FETCH FIRST n ROWS ONLY to produce deterministic results?
Quiz 4. What does SUBSTR(email, 1, INSTR(email, '@') - 1) return for the value 'sarah@example.com'?
Quiz 5. A SELECT query computes unit_price * 1.2 AS vat_price. A developer adds WHERE vat_price > 100 to filter results. What happens?
Next up — WHERE Clause & Filtering — Learn how to filter rows using comparison operators, BETWEEN, IN, LIKE, IS NULL, and compound conditions with AND, OR, and NOT.