SQL DDL, DML, and PL/SQL

Unit 2CLO04, CLO05

SQL (Structured Query Language) is the standard language for relational databases, while PL/SQL (Procedural Language/SQL) extends SQL with procedural programming capabilities. Together, they provide complete database application development—from schema definition to complex business logic implementation.

The Basics

SQL Components:

1. DDL (Data Definition Language)
Defines database structure
Commands: CREATE, ALTER, DROP, TRUNCATE

2. DML (Data Manipulation Language)
Manipulates data
Commands: SELECT, INSERT, UPDATE, DELETE

3. DCL (Data Control Language)
Controls access
Commands: GRANT, REVOKE

4. TCL (Transaction Control Language)
Manages transactions
Commands: COMMIT, ROLLBACK, SAVEPOINT

PL/SQL:

Oracle's procedural extension to SQL that adds:

  • Variables and constants
  • Control structures (IF, LOOP, CASE)
  • Procedures and functions
  • Exception handling
  • Cursors for row-by-row processing
  • Triggers for automatic actions

PL/SQL Block Structure:

DECLARE
    -- Variable declarations
BEGIN
    -- Executable statements
EXCEPTION
    -- Exception handling
END;

Technical Details

DATA DEFINITION LANGUAGE (DDL)

CREATE TABLE:

CREATE TABLE table_name (
    column1 datatype constraints,
    column2 datatype constraints,
    ...
    table_constraints
);

Data Types:

  • Numeric: INT, DECIMAL(p,s), FLOAT, DOUBLE
  • String: CHAR(n), VARCHAR(n), TEXT
  • Date/Time: DATE, TIME, DATETIME, TIMESTAMP
  • Binary: BLOB, BYTEA
  • Boolean: BOOLEAN
  • JSON: JSON, JSONB (PostgreSQL)

Constraints:

  1. PRIMARY KEY:

    • Uniquely identifies each row
    • Cannot be NULL
    • One per table
    CREATE TABLE Employee (
        EmpID INT PRIMARY KEY,
        Name VARCHAR(100)
    );
    
  2. FOREIGN KEY:

    • References primary key in another table
    • Enforces referential integrity
    CREATE TABLE Order (
        OrderID INT PRIMARY KEY,
        CustomerID INT,
        FOREIGN KEY (CustomerID) REFERENCES Customer(CustomerID)
            ON DELETE CASCADE
            ON UPDATE CASCADE
    );
    

    Actions:

    • CASCADE: Delete/update related rows
    • SET NULL: Set foreign key to NULL
    • SET DEFAULT: Set to default value
    • NO ACTION/RESTRICT: Reject operation
  3. UNIQUE:

    • Ensures all values different
    • Can have multiple per table
    • Can be NULL (unless NOT NULL specified)
    Email VARCHAR(100) UNIQUE
    
  4. NOT NULL:

    • Value required
    Name VARCHAR(100) NOT NULL
    
  5. CHECK:

    • Custom constraint
    Age INT CHECK (Age >= 18 AND Age <= 100)
    Salary DECIMAL(10,2) CHECK (Salary > 0)
    
  6. DEFAULT:

    • Default value if not specified
    Status VARCHAR(20) DEFAULT 'Active'
    CreatedDate DATE DEFAULT CURRENT_DATE
    

ALTER TABLE:

-- Add column
ALTER TABLE Employee ADD Email VARCHAR(100);

-- Modify column
ALTER TABLE Employee MODIFY Salary DECIMAL(12,2);
-- or ALTER COLUMN in some databases

-- Drop column
ALTER TABLE Employee DROP COLUMN Email;

-- Add constraint
ALTER TABLE Employee ADD CONSTRAINT chk_salary CHECK (Salary > 0);

-- Drop constraint
ALTER TABLE Employee DROP CONSTRAINT chk_salary;

-- Rename table
ALTER TABLE Employee RENAME TO Staff;

