MySQLOperators

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

SQL
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
Note
Division by zero returns NULL in MySQL (no exception is raised). Use NULLIF(divisor, 0) or check the divisor in a CASE expression to handle this gracefully.

SQL
-- 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

SQL
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)
Tip
The NULL-safe equality operator <=> 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.

SQL
-- 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;
Warning
Always compare columns to values of the same type. Type mismatches cause implicit conversions that silently disable index usage, resulting in full table scans on large tables.
Logical Operators — Truth Tables

SQL
-- 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 NOT
BETWEEN 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.

SQL
-- 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';
Note
For DATETIME columns, use explicit range comparisons rather than BETWEEN when the end date has a time component. 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

SQL
-- 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);
Warning
Avoid 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.

SQL
-- % = 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 '';
Tip
Use a full-text index and MATCH ... AGAINST for searching inside text content instead of LIKE with leading wildcards. FULLTEXT search is orders of magnitude faster on large datasets.
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.

SQL
-- 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

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

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

SQL
-- 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

SQL
-- 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;
Note
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.

SQL
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

  • (unary negation), ~ (bitwise NOT)

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, ||

Tip
When in doubt, add parentheses. They make your intent clear to both MySQL and the next developer reading the query. 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.