Skip to content

Latest commit

 

History

History
232 lines (186 loc) · 9.23 KB

psycopg3.md

File metadata and controls

232 lines (186 loc) · 9.23 KB

PGAdapter - psycopg3 Connection Options

PGAdapter supports the Python psycopg3 driver.

Sample Application

See this sample application using psycopg3 for a Python sample application that embeds and starts PGAdapter automatically, and then connects to PGAdapter using psycopg3.

Usage

First start PGAdapter:

export GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json
docker pull gcr.io/cloud-spanner-pg-adapter/pgadapter
docker run \
  -d -p 5432:5432 \
  -v ${GOOGLE_APPLICATION_CREDENTIALS}:${GOOGLE_APPLICATION_CREDENTIALS}:ro \
  -e GOOGLE_APPLICATION_CREDENTIALS \
  gcr.io/cloud-spanner-pg-adapter/pgadapter \
  -p my-project -i my-instance \
  -x

Then connect to PGAdapter using TCP like this:

import psycopg

# Replace localhost and 5432 with the host and port number where PGAdapter is running.
# sslmode=disable instructs psycopg3 to try plain text mode directly instead of first trying SSL.
with psycopg.connect("host=localhost port=5432 dbname=my-database sslmode=disable") as conn:
  conn.autocommit = True
  with conn.cursor() as cur:
    cur.execute("select 'Hello world!' as hello")
    print("Greeting from Cloud Spanner PostgreSQL:", cur.fetchone()[0])

You can also connect to PGAdapter using Unix Domain Sockets if PGAdapter is running on the same host as the client application:

import psycopg

# '/tmp' is the default domain socket directory for PGAdapter. This can be changed using the -dir
# command line argument. 5432 is the default port number used by PGAdapter. Change this in the
# connection string if PGAdapter is running on a custom port.
with psycopg.connect("host=/tmp port=5432 dbname=my-database") as conn:
  conn.autocommit = True
  with conn.cursor() as cur:
    cur.execute("select 'Hello world!' as hello")
    print("Greeting from Cloud Spanner PostgreSQL:", cur.fetchone()[0])

Running PGAdapter

This example uses the pre-built Docker image to run PGAdapter. See README for more possibilities on how to run PGAdapter.

Limitations

Named Cursors

psycopg3 can be used to create server-side cursors. Server-side cursors are sometimes also referred to as 'named cursors'. This feature is currently not supported with PGAdapter.

Creating a server-side cursor in psycopg3 with PGAdapter will cause an error like the following:

psycopg.errors.RaiseException: Unknown statement: DECLARE "my_cursor" CURSOR FOR SELECT * FROM my_table WHERE my_column=$1

Nested Transactions

psycopg3 implements nested transactions using SAVEPOINT. Rolling back to a SAVEPOINT can fail if the transaction contained at least one query that called a volatile function or if the underlying data that has been accessed by the transaction has been modified by another transaction.

Performance Considerations

The following will give you the best possible performance when using psycopg3 with PGAdapter.

Unix Domain Sockets

Use Unix Domain Socket connections for the lowest possible latency when PGAdapter and the client application are running on the same host. psycopg3 uses libpq for the underlying connection. See https://www.postgresql.org/docs/current/libpq-connect.html#id-1.7.3.8.3.2 for more information on libpq connection string options.

PGAdapter uses /tmp as the default directory for domain sockets.

with psycopg.connect("host=/tmp port=5432 dbname=my-database") as conn:
  conn.execute("SELECT 1")

Autocommit or Read-Only Transactions

Use auto-commit or read-only transactions for workloads that only read data. Reading in a read/write transaction takes unnecessary locks if you are not planning on executing any write operations.

Autocommit

Set the Autocommit property of a Connection object to True to enable autocommit.

with psycopg.connect("host=localhost port=5432 dbname=my-database") as conn:
  conn.autocommit = True
  # The next command will not start a transaction.
  conn.execute("SELECT 1")

It can also be set directly when creating the connection:

with psycopg.connect("host=localhost port=5432 dbname=my-database", autocommit=True) as conn:
  # The next command will not start a transaction.
  conn.execute("SELECT 1")