DROP TABLE:

DROP TABLE Employee;  -- Delete table and data
DROP TABLE IF EXISTS Employee;  -- No error if doesn't exist

TRUNCATE TABLE:

TRUNCATE TABLE Employee;  -- Delete all rows, keep structure
-- Faster than DELETE, cannot rollback in some databases

CREATE INDEX:

CREATE INDEX idx_name ON Employee(Name);
CREATE UNIQUE INDEX idx_email ON Employee(Email);
CREATE INDEX idx_dept_sal ON Employee(DeptID, Salary);  -- Composite

CREATE VIEW:

CREATE VIEW HighEarners AS
SELECT Name, Salary, DeptID
FROM Employee
WHERE Salary > 50000;

-- Query view like a table
SELECT * FROM HighEarners WHERE DeptID = 10;

DATA MANIPULATION LANGUAGE (DML)

INSERT:

-- Single row
INSERT INTO Employee (EmpID, Name, DeptID, Salary)
VALUES (101, 'Alice', 10, 60000);

-- Multiple rows
INSERT INTO Employee VALUES
    (102, 'Bob', 20, 55000),
    (103, 'Charlie', 10, 70000);

-- From SELECT
INSERT INTO HighSalaryEmp
SELECT * FROM Employee WHERE Salary > 50000;

UPDATE:

-- Update specific rows
UPDATE Employee
SET Salary = Salary * 1.10
WHERE DeptID = 10;

-- Update multiple columns
UPDATE Employee
SET Salary = 65000, DeptID = 20
WHERE EmpID = 101;

-- Update with subquery
UPDATE Employee
SET Salary = (SELECT AVG(Salary) FROM Employee)
WHERE EmpID = 101;

DELETE:

-- Delete specific rows
DELETE FROM Employee WHERE DeptID = 30;

-- Delete all rows (slower than TRUNCATE)
DELETE FROM Employee;

-- Delete with subquery
DELETE FROM Employee
WHERE Salary < (SELECT AVG(Salary) FROM Employee);

SELECT:

-- Basic SELECT
SELECT Name, Salary FROM Employee;

-- WHERE clause
SELECT * FROM Employee WHERE Salary > 50000;

-- ORDER BY
SELECT * FROM Employee ORDER BY Salary DESC;

-- LIMIT/OFFSET (pagination)
SELECT * FROM Employee LIMIT 10 OFFSET 20;

-- DISTINCT
SELECT DISTINCT DeptID FROM Employee;

-- Aggregate functions
SELECT 
    DeptID,
    COUNT(*) AS NumEmployees,
    AVG(Salary) AS AvgSalary,
    MIN(Salary) AS MinSalary,
    MAX(Salary) AS MaxSalary,
    SUM(Salary) AS TotalSalary
FROM Employee
GROUP BY DeptID
HAVING AVG(Salary) > 55000;

-- Joins
SELECT e.Name, d.DeptName
FROM Employee e
INNER JOIN Department d ON e.DeptID = d.DeptID;

-- Subqueries
SELECT Name
FROM Employee
WHERE Salary > (SELECT AVG(Salary) FROM Employee);

-- EXISTS
SELECT Name
FROM Employee e
WHERE EXISTS (
    SELECT 1 FROM WorksOn w
    WHERE w.EmpID = e.EmpID
);

-- CASE expression
SELECT Name, Salary,
    CASE
        WHEN Salary < 50000 THEN 'Low'
        WHEN Salary BETWEEN 50000 AND 70000 THEN 'Medium'
        ELSE 'High'
    END AS SalaryGrade
FROM Employee;

PL/SQL BASICS

Block Structure:

DECLARE
    -- Variable declarations
    v_name VARCHAR2(100);
    v_salary NUMBER := 50000;  -- Default value
    c_bonus CONSTANT NUMBER := 0.10;  -- Constant
