- Introduction to Oracle Database
- Oracle Data Types
- Database Objects & Connecting to Oracle
- DDL — Creating & Altering Objects
- DML — Manipulating Data
- Constraints
- Operators & the WHERE Clause
- Sorting & Row Limiting (ROWNUM, FETCH, Top-N)
- Aggregate Functions & Grouping
- Joins
- Subqueries
- Set Operators (UNION, INTERSECT, MINUS)
- Views & Materialized Views
- Sequences & Synonyms
- Indexes
- String, Numeric & Date Functions
- Introduction to PL/SQL
- Cursors
- Stored Procedures & Functions
- Packages
- Triggers
- Exception Handling
- Transactions & Locking
- User Management & Privileges
- Advanced: Analytic Functions, Hierarchical Queries, Collections
- Performance: EXPLAIN PLAN & Optimization
Oracle Database is a commercial, enterprise-grade Relational Database Management System (RDBMS) developed by Oracle Corporation, first released in 1979 (making it one of the earliest commercial SQL-based RDBMS products). It is widely used in large enterprises, banking, telecom, and government systems due to its scalability, reliability, and rich feature set.
Key characteristics of Oracle:
- Strong support for PL/SQL (Procedural Language/SQL) — Oracle's procedural extension to SQL, enabling stored procedures, functions, packages, and triggers.
- Robust multi-version concurrency control (MVCC) — readers never block writers and vice versa.
- Advanced features: partitioning, materialized views, analytic (window) functions, Real Application Clusters (RAC) for high availability, Data Guard for disaster recovery.
- Uses tablespaces to logically organize physical storage.
- Available in editions: Express Edition (XE — free), Standard Edition, and Enterprise Edition.
Oracle vs MySQL — key differences to keep in mind:
| Feature | Oracle | MySQL |
|---|---|---|
| Procedural language | PL/SQL (very rich) | Simpler stored procedure syntax |
| Row limiting | ROWNUM / FETCH FIRST |
LIMIT |
| Sequence generation | SEQUENCE objects |
AUTO_INCREMENT column attribute |
| String concatenation | ` | |
| Empty string vs NULL | Treats empty string as NULL |
Treats them as distinct |
| Set operators | UNION, INTERSECT, MINUS |
Only UNION (INTERSECT/MINUS added in 8.0.31+) |
Connecting to Oracle (SQL*Plus command line):
sqlplus username/password@database_alias
-- Example:
sqlplus hr/hr@orclpdbSQL command categories (same as standard SQL):
| Category | Commands |
|---|---|
| DDL | CREATE, ALTER, DROP, TRUNCATE, RENAME |
| DML | INSERT, UPDATE, DELETE, MERGE |
| DQL | SELECT |
| DCL | GRANT, REVOKE |
| TCL | COMMIT, ROLLBACK, SAVEPOINT |
| Category | Data Type | Description |
|---|---|---|
| Character | CHAR(n) |
Fixed-length string (max 2000 bytes) |
VARCHAR2(n) |
Variable-length string (max 4000 bytes) — preferred over CHAR | |
CLOB |
Character Large Object — for large text data | |
NVARCHAR2 |
Unicode variable-length string | |
| Numeric | NUMBER(p,s) |
Universal numeric type — p = precision (total digits), s = scale (digits after decimal) |
INTEGER |
Whole number (subtype of NUMBER) | |
FLOAT |
Floating-point number | |
| Date/Time | DATE |
Stores date and time (down to seconds); no separate "time-only" type |
TIMESTAMP |
Date and time with fractional seconds precision | |
TIMESTAMP WITH TIME ZONE |
Includes timezone information | |
INTERVAL YEAR TO MONTH / INTERVAL DAY TO SECOND |
Represents a duration | |
| Binary | BLOB |
Binary Large Object — images, files |
RAW(n) |
Small binary data | |
| Other | ROWID |
Unique physical address of a row |
LONG |
Legacy large text type (deprecated — use CLOB) |
Example:
CREATE TABLE products (
product_id NUMBER(6),
name VARCHAR2(100),
price NUMBER(10,2),
added_on DATE,
description CLOB
);Important note: Oracle has no native BOOLEAN type for table columns (BOOLEAN exists only within PL/SQL). Booleans in tables are commonly simulated with
NUMBER(1)(0/1) orCHAR(1)('Y'/'N').
Oracle organizes data using schemas (a schema is owned by a database user and contains that user's objects: tables, views, sequences, etc.) rather than separate "databases" the way MySQL does.
-- Viewing current user / schema
SELECT USER FROM DUAL;
-- DUAL is a special one-row, one-column dummy table used for evaluating expressions
SELECT SYSDATE FROM DUAL;
SELECT 5 * 3 FROM DUAL;Viewing objects:
SELECT table_name FROM user_tables; -- tables owned by current user
DESCRIBE employees; -- table structure (SQL*Plus/Developer)
SELECT * FROM user_indexes WHERE table_name = 'EMPLOYEES';CREATE TABLE employees (
emp_id NUMBER(6) PRIMARY KEY,
first_name VARCHAR2(50) NOT NULL,
last_name VARCHAR2(50) NOT NULL,
email VARCHAR2(100) UNIQUE,
salary NUMBER(10,2) DEFAULT 0,
dept_id NUMBER(4),
hire_date DATE DEFAULT SYSDATE
);ALTER TABLE employees ADD phone VARCHAR2(15); -- add column
ALTER TABLE employees DROP COLUMN phone; -- drop column
ALTER TABLE employees MODIFY salary NUMBER(12,2); -- change data type/size
ALTER TABLE employees RENAME COLUMN first_name TO fname; -- rename column
RENAME employees TO staff; -- rename table (own statement)DROP TABLE employees; -- deletes structure + data (goes to "recycle bin" by default)
DROP TABLE employees PURGE; -- deletes permanently, bypassing recycle bin
TRUNCATE TABLE employees; -- removes all rows instantly, keeps structure; cannot be rolled back
FLASHBACK TABLE employees TO BEFORE DROP; -- restores a dropped table from the recycle binINSERT INTO employees (emp_id, first_name, last_name, email, salary, dept_id, hire_date)
VALUES (101, 'Amit', 'Sharma', 'amit@company.com', 55000, 10, TO_DATE('2022-03-15','YYYY-MM-DD'));
-- Multi-row insert using INSERT ALL
INSERT ALL
INTO employees (emp_id, first_name, salary) VALUES (102, 'Priya', 62000)
INTO employees (emp_id, first_name, salary) VALUES (103, 'Rahul', 48000)
SELECT * FROM DUAL;SELECT * FROM employees;
SELECT first_name, salary FROM employees WHERE dept_id = 10;
SELECT DISTINCT dept_id FROM employees;UPDATE employees
SET salary = salary * 1.10
WHERE dept_id = 10;DELETE FROM employees WHERE emp_id = 105;Very commonly used in Oracle for synchronizing data between a source and a target table.
MERGE INTO employees tgt
USING new_hires src
ON (tgt.emp_id = src.emp_id)
WHEN MATCHED THEN
UPDATE SET tgt.salary = src.salary, tgt.dept_id = src.dept_id
WHEN NOT MATCHED THEN
INSERT (emp_id, first_name, last_name, salary, dept_id)
VALUES (src.emp_id, src.first_name, src.last_name, src.salary, src.dept_id);| Constraint | Purpose |
|---|---|
PRIMARY KEY |
Uniquely identifies each row |
FOREIGN KEY |
Enforces referential integrity between tables |
UNIQUE |
Ensures distinct values in a column |
NOT NULL |
Disallows empty values |
CHECK |
Restricts values to satisfy a condition |
DEFAULT |
Supplies a default value |
Example with named constraints (recommended in Oracle for clarity in error messages):
CREATE TABLE departments (
dept_id NUMBER(4) CONSTRAINT dept_pk PRIMARY KEY,
dept_name VARCHAR2(50) CONSTRAINT dept_name_nn NOT NULL
);
CREATE TABLE employees (
emp_id NUMBER(6) CONSTRAINT emp_pk PRIMARY KEY,
salary NUMBER(10,2) CONSTRAINT salary_chk CHECK (salary >= 0),
dept_id NUMBER(4),
CONSTRAINT emp_dept_fk FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
ON DELETE CASCADE
);Adding/dropping constraints after creation:
ALTER TABLE employees ADD CONSTRAINT email_uk UNIQUE (email);
ALTER TABLE employees DROP CONSTRAINT salary_chk;
ALTER TABLE employees DISABLE CONSTRAINT emp_dept_fk; -- temporarily disable
ALTER TABLE employees ENABLE CONSTRAINT emp_dept_fk; -- re-enableSELECT * FROM employees WHERE salary > 50000;
SELECT * FROM employees WHERE dept_id = 10 AND salary > 40000;
SELECT * FROM employees WHERE salary BETWEEN 40000 AND 60000;
SELECT * FROM employees WHERE dept_id IN (10, 20, 30);
SELECT * FROM employees WHERE first_name LIKE 'A%'; -- starts with 'A'
SELECT * FROM employees WHERE email IS NULL;
SELECT * FROM employees WHERE NOT (dept_id = 10);Oracle-specific concatenation operator ||:
SELECT first_name || ' ' || last_name AS full_name FROM employees;SELECT * FROM employees ORDER BY salary DESC;
SELECT * FROM employees ORDER BY dept_id, salary DESC;ROWNUM is a pseudo-column assigned to rows as they are retrieved, before any ORDER BY is applied — so limiting top-N results correctly requires a subquery.
-- WRONG (ROWNUM is assigned before sorting, so this doesn't reliably give the "top 5")
SELECT * FROM employees WHERE ROWNUM <= 5 ORDER BY salary DESC;
-- CORRECT: sort first in a subquery, then apply ROWNUM outside
SELECT * FROM (
SELECT * FROM employees ORDER BY salary DESC
)
WHERE ROWNUM <= 5;SELECT * FROM employees
ORDER BY salary DESC
FETCH FIRST 5 ROWS ONLY;
SELECT * FROM employees
ORDER BY salary DESC
OFFSET 5 ROWS FETCH NEXT 5 ROWS ONLY; -- pagination: skip 5, take next 5SELECT * FROM (
SELECT e.*, ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn
FROM employees e
)
WHERE rn <= 5;SELECT COUNT(*) FROM employees;
SELECT AVG(salary), MAX(salary), MIN(salary), SUM(salary) FROM employees;
SELECT dept_id, COUNT(*) AS emp_count, AVG(salary) AS avg_salary
FROM employees
GROUP BY dept_id;
SELECT dept_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY dept_id
HAVING AVG(salary) > 55000;GROUP BY with ROLLUP / CUBE (Oracle extensions for subtotals):
-- ROLLUP adds subtotal rows and a grand total
SELECT dept_id, job_id, SUM(salary)
FROM employees
GROUP BY ROLLUP(dept_id, job_id);
-- CUBE adds subtotals for every combination of grouped columns
SELECT dept_id, job_id, SUM(salary)
FROM employees
GROUP BY CUBE(dept_id, job_id);Oracle supports standard ANSI join syntax (recommended) as well as its own older "Oracle join" syntax using (+) for outer joins (legacy, but still seen in older codebases).
-- INNER JOIN
SELECT e.first_name, d.dept_name
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id;
-- LEFT OUTER JOIN
SELECT e.first_name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id;
-- RIGHT OUTER JOIN
SELECT e.first_name, d.dept_name
FROM employees e
RIGHT JOIN departments d ON e.dept_id = d.dept_id;
-- FULL OUTER JOIN (natively supported in Oracle, unlike MySQL)
SELECT e.first_name, d.dept_name
FROM employees e
FULL OUTER JOIN departments d ON e.dept_id = d.dept_id;The (+) symbol is placed on the side of the join that may have missing matches (i.e., the "optional" side).
-- Equivalent to a LEFT JOIN (all employees, matching departments if they exist)
SELECT e.first_name, d.dept_name
FROM employees e, departments d
WHERE e.dept_id = d.dept_id(+);
-- Equivalent to a RIGHT JOIN
SELECT e.first_name, d.dept_name
FROM employees e, departments d
WHERE e.dept_id(+) = d.dept_id;Modern Oracle style strongly favors ANSI
JOINsyntax — it's clearer, and(+)cannot express aFULL OUTER JOINat all.
SELECT e.first_name AS employee, m.first_name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.emp_id;SELECT e.first_name, d.dept_name
FROM employees e
CROSS JOIN departments d;-- Scalar subquery in WHERE
SELECT first_name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
-- Subquery with IN
SELECT first_name
FROM employees
WHERE dept_id IN (SELECT dept_id FROM departments WHERE dept_name = 'Sales');
-- Correlated subquery
SELECT first_name, salary, dept_id
FROM employees e1
WHERE salary > (
SELECT AVG(salary) FROM employees e2 WHERE e2.dept_id = e1.dept_id
);
-- EXISTS
SELECT dept_name
FROM departments d
WHERE EXISTS (SELECT 1 FROM employees e WHERE e.dept_id = d.dept_id);
-- Subquery in FROM (inline view)
SELECT dept_id, avg_sal
FROM (SELECT dept_id, AVG(salary) AS avg_sal FROM employees GROUP BY dept_id)
WHERE avg_sal > 50000;Oracle supports the full set of relational set operators (unlike older MySQL versions):
| Operator | Returns |
|---|---|
UNION |
Combined rows from both queries, duplicates removed |
UNION ALL |
Combined rows, duplicates kept (faster) |
INTERSECT |
Only rows that appear in both query results |
MINUS |
Rows in the first query result that do not appear in the second |
SELECT first_name FROM employees WHERE dept_id = 10
UNION
SELECT first_name FROM employees WHERE dept_id = 20;
SELECT emp_id FROM employees
INTERSECT
SELECT emp_id FROM project_assignments;
-- Employees who are also assigned to at least one project
SELECT emp_id FROM employees
MINUS
SELECT emp_id FROM project_assignments;
-- Employees who are NOT assigned to any projectA virtual table based on a stored query — always reflects live, current data.
CREATE VIEW high_earners AS
SELECT first_name, last_name, salary
FROM employees
WHERE salary > 60000;
SELECT * FROM high_earners;
CREATE OR REPLACE VIEW high_earners AS
SELECT first_name, last_name, salary, dept_id
FROM employees
WHERE salary > 65000;
DROP VIEW high_earners;Unlike a regular view, a materialized view physically stores the query's result set, and must be refreshed to reflect changes in the underlying tables — used heavily for reporting/data warehousing to avoid re-running expensive queries repeatedly.
CREATE MATERIALIZED VIEW dept_salary_summary
BUILD IMMEDIATE
REFRESH COMPLETE ON DEMAND
AS
SELECT dept_id, AVG(salary) AS avg_salary, COUNT(*) AS emp_count
FROM employees
GROUP BY dept_id;
-- Manually refreshing it later, after underlying data changes
EXEC DBMS_MVIEW.REFRESH('DEPT_SALARY_SUMMARY');Since Oracle has no AUTO_INCREMENT column attribute (unlike MySQL), sequences are used to generate unique, sequential numbers — typically for primary keys.
CREATE SEQUENCE emp_seq
START WITH 1
INCREMENT BY 1
NOCACHE
NOCYCLE;
-- Using the sequence
INSERT INTO employees (emp_id, first_name, salary)
VALUES (emp_seq.NEXTVAL, 'Neha', 58000);
SELECT emp_seq.CURRVAL FROM DUAL; -- current value (after at least one NEXTVAL call)
DROP SEQUENCE emp_seq;In Oracle 12c+, columns can also use identity columns (
GENERATED AS IDENTITY), which work similarly to MySQL'sAUTO_INCREMENTunder the hood using a sequence automatically:
CREATE TABLE customers (
cust_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
cust_name VARCHAR2(100)
);An alternate name for a database object (table, view, sequence, procedure) — useful for simplifying references, especially across schemas.
CREATE SYNONYM emp AS employees;
SELECT * FROM emp; -- same as SELECT * FROM employees
CREATE PUBLIC SYNONYM global_emp FOR hr.employees; -- accessible to all users
DROP SYNONYM emp;CREATE INDEX idx_lastname ON employees(last_name);
CREATE UNIQUE INDEX idx_email ON employees(email);
CREATE INDEX idx_dept_salary ON employees(dept_id, salary); -- composite index
-- Function-based index (indexes the result of an expression)
CREATE INDEX idx_upper_lastname ON employees(UPPER(last_name));
DROP INDEX idx_lastname;Oracle-specific index types:
| Type | Best suited for |
|---|---|
| B-Tree (default) | General-purpose, high-cardinality columns |
| Bitmap | Low-cardinality columns (e.g., gender, status flags) — common in data warehouses |
| Function-based | Indexing computed expressions (e.g., UPPER(col)) |
CREATE BITMAP INDEX idx_status ON employees(status);SELECT first_name || ' ' || last_name AS full_name FROM employees;
SELECT UPPER(first_name), LOWER(last_name) FROM employees;
SELECT LENGTH(first_name) FROM employees;
SELECT SUBSTR(first_name, 1, 3) FROM employees; -- first 3 characters
SELECT TRIM(' Hello ') FROM DUAL;
SELECT REPLACE(email, '@old.com', '@new.com') FROM employees;
SELECT INSTR('Hello World', 'World') FROM DUAL; -- position of substring: 7
SELECT LPAD(emp_id, 6, '0') FROM employees; -- left-pad with zerosSELECT ROUND(salary, 0) FROM employees;
SELECT CEIL(45.2) FROM DUAL; -- 46
SELECT FLOOR(45.8) FROM DUAL; -- 45
SELECT MOD(10, 3) FROM DUAL; -- 1
SELECT TRUNC(45.789, 1) FROM DUAL; -- 45.7 (truncates, doesn't round)SELECT SYSDATE FROM DUAL; -- current date & time
SELECT SYSTIMESTAMP FROM DUAL; -- current timestamp with fractional seconds
SELECT ADD_MONTHS(hire_date, 6) FROM employees; -- add 6 months
SELECT MONTHS_BETWEEN(SYSDATE, hire_date) FROM employees; -- difference in months
SELECT TO_CHAR(hire_date, 'DD-MON-YYYY') FROM employees; -- formatted output
SELECT TO_DATE('2026-07-12', 'YYYY-MM-DD') FROM DUAL; -- string to date
SELECT NEXT_DAY(SYSDATE, 'MONDAY') FROM DUAL; -- next occurring Monday
SELECT LAST_DAY(SYSDATE) FROM DUAL; -- last day of current monthPL/SQL (Procedural Language extensions to SQL) is Oracle's proprietary procedural language, combining SQL with programming constructs like variables, loops, conditionals, and exception handling — used for stored procedures, functions, packages, and triggers.
DECLARE
-- variable declarations (optional section)
v_name VARCHAR2(50);
v_salary NUMBER(10,2) := 50000; -- := is the assignment operator in PL/SQL
BEGIN
-- executable statements (mandatory section)
v_name := 'Amit';
DBMS_OUTPUT.PUT_LINE('Name: ' || v_name || ', Salary: ' || v_salary);
EXCEPTION
-- exception handling (optional section)
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('An error occurred.');
END;
/
DBMS_OUTPUT.PUT_LINEprints output to the console (requiresSET SERVEROUTPUT ONin SQL*Plus/Developer to display). The trailing/executes the PL/SQL block.
DECLARE
v_emp_salary employees.salary%TYPE; -- automatically matches the column's data type
BEGIN
SELECT salary INTO v_emp_salary FROM employees WHERE emp_id = 101;
DBMS_OUTPUT.PUT_LINE('Salary: ' || v_emp_salary);
END;
/DECLARE
v_salary NUMBER := 45000;
BEGIN
IF v_salary > 50000 THEN
DBMS_OUTPUT.PUT_LINE('High earner');
ELSIF v_salary > 30000 THEN
DBMS_OUTPUT.PUT_LINE('Mid-level earner');
ELSE
DBMS_OUTPUT.PUT_LINE('Entry-level earner');
END IF;
END;
/-- Basic LOOP
DECLARE
i NUMBER := 1;
BEGIN
LOOP
DBMS_OUTPUT.PUT_LINE('i = ' || i);
i := i + 1;
EXIT WHEN i > 5;
END LOOP;
END;
/
-- WHILE loop
DECLARE
i NUMBER := 1;
BEGIN
WHILE i <= 5 LOOP
DBMS_OUTPUT.PUT_LINE('i = ' || i);
i := i + 1;
END LOOP;
END;
/
-- FOR loop (most common — range is inclusive)
BEGIN
FOR i IN 1..5 LOOP
DBMS_OUTPUT.PUT_LINE('i = ' || i);
END LOOP;
END;
/A cursor is a pointer to the private memory area (context area) holding the result set of a query, allowing row-by-row processing.
Oracle automatically creates one for every DML/SELECT-INTO statement; its status can be checked via attributes like SQL%ROWCOUNT, SQL%FOUND.
BEGIN
UPDATE employees SET salary = salary * 1.05 WHERE dept_id = 10;
DBMS_OUTPUT.PUT_LINE(SQL%ROWCOUNT || ' rows updated.');
END;
/Declared and controlled manually — needed when processing multiple rows one at a time.
DECLARE
CURSOR emp_cursor IS
SELECT first_name, salary FROM employees WHERE dept_id = 10;
v_name employees.first_name%TYPE;
v_salary employees.salary%TYPE;
BEGIN
OPEN emp_cursor;
LOOP
FETCH emp_cursor INTO v_name, v_salary;
EXIT WHEN emp_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_name || ' earns ' || v_salary);
END LOOP;
CLOSE emp_cursor;
END;
/BEGIN
FOR emp_rec IN (SELECT first_name, salary FROM employees WHERE dept_id = 10) LOOP
DBMS_OUTPUT.PUT_LINE(emp_rec.first_name || ' earns ' || emp_rec.salary);
END LOOP;
END;
/DECLARE
CURSOR emp_cursor(p_dept_id NUMBER) IS
SELECT first_name, salary FROM employees WHERE dept_id = p_dept_id;
BEGIN
FOR emp_rec IN emp_cursor(20) LOOP
DBMS_OUTPUT.PUT_LINE(emp_rec.first_name);
END LOOP;
END;
/CREATE OR REPLACE PROCEDURE give_raise(
p_emp_id IN NUMBER,
p_percent IN NUMBER
)
IS
BEGIN
UPDATE employees
SET salary = salary + (salary * p_percent / 100)
WHERE emp_id = p_emp_id;
COMMIT;
END give_raise;
/
-- Calling the procedure
BEGIN
give_raise(101, 10);
END;
/
-- or, in SQL*Plus:
EXEC give_raise(101, 10);Procedure with an OUT parameter:
CREATE OR REPLACE PROCEDURE get_salary(
p_emp_id IN NUMBER,
p_salary OUT NUMBER
)
IS
BEGIN
SELECT salary INTO p_salary FROM employees WHERE emp_id = p_emp_id;
END get_salary;
/
DECLARE
v_salary NUMBER;
BEGIN
get_salary(101, v_salary);
DBMS_OUTPUT.PUT_LINE('Salary: ' || v_salary);
END;
/Must return exactly one value, and can be used directly within SQL expressions.
CREATE OR REPLACE FUNCTION annual_salary(p_monthly NUMBER)
RETURN NUMBER
IS
BEGIN
RETURN p_monthly * 12;
END annual_salary;
/
SELECT first_name, salary, annual_salary(salary) AS yearly_salary FROM employees;A package groups logically related procedures, functions, variables, and cursors into a single named unit — consisting of a specification (public interface) and a body (implementation). Packages promote modularity, encapsulation (private vs public members), and improved performance (loaded into memory as a unit).
Package Specification (declares what's publicly available):
CREATE OR REPLACE PACKAGE emp_pkg AS
PROCEDURE give_raise(p_emp_id NUMBER, p_percent NUMBER);
FUNCTION annual_salary(p_monthly NUMBER) RETURN NUMBER;
END emp_pkg;
/Package Body (contains the actual implementation):
CREATE OR REPLACE PACKAGE BODY emp_pkg AS
PROCEDURE give_raise(p_emp_id NUMBER, p_percent NUMBER) IS
BEGIN
UPDATE employees
SET salary = salary + (salary * p_percent / 100)
WHERE emp_id = p_emp_id;
COMMIT;
END give_raise;
FUNCTION annual_salary(p_monthly NUMBER) RETURN NUMBER IS
BEGIN
RETURN p_monthly * 12;
END annual_salary;
END emp_pkg;
/Calling package members:
BEGIN
emp_pkg.give_raise(101, 10);
END;
/
SELECT emp_pkg.annual_salary(salary) FROM employees;CREATE TABLE salary_audit (
audit_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
emp_id NUMBER,
old_salary NUMBER,
new_salary NUMBER,
changed_on DATE
);
CREATE OR REPLACE TRIGGER trg_salary_audit
BEFORE UPDATE OF salary ON employees
FOR EACH ROW
BEGIN
IF :NEW.salary <> :OLD.salary THEN
INSERT INTO salary_audit (emp_id, old_salary, new_salary, changed_on)
VALUES (:OLD.emp_id, :OLD.salary, :NEW.salary, SYSDATE);
END IF;
END;
/- In Oracle,
OLD/NEWare referenced with a leading colon::OLD.column,:NEW.column. - Trigger timing:
BEFORE/AFTER; trigger event:INSERT/UPDATE/DELETE; granularity:FOR EACH ROW(row-level) vs statement-level (noFOR EACH ROWclause).
UPDATE employees SET salary = 70000 WHERE emp_id = 101;
SELECT * FROM salary_audit; -- shows the automatically logged changeDisabling/enabling a trigger:
ALTER TRIGGER trg_salary_audit DISABLE;
ALTER TRIGGER trg_salary_audit ENABLE;
DROP TRIGGER trg_salary_audit;PL/SQL provides structured error handling using EXCEPTION blocks, distinguishing predefined, named user-defined, and unnamed (ORA-numbered) exceptions.
DECLARE
v_salary NUMBER;
BEGIN
SELECT salary INTO v_salary FROM employees WHERE emp_id = 9999; -- doesn't exist
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('No employee found with that ID.');
WHEN TOO_MANY_ROWS THEN
DBMS_OUTPUT.PUT_LINE('Query returned more than one row.');
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Unexpected error: ' || SQLERRM);
END;
/Common predefined exceptions:
| Exception | Raised When |
|---|---|
NO_DATA_FOUND |
A SELECT INTO returns no rows |
TOO_MANY_ROWS |
A SELECT INTO returns more than one row |
ZERO_DIVIDE |
Division by zero is attempted |
DUP_VAL_ON_INDEX |
A unique constraint/index is violated |
VALUE_ERROR |
A conversion/assignment error (e.g., value too large for a variable) |
OTHERS |
Catch-all for any exception not explicitly handled |
DECLARE
e_invalid_salary EXCEPTION;
v_salary NUMBER := -5000;
BEGIN
IF v_salary < 0 THEN
RAISE e_invalid_salary;
END IF;
EXCEPTION
WHEN e_invalid_salary THEN
DBMS_OUTPUT.PUT_LINE('Error: Salary cannot be negative.');
END;
/Used to raise a custom error with a specific error number (must be between -20000 and -20999) and message — commonly used inside procedures/triggers to reject invalid operations.
CREATE OR REPLACE PROCEDURE update_salary(p_emp_id NUMBER, p_new_salary NUMBER)
IS
BEGIN
IF p_new_salary < 0 THEN
RAISE_APPLICATION_ERROR(-20001, 'Salary cannot be negative.');
END IF;
UPDATE employees SET salary = p_new_salary WHERE emp_id = p_emp_id;
COMMIT;
END;
/UPDATE accounts SET balance = balance - 5000 WHERE acc_id = 1;
UPDATE accounts SET balance = balance + 5000 WHERE acc_id = 2;
COMMIT; -- makes changes permanent
-- On error or to undo:
ROLLBACK;
-- Partial rollback with SAVEPOINT
SAVEPOINT before_update;
UPDATE accounts SET balance = balance - 1000 WHERE acc_id = 1;
ROLLBACK TO before_update;Unlike MySQL (where
autocommitis often ON by default per session), Oracle does not auto-commit DML by default within a session — changes stay pending until an explicitCOMMIT(or are undone byROLLBACK, or auto-committed when the session ends normally).
Oracle uses row-level locking automatically for DML. Explicit locking can be requested:
SELECT * FROM employees WHERE emp_id = 101 FOR UPDATE;
-- Locks the selected row(s) until COMMIT/ROLLBACK, preventing other sessions
-- from updating them concurrently.
SELECT * FROM employees WHERE dept_id = 10 FOR UPDATE NOWAIT;
-- Fails immediately with an error if rows are already locked, instead of waiting.| Property | Meaning |
|---|---|
| Atomicity | All-or-nothing execution of a transaction |
| Consistency | Database remains in a valid state, respecting constraints |
| Isolation | Concurrent transactions don't see each other's uncommitted changes |
| Durability | Committed changes survive a crash |
-- Creating a user
CREATE USER app_user IDENTIFIED BY StrongPassword123;
-- Granting system privileges
GRANT CREATE SESSION TO app_user; -- allows the user to log in
GRANT CREATE TABLE TO app_user;
-- Granting object privileges
GRANT SELECT, INSERT, UPDATE ON hr.employees TO app_user;
-- Granting all privileges on an object
GRANT ALL ON hr.employees TO app_user;
-- Revoking
REVOKE INSERT ON hr.employees FROM app_user;
-- Roles group multiple privileges together for easier management
CREATE ROLE hr_manager;
GRANT SELECT, INSERT, UPDATE, DELETE ON hr.employees TO hr_manager;
GRANT hr_manager TO app_user;
-- Removing a user
DROP USER app_user CASCADE; -- CASCADE also drops all objects owned by the userSELECT
first_name, dept_id, salary,
RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS dept_rank,
AVG(salary) OVER (PARTITION BY dept_id) AS dept_avg_salary,
LAG(salary) OVER (ORDER BY salary) AS prev_salary,
LEAD(salary) OVER (ORDER BY salary) AS next_salary
FROM employees;RANK()/DENSE_RANK()/ROW_NUMBER()— assign ranking/row numbers within partitions.LAG()/LEAD()— access a preceding/following row's value without a self-join.
Used to query tree-structured data (e.g., an organizational chart) — Oracle's own syntax, predating standard recursive CTEs.
SELECT emp_id, first_name, manager_id, LEVEL
FROM employees
START WITH manager_id IS NULL -- root of the hierarchy (e.g., CEO)
CONNECT BY PRIOR emp_id = manager_id -- each row's manager_id must equal parent's emp_id
ORDER SIBLINGS BY first_name;LEVEL— a pseudo-column indicating depth in the hierarchy (root = 1).PRIOR— refers to the "parent" row's column value in the recursive step.
Standard recursive CTE (also supported in Oracle 11g R2+):
WITH org_chart (emp_id, first_name, manager_id, lvl) AS (
SELECT emp_id, first_name, manager_id, 1
FROM employees WHERE manager_id IS NULL
UNION ALL
SELECT e.emp_id, e.first_name, e.manager_id, oc.lvl + 1
FROM employees e
JOIN org_chart oc ON e.manager_id = oc.emp_id
)
SELECT * FROM org_chart ORDER BY lvl;Collections are PL/SQL's array-like data structures for holding multiple values in memory.
| Type | Description |
|---|---|
| Associative Array (Index-by table) | Key-value pairs, indexed by integer or string, size not fixed upfront |
| Nested Table | Ordered collection, can be stored in a database column |
| VARRAY | Fixed maximum size ordered collection |
DECLARE
TYPE salary_table IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
v_salaries salary_table;
BEGIN
v_salaries(1) := 50000;
v_salaries(2) := 62000;
DBMS_OUTPUT.PUT_LINE(v_salaries(1) + v_salaries(2)); -- 112000
END;
/Reduces context-switching between the SQL and PL/SQL engines when processing many rows.
DECLARE
TYPE id_table IS TABLE OF employees.emp_id%TYPE;
v_ids id_table;
BEGIN
SELECT emp_id BULK COLLECT INTO v_ids FROM employees WHERE dept_id = 10;
FORALL i IN v_ids.FIRST..v_ids.LAST
UPDATE employees SET salary = salary * 1.05 WHERE emp_id = v_ids(i);
COMMIT;
END;
/EXPLAIN PLAN FOR
SELECT * FROM employees WHERE dept_id = 10;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY); -- displays the execution planKey things to look for in an execution plan:
| Concept | What to look for |
|---|---|
| Full Table Scan | Reading every row — acceptable for small tables, costly for large ones |
| Index Range Scan | Efficiently using an index for a range/equality condition — usually good |
| Cost | Oracle's internal estimate of query expense — lower is generally better (relative, not absolute) |
| Cardinality | Estimated number of rows a step will return |
Common optimization practices:
- Index columns used in
WHERE,JOIN, andORDER BY. - Use bind variables (
:variable) in application code to enable SQL statement reuse and avoid re-parsing. - Avoid wrapping indexed columns in functions in
WHEREclauses (unless a matching function-based index exists). - Use
EXPLAIN PLAN/DBMS_XPLANand Oracle's SQL Tuning Advisor for slow queries. - Consider partitioning very large tables (by range, list, or hash) to improve manageability and query performance.
- Gather optimizer statistics regularly:
EXEC DBMS_STATS.GATHER_TABLE_STATS('HR','EMPLOYEES');
| Concept | One-line takeaway |
|---|---|
| Oracle Database | Enterprise RDBMS with rich PL/SQL support, schemas instead of separate "databases" |
| Data Types | VARCHAR2, NUMBER, DATE, CLOB, BLOB — no native table-level BOOLEAN |
| DDL/DML | Standard CREATE/ALTER/DROP, INSERT/UPDATE/DELETE, plus Oracle's MERGE for upserts |
| Constraints | PRIMARY KEY, FOREIGN KEY, UNIQUE, NOT NULL, CHECK — nameable for clearer errors |
| Row limiting | ROWNUM (careful with ordering) or modern FETCH FIRST...ROWS ONLY |
| Aggregates/Grouping | GROUP BY/HAVING, plus ROLLUP/CUBE for subtotal reporting |
| Joins | ANSI JOIN syntax preferred; legacy (+) syntax still seen; native FULL OUTER JOIN support |
| Subqueries | Scalar, IN, correlated, EXISTS, inline views — same concepts as standard SQL |
| Set Operators | UNION, UNION ALL, INTERSECT, MINUS |
| Views | Regular (virtual, live) vs Materialized (physically stored, manually/auto refreshed) |
| Sequences/Synonyms | SEQUENCE for auto-numbering; SYNONYM for aliasing objects |
| PL/SQL | Procedural blocks: DECLARE / BEGIN / EXCEPTION / END, with variables, loops, conditionals |
| Cursors | Implicit (automatic) vs explicit (OPEN/FETCH/CLOSE) row-by-row processing |
| Procedures/Functions | Reusable PL/SQL units; functions must return exactly one value |
| Packages | Bundle related procedures/functions/variables into one modular, encapsulated unit |
| Triggers | Auto-fire on DML events; use :OLD/:NEW to access row values |
| Exception Handling | Predefined (NO_DATA_FOUND, etc.), user-defined, and RAISE_APPLICATION_ERROR |
| Transactions | No autocommit by default; COMMIT/ROLLBACK/SAVEPOINT; row-level locking, FOR UPDATE |
| User Management | CREATE USER, GRANT/REVOKE privileges and roles |
| Analytic Functions | RANK, ROW_NUMBER, LAG, LEAD with OVER (PARTITION BY...) |
| Hierarchical Queries | Oracle's CONNECT BY PRIOR / START WITH, or standard recursive WITH CTEs |
| Collections | Associative arrays, nested tables, VARRAYs; BULK COLLECT/FORALL for performance |
| Performance | EXPLAIN PLAN/DBMS_XPLAN, indexing, bind variables, partitioning, fresh optimizer stats |
End of Oracle Notes