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.

Beginner70–90 minutesPortability + migration labLast reviewed: August 2026

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.

01

Identify a portable core of common SQL type families.

02

Compare important SQLite, PostgreSQL, MySQL, SQL Server, and Oracle type differences.

03

Separate logical domain decisions from vendor-specific physical declarations.

04

Design and test a migration-friendly table with documented adaptation points.

Standard language, product dialects

Logical domain model
Portable SQL intent
Dialect-specific declaration
Engine storage + behavior

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.

Portability is evidence

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:

DomainPortable intentQuestions still requiring verification
Whole numberSMALLINT / INTEGER / BIGINTExact range and identity syntax
Exact decimalDECIMAL(p,s) / NUMERIC(p,s)Overflow, rounding, maximum precision
Approximate numberREAL / FLOAT / DOUBLE PRECISIONPrecision and alias mapping
Variable textVARCHAR(n) or a vendor text typeUnicode, length units, large-value limits
BooleanBOOLEAN intentNative type, alias, or constrained integer/bit
Calendar dateDATEAccepted literals and date arithmetic
Date-time / instantTIMESTAMP or vendor equivalentTime-zone semantics and precision
Binary bytesBLOB / VARBINARY / BYTEA / RAWMaximum 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

ConcernSQLitePostgreSQLMySQLSQL ServerOracle
BooleanINTEGER 0/1 conventionNative BOOLEANBOOLEAN alias commonly maps to TINYINT(1)BITOften NUMBER(1) or another constrained representation
Large textTEXTTEXTTEXT familyVARCHAR(MAX) / NVARCHAR(MAX)CLOB / NCLOB
BinaryBLOBBYTEABLOB / BINARY familyVARBINARY(MAX)BLOB / RAW
Date-timeTEXT/REAL/INTEGER conventionsTIMESTAMP with/without time zoneDATETIME and TIMESTAMP differDATETIME2 / DATETIMEOFFSETDATE includes time; TIMESTAMP adds precision/options
Generated integerINTEGER PRIMARY KEY behaviorIDENTITY or sequenceAUTO_INCREMENTIDENTITYIdentity column or sequence
JSONText plus JSON functionsJSON / JSONBNative JSONJSON functions over text storage in current releasesJSON 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.

sqlite · explicit portable intent through constraints
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.

postgresql · native declarations
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.

mysql · representative declarations
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.

sql server · representative declarations
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.

oracle · representative declarations
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:

  1. Logical model: define domains such as Account ID, Display Name, Active Flag, and Creation Instant.
  2. Portable contract: document required range, nullability, precision, uniqueness, and operations.
  3. Dialect migrations: map the contract to native declarations and constraints.
  4. Application adapter: normalize driver-level differences such as Boolean and timestamp values.
  5. Cross-engine tests: run boundary, round-trip, sorting, uniqueness, and invalid-input tests.
TestWhy it detects portability defects
Maximum and minimum numeric valuesReveals range and overflow differences
Unicode and long textReveals encoding and length semantics
Empty string and NULLReveals missing-value differences
Time-zone round tripReveals conversion and precision loss
Decimal arithmeticReveals rounding and scale behavior
Invalid inputReveals permissive conversion or SQL-mode differences

Lab: design a portable account contract

Create a vendor-neutral contract before writing migrations:

text · logical domain contract
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 fields

Then implement an SQLite learning version:

sqlite · contract implementation
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

  1. Map every logical field to PostgreSQL, MySQL, SQL Server, and Oracle declarations.
  2. Identify which engine can use a native Boolean and which needs an adapter.
  3. Define one round-trip test for timestamps and one for Unicode text.
  4. 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

  1. Why is matching type names not proof of portability?
  2. What logical domain can map to BOOLEAN, BIT, or a constrained integer?
  3. Why is Oracle DATE not necessarily equivalent to a date-only type in another system?
  4. 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.

References

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.