Mid-level 5 min · March 05, 2026

MySQL Stored Functions — DETERMINISTIC Causes Replica Drift

Wrong DETERMINISTIC on a MySQL stored function caused replica drift.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
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
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.

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.

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.
● 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

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.

Common mistakes to avoid

4 patterns
×

Forgetting the DELIMITER change before the function body

Symptom
MySQL treats the first semicolon inside BEGIN...END as the end of the CREATE FUNCTION statement, giving a cryptic syntax error on the line after the first semicolon in your body.
Fix
Always wrap the entire CREATE FUNCTION block with DELIMITER $$ at the top and DELIMITER ; at the bottom. GUI tools like MySQL Workbench do this automatically, but CLI and script files do not.
×

Declaring a NOT DETERMINISTIC function as DETERMINISTIC to 'avoid warnings'

Symptom
MySQL trusts your declaration and may cache or log the function incorrectly. In replicated setups, this causes silent data drift on replicas with no error thrown.
Fix
If the function calls NOW(), CURDATE(), RAND(), UUID(), or runs a SELECT on a mutable table, it must be declared NOT DETERMINISTIC. When in doubt, NOT DETERMINISTIC is the safe default.
×

Using a function that does a SELECT inside a high-frequency WHERE clause without understanding the N+1 cost

Symptom
If your function runs a SELECT query and you call it in the WHERE clause of a query returning 50,000 rows, MySQL executes that inner SELECT 50,000 times. The query becomes orders of magnitude slower.
Fix
For lookup-style functions used on large datasets, consider a JOIN instead of a function call, or materialize the computed column into the table with a generated column or scheduled update.
×

Not granting EXECUTE privilege to application users

Symptom
The application gets an 'EXECUTE command denied' error when trying to call the function, even if it has SELECT permissions on the underlying tables.
Fix
Explicitly grant EXECUTE on the function to the application database user: GRANT EXECUTE ON FUNCTION db.func_name TO 'app_user'@'%'. Do not grant CREATE ROUTINE to application users.
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?
🔥

That's MySQL & PostgreSQL. Mark it forged?

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

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