# Multi-Column Indexes

Advanced guide to using Biscuit with multiple columns, including automatic query optimization and best practices.

---

## Overview

Biscuit's multi-column support allows you to create indexes spanning multiple text columns. The query planner automatically reorders predicates for optimal performance based on selectivity analysis.

**Key Features**:
- Automatic predicate reordering
-  Per-column bitmap indices  
-  Intelligent query planning
-  Support for mixed data types
-  Early termination optimization
-  Candidate-mask threading: each key restricts the rows the next key examines

---

## Creating Multi-Column Indexes

### Basic Syntax

```sql
CREATE INDEX index_name ON table_name 
USING biscuit (column1, column2, column3, ...);
```

### Example: E-Commerce Product Search

```sql
-- Create multi-column index
CREATE INDEX idx_products_search ON products 
USING biscuit (name, description, category, sku);

-- All columns are now searchable with LIKE
SELECT * FROM products 
WHERE name LIKE '%laptop%'
  AND description LIKE '%gaming%'
  AND category LIKE 'Elect%'
  AND sku LIKE 'SKU-2024%';
```

---

## How Query Optimization Works

Biscuit analyzes each predicate and assigns a **priority** based on:

1. **Selectivity**: How much data gets filtered
2. **Pattern type**: Exact > Prefix > Suffix > Substring
3. **Concrete characters**: More = better selectivity
4. **Anchor strength**: Strong anchors filter more

### Automatic Reordering Example

```sql
-- Your query (predicates in arbitrary order)
SELECT * FROM users 
WHERE department LIKE '%engineering%'    -- Low selectivity
  AND username LIKE 'admin%'              -- High selectivity
  AND email LIKE '%@company.com';         -- Medium selectivity

-- Biscuit automatically executes as:
-- 1. username LIKE 'admin%'           (Priority 20 - prefix, high select)
-- 2. email LIKE '%@company.com'       (Priority 30 - suffix, medium)
-- 3. department LIKE '%engineering%'  (Priority 50 - substring, low)
```

**Result**: Query runs faster by filtering aggressively first!

---

## Query Planning Deep Dive

### Priority Calculation

Biscuit assigns priorities in tiers:

| Tier | Priority | Pattern Type | Example |
|------|----------|--------------|---------|
| 1 | 0-10 | Exact match | `'admin'` |
| 2 | 10-20 | Underscore patterns | `'test__%'` |
| 3 | 20-30 | Strong prefix/suffix | `'admin%'`, `'%@company.com'` |
| 4 | 30-40 | Weak anchors | `'a%'`, `'%m'` |
| 5 | 40-50 | Multi-partition | `'%a%b%c%'` |
| 6 | 50+ | Pure substring | `'%test%'` |

### Selectivity Scoring

```
Selectivity Formula:
  base = 1.0 / (concrete_chars + 1)
  - underscore_bonus = count('_') × 0.05
  - partition_penalty = count(partitions) × 0.15
  - anchor_bonus = anchor_strength / 200
  + substring_penalty = 0.5 (if '%...%')
```

**Lower score = more selective = executes first**

---

## Viewing Query Plans

Use the debug logging to see execution order:

```sql
-- Enable query logging
SET client_min_messages = INFO;

-- Run query
SELECT * FROM products 
WHERE name LIKE '%laptop%'
  AND sku LIKE 'SKU-2024%'
  AND category LIKE 'Elect%';
```

**Output**:
```
INFO:  === BISCUIT QUERY EXECUTION PLAN ===
INFO:  Total predicates: 3
INFO:  [0] Col=1 Pattern='SKU-2024%' Type=PREFIX Priority=20 
       Selectivity=0.048
INFO:  [1] Col=2 Pattern='Elect%' Type=PREFIX Priority=21 
       Selectivity=0.091
INFO:  [2] Col=0 Pattern='%laptop%' Type=SUBSTRING Priority=50 
       Selectivity=0.450
INFO:  ====================================
```

