Site logo
Tác giả
  • avatar Nguyễn Đức Xinh
    Name
    Nguyễn Đức Xinh
    Twitter
Ngày xuất bản
Ngày xuất bản

Procedures và Functions trong MySQL: Hướng dẫn toàn diện về stored routines

Procedures và Functions trong MySQL là gì?

Stored Procedures và Functions trong MySQL là những đoạn code SQL được lưu trữ trong database server và có thể được gọi lại nhiều lần. Chúng cho phép đóng gói business logic phức tạp, giảm network traffic, và tăng tính bảo mật cho ứng dụng.

Stored Procedures là tập hợp các SQL statements được lưu trữ và thực thi trên database server. Functions tương tự như procedures nhưng luôn trả về một giá trị và có thể được sử dụng trong SQL expressions.

Ưu điểm của Procedures và Functions

  1. Performance Optimization: Xử lý dữ liệu ngay trên database server, execution plan có thể được cache trong session, giảm thời gian parsing và execution với các thao tác lặp lại.

  2. Network Traffic Reduction: Thay vì gửi hàng nghìn rows về application để xử lý rồi ghi ngược lại, toàn bộ logic chạy trên server — chỉ trả về kết quả cuối cùng. Đây là lợi thế lớn nhất khi làm việc với dữ liệu lớn.

  3. Code Reusability: Một lần viết, nhiều ứng dụng/service (kể cả viết bằng ngôn ngữ khác nhau) đều có thể gọi chung.

  4. Security Enhancement: Có thể chỉ cấp quyền EXECUTE thay vì cấp quyền trực tiếp trên bảng; parameters giúp giảm rủi ro SQL injection.

  5. Data Consistency: Các thao tác nhiều bước (transaction, ràng buộc dữ liệu) được đóng gói tại một nơi, đảm bảo tính nhất quán dù được gọi từ đâu.

Nhược điểm của Procedures và Functions

  1. Khó maintain và quản lý phiên bản: Code nằm trong database nên khó quản lý bằng Git, khó review, khó đồng bộ giữa các môi trường (dev/staging/production). Thay đổi logic phải chạy lại DROP/CREATE thay vì deploy code thông thường.

  2. Khó debug và test: MySQL không có debugger chuẩn cho stored routines, không có unit test framework phổ biến như PHPUnit, Jest. Việc tìm lỗi thường chỉ dựa vào log thủ công.

  3. Business logic bị phân tán: Một phần logic nằm ở application, một phần nằm trong database — developer mới rất khó nắm được luồng xử lý đầy đủ.

  4. Khó scale: Application server có thể scale ngang dễ dàng, trong khi database thường là điểm nghẽn. Đẩy nhiều logic tính toán xuống database làm tăng tải cho thành phần khó scale nhất của hệ thống.

  5. Phụ thuộc vào database vendor: Cú pháp stored routines của MySQL khác PostgreSQL, SQL Server, Oracle — việc chuyển đổi database gần như phải viết lại toàn bộ.

  6. Ngôn ngữ hạn chế: Cú pháp procedural của MySQL nghèo nàn so với PHP, JavaScript, TypeScript: thiếu thư viện, xử lý chuỗi/JSON/ngày tháng phức tạp dài dòng, không tận dụng được ecosystem của ngôn ngữ lập trình.

Lưu ý: Khi nào nên (và không nên) dùng Stored Routines?

Phần lớn ứng dụng hiện đại xử lý business logic ở phía application (PHP/Laravel, Node.js/NestJS, Java/Spring, Python/Django, .NET...), kết hợp với ORM hoặc Query Builder. Database chủ yếu đảm nhiệm vai trò lưu trữ và truy vấn dữ liệu.

Nên cân nhắc dùng Stored Procedures/Functions trong một số trường hợp đặc thù:

  • Xử lý dữ liệu lớn (batch update, data migration, tổng hợp báo cáo hàng triệu rows) mà việc kéo dữ liệu về application quá tốn kém
  • Các tác vụ định kỳ chạy trực tiếp trên database (kết hợp với MySQL Event Scheduler)
  • Hệ thống legacy đã dùng sẵn stored routines, hoặc nhiều ứng dụng khác ngôn ngữ cùng dùng chung một logic dữ liệu
  • Các function tiện ích đơn giản, ổn định, ít thay đổi (format dữ liệu, tính toán thuần)

Không nên lạm dụng stored routines:

  • Không đặt toàn bộ business logic (quy trình đặt hàng, thanh toán, phân quyền...) vào database
  • Không dùng thay cho những gì application làm tốt hơn: validation, gọi API bên ngoài, gửi email, xử lý logic phức tạp thường xuyên thay đổi
  • Nếu dùng, hãy lưu code routines trong migration files (ví dụ Laravel migration) để quản lý bằng Git và đồng bộ giữa các môi trường

Các ví dụ trong bài viết mang tính minh họa cú pháp và khả năng của stored routines. Khi áp dụng thực tế, hãy cân nhắc kỹ giữa hiệu năng và chi phí bảo trì lâu dài.

So sánh Procedures vs Functions

Đặc điểm Stored Procedures Functions
Return Value Không bắt buộc Luôn trả về giá trị
Usage Gọi với CALL Dùng trong SELECT, WHERE
Parameters IN, OUT, INOUT Chỉ IN parameters
Transactions Có thể chứa Không thể chứa
DML Operations Cho phép Hạn chế
Recursion Hạn chế Cho phép
Called From Applications SQL statements

Cheat Sheet: Tổng hợp nhanh cú pháp

Stored Procedures

-- ✅ Tạo
DELIMITER //
CREATE PROCEDURE proc_name(IN p_id INT, OUT p_total INT)
BEGIN
    SELECT COUNT(*) INTO p_total FROM orders WHERE customer_id = p_id;
END //
DELIMITER ;

-- ✅ Gọi
CALL proc_name(1, @total);
SELECT @total;

-- ✅ Liệt kê
SHOW PROCEDURE STATUS WHERE Db = 'your_database';

