Tribute to Trigrams and Trees

Acknowledging PostgreSQL’s Pattern Matching Heritage

Biscuit exists because PostgreSQL already has exceptional pattern matching capabilities. We didn’t invent pattern matching for databases—we’re building on decades of research and production-hardened implementations. This section honors the tools that inspired Biscuit and explains where they still reign supreme.



B-tree: The Foundation of Database Indexing

What B-tree Does Brilliantly

The B-tree is the most battle-tested index structure in database history (50+ years). It’s PostgreSQL’s default for good reason.

1. Exact Equality & Range Queries

-- B-tree excels at these:
SELECT * FROM users WHERE email = 'user@example.com';  -- O(log n)
SELECT * FROM orders WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31';
SELECT * FROM products WHERE price > 100 AND price < 500;

Biscuit is not built to address these.

2. Sorted Data Access

-- B-tree provides sorted results for free
SELECT * FROM products ORDER BY name LIMIT 10;  -- No sort needed!

-- Index-only scan when possible
SELECT COUNT(*) FROM users WHERE created_at > '2024-01-01';

Biscuit cannot do this:

  • Must sort TIDs after collection: (uses Radix sort for large volumes)

  • No index-only scans (data cached separately)

3. Space Efficiency

The benchmarks conducted on a 1M row dataset showed that BTrees consumed 43 MB disk space for multi-columnar indexing, whereas Biscuit consumed ~900 MBs for the same.

4. Universal Compatibility

-- B-tree works on ANY orderable type:
CREATE INDEX ON events(user_id);        -- integers
CREATE INDEX ON orders(total_price);    -- numerics
CREATE INDEX ON logs(timestamp);        -- timestamps
CREATE INDEX ON users(email);           -- text

Biscuit only works on text-like data.

5. Lock-Free Concurrent Access

-- B-tree: MVCC-friendly, high concurrency
-- 1000s of concurrent readers + writers, no problem

Biscuit’s concurrency limitations:

// INSERT requires exclusive lock on entire index
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
// All queries blocked during insert!

// This is acceptable for read-heavy workloads
// Unacceptable for high-write scenarios

When to Use B-tree Instead of Biscuit

Use Case

Use B-tree

Why

Equality queries

✅ Almost always

Simpler than Biscuit

Range queries

✅ Always

Biscuit: no range support

Sorted access

✅ Always

Biscuit: must sort results

Numeric data

✅ Preferred

Biscuit: unsupported

High write load

✅ Preferred

Biscuit: exclusive locks on insert

Space-constrained

✅ Always

Biscuit: Often larger

Small strings

✅ Preferred

B-tree overhead acceptable


The Complementary Index Strategy

The Best Approach: Use Multiple Indices

-- Use each index for what it does best:

CREATE INDEX users_email_btree ON users(email);           -- Equality
CREATE INDEX users_name_trgm ON users USING GIN(name gin_trgm_ops);  -- Fuzzy
CREATE INDEX users_bio_biscuit ON users USING biscuit(bio);  -- Pattern matching

-- Queries automatically use the best index:

-- B-tree wins:
SELECT * FROM users WHERE email = 'exact@match.com';

-- pg_trgm wins:
SELECT * FROM users WHERE name % 'Jhon Doe';  -- Typo tolerance

-- Biscuit wins:
SELECT * FROM users WHERE bio LIKE '%worked at Google%software engineer%';

Honest Trade-off Summary

Biscuit’s Strengths

  • Pattern matching: Faster than seq scan

  • Case-insensitive: No lower() overhead

  • Multi-column patterns: Intelligent reordering

  • Anchored patterns: Prefix/suffix with dual indexing

Biscuit’s Weaknesses

  • Memory hog: Larger than B-tree

  • Cache-dependent: Rebuilds on invalidation (2-300s for large tables)

  • No fuzzy matching: Can’t handle typos

  • No regex: Limited to LIKE wildcards

  • Poor for equality: 100x slower than B-tree

  • Write-unfriendly: Exclusive locks on insert

  • No persistence: In-memory only, lost on restart

When NOT to Use Biscuit

🚫 Production systems with:

  • Strict latency SLAs (<10ms)

  • Frequent server restarts

  • Limited memory (<4GB per index)

  • High write throughput (>1k inserts/sec)

  • Primarily equality/range queries

🚫 Workloads requiring:

  • Fuzzy matching or similarity search

  • Regular expressions

  • Full-text search with ranking

  • Sub-millisecond equality lookups

  • 24/7 uptime with zero rebuild latency


Acknowledgments

Any shortcomings described here are Biscuit’s design trade-offs, not deficiencies in PostgreSQL’s core index implementations.

Biscuit stands on the shoulders of giants:

  • PostgreSQL Core Team: For the extensible index AM framework

  • pg_trgm Contributors: For proving text indexing can be fast

  • B-tree Pioneers: 50+ years of foundational research (Bayer & McCreight, 1972)

  • Roaring Bitmap Authors: For making compressed bitmaps practical (Lemire et al.)

We are not replacing these tools. We are complementing them.


Recommendation Matrix

Your Need

Recommended Index

Why

WHERE col = 'exact'

B-tree

Simpler for this case

WHERE col LIKE 'prefix%'

B-tree

Good enough, persistent

WHERE col LIKE '%suffix'

Biscuit

B-tree can’t help

WHERE col LIKE '%middle%'

Biscuit or pg_trgm

Biscuit if memory available

WHERE col ~ 'regex'

pg_trgm (GIN)

Biscuit: no regex

WHERE col % 'similar'

pg_trgm (GIN)

Biscuit: no fuzzy

Multi-column patterns

Biscuit

Unique strength

Case-insensitive

Biscuit

Avoids lower()

Production 24/7

pg_trgm

Persistent, no rebuilds

Limited memory

B-tree or pg_trgm

Biscuit too large


Summary

There is no “best” index. Only the right index for your workload.

  • B-tree: The reliable workhorse

  • pg_trgm: The fuzzy matcher

  • Biscuit: The pattern specialist

Choose wisely. Benchmark honestly. Respect the classics.


“If I have seen further, it is by standing on the shoulders of giants.”
— Isaac Newton

“If we have indexed faster, it is by studying B-trees and trigrams first.”
— Biscuit Team