SQL compatibility and executability
What must change to implement the same workload on DSQL?
Results summary
This experiment measured how much SQL has to be changed to move an existing PostgreSQL workload to Aurora DSQL. We ran 35 workload SQL items unchanged on DSQL (D1) and on three control services (RDS PostgreSQL, Aurora Provisioned, Aurora Serverless v2). DSQL passed 17 items without modification and rejected 16 items as “feature not supported” (SQLSTATE 0A000). The three control services passed all 34 items other than CREATE INDEX ASYNC, which is DSQL-only syntax.
The areas that needed changes on DSQL were sequence and identity declarations, the serial type, how indexes are created, temporary tables and partitions, PL/pgSQL functions and triggers, isolation level settings, statement_timeout, and server-side statement cancellation. In contrast, constraints such as PK, UNIQUE, CHECK and foreign keys, JOINs, CTEs, window functions, JSONB, and the core order transaction worked as written. SELECT FOR UPDATE was accepted syntactically but behaved differently. The controls made the other write wait, whereas DSQL did not make it wait and instead failed one side with a conflict (40001) at commit time. Applications moved to DSQL therefore need logic that retries failed transactions.
These results come from SQL feature checks only, on small configurations with a local client, and each configuration was run once. Performance, latency, and high availability were not evaluated, and because DSQL’s supported feature set keeps changing, the results should be read as observations as of 2026-09-24.
Production readiness
DSQL ran core OLTP SQL, such as constraints, foreign keys, JOINs, JSONB, and the order transaction, without modification, so for a new service designed around DSQL’s constraints from the start, the SQL-level barrier to adoption is low. Moving an existing PostgreSQL service, however, requires changing sequence and serial declarations, how indexes are created, PL/pgSQL and triggers, and temporary tables and partitions. In addition, REPEATABLE READ is the only isolation level, and statement_timeout and server-side cancellation do not work, so conflict retries and request deadlines must be implemented in the application. The more logic a system keeps in stored procedures and triggers, the higher the migration cost. This experiment is only a feature check; performance and availability are judged in E002 and later experiments.
Question
What must change to implement the same workload on DSQL? We ran 35 existing PostgreSQL workload SQL items on D1 (Aurora DSQL) and on three controls (R1 RDS PostgreSQL, A1 Aurora Provisioned, A2 Aurora Serverless v2), and judged both whether the syntax was accepted and whether results and invariants matched.
Test conditions
- Measurement date: 2026-09-24 UTC, region
ap-northeast-2, run prefixe001-20260924t071645z-d551. - Code commit:
dfbbe696da48df2df5434ee400c4386fff807197(the same code for all four configurations). - Configurations (scaled down from the plan, for SQL feature checks only):
| ID | Actual configuration | Difference from the planned configuration |
|---|---|---|
| D1 | DSQL single-Region cluster, admin IAM token, verify-full TLS |
None |
| R1-lite | RDS PostgreSQL 16.15, db.t4g.micro, gp3 20 GiB, Multi-AZ |
Smaller class and storage |
| A1-lite | Aurora PostgreSQL 16.15, one db.t4g.medium writer |
No reader, small class |
| A2-lite | Aurora Serverless v2 16.15, one writer, 0.5–2 ACU | No reader, narrower ACU range |
- Client: a local PC (Python and psycopg) connecting to public endpoints. Latency over this connection path is not used as a basis for comparison.
- Each item ran on its own table and a new connection, so that one item’s failure could not affect the others. Each statement had a 30-second client-side deadline.
- Deviation during the run: while R1 was being provisioned (started at 07:20Z), the agent session that was managing the run stopped because of a usage limit. The instance kept being created and became
availableat 07:31Z; we then added the connection details to the manifest under the same prefix and ranrun→cleanup→verifymanually. The check code and inputs did not change. We confirmed that R1 was created as Multi-AZ from the CloudTrailCreateDBInstancerequest (multiAZ=true).
Performance results
This experiment was a feature check and did not measure throughput or latency (not measured). The per-item verdicts are below. pass means the syntax was accepted and the semantic check passed; unsupported means SQLSTATE 0A000; rejected_needs_review means a syntax or object error that a person has to judge; semantic_mismatch means the statement was accepted but behaved differently from what was expected.
| Verdict | D1 | R1 | A1 | A2 |
|---|---|---|---|---|
| pass | 17 | 34 | 34 | 34 |
| unsupported (0A000) | 16 | 0 | 0 | 0 |
| rejected_needs_review | 1 | 1 | 1 | 1 |
| semantic_mismatch | 1 | 0 | 0 | 0 |
The single rejected_needs_review item on R1, A1 and A2 is the DSQL-only syntax CREATE INDEX ASYNC (42601), and its rejection by PostgreSQL is the expected result. There were no differences among the three control configurations.
Items whose results differed on DSQL (D1):
| Item | D1 result | Observation | How to port |
|---|---|---|---|
CREATE SEQUENCE (default CACHE), GENERATED ... AS IDENTITY (default CACHE) |
unsupported | Default CACHE value rejected | Passes when CACHE 65536 is specified |
serial type |
rejected (42704) | type "serial" does not exist |
Change to identity or a sequence (CACHE 65536) |
CREATE INDEX (B-tree, GIN, expression) |
unsupported | “please use CREATE INDEX ASYNC” | B-tree passes with CREATE INDEX ASYNC. Alternatives for GIN and expression indexes not verified |
| Temporary tables, range partitions | unsupported | — | Application design change required |
| PL/pgSQL functions, triggers | unsupported | SQL functions pass | Move the logic to SQL functions or the application |
Three forms of SET TRANSACTION ISOLATION LEVEL ..., BEGIN ISOLATION LEVEL READ COMMITTED/SERIALIZABLE |
unsupported | Only BEGIN ISOLATION LEVEL REPEATABLE READ accepted |
Review logic assuming REPEATABLE READ |
statement_timeout (sleep and CPU variants) |
unsupported | Session setting rejected | Replace with a client-side deadline |
| Client cancellation | semantic_mismatch | A cancel request was sent to pg_sleep(5), but the statement ran to completion and succeeded (expected: 57014) |
Limit the size of work so as not to rely on server-side interruption |
Items that passed on DSQL without modification: PK/UNIQUE/CHECK, foreign keys (both inserting an orphan row and deleting a referenced parent were rejected with 23503), UPSERT, JOIN/CTE/window, recursive CTE, JSONB at runtime and in stored columns, SQL functions, SELECT FOR UPDATE, lost update prevention under REPEATABLE READ, and the common order transaction (both the original with FKs and the variant without FKs matched on business results and invariants).
SELECT FOR UPDATE passed without lost updates on all four configurations, but the mechanism differs. In the scenario, connection A locks a row with FOR UPDATE and applies a +1 update, while connection B tries to apply a +10 update to the same row in the meantime. On R1, A1 and A2, B waited until A committed and both updates were applied (100 → 111). On D1, B’s write finished first while A held the lock, and A failed at commit time with 40001 (serialization conflict), so A’s update was rolled back (100 → 110). In other words, DSQL’s FOR UPDATE does not make other writes wait and preserves consistency through commit-time conflicts, so the application needs logic that retries failed transactions to get the same business result. Consistency and retry cost under contention are verified in E004.
Development and operations
- Provisioning time (from the create request until connections were possible): D1 16 seconds, A1 about 5 minutes, A2 about 7 minutes, R1 about 12 minutes (including the Multi-AZ conversion). Deletion (from the request until deletion was confirmed): D1 about 1.5 minutes, R1 about 3.5 minutes, A1 about 6.5 minutes, A2 about 6 minutes.
- D1 was reached with an IAM token, without a VPC, subnets, security groups or a DB password. For each control configuration we had to create a VPC, two subnets, an IGW, a security group and a DB subnet group, and manage a password.
- On the other hand, moving an existing schema and SQL to DSQL requires the changes in the table above (sequence CACHE, removing
serial, ASYNC indexes, moving PL/pgSQL and triggers, changing isolation level and timeout handling, and40001retries). The time needed for these changes was not measured in this experiment. - The times above are from a single run; variation was not checked with repeated measurements.
Cost
Unit prices are Seoul Region On-Demand prices checked with the AWS Price List API on 2026-09-24. The estimates in the table below take the time from the create request to deletion completion as the upper bound of running time, and the actual charges were checked on 2026-09-28 in Cost Explorer (Seoul Region, 2026-09-24 UTC, by usage type).
| Configuration | Running time (upper bound) | Unit price | Estimate | Actual charge |
|---|---|---|---|---|
| D1 | DSQL cluster create to delete, about 2 minutes | $0.00001/DPU | About $0.0006 from 57 DPU in one pilot run (DPU for the comparison run not collected) | $0 (within the DSQL monthly free usage) |
| R1 | 15.9 minutes | $0.051/hour (db.t4g.micro Multi-AZ) | About $0.014 | About $0.012 (including storage) |
| A1 | 12.0 minutes | $0.113/hour (db.t4g.medium) | About $0.023 | About $0.019 (10-minute minimum charge) |
| A2 | 13.0 minutes | $0.20/ACU-hour | About $0.022–0.087 for the 0.5–2 ACU range | About $0.048 (0.217 ACU-hours, including Aurora I/O) |
In the estimate we excluded storage, Aurora I/O and public IPv4 charges as small, and estimated the total at about $0.06–0.13. The actual total charge was about $0.08, within the estimated range. A1 actually ran for less than 10 minutes, so it was billed for the RDS minimum charge of 10 minutes (0.167 hours).
Conclusions and limitations
- Conclusion: on these 35 items, DSQL ran 17 (49%) without modification, and the three control services passed all PostgreSQL feature items. The areas that need changes when moving to DSQL are sequence and identity declarations,
serial, how indexes are created, temporary tables and partitions, PL/pgSQL and triggers, isolation level settings,statement_timeout, and server-side statement cancellation. The core business flow, the order transaction, passed on DSQL as the original, including FKs. - DSQL’s FK support is the behavior observed in this measurement (2026-09-24). DSQL’s supported feature set keeps changing, so results may differ at another time.
- Limitations: this is a feature check with a local client and small configurations; latency, throughput and HA were not evaluated. Each configuration was run once.
SELECT FOR UPDATEand isolation levels were observed in a single two-connection scenario. ORM schema changes and logical replication/CDC were not measured. Alternatives on DSQL for GIN and expression indexes were not verified.
Cleanup record
- Deleted: one D1 DSQL cluster, and for each of R1, A1 and A2 the DB instance (for Aurora, including the DB cluster), DB subnet group, security group, two subnets, IGW and VPC.
- Cleanup completion times (UTC): D1 07:18:46, R1 07:35:56, A1 07:48:12, A2 08:01:18. No deletion step failed.
- Verification: at 08:01:37Z the experiment tool’s
verifyqueried the manifest resources, snapshots, retained automated backups and tagged EC2 resources for this prefix and confirmedremaining_count=0. The pilot prefix (e001-20260924t071247z-cf9e) was also confirmed at 0 at 08:01:38Z. In addition, we queried the whole account for DB instances, clusters and cluster snapshots with thee001prefix, DSQL clusters, and VPCs taggede001:run-prefix, and confirmed that there were none.