AWS DynamoDB
AWS DynamoDB is a fully managed, serverless, key-value NoSQL database designed to run high-performance applications at any scale.
The DynamoDB Wrapper allows you to read and write DynamoDB table data within your Postgres database.
Preparation#
Before you can query DynamoDB, you need to enable the Wrappers extension and store your credentials in Postgres.
Enable Wrappers#
Make sure the wrappers extension is installed on your database:
1create extension if not exists wrappers with schema extensions;Enable the DynamoDB Wrapper#
Enable the dynamodb_wrapper FDW:
1create foreign data wrapper dynamodb_wrapper2 handler dynamo_db_fdw_handler3 validator dynamo_db_fdw_validator;Store your credentials (optional)#
By default, Postgres stores FDW credentials inside pg_catalog.pg_foreign_server in plain text. Anyone with access to this table will be able to view these credentials. Wrappers is designed to work with Vault, which provides an additional level of security for storing credentials. We recommend using Vault to store your credentials.
1-- Save your AWS access key ID in Vault2select vault.create_secret(3 '<access key id>',4 'dynamodb_access_key_id',5 'AWS access key ID for DynamoDB Wrappers'6);78-- Save your AWS secret access key in Vault9select vault.create_secret(10 '<secret access key>',11 'dynamodb_secret_access_key',12 'AWS secret access key for DynamoDB Wrappers'13);Connecting to DynamoDB#
We need to provide Postgres with the credentials to connect to DynamoDB. We can do this using the create server command.
1create server dynamodb_server2 foreign data wrapper dynamodb_wrapper3 options (4 -- The key id saved in Vault from above5 vault_access_key_id '<vault_key_id>',67 -- The secret id saved in Vault from above8 vault_secret_access_key '<vault_secret_id>',910 -- AWS region where your DynamoDB table(s) reside11 region 'us-east-1'12 );Additional Server Options#
| Option | Required | Description |
|---|---|---|
region | Yes | AWS region where your DynamoDB tables reside (e.g. us-east-1) |
endpoint_url | No | Custom endpoint URL, useful for local development with DynamoDB Local |
Create a schema#
We recommend creating a schema to hold all the foreign tables:
1create schema if not exists dynamodb;Options#
The full list of foreign table options are below:
| Option | Required | Description |
|---|---|---|
table | Yes | The DynamoDB table name to query |
rowid_column | No | The column to use as the row identifier for UPDATE and DELETE operations. Must be the partition key column. Required for write operations. |
Entities#
We can use SQL import foreign schema to automatically create foreign table definitions from your DynamoDB tables.
1import foreign schema dynamodb from server dynamodb_server into dynamodb;This will call ListTables and DescribeTable for each table and generate a CREATE FOREIGN TABLE statement with:
- Key columns (partition key and optional sort key) typed as
text - A
_attrs jsonbcatch-all column that captures every non-key attribute as a JSON object
For example, a DynamoDB table users with partition key id and attributes name, age, and active would produce:
1create foreign table users (2 id text not null,3 _attrs jsonb4)5server dynamodb_server6options (table 'users', rowid_column 'id');You can then access individual attributes via standard JSON operators:
1select2 id,3 _attrs->>'name' as name,4 (_attrs->>'age')::integer as age,5 (_attrs->>'active')::boolean as active6from dynamodb.users;The _attrs name was chosen to avoid conflicts with real DynamoDB attribute names. If you need typed columns or a different layout, create the foreign table manually instead (see Create a foreign table manually).
You can also limit the import to specific tables:
1-- Import only specific tables2import foreign schema dynamodb3 limit to (users, orders)4 from server dynamodb_server into dynamodb;56-- Import all tables except specific ones7import foreign schema dynamodb8 except (staging_table)9 from server dynamodb_server into dynamodb;DynamoDB Tables#
Each foreign table maps to one DynamoDB table.
Operations#
| Object | Select | Insert | Update | Delete | Truncate |
|---|---|---|---|---|---|
| Table | ✅ | ✅ | ✅ | ✅ | ❌ |
Usage#
1create foreign table dynamodb.users (2 id text,3 name text,4 age bigint,5 active boolean,6 metadata jsonb7)8server dynamodb_server9options (10 table 'users',11 rowid_column 'id'12);Notes#
- Column names in the foreign table must match the DynamoDB attribute names exactly (case-sensitive).
- Any DynamoDB attribute not listed as a column is silently ignored during reads, except when a
jsonbcolumn has no matching DynamoDB attribute by name — in that case it acts as a catch-all and receives all attributes not covered by any other declared column. This is how the_attrscolumn generated byimport_foreign_schemaworks. INSERTuses DynamoDB'sPutItemoperation, which replaces the entire item if an item with the same key already exists. UseUPDATEfor partial attribute changes.UPDATEandDELETErequirerowid_columnto be set to the partition key column name.
Query Pushdown Support#
The DynamoDB Wrapper supports two scan strategies depending on the WHERE clause:
Query (efficient — uses DynamoDB Query API)#
When a WHERE clause includes an equality filter on the partition key, the wrapper uses the DynamoDB Query API. This reads only the items matching the partition key, consuming far fewer read capacity units than a full scan.
1-- Uses Query API (efficient)2select * from dynamodb.users where id = 'user123';Additional conditions on the sort key (if the table has one) are also pushed down as key condition expressions:
1-- Uses Query API with sort key condition2select * from dynamodb.orders where customer_id = 'cust1' and order_date >= '2024-01-01';Scan (full table — uses DynamoDB Scan API)#
When no partition key equality filter is present, the wrapper performs a full Scan. Non-key column filters are sent as a FilterExpression, which reduces the data transferred over the network but does not reduce the read capacity units consumed — DynamoDB reads every item before applying the filter.
1-- Full Scan with FilterExpression (expensive on large tables)2select * from dynamodb.users where age > 30;34-- Full Scan, no filter5select * from dynamodb.users;Supported Operators#
| Operator | Pushdown |
|---|---|
= | ✅ |
< | ✅ |
<= | ✅ |
> | ✅ |
>= | ✅ |
<> | ✅ |
LIKE | ❌ |
IN | ❌ |
Scan costs on large tables
Full table scans on large DynamoDB tables consume significant read capacity and can be slow. Always include a partition key equality filter when querying large tables.
Supported Data Types#
| Postgres Type | DynamoDB Attribute Type |
|---|---|
boolean | Boolean (BOOL) |
text | String (S), Number (N as string), or default |
smallint | Number (N) |
integer | Number (N) |
bigint | Number (N) |
real | Number (N) |
double precision | Number (N) |
numeric | Number (N) |
date | String (S, ISO 8601 format) |
timestamp | String (S, ISO 8601 format) |
timestamptz | String (S, ISO 8601 format) |
bytea | Binary (B) |
jsonb | List (L), Map (M), String Set (SS), Number Set (NS), Binary Set (BS) |
DynamoDB stores numbers as strings internally. The wrapper coerces them to the declared Postgres column type at read time. If no numeric type is declared, numbers are returned as text.
Compound types (List, Map, String Set, Number Set, Binary Set) are always converted to jsonb.
Limitations#
This section describes important limitations and considerations when using this FDW:
TRUNCATEis not supported. UseDELETEwithout aWHEREclause to remove all rows (this performs a full scan and individualDeleteItemcalls, so it is slow on large tables).- Full table scans (
ScanAPI) do not reduce read capacity unit consumption even when aFilterExpressionis used. LIKEandINoperators are not pushed down — filtering is done in Postgres after fetching rows.UPDATEcannot change partition key or sort key values, as DynamoDB does not support key updates in place.INSERTreplaces the entire item if the key already exists (PutItemsemantics). UseUPDATEto add or change individual attributes without replacing the whole item.- Compound DynamoDB types (List, Map, sets) are read as
jsonband written as a JSON string (DynamoDB String attribute). To write native DynamoDB List or Map attributes, manage items directly through the AWS SDK. - Materialized views using these foreign tables may fail during logical backups.
- DynamoDB transactions are not supported.
Examples#
Basic setup#
1create extension if not exists wrappers with schema extensions;23create foreign data wrapper dynamodb_wrapper4 handler dynamo_db_fdw_handler5 validator dynamo_db_fdw_validator;67create server dynamodb_server8 foreign data wrapper dynamodb_wrapper9 options (10 aws_access_key_id '<aws_access_key>',11 aws_secret_access_key '<aws_secret_key>',12 region 'us-east-1'13 );1415create schema if not exists dynamodb;Import all tables automatically#
1import foreign schema dynamodb2 from server dynamodb_server into dynamodb;Each imported table has key columns plus _attrs jsonb for everything else:
1-- Access non-key attributes via JSON operators2select3 id,4 _attrs->>'name' as name,5 (_attrs->>'age')::integer as age,6 (_attrs->>'active')::boolean as active,7 _attrs->'tags' as tags -- nested Map8from dynamodb.users;910-- Partition key pushdown still works on imported tables11select * from dynamodb.orders where order_id = 'ord-001';Create a foreign table manually#
1create foreign table dynamodb.products (2 product_id text,3 name text,4 price numeric,5 in_stock boolean,6 tags jsonb7)8server dynamodb_server9options (10 table 'products',11 rowid_column 'product_id'12);Query with partition key pushdown#
1-- Efficient: uses DynamoDB Query API2select * from dynamodb.products where product_id = 'prod-001';Query with filter (full scan)#
1-- Full scan with server-side FilterExpression2select * from dynamodb.products where price > 50;Insert a new item#
1insert into dynamodb.products (product_id, name, price, in_stock)2values ('prod-999', 'Widget', 9.99, true);Update specific attributes#
1-- Only updates 'price' and 'in_stock'; other attributes are preserved2update dynamodb.products3set price = 12.99, in_stock = false4where product_id = 'prod-999';Delete an item#
1delete from dynamodb.products where product_id = 'prod-999';Using DynamoDB Local for development#
1create server dynamodb_local_server2 foreign data wrapper dynamodb_wrapper3 options (4 aws_access_key_id 'test',5 aws_secret_access_key 'test',6 region 'us-east-1',7 endpoint_url 'http://localhost:8000'8 );