BEGIN
    -- Executable statements
    SELECT Name, Salary INTO v_name, v_salary
    FROM Employee WHERE EmpID = 101;
    
    DBMS_OUTPUT.PUT_LINE('Employee: ' || v_name);
    DBMS_OUTPUT.PUT_LINE('Salary: ' || v_salary);
EXCEPTION
    -- Exception handling
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('Employee not found');
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
END;
/

Variables and Data Types:

DECLARE
    v_count NUMBER;
    v_name VARCHAR2(100);
    v_hire_date DATE;
    v_is_active BOOLEAN := TRUE;
    
    -- %TYPE: Match column data type
    v_salary Employee.Salary%TYPE;
    
    -- %ROWTYPE: Match entire row
    v_emp Employee%ROWTYPE;
BEGIN
    v_count := 10;
    v_emp.Name := 'Alice';
END;
/

Control Structures:

IF Statement:

IF v_salary > 70000 THEN
    DBMS_OUTPUT.PUT_LINE('High earner');
ELSIF v_salary > 50000 THEN
    DBMS_OUTPUT.PUT_LINE('Medium earner');
ELSE
    DBMS_OUTPUT.PUT_LINE('Low earner');
END IF;

CASE Statement:

CASE v_grade
    WHEN 'A' THEN DBMS_OUTPUT.PUT_LINE('Excellent');
    WHEN 'B' THEN DBMS_OUTPUT.PUT_LINE('Good');
    ELSE DBMS_OUTPUT.PUT_LINE('Average');
END CASE;

Loops:

Simple LOOP:

LOOP
    v_count := v_count + 1;
    EXIT WHEN v_count > 10;
END LOOP;

WHILE LOOP:

WHILE v_count <= 10 LOOP
    v_count := v_count + 1;
END LOOP;

FOR LOOP:

FOR i IN 1..10 LOOP
    DBMS_OUTPUT.PUT_LINE(i);
END LOOP;

-- Reverse
FOR i IN REVERSE 1..10 LOOP
    DBMS_OUTPUT.PUT_LINE(i);
END LOOP;

CURSORS

Implicit Cursors: Automatically created by Oracle for SQL statements

BEGIN
    UPDATE Employee SET Salary = Salary * 1.10;
    
    DBMS_OUTPUT.PUT_LINE('Rows updated: ' || SQL%ROWCOUNT);
    
    IF SQL%FOUND THEN
        DBMS_OUTPUT.PUT_LINE('At least one row updated');
    END IF;
END;
/

Explicit Cursors: Programmer-declared for SELECT statements

DECLARE
    CURSOR emp_cursor IS
        SELECT EmpID, Name, Salary
        FROM Employee
        WHERE DeptID = 10;
    
    v_emp_id Employee.EmpID%TYPE;
    v_name Employee.Name%TYPE;
    v_salary Employee.Salary%TYPE;
BEGIN
    OPEN emp_cursor;
    
    LOOP
        FETCH emp_cursor INTO v_emp_id, v_name, v_salary;
        EXIT WHEN emp_cursor%NOTFOUND;
        
        DBMS_OUTPUT.PUT_LINE(v_name || ': ' || v_salary);
    END LOOP;
    
    CLOSE emp_cursor;
END;
/

Cursor FOR Loop: Simpler syntax

DECLARE
    CURSOR emp_cursor IS SELECT * FROM Employee;
BEGIN
    FOR emp_rec IN emp_cursor LOOP
        DBMS_OUTPUT.PUT_LINE(emp_rec.Name || ': ' || emp_rec.Salary);
    END LOOP;
END;
/

Cursor with Parameters:

DECLARE
    CURSOR emp_cursor(p_dept_id NUMBER) IS
        SELECT * FROM Employee WHERE DeptID = p_dept_id;
BEGIN
    FOR emp_rec IN emp_cursor(10) LOOP
        DBMS_OUTPUT.PUT_LINE(emp_rec.Name);
    END LOOP;
