Overview
This guide demonstrates how to leverage PostgreSQL’s native AWS S3 integration to directly export and import table data between your database and Amazon S3 - all through SQL commands executed from a Spring Boot application.
flowchart TD
    A[Spring Boot Application] -->|JDBC| B[(PostgreSQL Database)]
    B -->|aws_s3 Extension| C[(Amazon S3 Bucket)]
    C -->|Imported Data| B
    B -->|Exported Data| C
Prerequisites
1. Database Requirements
- PostgreSQL 12+ (Amazon RDS or self-managed)
- aws_s3and- aws_commonsextensions installed
- Proper IAM permissions for S3 access
2. Application Requirements
- Spring Boot 2.7+
- Spring Data JDBC or JPA
- PostgreSQL JDBC driver
Setup Instructions
1. Enable Required PostgreSQL Extensions
| 1
2
3
4
5
6
 | -- For self-managed PostgreSQL:
CREATE EXTENSION IF NOT EXISTS aws_commons;
CREATE EXTENSION IF NOT EXISTS aws_s3;
-- Verify installation
SELECT * FROM pg_available_extensions WHERE name LIKE 'aws%';
 | 
Option A: IAM Role (Recommended for RDS)
For RDS PostgreSQL, attach an IAM role with these permissions:
| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
 | {
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::your-bucket-name/*"
    }
  ]
}
 | 
Option B: Access Keys (For non-RDS)
| 1
2
3
4
5
 | SELECT aws_commons.set_aws_credentials(
  'your-access-key-id',
  'your-secret-access-key',
  'us-east-1'
);
 | 
sequenceDiagram
    Admin->>PostgreSQL: CREATE EXTENSION aws_commons
    Admin->>PostgreSQL: CREATE EXTENSION aws_s3
    PostgreSQL-->>Admin: Extensions created
    Admin->>PostgreSQL: Configure AWS credentials
    PostgreSQL-->>AWS: Validate permissions
    AWS-->>PostgreSQL: Access granted
Exporting Data to S3
SQL Command Structure
| 1
2
3
4
5
6
7
8
9
 | SELECT aws_s3.query_export_to_s3(
  'SELECT * FROM your_table',          -- Query to export
  aws_commons.create_s3_uri(            -- S3 destination
    'your-bucket', 
    'path/to/file.csv', 
    'aws-region'
  ),
  options := 'format csv, header'       -- Export options
);
 | 
| Format | Options Example | Notes | 
|---|
| CSV | 'format csv, header' | Most common format | 
| Text | 'format text' | Tab-delimited | 
| Binary | 'format binary' | PostgreSQL binary format | 
flowchart LR
    SB[Spring Boot] -->|1. Execute SQL| PG[PostgreSQL]
    PG -->|2. Generate CSV| PG
    PG -->|3. Upload| S3[S3 Bucket]
    S3 -->|4. Confirm| PG
    PG -->|5. Return Status| SB
Importing Data from S3
SQL Command Structure
| 1
2
3
4
5
6
7
8
9
10
 | SELECT aws_s3.table_import_from_s3(
  'target_table',                      -- Target table name
  'col1,col2,col3',                    -- Optional column list
  '(format csv, header)',              -- Import options
  aws_commons.create_s3_uri(           -- S3 source
    'your-bucket', 
    'path/to/file.csv', 
    'aws-region'
  )
);
 | 
sequenceDiagram
    Spring Boot->>PostgreSQL: Import request
    PostgreSQL->>S3: Get file
    S3-->>PostgreSQL: File data
    PostgreSQL->>PostgreSQL: Parse and insert
    PostgreSQL-->>Spring Boot: Import result
Spring Boot Implementation
1. Database Configuration
| 1
2
3
4
5
6
7
8
 | spring.datasource.url=jdbc:postgresql://localhost:5432/yourdb
spring.datasource.username=user
spring.datasource.password=password
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.hikari.maximum-pool-size=10
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
 | 
2. S3 Export/Import Repository and Service
| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 | @Repository
public interface S3DataRepository extends Repository<Object, Long> {
    @Modifying
    @Query(value = """
        SELECT aws_s3.query_export_to_s3(
          :query,
          aws_commons.create_s3_uri(:bucket, :key, :region),
          :options
        )
        """, nativeQuery = true)
    void exportToS3(
        @Param("query") String query,
        @Param("bucket") String bucket,
        @Param("key") String key,
        @Param("region") String region,
        @Param("options") String options
    );
    @Modifying
    @Query(value = """
        SELECT aws_s3.table_import_from_s3(
          :table,
          :columns,
          :options,
          aws_commons.create_s3_uri(:bucket, :key, :region)
        )
        """, nativeQuery = true)
    void importFromS3(
        @Param("table") String table,
        @Param("columns") String columns,
        @Param("options") String options,
        @Param("bucket") String bucket,
        @Param("key") String key,
        @Param("region") String region
    );
}
 | 
| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 | @Service
@RequiredArgsConstructor
public class S3DataService {
    private final S3DataRepository s3DataRepository;
    @Transactional
    public void exportToS3(String query, String bucket, String key, 
                           String region, String formatOptions) {
        s3DataRepository.exportToS3(query, bucket, key, region, formatOptions);
    }
    @Transactional
    public void importFromS3(String table, String columns, 
                             String bucket, String key, 
                             String region, String formatOptions) {
        s3DataRepository.importFromS3(table, columns, formatOptions, bucket, key, region);
    }
}
 | 
3. REST Controller
| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 | @RestController
@RequestMapping("/api/data")
@RequiredArgsConstructor
public class DataTransferController {
    private final S3DataService s3DataService;
    @PostMapping("/export")
    public ResponseEntity<String> exportData(
            @RequestParam String query,
            @RequestParam String bucket,
            @RequestParam String key,
            @RequestParam(defaultValue = "us-east-1") String region,
            @RequestParam(defaultValue = "format csv, header") String options) {
        
        s3DataService.exportToS3(query, bucket, key, region, options);
        return ResponseEntity.ok("Data exported successfully to s3://%s/%s".formatted(bucket, key));
    }
    @PostMapping("/import")
    public ResponseEntity<String> importData(
            @RequestParam String table,
            @RequestParam(required = false) String columns,
            @RequestParam String bucket,
            @RequestParam String key,
            @RequestParam(defaultValue = "us-east-1") String region,
            @RequestParam(defaultValue = "(format csv, header)") String options) {
        
        s3DataService.importFromS3(table, columns, bucket, key, region, options);
        return ResponseEntity.ok("Data imported successfully from s3://%s/%s".formatted(bucket, key));
    }
}
 | 
Advanced Usage
1. Handling Large Exports
| 1
2
3
4
5
6
 | -- Export in chunks
SELECT aws_s3.query_export_to_s3(
  'SELECT * FROM large_table WHERE id BETWEEN 1 AND 100000',
  aws_commons.create_s3_uri('bucket', 'chunk1.csv', 'region'),
  'format csv'
);
 | 
2. Custom Delimiters
| 1
2
3
4
5
6
 | -- Using pipe delimiter
SELECT aws_s3.query_export_to_s3(
  'SELECT * FROM employees',
  aws_commons.create_s3_uri('bucket', 'file.psv', 'region'),
  'format csv, delimiter ''|'', header'
);
 | 
⚠️ Troubleshooting
| Issue | Solution | 
|---|
| Permission denied | Verify IAM role or access keys | 
| Extension not found | Install aws_s3 extension | 
| Invalid S3 URI | Check bucket/region spelling | 
| Malformed CSV | Verify format options | 
- Network Throughput: S3 transfers are network-bound
- File Size: Optimal chunk size 100MB-1GB
- Database Load: Schedule large exports during off-peak hours
Security Best Practices
- Use IAM roles instead of access keys when possible
- Restrict S3 bucket access with bucket policies
- Enable S3 server-side encryption
- Use VPC endpoints for private network access
Additional Resources
- AWS Documentation on PostgreSQL S3 Extension
- PostgreSQL Foreign Data Wrappers