Chapter 03 · Schemas, Tables, Data Types, Keys, Constraints, and SQL Modes
SQL Modes, Strictness, Implicit Conversion, Truncation, and Data-Quality Boundaries
Make MySQL sql_mode an explicit data contract by comparing strict and relaxed session behavior, warnings, coercion, invalid dates, grouping rules, and application mode drift.
Learning outcomes
MySQL SQL modes are part of the behavior of SQL, not merely an administrator preference. A client that inserts invalid data under strict mode may receive an error; another session with a different mode can receive warnings and store coerced values. Tests that do not reproduce production sql_mode can therefore certify behavior your application never sees—or miss data loss that production permits.
The MySQL 8.4 default includes ONLY_FULL_GROUP_BY, STRICT_TRANS_TABLES, NO_ZERO_IN_DATE, NO_ZERO_DATE, ERROR_FOR_DIVISION_BY_ZERO, and NO_ENGINE_SUBSTITUTION. Installation tooling can influence configuration, so always inspect the actual session/global values rather than assuming defaults.
Record global and session sql_mode and explain why session state is the immediate SQL contract.
Compare strict and non-strict handling of out-of-range, truncated, missing, and invalid values using a disposable table.
Use SHOW WARNINGS immediately after warning-producing statements and inspect the stored value.
Explain ONLY_FULL_GROUP_BY as a query-correctness rule involving functional dependence rather than a formatting preference.
Create an application initialization/checklist that prevents dev/test/production mode drift.
All relaxed-mode demonstrations use a disposable table and session-only SET statements. Do not weaken a production global mode just to reproduce a tutorial example.
Global mode versus session mode
@@GLOBAL.sql_mode is the value new sessions inherit. @@SESSION.sql_mode is the behavior contract for the current connection. Changing the global value does not retroactively rewrite existing sessions. Connection pools can therefore contain sessions created under different assumptions if configuration changes are not managed carefully.
SELECT VERSION() AS server_version, @@GLOBAL.sql_mode AS global_sql_mode, @@SESSION.sql_mode AS session_sql_mode;CREATE TABLE IF NOT EXISTS servicehub_lab.mode_probe ( id INT NOT NULL PRIMARY KEY, short_code VARCHAR(5) NOT NULL, qty TINYINT UNSIGNED NOT NULL, service_date DATE NOT NULL) ENGINE=InnoDB;SHOW CREATE TABLE servicehub_lab.mode_probe;Strict mode turns many coercions into statement failures
With STRICT_TRANS_TABLES active on transactional InnoDB tables, invalid or missing values that cannot be safely accepted cause errors in situations where a relaxed mode may coerce values and emit warnings. Strict mode is therefore an important data-quality boundary, but it does not eliminate the need for constraints and application validation.
SET @saved_sql_mode = @@SESSION.sql_mode;-- Keep the current strict session contract.SELECT @@SESSION.sql_mode;-- Too long for VARCHAR(5).INSERT INTO servicehub_lab.mode_probeVALUES (1, 'TOO-LONG', 10, '2026-08-16');SHOW WARNINGS;-- Negative value for TINYINT UNSIGNED.INSERT INTO servicehub_lab.mode_probeVALUES (2, 'NEG', -1, '2026-08-16');SHOW WARNINGS;On the default strict transactional contract, these writes are expected to fail rather than silently store truncated/clamped values. The exact error text is version/build dependent; preserve the real error from your server in lab notes.
Relaxed session mode: observe warnings and the stored value
For the controlled experiment, remove strict mode only from the current session. The safest way is not to hard-code a production replacement mode string; derive a temporary lab mode from the current value and restore it afterward.
SET SESSION sql_mode = sys.list_drop( sys.list_drop(@saved_sql_mode, 'STRICT_TRANS_TABLES'), 'STRICT_ALL_TABLES');SELECT @@SESSION.sql_mode;INSERT INTO servicehub_lab.mode_probeVALUES (10, 'TOO-LONG', 999, '2026-08-16');SHOW WARNINGS;SELECT * FROM servicehub_lab.mode_probe WHERE id=10;SET SESSION sql_mode = @saved_sql_mode;SELECT @@SESSION.sql_mode;Depending on the exact remaining modes, MySQL can warn and store coerced/truncated values rather than reject the statement. The lab is successful only if you inspect both SHOW WARNINGS and the stored row. “Query OK” does not mean the input survived unchanged.
SHOW WARNINGS describes the immediately preceding statement. Run it before another statement replaces the warning context.
Invalid dates and zero-date modes
Date validation combines type rules, strictness, and date-related SQL modes. Historical MySQL applications sometimes relied on zero dates such as '0000-00-00'. Modern schemas should model “unknown date” explicitly—often as NULL when the business meaning permits it—rather than depending on legacy sentinel dates.
SELECT @@SESSION.sql_mode;INSERT INTO servicehub_lab.mode_probeVALUES (20, 'DATE', 1, '0000-00-00');SHOW WARNINGS;-- Prefer an explicit nullable design if "not known yet" is valid.ALTER TABLE servicehub_lab.mode_probe MODIFY service_date DATE NULL;INSERT INTO servicehub_lab.mode_probeVALUES (21, 'NULL', 1, NULL);SELECT id, short_code, qty, service_dateFROM servicehub_lab.mode_probeWHERE id IN (20,21);Do not assume every environment has the same date modes. Record them and write migrations/tests against the target contract.
ONLY_FULL_GROUP_BY protects aggregation semantics
ONLY_FULL_GROUP_BY rejects selected expressions that are neither aggregated nor functionally dependent on the grouping columns. This prevents queries from returning an arbitrary representative value from a group when SQL does not uniquely determine which value is intended.
CREATE TABLE IF NOT EXISTS servicehub_lab.group_probe ( customer_id BIGINT UNSIGNED NOT NULL, customer_name VARCHAR(120) NOT NULL, amount DECIMAL(12,2) NOT NULL) ENGINE=InnoDB;TRUNCATE TABLE servicehub_lab.group_probe;INSERT INTO servicehub_lab.group_probe VALUES (1,'Northwind',10.00), (1,'Northwind',15.00), (2,'Contoso',20.00);-- Potentially invalid under ONLY_FULL_GROUP_BY because customer_name-- is not proven functionally dependent in this unconstrained table.SELECT customer_id, customer_name, SUM(amount)FROM servicehub_lab.group_probeGROUP BY customer_id;-- Deterministic formulation:SELECT customer_id, MAX(customer_name) AS customer_name, SUM(amount) AS totalFROM servicehub_lab.group_probeGROUP BY customer_id;A better schema can also establish functional dependence through keys/uniqueness. Do not disable ONLY_FULL_GROUP_BY merely to make an ambiguous report execute; decide which value is intended and express that rule.
Implicit conversion: convenience can hide data-quality defects
MySQL performs type conversion in expressions and assignments. The result can be surprising when strings that begin with digits are compared to numbers or inserted into numeric columns. Strict mode changes many assignment outcomes, but query-expression conversion still requires deliberate typing.
SELECT CAST('42' AS SIGNED) AS explicit_42, CAST('0042' AS UNSIGNED) AS explicit_unsigned;-- Inspect conversion behavior explicitly rather than relying on it.SELECT '10' = 10 AS string_number_comparison, CAST('10' AS SIGNED) = 10 AS explicit_comparison;-- Application rule: bind numeric parameters as numeric values and validate input-- before building business predicates.The repair pattern is explicit conversion at system boundaries plus correctly typed prepared-statement parameters. Do not scatter casts through every query to compensate for a schema that stores numbers as text.
Failure drill: dev and production use different session modes
Imagine tests run with strict mode, but one production connector issues SET SESSION sql_mode='' at connection startup. The same malformed payload that raises an exception in tests may be coerced into a production row with a warning that the application never reads.
Do not “fix compatibility” by clearing sql_mode globally or in a connector without enumerating which semantic protections you are removing. Treat mode changes like schema migrations: review, test, deploy deliberately, and verify every new session.
A robust connection initialization can assert required modes rather than silently accepting drift. The exact mechanism depends on your connector and privilege model; at minimum, log the session mode and fail health checks when required protections are absent.
SELECT FIND_IN_SET('STRICT_TRANS_TABLES', @@SESSION.sql_mode) > 0 AS strict_trans_tables_on, FIND_IN_SET('ONLY_FULL_GROUP_BY', @@SESSION.sql_mode) > 0 AS only_full_group_by_on, FIND_IN_SET('NO_ZERO_DATE', @@SESSION.sql_mode) > 0 AS no_zero_date_on;-- Your application can treat an unexpected 0 as a configuration failure-- if these modes are part of its declared compatibility contract.Hands-on lab: strictness matrix and cleanup
- Save
@@SESSION.sql_modein a user variable and in your lab notes. - Under the normal mode, test overlong text, negative unsigned numeric input, a missing required value, and a zero date. Record errors/warnings.
- Remove strict modes only for the current session; repeat selected writes and immediately run
SHOW WARNINGS. - Select the stored rows and compare input versus durable state.
- Restore the saved session mode and verify exact equality with the saved string.
- Run the ambiguous
GROUP BYexample and repair it without disablingONLY_FULL_GROUP_BY. - Drop only the disposable probe tables or retain them in a dedicated lab schema for repeatable regression tests.
SET SESSION sql_mode = @saved_sql_mode;SELECT @@SESSION.sql_mode = @saved_sql_mode AS mode_restored;DROP TABLE IF EXISTS servicehub_lab.mode_probe;DROP TABLE IF EXISTS servicehub_lab.group_probe;Knowledge check
- Why is @@SESSION.sql_mode more immediately relevant to one query than @@GLOBAL.sql_mode?
- What extra evidence should you collect after a warning-producing insert?
- Why is clearing sql_mode to make imports “work” dangerous?
- What problem does ONLY_FULL_GROUP_BY prevent?
- Why should sql_mode be tested as part of application compatibility?
Reveal answers
- The session value governs the current connection; the global value mainly supplies defaults for new sessions.
- Run SHOW WARNINGS immediately and then query the stored value to see whether coercion/truncation occurred.
- It can convert data-quality failures into silent/coerced durable values and remove multiple semantic protections at once.
- It prevents ambiguous grouped queries from selecting nonaggregated values that are not determined by the grouping keys.
- Different modes can change whether the same SQL fails, warns, coerces values, or is considered valid.
Chapter synthesis and production judgment
Chapter 03 has built one continuous data-contract story. A MySQL schema is a server namespace. Types define representation. Keys and constraints define identity and allowed states. AUTO_INCREMENT allocates identifiers without promising gapless numbering. sql_mode determines how strictly many SQL boundary conditions are interpreted. None of these should be implicit production folklore.
For deployment, capture effective DDL with SHOW CREATE TABLE, version migrations, record required sql_mode, test under the production server family, and make connector/session initialization observable. Alert on mode drift, failed migrations, unexpected coercion warnings, key exhaustion, and constraint violations that indicate upstream data-quality regressions.
Next chapter: use this schema contract to write precise SELECT queries, joins, subqueries, common table expressions, and set operations while reasoning about NULL and cardinality.
Authoritative references
- MySQL 8.4 Reference Manual — Server SQL Modes
- MySQL 8.4 Reference Manual — Strict SQL Mode
- MySQL 8.4 Reference Manual — sys.list_drop()
- MySQL 8.4 Reference Manual — Type Conversion in Expression Evaluation
- MySQL 8.4 Reference Manual — MySQL Handling of GROUP BY
- MySQL 8.4 Reference Manual — Server Command Options (sql_mode default)
- MySQL 8.4 Release Notes