MySQL Stored Functions MySQL · Stored Functions

MySQL Stored Functions

Master MySQL stored functions — the CREATE FUNCTION syntax, the differences from stored procedures, the three function characteristics required to avoid ERROR 1418 (DETERMINISTIC, NO SQL, READS SQL DATA), restrictions on functions, and worked examples covering pure computation, salary classification with IF/ELSEIF, table lookups with READS SQL DATA, functions in WHERE clauses, and calling functions from procedures.

A stored function is a named, parameterized block of SQL code that returns exactly one scalar value. You define it once with CREATE FUNCTION, then call it from anywhere in any expression — inside SELECT column lists, WHERE clauses, ORDER BY, even other stored programs. Use stored functions to encapsulate reusable formulas and business rules — tax calculations, format conversions, status mappings, anything you'd otherwise repeat across many queries.

The "function" name is literal: just like a function in any programming language, it takes inputs, computes, and returns one value. That distinguishes it sharply from a stored procedure, which is invoked with CALL and may return result sets but isn't usable in an expression.

The two stored-program types look similar but have fundamentally different purposes:

AspectFunctionProcedure
Invoked withIn any expression: SELECT fn(x), WHERE fn(x) > 5CALL proc_name(...) as its own statement
ReturnsExactly one scalar value via RETURNZero or more result sets via SELECT, plus OUT/INOUT params
Parameter modesIN only — implicit, can't be specifiedIN (default), OUT, INOUT
Required RETURNS clauseYes — RETURNS datatypeNo
Use inside SELECT/WHEREYes — that's the main reason they existNo
Can return result setsNo — single scalar onlyYes
Use in generated columnsYes (if DETERMINISTIC)No
Best forReusable computations and formulasMulti-statement work, transactions, batch processing
💡 Rule of thumb: If your code returns one value and could be inlined into a SELECT, write it as a function. If it modifies data, returns multiple rows, or runs as its own logical unit of work, write it as a procedure. Some teams prefix function names (fn_) and procedure names (sp_) so you can tell them apart at a glance.
DELIMITER $$ CREATE FUNCTION function_name( param1 datatype, param2 datatype, ... ) RETURNS return_datatype [characteristic ...] BEGIN -- declarations and statements RETURN some_expression; END $$ DELIMITER ;

The required pieces:

  • function_name — the name you'll use to call it. Unique per database.
  • Parameter list — zero or more parameters, each with a name and a data type. No mode keyword — all parameters are implicitly IN.
  • RETURNS datatype — the type of the value the function will return. Required, no exceptions.
  • At least one characteristic (see below) — required when binary logging is on.
  • RETURN expression — somewhere in the body, returning a value of the declared type. The function exits as soon as RETURN executes.

Function characteristics tell MySQL what kind of work the function does — does it read tables, modify them, depend only on its inputs, etc. The key ones:

CharacteristicMeaning
DETERMINISTICSame inputs always produce the same output. UPPER(s) is deterministic; NOW() is not.
NOT DETERMINISTICOutput may differ for the same inputs (default if neither is specified).
NO SQLFunction does pure computation — no SQL statements at all.
CONTAINS SQL (default)Function contains SQL but doesn't read or write tables.
READS SQL DATAFunction only SELECTs from tables, doesn't modify them.
MODIFIES SQL DATAFunction may run INSERT/UPDATE/DELETE.
SQL SECURITY DEFINER (default)Function runs with the creator's privileges.
SQL SECURITY INVOKERFunction runs with the caller's privileges.
⚠️ ERROR 1418 — the most common stored-function gotcha. When binary logging is enabled (the default for any server set up for replication or point-in-time recovery), MySQL refuses to create a function unless you declare at least one of: DETERMINISTIC, NO SQL, or READS SQL DATA. The error message:
ERROR 1418 (HY000): This function has none of DETERMINISTIC, NO SQL,
or READS SQL DATA in its declaration and binary logging is enabled
(you *might* want to use the less safe log_bin_trust_function_creators variable)
The fix: just add the appropriate characteristic. Most functions are at least READS SQL DATA if they query a table, or DETERMINISTIC if they only compute on their inputs.
MySQL — Setup
CREATE DATABASE IF NOT EXISTS gowtham;
USE gowtham;

DROP TABLE IF EXISTS employees;

