Apache Iceberg is increasingly used as a table format for large analytical datasets stored in object storage. It provides features such as schema evolution, partition management, snapshots, and reliable table metadata without requiring the data to live inside a traditional database.
That creates an interesting requirement for application developers: how can a PostgreSQL application work with Iceberg data without first copying the data into PostgreSQL?
The answer depends on the architecture and the query engine used to access the Iceberg tables.
This article explains the practical considerations when a PostgreSQL-based application needs to query Apache Iceberg data directly or expose Iceberg data through a PostgreSQL-compatible interface.
What Is Apache Iceberg?
Apache Iceberg is an open table format designed for large analytical datasets.
Instead of storing table information only inside a database catalog, Iceberg maintains table metadata and snapshots alongside data files in object storage.
A simplified layout might look like this:
Object Storage
│
├── data/
│ ├── part-0001.parquet
│ ├── part-0002.parquet
│ └── part-0003.parquet
│
└── metadata/
├── metadata.json
├── snapshots/
└── manifests/
The actual structure varies by catalog and implementation, but the important concept is that the table is represented through metadata that describes the underlying data files.
Iceberg commonly works with columnar formats such as Parquet.
Why Would a PostgreSQL Application Query Iceberg?
PostgreSQL is excellent for transactional workloads and many application-facing queries.
Iceberg is designed for large-scale analytical workloads.
A system may therefore have both:
Application Data
|
v
PostgreSQL
|
+---- Transactional queries
|
v
Analytics Platform
|
v
Iceberg Tables
The application may need access to analytical information without copying millions or billions of rows into PostgreSQL.
For example, an application could need:
- Customer analytics
- Historical transactions
- Product usage data
- Audit information
- Event data
- Large reporting datasets
Copying the complete dataset into PostgreSQL just to run an occasional analytical query can increase storage, ETL, and maintenance costs.
PostgreSQL and Iceberg Solve Different Problems
It is useful to keep their roles separate.
| Capability |
PostgreSQL |
Apache Iceberg |
| Primary purpose |
Database |
Table format |
| Typical workload |
OLTP and mixed workloads |
Analytics |
| Storage |
Database-managed storage |
Object storage |
| Transactions |
Database transactions |
Snapshot-based table operations |
| Schema evolution |
Supported |
Strong support |
| Large analytical datasets |
Possible |
Designed for this use case |
| Query engine |
PostgreSQL |
Requires a compatible query engine |
| Data location |
PostgreSQL storage |
Commonly object storage |
The key distinction is that Iceberg is not itself a database engine.
An Iceberg table needs a query engine capable of interpreting the Iceberg metadata and reading the underlying data files.
The Query Architecture
A PostgreSQL application should not assume that PostgreSQL can simply open an Iceberg table like a local PostgreSQL table.
A common architecture is:
PostgreSQL Application
|
v
PostgreSQL-Compatible Query Interface
|
v
Query Engine
|
v
Iceberg Catalog
|
v
Object Storage
|
v
Parquet Data
The query engine handles the Iceberg metadata and data files.
The PostgreSQL-compatible layer provides an interface that applications can use when PostgreSQL protocol compatibility is available.
Why a Query Engine Is Required
Consider an Iceberg table:
sales_2026
The table might contain:
customer_id
order_id
order_date
amount
region
Those rows may physically exist as Parquet files in object storage.
A PostgreSQL client does not understand Iceberg metadata by itself.
The query engine needs to determine:
- Which Iceberg snapshot is current.
- Which manifest files describe the table.
- Which data files contain relevant records.
- Which files can be skipped.
- How to execute the SQL query.
- How to return the result through the PostgreSQL interface.
That is why the query engine is an important part of the architecture.
PostgreSQL-Compatible Access
If the selected query engine provides PostgreSQL wire-protocol compatibility, an application can use familiar PostgreSQL client libraries.
For example, a .NET application can use Npgsql:
await using var connection =
new NpgsqlConnection(connectionString);
await connection.OpenAsync();
await using var command =
new NpgsqlCommand(
"""
SELECT customer_id, SUM(amount)
FROM sales_2026
GROUP BY customer_id
""",
connection);
await using var reader =
await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var customerId = reader.GetInt64(0);
var total = reader.GetDecimal(1);
Console.WriteLine(
$"{customerId}: {total}");
}
The important detail is that the PostgreSQL client is talking to a PostgreSQL-compatible SQL endpoint.
It does not mean PostgreSQL itself is reading Iceberg metadata.
Keep the Application Layer Simple
One advantage of using a PostgreSQL-compatible interface is that the application can keep its database access layer familiar.
For example:
ASP.NET Core
|
v
Npgsql
|
v
SQL Endpoint
|
v
Iceberg Query Engine
The application does not need to understand:
- Iceberg manifests
- Snapshot metadata
- Parquet file locations
- Partition specifications
- Object storage paths
Those responsibilities stay in the data platform.
Querying Iceberg From a .NET Application
A repository pattern can isolate the query implementation.
public sealed class SalesRepository
{
private readonly NpgsqlDataSource _dataSource;
public SalesRepository(NpgsqlDataSource dataSource)
{
_dataSource = dataSource;
}
public async Task<IReadOnlyList<SalesSummary>>
GetSalesAsync(
DateTime startDate,
DateTime endDate,
CancellationToken cancellationToken)
{
const string sql = """
SELECT region,
SUM(amount) AS total_amount
FROM sales_2026
WHERE order_date >= $1
AND order_date < $2
GROUP BY region
ORDER BY total_amount DESC;
""";
await using var command =
_dataSource.CreateCommand(sql);
command.Parameters.AddWithValue(startDate);
command.Parameters.AddWithValue(endDate);
var results = new List<SalesSummary>();
await using var reader =
await command.ExecuteReaderAsync(
cancellationToken);
while (await reader.ReadAsync(
cancellationToken))
{
results.Add(
new SalesSummary(
reader.GetString(0),
reader.GetDecimal(1)));
}
return results;
}
}
The application code remains similar to normal PostgreSQL data access.
The difference is where the query is executed.
Use Parameterized Queries
Do not construct SQL by concatenating user input.
Avoid:
var sql =
$"SELECT * FROM sales WHERE region = '{region}'";
Use parameters:
const string sql = """
SELECT *
FROM sales
WHERE region = $1
""";
Then:
command.Parameters.AddWithValue(region);
This keeps the application safer and also makes query handling more predictable.
Query Only the Data You Need
Iceberg is commonly used with very large datasets.
Avoid:
SELECT *
FROM sales_2026;
when the application needs only a few columns.
Prefer:
SELECT
customer_id,
order_date,
amount
FROM sales_2026
WHERE region = 'APAC';
Column pruning can reduce the amount of data that needs to be read.
This becomes particularly important when the underlying files are large.
Filtering Is Important
Analytical tables can contain huge amounts of data.
A query such as:
SELECT COUNT(*)
FROM sales_2026;
may require substantially more work than a selective query.
Prefer filters that match the application's actual requirement:
SELECT
SUM(amount)
FROM sales_2026
WHERE order_date >= DATE '2026-10-01'
AND order_date < DATE '2026-11-01';
Good filtering allows the query engine to eliminate unnecessary work where the underlying table organization and metadata support it.
Partitioning and Query Performance
Iceberg supports partition specifications that can help analytical queries avoid unnecessary data.
For example, a table may logically organize data by:
year(order_date)
or another appropriate partition transformation.
The application should not need to know the physical file layout.
Instead, it should write queries around meaningful business columns.
For example:
SELECT
region,
SUM(amount)
FROM sales_2026
WHERE order_date >= DATE '2026-01-01'
AND order_date < DATE '2026-02-01'
GROUP BY region;
The query engine can use the Iceberg metadata and partition information when planning the query.
Do Not Treat Iceberg Like a PostgreSQL Table
One of the most common architectural mistakes is assuming that all PostgreSQL query patterns will have the same performance characteristics.
A PostgreSQL table might be backed by indexes designed around application lookups.
An Iceberg table is typically optimized for analytical scans and data skipping.
Therefore, queries should be designed around analytical access patterns.
For example, an application query such as:
SELECT *
FROM sales_2026
WHERE customer_id = 123456;
may not have the same performance characteristics as querying an indexed PostgreSQL table.
If this becomes a high-frequency request, PostgreSQL may still be the better serving layer.
Keep Transactional Data in PostgreSQL
Suppose an application needs to update an order:
UPDATE orders
SET status = 'Completed'
WHERE order_id = 1001;
This is a normal transactional workload.
PostgreSQL is a better fit for this type of operation.
The architecture can instead use:
Application
|
+---- PostgreSQL
| |
| +-- Orders
| +-- Users
| +-- Payments
|
+---- Analytics Query Engine
|
+-- Iceberg
+-- Historical Events
+-- Large Analytical Tables
This separation prevents analytical workloads from competing directly with transactional workloads.
Use PostgreSQL for Serving and Iceberg for Analytics
A practical architecture often combines both systems.
For example:
+----------------+
| ASP.NET Core |
| Application |
+-------+--------+
|
+------------+------------+
| |
v v
+---------------+ +------------------+
| PostgreSQL | | SQL Query Layer |
| OLTP | | |
+---------------+ +--------+---------+
|
v
+--------------+
| Iceberg |
| Tables |
+------+-------+
|
v
Object Storage
PostgreSQL handles operational application data.
The Iceberg side handles large-scale analytical data.
Watch Out for Query Latency
A PostgreSQL-compatible interface does not automatically make Iceberg queries suitable for every application request.
Consider two types of operations.
Good fit
User requests a dashboard
|
v
Aggregate millions of historical records
|
v
Return summarized results
Poor fit
User clicks "Save"
|
v
Query huge Iceberg dataset
|
v
Wait several seconds
|
v
Return transaction result
For user-facing transactional operations, query latency must be predictable.
If the application requires consistently low-latency lookups, consider materializing the required data into a serving database or cache.
Add Caching for Repeated Analytical Queries
If multiple users repeatedly request the same aggregate, caching can reduce pressure on the analytical system.
For example:
Request
|
v
Cache
|
+-- Hit --> Return result
|
+-- Miss
|
v
Iceberg query
|
v
Store result
|
v
Return result
Caching is particularly useful for dashboards and reports that change less frequently than users access them.
Handle Large Result Sets Carefully
Avoid returning millions of rows directly to a web client.
Instead, use:
- Aggregation
- Pagination
- Filtering
- Limits
- Background exports
- Asynchronous processing
For example:
SELECT
region,
SUM(amount) AS total_sales
FROM sales_2026
GROUP BY region
ORDER BY total_sales DESC;
is usually more appropriate for an application dashboard than returning every underlying transaction.
Monitor Query Performance
Track more than just application response time.
Useful metrics include:
- Query execution time
- Number of rows returned
- Data scanned
- Cache hit rate
- Query failure rate
- Concurrent queries
- Connection pool usage
- Timeout count
A slow query should be investigated at the query-engine level rather than assuming the PostgreSQL client is responsible.
Connection Pooling
A .NET application should avoid opening a completely new database connection for every operation without pooling.
With NpgsqlDataSource, connection management can be centralized:
var builder =
new NpgsqlDataSourceBuilder(connectionString);
await using var dataSource =
builder.Build();
The application can then use the shared data source for repository operations.
Connection pooling behavior should be configured according to the selected query endpoint and workload.
Common Mistakes
Treating Iceberg as a database
Iceberg is a table format, not the query engine itself.
Sending every application query to Iceberg
Use Iceberg for workloads that benefit from analytical storage and execution.
Selecting every column
Only request the columns required by the application.
Returning huge result sets
Aggregate or paginate large datasets before sending them to clients.
Ignoring query latency
A PostgreSQL-compatible protocol does not guarantee PostgreSQL-like latency.
Putting transactions on the analytical path
Keep transactional application operations in PostgreSQL when PostgreSQL is the appropriate system of record.
Hard-coding storage paths
The application should normally work through the query engine and catalog rather than directly managing Iceberg data files.
When This Architecture Makes Sense
Querying Iceberg through a PostgreSQL-compatible interface is useful when:
- Existing applications already use PostgreSQL drivers.
- Large analytical datasets should remain in object storage.
- The organization wants to avoid copying all analytical data into PostgreSQL.
- A query engine provides the required SQL interface.
- Analytical queries can tolerate the latency characteristics of the data platform.
- The application needs a controlled SQL access layer.
It is less appropriate when the application requires high-frequency transactional updates or consistently low-latency point lookups.
Practical Checklist
Before connecting a PostgreSQL application to Iceberg data, verify:
- Which query engine will execute the SQL.
- Whether it supports the required PostgreSQL client protocol.
- How the Iceberg catalog is configured.
- Where the underlying data is stored.
- Which authentication method the application will use.
- Whether the query workload is analytical or transactional.
- Whether filters and partitioning support the expected queries.
- Whether large result sets are avoided.
- Whether connection pooling is configured correctly.
- Whether query latency is acceptable for the application.
- Whether repeated queries should be cached.
- Whether PostgreSQL should remain the serving layer for operational data.
Advantages
This architecture can provide several practical benefits:
- Existing PostgreSQL client libraries can often be reused.
- Analytical data can remain in object storage.
- Large datasets do not need to be copied into PostgreSQL.
- Application and analytics workloads can remain separated.
- SQL remains a familiar interface for developers.
- Iceberg's table-management capabilities remain available to the analytical platform.
Disadvantages
There are also trade-offs:
- Another query engine is required.
- Query latency may be higher than PostgreSQL for some workloads.
- Operational complexity increases.
- PostgreSQL-specific features may not be available.
- Transactional application patterns are generally a poor fit.
- Performance depends heavily on the query engine, catalog, storage, and table layout.
Conclusion
A PostgreSQL application can work with Apache Iceberg data without turning Iceberg into a traditional PostgreSQL database.
The key is to separate responsibilities.
PostgreSQL can continue serving transactional application data, while an Iceberg-compatible query engine handles large analytical datasets stored in object storage. If that query engine exposes a PostgreSQL-compatible interface, existing application code and tools such as Npgsql can often be used to execute SQL without teaching the application how Iceberg metadata works.
The most important design decision is not whether PostgreSQL or Iceberg is better. It is deciding which workload belongs in which system.
Use PostgreSQL where the application needs predictable transactional behavior. Use Iceberg and its query engine where large-scale analytical access, historical data, and object-storage-based analytics are the better fit.
Join the conversation! Your thoughts help the community grow.