What I Was Trying to Do

I had structured data in SQLite and didn’t want to write SQL for every question. LlamaIndex let me ask things like “which city has the most people?” in plain English — it figures out the query, runs it, and returns the answer.

Key capabilities include:

  • Translating natural language into SQL queries.
  • Converting unstructured context into structured insights.
  • Dynamically retrieving relevant schemas or tables for complex databases.

Note: Never expose full write-access databases to LLMs. Always use read-only credentials or sandboxed environments for safety.


Setup: Creating a Sample Database

Let’s begin with a simple in-memory SQLite database using SQLAlchemy:

from sqlalchemy import create_engine, MetaData, Table, Column, String, Integer

engine = create_engine("sqlite:///:memory:")
metadata_obj = MetaData()

city_stats_table = Table(
    "city_stats",
    metadata_obj,
    Column("city_name", String(16), primary_key=True),
    Column("population", Integer),
    Column("country", String(16), nullable=False),
)
metadata_obj.create_all(engine)

Add some sample rows:

from sqlalchemy import insert

rows = [
    {"city_name": "Toronto", "population": 2731571, "country": "Canada"},
    {"city_name": "Tokyo", "population": 13929286, "country": "Japan"},
    {"city_name": "Berlin", "population": 600000, "country": "Germany"},
]

for row in rows:
    stmt = insert(city_stats_table).values(**row)
    with engine.begin() as connection:
        connection.execute(stmt)

Connecting with LlamaIndex

Now, we’ll connect this database to LlamaIndex using its SQLDatabase wrapper:

from llama_index.core import SQLDatabase

sql_database = SQLDatabase(engine, include_tables=["city_stats"])

This wrapper allows LlamaIndex to inspect the database schema and use it for reasoning during query generation.


Ask Questions in Natural Language

LlamaIndex provides the NLSQLTableQueryEngine, which takes a question in plain English and automatically converts it to SQL:

from llama_index.llms.ollama import Ollama
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.core import Settings
from llama_index.core.indices.struct_store import NLSQLTableQueryEngine

llm = Ollama(model="llama3", request_timeout=120)
embed_model = OllamaEmbedding(model_name="nomic-embed-text")

Settings.llm = llm
Settings.embed_model = embed_model

query_engine = NLSQLTableQueryEngine(
    sql_database=sql_database,
    tables=["city_stats"],
    llm=llm,
    embed_model=embed_model,
)
response = query_engine.query("Which city has the highest population?")

Output:

Tokyo has the highest population.

This approach is perfect when your database has a small number of known tables.


Creating a Table Schema Index

For larger databases or dynamic querying, you can create a schema index to help the model understand table structures semantically:

from llama_index.core import VectorStoreIndex
from llama_index.core.objects import SQLTableNodeMapping, ObjectIndex, SQLTableSchema

table_node_mapping = SQLTableNodeMapping(sql_database)
table_schema_objs = [SQLTableSchema(table_name="city_stats")]

obj_index = ObjectIndex.from_objects(
    table_schema_objs,
    table_node_mapping,
    VectorStoreIndex,
)

This allows LlamaIndex to retrieve relevant table schemas automatically during queries.


Schema-Aware Querying

When multiple tables exist, you can use the SQLTableRetrieverQueryEngine to pick the best one:

from llama_index.core.indices.struct_store import SQLTableRetrieverQueryEngine

query_engine = SQLTableRetrieverQueryEngine(
    sql_database, obj_index.as_retriever(similarity_top_k=1)
)
response = query_engine.query("Which city has the highest population?")

Result: Tokyo has the highest population.


Improving Query Accuracy

1. Row-Level Embeddings

You can embed individual rows to allow semantic row retrieval:

from llama_index.core.schema import TextNode

with engine.connect() as connection:
    results = connection.execute(stmt).fetchall()

city_nodes = [TextNode(text=str(t)) for t in results]
city_rows_index = VectorStoreIndex(city_nodes, embed_model=embed_model)
city_rows_retriever = city_rows_index.as_retriever(similarity_top_k=1)

This helps for ambiguous queries like:

How many cities belong to a specific country?”

2. Column-Level Embeddings

Instead of indexing full rows, you can embed distinct column values for better efficiency:

for column_name in ["city_name", "country"]:
    stmt = select(city_stats_table.c[column_name]).distinct()
    ...

Using the Retriever Directly

For more flexibility, use the NLSQLRetriever directly:

from llama_index.core.retrievers import NLSQLRetriever

nl_sql_retriever = NLSQLRetriever(
    sql_database, tables=["city_stats"], llm=llm, return_raw=True
)

Or pair it with a query engine:

from llama_index.core.query_engine import RetrieverQueryEngine

query_engine = RetrieverQueryEngine.from_args(nl_sql_retriever)
response = query_engine.query(
    "Return the top 5 cities with the highest population."
)

Summary

ComponentPurpose
SQLDatabaseWraps SQLAlchemy engine for LlamaIndex integration
NLSQLTableQueryEngineConverts natural language into SQL queries
ObjectIndexStores table schemas for retrieval
SQLTableRetrieverQueryEngineCombines schema retrieval and SQL generation
Row/Column RetrieversEnhance accuracy using semantic similarity

Key Takeaways

  • LlamaIndex enables conversational querying over structured databases.
  • Supports schema understanding, reasoning, and contextual retrieval.
  • Can dynamically select relevant tables, rows, and columns during inference.
  • Useful for building smart analytics dashboards, internal assistants, or AI-powered data explorers.

References: