# Performance Tuning Guide

Comprehensive guide to optimizing Biscuit index performance for maximum query speed.

---

## Quick Performance Checklist

Before diving deep, verify these essentials:

-  Index is actually being used (`EXPLAIN ANALYZE`)
-  Patterns have concrete characters (not just `%`)
-  PostgreSQL memory settings are appropriate
-  Statistics are up to date (`ANALYZE`)
-  No competing indexes interfering

---

## Understanding Biscuit Performance


### Active Optimizations

Biscuit includes 12 built-in optimizations:

1. **Skip wildcard intersections** - `_` wildcards don't create bitmaps
2. **Early termination** - Stop when no candidates remain
3. **Avoid redundant copies** - Reuse bitmaps when possible
4. **Single-part fast path** - Optimized prefix/suffix/exact
5. **Skip unnecessary length ops** - Length filters only when needed
6. **TID sorting** - Sequential I/O for large result sets
7. **Batch TID insertion** - Efficient bitmap scan support
8. **Direct bitmap iteration** - No intermediate arrays
9. **Parallel bitmap heap scan** - Multi-worker heap access above a serial index scan
10. **Batch cleanup** - Tombstone removal at thresholds
11. **Skip sorting for aggregates** - COUNT(*) doesn't need order
12. **LIMIT-aware collection** - Early termination for small limits

---

## PostgreSQL Configuration

### Memory Settings

```ini
# postgresql.conf

# Shared memory for all connections
shared_buffers = 4GB           # 25% of RAM (for caching)

# Per-query work memory
work_mem = 256MB               # For sorting/hash ops

# Index build memory
maintenance_work_mem = 1GB     # For CREATE INDEX

# Effective cache size (helps planner)
effective_cache_size = 12GB    # 75% of RAM
```

**Reload config**:
```sql
SELECT pg_reload_conf();
```

---

### Planner Settings

```sql
-- Ensure Biscuit index usage
SET enable_seqscan = off;  -- Force index use (testing only!)

-- Cost settings (fine-tuning)
SET random_page_cost = 1.1;  -- For SSDs
SET cpu_tuple_cost = 0.01;

-- Parallel query settings
SET max_parallel_workers_per_gather = 4;
SET parallel_setup_cost = 100;
```

---

## Index Design Optimization

### Choose Selective Columns

```sql
-- Analyze column selectivity
SELECT 
    COUNT(DISTINCT name) * 1.0 / COUNT(*) as name_selectivity,
    COUNT(DISTINCT category) * 1.0 / COUNT(*) as category_selectivity,
    COUNT(DISTINCT sku) * 1.0 / COUNT(*) as sku_selectivity
FROM products;

-- High selectivity (>0.5) = excellent for Biscuit
-- Low selectivity (<0.1) = consider other approaches
```

**Rule**: Index high-cardinality columns (many unique values)

---

### Multi-Column Order Strategy

```sql
-- ✅ GOOD: High to low selectivity
CREATE INDEX idx_products ON products 
USING biscuit (
    sku,        -- Unique (selectivity ~1.0)
    name,       -- High variety (selectivity ~0.8)
    category    -- Few values (selectivity ~0.1)
);

-- Biscuit will reorder predicates automatically,
-- but this order makes the index structure clearer
```

---

### Avoid Over-Indexing

```sql
-- ❌ BAD: Redundant indexes
CREATE INDEX idx1 ON products USING biscuit (name);
CREATE INDEX idx2 ON products USING biscuit (name, sku);
-- idx1 is completely redundant!

-- ✅ GOOD: Single comprehensive index
CREATE INDEX idx_products ON products 
USING biscuit (name, sku, category);
```

**Benefit**: Reduces maintenance overhead, saves memory

---

## Query Optimization

### Pattern Design

#### Maximize Concrete Characters

```sql
-- ❌ SLOW: Too generic (matches 90% of data)
WHERE name LIKE '%e%'

-- ✅ FAST: More specific (matches 5% of data)
WHERE name LIKE '%wireless mouse%'

-- ✅✅ FASTEST: Very specific (matches <1%)
WHERE name LIKE '%Dell Wireless USB Mouse Pro%'
```

**Benchmark**:
```sql
-- Test different pattern selectivity
EXPLAIN ANALYZE SELECT COUNT(*) FROM products WHERE name LIKE '%e%';
EXPLAIN ANALYZE SELECT COUNT(*) FROM products WHERE name LIKE '%wireless%';
EXPLAIN ANALYZE SELECT COUNT(*) FROM products WHERE name LIKE '%wireless mouse%';
```

---

#### Use Strong Anchors

```sql
-- ❌ WEAKEST: No anchor
WHERE email LIKE '%@%'

-- ✅ BETTER: Suffix anchor  
WHERE email LIKE '%@company.com'

-- ✅✅ BEST: Both anchors
WHERE email LIKE 'admin%@company.com'
```

---

#### Minimize Pattern Complexity

```sql
-- ❌ COMPLEX: 4 partitions
WHERE message LIKE '%ERROR%timeout%connection%retry%'

-- ✅ SIMPLER: Combine into fewer parts
WHERE message LIKE '%ERROR timeout connection%'
```