CREATE TABLE employees (
    employee_id     INT UNSIGNED PRIMARY KEY,
    first_name      VARCHAR(20),
    last_name       VARCHAR(25)    NOT NULL,
    salary          DECIMAL(10, 2) NOT NULL,
    department_id   INT
);

INSERT INTO employees VALUES
    (100, 'Steven',    'King',      24000.00,  9),
    (101, 'Neena',     'Kochhar',   17000.00,  9),
    (102, 'Lex',       'De Haan',   17000.00,  9),
    (103, 'Alexander', 'Hunold',     9000.00,  6),
    (104, 'Bruce',     'Ernst',      6000.00,  6),
    (105, 'David',     'Austin',     4800.00,  6),
    (106, 'Valli',     'Pataballa',  4800.00,  6),
    (107, 'Diana',     'Lorentz',    4200.00,  6),
    (108, 'Nancy',     'Greenberg', 12000.00, 10),
    (109, 'Daniel',    'Faviet',     9000.00, 10);
Example 1 — A Simple Pure-Computation Function

The simplest case: a function that does pure math with no database access. Convert a salary to its hourly equivalent (assuming a 2080-hour work year):

MySQL — Pure DETERMINISTIC function
DROP FUNCTION IF EXISTS hourly_rate;

DELIMITER $$

CREATE FUNCTION hourly_rate(annual_salary DECIMAL(10, 2))
RETURNS DECIMAL(10, 2)
DETERMINISTIC
NO SQL
BEGIN
    RETURN annual_salary / 2080;
END$$

DELIMITER ;

-- Use it directly in a SELECT
SELECT first_name, last_name, salary, hourly_rate(salary) AS hourly
FROM   employees
ORDER  BY salary DESC
LIMIT  5;
OUTPUT
mysql — hourly_rate
mysql>CREATE FUNCTION hourly_rate(...) RETURNS DECIMAL(10,2) DETERMINISTIC NO SQL ... END$$
Query OK, 0 rows affected (0.02 sec)
mysql>SELECT first_name, last_name, salary, hourly_rate(salary) AS hourly FROM employees ORDER BY salary DESC LIMIT 5;
+------------+-----------+----------+----------+
| first_name | last_name | salary | hourly |
+------------+-----------+----------+----------+
| Steven | King | 24000.00 | 11.54 |
| Neena | Kochhar | 17000.00 | 8.17 |
| Lex | De Haan | 17000.00 | 8.17 |
| Nancy | Greenberg | 12000.00 | 5.77 |
| Alexander | Hunold | 9000.00 | 4.33 |
+------------+-----------+----------+----------+
5 rows in set (0.00 sec)

Notice hourly_rate(salary) appears in the SELECT column list as if it were any built-in function. This is what stored functions are for — you can't do this with a procedure.

Example 2 — Salary Range Classifier with IF/ELSEIF

A function returning a categorical label based on the input. The original tutorial alluded to this function but never actually wrote it — here's the full definition:

MySQL — Salary range function
DROP FUNCTION IF EXISTS empsal_range;

DELIMITER $$

CREATE FUNCTION empsal_range(p_salary DECIMAL(10, 2))
RETURNS VARCHAR(10)
DETERMINISTIC
NO SQL
BEGIN
    DECLARE label VARCHAR(10);

    IF p_salary >= 17000 THEN
        SET label = 'high';
    ELSEIF p_salary >= 10000 THEN
        SET label = 'avg';
    ELSE
        SET label = 'low';
    END IF;

    RETURN label;
END$$

DELIMITER ;

-- Use it on every row
SELECT first_name, last_name, salary, empsal_range(salary) AS range_label
FROM   employees
ORDER  BY salary DESC;
OUTPUT
mysql — empsal_range
mysql>SELECT first_name, last_name, salary, empsal_range(salary) AS range_label FROM employees ORDER BY salary DESC;
+------------+-----------+----------+-------------+
| first_name | last_name | salary | range_label |
+------------+-----------+----------+-------------+
| Steven | King | 24000.00 | high |
| Neena | Kochhar | 17000.00 | high |
| Lex | De Haan | 17000.00 | high |
| Nancy | Greenberg | 12000.00 | avg |
| Alexander | Hunold | 9000.00 | low |
| Daniel | Faviet | 9000.00 | low |
| Bruce | Ernst | 6000.00 | low |
| David | Austin | 4800.00 | low |
| Valli | Pataballa | 4800.00 | low |
| Diana | Lorentz | 4200.00 | low |
+------------+-----------+----------+-------------+
10 rows in set (0.00 sec)