-- ✅ Xem định nghĩa
SHOW CREATE PROCEDURE proc_name;

-- ✅ Xoá
DROP PROCEDURE IF EXISTS proc_name;

Functions

-- ✅ Tạo
DELIMITER //
CREATE FUNCTION func_name(p_price DECIMAL(10,2))
RETURNS DECIMAL(10,2)
DETERMINISTIC
BEGIN
    RETURN p_price * 1.1;
END //
DELIMITER ;

-- ✅ Gọi
SELECT func_name(100);
SELECT id, func_name(price) FROM products;

-- ✅ Liệt kê
SHOW FUNCTION STATUS WHERE Db = 'your_database';

-- ✅ Xem định nghĩa
SHOW CREATE FUNCTION func_name;

-- ✅ Xoá
DROP FUNCTION IF EXISTS func_name;

Quản lý chung

-- ✅ Liệt kê tất cả routines qua INFORMATION_SCHEMA
SELECT ROUTINE_NAME, ROUTINE_TYPE, CREATED, LAST_ALTERED
FROM INFORMATION_SCHEMA.ROUTINES
WHERE ROUTINE_SCHEMA = 'your_database';

-- ✅ Sửa: MySQL không hỗ trợ sửa body → DROP rồi CREATE lại
-- (ALTER chỉ đổi được thuộc tính như COMMENT, SQL SECURITY)
ALTER PROCEDURE proc_name COMMENT 'Mô tả mới';

-- ✅ Cấp / thu hồi quyền thực thi
GRANT EXECUTE ON PROCEDURE your_database.proc_name TO 'app_user'@'%';
REVOKE EXECUTE ON PROCEDURE your_database.proc_name FROM 'app_user'@'%';

-- ✅ Cho phép tạo function khi bật binary log (lỗi 1418)
SET GLOBAL log_bin_trust_function_creators = 1;

Checklist nhanh

Thao tác Procedure Function
Tạo CREATE PROCEDURE CREATE FUNCTION ... RETURNS
Gọi CALL proc_name() SELECT func_name()
Liệt kê SHOW PROCEDURE STATUS SHOW FUNCTION STATUS
Xem code SHOW CREATE PROCEDURE SHOW CREATE FUNCTION
Xoá DROP PROCEDURE IF EXISTS DROP FUNCTION IF EXISTS
Sửa body DROP + CREATE DROP + CREATE
Cấp quyền GRANT EXECUTE ON PROCEDURE GRANT EXECUTE ON FUNCTION

💡 Luôn dùng DELIMITER // khi tạo routine trong MySQL CLI — xem chi tiết ở mục DELIMITER trong MySQL bên dưới.

DELIMITER trong MySQL

DELIMITER là gì?

Mặc định MySQL client dùng dấu ; để biết một câu lệnh kết thúc và gửi nó lên server. Nhưng body của routine (BEGIN ... END) lại chứa nhiều dấu ; — nếu không đổi delimiter, client sẽ cắt câu lệnh CREATE ngay tại dấu ; đầu tiên và báo lỗi cú pháp.

-- ❌ Lỗi: client gửi lên server tới "... FROM customers;" rồi dừng
CREATE PROCEDURE GetCustomerCount()
BEGIN
    SELECT COUNT(*) FROM customers;
END;

-- ✅ Đổi delimiter tạm thời sang // → client chỉ gửi khi gặp //
DELIMITER //
CREATE PROCEDURE GetCustomerCount()
BEGIN
    SELECT COUNT(*) FROM customers;
END //
DELIMITER ;   -- Trả delimiter về lại ;

⚠️ DELIMITER là lệnh của MySQL client (mysql CLI, Workbench, phpMyAdmin...), không phải câu lệnh SQL. Server MySQL không bao giờ nhận được lệnh này.

DELIMITER // và DELIMITER $$ khác gì nhau?

Không khác gì cả. //, $$, ;; chỉ là ký hiệu tự chọn làm dấu kết thúc câu lệnh ($$ hay gặp trong Workbench, phpMyAdmin). Chỉ cần ký hiệu đó không xuất hiện trong body và END kết thúc đúng ký hiệu đã khai báo.

Stored Procedures

1. Tạo Stored Procedure cơ bản

-- Cú pháp cơ bản
DELIMITER //
CREATE PROCEDURE procedure_name(
    [IN | OUT | INOUT] parameter_name datatype,
    ...
)
BEGIN
    -- SQL statements
END //
DELIMITER ;

-- Ví dụ đơn giản
DELIMITER //
CREATE PROCEDURE GetCustomerCount()
BEGIN
    SELECT COUNT(*) as customer_count FROM customers;
END //
DELIMITER ;

-- Gọi procedure
CALL GetCustomerCount();

2. Parameters trong Procedures

-- IN Parameters (input only)
DELIMITER //
CREATE PROCEDURE GetCustomerById(
    IN customer_id INT
)
BEGIN
    SELECT * FROM customers WHERE id = customer_id;
END //
DELIMITER ;

-- OUT Parameters (output only)
DELIMITER //
CREATE PROCEDURE GetCustomerCountByCity(
    IN city_name VARCHAR(50),
    OUT customer_count INT
)
BEGIN
    SELECT COUNT(*) INTO customer_count 
    FROM customers 
    WHERE city = city_name;
END //
DELIMITER ;

-- INOUT Parameters (both input and output)
DELIMITER //
CREATE PROCEDURE CalculateTax(
    INOUT amount DECIMAL(10,2),
    IN tax_rate DECIMAL(5,4)
)
BEGIN
    SET amount = amount * (1 + tax_rate);
END //
DELIMITER ;

-- Sử dụng các procedures
CALL GetCustomerById(123);

CALL GetCustomerCountByCity('New York', @count);
SELECT @count;

SET @price = 1000;
CALL CalculateTax(@price, 0.08);
SELECT @price; -- Kết quả: 1080.00

3. Control Structures

