Postgres by Example: Numeric Types
June 22, 2026 · View on GitHub
PostgreSQL offers three families of numeric types: integers, exact decimals, and floating-point. Each has its place. Integers are fast and compact for counters and IDs. numeric (also spelled decimal) gives you exact arithmetic — essential for money. Floats (real, double precision) are fast and small but can produce tiny rounding errors and should not be used for currency.
What you'll learn:
- Integer family:
smallint,integer,bigint - Exact decimals:
numeric(p, s)and unconstrainednumeric - Floating-point:
real,double precision - How precision and scale work
- When each type is the right choice
-- Integer family
SELECT 1::smallint AS s, 1::integer AS i, 1000000::bigint AS b;
-- Range limits (smallint -32768..32767, integer -2.1B..2.1B, bigint very large)
SELECT pg_typeof(1) AS literal_int_type; -- 'integer' by default
-- Exact decimal: numeric(precision, scale)
-- precision = total digits, scale = digits after the decimal point
SELECT 3.14::numeric(4, 2) AS pi, 12345.67::numeric(7, 2) AS money;
-- Unconstrained numeric stores any number with full precision
SELECT '0.1'::numeric + '0.2'::numeric AS exact; -- 0.3, exactly
-- Floats are approximate but fast
SELECT 0.1::double precision + 0.2::double precision AS approximate; -- 0.30000000000000004
-- Useful generators
SELECT generate_series(1, 5) AS n;
integer is your default integer type and is what 1 parses as. Switch to bigint when you might exceed two billion (rows in a large table, milliseconds since epoch, etc.). smallint is rarely worth it — the storage savings are tiny and you risk silent overflows.
numeric(p, s) enforces up to p total digits with s of them after the decimal point. numeric(10, 2) stores values from -99,999,999.99 to 99,999,999.99. Unconstrained numeric (no p, s) accepts any number with full precision — slower than fixed integers, but exact. Use it for currency, lab measurements, anything where rounding errors are unacceptable.
real (4 bytes, ~6 significant digits) and double precision (8 bytes, ~15 significant digits) are IEEE 754 floats. They are fast and small, but 0.1 + 0.2 is not exactly 0.3. Use them for physical measurements, ML features, statistical aggregations where you can tolerate the imprecision.
To run:
$ psql -f source/numeric-types.sql postgres
s | i | b
---+---+---------
1 | 1 | 1000000
(1 row)
...
exact
-------
0.3
(1 row)
approximate
------------------
0.30000000000000004
(1 row)
Common pitfalls:
- Storing money in
double precision— eventually the cent-rounding catches you. Usenumeric(p, 2)(ornumericwithout bounds) and let the database handle exact arithmetic. - Choosing
integerfor an ID column on a large table — you can hit the 2.1-billion limit. Usebigintby default for primary keys you expect to grow. - Casting strings with non-numeric characters:
'\$5'::numericerrors. Strip first, then cast. - Dividing integers and being surprised:
7 / 2is3, not3.5. Cast:7::numeric / 2.
Tip: pg_typeof(expression) returns the inferred type — useful when you are uncertain. For example, SELECT pg_typeof(1 / 2); returns integer, while SELECT pg_typeof(1.0 / 2); returns numeric.
Try it: Create a small table with an integer id and a numeric(10, 2) price column, then insert and inspect: CREATE TEMP TABLE p(id integer, price numeric(10,2)); INSERT INTO p VALUES (1, 9.99), (2, 1234.5); SELECT * FROM p;. Then watch what happens with SELECT 0.1::real + 0.2::real;.
Source: numeric-types.sql
Next: Text Types
Home: Postgres by Example