3.1 Relational Data Types & Table Design
Key Takeaways
- PostgreSQL provides standard integer types (smallint, integer, bigint) and arbitrary-precision numeric/decimal, where numeric guarantees exact precision for financial computations at the expense of CPU speed compared to hardware floating-point types (real, double precision).
- Character types text, varchar(n), and char(n) share identical underlying varlena storage structures and retrieval performance in PostgreSQL; char(n) actually incurs additional CPU and storage overhead due to mandatory blank-padding semantics.
- The timestamptz (timestamp with time zone) data type normalizes all input values to UTC for internal 8-byte storage; upon query execution, PostgreSQL converts the stored UTC value to the client session's active TimeZone setting.
- The jsonb data type decomposes documents into an indexed binary format that strips extraneous whitespace and duplicate keys, supporting high-speed querying and GIN indexing, unlike textual json which stores raw strings verbatim.
- Modern SQL:2003 GENERATED ALWAYS AS IDENTITY and GENERATED BY DEFAULT AS IDENTITY primary key constructs supersede legacy SERIAL pseudo-types by integrating sequence ownership directly into core table metadata and preventing accidental sequence desynchronization.
3.1 Relational Data Types & Table Design
[!NOTE] Exam Blueprint Focus: The EDB PostgreSQL Associate exam heavily tests your practical knowledge of PostgreSQL data types, storage trade-offs, internal representations, and modern schema design best practices. Expect questions contrasting exact numeric types with floating-point representations, the storage equivalence of
textandvarchar, the precise UTC conversion mechanics oftimestamptz, the structural indexing differences betweenjsonandjsonb, and the behavioral differences between legacySERIALpseudo-types and SQL-standardIDENTITYcolumns.
Designing robust, high-performance database schemas in PostgreSQL requires selecting the most appropriate native data types. PostgreSQL offers a rich set of built-in data types, ranging from fundamental relational primitives to complex structured and semi-structured types. Choosing the right data type ensures domain integrity, minimizes storage footprints on disk and in shared buffers, and enables query optimizer indexing paths.
Numeric Data Types: Integers, Exact Precision & Floating-Point
PostgreSQL categorizes numeric data into three distinct architectural classes: fixed-width integers, arbitrary-precision exact decimals, and variable-precision IEEE floating-point numbers.
1. Fixed-Width Integer Types
Integers store whole numbers without fractional components. PostgreSQL implements three signed two's-complement integer types:
smallint: 2 bytes (16 bits), storing signed values from -32,768 to +32,767. Ideal for small enumerated codes, days of the week, or small geographic units.integer(orint): 4 bytes (32 bits), storing signed values from -2,147,483,648 to +2,147,483,647. This is the standard, general-purpose choice for counters and foreign keys.bigint: 8 bytes (64 bits), storing signed values from -9,223,372,036,854,775,808 to +9,223,372,036,854,775,807. Mandatory for large surrogate keys, high-frequency transaction logs, and tables expected to exceed two billion rows.
CREATE TABLE sensor_readings (
sensor_id smallint,
reading_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device_cycle integer
);
2. Arbitrary-Precision Exact Numbers: numeric and decimal
The numeric type (synonymous with decimal) stores numbers with exact precision up to 131,072 digits before the decimal point and up to 16,383 digits after the decimal point:
- Syntax:
numeric(precision, scale), whereprecisionis the total count of significant decimal digits across the entire number, andscaleis the count of digits in the fractional component to the right of the decimal point. - Exactness: Arithmetic on
numericvalues is implemented in software rather than CPU hardware registers. It produces zero rounding errors, making it mandatory for monetary, banking, accounting, and legal computations. - Storage Overhead: Unlike fixed-width integers,
numericis a variable-length type. PostgreSQL storesnumericas a sequence of base-10,000 digits (2 bytes per digit group), adding a 3-to-8 byte header per value. Consequently, calculations onnumericare significantly slower than integer or floating-point operations.
-- Stores values up to 99999999.99 with exact cent precision
CREATE TABLE invoices (
invoice_id bigint PRIMARY KEY,
total_amount numeric(10, 2) NOT NULL
);
3. Floating-Point Numbers: real and double precision
Floating-point numbers conform to the IEEE 754 standard for binary floating-point arithmetic:
real: 4 bytes (single precision), providing at least 6 decimal digits of precision with a range of approximately $1E-37$ to $1E+37$.double precision(orfloat8): 8 bytes (double precision), providing at least 15 decimal digits of precision with a range of approximately $1E-307$ to $1E+308$.- Inexact Arithmetic: Floating-point values cannot represent all decimal fractions exactly (e.g., $0.1$ has an infinite repeating expansion in binary). Arithmetic operations accumulate inexact rounding artifacts. Use floating-point types exclusively for scientific data, sensor telemetry, and geometric coordinates where calculation speed outweighs penny-perfect decimal accuracy.
| Type | Storage Size | Minimum Value | Maximum Value | Precision Type |
|---|---|---|---|---|
smallint | 2 bytes | -32,768 | 32,767 | Exact integer |
integer | 4 bytes | -2,147,483,648 | 2,147,483,647 | Exact integer |
bigint | 8 bytes | -9.22E+18 | 9.22E+18 | Exact integer |
numeric(p,s) | Variable | Variable | Variable | Exact decimal |
real | 4 bytes | ~1E-37 | ~1E+37 | Inexact (6 decimal digits) |
double precision | 8 bytes | ~1E-307 | ~1E+308 | Inexact (15 decimal digits) |
Character Data Types: char(n), varchar(n), and text
PostgreSQL supports three core character types: character(n) (or char(n)), character varying(n) (or varchar(n)), and text.
Storage Architecture and TOAST
All three character types share the exact same underlying PostgreSQL storage engine structure known as varlena (variable-length array). A varlena structure consists of a 1-byte or 4-byte header describing the length of the string, followed by the raw byte data encoded in the database's character set (such as UTF-8):
- If a text value exceeds approximately 2 KB (one-fourth of an 8 KB page), PostgreSQL automatically compresses the value and, if necessary, moves it out-of-line into a separate table storage area called TOAST (The Oversized-Attribute Storage Technique). The maximum field size for any character value is 1 GB.
Behavioral Differences
char(n): Fixed-length character string. If the stored string contains fewer than $n$ characters, PostgreSQL automatically pads the remainder with trailing space characters. When retrieving or comparingchar(n)strings, these trailing spaces are ignored semantically, but they remain physically stored on disk.varchar(n): Variable-length character string with an explicit maximum length cap of $n$ characters. No trailing spaces are padded.text(or unboundedvarchar): Variable-length character string without any artificial length limit.
[!IMPORTANT] The PostgreSQL Performance Myth: In many relational database systems (such as legacy MySQL or Microsoft SQL Server),
char(n)offers performance advantages over variable-length strings because fixed offsets can be calculated statically. In PostgreSQL, this is completely false. Because all three types use the exact samevarlenastorage mechanism, there is zero performance difference betweentextandvarchar(n).Furthermore,
char(n)is actually the slowest of the three types because PostgreSQL must expend CPU cycles calculating, padding, and stripping trailing whitespace during storage and comparison operations. In modern PostgreSQL schema design, prefertextor unboundedvarcharcombined withCHECKconstraints when length restrictions are necessary.
Temporal Data Types & Timezone Normalization
Handling dates, times, and durations accurately requires understanding how PostgreSQL handles time zones.
Date and Time Primitives
date: 4 bytes, storing calendar dates from 4713 BC to 5874897 AD with 1-day resolution.time [without time zone]: 8 bytes, storing the time of day from 00:00:00.000000 to 24:00:00.000000 with microsecond resolution (up to 6 digits).interval: 16 bytes, storing a span of time (e.g.,'1 year 2 months 3 days 4 hours 30 seconds'). Intervals can be added to or subtracted from dates and timestamps.
timestamp vs. timestamptz
PostgreSQL provides two primary timestamp variants:
timestamp without time zone(shorthandtimestamp): 8 bytes, storing date and time (year, month, day, hour, minute, second, microsecond). It does not know or care about time zones. If you insert'2026-09-06 14:30:00', it stores and outputs exactly that value, regardless of the client session's timezone setting.timestamp with time zone(shorthandtimestamptz): 8 bytes, storing date and time normalized to UTC.
How timestamptz Works Internally
A frequent source of confusion on certification exams is how timestamptz stores data:
- PostgreSQL DOES NOT store the client's timezone offset in the column!
- When a client transmits a timestamp string with an offset (e.g.,
'2026-09-06 14:30:00-04'), PostgreSQL immediately converts the timestamp to Coordinated Universal Time (UTC) and stores it as an 8-byte integer representing microseconds since January 1, 2000, UTC. - When a client queries the column, PostgreSQL reads the UTC value and converts it into the timezone specified by the client's active session parameter
TimeZone(e.g.,SET TimeZone = 'America/New_York';orSET TimeZone = 'Europe/London';).
-- Session 1: UTC client
SET TimeZone = 'UTC';
CREATE TABLE event_log (event_time timestamptz);
INSERT INTO event_log VALUES ('2026-09-06 12:00:00+00');
-- Session 2: New York client (-04:00 in daylight saving)
SET TimeZone = 'America/New_York';
SELECT event_time FROM event_log;
-- Output: '2026-09-06 08:00:00-04'
Boolean, UUID & Structured Types
The boolean Type
PostgreSQL implements a true 1-byte boolean type supporting three-valued logic (TRUE, FALSE, and NULL / unknown). Accepted input literals include:
- True literals:
't','true','y','yes','1',TRUE - False literals:
'f','false','n','no','0',FALSE
Universally Unique Identifier (uuid)
The uuid type occupies 16 bytes (128 bits), conforming to RFC 4122. It provides much greater collision resistance and distribution than 4-byte integers when generating keys in distributed systems. Since PostgreSQL 13, the cryptographically secure random UUID generator function gen_random_uuid() is built into the core engine without requiring external extensions:
CREATE TABLE distributed_accounts (
account_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email text NOT NULL UNIQUE
);
Semistructured Documents: json vs. jsonb
PostgreSQL features world-class native JSON support through two data types:
json(Textual JSON): Stores an exact verbatim copy of the input text, including extraneous whitespace, indentation, and key ordering. It also preserves duplicate object keys. Every time a query reads a field from ajsoncolumn, PostgreSQL must completely re-parse the text string.jsonb(Decomposed Binary JSON): Parses the JSON text upon input and stores it in a decomposed binary format. Extraneous whitespace is stripped, object keys are sorted and deduplicated (if duplicate keys are provided, only the last value is retained), and numbers are converted to binarynumericrepresentations.
Indexing JSONB with GIN
While jsonb has a slightly higher ingestion cost due to binary conversion overhead, it is vastly superior for querying. Crucially, jsonb supports Generalized Inverted Indexes (GIN), enabling sub-millisecond document lookups:
CREATE TABLE customer_profiles (
user_id bigint PRIMARY KEY,
metadata jsonb NOT NULL
);
-- Create GIN index on entire JSONB document
CREATE INDEX idx_profiles_metadata ON customer_profiles USING GIN (metadata);
-- Fast query using JSON containment operator (@>)
SELECT * FROM customer_profiles WHERE metadata @> '{"tier": "enterprise", "active": true}';
-- Path-specific operators:
-- '->' extracts JSON object field (returns jsonb)
-- '->>' extracts JSON object field as raw text
SELECT metadata->'tier' AS json_val, metadata->>'tier' AS text_val FROM customer_profiles;
Native Arrays
PostgreSQL allows columns to be defined as multidimensional arrays of any valid base data type, such as integer[] or text[]:
CREATE TABLE articles (
article_id bigint PRIMARY KEY,
tags text[] DEFAULT '{}'
);
-- Array constructor and literals
INSERT INTO articles VALUES (1, ARRAY['postgresql', 'dba', 'sql']);
INSERT INTO articles VALUES (2, '{"cloud", "performance"}');
-- Querying array elements (PostgreSQL uses 1-based indexing by default!)
SELECT tags[1] FROM articles WHERE article_id = 1; -- Returns 'postgresql'
-- Array search operators
SELECT * FROM articles WHERE 'dba' = ANY(tags);
SELECT unnest(tags) FROM articles WHERE article_id = 1; -- Expands array to relational rows
Primary Key Generation: SERIAL vs. Modern IDENTITY
Surrogate primary keys require monotonic sequence generation. Historically, PostgreSQL applications relied on SERIAL pseudo-types. Modern applications use the SQL:2003 standard IDENTITY column syntax.
+-----------------------------------------------------------------------------+
| SERIAL Pseudo-Type Architecture (Legacy) |
| CREATE TABLE tbl (id SERIAL PRIMARY KEY); |
| ├── Creates independent sequence: tbl_id_seq |
| ├── Sets column default: nextval('tbl_id_seq'::regclass) |
| └── Sets sequence ownership: tbl_id_seq OWNED BY tbl.id |
| * Flaw: Permits silent manual overrides; sequence easily falls out of sync!|
+-----------------------------------------------------------------------------+
VS
+-----------------------------------------------------------------------------+
| GENERATED AS IDENTITY Architecture (SQL Standard) |
| CREATE TABLE tbl (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY); |
| ├── Integrates sequence directly into table schema metadata (pg_class) |
| ├── Enforces strict system generation; blocks manual user inserts |
| └── Permits manual inserts ONLY with OVERRIDING SYSTEM VALUE |
+-----------------------------------------------------------------------------+
1. Legacy SERIAL Pseudo-Types
The SERIAL family (smallserial [2 bytes], serial [4 bytes], bigserial [8 bytes]) is not a true data type. It is syntactic sugar that causes PostgreSQL to execute three separate actions under the hood:
- Creates an independent sequence object named
<tablename>_<colname>_seq. - Sets the column data type to
integer(orsmallint/bigint). - Assigns the column default expression to
nextval('<tablename>_<colname>_seq'::regclass). - Marks the sequence as
OWNED BYthe table column so that dropping the table or column drops the sequence.
Weaknesses of SERIAL:
- Users can bypass the sequence by explicitly specifying a value in an
INSERTstatement (INSERT INTO tbl (id) VALUES (42)). PostgreSQL accepts this without warning. - Subsequent sequence calls (
nextval()) will eventually generate42, resulting in a duplicate key collision violation (ERROR: duplicate key value violates unique constraint).
2. SQL-Standard IDENTITY Columns (PostgreSQL 10+)
Modern PostgreSQL supports the SQL standard IDENTITY specification, which binds sequence management directly into column metadata:
GENERATED ALWAYS AS IDENTITY: PostgreSQL guarantees that the sequence generates the value. If an application attempts to executeINSERT INTO tbl (id, ...) VALUES (10, ...), PostgreSQL raises an immediate error:ERROR: cannot insert a non-DEFAULT value into column "id"To override this protection, the user must explicitly declareOVERRIDING SYSTEM VALUE.GENERATED BY DEFAULT AS IDENTITY: PostgreSQL generates sequence values when the column is omitted or defaulted, but gracefully accepts user-supplied values if explicitly provided.
-- Modern best practice: standard identity column
CREATE TABLE order_records (
order_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id bigint NOT NULL,
placed_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
-- Attempting to manually insert an ID fails under GENERATED ALWAYS:
-- INSERT INTO order_records (order_id, customer_id) VALUES (999, 101); -- ERROR!
-- Successful manual override using explicit clause:
INSERT INTO order_records (order_id, customer_id)
OVERRIDING SYSTEM VALUE
VALUES (999, 101);
An administrator inserts the timestamp string '2026-10-15 14:00:00-04' into a column defined as timestamptz. How does PostgreSQL physically store this timestamp on disk, and how is it rendered when queried by a client whose session parameter is set to SET TimeZone = 'UTC'?
A development team needs to store large JSON documents containing nested metadata. They require high-speed search queries using containment filters and plan to build indexes on document attributes. What are the key architectural distinctions between json and jsonb that should guide their selection?
Consider a table defined as: CREATE TABLE accounts (account_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text);. An application executes: INSERT INTO accounts (account_id, name) VALUES (500, 'Acme Corp');. What is the result of this statement in PostgreSQL?