Home Database MySQL Stored Functions — DETERMINISTIC Causes Replica Drift
Intermediate 8 min · March 05, 2026

MySQL Stored Functions — DETERMINISTIC Causes Replica Drift

Wrong DETERMINISTIC on a MySQL stored function caused replica drift.

N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 25 min
  • Solid grasp of fundamentals
  • Comfortable reading code examples
  • Basic production concepts
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • MySQL stored functions: named, reusable SQL logic returning one scalar value
  • Called inline in SELECT, WHERE, ORDER BY — like built-in functions
  • DETERMINISTIC declaration affects query optimization and replication correctness
  • Performance: Function call in WHERE can't use indexes without functional index (MySQL 8.0+ only)
  • Biggest mistake: Forgetting DELIMITER change causes cryptic syntax errors
  • Production insight: Wrong DETERMINISTIC label causes silent replica drift
✦ Definition~90s read
What is MySQL Stored Functions?

A MySQL stored function is a named, reusable routine that accepts parameters, executes SQL logic, and returns a single value. Unlike stored procedures, which are invoked with CALL and can return multiple result sets, a stored function is designed to be used directly inside SQL expressions — in SELECT, WHERE, ORDER BY, or computed column definitions.

Imagine your favorite coffee shop has a secret recipe for calculating the total price of any order — including tax, loyalty discounts, and cup size.

You reach for a stored function when you need to encapsulate deterministic or semi-deterministic business logic (e.g., calculating a tax rate, formatting a phone number, or deriving a status flag) that must be evaluated row-by-row in a query. The key trade-off: every row that hits the function triggers a context switch between the SQL engine and the routine, so performance degrades linearly with row count — never use a stored function inside a WHERE clause on a million-row table unless you enjoy watching your DBA cry.

Stored functions live in the MySQL ecosystem alongside built-in functions (like CONCAT or NOW) and user-defined functions (UDFs written in C/C++). They differ from views and generated columns in that they execute arbitrary procedural logic, not just expressions.

The critical — and often misunderstood — attribute is the DETERMINISTIC flag. Declaring a function DETERMINISTIC tells MySQL it always returns the same result for the same inputs (e.g., calculate_discount(price, rate)). This enables query optimizations like caching and statement-based replication (SBR) to replay the function call safely.

Declaring it NOT DETERMINISTIC (or omitting the clause) forces row-based replication (RBR) for any statement that calls it, because the replica cannot guarantee the same output. Mislabeling a function as DETERMINISTIC when it reads from a table or uses random values is a classic cause of replica drift — the primary and replica diverge silently, often surfacing as a data integrity nightmare weeks later.

In production, stored functions are a double-edged sword. They centralize logic but bypass query caching, defeat index usage when wrapped around columns, and introduce replication fragility. Use them sparingly — prefer generated columns or application-layer logic for simple transformations.

When you must use one, always audit the DETERMINISTIC declaration against the function body: if it reads from a table, calls NOW(), or uses RAND(), it must be NOT DETERMINISTIC. The MySQL manual and Percona’s replication guides both hammer this point — ignoring it is the #1 cause of silent replica drift in shops that rely on statement-based replication.

Plain-English First

Imagine your favorite coffee shop has a secret recipe for calculating the total price of any order — including tax, loyalty discounts, and cup size. Instead of every barista doing that math from scratch each time, the recipe lives in one laminated card behind the counter. A MySQL stored function is exactly that laminated card: a named, reusable piece of logic you write once, store inside the database, and call any time you need that calculation — no copy-pasting, no inconsistency.

Every production database eventually accumulates the same calculation scattered across dozens of queries — tax rates, age from a birthdate, discounted prices, formatted phone numbers. When that logic changes (and it always does), you're hunting through hundreds of SQL files trying to update every copy. That's not a maintenance strategy; that's a liability. MySQL stored functions exist to solve exactly this problem by letting you define reusable, named logic that lives inside the database itself.

Unlike a stored procedure, a stored function is purpose-built to compute and return a single value. It plugs directly into a SELECT statement, a WHERE clause, or a computed column — anywhere an expression is valid. That distinction matters enormously: you're not running a batch process, you're enriching a query with encapsulated intelligence. The database engine treats your function like any built-in function such as NOW() or UPPER(), which means your code reads naturally and your business rules stay consistent regardless of which application, microservice, or analyst is querying the data.

By the end of this article you'll know how to write a stored function from scratch, understand the determinism rules that trip up even experienced developers, call functions inside real queries, and avoid the four most common mistakes that cause silent errors or permission failures in production. You'll also walk away knowing exactly how to answer stored-function questions in a technical interview.

What a Stored Function Actually Is (and When to Reach for One)

A stored function is a named database object that accepts zero or more input parameters, executes SQL logic, and returns exactly one scalar value — a number, a string, a date, a boolean equivalent. The key word is 'returns'. That return value is what separates a stored function from a stored procedure. A procedure does things; a function computes a value.

Use a stored function when you have a calculation or transformation that: 1. Appears in multiple queries across your application. 2. Needs to stay consistent — if the rule changes, you want one place to update. 3. Produces a single result you can embed inline, like in a SELECT list or a WHERE condition.

Classic real-world candidates include: calculating a customer's current loyalty tier based on their total spend, deriving a human-readable order status label from a numeric status code, computing net price after dynamic discount rules, or formatting an international phone number from raw digits.

What you should NOT use a stored function for: anything that modifies data (INSERT, UPDATE, DELETE). MySQL technically allows data modification inside functions in some configurations, but it's a trap — we'll cover exactly why in the Gotchas section.

create_loyalty_tier_function.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
-- ============================================================
-- SCENARIO: An e-commerce platform needs to show each customer
-- their loyalty tier (Bronze / Silver / Gold) based on their
-- total lifetime spend. This logic appears in dashboards, emails,
-- and reports — so it must live in one place.
-- ============================================================

-- Step 1: Switch to your target database
USE ecommerce_db;

