Operators in MySQL
MySQL provides a comprehensive set of operators for arithmetic, comparison, pattern matching, logical evaluation, and bit manipulation. Understanding operator behaviour — especially around NULL and precedence — is essential for writing correct queries.
Arithmetic Operators
SELECT 10 + 3 AS addition, -- 13 10 - 3 AS subtraction, -- 7 10 * 3 AS multiplication, -- 30 10 / 3 AS division, -- 3.3333 (always float) 10 DIV 3 AS int_division, -- 3 (integer division, truncates toward zero) 10 % 3 AS modulo, -- 1 10 MOD 3 AS modulo2, -- 1 (same as %) -10 AS negation; -- -10
NULLIF(divisor, 0) or check the divisor in a CASE expression to handle this gracefully.-- Safe division: NULL instead of error when denominator is 0 SELECT price / NULLIF(quantity, 0) AS unit_cost FROM order_items; -- Integer division edge cases SELECT 7 DIV 2; -- 3 (not 3.5) SELECT -7 DIV 2; -- -3 (truncates toward zero, not floor) SELECT 7 MOD -2; -- 1 (sign follows dividend in MySQL) SELECT -7 MOD 2; -- -1
Comparison Operators
SELECT 5 = 5 AS eq, -- 1 (true) 5 != 3 AS neq, -- 1 5 <> 3 AS neq2, -- 1 (same as !=) 5 < 10 AS lt, -- 1 5 > 3 AS gt, -- 1 5 <= 5 AS lte, -- 1 5 >= 6 AS gte, -- 0 (false) NULL = NULL AS null_eq, -- NULL (not 1!) NULL <=> NULL AS null_safe; -- 1 (NULL-safe equality)
<=> returns 1 when both sides are NULL, 0 when one side is NULL. Regular = returns NULL when either side is NULL — making WHERE col = NULL always return zero rows.String vs Number Comparison Gotchas
When MySQL compares a string to a number, it silently converts the string to a number. This implicit coercion can produce surprising results and prevents index usage.
-- String '10abc' converts to 10 for the comparison SELECT '10abc' = 10; -- returns 1 (TRUE!) — the string becomes 10 SELECT '10abc' = '10'; -- returns 0 — both are strings, compared as strings -- This can silently match unintended rows SELECT * FROM users WHERE id = '1 OR 1=1'; -- MySQL converts '1 OR 1=1' to 1 — finds user id=1 only (not an injection risk here) -- But implicit coercion blocks index use -- BAD: index on id (INT) cannot be used because of type mismatch SELECT * FROM users WHERE id = '42'; -- implicit cast string to int -- GOOD: same type, index used SELECT * FROM users WHERE id = 42;
Logical Operators — Truth Tables
-- AND: true only when both sides are true
SELECT 1 AND 1, -- 1
1 AND 0, -- 0
0 AND 0, -- 0
NULL AND 1, -- NULL (unknown)
NULL AND 0; -- 0 (false regardless of NULL)
-- OR: true when at least one side is true
SELECT 1 OR 0, -- 1
0 OR 0, -- 0
NULL OR 1, -- 1 (true regardless of NULL)
NULL OR 0; -- NULL (unknown)
-- NOT: inverts the boolean
SELECT NOT 1, -- 0
NOT 0, -- 1
NOT NULL; -- NULL
-- XOR: true only when exactly one side is true
SELECT 1 XOR 1, -- 0 (both true -> false)
1 XOR 0, -- 1 (one true -> true)
0 XOR 0; -- 0 (both false -> false)
-- Symbol equivalents
SELECT 1 && 1, -- same as AND
1 || 0, -- same as OR
!1; -- same as NOTBETWEEN Operator
BETWEEN low AND high is inclusive on both ends — equivalent to col >= low AND col <= high.
This is important to remember for date ranges.
-- Inclusive range: expr BETWEEN low AND high SELECT * FROM products WHERE price BETWEEN 10.00 AND 99.99; -- NOT BETWEEN SELECT * FROM orders WHERE total NOT BETWEEN 0 AND 100; -- Date range — BETWEEN is inclusive, so this includes all of June 30 SELECT * FROM events WHERE event_date BETWEEN '2024-06-01' AND '2024-06-30'; -- Equivalent to: SELECT * FROM events WHERE event_date >= '2024-06-01' AND event_date <= '2024-06-30';
BETWEEN '2024-06-01' AND '2024-06-30' misses rows with times after midnight on June 30. Use event_date < '2024-07-01' instead.IN and NOT IN
-- Match any value in the list
SELECT * FROM employees WHERE department_id IN (1, 2, 5);
-- Equivalent to multiple OR conditions (but cleaner)
SELECT * FROM orders WHERE status IN ('pending', 'processing', 'on_hold');
-- NOT IN
SELECT * FROM users WHERE role NOT IN ('admin', 'superuser');
-- IN with subquery
SELECT * FROM products
WHERE category_id IN (SELECT id FROM categories WHERE featured = 1);NOT IN with a subquery that might return NULL values — if any value in the list is NULL, the entire NOT IN expression evaluates to NULL (no rows match). Use NOT EXISTS instead for subqueries.LIKE and NOT LIKE
LIKE performs pattern matching with two wildcards: % (zero or more characters) and
_ (exactly one character). For performance, note that leading wildcards (LIKE '%term')
prevent index usage — MySQL must scan the entire table.
-- % = zero or more characters, _ = exactly one character SELECT * FROM customers WHERE last_name LIKE 'Mac%'; -- uses index SELECT * FROM products WHERE sku LIKE 'A_01'; SELECT * FROM articles WHERE title LIKE '%performance%'; -- no index -- NOT LIKE SELECT * FROM users WHERE email NOT LIKE '%@internal.%'; -- Escape special characters with ESCAPE clause SELECT * FROM messages WHERE body LIKE '50% off' ESCAPE '';
REGEXP / RLIKE
REGEXP (alias RLIKE) matches a column against a regular expression. MySQL uses the ICU
regex library in 8.0+, which supports full Unicode and a rich syntax including anchors,
character classes, quantifiers, and alternation.
-- Email format validation
SELECT * FROM users
WHERE email REGEXP '^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$';
-- Phone number format
SELECT * FROM contacts WHERE phone REGEXP '^\+?[0-9]{10,15}$';
-- Starts with a digit
SELECT * FROM products WHERE sku REGEXP '^[0-9]';
-- Anchors: ^ = start, $ = end
-- Character classes: [a-z], [0-9], [^abc] (negation)
-- Quantifiers: * (0+), + (1+), ? (0 or 1), {n,m}
-- Alternation: cat|dog matches 'cat' or 'dog'
SELECT 'hello' REGEXP '^(hello|world)$'; -- 1
-- Case-sensitive match
SELECT * FROM products WHERE sku REGEXP BINARY '^[A-Z]{2}[0-9]{3}$';
-- REGEXP_REPLACE (MySQL 8.0): strip non-digits from phone numbers
SELECT REGEXP_REPLACE(phone, '[^0-9]', '') AS digits_only FROM contacts;IS NULL and IS NOT NULL
-- Check for NULL (= NULL does NOT work) SELECT * FROM employees WHERE manager_id IS NULL; -- Not null SELECT * FROM orders WHERE shipped_at IS NOT NULL; -- In UPDATE UPDATE tasks SET assigned_to = 1 WHERE assigned_to IS NULL; -- Wrong: always returns 0 rows SELECT * FROM employees WHERE manager_id = NULL;
COALESCE
COALESCE returns the first non-NULL value from its argument list. It is the standard SQL
way to provide a fallback when a column might be NULL.
-- Return display_name, fall back to username, then 'Anonymous' SELECT COALESCE(display_name, username, 'Anonymous') AS name FROM users; -- Treat NULL stock as 0 SELECT product_name, COALESCE(stock, 0) AS available FROM products; -- Safe arithmetic: avoid NULL propagation SELECT id, COALESCE(discount_pct, 0) * price AS discount_amount FROM products;
NULLIF
NULLIF(a, b) returns NULL if a equals b, otherwise returns a. Most commonly used to
prevent division-by-zero errors.
-- Safe division: returns NULL instead of division by zero error SELECT total_revenue / NULLIF(total_orders, 0) AS avg_order_value FROM sales_summary; -- Replace empty strings with NULL for cleaner data SELECT NULLIF(TRIM(notes), '') AS clean_notes FROM tickets;
GREATEST and LEAST
-- Return the largest / smallest value from a list of expressions SELECT GREATEST(10, 25, 7, 42) AS greatest_val, -- 42 LEAST(10, 25, 7, 42) AS least_val; -- 7 -- Practical: clamp a discount to valid range 0-50 SELECT product_id, GREATEST(0, LEAST(50, discount_pct)) AS clamped_discount FROM promotions;
GREATEST and LEAST return NULL if any argument is NULL. Use COALESCE to provide defaults before passing to these functions when NULL arguments are possible.Bitwise Operators — Permission Bitmask Example
Bitwise operators work on the binary representation of integers. A common practical use is storing and checking permission flags as a bitmask — each bit represents one permission, and you can test, set, and clear individual bits efficiently.
SELECT 6 & 3 AS bitwise_and, -- 2 (0110 & 0011 = 0010) 6 | 3 AS bitwise_or, -- 7 (0110 | 0011 = 0111) 6 ^ 3 AS bitwise_xor, -- 5 (0110 ^ 0011 = 0101) ~6 AS bitwise_not, -- depends on int size (complement) 1 << 3 AS left_shift, -- 8 (1 * 2^3) 16 >> 2 AS right_shift; -- 4 (16 / 2^2) -- Permission bitmask: 1=read, 2=write, 4=delete, 8=admin CREATE TABLE user_perms ( user_id INT NOT NULL PRIMARY KEY, permissions TINYINT UNSIGNED NOT NULL DEFAULT 1 ); -- Grant write permission to user 42 UPDATE user_perms SET permissions = permissions | 2 WHERE user_id = 42; -- Check if user has write permission (bit 2) SELECT user_id FROM user_perms WHERE permissions & 2 = 2; -- Revoke delete permission (bit 4) UPDATE user_perms SET permissions = permissions & ~4 WHERE user_id = 42; -- Check if user has BOTH read and write (bits 1 and 2) SELECT user_id FROM user_perms WHERE permissions & 3 = 3;
Operator Precedence Table
Precedence (high to low) | Operators |
|---|---|
1 (highest) | INTERVAL |
2 | BINARY, COLLATE |
3 | ! (unary NOT) |
4 |
|
5 | ^ |
6 | *, /, DIV, %, MOD |
7 | +, - |
8 | <<, >> |
9 | & |
10 | | |
11 | =, <=>, !=, <>, <, >, <=, >=, IS, LIKE, REGEXP, IN, BETWEEN |
12 | CASE, WHEN, THEN, ELSE |
13 | NOT |
14 | AND, && |
15 | XOR |
16 (lowest) | OR, || |
a OR b AND c means a OR (b AND c) — not (a OR b) AND c.Best Practices
Use IS NULL and IS NOT NULL for NULL comparisons — never = NULL or != NULL.
Use the NULL-safe equality operator (<=>) when you want NULL = NULL to return true.
Use parentheses to make AND/OR precedence explicit in complex conditions.
Use COALESCE to provide default values for potentially-NULL columns in expressions.
Use NULLIF(col, 0) to prevent division-by-zero errors in calculated columns.
Prefer BETWEEN for range checks — it is clearer than two separate comparisons.
Avoid wrapping indexed columns in functions or operators — MySQL cannot use the index.
Match comparison types — compare INT columns to INT literals, not string literals.