-- IF Statement
DELIMITER //
CREATE PROCEDURE ClassifyCustomer(
    IN customer_id INT,
    OUT customer_class VARCHAR(20)
)
BEGIN
    DECLARE total_orders INT DEFAULT 0;
    DECLARE total_amount DECIMAL(12,2) DEFAULT 0;
    
    SELECT COUNT(*), COALESCE(SUM(total_amount), 0)
    INTO total_orders, total_amount
    FROM orders 
    WHERE customer_id = customer_id;
    
    IF total_amount > 50000 THEN
        SET customer_class = 'VIP';
    ELSEIF total_amount > 20000 THEN
        SET customer_class = 'Premium';
    ELSEIF total_amount > 5000 THEN
        SET customer_class = 'Regular';
    ELSE
        SET customer_class = 'New';
    END IF;
END //
DELIMITER ;

-- CASE Statement
DELIMITER //
CREATE PROCEDURE GetDiscountRate(
    IN customer_type VARCHAR(20),
    OUT discount_rate DECIMAL(5,4)
)
BEGIN
    CASE customer_type
        WHEN 'VIP' THEN SET discount_rate = 0.15;
        WHEN 'Premium' THEN SET discount_rate = 0.10;
        WHEN 'Regular' THEN SET discount_rate = 0.05;
        ELSE SET discount_rate = 0.00;
    END CASE;
END //
DELIMITER ;

-- WHILE Loop
DELIMITER //
CREATE PROCEDURE GenerateSequence(
    IN max_num INT
)
BEGIN
    DECLARE counter INT DEFAULT 1;
    
    DROP TEMPORARY TABLE IF EXISTS temp_sequence;
    CREATE TEMPORARY TABLE temp_sequence (
        id INT,
        value INT
    );
    
    WHILE counter <= max_num DO
        INSERT INTO temp_sequence VALUES (counter, counter * counter);
        SET counter = counter + 1;
    END WHILE;
    
    SELECT * FROM temp_sequence;
END //
DELIMITER ;

-- FOR Loop (MySQL 8.0+)
DELIMITER //
CREATE PROCEDURE ProcessMonthlyReports()
BEGIN
    DECLARE done INT DEFAULT FALSE;
    DECLARE month_num INT;
    
    FOR month_num IN 1..12 DO
        -- Process each month
        INSERT INTO monthly_reports (month, processed_date)
        VALUES (month_num, NOW());
    END FOR;
END //
DELIMITER ;

4. Error Handling

DELIMITER //
CREATE PROCEDURE SafeTransferMoney(
    IN from_account VARCHAR(20),
    IN to_account VARCHAR(20),
    IN transfer_amount DECIMAL(10,2),
    OUT result_message VARCHAR(255)
)
BEGIN
    DECLARE v_from_balance DECIMAL(10,2);
    DECLARE v_error_count INT DEFAULT 0;
    
    -- Error handlers
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        GET DIAGNOSTICS CONDITION 1
            @sqlstate = RETURNED_SQLSTATE,
            @errno = MYSQL_ERRNO,
            @text = MESSAGE_TEXT;
        SET result_message = CONCAT('Error: ', @errno, ' - ', @text);
    END;
    
    DECLARE EXIT HANDLER FOR SQLWARNING
    BEGIN
        ROLLBACK;
        SET result_message = 'Warning occurred, transaction rolled back';
    END;
    
    START TRANSACTION;
    
    -- Check source account balance
    SELECT balance INTO v_from_balance 
    FROM accounts 
    WHERE account_number = from_account;
    
    IF v_from_balance IS NULL THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Source account not found';
    END IF;
    
    IF v_from_balance < transfer_amount THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insufficient funds';
    END IF;
    
    -- Perform transfer
    UPDATE accounts 
    SET balance = balance - transfer_amount 
    WHERE account_number = from_account;
    
    UPDATE accounts 
    SET balance = balance + transfer_amount 
    WHERE account_number = to_account;
    
    -- Check if destination account exists
    IF ROW_COUNT() = 0 THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Destination account not found';
    END IF;
    
    COMMIT;
    SET result_message = 'Transfer completed successfully';
END //
DELIMITER ;

-- Test error handling
CALL SafeTransferMoney('ACC001', 'ACC002', 500.00, @msg);
SELECT @msg;

Functions

1. Tạo Functions cơ bản

-- Scalar Function
DELIMITER //
CREATE FUNCTION CalculateAge(birth_date DATE)
RETURNS INT
READS SQL DATA
DETERMINISTIC
BEGIN
    RETURN TIMESTAMPDIFF(YEAR, birth_date, CURDATE());
END //
DELIMITER ;

-- Sử dụng function
SELECT 
    customer_id,
    first_name,
    last_name,
    birth_date,
    CalculateAge(birth_date) as age
FROM customers;

-- String processing function
DELIMITER //
CREATE FUNCTION FormatPhoneNumber(phone VARCHAR(20))
RETURNS VARCHAR(20)
DETERMINISTIC
BEGIN
    DECLARE formatted_phone VARCHAR(20);
    
    -- Remove all non-digit characters
    SET formatted_phone = REGEXP_REPLACE(phone, '[^0-9]', '');
    
    -- Format as (XXX) XXX-XXXX
    IF LENGTH(formatted_phone) = 10 THEN
        SET formatted_phone = CONCAT(
            '(', SUBSTRING(formatted_phone, 1, 3), ') ',
            SUBSTRING(formatted_phone, 4, 3), '-',
            SUBSTRING(formatted_phone, 7, 4)
        );
    END IF;
    
    RETURN formatted_phone;
END //
DELIMITER ;

-- Test function
SELECT FormatPhoneNumber('1234567890'); -- Returns: (123) 456-7890

2. Business Logic Functions