-- Step 2: Tell MySQL what delimiter to use while defining the function.
-- Without this, MySQL would interpret the first semicolon inside the
-- function body as the END of the CREATE statement — which breaks everything.
DELIMITER $$

CREATE FUNCTION get_loyalty_tier(
    total_spend DECIMAL(10, 2)   -- lifetime spend in dollars
)
RETURNS VARCHAR(10)              -- will return 'Bronze', 'Silver', or 'Gold'
DETERMINISTIC                   -- same input always gives same output (no randomness, no NOW())
COMMENT 'Returns customer loyalty tier based on lifetime spend thresholds'
BEGIN
    -- Declare a local variable to hold our result
    DECLARE tier_label VARCHAR(10);

    -- Apply the business rule: thresholds defined by the product team
    IF total_spend >= 1000.00 THEN
        SET tier_label = 'Gold';
    ELSEIF total_spend >= 250.00 THEN
        SET tier_label = 'Silver';
    ELSE
        SET tier_label = 'Bronze';   -- default for new or low-spend customers
    END IF;

    RETURN tier_label;   -- every code path MUST hit a RETURN
END$$

-- Step 3: Restore the normal semicolon delimiter
DELIMITER ;
Output
Query OK, 0 rows affected (0.02 sec)
⚠ Watch Out: DELIMITER Is Not Optional
Forgetting to change the DELIMITER before a function body is the single most common reason CREATE FUNCTION fails with a syntax error. MySQL's client sees the first semicolon inside BEGIN...END and thinks the statement is over. Always wrap your function definition with DELIMITER $$ ... DELIMITER ; when using the CLI or a script file. GUI tools like MySQL Workbench handle this automatically.
📊 Production Insight
On a busy primary, forgetting DELIMITER in a migration script caused a deployment rollback that took 20 minutes.
The error message '... at line 2' never mentions DELIMITER — it just shows the wrong line.
Rule: Always include DELIMITER $$ at the start of every CREATE FUNCTION script, even in CI.
🎯 Key Takeaway
Stored functions return one value and live in the database.
Change DELIMITER before CREATE, restore after END.
All paths in the body must hit a RETURN — no path = NULL.
mysql-stored-functions Stored Function Execution Layers From SQL call to binary log propagation Application Layer SELECT query | WHERE clause | SET assignment MySQL Server Parser | Optimizer | Executor Stored Function Engine Function cache | DETERMINISTIC flag | NOT DETERMINISTIC flag Binary Log Statement-based log | Row-based log | Mixed mode Replication Source thread | Relay log | Replica applier THECODEFORGE.IO
thecodeforge.io
Mysql Stored Functions

Calling Your Function — Inside SELECT, WHERE, and Computed Columns

Once your function exists, you call it exactly like any built-in MySQL function — by name, with parentheses, passing the required arguments. There's no special keyword, no EXEC, no CALL. This is what makes stored functions so powerful: they integrate invisibly into ordinary SQL.

The examples below use a customers table with columns: customer_id, full_name, email, and lifetime_spend. Notice how the function call in the SELECT list creates a clean derived column, the WHERE clause uses it for filtering, and ORDER BY uses it for sorting — all without any application-side logic.

One performance note worth understanding: when you call a stored function in a WHERE clause on a large table, MySQL cannot use an index on that expression unless you're on MySQL 8.0+ with a functional index defined for it. For high-traffic filter conditions, consider materializing the tier into a real indexed column and updating it on a schedule. For dashboard and reporting queries that run occasionally, inline function calls are perfectly fine.

query_with_loyalty_tier.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
-- ============================================================
-- Assume this customers table exists:
-- CREATE TABLE customers (
--     customer_id   INT PRIMARY KEY AUTO_INCREMENT,
--     full_name     VARCHAR(100),
--     email         VARCHAR(150),
--     lifetime_spend DECIMAL(10,2) DEFAULT 0.00
-- );
--
-- With sample data:
-- (1, 'Alice Hartman',  'alice@example.com',  1250.75)
-- (2, 'Ben Okafor',     'ben@example.com',     310.00)
-- (3, 'Cleo Ramirez',   'cleo@example.com',     89.50)
-- ============================================================

-- USE CASE 1: Show every customer with their computed loyalty tier
SELECT
    customer_id,
    full_name,
    lifetime_spend,
    get_loyalty_tier(lifetime_spend) AS loyalty_tier   -- function called inline
FROM customers
ORDER BY lifetime_spend DESC;

-- USE CASE 2: Filter — show only Gold-tier customers for a VIP email campaign
SELECT
    full_name,
    email
FROM customers
WHERE get_loyalty_tier(lifetime_spend) = 'Gold';

-- USE CASE 3: Quick sanity check — test the function directly
SELECT
    get_loyalty_tier(1500.00) AS result_gold,
    get_loyalty_tier(500.00)  AS result_silver,
    get_loyalty_tier(10.00)   AS result_bronze;
Output
-- USE CASE 1 output:
+-------------+--------------+---------------+--------------+
| customer_id | full_name | lifetime_spend | loyalty_tier |
+-------------+--------------+---------------+--------------+
| 1 | Alice Hartman | 1250.75 | Gold |
| 2 | Ben Okafor | 310.00 | Silver |
| 3 | Cleo Ramirez | 89.50 | Bronze |
+-------------+--------------+---------------+--------------+
-- USE CASE 2 output:
+--------------+-------------------+
| full_name | email |
+--------------+-------------------+
| Alice Hartman| alice@example.com |
+--------------+-------------------+
-- USE CASE 3 output:
+-------------+---------------+--------------+
| result_gold | result_silver | result_bronze |
+-------------+---------------+--------------+
| Gold | Silver | Bronze |
+-------------+---------------+--------------+
💡Pro Tip: Always Test Your Function in Isolation First
Before embedding a new function inside a complex JOIN query, always run SELECT your_function(test_value) AS result; on its own. It takes 10 seconds and immediately tells you if the logic is correct before you spend 20 minutes debugging a query that was never the problem.
📊 Production Insight
A team in production added a function call to a WHERE clause on a 2 million row table.
The query dev team thought was 'fast enough' in staging went from 200ms to 12 seconds.
Rule: Profile any function call in WHERE with EXPLAIN; if it says 'Using where; Using filesort', consider a generated column.
🎯 Key Takeaway
Functions integrate into SQL seamlessly.
Test in isolation before embedding in complex queries.
High-frequency filter conditions need an indexed materialization, not an inline function.