---

## Best Practices

### 1. Order Columns by Query Frequency

```sql
-- ✅ GOOD: Most-queried columns first
CREATE INDEX idx_products ON products 
USING biscuit (name, category, sku);

--  LESS OPTIMAL: Rarely-queried columns first  
CREATE INDEX idx_products ON products 
USING biscuit (internal_notes, name, category);
```

**Why?** Column order doesn't affect query performance (Biscuit reorders automatically), but it's clearer for maintenance.

---

### 2. Mix Selective and General Columns

```sql
-- ✅ GOOD: Mix of selective and broad columns
CREATE INDEX idx_users ON users 
USING biscuit (username, email, department);
-- username: high selectivity (unique)
-- email: medium selectivity (domains)
-- department: low selectivity (few values)

-- This allows queries like:
WHERE department LIKE '%sales%'     -- Broad filter
  AND username LIKE 'john%';        -- Narrows down quickly
```

---

### 3. Consider Column Cardinality

```sql
-- Check column cardinality
SELECT 
    COUNT(DISTINCT name) as name_unique,
    COUNT(DISTINCT category) as category_unique,
    COUNT(*) as total
FROM products;

-- High cardinality (many unique values) = better for Biscuit
-- Low cardinality (few values) = consider other strategies
```

**Rule of Thumb**:
- High cardinality (>1000 unique): ✅ Excellent for Biscuit
- Medium cardinality (100-1000): ✅ Good for Biscuit
- Low cardinality (<100): ⚠️ Consider B-tree or partial index

---

### 4. Avoid Redundant Indexes

```sql
--  DON'T: Create overlapping indexes
CREATE INDEX idx1 ON products USING biscuit (name);
CREATE INDEX idx2 ON products USING biscuit (name, category);
-- idx1 is redundant!

-- ✅ DO: Use multi-column for flexibility
CREATE INDEX idx_products ON products 
USING biscuit (name, category, sku);
-- Handles queries on any combination of these columns
```

---

## Advanced Patterns

### Pattern 1: User Search with Email Domain

```sql
CREATE INDEX idx_users_search ON users 
USING biscuit (username, email, full_name);

-- Find all admins at company.com
SELECT * FROM users 
WHERE username LIKE 'admin%'
  AND email LIKE '%@company.com';

-- Execution:
-- 1. username filter (prefix, high selectivity)
-- 2. email filter (suffix, medium selectivity)
-- Result: ~2-5ms
```

---

### Pattern 2: Product Search with Multiple Attributes

```sql
CREATE INDEX idx_products_full ON products 
USING biscuit (name, brand, category, sku, tags);

-- Complex product search
SELECT * FROM products 
WHERE name LIKE '%laptop%'
  AND brand LIKE 'Dell%'
  AND category LIKE 'Computers%'
  AND tags LIKE '%business%';

-- Biscuit reorders to execute brand first (prefix),
-- then category, then tags, then name (substring last)
```

---

### Pattern 3: Log Analysis

```sql
CREATE INDEX idx_logs ON application_logs 
USING biscuit (level, service, message, user_id);

-- Find errors from auth service with specific message
SELECT * FROM application_logs 
WHERE level LIKE 'ERROR'              -- Exact (fastest)
  AND service LIKE 'auth%'            -- Prefix (fast)
  AND message LIKE '%timeout%'        -- Substring (slower)
  AND user_id LIKE 'user_%';          -- Underscore (fast)

-- Execution order: level → service → user_id → message
```

---


## Performance Characteristics

### Query Complexity vs. Performance

| Predicate Count | Typical Time | Optimization |
|-----------------|--------------|--------------|
| 1 column | 1-5ms | Single bitmap lookup |
| 2 columns | 2-8ms | Sequential intersection |
| 3-4 columns | 5-15ms | Early termination |
| 5+ columns | 10-30ms | LIMIT-aware collection |

### Early Termination Example