Read-Only Transaction

Set the read_only property of the connection to True to instruct psycopg3 to create read-only transactions instead of read/write transactions. A read-only transaction will not take any locks. See the Cloud Spanner read-only transaction documentation for more information on read-only transactions.

NOTE: You must commit or rollback the read-only transaction in psycopg3 to end the transaction. There is no semantic or performance difference between committing or rolling back a read-only transaction.

with psycopg.connect("host=localhost port=5432 dbname=my-database") as conn:
  conn.read_only = True
  # The next command will start a read-only transaction.
  conn.execute("SELECT 1")
  conn.execute("SELECT 2")
  # The commit call will mark the end of the transaction. There is no semantic difference between
  # committing or rolling back a read-only transaction.
  conn.commit()

Stale Reads

Read-only transactions and connections using AUTOCOMMIT will by default use strong reads for queries. Cloud Spanner also supports stale reads. A strong read is a read at a current timestamp and is guaranteed to see all data that has been committed up until the start of this read. Cloud Spanner defaults to using strong reads to serve read requests.

A stale read is read at a timestamp in the past. If your application is latency sensitive but tolerant of stale data, then stale reads can provide performance benefits. See also https://cloud.google.com/spanner/docs/reads#read_types

You can create a connection that will use stale reads in autocommit mode by adding the following to the connection string:

with psycopg.connect("host=localhost port=5432 dbname=my-database options='-c spanner.read_only_staleness=\\'MAX_STALENESS\\ 10s\\''", autocommit=True) as conn:
  # This connection will read data that is up to 10 seconds stale.
  conn.execute("SELECT * FROM my_table WHERE key='my-key'")

Batching / Pipelining

Use batching / pipelining for optimal performance when executing multiple statements. This both saves round-trips between your application and PGAdapter and between PGAdapter and Cloud Spanner.

You can batch any type of statement. A batch can also contain a mix of different types of statements, such as both queries and DML statements. It is also possible (and recommended!) to batch DDL statements, but it is not recommended to mix DDL statements with other types of statements in one batch.

DML Batch Example

with psycopg.connect("host=localhost port=5432 dbname=my-database") as conn:
  with conn.pipeline():
    curs = conn.cursor()
    curs.execute("INSERT INTO my_table (key, value) values (%s, %s)", (1, 'One',))
    curs.execute("INSERT INTO my_table (key, value) values (%s, %s)", (2, 'Two',))
    curs.execute("update my_table set value='Zero' where key=%s", (0,))

Execute Many

psycopg3 implements the executemany method using pipelining. Prefer this method for executing the same statement multiple times with different parameter values, for example for inserting multiple rows into the same table.

with psycopg.connect("host=localhost port=5432 dbname=my-database") as conn:
  curs = conn.cursor()
  # executemany is automatically translated to Batch DML by PGAdapter.
  curs.executemany(
    "INSERT INTO my_table (key, value) values (%s, %s)",
    [(1, "One",), (2, "Two",), (4, "Four",)])
  print("Insert count:", curs.rowcount)

Note that it is recommended to use COPY for bulk insert operations.

DDL Batch Example

with psycopg.connect("host=localhost port=5432 dbname=my-database") as conn:
  # Note: Cloud Spanner does not support DDL transactions.
  # Set the connection to autocommit before executing any DDL statements.
  conn.autocommit = True
  curs = conn.cursor()
  with conn.pipeline():
    curs.execute("""
      create table singers (
        id         varchar not null primary key,
        version_id int not null,
        first_name varchar,
        last_name  varchar not null,
        full_name  varchar generated always as (coalesce(concat(first_name, ' '::varchar, last_name), last_name)) stored,
        active     boolean,
        created_at timestamptz,
        updated_at timestamptz
      )""")
    curs.execute("""
      create table albums (
        id               varchar not null primary key,
        version_id       int not null,
        title            varchar not null,
        marketing_budget numeric,
        release_date     date,
        cover_picture    bytea,
        singer_id        varchar not null,
        created_at       timestamptz,
        updated_at       timestamptz,
        constraint fk_albums_singers foreign key (singer_id) references singers (id)
      )""")