DETERMINISTIC vs NOT DETERMINISTIC — Why This Label Changes Everything

When you declare a stored function, MySQL asks you to describe its behavior with one of these characteristics: DETERMINISTIC, NOT DETERMINISTIC, or READS SQL DATA / NO SQL / MODIFIES SQL DATA. Most tutorials gloss over these as bureaucratic box-ticking. They're not — they directly affect query optimization, binary log replication, and whether your function is even allowed to run in certain server configurations.

DETERMINISTIC means: given the same inputs, the function will always return the same output. Think of it like a pure math function — f(5) always equals 25. MySQL's optimizer can cache the result and reuse it within the same query execution, which can be a meaningful performance win when the function is called thousands of times in a large result set.

NOT DETERMINISTIC means the output can vary even with identical inputs — because it relies on the current time, a random number, or data in the database. Functions that call NOW(), RAND(), or run a SELECT to fetch changing data must be declared NOT DETERMINISTIC.

The dangerous middle ground: declaring a function DETERMINISTIC when it actually isn't. MySQL trusts your declaration. If you lie, the optimizer may cache a stale result, and your queries silently return wrong answers — no error, no warning. This is especially bad on replicated setups where binary logging can produce inconsistent replicas.

determinism_examples.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
-- ============================================================
-- EXAMPLE 1: Correctly declared DETERMINISTIC function
-- Converts a raw numeric discount rate (e.g. 0.15) to a
-- human-readable label. Same input = same output. Always.
-- ============================================================
DELIMITER $$

CREATE FUNCTION format_discount_label(
    discount_rate DECIMAL(5, 4)   -- e.g. 0.1500 for 15%
)
RETURNS VARCHAR(20)
DETERMINISTIC
NO SQL                            -- no database reads or writes
BEGIN
    RETURN CONCAT(FORMAT(discount_rate * 100, 0), '% OFF');
END$$

DELIMITER ;

-- Test it:
SELECT format_discount_label(0.1500) AS label;
-- Output: '15% OFF'


-- ============================================================
-- EXAMPLE 2: Correctly declared NOT DETERMINISTIC function
-- Calculates how many days ago an order was placed.
-- The result changes every day — it reads the current date.
-- ============================================================
DELIMITER $$

CREATE FUNCTION days_since_order(
    order_placed_at DATE
)
RETURNS INT
NOT DETERMINISTIC                 -- result changes with time; optimizer must NOT cache it
NO SQL
BEGIN
    RETURN DATEDIFF(CURDATE(), order_placed_at);
END$$

DELIMITER ;

-- Test it (assuming today is 2025-07-15):
SELECT days_since_order('2025-07-01') AS days_ago;
-- Output: 14


-- ============================================================
-- EXAMPLE 3: Function that READS from a table
-- Gets a product's current category name by product_id.
-- Requires READS SQL DATA because it runs a SELECT.
-- ============================================================
DELIMITER $$

CREATE FUNCTION get_product_category(
    product_id_input INT
)
RETURNS VARCHAR(100)
NOT DETERMINISTIC                 -- data in the table can change between calls
READS SQL DATA                    -- required: this function runs a SELECT statement
BEGIN
    DECLARE category_name VARCHAR(100);

    SELECT c.category_name
    INTO category_name             -- stores query result into the local variable
    FROM products p
    JOIN categories c ON p.category_id = c.category_id
    WHERE p.product_id = product_id_input
    LIMIT 1;                       -- LIMIT 1 prevents 'Result consisted of more than one row' error

    RETURN IFNULL(category_name, 'Uncategorized');  -- graceful fallback if no match
END$$