---

### Combine with Other Filters

```sql
-- Use multiple index types together
SELECT * FROM orders 
WHERE created_at > '2024-01-01'      -- B-tree index
  AND status = 'pending'              -- B-tree index
  AND customer_name LIKE '%Smith%';   -- Biscuit index

-- PostgreSQL combines all three efficiently
```

---

### Aggregate Query Optimization

Biscuit has special optimizations for aggregates:

```sql
-- ✅ OPTIMIZED: No tuple fetching, no sorting
SELECT COUNT(*) FROM products WHERE name LIKE '%laptop%';

-- ✅ OPTIMIZED: Bitmap-only operation
SELECT EXISTS(SELECT 1 FROM products WHERE name LIKE '%laptop%');

-- ✅ OPTIMIZED: Grouped aggregates
SELECT category, COUNT(*) 
FROM products 
WHERE name LIKE '%wireless%'
GROUP BY category;
```

**Performance**: 2-5x faster than regular queries!

---

### LIMIT Query Optimization

```sql
-- ✅ OPTIMIZED: Early termination after ~20 results
SELECT * FROM products 
WHERE name LIKE '%laptop%'
LIMIT 10;

-- Biscuit stops collecting TIDs once LIMIT is satisfied
```

**Benchmark**:
```sql
-- Compare with and without LIMIT
EXPLAIN ANALYZE 
SELECT * FROM products WHERE name LIKE '%gaming%';
-- Execution Time: 45ms

EXPLAIN ANALYZE 
SELECT * FROM products WHERE name LIKE '%gaming%' LIMIT 10;
-- Execution Time: 8ms (5x faster!)
```

---

## Maintenance and Monitoring

### Regular Statistics Updates

```sql
-- Update table statistics
ANALYZE products;

-- Update specific columns
ANALYZE products (name, category, sku);

-- Check last analyze time
SELECT schemaname, tablename, last_analyze, last_autoanalyze
FROM pg_stat_user_tables 
WHERE tablename = 'products';
```

**Frequency**: After bulk inserts/updates (>10% of rows)

---

### Monitor Index Health

```sql
-- Check index statistics
SELECT biscuit_index_stats('idx_products_name'::regclass);
```

Key metrics to watch:

```
Active records: 100000     ← Should match table row count
Tombstones: 245           ← Should be < 1000
Free slots: 15            ← Reused space
Max length: 256           ← Pattern matching limit

CRUD Statistics:
  Inserts: 105000         ← Total insertions
  Updates: 2400           ← Update operations
  Deletes: 5000           ← Deletion operations
```

---

### Cleanup Tombstones

Tombstones accumulate from DELETEs:

```sql
-- Check tombstone count
SELECT biscuit_index_stats('idx_products_name'::regclass);

-- VACUUM performs a full pending-list drain pass
VACUUM products;

-- VACUUM does not shrink the index; use REINDEX to reclaim space
REINDEX INDEX CONCURRENTLY idx_products_name;
```

---

### Index Rebuild Strategy

When to rebuild:

- ✅ After massive data changes (>50% rows)
- ✅ Tombstone count > 5000
- ✅ Performance degradation detected
- ✅ Schema changes to indexed columns

```sql
-- Concurrent rebuild (minimal downtime)
CREATE INDEX CONCURRENTLY idx_products_new ON products 
USING biscuit (name, sku, category);

DROP INDEX idx_products;
ALTER INDEX idx_products_new RENAME TO idx_products;
```

---

## Hardware Considerations

### Memory Requirements

**Formula**: `Index Size ≈ (num_records × avg_string_length × 0.1)`

Example:
- 1M records × 50 bytes avg × 0.1 = ~5MB per column

**Rule of Thumb**:
- Small dataset (<100K rows): Any hardware works
- Medium dataset (100K-1M rows): 4GB+ RAM recommended
- Large dataset (>1M rows): 16GB+ RAM recommended

---

### CPU Considerations

Biscuit is CPU-bound for:
- Pattern matching (bitmap intersections)
- Multi-column predicate ordering
- Sorting large TID arrays

**Recommendations**:
- 4+ cores for production
- High clock speed > many cores
- Modern CPU with good cache (L3 cache helps)

---

## Benchmarking Your Setup

### Create Benchmark Suite

```text
-- Create test table
CREATE TABLE bench_products AS
SELECT 
    i as id,
    'Product ' || i as name,
    'Category ' || (i % 10) as category,
    'SKU-' || LPAD(i::TEXT, 8, '0') as sku
FROM generate_series(1, 100000) i;

-- Create index
CREATE INDEX idx_bench ON bench_products 
USING biscuit (name, category, sku);

-- Run benchmark
\timing on

-- Test 1: Prefix query
SELECT COUNT(*) FROM bench_products WHERE name LIKE 'Product 1%';

-- Test 2: Substring query
SELECT COUNT(*) FROM bench_products WHERE name LIKE '%Product 5%';

-- Test 3: Multi-column query
SELECT COUNT(*) FROM bench_products 
WHERE name LIKE '%Product%'
  AND category LIKE 'Category 5%'
  AND sku LIKE 'SKU-00005%';

-- Test 4: Complex pattern
SELECT COUNT(*) FROM bench_products 
WHERE name LIKE '%Product%1%0%';
```

