I used to push almost all of my logic into application code, treating the database as a dumb storage layer. That worked fine until I had five different services all needing to compute the same “customer lifetime value” figure, and each one implemented it slightly differently. Moving that calculation into a single MySQL stored function fixed the inconsistency overnight — one source of truth, callable from any query. That’s really the core value of MySQL functions: consistency and reuse, expressed as SQL you can call just like a built-in.
What a Stored Function Is (and Isn’t)
A MySQL stored function is a piece of reusable, named logic that lives inside the database, accepts parameters, and returns a single value. It’s different from a stored procedure in a few important ways:
| Feature | Function | Procedure |
|---|---|---|
| Return value | Must return exactly one value | Can return zero, one, or multiple result sets |
| Usable in SQL expressions | Yes (SELECT my_func(x)) | No, must be called with CALL |
| Can modify data | Restricted in many contexts | Yes, freely |
| Called from | Inside SELECT, WHERE, ORDER BY, etc. | Standalone CALL statement |
Architecture: Where Functions Fit in Query Execution
graph TD
A[SQL Query] --> B[Parser]
B --> C{Contains Function Call?}
C -->|Yes| D[Function Body Execution]
D --> E[Return Single Value]
E --> F[Continue Query Execution]
C -->|No| F
F --> G[Result Set]
Because a function can be called inline within a query — in the SELECT list, WHERE clause, or ORDER BY — MySQL may invoke it once per row, which has real performance implications we’ll cover later.
Basic Syntax
DELIMITER $$
CREATE FUNCTION calculate_discount(price DECIMAL(10,2), discount_pct DECIMAL(5,2))
RETURNS DECIMAL(10,2)
DETERMINISTIC
BEGIN
DECLARE final_price DECIMAL(10,2);
SET final_price = price - (price * discount_pct / 100);
RETURN final_price;
END$$
DELIMITER ;
Using it:
SELECT product_name, price, calculate_discount(price, 15) AS discounted_price
FROM products;
Output:
+---------------+--------+-------------------+
| product_name | price | discounted_price |
+---------------+--------+-------------------+
| Wireless Mouse| 25.00 | 21.25 |
| USB-C Cable | 12.00 | 10.20 |
+---------------+--------+-------------------+
Understanding DETERMINISTIC vs NOT DETERMINISTIC
This keyword isn’t optional decoration — it tells MySQL whether the function always returns the same output for the same input.
CREATE FUNCTION get_current_year()
RETURNS INT
NOT DETERMINISTIC
READS SQL DATA
BEGIN
RETURN YEAR(NOW());
END;
Marking a function DETERMINISTIC when it isn’t (e.g., it uses NOW() or RAND()) can cause MySQL to cache and reuse a stale result incorrectly, especially with binary logging and replication. Get this classification right — it affects both correctness and replication safety.
Functions with Conditional Logic
DELIMITER $$
CREATE FUNCTION customer_tier(total_spent DECIMAL(10,2))
RETURNS VARCHAR(20)
DETERMINISTIC
BEGIN
DECLARE tier VARCHAR(20);
IF total_spent >= 10000 THEN
SET tier = 'Platinum';
ELSEIF total_spent >= 5000 THEN
SET tier = 'Gold';
ELSEIF total_spent >= 1000 THEN
SET tier = 'Silver';
ELSE
SET tier = 'Bronze';
END IF;
RETURN tier;
END$$
DELIMITER ;
SELECT customer_id, total_spent, customer_tier(total_spent) AS tier
FROM customers;
Functions That Read Data (READS SQL DATA)
Functions can query other tables, which is powerful but requires the right characteristic declaration:
DELIMITER $$
CREATE FUNCTION get_order_count(cust_id INT)
RETURNS INT
READS SQL DATA
DETERMINISTIC
BEGIN
DECLARE order_count INT;
SELECT COUNT(*) INTO order_count
FROM orders
WHERE customer_id = cust_id;
RETURN order_count;
END$$
DELIMITER ;
SELECT customer_id, get_order_count(customer_id) AS orders_placed
FROM customers;
Be aware: calling a function that queries a table, once per row of an outer query, is effectively an N+1 query pattern executed inside the database — it works, but it’s rarely the fastest option compared to a proper JOIN, which we’ll address in the optimization section.
Managing Functions
List all functions in the current database:
SHOW FUNCTION STATUS WHERE Db = 'mydatabase';
View a function’s definition:
SHOW CREATE FUNCTION calculate_discount;
Modify a function: MySQL doesn’t support ALTER FUNCTION for changing logic — you must drop and recreate it.
DROP FUNCTION IF EXISTS calculate_discount;
DELIMITER $$
CREATE FUNCTION calculate_discount(price DECIMAL(10,2), discount_pct DECIMAL(5,2))
RETURNS DECIMAL(10,2)
DETERMINISTIC
BEGIN
RETURN price - (price * discount_pct / 100);
END$$
DELIMITER ;
Drop a function:
DROP FUNCTION calculate_discount;
Required Privileges
GRANT CREATE ROUTINE, ALTER ROUTINE, EXECUTE ON mydatabase.* TO 'dev_user'@'%';
If log_bin_trust_function_creators is disabled (the default, for good reason), creating a non-deterministic function that writes data will be rejected unless you either mark it correctly or explicitly enable this system variable — which itself is a security-relevant decision, since it relaxes a safeguard against unsafe statements being replicated inconsistently.
Real-World Scenario: Centralizing Business Logic
A recurring pattern I’ve used across several projects: tax calculation logic that multiple reporting queries, application services, and scheduled jobs all need to agree on.
DELIMITER $$
CREATE FUNCTION calculate_tax(amount DECIMAL(10,2), region_code VARCHAR(5))
RETURNS DECIMAL(10,2)
READS SQL DATA
DETERMINISTIC
BEGIN
DECLARE tax_rate DECIMAL(5,4);
SELECT rate INTO tax_rate FROM tax_rates WHERE region = region_code LIMIT 1;
IF tax_rate IS NULL THEN
SET tax_rate = 0.00;
END IF;
RETURN ROUND(amount * tax_rate, 2);
END$$
DELIMITER ;
SELECT order_id, total_amount, calculate_tax(total_amount, region) AS tax_due
FROM orders;
When tax rates change, I update one table and one function — every report and application that calls it stays correct automatically, instead of chasing down five different hardcoded implementations.
Performance Considerations
This is the part people often get wrong: stored functions are convenient, but they can be a genuine performance trap when used carelessly in large queries, because MySQL frequently can’t optimize across the function boundary the way it can with native expressions or joins.
-- This looks clean, but calls get_order_count() once per row
SELECT customer_id, get_order_count(customer_id) FROM customers;
EXPLAIN SELECT customer_id, get_order_count(customer_id) FROM customers;
For large tables, rewrite the equivalent logic as a JOIN or subquery wherever the query is performance-sensitive:
SELECT c.customer_id, COUNT(o.order_id) AS orders_placed
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
GROUP BY c.customer_id;
The JOIN version lets the optimizer use indexes and set-based execution properly, rather than executing a black-box function call per row.
Rule of thumb I follow: use stored functions for pure calculations without table access (tax math, formatting, scoring formulas) freely — they’re fast and safe. Be cautious with functions that read from tables inside large-scale queries; profile them with EXPLAIN and SHOW PROFILE before trusting them at scale.
Security Considerations
- Grant
EXECUTEprivilege narrowly — don’t hand outCREATE ROUTINE/ALTER ROUTINEbroadly in production. - Be deliberate about
SQL SECURITY DEFINERvsSQL SECURITY INVOKER— aDEFINERfunction runs with the privileges of whoever created it, which can unintentionally grant elevated access to callers who wouldn’t otherwise have permission to the underlying tables.
CREATE FUNCTION sensitive_lookup(id INT)
RETURNS VARCHAR(100)
SQL SECURITY INVOKER
READS SQL DATA
BEGIN
RETURN (SELECT ssn FROM sensitive_table WHERE customer_id = id);
END;
- Validate and sanitize any input parameters used to build dynamic behavior inside a function, though note MySQL functions don’t support dynamic SQL execution the way procedures can with
PREPARE/EXECUTE.
Troubleshooting Common Issues
“FUNCTION does not exist” errors after a fresh restore. Functions aren’t always included in a basic mysqldump unless you include --routines — always verify:
mysqldump --routines --triggers --events -u root -p mydatabase > full_backup.sql
“This function has none of DETERMINISTIC…” error. MySQL requires an explicit characteristic when binary logging is strict; add DETERMINISTIC, NO SQL, READS SQL DATA, or MODIFIES SQL DATA as appropriate.
Unexpected slow queries after adding a function to a SELECT. Check with EXPLAIN and consider whether the logic can be rewritten as a join, case expression, or native SQL rather than a per-row function call.
Frequently Asked Questions
Can a MySQL function modify data? Generally no in standard configuration — functions are meant for computing values, not data modification; use a stored procedure for that. MODIFIES SQL DATA characteristic exists but is heavily restricted and rarely appropriate.
Are stored functions faster than doing the same logic in application code? Not necessarily — for row-by-row logic over large result sets, network round-trip savings can help, but query-optimizer opacity can hurt. Test with real data volumes rather than assuming.
Can I call one function from another? Yes, functions can call other functions, though excessive nesting makes debugging and performance analysis harder.
How do I back up functions along with my database? Use mysqldump --routines (and --triggers --events if relevant) to ensure stored functions are included in the dump.
Interview Questions
- What’s the fundamental difference between a stored function and a stored procedure in MySQL?
- Why does the
DETERMINISTICcharacteristic matter for correctness and replication? - When would using a function inside a SELECT clause hurt query performance, and how would you rewrite it more efficiently?
- What’s the difference between
SQL SECURITY DEFINERandSQL SECURITY INVOKER, and why does it matter for access control? - Why might
mysqldumpnot include your custom functions by default, and how do you fix that? - Give an example of a good use case for a stored function versus a bad one.
Summary and Key Takeaways
- MySQL functions centralize reusable logic and can be called directly inside SQL expressions, unlike procedures.
- Correctly declaring
DETERMINISTIC,READS SQL DATA, orMODIFIES SQL DATAisn’t optional — it affects correctness and replication safety. - Functions are excellent for pure calculations; be cautious using table-querying functions inside large SELECT queries, since they can silently turn into an N+1 performance problem.
- Security context (
DEFINERvsINVOKER) matters, especially for functions touching sensitive tables. - Always back up routines explicitly with
--routinesinmysqldump.
Used thoughtfully, stored functions are one of the cleanest ways to guarantee that “the business logic” means the same thing everywhere it’s used — one definition, one source of truth.