S3 Tables Best Practices
Iceberg Type Mapping
For the full list of supported Iceberg data types and their mappings to query engine types, search AWS docs for "Supported data types for Iceberg tables in Athena". Complex types (list, map, struct) require schemaV2 instead of schema in API metadata. Search AWS docs for "IcebergSchemaV2 S3 Tables" for the full spec. Example with nested struct:
{"iceberg":{"schemaV2":{"type":"struct","fields":[
{"id":1,"name":"device_id","required":true,"type":"string"},
{"id":2,"name":"location","required":false,"type":{
"type":"struct","fields":[
{"id":3,"name":"latitude","required":true,"type":"double"},
{"id":4,"name":"longitude","required":true,"type":"double"}
]}}
]}}}Key: top-level must have "type":"struct", all fields need explicit "id", nested struct uses "type":{"type":"struct","fields":[...]}.
Default choices when ambiguous:
- IDs: use
long(safer thanintfor growth) - Text: use
string(no need to specify length in Iceberg) - Timestamps: use
timestampunless timezone awareness is needed, thentimestamptz - Money: use
intorlongstoring cents/smallest unit to avoid floating-point errors. Usedecimal(p,s)only when fractional amounts are required.
Partition Strategy
Choose partitions based on query access patterns, not data structure.
Time-series (events, logs, metrics):
- High/medium-volume (≥100K rows/day):
PARTITIONED BY (event_date)with identity transform - Low-volume (<100K rows/day): partition by month transform
Multi-tenant: PARTITIONED BY (tenant_id), add date if high volume per tenant.
No clear pattern: Start without partitions. Iceberg supports adding partitions later without rewriting data.
Partition guidelines:
- Use columns with low cardinality (10-10,000 unique values) frequently in WHERE clauses
- Limit to 2-3 partition levels
- Do NOT partition by high-cardinality columns (user_id, transaction_id)
- Aim for partition sizes of 100MB-1GB
Naming Conventions
All names MUST be lowercase (Glue Data Catalog requirement).
- Table bucket: lowercase, numbers, hyphens. 3-63 chars. Name by team/domain (e.g.,
analytics-tables,marketing-data) - Namespace: lowercase, underscores. Name by data stage or domain (e.g.,
raw_events,processed,analytics) - Table: lowercase, underscores. Name by entity (e.g.,
customer_orders,click_events) - Columns: lowercase, snake_case. Descriptive names, avoid abbreviations.
Schema Design
- Use descriptive names that won't need renaming
- Avoid packing JSON strings into single columns -- use Iceberg struct/map/array types
- For schema evolution, see
athena-ddl-path.md.
Storage Class
Default is STANDARD. For tables with infrequently accessed historical data, set Intelligent Tiering at bucket creation:
aws s3tables create-table-bucket --name <NAME> --region <REGION> \
--storage-class-configuration '{"storageClass":"INTELLIGENT_TIERING"}'Bucket default can be changed with aws s3tables put-table-bucket-storage-class (applies to new tables only). Per-table storage class is set at creation via create-table --storage-class-configuration and cannot be changed after.
Common Errors
| Error | Fix |
|---|---|
| "Access denied creating table bucket" | Need s3tables:CreateTableBucket, s3tables:ListTableBuckets. For full workflow see Step 6 in SKILL.md and references/table-creation-glue-etl.md for granular permissions. |
| "Namespace not found" | Namespaces must exist before tables. Create with aws s3tables create-namespace. |
| Table not visible in Athena | Run aws glue get-catalog --catalog-id s3tablescatalog. If missing, follow Step 5 in SKILL.md. If present, check execution context format in athena-ddl-path.md. |
| Write operations fail | Verify IAM role has s3tables:PutTableData and s3tables:UpdateTableMetadataLocation. |
AccessDeniedException despite correct IAM policy |
s3tablescatalog may be in Lake Formation mode. Check with aws glue get-catalog --catalog-id s3tablescatalog — if CreateDatabaseDefaultPermissions is empty, the catalog is in LF mode. Migrate with aws glue update-catalog using OverwriteChildResourcePermissionsWithDefault: Accept. WARNING: this propagates to ALL child resources and removes existing LF grants. You MUST confirm with user. Search AWS docs for "Change access control from Lake Formation to IAM". |
Shell escaping errors with --catalog-input JSON |
Save JSON to a file and use --catalog-input file://catalog-input.json instead of inline JSON. |