Expose the snapshot timestamp of a stale read for reuse across split/paginated queries
- Dominant language
- Go
- Stars
- 40.5k
- Forks
- 6.2k
- PR merge metrics
- PR metrics pending
Description
## Feature Request
**Is your feature request related to a problem? Please describe:**
We use bounded-staleness stale reads (`AS OF TIMESTAMP TIDB_BOUNDED_STALENESS(...)`) to serve reads. A read may be served as a single query or, for a small fraction of cases, "split" across multiple pages/round-trips. When a read splits across pages, we need every subsequent page to read from the *exact same snapshot* as the first page, so paginated results stay consistent.
The problem is there is no clean way to (1) capture the snapshot timestamp/TSO that a bounded-staleness statement actually resolved to, and (2) reuse that exact timestamp on follow-up requests from upstream service — without resorting to explicit transactions and session-variable juggling as there is no sticky session across requests from upstream service.
A complication is that we don't reliably know up front whether a read will split. So any approach that pays a per-query cost (e.g. wrapping every read in a transaction) is paying it on the vast majority of reads that never need it.
The workarounds we've tried all have drawbacks:
- `START TRANSACTION ... AS OF TIMESTAMP` adds a round-trip and parsing/overhead, and the intermediate layers (Connection Pool, JDBC, proxy) each have their own assumptions about transactions, which causes breakage (e.g. JDBC errors on rollback if you start a transaction without setting `autocommit`, forcing us to set both).
- `AS OF TIMESTAMP` can't be used inside a transaction (`ERROR 8135 (HY000): invalid as of timestamp: as of timestamp can't be set in transaction`), and `START TRANSACTION` doesn't currently support `TIDB_BOUNDED_STALENESS`.
- Translating `NOW(6)` to a timestamp and reusing it is fragile, since it relies on a stable/deterministic timestamp→TSO conversion that TiDB doesn't currently guarantee.
This matters because we'd also like to embed the snapshot timestamp in a user-facing pagination cursor (a large fraction of reads — don't reach the last page), so paging stays consistent even across the rare split, without wrapping every query in a transaction when only a tiny fraction actually splits.
**Describe the feature you'd like:**
A lightweight way to capture and reuse the resolved snapshot timestamp of a bounded-staleness read, without explicit transactions. Concretely, two complementary additions:
1. A read-only session variable (e.g. `@@last_statement_ts`) that returns the TSO (or timestamp) the most recently executed statement read at. This lets a caller run their normal bounded-staleness read and, only if it splits, fetch the exact snapshot to pin subsequent pages:
```sql
SELECT * FROM reservations
AS OF TIMESTAMP TIDB_BOUNDED_STALENESS(NOW(6) - INTERVAL 1000000 MICROSECOND, NOW(6))
WHERE id = -1;
SELECT @@last_statement_ts;
```
2. A way to pin a subsequent read to a specific snapshot TSO without an explicit transaction — ideally a first-class syntax such as `AS OF TSO `, so it's an enforced, relied-upon feature rather than a best-effort optimizer hint. (Making `@@tidb_snapshot` settable via a statement-scoped hint was discussed but is less desirable, since hints — like `MAX_EXECUTION_TIME` — aren't guaranteed to be applied, which is unacceptable for a correctness/consistency guarantee.)
If `@@last_statement_ts` returns a stable timestamp (rather than a raw TSO), this depends on a guaranteed-deterministic timestamp→TSO translation, which would need to hold (and ideally be covered by upstream tests).
**Describe alternatives you've considered:**
- `START TRANSACTION AS OF TIMESTAMP TIDB_BOUNDED_STALENESS(...); SELECT ...; SELECT @@tidb_current_ts; COMMIT;` — committed in a single statement so JDBC is unaware. Rejected due to transaction overhead on the reads that never split, and because `START TRANSACTION` doesn't support `TIDB_BOUNDED_STALENESS` today (it would need extending).
- `BEGIN; SET @ts := @@tidb_current_ts - 1 second; ROLLBACK; SET @@tidb_snapshot=@ts; SELECT ...; SET @@tidb_snapshot='';` — heavy multi-step overhead to run on every read when only a fraction need it.
- Reusing `NOW(6)` converted to a timestamp — fragile without a guaranteed deterministic timestamp→TSO conversion.
**Teachability, Documentation, Adoption, Migration Strategy:**
Primary scenario: serving consistent paginated reads. A client issues a normal bounded-staleness read; in the common case (no split) nothing extra is needed. When a read splits across pages, the client retrieves the resolved snapshot (`SELECT @@last_statement_ts`) and pins all subsequent pages — and optionally the user-facing cursor — to that exact snapshot (`... AS OF TSO `), guaranteeing every page reads from the same point in time. Because the snapshot can be embedded in the cursor, consistency is preserved even if the client gives up the snapshot after some staleness threshold (e.g. fall back to a fresh read if the cursor is older than a few minutes — a user paging through results from a long time ago may not actually want to keep paging anyway).
This is purely additive — a new read-only session variable and a new snapshot-pinning form — so it requires no migration and doesn't affect existing queries. The variable approach was noted as much easier to introduce than new syntax; the `AS OF TSO` syntax is preferred for the pinning piece specifically because it's an enforced, dependable consistency primitive rather than a hint.
Contributor guide
Research direction
The issue names no files, tests, or entry points. Start by locating the session-variable and AS OF TIMESTAMP implementations and their existing tests in TiDB. Done means defining and testing a reliable way to expose the resolved snapshot and pin later reads without explicit transactions, including the timestamp-to-TSO consistency requirement.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, sql
- Domain
- databases
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100