-- Calculate customer loyalty points
DELIMITER //
CREATE FUNCTION CalculateLoyaltyPoints(
    customer_id INT,
    order_amount DECIMAL(10,2)
)
RETURNS INT
READS SQL DATA
DETERMINISTIC
BEGIN
    DECLARE customer_tier VARCHAR(20);
    DECLARE points_multiplier DECIMAL(3,2);
    DECLARE base_points INT;
    
    -- Determine customer tier
    SELECT 
        CASE 
            WHEN total_spent > 50000 THEN 'VIP'
            WHEN total_spent > 20000 THEN 'Premium'
            WHEN total_spent > 5000 THEN 'Regular'
            ELSE 'New'
        END INTO customer_tier
    FROM (
        SELECT COALESCE(SUM(total_amount), 0) as total_spent
        FROM orders
        WHERE customer_id = customer_id
    ) customer_summary;
    
    -- Set points multiplier based on tier
    CASE customer_tier
        WHEN 'VIP' THEN SET points_multiplier = 3.0;
        WHEN 'Premium' THEN SET points_multiplier = 2.0;
        WHEN 'Regular' THEN SET points_multiplier = 1.5;
        ELSE SET points_multiplier = 1.0;
    END CASE;
    
    -- Calculate base points (1 point per dollar)
    SET base_points = FLOOR(order_amount);
    
    RETURN FLOOR(base_points * points_multiplier);
END //
DELIMITER ;

-- Validate credit card number (Luhn algorithm)
DELIMITER //
CREATE FUNCTION ValidateCreditCard(card_number VARCHAR(20))
RETURNS BOOLEAN
DETERMINISTIC
BEGIN
    DECLARE card_length INT;
    DECLARE digit_sum INT DEFAULT 0;
    DECLARE i INT DEFAULT 1;
    DECLARE digit INT;
    DECLARE doubled_digit INT;
    
    -- Remove spaces and dashes
    SET card_number = REPLACE(REPLACE(card_number, ' ', ''), '-', '');
    SET card_length = LENGTH(card_number);
    
    -- Check if all characters are digits
    IF card_number REGEXP '[^0-9]' THEN
        RETURN FALSE;
    END IF;
    
    -- Check length (13-19 digits for most cards)
    IF card_length < 13 OR card_length > 19 THEN
        RETURN FALSE;
    END IF;
    
    -- Luhn algorithm
    WHILE i <= card_length DO
        SET digit = CAST(SUBSTRING(card_number, card_length - i + 1, 1) AS UNSIGNED);
        
        IF i % 2 = 0 THEN  -- Every second digit from right
            SET doubled_digit = digit * 2;
            IF doubled_digit > 9 THEN
                SET doubled_digit = doubled_digit - 9;
            END IF;
            SET digit_sum = digit_sum + doubled_digit;
        ELSE
            SET digit_sum = digit_sum + digit;
        END IF;
        
        SET i = i + 1;
    END WHILE;
    
    RETURN (digit_sum % 10 = 0);
END //
DELIMITER ;

-- Test credit card validation
SELECT ValidateCreditCard('4532-1234-5678-9012'); -- Test with a valid format

Ví dụ thực tế

1. E-commerce Order Processing

-- Complete order processing procedure
DELIMITER //
CREATE PROCEDURE ProcessCompleteOrder(
    IN p_customer_id INT,
    IN p_product_list JSON, -- [{"product_id": 1, "quantity": 2}, ...]
    IN p_shipping_address JSON,
    IN p_payment_method VARCHAR(50),
    OUT p_order_id INT,
    OUT p_total_amount DECIMAL(12,2),
    OUT p_result_message VARCHAR(255)
)
BEGIN
    DECLARE v_product_count INT DEFAULT 0;
    DECLARE v_i INT DEFAULT 0;
    DECLARE v_product_id INT;
    DECLARE v_quantity INT;
    DECLARE v_unit_price DECIMAL(10,2);
    DECLARE v_stock_quantity INT;
    DECLARE v_subtotal DECIMAL(12,2) DEFAULT 0;
    DECLARE v_tax_amount DECIMAL(12,2);
    DECLARE v_shipping_cost DECIMAL(10,2) DEFAULT 10.00;
    
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SET p_result_message = 'Order processing failed';
        SET p_order_id = 0;
    END;
    
    START TRANSACTION;
    
    -- Validate customer
    IF NOT EXISTS (SELECT 1 FROM customers WHERE id = p_customer_id) THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Invalid customer ID';
    END IF;
    
    -- Get product count from JSON
    SET v_product_count = JSON_LENGTH(p_product_list);
    
    -- Create order
    INSERT INTO orders (
        customer_id, 
        order_date, 
        status, 
        shipping_address,
        payment_method
    ) VALUES (
        p_customer_id, 
        NOW(), 
        'pending',
        p_shipping_address,
        p_payment_method
    );
    
    SET p_order_id = LAST_INSERT_ID();
    
    -- Process each product
    WHILE v_i < v_product_count DO
        SET v_product_id = JSON_UNQUOTE(JSON_EXTRACT(p_product_list, CONCAT('$[', v_i, '].product_id')));
        SET v_quantity = JSON_UNQUOTE(JSON_EXTRACT(p_product_list, CONCAT('$[', v_i, '].quantity')));
        
        -- Get product info and check stock
        SELECT price, stock_quantity 
        INTO v_unit_price, v_stock_quantity
        FROM products 
        WHERE id = v_product_id AND is_active = TRUE;
        
        IF v_unit_price IS NULL THEN
            SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = CONCAT('Product not found: ', v_product_id);
        END IF;
        
        IF v_stock_quantity < v_quantity THEN
            SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = CONCAT('Insufficient stock for product: ', v_product_id);
        END IF;
        
        -- Add order item
        INSERT INTO order_items (order_id, product_id, quantity, unit_price, total_price)
        VALUES (p_order_id, v_product_id, v_quantity, v_unit_price, v_quantity * v_unit_price);
        
        -- Update stock
        UPDATE products 
        SET stock_quantity = stock_quantity - v_quantity 
        WHERE id = v_product_id;
        
        -- Add to subtotal
        SET v_subtotal = v_subtotal + (v_quantity * v_unit_price);
        
        SET v_i = v_i + 1;
    END WHILE;
    
    -- Calculate tax (8%)
    SET v_tax_amount = v_subtotal * 0.08;
    
    -- Calculate total
    SET p_total_amount = v_subtotal + v_tax_amount + v_shipping_cost;
    
    -- Update order with totals
    UPDATE orders 
    SET 
        subtotal = v_subtotal,
        tax_amount = v_tax_amount,
        shipping_amount = v_shipping_cost,
        total_amount = p_total_amount
    WHERE id = p_order_id;
    
    COMMIT;
    SET p_result_message = 'Order processed successfully';
