Skip to content
Discord Get Started

Transactions & COPY

Transaction control, savepoints, isolation semantics, and autocommit behavior.

SQL
BEGIN;
-- ... your SQL statements ...
COMMIT; -- persist all changes atomically
ROLLBACK; -- discard all changes

Single statements outside an explicit BEGIN block run in implicit autocommit mode. The system begins a transaction, executes the statement, and commits (or rolls back on error). Retryable TiKV errors trigger automatic retry with backoff.

SQL
BEGIN;
INSERT INTO users (name) VALUES ('Alice');
SAVEPOINT sp1;
INSERT INTO users (name) VALUES ('Bob');
ROLLBACK TO SAVEPOINT sp1; -- Bob's insert is undone
RELEASE SAVEPOINT sp1; -- destroys savepoint, merges state
COMMIT; -- only Alice is committed

Savepoint behavior matches PostgreSQL: duplicate names are allowed (most recent is targeted), RELEASE destroys the named savepoint and all later ones, ROLLBACK TO re-establishes the savepoint for reuse.

Transactions are pessimistic, and the default isolation level is READ COMMITTED.

LevelBehavior
READ COMMITTED (default)Statement-level snapshots, as in PostgreSQL — each statement sees rows committed before it started
READ UNCOMMITTEDBehaves as READ COMMITTED, as in PostgreSQL
REPEATABLE READOne snapshot held for the transaction’s lifetime, taken when the transaction starts (PostgreSQL takes it at the transaction’s first query)
SERIALIZABLENot implemented — downgraded to REPEATABLE READ

Under READ COMMITTED, a statement later in the transaction sees another session’s committed writes:

SQL
-- Session A -- Session B
BEGIN; -- default: READ COMMITTED
SELECT count(*) FROM t; -- 1
INSERT INTO t VALUES (2); -- commits
SELECT count(*) FROM t; -- 2
COMMIT;

Under REPEATABLE READ the same sequence returns 1 both times.

BEGIN READ ONLY, SET TRANSACTION READ ONLY, and SET default_transaction_read_only = on all enable read-only transaction enforcement. DML and DDL writes are rejected with SQLSTATE 25006:

SQL
BEGIN READ ONLY;
INSERT INTO t VALUES (1);
-- ERROR: cannot execute INSERT in a read-only transaction
-- SQLSTATE: 25006
ROLLBACK;

For credential-level write restrictions that apply across sessions, issue a token with a read-only scope (db9 token create --scope mydb:ro) — see CLI reference.

SELECT ... FOR UPDATE, FOR NO KEY UPDATE, FOR SHARE and FOR KEY SHARE, with or without NOWAIT or SKIP LOCKED, need a primary key on the table they lock. On a table without one they fail as soon as the query returns a row, where PostgreSQL locks the rows. A query that matches nothing returns no rows without an error, so test against data:

SQL
CREATE TABLE notes (id INT, body TEXT); -- no primary key
INSERT INTO notes VALUES (1, 'draft');
SELECT * FROM notes FOR UPDATE;
-- ERROR: FOR UPDATE/SHARE requires primary key
-- SQLSTATE: 0A000

A RANGE-partitioned table is the exception: locking through the parent works with or without a primary key, and another session’s NOWAIT and SKIP LOCKED see those locks. A partition without a primary key, queried directly, fails as above.

When a statement fails inside a transaction block, the transaction enters a failed state, and every later statement returns 25P02 (current transaction is aborted, commands ignored until end of transaction block). Only ROLLBACK (or ABORT), ROLLBACK TO SAVEPOINT, COMMIT and END are accepted. COMMIT and END roll the transaction back and report ROLLBACK. ROLLBACK TO SAVEPOINT undoes the work done after the savepoint and makes the transaction usable again. This matches PostgreSQL behavior, including after a serialization failure (40001) or deadlock (40P01) raised by a statement.


Bulk data import and export.

SQL
-- Import from file (via client)
COPY table_name FROM STDIN;
-- Import from Parquet (requires CREATE EXTENSION parquet)
COPY table_name FROM 'fs9:///data/file.parquet' WITH (FORMAT parquet);
-- Export a table (PostgreSQL wire protocol only)
COPY table_name TO STDOUT WITH (FORMAT csv, HEADER);

Text, CSV, and Parquet formats are supported for COPY FROM; FORMAT binary is rejected with 0A000. A Parquet source must be an http://, https:// or fs9:// URL — a bare path such as '/data/file.parquet' fails with unsupported Parquet URL scheme. See Parquet Import.

COPY ... TO writes only to STDOUT, only in the table form (COPY table [(columns)] TO STDOUT), and only over the PostgreSQL wire protocol: COPY (SELECT ...) TO STDOUT is rejected, a file target fails, and the HTTP SQL API cannot carry the stream. See Migrate from PostgreSQL.