---

## Troubleshooting Performance Issues

### Issue 1: Index Not Being Used

**Symptoms**:
```sql
EXPLAIN SELECT * FROM products WHERE name LIKE '%laptop%';
-- Shows: Seq Scan on products
```

**Solutions**:

```text
-- 1. Update statistics
ANALYZE products;

-- 2. Check if index exists
\d products

-- 3. Verify extension loaded
SELECT * FROM pg_extension WHERE extname = 'biscuit';

-- 4. Force index usage (testing only)
SET enable_seqscan = off;
```

---

### Issue 2: Slow Query Despite Index

**Symptoms**: Query uses Biscuit but takes 100ms+

**Debug steps**:

```sql
-- 1. Check pattern selectivity
SELECT COUNT(*) FROM products WHERE name LIKE '%e%';
-- If this returns >50% of rows, pattern is too generic

-- 2. Verify index health
SELECT biscuit_index_stats('idx_products_name'::regclass);
-- Look for high tombstone count

-- 3. Check for pattern complexity
EXPLAIN (ANALYZE, VERBOSE) 
SELECT * FROM products WHERE name LIKE '%a%b%c%d%e%';
-- Many partitions = slower
```

**Solutions**:
- Use more specific patterns
- Rebuild index if tombstone count > 1000
- Combine with additional filters

---

### Issue 3: High Memory Usage

**Symptoms**: PostgreSQL using excessive RAM

**Solutions**:

```sql
-- 1. Reduce work_mem for this connection
SET work_mem = '64MB';

-- 2. Check index size
SELECT pg_size_pretty(pg_relation_size('idx_products_name'));

-- 3. Consider partial indexes for large tables
CREATE INDEX idx_products_active ON products 
USING biscuit (name)
WHERE status = 'active';  -- Only index active products
```

---

### Issue 4: Slow Index Builds

**Symptoms**: `CREATE INDEX` takes minutes

**Solutions**:

```sql
-- 1. Increase maintenance_work_mem
SET maintenance_work_mem = '2GB';

-- 2. Use CONCURRENTLY for production
CREATE INDEX CONCURRENTLY idx_products_name ON products 
USING biscuit (name);

-- 3. Build during low-traffic periods
-- 4. Consider building index in steps for huge tables (>10M rows)
```

---

## Advanced Tuning

### Runtime Settings

Biscuit registers two settings:

```sql
-- Pending-list drain threshold, in slots (superuser).
-- Raising it defers compaction and reduces write-time work; lowering it keeps
-- pending chains shorter, at the cost of more frequent compaction.
SHOW biscuit.delta_compaction_slots;
SET biscuit.delta_compaction_slots = 20000;

-- Per-session scan tracing, for diagnostics only.
SET biscuit.diag_scan_trace = on;
```

Leave `biscuit.diag_scan_trace` off in production; it is a debugging aid.

---

### Custom Cost Parameters

```sql
-- Fine-tune Biscuit's cost estimation
ALTER INDEX idx_products_name SET (fillfactor = 90);

-- Adjust planner costs
SET random_page_cost = 1.0;   -- SSD-optimized
SET seq_page_cost = 1.0;
SET cpu_index_tuple_cost = 0.001;
```

---

### Parallel Query Configuration

```sql
-- Enable parallel bitmap heap scans
SET max_parallel_workers_per_gather = 4;
SET parallel_tuple_cost = 0.01;
SET parallel_setup_cost = 100;

-- Test parallel execution
EXPLAIN (ANALYZE, VERBOSE) 
SELECT COUNT(*) FROM products WHERE name LIKE '%laptop%';
-- Look for "Parallel Bitmap Heap Scan"
```

The bitmap index scan itself runs serially and feeds a Parallel Bitmap Heap
Scan above it; heap access and aggregation are parallelised, the index lookup
is not. For very broad patterns the planner may prefer a parallel sequential
scan outright, which is often the better plan.

---

## Performance Monitoring Dashboard

Create a monitoring view:

```sql
CREATE OR REPLACE VIEW biscuit_performance AS
SELECT 
    i.indexrelid::regclass AS index_name,
    pg_size_pretty(pg_relation_size(i.indexrelid)) AS index_size,
    s.idx_scan AS scans,
    s.idx_tup_read AS tuples_read,
    s.idx_tup_fetch AS tuples_fetched,
    ROUND(s.idx_tup_fetch::numeric / NULLIF(s.idx_scan, 0), 2) AS avg_tuples_per_scan
FROM pg_index i
JOIN pg_stat_user_indexes s ON i.indexrelid = s.indexrelid
WHERE i.indexrelid::regclass::text LIKE '%biscuit%';

-- Monitor over time
SELECT * FROM biscuit_performance;
```

---

## Next Steps

- Learn about [Architecture](architecture.md) internals
-  Review [Pattern Syntax](patterns.md) for optimization
-  Explore [Multi-Column Indexes](multicolumn.md)
-  Check the [FAQ](faq.md)