Now empsal_range(salary) works as a single column in any query — like a tiny view materialized per-row.

Example 3 — Function That Reads From Tables

Functions can also query tables to compute their result. Look up an employee's full name by ID:

MySQL — Function with READS SQL DATA
DROP FUNCTION IF EXISTS emp_full_name;

DELIMITER $$

CREATE FUNCTION emp_full_name(p_emp_id INT)
RETURNS VARCHAR(50)
READS SQL DATA
BEGIN
    DECLARE full_name VARCHAR(50);

    SELECT CONCAT(first_name, ' ', last_name)
    INTO   full_name
    FROM   employees
    WHERE  employee_id = p_emp_id
    LIMIT  1;

    RETURN COALESCE(full_name, '(unknown)');
END$$

DELIMITER ;

SELECT emp_full_name(100) AS who_is_100,
       emp_full_name(108) AS who_is_108,
       emp_full_name(999) AS who_is_999;
OUTPUT
mysql — emp_full_name
mysql>SELECT emp_full_name(100), emp_full_name(108), emp_full_name(999);
+--------------+------------------+--------------+
| who_is_100 | who_is_108 | who_is_999 |
+--------------+------------------+--------------+
| Steven King | Nancy Greenberg | (unknown) |
+--------------+------------------+--------------+
1 row in set (0.00 sec)
💡 Three good practices in this function:
  • LIMIT 1 — guards against multiple-row errors (1172) if the WHERE somehow matches more than one
  • COALESCE(full_name, '(unknown)') — handles the case where no row matches; SELECT INTO would leave full_name as NULL
  • READS SQL DATA characteristic — required because the function queries a table, also makes the function safe for replication
Example 4 — Functions in WHERE Clauses

The killer use case: filter rows using a stored function. Find everyone classified as "high" salary:

MySQL — Function in WHERE
SELECT employee_id, first_name, last_name, salary
FROM   employees
WHERE  empsal_range(salary) = 'high'
ORDER  BY salary DESC;
OUTPUT
mysql — function in WHERE
mysql>SELECT ... FROM employees WHERE empsal_range(salary) = 'high' ORDER BY salary DESC;
+-------------+------------+-----------+----------+
| employee_id | first_name | last_name | salary |
+-------------+------------+-----------+----------+
| 100 | Steven | King | 24000.00 |
| 101 | Neena | Kochhar | 17000.00 |
| 102 | Lex | De Haan | 17000.00 |
+-------------+------------+-----------+----------+
3 rows in set (0.00 sec)
⚠️ Functions in WHERE clauses can defeat indexes. A query like WHERE empsal_range(salary) = 'high' has to compute the function for every row before filtering — even if there's an index on salary. For predicates that need to be fast on large tables, write the condition out: WHERE salary >= 17000. Use functional indexes (MySQL 8.0+) only when you genuinely can't avoid the function in the predicate.

The same tools that list procedures work for functions:

MySQL — List functions
-- All functions in a database
SHOW FUNCTION STATUS WHERE db = 'gowtham';

-- Same thing, programmatic version
SELECT routine_name, data_type AS returns
FROM   information_schema.ROUTINES
WHERE  routine_type   = 'FUNCTION'
  AND  routine_schema = 'gowtham';

-- Full DDL
SHOW CREATE FUNCTION empsal_range\G
OUTPUT
mysql — list functions
mysql>SELECT routine_name, data_type AS returns FROM information_schema.ROUTINES WHERE ...;
+---------------+------------+
| routine_name | returns |
+---------------+------------+
| emp_full_name | varchar |
| empsal_range | varchar |
| hourly_rate | decimal |
+---------------+------------+
3 rows in set (0.00 sec)
Example 5 — Function Used by a Procedure

A procedure that uses our salary-range function:

MySQL — Procedure calling a function
DROP PROCEDURE IF EXISTS GetEmpDetails;

DELIMITER $$

CREATE PROCEDURE GetEmpDetails()
    READS SQL DATA
    COMMENT 'List employees with their salary range label'
BEGIN
    SELECT first_name,
           last_name,
           salary,
           empsal_range(salary) AS range_label
    FROM   employees
    ORDER  BY salary DESC;
END$$

