Back to the experiment list
E001CompletedP0

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 prefix e001-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 available at 07:31Z; we then added the connection details to the manifest under the same prefix and ran run → cleanup → verify manually. The check code and inputs did not change. We confirmed that R1 was created as Multi-AZ from the CloudTrail CreateDBInstance request (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, and 40001 retries). 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 UPDATE and 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 verify queried the manifest resources, snapshots, retained automated backups and tagged EC2 resources for this prefix and confirmed remaining_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 the e001 prefix, DSQL clusters, and VPCs tagged e001:run-prefix, and confirmed that there were none.