Managing currency data requires efficient storage, high-precision calculations, and clear reporting – including complex logic for handling tax rates, currency conversions, and varying international formats.
As a robust enterprise database, PostgreSQL offers specialized support for currency data through its money and numeric data types along with powerful localization features.
This comprehensive guide covers everything developers and analysts need to know to leverage PostgreSQL for tax and finance applications revolving around monetary data storage and processing.
Overview: Evaluating PostgreSQL Currency Types
PostgreSQL provides three primary data types to handle money values:
- money – 8 byte fixed precision value upto 2 decimal places
- numeric – Arbitrary precision user-defined decimal
- double precision – Floating point number with 15-17 significant digits
The first two are specifically designed for robust currency calculations, while the latter should generally be avoided due to rounding errors as we will analyze.
But when should each be used? Consider these key capabilities:
| Feature | money | numeric |
|---|---|---|
| Precision | Fixed (2) | User-defined |
| Range | -922 quadrillion to +922 quadrillion | Up to 131072 digits (bounded by precision) |
| Storage size | 8 bytes | Variable based on precision |
| Automatic rounding | Yes (to 2 decimals) | No |
Summary: The money type provides fixed 2-decimal storage that is easy to use but less accurate for values like tax rates. The numeric type supports custom precision and exact values without rounding, but requires formatting considerations.
Now let‘s look at each in more detail with usage guidance.
Storing Currencies Accurately with PostgreSQL money Type
The core advantage of the PostgreSQL money data type is automatic rounding to two decimal places – greatly simplifying application logic:
CREATE TABLE sales (
id SERIAL PRIMARY KEY,
price money
);
INSERT INTO sales (price) VALUES
(129.9955),
(79.995);
Gives:
id | price
----+--------
1 | $129.99
2 | $79.99
This fixed 8-byte precision makes space allocation and performance predictable. Indexes on money columns also operate efficiently for aggregates and reporting.
However for certain applications, two decimal places are inadequate such as:
- Foreign exchange – Currencies are generally denoted beyond 2 decimals
- Banking systems – Interest calculations require high precision
- Accounting software – Tax rates or revenues may have >= 3 decimal precision
Rounding in monetary calculations also means compounding inaccuracies over large datasets and complex application logic.
Overall the PostgreSQL money type gives a convenient built-in currency formatting – but lacks flexibility for precision and growth.
Achieving High Precision with PostgreSQL numeric Type
For currency use cases that demand exact unrounded values and custom precision, PostgreSQL provides the versatile numeric type.
Consider tax rate storage that requires 4 decimal places:
CREATE TABLE tax_rates (
id SERIAL PRIMARY KEY,
name text,
rate numeric(5,4)
);
INSERT INTO tax_rates (name, rate) VALUES
(‘GST‘, 0.1800),
(‘VAT‘, 0.1575);
This stores the full values without rounding to flexibility represent precise percentages:
id | name | rate
----+------+------------
1 | GST | 0.1800
2 | VAT | 0.1575
The syntax allows controlling both precision and scale for exact monetary data storage needs:
numeric(precision, scale)
-- Examples:
numeric(3,2) -> -99.99 .. 99.99
numeric(10,4) -> +/- 9,999,999.9999
With up to 1000 digits of precision possible, numeric provides essentially unbounded flexibility compared to money – at the cost of handling rounding and formatting yourself.
Optimizing Performance of Numeric Types
A downside of the numeric‘s variable length storage is less predictable disk usage growth and index performance. However by planning precision carefully, storage costs can be optimized:
Use smallest fit-for-purpose precision
For example currency conversion ratios needing 5 decimals:
Good: numeric(6,5) -> 0.10000 .. 9.99999
Unnecessary: numeric(10,5) -> more storage bytes
Control range based on real needs – avoiding unneeded precision digits.
Prefer fixed precision
Variable precision numerics inhibit query optimizations and have slower comparisons:
-- Constrained single precision
numeric(8,2)
-- Variable precision
numeric
So set explicit precision for numeric columns whenever possible based on data patterns.
Overall while not as lean as money, robust schema design allows maximizing numeric performance – crucial for large production datasets.
Formatting Decimals as Monetary Values
A tradeoff of precise numeric storage is lack of automatic currency formatting – so appropriate rounding and decoration (symbols, grouping separators etc) must be handled in presentation layers.
Let‘s fetch raw tax rates and format for display:
SELECT
name,
round(rate::numeric, 2) || ‘%‘ AS rate
FROM tax_rates;
Gives output formatted as percentages:
name | rate
------+----------
GST | 18.00%
VAT | 15.75%
Similar methods can format numerics as currencies using concatenation and to_char():
SELECT
to_char(price, ‘L999G999D99‘) AS price
FROM order_values;
This leverages localization rules to handle currency symbols, separators etc. based on region – keeping application code generic.
The downside is complex handling for rounding, conversions and presentations – so choose carefully between fixed money and flexible numeric.
Why Floating Point Types Fall Short
PostgreSQL provides two floating point types that seem appropriate for currencies:
- real – 6 decimal digit precision
- double precision – 15-17 decimal precision
However as investigated in detail by several sources, floating point types should never be used for financial data!
The reason comes down to binary representation which approximates some decimal values:
CREATE TABLE data (
id SERIAL PRIMARY KEY,
value REAL
);
INSERT INTO data (value) VALUES (3.3);
SELECT * FROM data;
Gives inaccurate value:
id | value
----+---------------------------
1 | 3.2999999523162842
Now imagine this approximation multiplying over thousands of records in complex aggregations! Significant monetary losses are inevitable.
Thus for all finance use cases – always prefer numeric or money types that handle exact decimal representations. Floating point imprecision has no place processing currencies or sensitive data.
Benchmarking Monetary Data Type Performance
To pick the optimal PostgeSQL data type for money, let‘s benchmark insert and retrieval throughput:
CREATE TABLE money_test (
id serial PRIMARY KEY,
cash_amount money
);
CREATE TABLE numeric_test (
id serial PRIMARY KEY,
cash_amount numeric(10,2)
);
\timing on
INSERT INTO money_test (cash_amount)
SELECT random() * 100000 :: money
FROM generate_series(1, 1000000) s(i);
INSERT INTO numeric_test (cash_amount)
SELECT random() * 100000 :: numeric(10,2)
FROM generate_series(1, 1000000) s(i);
Gives the following performance for a 1 million row bulk insert on commodity cloud hardware:
type | insert | 100 reads
------------------+---------+-----------
money | 17s | 1.4s
numeric(10, 2) | 27s | 1.7s
We see a significant INSERT throughput advantage for money type due to the fixed size representation. But additional precision comes at a 1.6x insertion cost.
Retrieval speeds remain comparable between the two. And money trailing in features – so choose based on requirements between performance vs precision.
Comparing PostgreSQL Money Handling to Other Databases
Let‘s briefly contrast Postgres‘ monetary abilities to some alternatives:
| RDBMS | Money Type | High Precision Type | Localization |
|---|---|---|---|
| PostgreSQL | money | numeric | LC settings |
| SQL Server | money/smallmoney | decimal | @@LANGID |
| MySQL | decimal | decimal | locale |
| Oracle | – | NUMBER | NLS settings |
- PostgreSQL requires more manual formatting than SQL Server money but allows better precision control.
- All provide high-precision decimal storage – but attention to precision constraints needed.
- Localization settings handle regional formats generically in most.
Overall PostgreSQL matches or exceeds abilities for robust currency data – just needing more vigilance to constraints and presentation formatting. The extensive locale support is powerful for international apps.
Real-World Examples Using Monetary Data Types
Let‘s look at some practical use cases leveraging PostgreSQL‘s currency abilities for caching, calculations and analytics.
1. Caching Exchange Rates with High Precision
Financial applications like e-commerce may integrate with external services to retrieve latest currency exchange rates. Hitting APIs on every conversion would be inefficient – so rates can be cached in PostgreSQL for fast lookups:
CREATE TABLE exchange_rates (
currency_code char(3) PRIMARY KEY,
rate numeric(18,10) NOT NULL
);
INSERT INTO exchange_rates (currency_code, rate) VALUES
(‘USD‘, 1.0),
(‘EUR‘, 0.9481247154);
-- Refresh nightly from API using high precision
Now code can query rates from this table directly instead of remote API calls for conversion needs – cutting latency overheads.
2. Tax Calculation UDFs for Invoices
PostgreSQL user-defined functions provide powerful ways to encapsulate business logic like tax rates in reusable code:
CREATE FUNCTION add_gst(
amount numeric
)
RETURNS numeric AS $$
BEGIN
RETURN round(amount * 0.18, 2); -- Australian GST
END;
$$ LANGUAGE plpgsql;
SELECT add_gst(100);
Gives:
add_gst
------------------------
18.00
Invoicing apps can apply taxes on-the-fly via such UDFs before persists or retrievals – keeping precision high.
3. Analyzing Revenue Time-Series
The high numeric precision along with date/time handling allows detailed analytic insights on financial data like revenue over time:
SELECT
date_trunc(‘quarter‘, payment_date)::date AS quarter,
SUM(amount) AS revenue
FROM payments
GROUP BY quarter
ORDER BY quarter;
Thus PostgreSQL fits right into complex reporting needs on top of currency database storage – no need for separate analytical systems.
There are many more examples like handling tenant billing, price experimentation etc where money management directly benefits from PostgreSQL‘s versatile data types.
Conclusion: Best Practices for Monetary Data with PostgreSQL
PostgreSQL provides two purpose-built data types in money and numeric to handle currency storage and calculations effectively:
- Money – Fixed 8-byte precision to two decimals. Use where automatic rounding is convenient.
- Numeric – Arbitrary high precision values without rounding. Better for tax rates or high accuracy needs despite more complex handling.
Additional guidelines when modeling monetary data:
- Store currency codes and symbols distinctly from raw amounts
- Leverage localization rules for regional formats and sort order
- Avoid floating point data types due to approximation issues
- Benchmark and tune numeric constraints carefully
- Encapsulate business logic into parameterized SQL functions
Following these best practices will ensure your finance and accounting oriented applications benefit fully from PostgreSQL‘s rock-solid desktop-to-enterprise capabilities.
The extensive flexibility in precision, performance and internationalization options make PostgreSQL a top choice for managing wide-ranging currency needs – outclassing niche financial databases.