DELIMITER ;

CALL GetEmpDetails();
OUTPUT
mysql — GetEmpDetails
mysql>CALL GetEmpDetails();
+------------+-----------+----------+-------------+
| first_name | last_name | salary | range_label |
+------------+-----------+----------+-------------+
| Steven | King | 24000.00 | high |
| Neena | Kochhar | 17000.00 | high |
| Lex | De Haan | 17000.00 | high |
| Nancy | Greenberg | 12000.00 | avg |
| ... (6 more rows, all "low") |
+------------+-----------+----------+-------------+
10 rows in set (0.00 sec)

Functions are also callable from triggers, events, and other functions — anywhere SQL expressions are valid.

DROP FUNCTION [IF EXISTS] function_name;
MySQL — Safe drop
DROP FUNCTION IF EXISTS empsal_range;

Always use IF EXISTS in deployment scripts. Note: like DROP PROCEDURE, no parentheses on the name.

Functions have several restrictions that procedures don't, because they need to behave predictably when used inside expressions:

RestrictionWhy
Can only return a single scalar valueThe point of functions — they're called inside expressions where one value fits
Can't return a result set with bare SELECTResult sets don't fit in expressions; use SELECT INTO var to capture into a local variable instead
Parameters are IN onlyOUT/INOUT don't make sense — there's no caller variable to write back to
Can't call dynamic SQL (PREPARE / EXECUTE)Disallowed inside functions — use a procedure if you need dynamic SQL
Can't call procedures that return result setsSame reason — no place to put a result set inside an expression
Can't directly start/commit transactionsThe calling statement is already in a transaction; nesting isn't allowed
Functions used in indexed-column predicates can defeat indexesThe optimizer has to evaluate the function for each row, so index lookups don't apply unless you use a functional index (8.0+)

If you have many existing functions to migrate without adding characteristics, MySQL has an escape hatch:

MySQL — Trust function creators
-- Skip the determinism check — risky for replication, use sparingly
SET GLOBAL log_bin_trust_function_creators = 1;
⚠️ Don't reach for this lightly. The setting tells MySQL "trust me, I know what I'm doing" — it disables the safety check that prevents non-deterministic functions from corrupting replication. The right fix in 99% of cases is to add the appropriate characteristic (DETERMINISTIC, NO SQL, or READS SQL DATA) to your function definition. Use this setting only as a last resort and revisit later.
  1. Always declare characteristics. DETERMINISTIC for pure computation, READS SQL DATA for table reads, NO SQL for math-only functions. Avoids ERROR 1418 and helps the optimizer.
  2. Match RETURNS type to actual return values. Implicit conversions can mask bugs.
  3. Always use DROP FUNCTION IF EXISTS at the top of deployment scripts.
  4. Prefix parameter names (p_salary) to avoid shadowing column names. Inside a SELECT INTO, WHERE salary = salary is a self-comparison, not what you want.
  5. Use LIMIT 1 on SELECT INTO when the predicate isn't on a unique key — prevents error 1172 if more than one row matches.
  6. Handle no-match cases. A SELECT INTO with no matching rows leaves the variable NULL; use COALESCE or check explicitly.
  7. Don't use functions in WHERE clauses on large tables unless you've added a functional index. The function runs per row.
  8. Keep functions deterministic when possible. Deterministic functions can be cached, used in generated columns, and indexed.
  9. Add a COMMENT describing what the function does and any non-obvious assumptions.
  • A stored function is a named, parameterized SQL block that returns exactly one scalar value via RETURN. Use it anywhere an expression is allowed: SELECT columns, WHERE clauses, ORDER BY, even in other functions.
  • Functions differ from procedures: functions return a value usable in expressions; procedures use CALL and may return result sets and OUT/INOUT parameters.
  • All function parameters are IN-only — no mode keyword needed or allowed.
  • Required syntax: CREATE FUNCTION name(params) RETURNS type [characteristic ...] BEGIN ... RETURN expr; END.
  • Always declare a characteristic (DETERMINISTIC, NO SQL, or READS SQL DATA) — without one, you'll hit ERROR 1418 on any server with binary logging enabled.
  • Restrictions: no result sets, no OUT/INOUT, no PREPARE, no transaction control, scalar return only.
  • Functions in WHERE clauses can defeat indexes — for performance-critical predicates, inline the logic or use a functional index (MySQL 8.0+).