📚 Learning Objectives
By the end of this lesson, you will be able to: - Create and execute stored procedures - Define user-defined functions - Use parameters in procedures and functions - Handle errors and exceptions - Understand security implications - Optimize database operations with stored code
🎯 Prerequisites
- Completed Lessons 1-12
- Understanding of SQL statements and data manipulation
- Knowledge of views and indexes
- Basic understanding of programming concepts
📖 Table of Contents
- Introduction to Stored Procedures
- Creating Stored Procedures
- Parameters and Variables
- Control Structures
- Error Handling
- User-Defined Functions
- Security Considerations
- Best Practices
- Common Pitfalls
1. Introduction to Stored Procedures
A stored procedure is a prepared SQL code that you can save and reuse. It's like a function in other programming languages - you can pass parameters and get return values.
Benefits of Stored Procedures
- Performance: Pre-compiled and optimized
- Security: Centralized business logic
- Maintainability: Single point of code maintenance
- Reusability: Can be called from multiple applications
- Network Efficiency: Reduce network traffic
When to Use Stored Procedures
- Complex business logic
- Data validation and constraints
- Batch operations
- Security-sensitive operations
- Performance-critical operations
2. Creating Stored Procedures
Basic Stored Procedure
-- Simple procedure to get student information
DELIMITER //
CREATE PROCEDURE GetStudentInfo(IN student_id_param INT)
BEGIN
SELECT
s.student_id,
s.first_name,
s.last_name,
s.email,
d.name AS department_name,
s.gpa
FROM students s
JOIN departments d ON s.department_id = d.department_id
WHERE s.student_id = student_id_param;
END //
DELIMITER ;
-- Call the procedure
CALL GetStudentInfo(1);
Procedure with Multiple Parameters
-- Procedure to add a new student
DELIMITER //
CREATE PROCEDURE AddStudent(
IN first_name_param VARCHAR(50),
IN last_name_param VARCHAR(50),
IN email_param VARCHAR(100),
IN department_id_param INT,
IN gpa_param DECIMAL(3,2)
)
BEGIN
INSERT INTO students (
first_name,
last_name,
email,
department_id,
gpa,
status,
enrollment_date
) VALUES (
first_name_param,
last_name_param,
email_param,
department_id_param,
gpa_param,
'Active',
CURDATE()
);
SELECT LAST_INSERT_ID() AS new_student_id;
END //
DELIMITER ;
-- Call the procedure
CALL AddStudent('John', 'Doe', 'john.doe@email.com', 1, 3.75);
3. Parameters and Variables
Parameter Types
-- Procedure with different parameter types
DELIMITER //
CREATE PROCEDURE UpdateStudentGPA(
IN student_id_param INT, -- Input parameter
IN new_gpa_param DECIMAL(3,2), -- Input parameter
OUT success_param BOOLEAN, -- Output parameter
OUT message_param VARCHAR(255) -- Output parameter
)
BEGIN
DECLARE student_exists INT DEFAULT 0;
-- Check if student exists
SELECT COUNT(*) INTO student_exists
FROM students
WHERE student_id = student_id_param;
IF student_exists > 0 THEN
-- Update the GPA
UPDATE students
SET gpa = new_gpa_param
WHERE student_id = student_id_param;
SET success_param = TRUE;
SET message_param = 'GPA updated successfully';
ELSE
SET success_param = FALSE;
SET message_param = 'Student not found';
END IF;
END //
DELIMITER ;
-- Call with output parameters
SET @success = FALSE;
SET @message = '';
CALL UpdateStudentGPA(1, 3.85, @success, @message);
SELECT @success, @message;
Local Variables
-- Procedure with local variables
DELIMITER //
CREATE PROCEDURE CalculateDepartmentStats(IN dept_id_param INT)
BEGIN
DECLARE total_students INT DEFAULT 0;
DECLARE avg_gpa DECIMAL(4,2) DEFAULT 0.00;
DECLARE dept_name VARCHAR(100);
-- Get department name
SELECT name INTO dept_name
FROM departments
WHERE department_id = dept_id_param;
-- Count students
SELECT COUNT(*) INTO total_students
FROM students
WHERE department_id = dept_id_param
AND gpa IS NOT NULL;
-- Calculate average GPA
IF total_students > 0 THEN
SELECT AVG(gpa) INTO avg_gpa
FROM students
WHERE department_id = dept_id_param
AND gpa IS NOT NULL;
END IF;
-- Return results
SELECT
dept_name AS department_name,
total_students,
avg_gpa,
CASE
WHEN avg_gpa >= 3.5 THEN 'Excellent'
WHEN avg_gpa >= 3.0 THEN 'Good'
WHEN avg_gpa >= 2.5 THEN 'Average'
ELSE 'Below Average'
END AS performance_rating;
END //
DELIMITER ;
-- Call the procedure
CALL CalculateDepartmentStats(1);
4. Control Structures
IF-ELSE Statements
-- Procedure with conditional logic
DELIMITER //
CREATE PROCEDURE EnrollStudent(
IN student_id_param INT,
IN course_id_param INT,
OUT result_param VARCHAR(255)
)
BEGIN
DECLARE student_exists INT DEFAULT 0;
DECLARE course_exists INT DEFAULT 0;
DECLARE already_enrolled INT DEFAULT 0;
DECLARE max_capacity INT DEFAULT 0;
DECLARE current_enrollments INT DEFAULT 0;
-- Check if student exists
SELECT COUNT(*) INTO student_exists
FROM students
WHERE student_id = student_id_param AND status = 'Active';
-- Check if course exists
SELECT COUNT(*) INTO course_exists
FROM courses
WHERE course_id = course_id_param;
-- Check if already enrolled
SELECT COUNT(*) INTO already_enrolled
FROM enrollments
WHERE student_id = student_id_param AND course_id = course_id_param;
-- Check course capacity
SELECT capacity,
(SELECT COUNT(*) FROM enrollments WHERE course_id = course_id_param)
INTO max_capacity, current_enrollments
FROM courses
WHERE course_id = course_id_param;
-- Decision logic
IF student_exists = 0 THEN
SET result_param = 'Error: Student not found or inactive';
ELSEIF course_exists = 0 THEN
SET result_param = 'Error: Course not found';
ELSEIF already_enrolled > 0 THEN
SET result_param = 'Error: Student already enrolled in this course';
ELSEIF current_enrollments >= max_capacity THEN
SET result_param = 'Error: Course is at maximum capacity';
ELSE
-- Enroll the student
INSERT INTO enrollments (student_id, course_id, enrollment_date)
VALUES (student_id_param, course_id_param, CURDATE());
SET result_param = 'Success: Student enrolled successfully';
END IF;
END //
DELIMITER ;
-- Test the procedure
SET @result = '';
CALL EnrollStudent(1, 1, @result);
SELECT @result;
Loops
-- Procedure with loop to generate sample data
DELIMITER //
CREATE PROCEDURE GenerateSampleGrades(IN course_id_param INT)
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE student_id_var INT;
DECLARE grade_var INT;
DECLARE enrollment_cursor CURSOR FOR
SELECT enrollment_id, student_id
FROM enrollments
WHERE course_id = course_id_param AND grade IS NULL;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN enrollment_cursor;
read_loop: LOOP
FETCH enrollment_cursor INTO enrollment_id_var, student_id_var;
IF done THEN
LEAVE read_loop;
END IF;
-- Generate random grade between 60 and 100
SET grade_var = FLOOR(RAND() * 41) + 60;
-- Update the enrollment with the grade
UPDATE enrollments
SET grade = grade_var
WHERE enrollment_id = enrollment_id_var;
END LOOP;
CLOSE enrollment_cursor;
SELECT CONCAT('Generated grades for course ID: ', course_id_param) AS result;
END //
DELIMITER ;
-- Call the procedure
CALL GenerateSampleGrades(1);
5. Error Handling
Basic Error Handling
-- Procedure with error handling
DELIMITER //
CREATE PROCEDURE SafeUpdateStudent(
IN student_id_param INT,
IN new_gpa_param DECIMAL(3,2)
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
-- Validate GPA range
IF new_gpa_param < 0 OR new_gpa_param > 4.0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'GPA must be between 0.0 and 4.0';
END IF;
-- Update student GPA
UPDATE students
SET gpa = new_gpa_param
WHERE student_id = student_id_param;
IF ROW_COUNT() = 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Student not found';
END IF;
COMMIT;
SELECT 'Student GPA updated successfully' AS result;
END //
DELIMITER ;
-- Test with valid data
CALL SafeUpdateStudent(1, 3.75);
-- Test with invalid data
CALL SafeUpdateStudent(1, 5.0);
Advanced Error Handling
-- Procedure with comprehensive error handling
DELIMITER //
CREATE PROCEDURE ProcessStudentEnrollment(
IN student_id_param INT,
IN course_id_param INT
)
BEGIN
DECLARE error_occurred BOOLEAN DEFAULT FALSE;
DECLARE error_message VARCHAR(255) DEFAULT '';
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
GET DIAGNOSTICS CONDITION 1
error_message = MESSAGE_TEXT;
ROLLBACK;
SELECT CONCAT('Error: ', error_message) AS result;
END;
START TRANSACTION;
-- Validation checks
IF NOT EXISTS (SELECT 1 FROM students WHERE student_id = student_id_param) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Invalid student ID';
END IF;
IF NOT EXISTS (SELECT 1 FROM courses WHERE course_id = course_id_param) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Invalid course ID';
END IF;
IF EXISTS (SELECT 1 FROM enrollments
WHERE student_id = student_id_param AND course_id = course_id_param) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Student already enrolled in this course';
END IF;
-- Perform enrollment
INSERT INTO enrollments (student_id, course_id, enrollment_date)
VALUES (student_id_param, course_id_param, CURDATE());
COMMIT;
SELECT 'Enrollment processed successfully' AS result;
END //
DELIMITER ;
6. User-Defined Functions
Scalar Functions
-- Function to calculate letter grade
DELIMITER //
CREATE FUNCTION GetLetterGrade(numeric_grade INT)
RETURNS VARCHAR(2)
READS SQL DATA
DETERMINISTIC
BEGIN
DECLARE letter_grade VARCHAR(2);
IF numeric_grade >= 90 THEN
SET letter_grade = 'A';
ELSEIF numeric_grade >= 80 THEN
SET letter_grade = 'B';
ELSEIF numeric_grade >= 70 THEN
SET letter_grade = 'C';
ELSEIF numeric_grade >= 60 THEN
SET letter_grade = 'D';
ELSE
SET letter_grade = 'F';
END IF;
RETURN letter_grade;
END //
DELIMITER ;
-- Use the function
SELECT
e.enrollment_id,
s.first_name,
s.last_name,
c.course_code,
e.grade,
GetLetterGrade(e.grade) AS letter_grade
FROM enrollments e
JOIN students s ON e.student_id = s.student_id
JOIN courses c ON e.course_id = c.course_id
WHERE e.grade IS NOT NULL;
Table-Valued Functions (MySQL doesn't support table-valued functions, but we can create procedures that return result sets)
-- Procedure that acts like a table-valued function
DELIMITER //
CREATE PROCEDURE GetTopStudents(IN limit_param INT)
BEGIN
SELECT
s.student_id,
s.first_name,
s.last_name,
d.name AS department_name,
s.gpa,
COUNT(e.enrollment_id) AS total_courses,
AVG(e.grade) AS average_grade
FROM students s
JOIN departments d ON s.department_id = d.department_id
LEFT JOIN enrollments e ON s.student_id = e.student_id
WHERE s.status = 'Active'
AND s.gpa IS NOT NULL
AND e.grade IS NOT NULL
GROUP BY s.student_id, s.first_name, s.last_name, d.name, s.gpa
ORDER BY s.gpa DESC, average_grade DESC
LIMIT limit_param;
END //
DELIMITER ;
-- Call the procedure
CALL GetTopStudents(10);
7. Security Considerations
SQL Injection Prevention
-- Secure procedure with parameterized queries
DELIMITER //
CREATE PROCEDURE GetStudentByEmail(IN email_param VARCHAR(100))
BEGIN
-- This is automatically protected against SQL injection
-- because we're using parameters
SELECT
student_id,
first_name,
last_name,
email,
status
FROM students
WHERE email = email_param;
END //
DELIMITER ;
-- ❌ DANGEROUS: Don't do this
-- CREATE PROCEDURE BadProcedure(IN query_param TEXT)
-- BEGIN
-- SET @sql = CONCAT('SELECT * FROM students WHERE ', query_param);
-- PREPARE stmt FROM @sql;
-- EXECUTE stmt;
-- DEALLOCATE PREPARE stmt;
-- END;
Privilege Management
-- Grant execute privilege to specific user
GRANT EXECUTE ON PROCEDURE school_db.GetStudentInfo TO 'student_user'@'localhost';
-- Create procedure with definer rights (runs with creator's privileges)
DELIMITER //
CREATE DEFINER='admin'@'localhost' PROCEDURE AdminOnlyProcedure()
SQL SECURITY DEFINER
BEGIN
-- This procedure runs with admin privileges
SELECT * FROM sensitive_table;
END //
DELIMITER ;
-- Create procedure with invoker rights (runs with caller's privileges)
DELIMITER //
CREATE PROCEDURE UserSpecificProcedure()
SQL SECURITY INVOKER
BEGIN
-- This procedure runs with caller's privileges
SELECT * FROM user_accessible_table;
END //
DELIMITER ;
8. Best Practices
Naming Conventions
-- Use descriptive names
CREATE PROCEDURE sp_GetStudentAcademicHistory(IN student_id INT);
CREATE FUNCTION fn_CalculateGPA(grade1 DECIMAL, grade2 DECIMAL) RETURNS DECIMAL;
-- Use prefixes for clarity
-- sp_ = stored procedure
-- fn_ = function
-- tr_ = trigger (next lesson)
Documentation
-- Well-documented procedure
DELIMITER //
-- Procedure: sp_EnrollStudentInCourse
-- Purpose: Enrolls a student in a course with validation
-- Parameters:
-- - student_id_param: ID of the student to enroll
-- - course_id_param: ID of the course to enroll in
-- Returns: Success/error message
-- Author: Database Team
-- Created: 2024-01-15
-- Modified: 2024-01-15
CREATE PROCEDURE sp_EnrollStudentInCourse(
IN student_id_param INT,
IN course_id_param INT
)
BEGIN
-- Implementation here
END //
DELIMITER ;
Performance Optimization
-- Efficient procedure with proper indexing
DELIMITER //
CREATE PROCEDURE GetStudentPerformance(IN student_id_param INT)
BEGIN
-- Use indexed columns in WHERE clauses
SELECT
s.first_name,
s.last_name,
c.course_code,
e.grade,
e.semester
FROM students s
JOIN enrollments e ON s.student_id = e.student_id -- Indexed
JOIN courses c ON e.course_id = c.course_id -- Indexed
WHERE s.student_id = student_id_param -- Indexed
ORDER BY e.semester DESC, c.course_code; -- Indexed columns
END //
DELIMITER ;
9. Common Pitfalls
Pitfall 1: Missing DELIMITER Changes
-- ❌ ERROR: Missing delimiter change
CREATE PROCEDURE BadProcedure()
BEGIN
SELECT * FROM students;
END; -- This will cause syntax error
-- ✅ CORRECT: Use delimiter changes
DELIMITER //
CREATE PROCEDURE GoodProcedure()
BEGIN
SELECT * FROM students;
END //
DELIMITER ;
Pitfall 2: Not Handling NULL Values
-- ❌ POTENTIAL ISSUE: Not handling NULL values
CREATE FUNCTION BadFunction(grade INT)
RETURNS VARCHAR(10)
BEGIN
IF grade >= 90 THEN
RETURN 'A';
-- Missing NULL check
END;
-- ✅ BETTER: Handle NULL values
CREATE FUNCTION GoodFunction(grade INT)
RETURNS VARCHAR(10)
BEGIN
IF grade IS NULL THEN
RETURN 'N/A';
ELSEIF grade >= 90 THEN
RETURN 'A';
END IF;
END;
Pitfall 3: Infinite Loops
-- ❌ DANGEROUS: Potential infinite loop
DELIMITER //
CREATE PROCEDURE BadLoop()
BEGIN
DECLARE counter INT DEFAULT 0;
WHILE counter < 10 DO
-- Missing counter increment - infinite loop!
SELECT counter;
END WHILE;
END //
DELIMITER ;
-- ✅ CORRECT: Proper loop control
DELIMITER //
CREATE PROCEDURE GoodLoop()
BEGIN
DECLARE counter INT DEFAULT 0;
WHILE counter < 10 DO
SELECT counter;
SET counter = counter + 1; -- Increment counter
END WHILE;
END //
DELIMITER ;
🎯 Summary
Stored procedures and functions are powerful tools for database programming:
Stored Procedures
- Reusable code blocks for complex operations
- Parameters for input and output
- Control structures for logic flow
- Error handling for robust operations
- Security through centralized business logic
User-Defined Functions
- Scalar functions for calculations and transformations
- Deterministic and non-deterministic options
- Performance benefits for repeated calculations
- Reusability across queries and procedures
Key Takeaways
- Use procedures for complex business logic and data manipulation
- Use functions for calculations and data transformations
- Always handle errors and validate input parameters
- Follow security best practices to prevent SQL injection
- Document your code for maintainability
- Test thoroughly before deploying to production
🔄 Next Steps
- Practice creating stored procedures for your common operations
- Experiment with user-defined functions
- Move on to Lesson 14: Triggers
- Consider security implications in your database design
📚 Additional Resources
- MySQL Documentation: Stored Procedures
- MySQL Documentation: Functions
- MySQL Documentation: Error Handling