Part V · Reference

All Databases for Agentic Systems

Every type of database an agent might need — vector, relational, document, graph, time-series, cache, and search. When to use each, how to connect agents to them, and production patterns.

Chapter 17
Vector Databases — Production Patterns

Chapter 8 introduced vector databases. This chapter covers production patterns: multi-tenancy, filtering optimisation, index tuning, backup strategies, and migration between vector DBs.

Multi-tenancy patterns

PatternWhen to useIsolationComplexity
Collection per tenant<100 tenants, strict isolation requiredCompleteHigh ops burden at scale
Payload filter per tenant100–100K tenantsLogical (filter enforced in query)Low — one collection, filter by tenant_id
Namespace per tenantPinecone users, medium scaleNamespace isolationMedium — managed by Pinecone
Dedicated cluster per tenantEnterprise, highest securityPhysicalHighest — separate infrastructure

Production Qdrant — index tuning

Python — Qdrant production configuration
from qdrant_client import QdrantClient, models

client = QdrantClient(url="https://cluster.qdrant.io", api_key="...")

# Optimise for high-recall production search
client.create_collection(
    collection_name="production_docs",
    vectors_config=models.VectorParams(
        size=1536,
        distance=models.Distance.COSINE,
        hnsw_config=models.HnswConfigDiff(
            m=32,             # Higher m = better recall, more RAM (default 16)
            ef_construct=200, # Higher = better index quality (default 100)
            full_scan_threshold=10000  # Use full scan below this size
        )
    ),
    optimizers_config=models.OptimizersConfigDiff(
        indexing_threshold=50000,  # Build HNSW after 50K vectors
        memmap_threshold=50000     # Memory-map vectors above this threshold
    ),
    quantization_config=models.ScalarQuantization(
        scalar=models.ScalarQuantizationConfig(
            type=models.ScalarType.INT8,  # 4x storage reduction, small accuracy loss
            quantile=0.99,
            always_ram=True
        )
    )
)

# Always create payload indexes for your filter fields!
for field in ["tenant_id", "doc_type", "created_at"]:
    client.create_payload_index(
        collection_name="production_docs",
        field_name=field,
        field_schema=models.PayloadSchemaType.KEYWORD
    )

Chapter 18
Relational Databases — SQL Agents

PostgreSQL and MySQL are the most widely deployed databases in the world. Agents increasingly need to query them via natural language. This chapter covers NL-to-SQL patterns, safety constraints, and production SQL agent design. For library implementation examples, see the LangChain SQL Agent Tutorial.

NL-to-SQL — how it works

Natural Language → SQL Pipeline
1. User Query
"How many Q3 orders from enterprise users?"
2. SQL Gen (LLM)
SELECT COUNT(*)...
🔒 3. Safety Check
Block updates/drops.
Enforce SELECT limit.
4. DB Execution
Run on read-only replica.
Returns rows.
5. Format Answer
"There were 1,847 orders placed last week."
Python — Production SQL agent with safety
import re, asyncpg
from langchain_community.utilities import SQLDatabase
from langchain.chains import create_sql_query_chain

# ALWAYS connect to a read-only replica for agent queries
db = SQLDatabase.from_uri(
    "postgresql://readonly_user:pass@replica.db.internal/prod",
    include_tables=["orders", "customers", "products", "invoices"],
    # Only expose tables agents should see — never all tables
)

def validate_sql(sql: str) -> str:
    """Safety layer — reject any non-SELECT queries"""
    sql_clean = sql.strip().upper()
    dangerous = ["DROP", "DELETE", "UPDATE", "INSERT", "ALTER", "TRUNCATE", "CREATE"]
    for keyword in dangerous:
        if re.search(rf'\b{keyword}\b', sql_clean):
            raise ValueError(f"Unsafe SQL operation detected: {keyword}")
    if not sql_clean.startswith("SELECT"):
        raise ValueError("Only SELECT queries are permitted")
    # Enforce row limit
    if "LIMIT" not in sql_clean:
        sql += " LIMIT 1000"
    return sql

chain = create_sql_query_chain(llm, db)

