Loading learning content...
Consider this innocent-looking query:
SELECT * FROM employees WHERE salary > 'fifty thousand'
The syntax is perfect. The table exists. The column exists. Yet this query will likely fail—because you cannot compare a numeric salary to a text string using a greater-than operator. This is a type error, and detecting it is the job of the type checking phase.
Type checking is the semantic analysis phase that ensures all expressions in your query operate on compatible data types. It verifies that:
Without type checking, queries would fail at execution time with cryptic errors—or worse, produce silently incorrect results by interpreting data in unintended ways.
By the end of this page, you will understand SQL's type system, how databases verify type compatibility, when and how implicit type conversions (coercions) occur, and how to diagnose and fix type-related query errors. You'll also learn how type information flows through expressions and what makes some type systems stricter than others.
SQL has a rich type system that has evolved over decades. Understanding the type hierarchy is essential for understanding type checking behavior.
Core SQL Data Types:
The SQL standard defines several type categories:
| Category | Types | Examples |
|---|---|---|
| Numeric | INTEGER, SMALLINT, BIGINT, DECIMAL, NUMERIC, REAL, DOUBLE PRECISION | 42, 3.14159, -1000000000000 |
| Character | CHAR, VARCHAR, TEXT, CLOB | 'Hello', 'SQL is fun' |
| Binary | BINARY, VARBINARY, BLOB, BYTEA | \x48656C6C6F, binary files |
| Temporal | DATE, TIME, TIMESTAMP, INTERVAL | '2024-01-15', '14:30:00' |
| Boolean | BOOLEAN | TRUE, FALSE, NULL |
| UUID | UUID (extension) | 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' |
| JSON | JSON, JSONB (extension) | '{"key": "value"}' |
| Array | Type[] (extension) | '{1,2,3}', ARRAY['a','b'] |
| Composite | User-defined types, ROW | ROW(1, 'value', TRUE) |
Type Properties:
Each type has associated properties that affect type checking:
Type checking must consider these properties. For example, assigning a VARCHAR(100) value to a VARCHAR(50) column may require truncation or raise an error.
Type Compatibility:
Types are compatible if they can be compared, combined, or assigned. The rules vary:
Every expression in SQL has a type, either explicit or inferred. The type checker must determine the type of each expression node in the parse tree.
Type Inference Rules:
Literals:
42) → INTEGER or BIGINT depending on size3.14) → NUMERIC'hello') → Unknown/varchar until context determinesTRUE) → BOOLEANColumn References:
Operators:
1234567891011121314151617181920212223242526272829303132333435
-- Type inference examples -- Literal type inferenceSELECT 42, -- integer (small enough for INT) 9999999999999999999, -- bigint (too large for INT) 3.14159, -- numeric 'hello', -- text/varchar/unknown TRUE, -- boolean NULL; -- unknown (determined by context) -- Operator result typeSELECT 10 + 5, -- integer (int + int) 10 + 5.0, -- numeric (int + numeric → numeric) 10 / 3, -- integer division = 3 10.0 / 3, -- numeric division = 3.333... 'Hello' || ' World', -- text concatenation CURRENT_DATE + 7; -- date + integer = date -- Function return typeSELECT LENGTH('hello'), -- integer UPPER('hello'), -- text NOW(), -- timestamp with time zone ARRAY_LENGTH('{1,2,3}'::int[], 1); -- integer -- Expression compositionSELECT (salary * 12) / 52 AS weekly_from_yearly, -- salary: numeric → *12: numeric → /52: numeric hire_date + (years_employed * INTERVAL '1 year') AS projected_date -- hire_date: date → +interval: timestampFROM employees;Type Propagation:
Types propagate through complex expressions. The type checker walks the AST bottom-up:
Example Type Walk:
SELECT salary * 1.1 + bonus FROM employees
AST with types:
[+] : numeric ← result of numeric + numeric
/
[*] bonus:numeric
/
salary:numeric 1.1:numeric
String literals and NULL initially have an 'unknown' type that gets resolved by context. In WHERE name = 'John', the literal 'John' resolves to the type of name. In SELECT COALESCE(NULL, 0), NULL resolves to INTEGER because 0 is an integer. This deferred resolution allows flexible literal usage.
When types don't match exactly, databases often perform implicit type coercion (also called implicit casting or type promotion). This allows expressions like salary + 100 to work even if salary is DECIMAL and 100 is INTEGER.
Coercion Rules:
Databases define a hierarchy of which types can implicitly convert to others:
Less Precise ──────────────────────→ More Precise
SMALLINT → INTEGER → BIGINT → NUMERIC → FLOAT
Narrower ──────────────────────────→ Wider
CHAR(10) → VARCHAR(10) → VARCHAR(100) → TEXT
Less Information ──────────────────→ More Information
DATE → TIMESTAMP → TIMESTAMP WITH TIME ZONE
Implicit coercion typically goes from less precise to more precise (avoiding data loss).
1234567891011121314151617181920212223242526272829303132
-- Safe implicit coercions (no data loss) -- Integer to numeric (widening)SELECT 10 + 3.5; -- 10 becomes 10.0, result is 13.5 -- Integer to floatSELECT 10 / 3.0; -- 10 becomes 10.0, result is 3.333... -- Char to varcharSELECT 'ab'::CHAR(2) = 'ab'::VARCHAR; -- Works -- Date to timestamp SELECT '2024-01-15'::DATE + INTERVAL '1 hour';-- Date promoted to timestamp for interval arithmetic -- Dangerous implicit coercions (potential data loss) -- Float to integer in some contextsINSERT INTO int_table (value) VALUES (3.99);-- Some DBs: truncates to 3 (data loss!)-- Some DBs: rounds to 4-- Best DBs: error without explicit cast -- Long string to short columnINSERT INTO users (code) VALUES ('ABCDEFGHIJ'); -- code is VARCHAR(5)-- Some DBs: silently truncates to 'ABCDE' (data loss!)-- Better DBs: error -- String to numberSELECT * FROM orders WHERE order_id = '12345';-- order_id is INTEGER, '12345' coerced to integer 12345-- Works but hides potential index usage issuesCoercion Categories in PostgreSQL:
PostgreSQL classifies type conversions into categories:
This fine-grained control prevents unexpected conversions while allowing convenience.
| From Type | To Type | Behavior | Safe? |
|---|---|---|---|
| INTEGER | BIGINT | Always allowed | ✓ Yes |
| INTEGER | NUMERIC | Always allowed | ✓ Yes |
| FLOAT | INTEGER | DB-dependent (often assignment only) | ✗ Data loss |
| VARCHAR(100) | VARCHAR(50) | May truncate | ✗ Data loss |
| TEXT | INTEGER | Usually requires explicit cast | ✗ May fail |
| DATE | TIMESTAMP | Adds midnight time | ✓ Yes |
| TIMESTAMP | DATE | Assignment only, loses time | ✗ Data loss |
Implicit coercion can hide bugs. If a column type changes from INTEGER to VARCHAR, queries comparing with integers might silently start doing VARCHAR comparisons (where '9' > '10' lexicographically). Always be explicit with casts when type conversion is intentional.
Type checking applies throughout the query with context-specific rules. Let's examine type requirements in different SQL clauses.
WHERE Clause:
The WHERE clause must evaluate to a boolean (or NULL):
-- Correct: produces boolean
WHERE salary > 50000
WHERE status = 'active' AND hire_date > '2020-01-01'
-- Error: not boolean
WHERE salary -- Just a number, not a boolean
WHERE 'active' -- Just a string
Comparison Operators:
Both operands must be of comparable types:
-- Same types: always works
WHERE salary = 50000 -- numeric = numeric
WHERE name = 'John' -- text = text
-- Compatible types: works with coercion
WHERE int_column = 100.0 -- int comparable to numeric
-- Incompatible types: error
WHERE date_column = 12345 -- date not comparable to int
12345678910111213141516171819202122232425262728293031323334353637383940414243
-- SELECT list: any expression type allowedSELECT 42, -- integer 'text', -- text TRUE, -- boolean ARRAY[1,2,3], -- integer array ROW(1, 'a'); -- composite -- INSERT: must match column typesINSERT INTO employees (id, name, salary, hire_date)VALUES ( 1, -- must be compatible with id type 'John Doe', -- must be compatible with name type 75000.00, -- must be compatible with salary type '2024-01-15' -- must be compatible with hire_date type); -- UPDATE: SET expressions must match column typesUPDATE employeesSET salary = salary * 1.1, -- salary type = numeric expression ✓ name = 'Updated', -- name type = text ✓ hire_date = CURRENT_DATE; -- hire_date type = date ✓ -- UNION: corresponding columns must be compatibleSELECT id, name, salary FROM employeesUNIONSELECT id, name, hourly_rate * 2080 FROM contractors;-- Column 3: salary (numeric) must be compatible with expression (numeric) ✓ -- GROUP BY: grouping columns must support equality comparisonGROUP BY department_id, hire_date-- Both integer and date support equality ✓ -- ORDER BY: sorting columns must be orderableORDER BY salary DESC, name ASC-- Both numeric and text support ordering ✓ -- CASE expressions: all branches must have compatible typesSELECT CASE status WHEN 'A' THEN 'Active' -- text WHEN 'I' THEN 'Inactive' -- text ELSE 'Unknown' -- textEND AS status_label; -- All branches are text ✓Functions have type signatures that define their parameter types and return type. Type checking verifies that function calls provide arguments matching an available signature.
Function Signature Components:
function_name(param1_type, param2_type, ...) → return_type
SUBSTRING(text, integer, integer) → text
ROUND(numeric, integer) → numeric
COALESCE(anytype, anytype, ...) → anytype
Overloading:
Many SQL functions are overloaded—multiple signatures with the same name:
-- PostgreSQL ROUND overloads:
ROUND(DOUBLE PRECISION) → DOUBLE PRECISION
ROUND(NUMERIC) → NUMERIC
ROUND(NUMERIC, INTEGER) → NUMERIC
The type checker must select the best matching overload.
12345678910111213141516171819202122232425262728
-- Function overload resolution -- Exact match preferredSELECT ROUND(3.14159); -- Uses ROUND(numeric) → 3 -- Argument count selects overloadSELECT ROUND(3.14159, 2); -- Uses ROUND(numeric, int) → 3.14 -- Type coercion to find matchSELECT ROUND(3); -- 3 is integer, no ROUND(int) -- Coerced to numeric, uses ROUND(numeric) -- Ambiguous overload errorCREATE FUNCTION foo(INTEGER) RETURNS INTEGER AS $$ SELECT $1; $$ LANGUAGE SQL;CREATE FUNCTION foo(BIGINT) RETURNS BIGINT AS $$ SELECT $1; $$ LANGUAGE SQL;SELECT foo(42); -- 42 fits both INTEGER and BIGINT equally well-- ERROR: function foo(integer) is ambiguous -- Resolve with explicit castSELECT foo(42::INTEGER); -- Now clearly calls foo(INTEGER)SELECT foo(42::BIGINT); -- Now clearly calls foo(BIGINT) -- Polymorphic functions-- COALESCE accepts any type but all must be compatibleSELECT COALESCE(null, 0); -- Returns integerSELECT COALESCE(null, 'default'); -- Returns textSELECT COALESCE(null, 0, 'text'); -- Error: incompatible typesOverload Resolution Algorithm:
Type Distance Examples:
When overload resolution fails with 'ambiguous function call', add explicit casts to your arguments. This both fixes the error and documents your intent. For example: SUM(column::NUMERIC) clearly specifies which SUM overload you want.
Type errors are among the most common query failures. Let's examine typical type errors and how to diagnose and fix them.
Error Category 1: Direct Type Incompatibility
12345678910111213141516171819202122
-- Error: Comparing incompatible typesSELECT * FROM orders WHERE order_date = 12345;-- ERROR: operator does not exist: date = integer -- Fix: Use appropriate literal typeSELECT * FROM orders WHERE order_date = '2024-01-15';SELECT * FROM orders WHERE order_date = DATE '2024-01-15'; -- Error: Arithmetic on non-numeric typeSELECT name + 10 FROM employees;-- ERROR: operator does not exist: character varying + integer -- Fix: Use appropriate operationSELECT name || ' (ID: ' || employee_id || ')' FROM employees; -- Error: Boolean expression expectedSELECT * FROM employees WHERE salary;-- ERROR: argument of WHERE must be type boolean -- Fix: Create boolean expressionSELECT * FROM employees WHERE salary > 0;SELECT * FROM employees WHERE salary IS NOT NULL;Error Category 2: Function Type Mismatch
12345678910111213141516171819202122232425262728
-- Error: Function doesn't exist for argument typesSELECT LENGTH(12345);-- ERROR: function length(integer) does not exist-- HINT: No function matches the given name and argument types. -- Fix: Cast to appropriate typeSELECT LENGTH(12345::TEXT); -- Returns 5 -- Error: String function on non-stringSELECT UPPER(salary) FROM employees;-- ERROR: function upper(numeric) does not exist -- Fix: Cast to string if that's what you wantSELECT UPPER(salary::TEXT) FROM employees; -- Error: Aggregate type mismatchSELECT SUM(name) FROM employees;-- ERROR: function sum(character varying) does not exist -- Fix: Use string aggregate if neededSELECT STRING_AGG(name, ', ') FROM employees; -- Error: Date function on wrong typeSELECT EXTRACT(YEAR FROM employee_id) FROM employees;-- ERROR: cannot extract year from type integer -- Fix: Use correct columnSELECT EXTRACT(YEAR FROM hire_date) FROM employees;Error Category 3: Insert/Update Type Mismatch
1234567891011121314151617181920212223242526
-- Error: Wrong type for columnINSERT INTO employees (id, hire_date) VALUES (1, 'not-a-date');-- ERROR: invalid input syntax for type date: "not-a-date" -- Fix: Use valid date formatINSERT INTO employees (id, hire_date) VALUES (1, '2024-01-15'); -- Error: Type conversion failureINSERT INTO products (price) VALUES ('N/A');-- ERROR: invalid input syntax for type numeric: "N/A" -- Fix: Use NULL for missing numericINSERT INTO products (price) VALUES (NULL);-- Or use CASE in data migrationINSERT INTO products (price)SELECT CASE WHEN price_str = 'N/A' THEN NULL ELSE price_str::NUMERIC ENDFROM import_table; -- Error: Precision overflowINSERT INTO items (quantity) VALUES (99999999999999999999);-- ERROR: integer out of range -- Fix: Use appropriate typeALTER TABLE items ALTER COLUMN quantity TYPE BIGINT;-- Or check data validity before insert\d tablename (psql) or equivalent to verify typesWhen implicit coercion isn't sufficient (or when you want to be explicit), SQL provides explicit casting syntax.
Cast Syntax Variants:
123456789101112131415161718
-- CAST function (ANSI SQL standard)SELECT CAST(salary AS INTEGER) FROM employees;SELECT CAST('2024-01-15' AS DATE);SELECT CAST(123 AS VARCHAR(10)); -- Double-colon syntax (PostgreSQL, commonly used)SELECT salary::INTEGER FROM employees;SELECT '2024-01-15'::DATE;SELECT 123::VARCHAR(10); -- Type function syntax (some databases)SELECT INTEGER(salary) FROM employees;SELECT DATE('2024-01-15'); -- Comparison: All equivalent in PostgreSQLSELECT CAST(3.14159 AS INTEGER); -- Standard SQLSELECT 3.14159::INTEGER; -- PostgreSQL shorthandSELECT INTEGER '3.14159'; -- Type literal syntax (constants only)When to Cast Explicitly:
12345678910111213141516171819202122232425262728
-- Numeric precision controlSELECT 10 / 3; -- Integer division: 3SELECT 10::NUMERIC / 3; -- Decimal division: 3.333...SELECT 10 / 3::NUMERIC; -- Also decimal: 3.333...SELECT (10 / 3)::NUMERIC; -- Still 3 (division already happened) -- String to number conversionSELECT '123'::INTEGER; -- 123SELECT '123.45'::NUMERIC; -- 123.45SELECT 'abc'::INTEGER; -- ERROR: invalid input syntax -- Date/time conversionsSELECT NOW()::DATE; -- Current date (drops time)SELECT NOW()::TIME; -- Current time (drops date)SELECT '2024-01-15'::TIMESTAMP; -- Midnight on that date -- Complex castingSELECT ARRAY[1,2,3]::TEXT; -- '{1,2,3}' (array to text)SELECT '{1,2,3}'::INTEGER[]; -- {1,2,3} (text to array) -- JSON castingSELECT '{"key": "value"}'::JSON;SELECT '{"key": "value"}'::JSONB;SELECT row_to_json(t) FROM mytable t; -- Row to JSON -- Casting with NULL handlingSELECT COALESCE(nullable_col::TEXT, 'N/A');SELECT NULLIF(value::TEXT, '')::INTEGER; -- Empty string to NULL to intNot all casts succeed. Casting 'hello' to INTEGER fails at runtime. Casting a large BIGINT to INTEGER may overflow. Test your casts with edge cases, and consider using TRY_CAST (SQL Server) or error handling where available.
Type checking ensures that all expressions in your query operate on compatible data types. Let's consolidate the key concepts:
What's Next:
With syntax validated, semantics checked, names resolved, and types verified, the query's abstract syntax tree is fully annotated and ready for the next phase. The final page in this module examines the parse tree itself—its structure, components, and how it serves as the foundation for query optimization and execution.
You now understand SQL type checking: how databases infer types, apply implicit coercions, verify type compatibility across query contexts, and how to diagnose and fix type-related errors. This knowledge helps you write type-safe queries and understand type error messages.