JOINs in Oracle
Oracle supports both ANSI SQL JOIN syntax and its older proprietary syntax. Always prefer ANSI syntax for readability.
INNER JOIN
-- Returns rows with matching values in both tables
SELECT e.first_name, e.last_name, e.salary, d.dept_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
WHERE e.salary > 50000
ORDER BY e.salary DESC;
LEFT / RIGHT / FULL OUTER JOIN
-- LEFT JOIN: All employees including those without a department
SELECT e.first_name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id;
-- RIGHT JOIN: All departments including empty ones
SELECT e.first_name, d.dept_name
FROM employees e
RIGHT JOIN departments d ON e.dept_id = d.dept_id;
-- FULL OUTER JOIN: All rows from both tables
SELECT e.first_name, d.dept_name
FROM employees e
FULL OUTER JOIN departments d ON e.dept_id = d.dept_id;
SELF JOIN
-- Find employee and their manager
SELECT e.first_name || ' ' || e.last_name AS employee,
m.first_name || ' ' || m.last_name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.emp_id;
Subqueries
-- Scalar subquery (returns single value)
SELECT first_name, salary,
(SELECT ROUND(AVG(salary),2) FROM employees) AS avg_salary
FROM employees;
-- Inline view (FROM clause subquery)
SELECT dept_name, avg_sal FROM (
SELECT d.dept_name, ROUND(AVG(e.salary),2) AS avg_sal
FROM employees e JOIN departments d ON e.dept_id = d.dept_id
GROUP BY d.dept_name
)
WHERE avg_sal > 60000;
-- Correlated subquery (references outer query)
SELECT first_name, salary, dept_id
FROM employees e1
WHERE salary > (
SELECT AVG(salary)
FROM employees e2
WHERE e2.dept_id = e1.dept_id -- correlated to outer dept_id
);
WITH Clause (Common Table Expressions)
-- CTE for readability and reuse
WITH dept_stats AS (
SELECT dept_id,
COUNT(*) AS headcount,
ROUND(AVG(salary),2) AS avg_salary,
MAX(salary) AS max_salary
FROM employees
GROUP BY dept_id
),
high_paying_depts AS (
SELECT dept_id FROM dept_stats WHERE avg_salary > 70000
)
SELECT e.first_name, e.salary, d.dept_name
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
WHERE e.dept_id IN (SELECT dept_id FROM high_paying_depts)
ORDER BY e.salary DESC;
Analytical (Window) Functions
Window functions perform calculations across a set of rows related to the current row — without collapsing rows like GROUP BY does.
-- ROW_NUMBER: Unique sequential number per partition
SELECT first_name, dept_id, salary,
ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn
FROM employees;
-- RANK vs DENSE_RANK
-- RANK: gaps after ties (1,1,3)
-- DENSE_RANK: no gaps (1,1,2)
SELECT first_name, salary,
RANK() OVER (ORDER BY salary DESC) AS rnk,
DENSE_RANK() OVER (ORDER BY salary DESC) AS d_rnk
FROM employees;
-- Top 2 earners per department
SELECT * FROM (
SELECT first_name, dept_id, salary,
DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS dr
FROM employees
)
WHERE dr <= 2;
LAG and LEAD
-- Compare current row to previous/next row
SELECT first_name, hire_date, salary,
LAG(salary, 1, 0) OVER (ORDER BY hire_date) AS prev_salary,
LEAD(salary, 1, 0) OVER (ORDER BY hire_date) AS next_salary,
salary - LAG(salary,1,0) OVER (ORDER BY hire_date) AS salary_diff
FROM employees
ORDER BY hire_date;
SUM / AVG as Window Function
-- Running total salary by department
SELECT first_name, dept_id, salary,
SUM(salary) OVER (
PARTITION BY dept_id
ORDER BY hire_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM employees;
PIVOT and UNPIVOT
-- PIVOT: Rows to columns
SELECT * FROM (
SELECT dept_id, EXTRACT(YEAR FROM hire_date) AS yr, salary
FROM employees
)
PIVOT (
SUM(salary)
FOR yr IN (2021 AS "2021", 2022 AS "2022", 2023 AS "2023", 2024 AS "2024")
);
-- UNPIVOT: Columns to rows
SELECT dept_id, year_label, total_salary
FROM dept_salary_pivot
UNPIVOT (
total_salary
FOR year_label IN ("2021", "2022", "2023", "2024")
);
CONNECT BY (Hierarchical Queries)
-- Org chart: employees and their hierarchy
SELECT LEVEL,
LPAD(' ', 2*(LEVEL-1)) || first_name AS org_chart,
emp_id, manager_id
FROM employees
START WITH manager_id IS NULL -- root node
CONNECT BY PRIOR emp_id = manager_id
ORDER SIBLINGS BY first_name;
Lab 4: Real-World Query Challenges
-- Challenge 1: Median salary per department
SELECT dept_id,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary) AS median_salary
FROM employees
GROUP BY dept_id;
-- Challenge 2: Employees hired in the last 90 days
SELECT first_name, hire_date
FROM employees
WHERE hire_date >= SYSDATE - 90;
-- Challenge 3: Cumulative % of total salary
SELECT first_name, salary,
ROUND(
SUM(salary) OVER (ORDER BY salary ROWS UNBOUNDED PRECEDING) /
SUM(salary) OVER () * 100, 2
) AS cumulative_pct
FROM employees ORDER BY salary;
Common Errors
ORA-00904: invalid identifier – Column alias used in WHERE (use HAVING or inline view instead).
-- WRONG
SELECT salary * 1.1 AS new_sal FROM employees WHERE new_sal > 50000;
-- CORRECT: Use inline view
SELECT * FROM (SELECT salary * 1.1 AS new_sal FROM employees) WHERE new_sal > 50000;
ORA-01427: single-row subquery returns more than one row – Use IN instead of = for multi-row subqueries.
Summary
- Always use ANSI JOIN syntax; avoid Oracle's old (+) outer join syntax
- CTEs (WITH clause) improve readability for complex queries
- Window functions (OVER/PARTITION BY) are powerful for rankings, running totals, and comparisons
- Use CONNECT BY for tree/hierarchy data without recursive CTEs
Next: Part 5 – PL/SQL Essentials: Blocks, Cursors & Procedures
Frequently asked questions
Why do I get ORA-00904 when I use a column alias in WHERE?
WHERE is evaluated before the SELECT list, so the alias does not exist yet. Repeat the expression in WHERE, or wrap the query in an inline view and filter on the alias outside it.
What is the difference between ROW_NUMBER, RANK and DENSE_RANK?
ROW_NUMBER always gives unique numbers (1,2,3). RANK gives ties the same number and then skips (1,1,3). DENSE_RANK gives ties the same number without gaps (1,1,2).
How do I get the top 3 salaries in each department?
Number the rows with
ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) in an inline view, then filter on that number <= 3 outside it. For a single top-N over the whole table, FETCH FIRST 3 ROWS ONLY (12c+) is simpler.How do I fix ORA-01427: single-row subquery returns more than one row?
The subquery returns several rows where only one is allowed, for example after
=. Use IN instead of =, or make the subquery return a single row.Why use ANSI JOIN syntax instead of (+)?
ANSI joins keep the join conditions separate from the filters, which makes queries easier to read. They also support FULL OUTER JOIN, which the old (+) syntax cannot express.
Comments
0Please sign in to leave a comment.
No comments yet
Be the first to share your thoughts!