END //
DELIMITER ;

-- Test the procedure
SET @products = '[{"product_id": 1, "quantity": 2}, {"product_id": 2, "quantity": 1}]';
SET @address = '{"street": "123 Main St", "city": "New York", "state": "NY", "zip": "10001"}';

CALL ProcessCompleteOrder(
    1, 
    @products, 
    @address, 
    'credit_card',
    @order_id, 
    @total, 
    @message
);

SELECT @order_id, @total, @message;

2. Inventory Management System

-- Procedure để restock inventory với notifications
DELIMITER //
CREATE PROCEDURE RestockInventory(
    IN p_product_id INT,
    IN p_restock_quantity INT,
    IN p_supplier_id INT,
    OUT p_result VARCHAR(255)
)
BEGIN
    DECLARE v_current_stock INT;
    DECLARE v_min_stock_level INT;
    DECLARE v_product_name VARCHAR(200);
    DECLARE v_reorder_point INT;
    
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SET p_result = 'Restock failed due to error';
    END;
    
    START TRANSACTION;
    
    -- Get current product info
    SELECT 
        stock_quantity, 
        min_stock_level, 
        name,
        reorder_point
    INTO 
        v_current_stock, 
        v_min_stock_level, 
        v_product_name,
        v_reorder_point
    FROM products 
    WHERE id = p_product_id;
    
    IF v_current_stock IS NULL THEN
        SET p_result = 'Product not found';
        ROLLBACK;
    ELSE
        -- Update stock
        UPDATE products 
        SET 
            stock_quantity = stock_quantity + p_restock_quantity,
            last_restock_date = NOW(),
            updated_at = NOW()
        WHERE id = p_product_id;
        
        -- Log inventory movement
        INSERT INTO inventory_movements (
            product_id,
            movement_type,
            quantity_change,
            old_quantity,
            new_quantity,
            supplier_id,
            created_at
        ) VALUES (
            p_product_id,
            'restock',
            p_restock_quantity,
            v_current_stock,
            v_current_stock + p_restock_quantity,
            p_supplier_id,
            NOW()
        );
        
        -- Create notification if still below minimum
        IF (v_current_stock + p_restock_quantity) < v_min_stock_level THEN
            INSERT INTO notifications (
                type,
                title,
                message,
                created_at
            ) VALUES (
                'low_stock',
                'Low Stock Warning',
                CONCAT('Product "', v_product_name, '" is still below minimum stock level after restock'),
                NOW()
            );
        END IF;
        
        COMMIT;
        SET p_result = CONCAT('Successfully restocked ', p_restock_quantity, ' units');
    END IF;
END //
DELIMITER ;

-- Function để check products cần reorder
DELIMITER //
CREATE FUNCTION GetLowStockProducts()
RETURNS JSON
READS SQL DATA
BEGIN
    DECLARE result_json JSON;
    
    SELECT JSON_ARRAYAGG(
        JSON_OBJECT(
            'product_id', id,
            'name', name,
            'current_stock', stock_quantity,
            'min_level', min_stock_level,
            'suggested_order', reorder_quantity
        )
    ) INTO result_json
    FROM products
    WHERE stock_quantity <= reorder_point
    AND is_active = TRUE;
    
    RETURN COALESCE(result_json, JSON_ARRAY());
END //
DELIMITER ;

-- Sử dụng function
SELECT GetLowStockProducts() as low_stock_report;

3. Financial Reports và Analytics

-- Procedure tạo monthly sales report
DELIMITER //
CREATE PROCEDURE GenerateMonthlySalesReport(
    IN p_year INT,
    IN p_month INT
)
BEGIN
    DECLARE v_start_date DATE;
    DECLARE v_end_date DATE;
    
    SET v_start_date = DATE(CONCAT(p_year, '-', LPAD(p_month, 2, '0'), '-01'));
    SET v_end_date = LAST_DAY(v_start_date);
    
    -- Create temporary report table
    DROP TEMPORARY TABLE IF EXISTS temp_monthly_report;
    CREATE TEMPORARY TABLE temp_monthly_report (
        category VARCHAR(100),
        total_orders INT,
        total_revenue DECIMAL(15,2),
        avg_order_value DECIMAL(10,2),
        unique_customers INT,
        top_product VARCHAR(200),
        growth_rate DECIMAL(5,2)
    );
    
    -- Insert category-wise data
    INSERT INTO temp_monthly_report (category, total_orders, total_revenue, avg_order_value, unique_customers)
    SELECT 
        c.name as category,
        COUNT(DISTINCT o.id) as total_orders,
        SUM(oi.quantity * oi.unit_price) as total_revenue,
        AVG(o.total_amount) as avg_order_value,
        COUNT(DISTINCT o.customer_id) as unique_customers
    FROM orders o
    INNER JOIN order_items oi ON o.id = oi.order_id
    INNER JOIN products p ON oi.product_id = p.id
    INNER JOIN categories c ON p.category_id = c.id
    WHERE o.order_date BETWEEN v_start_date AND v_end_date
    AND o.status = 'completed'
    GROUP BY c.id, c.name;
    
    -- Calculate growth rates and top products
    UPDATE temp_monthly_report tmr
    SET 
        growth_rate = (
            SELECT 
                CASE 
                    WHEN prev_revenue > 0 THEN 
                        ((tmr.total_revenue - prev_revenue) / prev_revenue) * 100
                    ELSE 0
                END
            FROM (
                SELECT 
                    c.name,
                    SUM(oi.quantity * oi.unit_price) as prev_revenue
                FROM orders o
                INNER JOIN order_items oi ON o.id = oi.order_id
                INNER JOIN products p ON oi.product_id = p.id
                INNER JOIN categories c ON p.category_id = c.id
                WHERE o.order_date BETWEEN 
                    DATE_SUB(v_start_date, INTERVAL 1 MONTH) AND 
                    DATE_SUB(v_end_date, INTERVAL 1 MONTH)
                AND o.status = 'completed'
                AND c.name = tmr.category
                GROUP BY c.name
            ) prev_month
        ),
        top_product = (
            SELECT p.name
            FROM orders o
            INNER JOIN order_items oi ON o.id = oi.order_id
            INNER JOIN products p ON oi.product_id = p.id
            INNER JOIN categories c ON p.category_id = c.id
            WHERE o.order_date BETWEEN v_start_date AND v_end_date
            AND o.status = 'completed'
            AND c.name = tmr.category
            GROUP BY p.id, p.name
            ORDER BY SUM(oi.quantity) DESC
            LIMIT 1
        );
    
    -- Return the report
    SELECT 
        category,
        total_orders,
        total_revenue,
        avg_order_value,
        unique_customers,
        top_product,
        ROUND(growth_rate, 2) as growth_rate_percent
    FROM temp_monthly_report
    ORDER BY total_revenue DESC;
    
    -- Summary totals
    SELECT 
        'TOTAL' as summary,
        SUM(total_orders) as total_orders,
        SUM(total_revenue) as total_revenue,
        AVG(avg_order_value) as overall_avg_order,
        SUM(unique_customers) as total_unique_customers
    FROM temp_monthly_report;
