cockroachdb / cockroachdb/cockroach

`select for update` with `skip locked` is slow

Open
#97,135 6 comments 2 reactions 0 assignees View on GitHub
C-bug O-community O-support P-3 T-kv X-blathers-triaged
Dominant language
Go
Stars
32.5k
Forks
4.1k
PR merge metrics
PR metrics pending

Description

**Describe the problem**

`select for update` with `skip locked` performs slowly when there is a high number of concurrent queries.

**To Reproduce**

To make the issue easier to reproduce, I am posting a Go example.

In this example, I am trying to model the following situation: there is a set of items, and a set of workers.
Each worker can claim an item, and each item can be claimed by only one worker at a time. Once a worker claims
an item, the claim expires after a fixed amount of time (e.g., 5 minutes). After the claim for a given item expires, other workers can claim that item.

The `items` table models the items. In order to claim an item, each worker a `select for update ... skip locked` query followed by an `update` query, in one transaction.

The sample code is below. It creates 1000 items and spins up 500 workers.

```go
package main

import (
"context"
"fmt"
"sync"
"time"

"github.com/jackc/pgx/v4/pgxpool"
)

func createTable(ctx context.Context, dbpool *pgxpool.Pool) error {
createTableStmt := `
create table if not exists items (
item_id int64 primary key,
worker_id int64 null,
expires_at timestamp not null default timestamp 'epoch'
);

create index on items ("expires_at");
`
if _, err := dbpool.Exec(ctx, createTableStmt); err != nil {
return fmt.Errorf("create tables: %w", err)
}
return nil
}

func insertItems(ctx context.Context, dbpool *pgxpool.Pool) error {
_, err := dbpool.Exec(ctx, `delete from items`)
if err != nil {
return err
}

// Insert rows into items table.
c := 1000
var wg sync.WaitGroup
wg.Add(c)
for i := 0; i < c; i++ {
i := i
go func() {
defer wg.Done()
stmt := `insert into items(item_id) values ($1)`
_, err := dbpool.Exec(ctx, stmt, i)
if err != nil {
panic(err)
}
}()
}
wg.Wait()
return nil
}

func main() {
pgURL := "postgresql://root@0.0.0.0:26257/testdb?sslmode=disable&pool_max_conns=200"
var dbpool *pgxpool.Pool
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

pgPoolConfig, err := pgxpool.ParseConfig(pgURL)
if err != nil {
panic(err)
}
pgPoolConfig.MaxConns = int32(200)

dbpool, err = pgxpool.ConnectConfig(ctx, pgPoolConfig)
if err != nil {
panic(err)
}

if err := createTable(ctx, dbpool); err != nil {
panic(err)
}

// Clear items table and insert items.
start := time.Now()
if err := insertItems(ctx, dbpool); err != nil {
panic(err)
}
fmt.Println("Inserted items in", time.Since(start))

start = time.Now()

var wg sync.WaitGroup

// Launch concurrent workers. Each worker will claim one item.
numWorkers := 500
wg.Add(numWorkers)
for i := 0; i < numWorkers; i++ {
i := i
go func() {
defer wg.Done()
tx, err := dbpool.Begin(ctx)
if err != nil {
panic(err)
}
defer tx.Rollback(ctx)

var tid int64
var t time.Time

if err := tx.QueryRow(ctx, `
select
item_id, expires_at
from items
where expires_at < $1
order by expires_at asc
limit 1
for update
skip locked`, time.Now().UTC()).Scan(&tid, &t); err != nil {
panic(err)
}

_, err = tx.Exec(ctx, `update items set worker_id=$1, expires_at=$2 where item_id=$3`, i, time.Now().UTC().Add(5*time.Minute), tid)
if err != nil {
panic(err)
}

if err := tx.Commit(ctx); err != nil {
panic(err)
}
}()
}
wg.Wait()
fmt.Println("Workers took", time.Since(start))
}
```

**Observed behavior:**

Running the above example, the output in my local machine was the following:

```
Inserted items in 277.690375ms
Workers took 2.082947129s
```

Additionally, the CockroachDB WebUI's "SQL Activity" tab shows the following:
* The mean latency for the `select for update ... skip locked` query is 311.6 ms;
* The mean latency for the `update` query is 306.6 ms.

**Expected behavior:**

I would have expected a lower mean latency, in the order of tens of milliseconds rather than hundreds of milliseconds.

**Environment:**
- CockroachDB version: 22.2.3.
- Server OS: WSL2 on Windows 10.
- RAM: 32 GB.
- Client app: Golang package `github.com/jackc/pgx/v4` v4.18.0 .
- Go version: 1.19.

Jira issue: CRDB-24536

Epic CRDB-65887

Contributor guide

Open the contributing guide

Research direction

Start by running the Go reproducer from the issue against CockroachDB and compare the SELECT FOR UPDATE SKIP LOCKED and UPDATE latencies in SQL Activity. Then trace the database execution path for these concurrent queries. Done means identifying and addressing the cause of the high latency, with the reproducer showing substantially lower timings.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, sql
Domain
databases, performance
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.