def ask_database(question: str) -> str:
    raw_sql  = chain.invoke({"question": question})
    safe_sql = validate_sql(raw_sql)
    result   = db.run(safe_sql)
    # Let LLM format the result naturally
    return llm.invoke(f"Question: {question}\nSQL result: {result}\nAnswer naturally:").content

All relational databases agents use

DatabaseAgent use caseConnection patternProduction note
PostgreSQLGeneral analytics, NL-to-SQL, pgvector hybridMCP server or SQLAlchemyRead-only replica for agents
MySQL / MariaDBLegacy systems, WordPress dataSQLAlchemy + LangChainSame safety constraints apply
SQLiteLangGraph checkpointing, local dev, file-based agentsDirect — sqlite3 Python libraryNo concurrent writes — single process
DuckDBCSV/Parquet analytics, data agents, local OLAPDirect Python libraryExceptional for in-process analytics
SnowflakeEnterprise data warehouse queriesSnowflake MCP (official)Separate warehouse for agent queries
BigQueryGoogle Cloud analytics agentsGoogle ADK native, BigQuery MCPSet slot reservations to cap cost
DatabricksML feature stores, large-scale pipelinesDatabricks MCP serverUnity Catalog for governance
CockroachDBGlobally distributed transactionalPostgreSQL-compatible — same as PGMulti-region agent deployments

Chapter 19
MongoDB & NoSQL Databases

Document databases like MongoDB store flexible JSON-like documents, making them natural fits for agent memory, conversation history, and flexible schema requirements. MongoDB Atlas adds vector search, making it a unified store for both operational data and embeddings.

MongoDB for agent episodic memory

Python — MongoDB agent memory store
from pymongo import MongoClient, DESCENDING
from datetime import datetime, timedelta
import uuid

client = MongoClient("mongodb+srv://...")
db = client["agent_platform"]

# Store agent conversations with full context
def save_conversation(user_id: str, session_id: str, messages: list, outcome: str):
    db.conversations.insert_one({
        "_id": session_id,
        "user_id": user_id,
        "messages": messages,         # Full message history
        "outcome": outcome,
        "created_at": datetime.utcnow(),
        "tools_used": list({m["tool"] for m in messages if "tool" in m}),
        "token_count": sum(m.get("tokens", 0) for m in messages)
    })

# Retrieve recent conversations for a user
def get_recent_context(user_id: str, days: int = 30) -> list:
    return list(db.conversations.find(
        {"user_id": user_id, "created_at": {"$gte": datetime.utcnow() - timedelta(days=days)}},
        {"messages": 0}  # Exclude full messages for efficiency — get summaries
    ).sort("created_at", DESCENDING).limit(10))

# MongoDB Atlas Vector Search — semantic search over agent history
def semantic_memory_search(user_id: str, query: str, limit: int = 5) -> list:
    query_embedding = embed(query)
    pipeline = [
        {"$vectorSearch": {
            "index": "conversation_vector_index",
            "path": "embedding",
            "queryVector": query_embedding,
            "numCandidates": 100,
            "limit": limit,
            "filter": {"user_id": user_id}  # Pre-filter by user
        }},
        {"$project": {"outcome": 1, "created_at": 1, "score": {"$meta": "vectorSearchScore"}}}
    ]
    return list(db.conversations.aggregate(pipeline))

Other NoSQL databases agents use

DatabaseTypeAgent use caseKey strength
RedisIn-memory KV + vectorSession cache, rate limiting, real-time state, pub/subSub-millisecond latency
CassandraWide-columnTime-series agent logs, high-write audit trailsMassive write throughput
DynamoDBKV + documentServerless agent state, AWS-native applicationsInfinite scale, no ops
FirestoreDocumentMobile agent apps, GCP-native, real-time syncReal-time listeners
CouchbaseDocument + SQLEdge deployments, offline-capable agentsMobile sync, edge computing
HBaseWide-columnHadoop-integrated analytics, massive datasetsPetabyte-scale reads

Redis for agent infrastructure

Python — Redis patterns for agents
import redis, json
from datetime import timedelta

r = redis.Redis(host="localhost", port=6379, decode_responses=True)

# 1. Rate limiting — prevent runaway tool calls
def is_rate_limited(user_id: str, tool: str, max_per_hour: int = 100) -> bool:
    key = f"ratelimit:{user_id}:{tool}"
    count = r.incr(key)
    if count == 1: r.expire(key, 3600)  # 1 hour window
    return count > max_per_hour