DELIMITER ;
Output
-- format_discount_label(0.1500):
+---------+
| label |
+---------+
| 15% OFF |
+---------+
-- days_since_order('2025-07-01') on 2025-07-15:
+----------+
| days_ago |
+----------+
| 14 |
+----------+
🔥Interview Gold: Why Does DETERMINISTIC Matter for Replication?
In statement-based binary logging (MySQL's default replication mode), the server logs the function call, not its result. When a replica replays that call, a NOT DETERMINISTIC function like one using NOW() could produce a different value than it did on the primary — causing data drift. Marking functions correctly isn't just an optimizer hint; it's a replication correctness guarantee. Interviewers love this answer.
📊 Production Insight
A misdeclared DETERMINISTIC function caused a weekly batch report on a read replica to show different customer counts than the primary.
The replica had cached the function result from an earlier execution with different data.
Fix: inspect function body for any non-deterministic call (NOW, RAND, table reads) and correct the declaration.
🎯 Key Takeaway
DETERMINISTIC is an optimizer and replication contract — not a suggestion.
If your function calls any non-deterministic function, it must be NOT DETERMINISTIC.
When in doubt, default to NOT DETERMINISTIC.
mysql-stored-functions DETERMINISTIC vs NOT DETERMINISTIC Impact on replication safety and performance DETERMINISTIC NOT DETERMINISTIC Replication safety Risks drift if function is actually non- Safe; forces row-based logging or re-eva Binary log mode Logged as statement; replayed as-is Logged as row changes or with warning Performance Faster; result may be cached Slower; re-executed on replica Use case Pure mathematical functions (e.g., ABS, Functions using RAND, NOW, UUID, or sess THECODEFORGE.IO
thecodeforge.io
Mysql Stored Functions

Managing Functions — View, Alter, Drop, and Control Permissions

Creating a function is only the beginning. In real projects you need to inspect what's deployed, update logic when business rules change, and control who can execute sensitive functions. MySQL gives you clean tooling for all of this — but the workflow is less obvious than it should be.

MySQL has no ALTER FUNCTION statement for changing the body. To update a function's logic, you DROP it and CREATE it again. This feels crude but is intentional — it forces an explicit, auditable replacement rather than silent in-place mutation. In CI/CD pipelines, this means your migration scripts should always DROP FUNCTION IF EXISTS before recreating it.

Permissions matter more than most developers realize. The EXECUTE privilege controls who can call a function. The CREATE ROUTINE and ALTER ROUTINE privileges control who can create or drop them. In production, your application's database user should only have EXECUTE — never CREATE ROUTINE. That separation means a SQL injection attack can't overwrite your business logic even if it somehow gets write access.

manage_stored_functions.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
-- ============================================================
-- INSPECTING FUNCTIONS
-- ============================================================

-- List all stored functions in the current database
SHOW FUNCTION STATUS WHERE Db = 'ecommerce_db';

-- View the exact CREATE statement for an existing function
-- (useful for code review, documentation, or migrating servers)
SHOW CREATE FUNCTION get_loyalty_tier;

-- Query the information schema for richer metadata
SELECT
    ROUTINE_NAME,
    ROUTINE_TYPE,
    DATA_TYPE         AS return_type,
    IS_DETERMINISTIC,
    ROUTINE_COMMENT   AS description,
    CREATED,
    LAST_ALTERED
FROM information_schema.ROUTINES
WHERE ROUTINE_SCHEMA = 'ecommerce_db'
  AND ROUTINE_TYPE   = 'FUNCTION'
ORDER BY ROUTINE_NAME;


-- ============================================================
-- UPDATING A FUNCTION (drop-and-recreate pattern)
-- The product team changed the Gold threshold from $1000 to $750
-- ============================================================

-- Always use IF EXISTS to avoid an error if it somehow doesn't exist
DROP FUNCTION IF EXISTS get_loyalty_tier;

DELIMITER $$

CREATE FUNCTION get_loyalty_tier(
    total_spend DECIMAL(10, 2)
)
RETURNS VARCHAR(10)
DETERMINISTIC
COMMENT 'v2: Gold threshold lowered to $750 per 2025 loyalty program update'
BEGIN
    DECLARE tier_label VARCHAR(10);

    IF total_spend >= 750.00 THEN      -- updated threshold
        SET tier_label = 'Gold';
    ELSEIF total_spend >= 250.00 THEN
        SET tier_label = 'Silver';
    ELSE
        SET tier_label = 'Bronze';
    END IF;

    RETURN tier_label;
END$$

DELIMITER ;


-- ============================================================
-- CONTROLLING PERMISSIONS
-- ============================================================

-- Grant the app's read-only user permission to CALL functions
-- but NOT to create or modify them
GRANT EXECUTE ON FUNCTION ecommerce_db.get_loyalty_tier TO 'app_readonly'@'%';

-- Grant a developer account full routine management rights
GRANT CREATE ROUTINE, ALTER ROUTINE, EXECUTE
    ON ecommerce_db.*
    TO 'dev_user'@'localhost';

-- Revoke all routine privileges from a deprecated service account
REVOKE EXECUTE ON ecommerce_db.* FROM 'legacy_service'@'%';

-- Verify current grants on the function
SHOW GRANTS FOR 'app_readonly'@'%';
Output
-- SHOW FUNCTION STATUS output (abbreviated):
+----------------+------------------+-----------+---------+------------------+
| Db | Name | Type | Definer | Is_deterministic |
+----------------+------------------+-----------+---------+------------------+
| ecommerce_db | get_loyalty_tier | FUNCTION | root@% | YES |
+----------------+------------------+-----------+---------+------------------+
-- information_schema query output:
+-------------------+--------------+-------------+-----------------+------------------------------+
| ROUTINE_NAME | ROUTINE_TYPE | return_type | IS_DETERMINISTIC | description |
+-------------------+--------------+-------------+-----------------+------------------------------+
| format_discount | FUNCTION | varchar | YES | NULL |
| get_loyalty_tier | FUNCTION | varchar | YES | v2: Gold threshold lowered |
| days_since_order | FUNCTION | int | NO | NULL |
+-------------------+--------------+-------------+-----------------+------------------------------+
-- GRANT EXECUTE:
Query OK, 0 rows affected (0.01 sec)
💡Pro Tip: Use ROUTINE_COMMENT as Living Documentation
The COMMENT clause on CREATE FUNCTION is stored in information_schema.ROUTINES.ROUTINE_COMMENT and shows up in SHOW FUNCTION STATUS. Use it like a git commit message — include the version, the date, and what changed. It's the cheapest form of database-layer documentation you'll ever write.
📊 Production Insight
A CI/CD pipeline that did not drop functions before recreating caused a stuck migration when a function's signature changed but the old function was not dropped first.
The error 'Function already exists' halted deployment for an hour.
Rule: Always use DROP FUNCTION IF EXISTS before CREATE FUNCTION in migration scripts.
🎯 Key Takeaway
MySQL has no ALTER FUNCTION body — use drop-and-recreate.
Grant app users only EXECUTE, never CREATE ROUTINE.
Use ROUTINE_COMMENT for version tracking.

Performance Considerations for Stored Functions in Production

Stored functions are not free. Every time you call them, MySQL executes the function body. For simple DETERMINISTIC functions with NO SQL, the overhead is small — on par with built-in functions. But functions that read data (READS SQL DATA) can become a hidden performance bomb.

When a function is called in a WHERE clause, MySQL executes it once per row. If the function itself runs a SELECT query, you get an N+1 problem: the outer query returns N rows, and each row triggers an inner query. A query that would take 10ms as a JOIN can balloon to 30 seconds using a function in WHERE.

The fix depends on use case
  • For lookup-style functions (e.g., get category name by ID), replace the function call with a JOIN to the lookup table.
  • For computation-heavy functions (e.g., complex calculations), consider a generated column with a stored value, especially if you filter or sort on the result.
  • For functions used in SELECT lists on reporting queries, inline calls are fine as long as the function is DETERMINISTIC and NO SQL — the overhead is minimal.

Another common performance trap: functions that call other functions. If each nested function reads data, the cost multiplies. Review the full execution plan with EXPLAIN to see how many times the function is invoked.

performance_comparison.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
-- ============================================================
-- BAD: Function that reads a table called in WHERE
-- Runs the SELECT inside get_product_category for every row of the outer query
-- ============================================================
SELECT p.product_name, p.price
FROM products p
WHERE get_product_category(p.product_id) = 'Electronics';

-- EXPLAIN shows: "Using where" with a note that a function is applied per row

-- ============================================================
-- GOOD: Replace with JOIN
-- ============================================================
SELECT p.product_name, p.price
FROM products p
JOIN categories c ON p.category_id = c.category_id
WHERE c.category_name = 'Electronics';

-- EXPLAIN shows index usage on categories.name


-- ============================================================
-- BETTER: Use a generated column for frequently filtered computed values
-- ============================================================
ALTER TABLE products
ADD COLUMN category_name VARCHAR(100)
GENERATED ALWAYS AS (get_category_name(category_id)) VIRTUAL;

-- Now you can create an index on it (MySQL 8.0+):
CREATE INDEX idx_category_name ON products(category_name);

SELECT product_name, price
FROM products
WHERE category_name = 'Electronics';   -- uses index

-- ============================================================
-- Quick check: How many times does your function get called?
-- Run this to see if the function appears in the execution plan
-- ============================================================
EXPLAIN FORMAT=JSON
SELECT ... FROM ... WHERE my_function(col) = X;
-- Look for "materialized_from_subquery" or "dependent" in the JSON plan
Output
EXPLAIN for the BAD query (abbreviated):
+----+-------------+----------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| 1 | SIMPLE | products | NULL | ALL | NULL | NULL | NULL | NULL | 100000 | 100.00 | Using where |
+----+-------------+----------+------------+------+---------------+------+---------+------+--------+----------+-------------+
-- No index used, full table scan with function evaluation per row
⚠ The N+1 Trap: Function in WHERE
A SELECT that returns 50,000 rows and calls a READS SQL DATA function in the WHERE clause executes 50,001 queries: one for the outer query and one per row for the function. That's a full table scan multiplied by another query. Monitor slow query logs for functions appearing as 'dependent subqueries'.
📊 Production Insight
A production incident: a reporting query that filtered by a function took 47 seconds instead of 200ms.
The function called another function that read a 10MB configuration table.
After replacing the function call with a JOIN to the configuration table, the query dropped to 180ms.
Rule: If a function reads data, do NOT use it in WHERE — replace with a JOIN or a generated column.
🎯 Key Takeaway
Stored functions in SELECT lists are cheap.
Stored functions in WHERE clauses are expensive — especially if they read data.
Materialize and index frequent filter conditions instead of using functions.
Should You Use a Stored Function or a Different Approach?
IfFunction is DETERMINISTIC, NO SQL, called in SELECT list
UseSafe to use — overhead negligible.
IfFunction is READS SQL DATA, called in WHERE clause on large table
UseReplace with JOIN to the lookup table.
IfFunction result is filtered or sorted on frequently
UseMaterialize as a generated column and index it.
IfFunction is called only in SELECT list on small result sets
UseFunction use is fine — no significant performance impact.

Calling a Stored Function From a Stored Procedure — Stop Writing Useless Wrappers

A stored function returns one value. A stored procedure does work. These are not the same thing, yet I've seen devs call a function from a procedure just so they can have a procedure do something a SELECT already could. That's cargo cult engineering.

Call a function from a procedure when you need to compute a value mid-logic that is reused across multiple statements. For example: calculate a discount rate once, then use it in an UPDATE, an INSERT, and a logging row. The function keeps the business rule in one place instead of scattered across three SQL statements.

Performance trap: if your function is not DETERMINISTIC and reads data, each call inside the procedure triggers a new query. That's fine for one call. For a loop calling it 10,000 times? You will learn why we have temp tables.

Here's the pattern: declare a local variable, assign the function result to it, then use that variable everywhere. Don't call the function multiple times — that defeats the point.

FulfillmentOrderDiscount.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// io.thecodeforge — database tutorial

DELIMITER $$

CREATE PROCEDURE ApplyBulkDiscount(
    IN order_id INT,
    IN customer_tier VARCHAR(10)
)
BEGIN
    DECLARE discount_rate DECIMAL(5,2);
    
    -- Call function ONCE, store result
    SET discount_rate = CalculateTierDiscount(customer_tier);
    
    UPDATE order_line_items
    SET unit_price = base_price * (1 - discount_rate)
    WHERE fk_order_id = order_id;
    
    INSERT INTO order_audit_log(order_id, discount_applied, applied_at)
    VALUES(order_id, discount_rate, NOW());
END$$

DELIMISTER ;
Output
Query OK, 0 rows affected (0.01 sec)
Query OK, 0 rows affected (0.02 sec)
⚠ Production Trap:
Never call a non-DETERMINISTIC function inside a cursor loop. Each invocation hits the database again. Cache the result in a variable first, or refactor into a JOIN. I've seen a 3-row cursor take 8 seconds because the function was reading the same FK table every iteration.
🎯 Key Takeaway
Call a stored function exactly once per procedure — assign to a variable — and reuse the variable. Multiple function calls in one procedure is a code smell.

Error Handling Inside Stored Functions — Because Your Code WILL Fail

Stored functions have a dirty secret: they cannot use handler statements like DECLARE ... HANDLER that stored procedures can. If your function hits a constraint violation, a foreign key failure, or a division by zero, it will silently return NULL or throw an unhandled exception that kills the calling query.

This is not theory. I inherited a function that divided by a column that could be zero. The function worked for two years until a data migration inserted a zero. Suddenly every SELECT that used that function returned errors. Not NULL. Full stop errors.

Your only defense: validate inputs and use defensive SQL patterns. Check for NULL, check for division by zero, check for out-of-range values. Use IF statements before any risky operation. If the logic is complex enough to need exception handling, it should be a stored procedure — not a function.

If you absolutely must handle an error inside a function, consider using CONTINUE handlers with a simple workaround: set a flag variable and check it after the risky operation. It's ugly, but it works.

SafeCommissionCalc.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// io.thecodeforge — database tutorial

DELIMITER $$

CREATE FUNCTION SafeCommissionPct(sales_amount DECIMAL(12,2), target DECIMAL(12,2))
RETURNS DECIMAL(5,2)
DETERMINISTIC
BEGIN
    DECLARE result DECIMAL(5,2) DEFAULT 0.00;
    
    -- Defensive: guard against NULL and zero division
    IF sales_amount IS NOT NULL AND target IS NOT NULL AND target > 0 THEN
        SET result = (sales_amount / target) * 0.15;
        
        -- Cap commission at 25%
        IF result > 25.00 THEN
            SET result = 25.00;
        END IF;
    END IF;
    
    RETURN result;
END$$

DELIMITER ;
Output
SELECT SafeCommissionPct(50000, 200000);
-- Result: 3.75
SELECT SafeCommissionPct(50000, 0);
-- Result: 0.00 (no error, no silent NULL)
SELECT SafeCommissionPct(NULL, 200000);
-- Result: 0.00
🔥Senior Shortcut:
If you need try-catch logic, don't fight the function syntax. Move the logic to a procedure, call it, and handle errors there. Functions are for pure computations — they aren't transaction handlers.
🎯 Key Takeaway
Stored functions cannot use error handlers. Validate every input and every division. If the logic needs exception handling, it belongs in a stored procedure.

MySQL Stored Functions vs Stored Procedures

Stored functions and stored procedures are both database objects that encapsulate SQL logic, but they serve different purposes and have distinct characteristics. A stored function must return a single value and can be used directly in SQL expressions like SELECT, WHERE, or computed columns. In contrast, a stored procedure can perform multiple operations, return multiple result sets, and does not require a return value. Functions are limited to input parameters only (IN), while procedures support IN, OUT, and INOUT parameters. Functions cannot use statements that produce result sets (like SELECT without INTO), but procedures can. Additionally, functions are often required to be deterministic for replication safety, whereas procedures have more flexibility. Understanding these differences helps you choose the right tool: use functions for computations and data transformations within queries, and use procedures for complex business logic, batch processing, or operations that require transaction control.

function_vs_procedure.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- Stored Function: returns a single value, used in SQL expressions
CREATE FUNCTION calculate_discount(price DECIMAL(10,2), discount_rate DECIMAL(5,2))
RETURNS DECIMAL(10,2)
DETERMINISTIC
BEGIN
    RETURN price * (1 - discount_rate / 100);
END;

-- Usage in SELECT
SELECT product_name, calculate_discount(price, 10) AS discounted_price FROM products;

-- Stored Procedure: can have OUT parameters and multiple statements
CREATE PROCEDURE apply_discount(IN product_id INT, IN discount_rate DECIMAL(5,2), OUT new_price DECIMAL(10,2))
BEGIN
    DECLARE current_price DECIMAL(10,2);
    SELECT price INTO current_price FROM products WHERE id = product_id;
    SET new_price = current_price * (1 - discount_rate / 100);
    UPDATE products SET price = new_price WHERE id = product_id;
END;

-- Calling procedure
CALL apply_discount(1, 10, @new_price);
SELECT @new_price;
🔥Key Distinction
📊 Production Insight
In production, avoid using stored functions in WHERE clauses on large tables if they are non-deterministic, as it can prevent index usage and cause performance issues. Prefer deterministic functions to enable query optimizations and safe replication.
🎯 Key Takeaway
Stored functions return a single value and are used in SQL expressions; stored procedures can perform multiple operations and return result sets. Choose based on whether you need a value for a query or a sequence of actions.

MySQL 8.4: New SQL Features for Stored Functions

MySQL 8.4 introduces several enhancements that improve the usability and performance of stored functions. One notable feature is the ability to use window functions and CTEs (Common Table Expressions) inside stored functions, which was previously restricted. This allows for more complex analytical computations within a function. Additionally, MySQL 8.4 improves error handling with the RESIGNAL statement, enabling functions to propagate custom error messages. The new SQL standard compliance also allows for optional DETERMINISTIC clause enforcement in binary logging, reducing replica drift risks. Another improvement is the support for DEFAULT values in function parameters, simplifying function calls. For example, you can now define a function with a default parameter value, making it optional. These features make stored functions more powerful and flexible for modern application development. However, be aware that some features like window functions may impact performance if used excessively inside functions called in large queries.

mysql84_features.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
-- Window function inside a stored function (MySQL 8.4+)
CREATE FUNCTION get_ranked_price(product_category INT)
RETURNS JSON
DETERMINISTIC
BEGIN
    DECLARE result JSON;
    SELECT JSON_ARRAYAGG(
        JSON_OBJECT('name', name, 'price', price, 'rank', rnk)
    ) INTO result
    FROM (
        SELECT name, price,
               RANK() OVER (ORDER BY price DESC) AS rnk
        FROM products
        WHERE category_id = product_category
    ) ranked;
    RETURN result;
END;

-- Function with default parameter (MySQL 8.4+)
CREATE FUNCTION calculate_tax(amount DECIMAL(10,2), tax_rate DECIMAL(5,2) DEFAULT 0.08)
RETURNS DECIMAL(10,2)
DETERMINISTIC
BEGIN
    RETURN amount * tax_rate;
END;

-- Calling with default
SELECT calculate_tax(100); -- uses default 0.08
SELECT calculate_tax(100, 0.10); -- overrides default
💡Leverage New Features
📊 Production Insight
When upgrading to MySQL 8.4, review existing stored functions to leverage new features like window functions for analytical queries. However, be cautious with non-deterministic functions that use random or time-based window functions, as they can still cause replica drift.
🎯 Key Takeaway
MySQL 8.4 adds window functions, CTEs, default parameters, and improved error handling to stored functions, enabling more sophisticated logic while maintaining replication safety.

Deterministic vs Non-Deterministic Functions: Binary Logging Impact

The DETERMINISTIC attribute is critical for MySQL replication and binary logging. A deterministic function always produces the same output for the same input, while a non-deterministic function may return different results (e.g., using RAND(), NOW(), or UUID()). When binary logging is set to STATEMENT or MIXED mode, MySQL logs the actual SQL statements. If a non-deterministic function is used in a statement, replicas may compute different values, leading to data inconsistency (replica drift). To prevent this, MySQL requires that stored functions used in statements that modify data (INSERT...SELECT, UPDATE, DELETE) be declared DETERMINISTIC or have a specific characteristic like NO SQL or READS SQL DATA. Otherwise, replication may fail or produce incorrect results. In ROW-based logging, the actual row changes are logged, so non-deterministic functions are safe, but performance may be impacted. Best practice: always declare functions as DETERMINISTIC if they are, and avoid non-deterministic functions in replication-critical statements. If you must use non-deterministic logic, consider using ROW-based logging or moving the logic to the application layer.

deterministic_example.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
-- Deterministic function: same input always same output
CREATE FUNCTION add_tax(amount DECIMAL(10,2))
RETURNS DECIMAL(10,2)
DETERMINISTIC
BEGIN
    RETURN amount * 1.08;
END;

-- Non-deterministic function: uses current timestamp
CREATE FUNCTION get_current_timestamp()
RETURNS DATETIME
NOT DETERMINISTIC
BEGIN
    RETURN NOW();
END;

-- This UPDATE may cause replica drift if binary logging is STATEMENT-based
UPDATE orders SET processed_at = get_current_timestamp() WHERE id = 1;

-- Safe alternative: use ROW-based logging or compute timestamp in application
-- Or use a deterministic function with a parameter:
CREATE FUNCTION set_timestamp(ts DATETIME)
RETURNS DATETIME
DETERMINISTIC
BEGIN
    RETURN ts;
END;

UPDATE orders SET processed_at = set_timestamp(NOW()) WHERE id = 1; -- still non-deterministic because NOW() is passed
⚠ Replication Risk
📊 Production Insight
In production, audit all stored functions for correct DETERMINISTIC declaration. Use ROW-based binary logging if non-deterministic functions are unavoidable. Monitor replication lag and consistency checks to catch drift early.
🎯 Key Takeaway
Deterministic functions guarantee consistent results across replicas; non-deterministic functions can cause replica drift in STATEMENT-based binary logging. Always label functions correctly and prefer deterministic logic for replication safety.
● Production incidentPOST-MORTEMseverity: high

The Silent Replica Drift: How a Wrong DETERMINISTIC Declaration Caused Hours of Data Inconsistency

Symptom
A scheduled report on the read replica showed different loyalty tier counts than the primary. The campaign sent 'Gold' offers to customers who had actually dropped to 'Silver' days earlier.
Assumption
The function was safe to mark DETERMINISTIC because it used only input parameters and a simple IF statement.
Root cause
The function called another function that internally used CURDATE() to calculate 'days since last purchase'. That inner function was NOT DETERMINISTIC, but the outer loyalty tier function was declared DETERMINISTIC. MySQL trusted the declaration and cached results on the primary, but the replica replayed the statement binary log — which included the outer function call — and because the outer was marked DETERMINISTIC, the optimizer on the replica reused a stale cached result.
Fix
1. Drop the outer function. 2. Recreate it with NOT DETERMINISTIC and READS SQL DATA (because it reads from the inner function that reads data). 3. Run a full data reconciliation script to correct the loyalty tier assignments on both primary and replica. 4. Add a test in CI that verifies inner function determinism matches outer declarations.
Key lesson
  • Determinism is transitive — if your function calls another function, the least deterministic member forces the whole chain to be NOT DETERMINISTIC.
  • Never trust a function's determinism without reviewing every function it calls.
  • When in doubt, use NOT DETERMINISTIC as the default. The performance penalty is negligible compared to the cost of data inconsistency.
Production debug guideSymptom → Action pairs for diagnosing function failures without guesswork5 entries
Symptom · 01
CREATE FUNCTION fails with syntax error near first line of body
Fix
Check DELIMITER. You must change the delimiter before the function body so MySQL doesn't interpret the first semicolon inside BEGIN...END as the end of the statement. Use: DELIMITER $$ ... DELIMITER ;
Symptom · 02
Function returns NULL unexpectedly
Fix
Isolate the function call: SELECT your_function(test_input). Verify all code paths return a value. If the function runs a SELECT ... INTO, ensure the query returns a row; use IFNULL or a fallback. Check for missing RETURN in an ELSE branch.
Symptom · 03
Function returns wrong value in production but works locally
Fix
Check determinism declaration. If the function uses NOW(), RAND(), or reads from a table, it must be NOT DETERMINISTIC. Compare execution plans on primary vs replica — wrong determinism causes cached results to differ.
Symptom · 04
Slow query after adding function to WHERE clause
Fix
Verify the function contains a SELECT (reads SQL data). If so, it's called per row — potential O(n²) behavior. Replace with a JOIN to a precomputed value, add a generated column, or use a functional index (MySQL 8.0+).
Symptom · 05
Function exists but calling it gives 'EXECUTE command denied to user'
Fix
Check grants: SHOW GRANTS FOR 'user'@'host'. Grant EXECUTE on the specific function: GRANT EXECUTE ON FUNCTION db.func_name TO 'user'@'host'. Application users should only have EXECUTE, not CREATE ROUTINE.
★ Stored Function Quick Debug Cheat SheetFive most common stored function issues — grab the command and fix in under 60 seconds.
Syntax error on CREATE FUNCTION
Immediate action
Insert DELIMITER $$ before CREATE and DELIMITER ; after END
Commands
SHOW WARNINGS; to get the exact line
Check for missing semicolons inside the body
Fix now
Wrap with DELIMITER $$ ... $$ DELIMITER ;
Function returns NULL but shouldn't+
Immediate action
Test with literal input
Commands
SELECT your_function(valid_input);
SHOW CREATE FUNCTION your_function;
Fix now
Add ELSE clause with default RETURN value
Query slow with function in WHERE+
Immediate action
Check if function reads a table
Commands
SHOW CREATE FUNCTION your_function;
EXPLAIN SELECT ... WHERE your_function(col) = X;
Fix now
Replace with JOIN or generated column
Permission denied on function execution+
Immediate action
Check current user grants
Commands
SHOW GRANTS FOR CURRENT_USER();
Identify function schema: SELECT ROUTINE_SCHEMA, ROUTINE_NAME FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_TYPE='FUNCTION';
Fix now
GRANT EXECUTE ON FUNCTION db.func TO 'user'@'host';
Data inconsistency between primary and replica+
Immediate action
Identify all NOT DETERMINISTIC functions used in replicated queries
Commands
SELECT ROUTINE_NAME, IS_DETERMINISTIC FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_TYPE='FUNCTION';
Check binary log format: SHOW VARIABLES LIKE 'binlog_format';
Fix now
Recreate incorrectly marked functions with correct determinism and test with pt-table-checksum
Stored Function vs Stored Procedure
Feature / AspectStored FunctionStored Procedure
Returns a valueYes — exactly one scalar value via RETURNNot directly — uses OUT parameters or result sets
Can be used in SELECT / WHEREYes — called inline like UPPER() or NOW()No — must be called with CALL, separately
Can modify data (INSERT/UPDATE/DELETE)Technically possible but strongly discouragedYes — this is a primary use case
Can return multiple rowsNo — single scalar onlyYes — via SELECT or multiple result sets
Invocation syntaxSELECT my_fn(arg) or WHERE my_fn(arg) = valCALL my_proc(arg)
Use inside triggersYes — functions can be called from trigger bodiesNo — procedures cannot be called from triggers
DETERMINISTIC declaration requiredYes — must explicitly declare behaviorNot required
Best suited forCalculations, formatting, lookups, transformationsWorkflows, batch operations, multi-step transactions
⚙ Quick Reference
10 commands from this guide
FileCommand / CodePurpose
create_loyalty_tier_function.sqlUSE ecommerce_db;What a Stored Function Actually Is (and When to Reach for On
query_with_loyalty_tier.sqlSELECTCalling Your Function
determinism_examples.sqlDELIMITER $$DETERMINISTIC vs NOT DETERMINISTIC
manage_stored_functions.sqlSHOW FUNCTION STATUS WHERE Db = 'ecommerce_db';Managing Functions
performance_comparison.sqlSELECT p.product_name, p.pricePerformance Considerations for Stored Functions in Productio
FulfillmentOrderDiscount.sqlDELIMITER $$Calling a Stored Function From a Stored Procedure
SafeCommissionCalc.sqlDELIMITER $$Error Handling Inside Stored Functions
function_vs_procedure.sqlCREATE FUNCTION calculate_discount(price DECIMAL(10,2), discount_rate DECIMAL(5,...MySQL Stored Functions vs Stored Procedures
mysql84_features.sqlCREATE FUNCTION get_ranked_price(product_category INT)MySQL 8.4
deterministic_example.sqlCREATE FUNCTION add_tax(amount DECIMAL(10,2))Deterministic vs Non-Deterministic Functions

Key takeaways

1
A stored function returns exactly one scalar value and can be called inline anywhere an expression is valid
SELECT list, WHERE clause, ORDER BY, computed columns — unlike a stored procedure which requires CALL.
2
DETERMINISTIC is not just a hint; it affects query optimization, binary log behavior, and replication correctness. Incorrectly labeling a non-deterministic function causes silent wrong results on replicas
always declare it honestly.
3
MySQL has no ALTER FUNCTION for changing the body
the correct update pattern is DROP FUNCTION IF EXISTS followed by CREATE FUNCTION. Build this into every migration script from day one.
4
Grant your application user only EXECUTE on specific functions, never CREATE ROUTINE. This means a compromised app account cannot overwrite your database's business logic
a simple but powerful security boundary.
5
Stored functions that read data in WHERE clauses cause N+1 query patterns. Replace them with JOINs or use generated columns for performance-critical filtering.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is the difference between a stored function and a stored procedure ...
Q02SENIOR
What does the DETERMINISTIC keyword mean when declaring a stored functio...
Q03SENIOR
Can a MySQL stored function execute an INSERT or UPDATE statement? What ...
Q04SENIOR
How do you update the logic of an existing stored function in MySQL?
Q01 of 04SENIOR

What is the difference between a stored function and a stored procedure in MySQL, and how do you decide which one to use?

ANSWER
A stored function returns exactly one scalar value via a RETURN statement and can be used inline in SQL statements like SELECT, WHERE, or ORDER BY. A stored procedure does not directly return a value; it executes actions and may return output via OUT parameters or result sets. Use a function when you need a reusable calculation or lookup that can be embedded in a query. Use a procedure when you need to perform a sequence of operations, including data modifications, and when you don't need to embed the call in a query. Functions are also used inside triggers; procedures cannot be called from triggers.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Can a MySQL stored function call another stored function?
02
Why do I get 'This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA' error?
03
What's the difference between a stored function and a generated column in MySQL?
04
Can I create a stored function in MySQL without specifying DETERMINISTIC or NOT DETERMINISTIC?
05
What is the maximum number of parameters a MySQL stored function can accept?
N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
🔥

That's MySQL & PostgreSQL. Mark it forged?

8 min read · try the examples if you haven't

Previous
PostgreSQL Installation and Setup
4 / 20 · MySQL & PostgreSQL
Next
PostgreSQL JSON Support