END //
DELIMITER ;

-- Run monthly report
CALL GenerateMonthlySalesReport(2024, 12);

Performance Optimization

1. Caching và Optimization

-- Optimized procedure với caching
DELIMITER //
CREATE PROCEDURE GetCustomerSummaryOptimized(
    IN p_customer_id INT
)
BEGIN
    -- Check if summary exists in cache table
    IF EXISTS (
        SELECT 1 FROM customer_summary_cache 
        WHERE customer_id = p_customer_id 
        AND updated_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)
    ) THEN
        -- Return cached data
        SELECT * FROM customer_summary_cache 
        WHERE customer_id = p_customer_id;
    ELSE
        -- Calculate and cache new data
        REPLACE INTO customer_summary_cache (
            customer_id,
            total_orders,
            total_spent,
            avg_order_value,
            last_order_date,
            customer_tier,
            updated_at
        )
        SELECT 
            c.id,
            COUNT(o.id),
            COALESCE(SUM(o.total_amount), 0),
            COALESCE(AVG(o.total_amount), 0),
            MAX(o.order_date),
            CASE 
                WHEN COALESCE(SUM(o.total_amount), 0) > 50000 THEN 'VIP'
                WHEN COALESCE(SUM(o.total_amount), 0) > 20000 THEN 'Premium'
                WHEN COALESCE(SUM(o.total_amount), 0) > 5000 THEN 'Regular'
                ELSE 'New'
            END,
            NOW()
        FROM customers c
        LEFT JOIN orders o ON c.id = o.customer_id AND o.status = 'completed'
        WHERE c.id = p_customer_id
        GROUP BY c.id;
        
        -- Return calculated data
        SELECT * FROM customer_summary_cache 
        WHERE customer_id = p_customer_id;
    END IF;
END //
DELIMITER ;

2. Batch Processing

-- Batch update procedure
DELIMITER //
CREATE PROCEDURE UpdateCustomerTiersBatch(
    IN p_batch_size INT DEFAULT 1000
)
BEGIN
    DECLARE v_offset INT DEFAULT 0;
    DECLARE v_rows_processed INT;
    
    process_loop: LOOP
        -- Process batch
        UPDATE customers c
        INNER JOIN (
            SELECT 
                customer_id,
                CASE 
                    WHEN total_spent > 50000 THEN 'VIP'
                    WHEN total_spent > 20000 THEN 'Premium'
                    WHEN total_spent > 5000 THEN 'Regular'
                    ELSE 'New'
                END as new_tier
            FROM (
                SELECT 
                    o.customer_id,
                    SUM(o.total_amount) as total_spent
                FROM orders o
                WHERE o.status = 'completed'
                GROUP BY o.customer_id
                LIMIT p_batch_size OFFSET v_offset
            ) customer_totals
        ) tiers ON c.id = tiers.customer_id
        SET c.customer_tier = tiers.new_tier,
            c.updated_at = NOW();
        
        SET v_rows_processed = ROW_COUNT();
        SET v_offset = v_offset + p_batch_size;
        
        IF v_rows_processed < p_batch_size THEN
            LEAVE process_loop;
        END IF;
        
        -- Small delay to avoid overwhelming the server
        DO SLEEP(0.1);
    END LOOP;
    
    SELECT CONCAT('Processed customer tiers in batches of ', p_batch_size) as result;
END //
DELIMITER ;

Best Practices

1. Security và Validation

-- Input validation function
DELIMITER //
CREATE FUNCTION ValidateEmail(email VARCHAR(255))
RETURNS BOOLEAN
DETERMINISTIC
BEGIN
    RETURN email REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$';
END //
DELIMITER ;