# 2. Tool result caching — same search query = cached result
def cached_search(query: str, ttl: int = 3600) -> dict:
    key = f"search_cache:{hash(query)}"
    cached = r.get(key)
    if cached: return json.loads(cached)
    result = actual_search(query)
    r.setex(key, ttl, json.dumps(result))
    return result

# 3. Agent session state (fast short-term memory)
def save_session_state(session_id: str, state: dict, ttl: int = 86400):
    r.setex(f"session:{session_id}", ttl, json.dumps(state))

def load_session_state(session_id: str) -> dict:
    raw = r.get(f"session:{session_id}")
    return json.loads(raw) if raw else {}

# 4. Pub/sub for multi-agent coordination
def publish_task_complete(task_id: str, result: dict):
    r.publish(f"task_results", json.dumps({"task_id": task_id, "result": result}))

Chapter 20
Graph Databases

Graph databases store data as nodes and relationships. They are exceptionally powerful for agents working with connected data — knowledge graphs, organisational hierarchies, supply chains, social networks, and any domain where relationships between entities are as important as the entities themselves.

When graph databases beat relational

Query typeRelational (SQL)Graph (Cypher)Winner
"Who are the friends of friends of Alice?"Multiple JOINs, slow at depth 3+MATCH (a)-[:FRIEND*2]->(fof) WHERE a.name='Alice'Graph — 10–1000x faster
"What's the shortest path from A to B?"Recursive CTEs, complexshortestPath() built-inGraph — trivial
"Find all suppliers 2 hops from bankrupt supplier X"Very complex, slowMATCH (:Supplier{name:'X'})-[:SUPPLIES*1..2]->(s)Graph — simple
"Total revenue by region last month"Simple GROUP BYPossible but awkwardRelational — designed for this
Python — Neo4j agent tool
from neo4j import AsyncGraphDatabase

driver = AsyncGraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password"))

# Agent tool: natural language → Cypher → graph query → answer
async def query_knowledge_graph(question: str) -> str:
    # 1. LLM generates Cypher query from natural language
    cypher = await llm.ainvoke(f"""
    Convert to Neo4j Cypher query for a company knowledge graph.
    Schema:
      (Company)-[:COMPETES_WITH]->(Company)
      (Company)-[:FOUNDED_BY]->(Person)
      (Company)-[:ACQUIRED]->(Company)
      (Person)-[:WORKS_AT {{role: 'CEO'}}]->(Company)
      (Company)-[:BACKED_BY]->(Investor)
      (Investor)-[:INVESTED_IN]->(Company)

    Only generate MATCH...RETURN queries (no writes).
    
    Question: {question}
    Cypher:""")

    # 2. Validate it's read-only
    if any(kw in cypher.upper() for kw in ["CREATE", "DELETE", "SET", "MERGE"]):
        return "Error: Only read queries are permitted"

    # 3. Execute on graph database
    async with driver.session() as session:
        result = await session.run(cypher)
        records = [dict(r) async for r in result]

    # 4. LLM formats the graph result as natural language
    return await llm.ainvoke(
        f"Question: {question}\nGraph data: {records}\nAnswer naturally:"
    )

# Example uses:
# "Who are all the investors who backed both Stripe and Adyen?"
# "Which companies did Patrick Collison found?"
# "What companies compete with Zetwerk in the B2B manufacturing space?"

Graph database options

DatabaseQuery languageBest forManaged cloud
Neo4jCypherKnowledge graphs, fraud detection, recommendationNeo4j AuraDB
Amazon NeptuneGremlin / SPARQLAWS-native, compliance graphs, identityAWS Neptune
TigerGraphGSQLMassive graph analytics, ML on graphsTigerGraph Cloud
ArangoDBAQLMulti-model: document + graph + key-valueArangoDB AuraGraph
FalkorDBCypher (Redis-based)High-performance, Redis-compatible graphFalkorDB Cloud

Chapter 21
VectorLess Retrieval Approaches

Not every retrieval problem needs a vector database. Several approaches achieve semantic or keyword retrieval without the embedding pipeline complexity. Knowing when NOT to use vectors is as important as knowing how to use them.

