Best PostgreSQL Data Types for Performance
Level: intermediate · ~14 min read · Intent: informational
Audience: backend developers, database engineers, technical teams
Prerequisites
- basic familiarity with PostgreSQL
Key takeaways
- For most schemas, the fastest default choices are simpler and narrower than many teams expect: integer for common whole numbers, text for most strings, date for date-only values, and timestamptz for real-world timestamps.
- The biggest PostgreSQL data type performance mistakes usually come from overusing numeric, char(n), json, oversized identifiers, and wide row designs that increase storage and index cost.
FAQ
- Is text or varchar faster in PostgreSQL?
- In PostgreSQL, there is no performance advantage for varchar over text. Use text by default, and only use varchar(n) when you specifically want a length constraint as a business rule.
- Is numeric slower than integer in PostgreSQL?
- Yes. PostgreSQL documents that numeric calculations are much slower than integer and floating-point calculations, so numeric should be reserved for cases where exact precision is required.
- Should I use json or jsonb for performance?
- Use jsonb in most application schemas when you need to query, index, or process JSON data. Use json only when preserving the exact original input text matters more than query speed.
- Should I use timestamp or timestamptz?
- For most production applications, timestamptz is the safer default for event times such as created_at, updated_at, and scheduled jobs because it stores time in a time zone-aware form and converts output based on the session time zone.
Choosing the right PostgreSQL data type is one of the easiest performance wins in database design.
It affects:
- row width
- index size
- memory usage
- sort cost
- comparison cost
- and how much work PostgreSQL has to do on every query
That is why data types are not just a schema detail. They are part of the performance model.
A bad type choice does not always hurt on day one. But once the table grows, the cost starts showing up in:
- slower scans
- bigger indexes
- heavier cache pressure
- more TOAST activity
- and application code that becomes harder to optimize cleanly
The good news is that the best choices are usually not exotic. They are usually the simplest types that still match the data correctly.
Quick Answer
If you want the shortest practical answer, these are the best default choices for many PostgreSQL applications:
| Use Case | Best Default Choice | Why |
|---|---|---|
| Common whole numbers | integer |
Best balance of range, storage, and performance |
| Very large counters or IDs | bigint |
Use only when integer range is not enough |
| Exact money or precision math | numeric |
Exact, but slower, so use only when precision matters |
| Most strings | text |
Flexible and no performance disadvantage versus varchar |
| Strings with a real business length rule | varchar(n) |
Good when the length limit is semantically meaningful |
| Fixed-width padded strings | char(n) |
Usually avoid for performance-sensitive schemas |
| Date only | date |
Smaller and semantically correct |
| Event timestamps | timestamptz |
Best default for production systems across time zones |
| Queried JSON documents | jsonb |
Better for querying and indexing |
| Exact original JSON text preservation | json |
Only when exact input preservation matters |
| Distributed unique IDs | uuid |
Strong fit for multi-writer and distributed systems |
| Binary blobs | bytea |
Correct for binary data, but keep an eye on row width |
That table is not the whole story, but it covers most real application decisions.
What Makes a Data Type “Fast” in PostgreSQL?
A faster data type is not only one that compares quickly.
It is usually one that also helps with:
- narrower rows
- smaller indexes
- cheaper sorts
- less memory pressure
- better cache density
- and fewer surprises in query planning
In practice, good type selection is about matching the type closely to the data:
- not wider than needed
- not more precise than needed
- not more flexible than needed
- and not semantically misleading
That last one matters too.
A type that “works” but says the wrong thing about the data often leads to performance problems later because the schema becomes harder to reason about.
1. Best Numeric Types for Performance
Numeric columns are where many teams overspend performance.
Use integer by default for common whole numbers
For many counters, status codes, quantities, and internal foreign keys, integer is the best default.
PostgreSQL’s own documentation describes integer as the common choice because it offers the best balance of range, storage size, and performance. smallint uses 2 bytes, integer uses 4 bytes, and bigint uses 8 bytes. :contentReference[oaicite:1]{index=1}
That means a good default rule is:
- choose
integerfirst - use
smallintonly when space genuinely matters - use
bigintonly whenintegerrange is not enough
When to use smallint
Use smallint for small bounded values like:
- tiny status codes
- fixed rating values
- very small counters
- compact lookup values
But do not force it everywhere just because it is smaller.
The savings are real, but the complexity is not always worth it unless the column is very common and very large in aggregate.
When to use bigint
Use bigint when the value can realistically exceed 32-bit integer range.
Common examples:
- large event tables
- high-volume primary keys
- very large counters
- analytics pipelines
- globally growing identifiers
Do not choose bigint by habit for every column.
It uses twice the storage of integer, which also affects index size and row width.
2. Avoid numeric Unless You Truly Need Exact Precision
This is one of the biggest data-type performance mistakes in PostgreSQL schemas.
numeric is excellent when you need exact arithmetic, especially for things like money, accounting rules, and quantities where rounding error is unacceptable. But PostgreSQL explicitly documents that calculations on numeric values are much slower than calculations on integer or floating-point types. :contentReference[oaicite:2]{index=2}
That leads to a very practical rule:
Use numeric for:
- money and accounting values
- precise decimal math
- quantities where exactness is legally or financially important
Avoid numeric for:
- IDs
- counters
- approximate measurements
- percentages that do not need exact decimal precision
- columns that are frequently aggregated but do not require exact decimal arithmetic
Better alternatives when exactness is not required
Use:
integerfor whole numbersbigintfor large whole numbersrealordouble precisiononly when inexact floating-point math is acceptable
For money-like application logic, many teams also choose:
- integer cents instead of
- wide decimal arithmetic everywhere
That can simplify performance-sensitive aggregation paths significantly.
3. Best String Types: Use text by Default
This is one of the most misunderstood PostgreSQL topics.
A lot of developers coming from other systems assume:
varchar(n)is faster thantext- or
char(n)is more efficient because it is fixed-width
In PostgreSQL, that is not how it works.
The docs state that there is no performance difference among text, varchar, and char(n) except for extra storage when using the blank-padded fixed-length type, and that character(n) is usually the slowest of the three because of its additional storage costs. :contentReference[oaicite:3]{index=3}
Best default choice: text
Use text for most strings:
- names
- titles
- descriptions
- emails
- slugs
- comments
- URLs
- external identifiers
- free-form application strings
This is usually the best practical default because it is flexible and does not cost you a performance penalty versus varchar.
When to use varchar(n)
Use varchar(n) when the length limit is a real business rule, not as a fake performance trick.
Good examples:
- country codes with a real max length rule
- usernames with a product-defined length cap
- short titles with a strict form constraint
In other words: use it for validation semantics, not imaginary speed.
When to avoid char(n)
Avoid char(n) in performance-sensitive app schemas unless you truly need blank-padded fixed-width behavior.
It is usually the wrong choice for:
- API data
- app strings
- emails
- slugs
- codes that are not truly fixed-width by semantics
4. Date and Time Types: Be Precise About Meaning
Date and time types cause both performance and correctness problems when teams choose them casually.
PostgreSQL documents these storage sizes:
date: 4 bytestimestamp: 8 bytestimestamptz: 8 bytestimetz: 12 bytes :contentReference[oaicite:4]{index=4}
That makes the decision easier than many developers expect.
Use date when you only need a calendar date
Examples:
- birthdays
- contract start dates
- invoice dates
- accounting periods
- due dates where time-of-day does not matter
If you only need the date, do not use a timestamp.
A date column is smaller and expresses the meaning more clearly.
Use timestamptz for most event times
This is usually the safest default for:
created_atupdated_at- scheduled jobs
- audit logs
- user actions
- published timestamps
- anything crossing time zones or systems
PostgreSQL stores timestamp with time zone values in UTC internally and converts them for output based on the current session time zone. :contentReference[oaicite:5]{index=5}
That makes timestamptz the better default for real applications.
Use timestamp only when you deliberately want time without zone meaning
Examples:
- local wall-clock events where time zone is intentionally absent
- historical imports where you want to preserve naive timestamps
- domain models where “local timestamp” is explicitly correct
If you are unsure, timestamptz is usually the safer production choice.
Usually avoid timetz
A standalone time-with-time-zone column is not a common need in most application schemas.
It is larger, rarer, and easier to misuse than:
timedate- or
timestamptz
5. JSON: Prefer jsonb for Performance
This is another area where PostgreSQL is very explicit.
The docs explain that:
jsonstores an exact copy of the input text- processing functions must reparse it each time
jsonbis stored in a decomposed binary representationjsonbis slightly slower to input but significantly faster to process because no reparsing is needed :contentReference[oaicite:6]{index=6}
They also note that json preserves whitespace, key order, and duplicate keys, while jsonb does not. :contentReference[oaicite:7]{index=7}
Use jsonb for most app schemas
Choose jsonb when you:
- query fields
- filter on JSON content
- use containment operators
- build indexes on JSON fields
- treat the column like semi-structured application data
That is the common performance choice.
Use json only when exact input preservation matters
Choose json only if you specifically need:
- original whitespace
- original key order
- exact textual preservation of the payload
That is much less common in ordinary app design.
Performance rule
If the application needs to search, filter, or index inside the document:
- use
jsonb
If the application only stores the payload and rarely touches it:
jsonmay be acceptable, butjsonbis still usually the simpler long-term default
6. UUID vs Integer Keys
This is less about one universal winner and more about what problem you are solving.
PostgreSQL documents that uuid is a 128-bit type and notes that in distributed systems UUIDs provide a better uniqueness guarantee than sequence generators, which are only unique within a single database. PostgreSQL 18 also provides native support for UUIDv4 and UUIDv7 generation. :contentReference[oaicite:8]{index=8}
Use uuid when you need:
- distributed ID generation
- safer cross-system uniqueness
- IDs generated outside the database
- public IDs that should not expose row counts
- multi-writer or multi-region workflows
Use integer or bigint when you want:
- compact internal surrogate keys
- simpler join keys
- sequence-based IDs
- narrow indexes and straightforward operational patterns
Practical recommendation
A common high-performance pattern is:
bigintprimary key internally- separate public
uuidif the product needs external-safe identifiers
That gives you compact internals without giving up distributed-safe external IDs.
7. Boolean Flags: Use boolean, Not Text
This sounds obvious, but bad schemas still store true/false style values as:
textvarchar"Y"/"N""active"/"inactive"flags- integers with unclear meaning
If the column is genuinely binary, use boolean.
It improves:
- schema clarity
- query readability
- validation correctness
- and avoids needless storage and parsing overhead
Good examples:
is_activeis_deletedemail_verifiedarchived
Do not make the database interpret business state through vague text values when a Boolean is what you really mean.
8. Use bytea for Binary Data, But Watch Row Width
PostgreSQL’s general-purpose binary type is bytea. The more important performance lesson is what happens when rows become wide.
The TOAST system can compress and/or move data out-of-line when row values exceed the TOAST threshold, which is normally about 2 KB. :contentReference[oaicite:9]{index=9}
That means wide variable-length columns such as:
- big text fields
- JSON documents
- binary blobs
- long metadata columns
can push rows into more expensive storage behavior.
Use bytea when:
- binary data truly belongs in the database
- the application benefits from transactional storage of the blob
- the binary object is not huge or unbounded in practice
Be careful when:
- storing large files
- mixing many wide columns into hot tables
- putting blob-heavy data into rows that are frequently updated or frequently scanned
In many application architectures, large media objects are better stored outside the main transactional tables, with PostgreSQL holding:
- metadata
- URLs
- hashes
- sizes
- or file references
9. Do Not Use Wider Types “Just in Case”
This is one of the most common schema-design habits that quietly hurts performance.
Examples:
- using
biginteverywhere even whenintegeris enough - using
numericeverywhere instead of integer where appropriate - using timestamps where dates are enough
- storing structured values as
text - storing everything as JSON just to stay flexible
That kind of design tends to create:
- fatter rows
- larger indexes
- more work for scans
- more memory pressure
- and weaker long-term query patterns
A better rule is:
Choose the narrowest type that correctly models the data
Not the narrowest possible type blindly. The narrowest correct type.
That is the performance sweet spot.
10. Match Data Types to Index Strategy
Data types and index strategy are tightly connected.
For example:
- B-tree indexes work well for equality and range queries on sortable values such as integers, timestamps, and many text patterns with the right operator behavior. :contentReference[oaicite:10]{index=10}
jsonbopens up JSON-specific indexing strategies and operators.- Wider keys make wider indexes.
- Poor type choices can make otherwise reasonable indexes less efficient.
That means data type design is not separate from query design. It is part of it.
A smaller, cleaner column type often improves not only storage, but also the cost profile of:
- sorts
- joins
- filters
- and index scans
Recommended Type Choices by Common Scenario
User table
create table app_user (
id bigint generated always as identity primary key,
public_id uuid not null unique,
email text not null,
display_name text,
is_active boolean not null default true,
created_at timestamptz not null default now()
);
Why this is strong:
- compact internal key
- UUID available for external references
- text for strings
- boolean for flags
- timestamptz for event time
Orders table
create table orders (
id bigint generated always as identity primary key,
customer_id bigint not null,
total_cents bigint not null,
currency_code char(3) not null,
status text not null,
created_at timestamptz not null default now()
);
Why this is strong:
- integer-style money storage if cents are acceptable
- compact keys
- clear event timestamps
Analytics event table
create table analytics_event (
id bigint generated always as identity primary key,
user_id bigint,
event_name text not null,
occurred_at timestamptz not null,
properties jsonb
);
Why this is strong:
- timestamptz for event time
- jsonb for queryable flexible properties
- narrow scalar columns for frequently filtered fields
Common PostgreSQL Data Type Mistakes
Mistake 1: Using numeric for ordinary counts
If the value is a whole-number count, use:
integer- or
bigint
not arbitrary precision decimals.
Mistake 2: Using varchar(n) everywhere
In PostgreSQL, this gives you no built-in speed advantage over text.
Mistake 3: Using char(n) for app strings
This is one of the easiest useless slowdowns to introduce.
Mistake 4: Using json when you really mean jsonb
If the app queries the document, jsonb is usually the right answer.
Mistake 5: Storing date-only values as timestamps
This wastes space and weakens schema clarity.
Mistake 6: Storing flags as strings or numbers
Use boolean when the state is truly binary.
Mistake 7: Building wide hot rows
Too many wide varlena columns in frequently accessed tables can lead to worse cache behavior and more TOAST activity.
Practical Rules You Can Apply Immediately
If you want a short performance checklist, use this:
- Default to
integerfor common whole numbers - Use
bigintonly when the range requires it - Use
numericonly when exact precision is necessary - Default to
textfor strings - Use
varchar(n)only when the limit is a true business rule - Avoid
char(n)unless fixed-width padding is semantically required - Use
datefor date-only values - Use
timestamptzfor most real application timestamps - Use
jsonbinstead ofjsonfor most app-side JSON data - Keep hot tables narrow and avoid casual wide-column design
Conclusion
The best PostgreSQL data types for performance are usually the simplest ones that still model the data correctly.
That is the real lesson.
For most schemas, that means:
integerover wider numeric types unless range requires moretextovervarchar(n)unless you need a length ruletimestamptzfor real event timesdatefor date-only valuesjsonbfor queryable JSON- and careful handling of wide columns that can trigger TOAST behavior
A strong PostgreSQL schema is not about choosing the fanciest type. It is about choosing the narrowest, clearest, most semantically correct type that keeps rows lean and queries predictable.
That is what usually performs best over time.