-- Secure procedure với validation
DELIMITER //
CREATE PROCEDURE CreateCustomerSecure(
    IN p_first_name VARCHAR(50),
    IN p_last_name VARCHAR(50),
    IN p_email VARCHAR(100),
    IN p_phone VARCHAR(20),
    OUT p_customer_id INT,
    OUT p_result VARCHAR(255)
)
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SET p_result = 'Failed to create customer';
        SET p_customer_id = 0;
    END;
    
    -- Input validation
    IF p_first_name IS NULL OR TRIM(p_first_name) = '' THEN
        SET p_result = 'First name is required';
        SET p_customer_id = 0;
        LEAVE main_proc;
    END IF;
    
    IF NOT ValidateEmail(p_email) THEN
        SET p_result = 'Invalid email format';
        SET p_customer_id = 0;
        LEAVE main_proc;
    END IF;
    
    -- Check for duplicate email
    IF EXISTS (SELECT 1 FROM customers WHERE email = p_email) THEN
        SET p_result = 'Email already exists';
        SET p_customer_id = 0;
        LEAVE main_proc;
    END IF;
    
    START TRANSACTION;
    
    INSERT INTO customers (first_name, last_name, email, phone, created_at)
    VALUES (p_first_name, p_last_name, p_email, p_phone, NOW());
    
    SET p_customer_id = LAST_INSERT_ID();
    
    COMMIT;
    SET p_result = 'Customer created successfully';
    
    main_proc: BEGIN END; -- Label for LEAVE statement
END //
DELIMITER ;

2. Monitoring và Debugging

-- Logging procedure
DELIMITER //
CREATE PROCEDURE LogProcedureExecution(
    IN p_procedure_name VARCHAR(100),
    IN p_parameters JSON,
    IN p_execution_time_ms INT,
    IN p_status VARCHAR(20),
    IN p_error_message TEXT
)
BEGIN
    INSERT INTO procedure_execution_log (
        procedure_name,
        parameters,
        execution_time_ms,
        status,
        error_message,
        executed_at
    ) VALUES (
        p_procedure_name,
        p_parameters,
        p_execution_time_ms,
        p_status,
        p_error_message,
        NOW()
    );
END //
DELIMITER ;

-- Wrapper procedure với logging
DELIMITER //
CREATE PROCEDURE ProcessOrderWithLogging(
    IN p_customer_id INT,
    IN p_product_list JSON,
    OUT p_result VARCHAR(255)
)
BEGIN
    DECLARE v_start_time BIGINT;
    DECLARE v_end_time BIGINT;
    DECLARE v_execution_time INT;
    
    SET v_start_time = UNIX_TIMESTAMP(NOW(6)) * 1000000 + MICROSECOND(NOW(6));
    
    -- Call actual procedure
    CALL ProcessCompleteOrder(p_customer_id, p_product_list, @order_id, @total, p_result);
    
    SET v_end_time = UNIX_TIMESTAMP(NOW(6)) * 1000000 + MICROSECOND(NOW(6));
    SET v_execution_time = (v_end_time - v_start_time) / 1000; -- Convert to milliseconds
    
    -- Log execution
    CALL LogProcedureExecution(
        'ProcessCompleteOrder',
        JSON_OBJECT('customer_id', p_customer_id, 'product_count', JSON_LENGTH(p_product_list)),
        v_execution_time,
        IF(p_result LIKE '%successfully%', 'SUCCESS', 'ERROR'),
        IF(p_result LIKE '%successfully%', NULL, p_result)
    );
END //
DELIMITER ;

Management và Maintenance

1. Xem và quản lý Procedures/Functions

-- List all procedures và functions
SELECT 
    ROUTINE_TYPE,
    ROUTINE_NAME,
    ROUTINE_SCHEMA,
    CREATED,
    LAST_ALTERED,
    ROUTINE_DEFINITION
FROM INFORMATION_SCHEMA.ROUTINES
WHERE ROUTINE_SCHEMA = 'your_database'
ORDER BY ROUTINE_TYPE, ROUTINE_NAME;

-- Show procedure definition
SHOW CREATE PROCEDURE GetCustomerById;
SHOW CREATE FUNCTION CalculateAge;

-- Drop procedures và functions
DROP PROCEDURE IF EXISTS GetCustomerById;
DROP FUNCTION IF EXISTS CalculateAge;

-- Rename procedure (recreate with new name)
-- MySQL không hỗ trợ RENAME cho procedures

2. Permissions và Security

-- Grant execute permissions
GRANT EXECUTE ON PROCEDURE GetCustomerById TO 'app_user'@'%';
GRANT EXECUTE ON FUNCTION CalculateAge TO 'report_user'@'%';

-- Create role-based access
CREATE ROLE 'procedure_executor';
GRANT EXECUTE ON *.* TO 'procedure_executor';
GRANT 'procedure_executor' TO 'app_user'@'%';

-- Revoke permissions
REVOKE EXECUTE ON PROCEDURE GetCustomerById FROM 'app_user'@'%';

3. DEFINER và SQL SECURITY

Khi export database hoặc chạy SHOW CREATE FUNCTION, bạn sẽ thường thấy routine có dạng như sau:

CREATE DEFINER=`root`@`localhost` FUNCTION `func_AddShipAndPaymentHist`(i_ShipmentNo BIGINT) RETURNS bigint(20)
    DETERMINISTIC
BEGIN
    DECLARE i_LiqSeq BIGINT;

    SET i_LiqSeq = func_AddShipHist(i_ShipmentNo);
    SET i_LiqSeq = func_AddPaymentHist(i_ShipmentNo, i_LiqSeq);

    RETURN i_LiqSeq;
END$$

DEFINER là tài khoản MySQL được ghi nhận là "chủ sở hữu" của routine. Kết hợp với SQL SECURITY, nó quyết định routine chạy với quyền của ai:

SQL SECURITY Routine chạy với quyền của Ghi chú
DEFINER (mặc định) User trong DEFINER Người gọi chỉ cần quyền EXECUTE, không cần quyền trên bảng
INVOKER User đang gọi routine Người gọi phải có đủ quyền trên các bảng bên trong

DEFINER có bắt buộc không?

Không bắt buộc. Nếu bỏ qua, MySQL tự gán DEFINER = CURRENT_USER — tức user đang chạy lệnh CREATE:

-- Hai cách viết tương đương khi đang đăng nhập bằng root@localhost
CREATE FUNCTION func_name() RETURNS INT DETERMINISTIC RETURN 1;
CREATE DEFINER = CURRENT_USER FUNCTION func_name() RETURNS INT DETERMINISTIC RETURN 1;