BM25 — the keyword retrieval standard

BM25 (Best Match 25) is the default ranking algorithm in Elasticsearch, OpenSearch, and Solr. It ranks documents by keyword relevance using term frequency, inverse document frequency, and document length normalisation. No embeddings, no GPU, no complexity:

Python — BM25 retrieval
from rank_bm25 import BM25Okapi
import nltk

def build_bm25_index(documents: list[str]) -> tuple:
    tokenised = [nltk.word_tokenize(doc.lower()) for doc in documents]
    return BM25Okapi(tokenised), tokenised

bm25, _ = build_bm25_index(your_documents)

def bm25_search(query: str, top_k: int = 10) -> list[str]:
    tokens = nltk.word_tokenize(query.lower())
    scores = bm25.get_scores(tokens)
    top_indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:top_k]
    return [your_documents[i] for i in top_indices]

# Great for: code search, API docs, exact terminology
# Poor for:  synonyms ("automobile" won't match "car"), paraphrases

Postgres full-text search — no vector DB needed

SQL — Full-text search in Postgres
-- Create a GIN index for full-text search (very fast)
CREATE INDEX docs_fts_idx ON documents USING gin(to_tsvector('english', content));

-- Full-text search query
SELECT id, content,
       ts_rank(to_tsvector('english', content), query) AS relevance
FROM documents, plainto_tsquery('english', 'rate limiting middleware') query
WHERE to_tsvector('english', content) @@ query
ORDER BY relevance DESC
LIMIT 10;

-- Phrase search (exact phrase)
SELECT * FROM documents
WHERE to_tsvector('english', content) @@ phraseto_tsquery('english', 'connection pool exhausted');

-- Weighted search (title matters more than body)
SELECT id,
  ts_rank(
    setweight(to_tsvector('english', title), 'A') ||
    setweight(to_tsvector('english', content), 'B'),
    plainto_tsquery('english', $1)
  ) AS rank
FROM documents
ORDER BY rank DESC LIMIT 20;

When to use what — retrieval decision guide

ScenarioBest approachReason
Semantic questions, synonyms, paraphrasesDense vector (Qdrant, pgvector)Captures meaning, not just keywords
Exact code/API symbol searchBM25 or Postgres FTSExact match beats semantic for code
Production, best qualityHybrid (vector + BM25) + rerankCombines strengths of both
Already on Postgres, simple use casePostgres FTS (tsvector)No new infrastructure needed
Small corpus (<10K docs), quality over speedLLM reranking onlyRead all, let LLM pick best — expensive but accurate
Multilingual contentMultilingual embeddings (BGE-M3)Language-agnostic semantic search
Real-time, sub-10ms requiredRedis Vector SearchIn-memory speed
Billion-scaleMilvus + GPU accelerationDistributed, GPU-accelerated ANN

Chapter 22
Time-Series, Search & Specialised Databases

Time-series databases for agents

Agents monitoring systems, analysing metrics, or working with IoT data need time-series databases:

DatabaseAgent use caseKey strength
InfluxDBInfrastructure monitoring agents, IoTNative time-series, excellent query performance
TimescaleDBPostgres-based time-series, analytics agentsSQL-compatible, continuous aggregations
PrometheusIncident diagnosis agents, SRE automationMetrics standard, AlertManager integration
ClickHouseHigh-speed analytics agents, log analysisColumnar, extremely fast aggregations

Elasticsearch for agents

Python — Elasticsearch agent tool
from elasticsearch import AsyncElasticsearch

es = AsyncElasticsearch("https://my-cluster.es.io", api_key="...")

async def search_logs(query: str, index: str, time_range: str = "1d") -> dict:
    """Search production logs — agent uses this to diagnose incidents"""
    return await es.search(
        index=index,
        body={
            "query": {
                "bool": {
                    "must": [{"match": {"message": query}}],
                    "filter": [{"range": {"@timestamp": {"gte": f"now-{time_range}"}}}]
                }
            },
            "aggs": {
                "error_count_over_time": {
                    "date_histogram": {"field": "@timestamp", "fixed_interval": "5m"}
                }
            },
            "size": 50,
            "sort": [{"@timestamp": "desc"}]
        }
    )
· · ·