Data Types
| Type | Aliases accepted | Stored as | Notes |
|---|---|---|---|
BIGINT |
INT, INTEGER, SMALLINT, TINYINT, MEDIUMINT |
64-bit signed | width is advisory, see below |
BIGINT UNSIGNED |
the UNSIGNED form of any of the above |
64-bit unsigned | negatives are refused (1264 / 22003) |
DOUBLE |
FLOAT, REAL |
64-bit float | |
BOOL |
BOOLEAN |
boolean | rendered as 0/1 |
TEXT |
VARCHAR, CHAR, STRING |
UTF-8 string | |
BLOB |
BYTEA |
raw bytes | |
DATE |
days since 1970-01-01 | 'YYYY-MM-DD' |
|
DATETIME |
TIMESTAMP |
microseconds since epoch | 'YYYY-MM-DD HH:MM:SS[.ffffff]' |
TIME |
microseconds since midnight | 'HH:MM:SS[.ffffff]' |
|
DECIMAL(p,s) |
NUMERIC(p,s) |
exact fixed-point | scale preserved |
JSON |
JSONB |
validated text | structural validation on insert |
VECTOR(n) |
n × float32 |
ANN search, see Vector Search |
Integer widths and UNSIGNED
Every integer width is stored as 64 bits: TINYINT, SMALLINT, MEDIUMINT,
INT and BIGINT occupy the same space here. The declared width is still a
constraint, though, and a value too wide for it is refused exactly as MySQL
refuses it:
CREATE TABLE t (a TINYINT);
INSERT INTO t VALUES (300); -- ERROR 1264 (22003): value 300 is out of range
UNSIGNED is enforced on every integer type as well. A negative value is
refused with the same error:
CREATE TABLE t (a INT UNSIGNED);
INSERT INTO t VALUES (-1); -- ERROR 1264 (22003): invalid UNSIGNED value: -1
Tables created by an older version
Both constraints arrived after the fact, and neither is applied retroactively:
- Until 1.8.0 only
BIGINT UNSIGNEDwas enforced; narrower unsigned types were stored as signed. A column created before then keeps the type it was created with and goes on accepting negatives. - Until 1.9.1 no width was recorded at all. A table created before then has no widths stored, so its columns accept any 64-bit value.
In both cases the fix applies from the next CREATE TABLE, or when the
column is re-declared with ALTER TABLE ... MODIFY. Nothing is rewritten on
upgrade, so an existing database cannot start rejecting data it already
holds.
Character length
CHAR(n) and VARCHAR(n) keep their declared length, and a longer string is
refused in strict mode rather than stored:
CREATE TABLE t (a VARCHAR(32));
INSERT INTO t VALUES (REPEAT('x', 40)); -- ERROR 1406 (22001): data too long
Like integer widths, this is recorded per table when the table is created, so a table created before 1.9.2 has no declared length stored and is not retroactively constrained.
Literals and coercion
Values are written as string or numeric literals and coerced to the column type on insert:
CREATE TABLE t (
id BIGINT PRIMARY KEY,
price DECIMAL(10,2),
d DATE,
ts DATETIME,
clock TIME,
doc JSON
);
INSERT INTO t VALUES
(1, 19.99, '2024-01-15', '2024-01-15 09:30:00', '09:30:00', '{"a":1}');
- DECIMAL keeps its declared scale exactly:
19.9stored inDECIMAL(10,2)reads back as19.90, andSUMover decimals is exact. - DATE/DATETIME/TIME accept string literals and compare correctly against
strings (
WHERE d >= '2024-01-01'). - JSON must be structurally valid; invalid JSON is rejected. Nesting is bounded (200 levels) so a pathological document can't exhaust the stack.
- VECTOR accepts a
'[a,b,c]'string literal of the declared dimension. - ENUM and SET are accepted and stored as their string value; a value outside the declared member list is rejected.
Arithmetic semantics
Arithmetic follows MySQL:
- Signed integer overflow raises an error rather than silently wrapping or
saturating:
9223372036854775807 + 1returnsERROR 1690 (22003): BIGINT value is out of range, in both scalar expressions and computed writes (UPDATE ... SET v = v + 1). Integer+,-,*are computed exactly (nof64precision loss for large values). - Division or modulo by zero returns
NULL:1 / 0,1 % 0andMOD(1, 0)are allNULL. DOUBLEoverflow returnsNULLrather thaninf/NaN(e.g.POW(10, 308) * 10).- Values above the signed range use
BIGINT UNSIGNED(exact 64-bit unsigned), andDECIMALarithmetic is exact to its scale.
JSON access
Extract values from JSON columns with the -> / ->> operators or
JSON_EXTRACT, using MySQL-style paths ($, $.key, $[0], chained):
SELECT doc->'$.name' AS name_json, -- returns JSON (quoted)
doc->>'$.name' AS name_text, -- returns unquoted text
doc->>'$.addr.city' AS city,
doc->>'$.tags[0]' AS first_tag,
JSON_EXTRACT(doc, '$.age') AS age
FROM docs;
JSON_UNQUOTE returns the raw scalar of a JSON value. A missing path yields
NULL.
JSON functions
| Function | Description |
|---|---|
JSON_ARRAY(v, ...) |
Build a JSON array |
JSON_OBJECT(k, v, ...) |
Build a JSON object from key/value pairs |
JSON_SET(doc, path, val, ...) |
Insert or update at each path |
JSON_INSERT(doc, path, val, ...) |
Set only paths that do not exist |
JSON_REPLACE(doc, path, val, ...) |
Set only paths that already exist |
JSON_REMOVE(doc, path, ...) |
Remove values at paths |
JSON_CONTAINS(doc, candidate[, path]) |
Containment test (1/0) |
JSON_LENGTH(doc[, path]) |
Element count (arrays/objects) |
JSON_KEYS(doc[, path]) |
Object keys as a JSON array |
JSON_TYPE(val) |
OBJECT/ARRAY/STRING/INTEGER/DOUBLE/BOOLEAN/NULL |
JSON_VALID(str) |
Whether a string parses as JSON |
JSON_QUOTE(str) |
Wrap a string as a JSON string literal |
SELECT JSON_SET('{"a":1}', '$.a', 10, '$.b', 2); -- {"a": 10, "b": 2}
UPDATE docs SET doc = JSON_SET(doc, '$.seen', 1) WHERE id = 5;
SELECT id FROM docs WHERE JSON_LENGTH(doc, '$.tags') >= 2;
Nested paths ($.a.b, $.a[0]) are supported for setting, removing, and
inspecting.
Parenthesize in `WHERE`/`ORDER BY`
The parser binds = tighter than ->>, so wrap the extraction in
parentheses when comparing:
SELECT id FROM docs WHERE (doc->>'$.addr.city') = 'Bergen';
Comparison semantics
Cross-type comparisons are coerced (date vs. text, decimal vs. numeric). NULL
compares as unknown and sorts first under ORDER BY.