Lý do DEFINER luôn xuất hiện trong code export là vì SHOW CREATE và mysqldump luôn in DEFINER ra một cách tường minh, dù lúc tạo bạn không viết.

Vấn đề thường gặp với DEFINER

ERROR 1449 (HY000): The user specified as a definer ('root'@'localhost') does not exist

Lỗi này xảy ra khi import routine từ môi trường khác (ví dụ dump từ local có root@localhost) sang server không có user đó. Routine vẫn có thể được tạo (kèm warning), nhưng khi gọi sẽ bị lỗi.

Ngoài ra, muốn chỉ định DEFINER là user khác chính mình thì bạn cần quyền đặc biệt (SUPER trên MySQL 5.7, SET_USER_ID trên MySQL 8.0, SET_ANY_DEFINER từ MySQL 8.2) — user thông thường trên cloud (RDS, Cloud SQL) thường không có quyền này.

Best practices

-- ✅ Không ghi DEFINER trong migration files → tự lấy user đang chạy migration
CREATE FUNCTION func_AddShipAndPaymentHist(i_ShipmentNo BIGINT) RETURNS BIGINT
    DETERMINISTIC
BEGIN
    ...
END;

-- ✅ Nếu cần DEFINER, dùng account riêng cho ứng dụng thay vì root
CREATE DEFINER = 'app_owner'@'%' PROCEDURE ...

-- ✅ Dùng SQL SECURITY INVOKER khi muốn áp dụng quyền của người gọi
CREATE PROCEDURE GetReport()
SQL SECURITY INVOKER
BEGIN
    SELECT * FROM orders;
END;

-- ✅ Kiểm tra DEFINER và SQL SECURITY của các routines hiện có
SELECT ROUTINE_NAME, ROUTINE_TYPE, DEFINER, SECURITY_TYPE
FROM INFORMATION_SCHEMA.ROUTINES
WHERE ROUTINE_SCHEMA = 'your_database';
# ✅ Loại bỏ DEFINER khỏi file dump trước khi import sang môi trường khác
sed -E 's/DEFINER=`[^`]+`@`[^`]+`//g' dump.sql > dump_clean.sql

💡 MySQL không hỗ trợ ALTER để đổi DEFINER của routine — muốn đổi phải DROP rồi CREATE lại.

Troubleshooting Common Issues

1. Debug Procedures

-- Debug procedure với detailed logging
DELIMITER //
CREATE PROCEDURE DebugProcedureTemplate(
    IN p_input_param INT
)
BEGIN
    DECLARE v_debug_mode BOOLEAN DEFAULT TRUE;
    DECLARE v_step VARCHAR(100);
    
    IF v_debug_mode THEN
        INSERT INTO debug_log (message, created_at) 
        VALUES (CONCAT('Starting procedure with param: ', p_input_param), NOW());
    END IF;
    
    SET v_step = 'Validation';
    IF v_debug_mode THEN
        INSERT INTO debug_log (message, created_at) 
        VALUES (CONCAT('Step: ', v_step), NOW());
    END IF;
    
    -- Your procedure logic here
    
    IF v_debug_mode THEN
        INSERT INTO debug_log (message, created_at) 
        VALUES ('Procedure completed successfully', NOW());
    END IF;
END //
DELIMITER ;

2. Performance Analysis

-- Performance monitoring query
SELECT 
    ROUTINE_SCHEMA,
    ROUTINE_NAME,
    ROUTINE_TYPE,
    COUNT(*) as execution_count,
    AVG(execution_time_ms) as avg_execution_time,
    MAX(execution_time_ms) as max_execution_time,
    SUM(CASE WHEN status = 'ERROR' THEN 1 ELSE 0 END) as error_count
FROM procedure_execution_log
WHERE executed_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
GROUP BY ROUTINE_SCHEMA, ROUTINE_NAME, ROUTINE_TYPE
ORDER BY avg_execution_time DESC;

Kết luận

Stored Procedures và Functions trong MySQL là công cụ mạnh mẽ, nhưng cần được sử dụng đúng chỗ:

Ưu điểm chính:

  1. Performance: Xử lý trực tiếp trên server, phù hợp với dữ liệu lớn
  2. Network Efficiency: Giảm data transfer giữa application và database
  3. Security: Kiểm soát access qua quyền EXECUTE
  4. Reusability: Nhiều ứng dụng dùng chung một logic

Nhược điểm cần cân nhắc:

  1. Maintainability: Khó quản lý version, review, deploy
  2. Debugging & Testing: Thiếu công cụ hỗ trợ
  3. Scalability: Tăng tải cho database — thành phần khó scale nhất
  4. Vendor lock-in: Khó chuyển đổi sang database khác

Khi nào sử dụng:

  • Mặc định: Xử lý business logic ở application (PHP/Laravel, Node.js...)
  • Procedures: Batch processing, data migration, báo cáo trên dữ liệu lớn
  • Functions: Tính toán, chuyển đổi dữ liệu đơn giản và ổn định

Best Practices tóm tắt:

  • Input validation và error handling
  • Use appropriate parameter types (IN/OUT/INOUT)
  • Implement logging và monitoring
  • Optimize với proper indexing
  • Security với principle of least privilege
  • Regular maintenance và performance review
  • Documentation và version control (lưu routines trong migration files)
  • Không lạm dụng — chỉ dùng khi thực sự cần tối ưu hiệu năng

Hãy xem Stored Procedures và Functions là công cụ tối ưu cho các bài toán đặc thù, không phải nơi chứa toàn bộ business logic của ứng dụng!

Tài liệu tham khảo

  1. MySQL 8.0 Reference Manual - Stored Programs
  2. MySQL Stored Procedure Programming
  3. MySQL 8.0 Reference Manual - CREATE PROCEDURE
  4. MySQL 8.0 Reference Manual - CREATE FUNCTION
  5. High Performance MySQL