Chapter 02 · Tables, Schemas, and Data Types
Portable SQL Types and Vendor-Specific Differences
Separate durable data meaning from vendor syntax so schemas remain understandable, testable, and easier to migrate.
Learning outcomes
SQL is standardized, but database products implement different type names, ranges, aliases, implicit conversions, temporal semantics, auto-generated keys, and specialized types. Portability means preserving data meaning across those differences—not pretending every engine is identical.
Identify a portable core of common SQL type families.
Compare important SQLite, PostgreSQL, MySQL, SQL Server, and Oracle type differences.
Separate logical domain decisions from vendor-specific physical declarations.
Design and test a migration-friendly table with documented adaptation points.
Standard language, product dialects
Keep the domain model stable. Isolate syntax and behavior differences in migrations, adapters, tests, and documentation.
The SQL standard defines broad concepts, but products evolve independently. A type name that parses in two systems may still differ in range, length semantics, time-zone behavior, collation, or implicit conversion rules.
A schema is portable only after its migrations and boundary-value tests succeed in every supported engine. Familiar-looking type names are not sufficient evidence.
A practical portable core
Most relational systems support recognizable equivalents for these domains:
| Domain | Portable intent | Questions still requiring verification |
|---|---|---|
| Whole number | SMALLINT / INTEGER / BIGINT | Exact range and identity syntax |
| Exact decimal | DECIMAL(p,s) / NUMERIC(p,s) | Overflow, rounding, maximum precision |
| Approximate number | REAL / FLOAT / DOUBLE PRECISION | Precision and alias mapping |
| Variable text | VARCHAR(n) or a vendor text type | Unicode, length units, large-value limits |
| Boolean | BOOLEAN intent | Native type, alias, or constrained integer/bit |
| Calendar date | DATE | Accepted literals and date arithmetic |
| Date-time / instant | TIMESTAMP or vendor equivalent | Time-zone semantics and precision |
| Binary bytes | BLOB / VARBINARY / BYTEA / RAW | Maximum size and functions |
The word intent matters. You may express “Boolean” as native BOOLEAN in PostgreSQL, BIT in SQL Server, or a constrained integer in SQLite while preserving the same logical domain.
Major vendor differences at a glance
| Concern | SQLite | PostgreSQL | MySQL | SQL Server | Oracle |
|---|---|---|---|---|---|
| Boolean | INTEGER 0/1 convention | Native BOOLEAN | BOOLEAN alias commonly maps to TINYINT(1) | BIT | Often NUMBER(1) or another constrained representation |
| Large text | TEXT | TEXT | TEXT family | VARCHAR(MAX) / NVARCHAR(MAX) | CLOB / NCLOB |
| Binary | BLOB | BYTEA | BLOB / BINARY family | VARBINARY(MAX) | BLOB / RAW |
| Date-time | TEXT/REAL/INTEGER conventions | TIMESTAMP with/without time zone | DATETIME and TIMESTAMP differ | DATETIME2 / DATETIMEOFFSET | DATE includes time; TIMESTAMP adds precision/options |
| Generated integer | INTEGER PRIMARY KEY behavior | IDENTITY or sequence | AUTO_INCREMENT | IDENTITY | Identity column or sequence |
| JSON | Text plus JSON functions | JSON / JSONB | Native JSON | JSON functions over text storage in current releases | JSON capabilities vary by release |
This table is an orientation, not a migration specification. Confirm the exact version and configuration of each target system.
SQLite: dynamic typing with optional STRICT tables
SQLite values use five storage classes and ordinary columns use affinity. Declaring VARCHAR(50) does not enforce a 50-character limit by itself. STRICT tables strengthen enforcement but intentionally accept only a compact set of declared type names.
CREATE TABLE account ( account_id INTEGER PRIMARY KEY, display_name TEXT NOT NULL CHECK (length(display_name) BETWEEN 1 AND 100), active_flag INTEGER NOT NULL DEFAULT 1 CHECK (active_flag IN (0, 1)), created_at TEXT NOT NULL CHECK (datetime(created_at) IS NOT NULL)) STRICT;The constraints express application meaning that a server DBMS might encode partly through native Boolean, timestamp, and length-constrained character types.
PostgreSQL: rich native domains and strict typing
PostgreSQL provides native Boolean, UUID, JSON/JSONB, arrays, ranges, network-address types, enumerations, domains, and extensive temporal support. These can improve integrity and querying, but they increase migration work when another engine lacks equivalents.
CREATE TABLE account ( account_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, public_id UUID NOT NULL UNIQUE, display_name VARCHAR(100) NOT NULL, active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, preferences JSONB NOT NULL DEFAULT '{}'::jsonb);MySQL: aliases, SQL modes, and temporal distinctions
MySQL commonly uses AUTO_INCREMENT for generated keys. BOOLEAN is an alias associated with a small integer representation. DATETIME and TIMESTAMP differ in range, storage, conversion, and default behavior. SQL modes influence validation and implicit conversion, so development and production modes must match.
CREATE TABLE account ( account_id BIGINT AUTO_INCREMENT PRIMARY KEY, display_name VARCHAR(100) NOT NULL, active BOOLEAN NOT NULL DEFAULT TRUE, created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), preferences JSON NOT NULL);SQL Server: Unicode choices and temporal precision
SQL Server uses BIT for Boolean-style values, IDENTITY for many generated numeric keys, NVARCHAR for Unicode text, DATETIME2 for precise date-time values, and DATETIMEOFFSET when an offset is stored.
CREATE TABLE dbo.account ( account_id BIGINT IDENTITY(1,1) PRIMARY KEY, display_name NVARCHAR(100) NOT NULL, active BIT NOT NULL CONSTRAINT DF_account_active DEFAULT (1), created_at DATETIME2(6) NOT NULL CONSTRAINT DF_account_created DEFAULT (SYSUTCDATETIME()), preferences NVARCHAR(MAX) NOT NULL CONSTRAINT CK_account_preferences_json CHECK (ISJSON(preferences) = 1));Oracle Database: NUMBER, VARCHAR2, DATE, and TIMESTAMP
Oracle commonly uses NUMBER for numeric domains, VARCHAR2 for variable character data, CLOB and BLOB for large values, and DATE for a date-time value that includes time to the second. TIMESTAMP families provide fractional seconds and optional time-zone behavior. Zero-length character strings are treated as NULL in SQL contexts, a major portability difference for applications that distinguish empty from missing text.
CREATE TABLE account ( account_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, display_name VARCHAR2(100 CHAR) NOT NULL, active_flag NUMBER(1) DEFAULT 1 NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL, preferences CLOB NOT NULL, CONSTRAINT ck_account_active CHECK (active_flag IN (0, 1)));Portability architecture
Use layers rather than forcing every engine into its weakest common syntax:
- Logical model: define domains such as Account ID, Display Name, Active Flag, and Creation Instant.
- Portable contract: document required range, nullability, precision, uniqueness, and operations.
- Dialect migrations: map the contract to native declarations and constraints.
- Application adapter: normalize driver-level differences such as Boolean and timestamp values.
- Cross-engine tests: run boundary, round-trip, sorting, uniqueness, and invalid-input tests.
| Test | Why it detects portability defects |
|---|---|
| Maximum and minimum numeric values | Reveals range and overflow differences |
| Unicode and long text | Reveals encoding and length semantics |
| Empty string and NULL | Reveals missing-value differences |
| Time-zone round trip | Reveals conversion and precision loss |
| Decimal arithmetic | Reveals rounding and scale behavior |
| Invalid input | Reveals permissive conversion or SQL-mode differences |
Lab: design a portable account contract
Create a vendor-neutral contract before writing migrations:
account_id purpose: internal identity range: at least signed 64-bit nullability: required uniqueness: primary keyemail purpose: login/contact identifier representation: Unicode text, maximum 320 characters nullability: required uniqueness: case-normalized business ruleactive purpose: two-state flag values: true or false nullability: requiredcreated_at purpose: global creation instant precision: microseconds preferred nullability: requiredprofile_json purpose: optional extensible profile attributes operations: validate JSON and retrieve selected fieldsThen implement an SQLite learning version:
CREATE TABLE account_portable ( account_id INTEGER PRIMARY KEY, email TEXT NOT NULL CHECK (length(email) BETWEEN 3 AND 320), email_normalized TEXT NOT NULL UNIQUE, active INTEGER NOT NULL DEFAULT 1 CHECK (active IN (0, 1)), created_at TEXT NOT NULL CHECK (datetime(created_at) IS NOT NULL), profile_json TEXT CHECK (profile_json IS NULL OR json_valid(profile_json))) STRICT;INSERT INTO account_portable (email, email_normalized, active, created_at, profile_json)VALUES ('Learner@Example.com', 'learner@example.com', 1, '2026-08-05T12:00:00Z', '{"language":"en"}');Create a mapping table for each target engine. Record the native type, generation strategy, default expression, JSON validation approach, and known conversion risks.
Migration exercise
- Map every logical field to PostgreSQL, MySQL, SQL Server, and Oracle declarations.
- Identify which engine can use a native Boolean and which needs an adapter.
- Define one round-trip test for timestamps and one for Unicode text.
- Decide whether profile JSON should use a native JSON type or portable text in each engine.
Common mistakes
Assuming ANSI SQL means identical behavior
Standards reduce differences; they do not eliminate implementation choices, extensions, version gaps, or configuration effects.
Using vendor aliases without documenting the domain
A migration tool can translate syntax only when the intended range, precision, and behavior are explicit.
Choosing the weakest common denominator too early
Native types may provide valuable integrity and performance. Isolate them behind clear contracts and migrations rather than discarding them automatically.
Testing only successful inserts
Portability defects often appear in invalid inputs, overflow, NULL handling, collation, time zones, and round-trip conversion.
Checkpoint and practice
Concept check
- Why is matching type names not proof of portability?
- What logical domain can map to BOOLEAN, BIT, or a constrained integer?
- Why is Oracle DATE not necessarily equivalent to a date-only type in another system?
- Which tests would you require before moving exact monetary values between engines?
Review the answers
Names can hide differences in range, precision, conversion, and semantics. A two-state flag maps to those Boolean representations. Oracle DATE includes a time component. Test precision, scale, rounding, overflow, serialization, and boundary values for money.
Chapter 2 summary
You can now distinguish tables, rows, columns, schemas, and namespaces; select among major type families; preserve meaning through deliberate type choices; reason about NULL and three-valued logic; and isolate vendor differences behind explicit contracts and tests.
Chapter 3 moves from individual columns to keys, relationships, and integrity rules. Its first lesson will distinguish candidate, primary, alternate, natural, and surrogate keys.