feat: add golang-database skill with best practices and reference documentation
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
# Database Performance
|
||||
|
||||
## Connection Pool Sizing
|
||||
|
||||
### Configuration
|
||||
|
||||
```go
|
||||
db, err := sqlx.Connect("postgres", dsn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to database: %w", err)
|
||||
}
|
||||
|
||||
db.SetMaxOpenConns(25) // total connections (match your DB capacity)
|
||||
db.SetMaxIdleConns(10) // keep connections warm, reduce handshake overhead
|
||||
db.SetConnMaxLifetime(5 * time.Minute) // recycle connections (DNS changes, server restarts)
|
||||
db.SetConnMaxIdleTime(1 * time.Minute) // release idle connections back to the pool
|
||||
```
|
||||
|
||||
| Setting | Too low | Too high |
|
||||
| --- | --- | --- |
|
||||
| `MaxOpenConns` | Requests queue waiting for conn | DB overwhelmed, context switches |
|
||||
| `MaxIdleConns` | Cold connections, slow queries | Wasted memory holding idle conns |
|
||||
| `ConnMaxLifetime` | Frequent reconnection overhead | Stale connections after failover |
|
||||
| `ConnMaxIdleTime` | Same as MaxIdleConns too low | Idle conns consume server memory |
|
||||
|
||||
### Monitoring
|
||||
|
||||
Check pool stats in production to detect exhaustion:
|
||||
|
||||
```go
|
||||
stats := db.Stats()
|
||||
slog.Info("db pool",
|
||||
"open", stats.OpenConnections,
|
||||
"in_use", stats.InUse,
|
||||
"idle", stats.Idle,
|
||||
"wait_count", stats.WaitCount, // total waits for a connection
|
||||
"wait_duration", stats.WaitDuration, // total wait time
|
||||
)
|
||||
```
|
||||
|
||||
If `WaitCount` keeps climbing, increase `MaxOpenConns` or optimize slow queries.
|
||||
|
||||
### Prometheus Metrics
|
||||
|
||||
Use a custom Prometheus collector to export pool metrics on-demand (scales to multiple pools automatically):
|
||||
|
||||
```go
|
||||
type DBCollector struct {
|
||||
pools map[string]*sqlx.DB
|
||||
}
|
||||
|
||||
func NewDBCollector(pools map[string]*sqlx.DB) *DBCollector {
|
||||
return &DBCollector{pools: pools}
|
||||
}
|
||||
|
||||
func (c *DBCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- prometheus.NewDesc("db_open_connections", "Number of open connections", []string{"pool"}, nil)
|
||||
ch <- prometheus.NewDesc("db_in_use_connections", "Connections currently in use", []string{"pool"}, nil)
|
||||
ch <- prometheus.NewDesc("db_idle_connections", "Idle connections in pool", []string{"pool"}, nil)
|
||||
ch <- prometheus.NewDesc("db_total_latency_seconds", "Total latency for a connection", []string{"pool"}, nil)
|
||||
}
|
||||
|
||||
func (c *DBCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
for poolName, db := range c.pools {
|
||||
stats := db.Stats()
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
prometheus.NewDesc("db_open_connections", "Number of open connections", []string{"pool"}, nil),
|
||||
prometheus.GaugeValue, float64(stats.OpenConnections), poolName)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
prometheus.NewDesc("db_in_use_connections", "Connections currently in use", []string{"pool"}, nil),
|
||||
prometheus.GaugeValue, float64(stats.InUse), poolName)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
prometheus.NewDesc("db_idle_connections", "Idle connections in pool", []string{"pool"}, nil),
|
||||
prometheus.GaugeValue, float64(stats.Idle), poolName)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
prometheus.NewDesc("db_total_latency_seconds", "Total latency for a connection", []string{"pool"}, nil),
|
||||
prometheus.CounterValue, float64(stats.LatencyCount), poolName)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
pools := map[string]*sqlx.DB{
|
||||
"primary": mainDB,
|
||||
"replica": replicaDB,
|
||||
}
|
||||
prometheus.MustRegister(NewDBCollector(pools))
|
||||
}
|
||||
```
|
||||
|
||||
**Collector advantages:**
|
||||
|
||||
- Metrics are collected on-demand during scrapes (no background goroutine)
|
||||
- Always returns current state (no stale data between scrapes)
|
||||
- Scales to multiple pools automatically
|
||||
- Lower memory footprint (no metric state in memory)
|
||||
|
||||
**Alert thresholds:**
|
||||
|
||||
- Open connections approaching `MaxOpenConns` → risk of request queuing
|
||||
- Wait count climbing steadily → pool is exhausted, increase `MaxOpenConns`
|
||||
- Idle connections too high → reduce `MaxIdleConns` or lower `ConnMaxIdleTime`
|
||||
|
||||
## Batch Processing
|
||||
|
||||
Avoid two extremes:
|
||||
|
||||
- **Row-by-row** — N round trips for N rows, extremely slow
|
||||
- **One giant batch** — locks tables, consumes memory, can timeout and block other queries
|
||||
|
||||
### Sweet spot: 100–1,000 rows per batch
|
||||
|
||||
Adjust based on row size and database load. Larger rows → smaller batches.
|
||||
|
||||
### Batch INSERT with sqlx
|
||||
|
||||
```go
|
||||
func insertUsersBatch(ctx context.Context, db *sqlx.DB, users []User) error {
|
||||
const batchSize = 500
|
||||
for i := 0; i < len(users); i += batchSize {
|
||||
end := min(i+batchSize, len(users))
|
||||
batch := users[i:end]
|
||||
|
||||
_, err := db.NamedExecContext(ctx, `INSERT INTO users (name, email) VALUES (:name, :email)`, batch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inserting users batch %d-%d: %w", i, end, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Bulk INSERT with pgx (PostgreSQL COPY protocol)
|
||||
|
||||
For maximum throughput on PostgreSQL, use `pgx.CopyFrom` which uses the binary COPY protocol — significantly faster than multi-row INSERT:
|
||||
|
||||
```go
|
||||
rows := make([][]any, len(users))
|
||||
for i, u := range users {
|
||||
rows[i] = []any{u.Name, u.Email}
|
||||
}
|
||||
_, err := pool.CopyFrom(ctx,
|
||||
pgx.Identifier{"users"},
|
||||
[]string{"name", "email"},
|
||||
pgx.CopyFromRows(rows),
|
||||
)
|
||||
```
|
||||
|
||||
### Cursor-based pagination (avoid OFFSET)
|
||||
|
||||
For reading large datasets, use cursor-based pagination instead of `OFFSET`. OFFSET re-scans skipped rows, getting slower as you paginate deeper:
|
||||
|
||||
```go
|
||||
// ✗ Bad — OFFSET re-scans rows, O(offset + limit)
|
||||
SELECT * FROM events ORDER BY created_at LIMIT 100 OFFSET 10000
|
||||
|
||||
// ✓ Good — cursor-based, O(limit) regardless of depth
|
||||
SELECT * FROM events WHERE created_at > $1 ORDER BY created_at LIMIT 100
|
||||
```
|
||||
|
||||
## Indexing Strategy
|
||||
|
||||
**Never create or drop indexes yourself.** Index changes affect production query performance and write throughput. Always suggest to the developer and let them decide.
|
||||
|
||||
### Use SQL MCP to check existing indexes
|
||||
|
||||
When a SQL MCP tool is available, query the database to check existing indexes before suggesting new ones:
|
||||
|
||||
```sql
|
||||
-- PostgreSQL: list indexes on a table
|
||||
SELECT indexname, indexdef
|
||||
FROM pg_indexes
|
||||
WHERE tablename = 'users';
|
||||
|
||||
-- Check for unused indexes (low scan count relative to writes)
|
||||
SELECT schemaname, relname, indexrelname, idx_scan, idx_tup_read
|
||||
FROM pg_stat_user_indexes
|
||||
WHERE idx_scan < 10
|
||||
ORDER BY idx_scan;
|
||||
```
|
||||
|
||||
### When to suggest adding indexes
|
||||
|
||||
- Foreign key columns (PostgreSQL does NOT auto-index foreign keys)
|
||||
- Columns frequently used in `WHERE`, `JOIN`, or `ORDER BY`
|
||||
- Composite indexes for multi-column queries (leftmost column is most selective)
|
||||
- Partial indexes for filtered queries (`WHERE active = true`)
|
||||
|
||||
### When to suggest removing indexes
|
||||
|
||||
- Indexes with near-zero `idx_scan` count (nobody reads them)
|
||||
- Duplicate indexes (same columns in same order)
|
||||
- Indexes on write-heavy tables that slow down INSERT/UPDATE/DELETE
|
||||
- Wide composite indexes where a narrower one would suffice
|
||||
|
||||
Always present findings as suggestions with data (scan counts, table size), never execute DDL yourself.
|
||||
|
||||
## Query Performance Tips
|
||||
|
||||
- **`EXPLAIN ANALYZE`** before optimizing — measure, don't guess
|
||||
- **List columns explicitly** — avoid `SELECT *`, it fetches unnecessary data and breaks struct scanning when schema changes
|
||||
- **Use `LIMIT`** for pagination, always with an `ORDER BY`
|
||||
- **Prefer `EXISTS` over `COUNT`** for existence checks — `EXISTS` stops at the first match
|
||||
- **Avoid N+1 queries** — use `JOIN` or batch `WHERE id IN (...)` instead of querying in a loop
|
||||
- **Suggest improvements, never execute them** — performance changes (indexes, query rewrites, configuration) need human review in context of production data and workload patterns
|
||||
|
||||
Batch operations SHOULD use 100–1,000 rows per batch — adjust based on row size and database load. Cursor-based pagination MUST replace `OFFSET` for large datasets — the cursor column MUST be chosen based on actual indexes (e.g., `created_at`, `user_id`). NEVER create indexes blindly — check existing indexes, measure with `EXPLAIN ANALYZE`, and present findings as suggestions. N+1 queries MUST be eliminated — use `JOIN` or batch `WHERE id IN (...)`.
|
||||
|
||||
→ See `samber/cc-skills-golang@golang-observability` skill for database metrics and query monitoring. → See `samber/cc-skills@promql-cli` skill for querying pool metrics (`db_open_connections`, `db_in_use_connections`, `db_idle_connections`) via CLI.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Struct Scanning and NULLable Columns
|
||||
|
||||
## Struct Scanning with sqlx
|
||||
|
||||
Tag struct fields with `db:"column_name"` for sqlx:
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
ID int64 `db:"id"`
|
||||
Name string `db:"name"`
|
||||
Email string `db:"email"`
|
||||
DeletedAt *time.Time `db:"deleted_at"` // NULLable
|
||||
}
|
||||
|
||||
// Single row
|
||||
var user User
|
||||
err := db.GetContext(ctx, &user, "SELECT id, name, email, deleted_at FROM users WHERE id = $1", id)
|
||||
|
||||
// Multiple rows
|
||||
var users []User
|
||||
err := db.SelectContext(ctx, &users, "SELECT id, name, email, deleted_at FROM users WHERE active = true")
|
||||
```
|
||||
|
||||
## Struct Scanning with pgx
|
||||
|
||||
With pgx (v5+), use `pgx.CollectRows` for automatic struct mapping:
|
||||
|
||||
```go
|
||||
rows, err := pool.Query(ctx, "SELECT id, name, email FROM users WHERE active = true")
|
||||
if err != nil {
|
||||
return fmt.Errorf("querying users: %w", err)
|
||||
}
|
||||
users, err := pgx.CollectRows(rows, pgx.RowToStructByName[User])
|
||||
```
|
||||
|
||||
## JSON Marshaling
|
||||
|
||||
Struct tags for both database and JSON work together. Pointer fields marshal to `null` in JSON when NULL in the database:
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
ID int64 `db:"id" json:"id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Email string `db:"email" json:"email"`
|
||||
Bio *string `db:"bio" json:"bio,omitempty"` // NULL → omitted in JSON
|
||||
DeletedAt *time.Time `db:"deleted_at" json:"deleted_at"` // NULL → null in JSON
|
||||
}
|
||||
```
|
||||
|
||||
## NULLable Columns
|
||||
|
||||
Three approaches, from most to least recommended:
|
||||
|
||||
**1. Pointer fields (recommended)** — clean, works with JSON marshaling:
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
ID int64 `db:"id" json:"id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
DeletedAt *time.Time `db:"deleted_at" json:"deleted_at"` // nil when NULL
|
||||
}
|
||||
// Check: if user.DeletedAt != nil { ... }
|
||||
```
|
||||
|
||||
**2. `sql.NullXxx` types** or `sql.Null[T]` generic — explicit but verbose, requires custom JSON marshaling:
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
ID int64 `db:"id"`
|
||||
Bio sql.NullString `db:"bio"`
|
||||
}
|
||||
// Check: if user.Bio.Valid { use(user.Bio.String) }
|
||||
```
|
||||
|
||||
**3. `COALESCE` in SQL** — moves NULL handling to the query:
|
||||
|
||||
```sql
|
||||
SELECT id, COALESCE(bio, '') AS bio FROM users WHERE id = $1
|
||||
```
|
||||
@@ -0,0 +1,209 @@
|
||||
# Testing Database Code
|
||||
|
||||
## Unit Tests with Mocks
|
||||
|
||||
Define a repository interface so business logic can be tested without a database. Mock the interface with `testify/mock`:
|
||||
|
||||
```go
|
||||
// Repository interface — the contract
|
||||
type UserRepository interface {
|
||||
GetByID(ctx context.Context, id int64) (*User, bool, error)
|
||||
Create(ctx context.Context, user *User) error
|
||||
}
|
||||
|
||||
// Production implementation
|
||||
type pgUserRepository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func (r *pgUserRepository) GetByID(ctx context.Context, id int64) (*User, bool, error) {
|
||||
var user User
|
||||
err := r.db.GetContext(ctx, &user, "SELECT id, name, email FROM users WHERE id = $1", id)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, false, nil
|
||||
}
|
||||
return nil, false, fmt.Errorf("querying user %d: %w", id, err)
|
||||
}
|
||||
return &user, true, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Mock for service-layer tests
|
||||
|
||||
```go
|
||||
type mockUserRepo struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockUserRepo) GetByID(ctx context.Context, id int64) (*User, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*User), args.Error(1)
|
||||
}
|
||||
|
||||
func TestUserService_GetUser(t *testing.T) {
|
||||
repo := new(mockUserRepo)
|
||||
svc := NewUserService(repo)
|
||||
|
||||
expected := &User{ID: 1, Name: "Alice", Email: "alice@example.com"}
|
||||
repo.On("GetByID", mock.Anything, int64(1)).Return(expected, nil)
|
||||
|
||||
user, err := svc.GetUser(context.Background(), 1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, user)
|
||||
repo.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestUserService_GetUser_NotFound(t *testing.T) {
|
||||
repo := new(mockUserRepo)
|
||||
svc := NewUserService(repo)
|
||||
|
||||
repo.On("GetByID", mock.Anything, int64(999)).Return(nil, ErrUserNotFound)
|
||||
|
||||
user, err := svc.GetUser(context.Background(), 999)
|
||||
assert.Nil(t, user)
|
||||
assert.ErrorIs(t, err, ErrUserNotFound)
|
||||
}
|
||||
```
|
||||
|
||||
Unit tests verify business logic, not SQL correctness. They run fast and without external dependencies.
|
||||
|
||||
## sqlmock for Query-Level Testing
|
||||
|
||||
When you need to verify exact SQL without a real database, use [DATA-DOG/go-sqlmock](https://github.com/DATA-DOG/go-sqlmock):
|
||||
|
||||
```go
|
||||
func TestGetByID_sqlmock(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
sqlxDB := sqlx.NewDb(db, "postgres")
|
||||
repo := &pgUserRepository{db: sqlxDB}
|
||||
|
||||
rows := sqlmock.NewRows([]string{"id", "name", "email"}).
|
||||
AddRow(1, "Alice", "alice@example.com")
|
||||
mock.ExpectQuery("SELECT id, name, email FROM users WHERE id = \\$1").
|
||||
WithArgs(1).
|
||||
WillReturnRows(rows)
|
||||
|
||||
user, err := repo.GetByID(context.Background(), 1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Alice", user.Name)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
```
|
||||
|
||||
sqlmock is useful for verifying query structure and error handling paths, but it does not validate that your SQL is correct against a real database schema.
|
||||
|
||||
## Integration Tests
|
||||
|
||||
Integration tests run against a real database. Gate them with build tags so `go test ./...` skips them by default:
|
||||
|
||||
```go
|
||||
//go:build integration
|
||||
|
||||
package repository_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type UserRepoSuite struct {
|
||||
suite.Suite
|
||||
db *sqlx.DB
|
||||
tx *sqlx.Tx
|
||||
}
|
||||
|
||||
func (s *UserRepoSuite) SetupSuite() {
|
||||
dsn := os.Getenv("TEST_DATABASE_URL") // e.g., postgres://test:test@localhost:5432/testdb?sslmode=disable
|
||||
db, err := sqlx.Connect("postgres", dsn)
|
||||
s.Require().NoError(err)
|
||||
s.db = db
|
||||
// Run migrations here if needed
|
||||
}
|
||||
|
||||
func (s *UserRepoSuite) TearDownSuite() {
|
||||
s.db.Close()
|
||||
}
|
||||
|
||||
func (s *UserRepoSuite) SetupTest() {
|
||||
tx, err := s.db.Beginx()
|
||||
s.Require().NoError(err)
|
||||
s.tx = tx
|
||||
}
|
||||
|
||||
func (s *UserRepoSuite) TearDownTest() {
|
||||
s.tx.Rollback() // rolls back all changes — each test starts clean
|
||||
}
|
||||
|
||||
func (s *UserRepoSuite) TestCreateAndGet() {
|
||||
repo := NewUserRepository(s.tx)
|
||||
user := &User{Name: "Alice", Email: "alice@example.com"}
|
||||
|
||||
err := repo.Create(context.Background(), user)
|
||||
s.Require().NoError(err)
|
||||
s.NotZero(user.ID)
|
||||
|
||||
got, err := repo.GetByID(context.Background(), user.ID)
|
||||
s.Require().NoError(err)
|
||||
s.Equal("Alice", got.Name)
|
||||
}
|
||||
|
||||
func TestUserRepoSuite(t *testing.T) {
|
||||
suite.Run(t, new(UserRepoSuite))
|
||||
}
|
||||
```
|
||||
|
||||
Run integration tests:
|
||||
|
||||
```bash
|
||||
go test -tags=integration -v ./internal/repository/...
|
||||
```
|
||||
|
||||
### Test database with testcontainers-go
|
||||
|
||||
For CI environments without a pre-existing database:
|
||||
|
||||
```go
|
||||
func (s *UserRepoSuite) SetupSuite() {
|
||||
ctx := context.Background()
|
||||
container, err := postgres.Run(ctx, "postgres:16-alpine",
|
||||
postgres.WithDatabase("testdb"),
|
||||
postgres.WithUsername("test"),
|
||||
postgres.WithPassword("test"),
|
||||
testcontainers.WithWaitStrategy(
|
||||
wait.ForLog("database system is ready to accept connections").
|
||||
WithOccurrence(2).
|
||||
WithStartupTimeout(30*time.Second),
|
||||
),
|
||||
)
|
||||
s.Require().NoError(err)
|
||||
s.container = container
|
||||
|
||||
connStr, err := container.ConnectionString(ctx, "sslmode=disable")
|
||||
s.Require().NoError(err)
|
||||
s.db, err = sqlx.Connect("postgres", connStr)
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
```
|
||||
|
||||
## What to Test
|
||||
|
||||
| What | Unit test (mock) | Integration test |
|
||||
| ------------------------- | :--------------: | :--------------: |
|
||||
| Business logic | ✓ | |
|
||||
| SQL correctness | | ✓ |
|
||||
| Error paths (not found) | ✓ | ✓ |
|
||||
| Transaction boundaries | | ✓ |
|
||||
| NULL handling round-trips | | ✓ |
|
||||
| Constraint violations | | ✓ |
|
||||
| Query performance | | ✓ (with EXPLAIN) |
|
||||
|
||||
Unit tests MUST use mocks (interface mocks or sqlmock) — no real database connections. Integration tests MUST use build tags (`//go:build integration`) to separate from unit tests. Integration tests SHOULD use testcontainers-go for reproducible database environments in CI. NEVER test against production databases.
|
||||
|
||||
→ See `samber/cc-skills-golang@golang-testing` skill for general test patterns and CI configuration.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Transactions, Isolation Levels, and Locking
|
||||
|
||||
## Basic transaction pattern
|
||||
|
||||
```go
|
||||
tx, err := db.BeginTxx(ctx, nil) // default isolation (READ COMMITTED)
|
||||
if err != nil {
|
||||
return fmt.Errorf("beginning transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback() // no-op if already committed
|
||||
|
||||
// ... execute queries using tx ...
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("committing transaction: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
## Custom isolation level
|
||||
|
||||
```go
|
||||
tx, err := db.BeginTxx(ctx, &sql.TxOptions{
|
||||
Isolation: sql.LevelSerializable, // strongest guarantee
|
||||
})
|
||||
```
|
||||
|
||||
| Level | Use when |
|
||||
| --- | --- |
|
||||
| `LevelReadCommitted` | Default — good for most operations |
|
||||
| `LevelRepeatableRead` | Need consistent reads within a transaction |
|
||||
| `LevelSerializable` | Financial operations, inventory, anything with strict consistency |
|
||||
|
||||
## SELECT FOR UPDATE — prevent race conditions
|
||||
|
||||
```go
|
||||
var balance int
|
||||
err := tx.GetContext(ctx, &balance, "SELECT balance FROM accounts WHERE id = $1 FOR UPDATE", accountID)
|
||||
// Row is locked until tx.Commit() or tx.Rollback()
|
||||
```
|
||||
|
||||
Use `FOR UPDATE` when you read a value, compute something from it, and then write it back. Without the lock, concurrent transactions can read stale data.
|
||||
|
||||
## Locking variants
|
||||
|
||||
| Clause | Effect |
|
||||
| --- | --- |
|
||||
| `FOR UPDATE` | Locks rows for write — other transactions block on same rows |
|
||||
| `FOR UPDATE NOWAIT` | Same, but fails immediately instead of waiting |
|
||||
| `FOR SHARE` | Locks rows for read — prevents writes but allows other reads |
|
||||
Reference in New Issue
Block a user