```sql
-- Query with 5 predicates
SELECT * FROM products 
WHERE name LIKE '%laptop%'        -- Priority 50
  AND brand LIKE 'Dell%'          -- Priority 20
  AND price::TEXT LIKE '1%'       -- Priority 25
  AND category LIKE 'Comp%'       -- Priority 21  
  AND rating::TEXT LIKE '4%';     -- Priority 22

-- Execution:
-- [1] brand filter → 1000 results
-- [2] category filter → 500 results
-- [3] rating filter → 200 results
-- [4] price filter → 50 results
-- [5] name filter (stopped early if <10 results remain)
```

---

## LIMIT Optimization

Biscuit optimizes LIMIT queries with early termination:

```sql
-- Only need 10 results
SELECT * FROM products 
WHERE name LIKE '%laptop%'
  AND brand LIKE '%'
  AND category LIKE '%'
LIMIT 10;

-- Biscuit stops collecting after ~20 TIDs
-- (2x buffer for safety)
```

**Benefit**: Faster for small LIMIT values!

---

## Monitoring and Diagnostics

### Check Index Statistics

```sql
SELECT biscuit_index_stats('idx_products_search'::regclass);
```

Output shows per-column statistics:
```
Biscuit Index Statistics
========================
Index: idx_products_search
Active records: 50000
Columns: 4

Column 0 (name):
  Max length: 128
  Character bitmaps: 256
  Length bitmaps: [0..128]
  
Column 1 (category):  
  Max length: 64
  Character bitmaps: 256
  Length bitmaps: [0..64]

[... continues for each column ...]
```

---

### Query Performance Analysis

```sql
EXPLAIN (ANALYZE, BUFFERS) 
SELECT * FROM products 
WHERE name LIKE '%laptop%'
  AND category LIKE 'Elect%';
```

Look for:
- **Bitmap Index Scan on a Biscuit index**: ✅ Good
- **Seq Scan**:  Index not being used — for broad patterns this may still be the better plan
- **Planning Time**: Should be <1ms
- **Execution Time**: Should be <20ms for typical queries

---

## Common Pitfalls

###  Pitfall 1: Too Many Columns

```sql
-- DON'T: Index everything
CREATE INDEX idx_huge ON products 
USING biscuit (col1, col2, col3, col4, col5, col6, col7, col8);
```

**Problem**: Larger index, slower builds, more memory

**Solution**: Index only frequently-queried columns 

---

###  Pitfall 2: Low Selectivity Patterns

```sql
-- All predicates are low-selectivity substrings
WHERE col1 LIKE '%a%'
  AND col2 LIKE '%e%'  
  AND col3 LIKE '%i%';
```

**Problem**: Even with reordering, all filters are weak

**Solution**: Combine with equality filters or use better patterns

---

###  Pitfall 3: Not Using EXPLAIN

```sql
-- Assuming the index is being used
SELECT * FROM products WHERE name LIKE '%laptop%';
```

**Problem**: Might be doing sequential scan!

**Solution**: Always verify with `EXPLAIN ANALYZE`

---

## Migration from Single-Column

Migrating from single-column to multi-column is straightforward:

```sql
-- Old: Multiple single-column indexes
CREATE INDEX idx_name ON products USING biscuit (name);
CREATE INDEX idx_sku ON products USING biscuit (sku);
CREATE INDEX idx_category ON products USING biscuit (category);

-- New: Single multi-column index
CREATE INDEX idx_products_multi ON products 
USING biscuit (name, sku, category);

-- Drop old indexes
DROP INDEX idx_name;
DROP INDEX idx_sku;
DROP INDEX idx_category;
```

**Benefits**:
- Single index to maintain
- Automatic query optimization
- Support for multi-column queries
- Reduced disk space

---

## Next Steps

-  Learn [Performance Tuning](performance.md) techniques
-  Understand the [Architecture](architecture.md)
-  Review [Pattern Syntax](patterns.md)
-  Check the [FAQ](faq.md)