END;
/

PROCEDURES

CREATE OR REPLACE PROCEDURE GiveSalaryRaise(
    p_emp_id IN NUMBER,
    p_percentage IN NUMBER,
    p_new_salary OUT NUMBER
) IS
    v_current_salary NUMBER;
BEGIN
    -- Get current salary
    SELECT Salary INTO v_current_salary
    FROM Employee
    WHERE EmpID = p_emp_id;
    
    -- Calculate new salary
    p_new_salary := v_current_salary * (1 + p_percentage/100);
    
    -- Update
    UPDATE Employee
    SET Salary = p_new_salary
    WHERE EmpID = p_emp_id;
    
    COMMIT;
    
    DBMS_OUTPUT.PUT_LINE('Salary updated from ' || v_current_salary || 
                         ' to ' || p_new_salary);
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('Employee not found');
        RAISE;
    WHEN OTHERS THEN
        ROLLBACK;
        RAISE;
END;
/

-- Call procedure
DECLARE
    v_new_sal NUMBER;
BEGIN
    GiveSalaryRaise(101, 10, v_new_sal);
    DBMS_OUTPUT.PUT_LINE('New salary: ' || v_new_sal);
END;
/

FUNCTIONS

CREATE OR REPLACE FUNCTION CalculateAnnualSalary(
    p_emp_id IN NUMBER
) RETURN NUMBER IS
    v_monthly_salary NUMBER;
    v_annual_salary NUMBER;
BEGIN
    SELECT Salary INTO v_monthly_salary
    FROM Employee
    WHERE EmpID = p_emp_id;
    
    v_annual_salary := v_monthly_salary * 12;
    
    RETURN v_annual_salary;
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        RETURN NULL;
END;
/

-- Use in SQL
SELECT Name, CalculateAnnualSalary(EmpID) AS AnnualSalary
FROM Employee;

-- Use in PL/SQL
DECLARE
    v_annual NUMBER;
BEGIN
    v_annual := CalculateAnnualSalary(101);
    DBMS_OUTPUT.PUT_LINE('Annual: ' || v_annual);
END;
/

TRIGGERS

Automatically execute in response to events (INSERT, UPDATE, DELETE)

Types:

  • BEFORE Trigger: Executes before operation
  • AFTER Trigger: Executes after operation
  • Row-level Trigger: Once per affected row (FOR EACH ROW)
  • Statement-level Trigger: Once per statement
-- Audit trigger
CREATE OR REPLACE TRIGGER Audit_Salary_Changes
AFTER UPDATE OF Salary ON Employee
FOR EACH ROW
BEGIN
    INSERT INTO SalaryAudit (EmpID, OldSalary, NewSalary, ChangeDate, ChangedBy)
    VALUES (:OLD.EmpID, :OLD.Salary, :NEW.Salary, SYSDATE, USER);
END;
/

-- Validation trigger
CREATE OR REPLACE TRIGGER Check_Salary_Range
BEFORE INSERT OR UPDATE OF Salary ON Employee
FOR EACH ROW
BEGIN
    IF :NEW.Salary < 30000 OR :NEW.Salary > 200000 THEN
        RAISE_APPLICATION_ERROR(-20001, 'Salary must be between 30000 and 200000');
    END IF;
END;
/

-- Auto-increment trigger (before sequences were common)
CREATE OR REPLACE TRIGGER Employee_ID_Trigger
BEFORE INSERT ON Employee
FOR EACH ROW
BEGIN
    IF :NEW.EmpID IS NULL THEN
        SELECT Employee_Seq.NEXTVAL INTO :NEW.EmpID FROM DUAL;
    END IF;
END;
/

