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 2 3 4 5 6 7 8 910111213
-- Save your AWS access key ID in Vaultselectvault.create_secret('<access key id>','dynamodb_access_key_id','AWS access key ID for DynamoDB Wrappers');-- Save your AWS secret access key in Vaultselectvault.create_secret('<secret access key>','dynamodb_secret_access_key','AWS secret access key for DynamoDB Wrappers');
Connecting to DynamoDB
We need to provide Postgres with the credentials to connect to DynamoDB. We can do this using the create server command.
1 2 3 4 5 6 7 8 9101112
createserverdynamodb_serverforeigndatawrapperdynamodb_wrapperoptions(-- The key id saved in Vault from abovevault_access_key_id'<vault_key_id>',-- The secret id saved in Vault from abovevault_secret_access_key'<vault_secret_id>',-- AWS region where your DynamoDB table(s) resideregion'us-east-1');
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:
123456789
-- Import only specific tablesimportforeignschemadynamodblimitto(users,orders)fromserverdynamodb_serverintodynamodb;-- Import all tables except specific onesimportforeignschemadynamodbexcept(staging_table)fromserverdynamodb_serverintodynamodb;
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 jsonb column 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 _attrs column generated by import_foreign_schema works.
INSERT uses DynamoDB's PutItem operation, which replaces the entire item if an item with the same key already exists. Use UPDATE for partial attribute changes.
UPDATE and DELETE require rowid_column to 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.
12
-- Uses Query API (efficient)select*fromdynamodb.userswhereid='user123';
Additional conditions on the sort key (if the table has one) are also pushed down as key condition expressions:
12
-- Uses Query API with sort key conditionselect*fromdynamodb.orderswherecustomer_id='cust1'andorder_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.
12345
-- Full Scan with FilterExpression (expensive on large tables)select*fromdynamodb.userswhereage>30;-- Full Scan, no filterselect*fromdynamodb.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:
TRUNCATE is not supported. Use DELETE without a WHERE clause to remove all rows (this performs a full scan and individual DeleteItem calls, so it is slow on large tables).
Full table scans (Scan API) do not reduce read capacity unit consumption even when a FilterExpression is used.
LIKE and IN operators are not pushed down — filtering is done in Postgres after fetching rows.
UPDATE cannot change partition key or sort key values, as DynamoDB does not support key updates in place.
INSERT replaces the entire item if the key already exists (PutItem semantics). Use UPDATE to add or change individual attributes without replacing the whole item.
Compound DynamoDB types (List, Map, sets) are read as jsonb and 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.
Each imported table has key columns plus _attrs jsonb for everything else:
1 2 3 4 5 6 7 8 91011
-- Access non-key attributes via JSON operatorsselectid,_attrs->>'name'asname,(_attrs->>'age')::integerasage,(_attrs->>'active')::booleanasactive,_attrs->'tags'astags-- nested Mapfromdynamodb.users;-- Partition key pushdown still works on imported tablesselect*fromdynamodb.orderswhereorder_id='ord-001';