Back to Documentation

Key-Value Store

High-performance key-value operations with sub-millisecond latency

Overview

entropyDB provides optimized key-value storage for caching, session management, and high-throughput workloads. Achieve Redis-like performance with SQL consistency.

Performance Characteristics

  • Sub-millisecond GET operations
  • 100k+ ops/sec per core
  • TTL and automatic expiration
  • Transactional consistency

Creating Key-Value Tables

-- Simple key-value store
CREATE TABLE cache (
  key TEXT PRIMARY KEY,
  value BYTEA NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);

-- With TTL support
CREATE TABLE sessions (
  key TEXT PRIMARY KEY,
  value JSONB NOT NULL,
  expires_at TIMESTAMP NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);

-- Create index for automatic cleanup
CREATE INDEX idx_sessions_expires 
ON sessions (expires_at)
WHERE expires_at IS NOT NULL;

-- With metadata
CREATE TABLE feature_flags (
  key TEXT PRIMARY KEY,
  value JSONB NOT NULL,
  metadata JSONB,
  version INT DEFAULT 1,
  updated_at TIMESTAMP DEFAULT NOW()
);

Basic Operations

SET (Insert/Update)

-- Simple SET
INSERT INTO cache (key, value)
VALUES ('user:123', 'John Doe'::bytea)
ON CONFLICT (key) DO UPDATE 
SET value = EXCLUDED.value;

-- SET with TTL (expire in 1 hour)
INSERT INTO sessions (key, value, expires_at)
VALUES (
  'session:abc123',
  '{"user_id": 123, "role": "admin"}'::jsonb,
  NOW() + INTERVAL '1 hour'
)
ON CONFLICT (key) DO UPDATE 
SET value = EXCLUDED.value,
    expires_at = EXCLUDED.expires_at;

-- SET multiple keys
INSERT INTO cache (key, value)
VALUES 
  ('key1', 'value1'::bytea),
  ('key2', 'value2'::bytea),
  ('key3', 'value3'::bytea)
ON CONFLICT (key) DO UPDATE 
SET value = EXCLUDED.value;

GET (Retrieve)

-- Simple GET
SELECT value FROM cache WHERE key = 'user:123';

-- GET with expiration check
SELECT value 
FROM sessions 
WHERE key = 'session:abc123'
  AND (expires_at IS NULL OR expires_at > NOW());

-- GET multiple keys
SELECT key, value 
FROM cache 
WHERE key IN ('key1', 'key2', 'key3');

-- GET with pattern matching
SELECT key, value 
FROM cache 
WHERE key LIKE 'user:%';

DELETE

-- Delete single key
DELETE FROM cache WHERE key = 'user:123';

-- Delete multiple keys
DELETE FROM cache WHERE key IN ('key1', 'key2');

-- Delete by pattern
DELETE FROM cache WHERE key LIKE 'temp:%';

-- Delete expired entries
DELETE FROM sessions WHERE expires_at < NOW();

Advanced Operations

Atomic Increment

-- Increment counter
INSERT INTO cache (key, value)
VALUES ('counter:views', '1'::bytea)
ON CONFLICT (key) DO UPDATE
SET value = ((cache.value::text::int + 1)::text)::bytea;

-- JSONB counter
UPDATE sessions
SET value = jsonb_set(
  value,
  '{counter}',
  to_jsonb(coalesce((value->>'counter')::int, 0) + 1)
)
WHERE key = 'user:123:stats';

Conditional Operations

-- SET if not exists
INSERT INTO cache (key, value)
VALUES ('lock:resource', 'locked'::bytea)
ON CONFLICT (key) DO NOTHING
RETURNING *;

-- Compare and swap
UPDATE feature_flags
SET value = '{"enabled": true}'::jsonb,
    version = version + 1
WHERE key = 'new_ui' 
  AND version = 5  -- Only update if version matches
RETURNING *;

Batch Operations

-- Batch SET with transaction
BEGIN;
INSERT INTO cache (key, value)
SELECT 
  'user:' || id,
  name::bytea
FROM users
WHERE active = true
ON CONFLICT (key) DO UPDATE 
SET value = EXCLUDED.value;
COMMIT;

-- Batch GET with JSON aggregation
SELECT jsonb_object_agg(key, value) as all_values
FROM cache
WHERE key LIKE 'config:%';

TTL and Expiration

Automatic Cleanup

-- Create cleanup function
CREATE OR REPLACE FUNCTION cleanup_expired_keys()
RETURNS void AS $$
BEGIN
  DELETE FROM sessions WHERE expires_at < NOW();
END;
$$ LANGUAGE plpgsql;

-- Schedule periodic cleanup (using pg_cron extension)
SELECT cron.schedule(
  'cleanup-expired-sessions',
  '*/5 * * * *',  -- Every 5 minutes
  'SELECT cleanup_expired_keys()'
);

Refresh TTL

-- Extend expiration on access
UPDATE sessions
SET expires_at = NOW() + INTERVAL '1 hour'
WHERE key = 'session:abc123'
  AND expires_at > NOW()
RETURNING value;

Performance Optimization

Indexing Strategy

-- Primary key is automatically indexed
-- For pattern queries, use prefix indexes
CREATE INDEX idx_cache_key_prefix 
ON cache (key text_pattern_ops);

-- For range scans
CREATE INDEX idx_cache_created 
ON cache (created_at DESC);

-- Partial index for active entries
CREATE INDEX idx_sessions_active 
ON sessions (key)
WHERE expires_at > NOW();

Connection Pooling

-- Use prepared statements for repeated queries
PREPARE get_key (text) AS
SELECT value FROM cache WHERE key = $1;

EXECUTE get_key('user:123');

-- Use connection pooler (PgBouncer recommended)
-- Configure in session mode for KV workloads

Common Use Cases

Session Storage

Store user sessions with automatic expiration

INSERT INTO sessions (key, value, expires_at)
VALUES ('session:' || gen_random_uuid(),
        '{"user_id": 123}'::jsonb,
        NOW() + INTERVAL '24 hours');

Rate Limiting

Track API usage per user/IP

-- Increment request count
UPDATE cache
SET value = ((value::text::int + 1)::text)::bytea
WHERE key = 'rate:user:123:' || 
            to_char(NOW(), 'YYYY-MM-DD-HH24');

Feature Flags

Dynamic configuration management

SELECT value FROM feature_flags
WHERE key = 'enable_beta_features';

Distributed Locking

Coordinate access to shared resources

INSERT INTO cache (key, value, expires_at)
VALUES ('lock:job:123', 'worker-1'::bytea,
        NOW() + INTERVAL '5 minutes')
ON CONFLICT (key) DO NOTHING;

Best Practices

Use Appropriate Data Types

BYTEA for binary data, JSONB for structured data, TEXT for strings.

Set Reasonable TTLs

Always use expiration for cache and session data to prevent unbounded growth.

Use Prepared Statements

Reduce parsing overhead for high-throughput workloads.

Monitor Table Size

Set up alerts for rapid growth and schedule periodic VACUUM operations.

Consider Partitioning

For very large KV stores, partition by key range or time.

Next Steps