-- Prevent operations trigger
CREATE OR REPLACE TRIGGER Prevent_Weekend_Changes
BEFORE INSERT OR UPDATE OR DELETE ON Employee
BEGIN
    IF TO_CHAR(SYSDATE, 'DY') IN ('SAT', 'SUN') THEN
        RAISE_APPLICATION_ERROR(-20002, 'No changes allowed on weekends');
    END IF;
END;
/

Trigger Pseudo-records:

  • :OLD: Old column values (UPDATE, DELETE)
  • :NEW: New column values (INSERT, UPDATE)

EXCEPTION HANDLING

Pre-defined Exceptions:

BEGIN
    -- Some operations
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        -- SELECT returns no rows
        DBMS_OUTPUT.PUT_LINE('No data found');
        
    WHEN TOO_MANY_ROWS THEN
        -- SELECT INTO returns multiple rows
        DBMS_OUTPUT.PUT_LINE('Too many rows');
        
    WHEN DUP_VAL_ON_INDEX THEN
        -- Duplicate key violation
        DBMS_OUTPUT.PUT_LINE('Duplicate value');
        
    WHEN VALUE_ERROR THEN
        -- Arithmetic, conversion, truncation error
        DBMS_OUTPUT.PUT_LINE('Value error');
        
    WHEN ZERO_DIVIDE THEN
        DBMS_OUTPUT.PUT_LINE('Division by zero');
        
    WHEN OTHERS THEN
        -- Catch all
        DBMS_OUTPUT.PUT_LINE('Error: ' || SQLCODE || ' - ' || SQLERRM);
END;
/

User-defined Exceptions:

DECLARE
    e_salary_too_high EXCEPTION;
    v_salary NUMBER;
BEGIN
    SELECT Salary INTO v_salary FROM Employee WHERE EmpID = 101;
    
    IF v_salary > 150000 THEN
        RAISE e_salary_too_high;
    END IF;
    
    DBMS_OUTPUT.PUT_LINE('Salary is acceptable');
EXCEPTION
    WHEN e_salary_too_high THEN
        DBMS_OUTPUT.PUT_LINE('Salary exceeds maximum allowed');
END;
/

RAISE_APPLICATION_ERROR:

BEGIN
    IF some_condition THEN
        RAISE_APPLICATION_ERROR(-20001, 'Custom error message');
    END IF;
END;
/

PACKAGES

Group related procedures, functions, types together

-- Package specification (interface)
CREATE OR REPLACE PACKAGE EmployeePkg IS
    PROCEDURE HireEmployee(p_name VARCHAR2, p_dept_id NUMBER, p_salary NUMBER);
    FUNCTION GetEmployeeCount RETURN NUMBER;
    PROCEDURE FireEmployee(p_emp_id NUMBER);
END EmployeePkg;
/

-- Package body (implementation)
CREATE OR REPLACE PACKAGE BODY EmployeePkg IS
    
    PROCEDURE HireEmployee(p_name VARCHAR2, p_dept_id NUMBER, p_salary NUMBER) IS
    BEGIN
        INSERT INTO Employee (Name, DeptID, Salary)
        VALUES (p_name, p_dept_id, p_salary);
        COMMIT;
    END;
    
    FUNCTION GetEmployeeCount RETURN NUMBER IS
        v_count NUMBER;
    BEGIN
        SELECT COUNT(*) INTO v_count FROM Employee;
        RETURN v_count;
    END;
    
    PROCEDURE FireEmployee(p_emp_id NUMBER) IS
    BEGIN
        DELETE FROM Employee WHERE EmpID = p_emp_id;
        COMMIT;
    END;
    
END EmployeePkg;
/

-- Usage
BEGIN
    EmployeePkg.HireEmployee('John Doe', 10, 60000);
    DBMS_OUTPUT.PUT_LINE('Total employees: ' || EmployeePkg.GetEmployeeCount);
END;
/

Examples

All examples integrated in the technicalDepth section above with detailed syntax, use cases, and real-world scenarios.

Real-World Use

PRACTICAL TIPS:

Best Practices:

  1. Keep transactions short in procedures
  2. Always handle exceptions
  3. Use bind variables (:variable) not string concatenation
  4. Close cursors explicitly
  5. Use packages to organize code
  6. Comment complex logic
  7. Test with edge cases
  8. Use meaningful variable names (v_ for variables, p_ for parameters, c_ for cursors)

Performance:

  • Use bulk operations for large data sets (BULK COLLECT, FORALL)
  • Minimize context switches between SQL and PL/SQL
  • Use native compilation for frequently executed code
  • Cache frequently accessed data

Security:

  • Validate all input parameters
  • Use bind variables to prevent SQL injection
  • Grant minimal privileges
  • Encrypt sensitive data

Real-World Usage:

  • Banking: Procedures for transactions, triggers for audit trails
  • E-commerce: Functions for pricing, procedures for order processing
  • HR Systems: Procedures for payroll, triggers for employee changes
  • Inventory: Triggers for stock updates, functions for reorder calculations

For exams

IMPORTANT EXAM QUESTIONS:

DDL:

  1. Write CREATE TABLE with all constraint types
  2. Explain difference between DROP and TRUNCATE
  3. Add/modify/drop columns using ALTER TABLE
  4. Create indexes and views
  5. Define foreign key with CASCADE options

DML:

  1. Write INSERT with multiple rows
  2. UPDATE with subquery
  3. DELETE with JOIN
  4. Complex SELECT with aggregates, GROUP BY, HAVING
  5. Nested subqueries with EXISTS/NOT EXISTS

PL/SQL:

  1. Explain PL/SQL block structure
  2. Difference between procedure and function
  3. Write cursor with FOR loop
  4. Implement exception handling
  5. Create trigger for audit logging
  6. Explain :OLD and :NEW in triggers
  7. What are implicit vs explicit cursors?
  8. Write procedure with IN, OUT parameters
  9. Create package specification and body
  10. Handle user-defined exceptions

Common Questions:

  • Why use stored procedures?
    Answer: Performance, security, reusability, centralized logic
  • When to use triggers?
    Answer: Audit trails, validation, automatic calculations, referential integrity
  • Cursor vs bulk operations?
    Answer: Cursors for row-by-row, bulk for large datasets
  • Package benefits?
    Answer: Organization, encapsulation, performance (loaded once)

Quick Revision:

  • DDL: CREATE, ALTER, DROP, TRUNCATE
  • DML: INSERT, UPDATE, DELETE, SELECT
  • Constraints: PK, FK, UNIQUE, NOT NULL, CHECK, DEFAULT
  • PL/SQL: DECLARE-BEGIN-EXCEPTION-END
  • Cursor: OPEN, FETCH, CLOSE
  • Trigger: BEFORE/AFTER, FOR EACH ROW
  • Exception: NO_DATA_FOUND, TOO_MANY_ROWS, OTHERS

Key points

KEY TAKEAWAYS:

SQL is declarative (what), PL/SQL is procedural (how)

DDL defines structure: CREATE, ALTER, DROP, TRUNCATE

DML manipulates data: INSERT, UPDATE, DELETE, SELECT

Constraints enforce integrity: PRIMARY KEY, FOREIGN KEY, UNIQUE, NOT NULL, CHECK

PL/SQL adds programming: variables, loops, conditions, exceptions

Cursors enable row-by-row processing for complex logic

Procedures for actions (no return), functions for calculations (return value)

Triggers automate responses to data changes (INSERT/UPDATE/DELETE)

Exception handling ensures robustness and graceful error recovery

Packages organize related code into reusable modules

:OLD and :NEW in triggers access before/after values

Bind variables prevent SQL injection and improve performance

Stored procedures centralize business logic in database

REMEMBER: SQL handles set-based operations efficiently. Use PL/SQL when you need procedural logic, complex calculations, or row-by-row processing that can't be expressed in pure SQL!