Handling Ambiguous Database Questions
Clarifying questions, schema disambiguation, and multi-table join reasoning.
Handling Ambiguous Database Questions is a free AI Agents lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the AI Agents learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
The Ambiguity Problem in NL-to-SQL
Natural language is inherently ambiguous. When someone asks "Show me recent orders", a human asks: recent for who? in what time frame? sorted how?
An agent that silently assumes defaults will produce results the user didn't expect. A good agent detects ambiguity and asks targeted clarifying questions.
Types of Ambiguity
There are four common categories of ambiguous questions in database contexts:
- Temporal ambiguity: 'recent', 'latest', 'old', 'this year'
- Scope ambiguity: 'top customers' — top by what metric?
- Table ambiguity: multiple tables could answer the question
- Filter ambiguity: 'active users' — what defines active?
# Examples of each type
ambiguous_examples = {
'temporal': 'Show me recent orders', # last 7 days? 30 days? 1 year?
'scope': 'Who are the top customers?', # by revenue, order count, or recency?
'table': 'Show me user activity', # from users, sessions, or audit_log?
'filter': 'List all active products' # active = in_stock? not discontinued?
}
if __name__ == '__main__':
print('Types of ambiguous questions:')
for kind, example in ambiguous_examples.items():
print(f' {kind}: "{example}"')
Detecting Ambiguity with the LLM
Rather than hardcoding ambiguity rules, ask the LLM to detect ambiguity. Provide it with the schema and ask: is this question clear enough to generate a definitive SQL query?
import json
AMBIGUITY_CHECK_PROMPT = '''You are a SQL assistant. Given a database schema and a user question,
determine if the question is clear enough to write a single correct SQL query.
Schema:
{schema}
Question: {question}
Respond with JSON:
- If clear: {{"ambiguous": false, "sql": "SELECT ..."}}
- If ambiguous: {{"ambiguous": true, "clarification": "What time range counts as recent?",
"options": ["Last 7 days", "Last 30 days", "Last 90 days"]}}
JSON:'''
def check_and_generate(question, schema):
response = llm_call(AMBIGUITY_CHECK_PROMPT.format(
schema=schema, question=question
))
return json.loads(response)The Clarification Loop
When ambiguity is detected, enter a clarification loop: ask the user a targeted question, receive their answer, then re-attempt SQL generation with the enriched context.
Limit the loop to 2 clarification rounds — asking too many questions frustrates users.
def nl_to_sql_with_clarification(user_question, schema, conn, ask_user_fn):
for attempt in range(2): # max 2 clarification rounds
result = check_and_generate(user_question, schema)
if not result.get('ambiguous'):
# Clear question — execute
rows = execute_query(conn, result['sql'])
return format_results(rows, user_question)
# Ambiguous — ask user
clarification = result['clarification']
options = result.get('options', [])
user_reply = ask_user_fn(clarification, options)
# Enrich the question with the answer
user_question = f'{user_question} ({clarification}: {user_reply})'
print(f'Enriched question: {user_question}')
# After 2 rounds, generate with best guess
return check_and_generate(user_question, schema)'Recent' Temporal Disambiguation
Temporal words like 'recent', 'latest', 'this week', 'new' are extremely common. Build a dedicated temporal resolver that maps ambiguous terms to default time ranges, while still allowing the LLM to ask when confidence is low.
from datetime import datetime, timedelta
TEMPORAL_DEFAULTS = {
'recent': 7, # days
'latest': 1, # days
'new': 30,
'old': 365,
'this week': 7,
'this month': 30,
'this year': 365
}
def resolve_temporal(question):
lower = question.lower()
for term, days in TEMPORAL_DEFAULTS.items():
if term in lower:
since = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
return question + f" ('{term}' means since {since})"
return question
print(resolve_temporal('Show me recent orders'))
# Show me recent orders ('recent' means since 2024-05-22)Schema Disambiguation: Multiple Tables
When multiple tables could answer a question, the agent needs to reason about which one is most appropriate. For example, 'user activity' could be in sessions, audit_log, or user_events.
TABLE_SEMANTIC_MAP = {
'user activity': ['sessions', 'user_events', 'audit_log'],
'purchases': ['orders', 'transactions', 'invoices'],
'product catalog': ['products', 'items', 'listings'],
'sign-ups': ['users', 'registrations', 'accounts']
}
def disambiguate_tables(question, schema_dict, ask_user_fn):
lower = question.lower()
for concept, tables in TABLE_SEMANTIC_MAP.items():
if concept in lower:
available = [t for t in tables if t in schema_dict]
if len(available) > 1:
chosen = ask_user_fn(
f"Which table should I query for '{concept}'?",
available
)
return question + f" (use the {chosen} table)"
return question
if __name__ == '__main__':
def ask_user_fn(prompt, options):
print(f'{prompt} -> choosing "{options[0]}" (demo default)')
return options[0]
demo_schema = {'sessions': [], 'transactions': [], 'orders': []}
resolved = disambiguate_tables('Show me user activity', demo_schema, ask_user_fn)
print('Resolved question:', resolved)
Handling 'Top N' Scope Ambiguity
'Top customers', 'best products', 'most active users' all require knowing by which metric. Present metric options to the user rather than silently choosing one.
RANKING_AMBIGUITY_PROMPT = '''The question asks for a ranking but the metric is unclear.
Question: {question}
Table columns available: {columns}
List 2-3 reasonable ranking metrics as a JSON array of objects:
[{{"label": "By total revenue", "sql_expr": "SUM(total) DESC"}},
{{"label": "By order count", "sql_expr": "COUNT(*) DESC"}}]
JSON:'''
def resolve_ranking(question, columns, ask_user_fn):
import json
response = llm_call(RANKING_AMBIGUITY_PROMPT.format(
question=question, columns=columns
))
options = json.loads(response)
labels = [o['label'] for o in options]
chosen_label = ask_user_fn('How should I rank the results?', labels)
chosen = next(o for o in options if o['label'] == chosen_label)
return question + f" (rank by: {chosen['sql_expr']})", chosen['sql_expr']Offering Intelligent Defaults
Asking the user every time can be annoying. A smarter approach: pick a sensible default, execute, and tell the user what you assumed. Include a note like: "I assumed 'recent' means the last 30 days. Did you mean a different period?"
def nl_to_sql_with_assumptions(question, schema, conn):
# Resolve common ambiguities with defaults
enriched = resolve_temporal(question)
result = check_and_generate(enriched, schema)
if result.get('ambiguous'):
# Still ambiguous — pick default option
options = result.get('options', ['the most common interpretation'])
default = options[0]
enriched = enriched + f' ({result["clarification"]}: {default})'
result = check_and_generate(enriched, schema)
rows = execute_query(conn, result['sql'])
answer = format_results(rows, question)
# Append assumption note
if enriched != question:
assumption = enriched[len(question):].strip().strip('()')
answer += f'\n\n[Note: I assumed {assumption}]'
return answerColumn-Level Ambiguity
Sometimes the ambiguity is at the column level. 'Show me orders sorted by date' — which date? created_at, updated_at, shipped_at, or delivery_date?
Include column descriptions in your schema injection to help the LLM and reduce column-level ambiguity.
COLUMN_DESCRIPTIONS = {
('orders', 'created_at'): 'When the order was placed',
('orders', 'updated_at'): 'When the order was last modified',
('orders', 'shipped_at'): 'When the order was shipped to customer',
('orders', 'delivery_date'): 'Expected or actual delivery date'
}
def format_columns_with_descriptions(table, columns):
parts = []
for col in columns:
desc = COLUMN_DESCRIPTIONS.get((table, col['name']), '')
label = f"{col['name']} ({col['type']})"
if desc:
label += f' [{desc}]'
parts.append(label)
return ', '.join(parts)
if __name__ == '__main__':
demo_columns = [
{'name': 'created_at', 'type': 'timestamp'},
{'name': 'shipped_at', 'type': 'timestamp'},
{'name': 'total', 'type': 'float'},
]
print(format_columns_with_descriptions('orders', demo_columns))
Logging Ambiguity Resolutions
Track how often each type of ambiguity occurs and which defaults or clarifications were chosen. This data helps you improve default values and reduce the number of questions asked over time.
import json
from datetime import datetime
ambiguity_log = []
def log_ambiguity(original_question, clarification, resolution, method):
ambiguity_log.append({
'timestamp': datetime.now().isoformat(),
'question': original_question,
'clarification': clarification,
'resolution': resolution,
'method': method # 'asked_user' | 'default' | 'llm_inferred'
})
# Periodically analyze to improve defaults
def analyze_ambiguity_log():
from collections import Counter
types = Counter(entry['clarification'] for entry in ambiguity_log)
print('Most common ambiguities:')
for q, count in types.most_common(5):
print(f' {count}x: {q}')
if __name__ == '__main__':
log_ambiguity('Show recent orders', 'temporal', 'last 30 days', 'default')
log_ambiguity('Show recent orders', 'temporal', 'last 7 days', 'asked_user')
log_ambiguity('Top customers', 'scope', 'by revenue', 'llm_inferred')
analyze_ambiguity_log()
Multi-Turn Conversation Context
In a chat interface, previous turns provide context. If the user already said "I'm looking at Q4 2024 data", later questions about 'recent orders' should default to that time range — not a hardcoded 30 days.
def nl_to_sql_with_context(question, schema, conn, conversation_history):
context_prompt = ''
if conversation_history:
context_prompt = 'Previous conversation context:\n'
for turn in conversation_history[-3:]: # last 3 turns
context_prompt += f"User: {turn['user']}\n"
if 'assumption' in turn:
context_prompt += f"Assumption made: {turn['assumption']}\n"
full_prompt = context_prompt + f'Current question: {question}'
result = check_and_generate(full_prompt, schema)
conversation_history.append({
'user': question,
'sql': result.get('sql', ''),
'assumption': result.get('assumption', '')
})
return resultKnowledge Check
What is the recommended strategy when a NL-to-SQL agent detects temporal ambiguity (e.g., 'recent orders')?
Recap: Handling Ambiguous Questions
Ambiguity in NL-to-SQL comes in four forms: temporal, scope, table, and filter. The best strategies combine intelligent defaults (resolve 'recent' = last 30 days automatically), LLM-based ambiguity detection, and targeted clarification when the question is genuinely unclear.
Always communicate assumptions to the user. Use conversation history for context, and log ambiguity resolutions to improve defaults over time.
Frequently asked questions
Is the “Handling Ambiguous Database Questions” lesson free?
Yes — the full text of “Handling Ambiguous Database Questions” is free to read here on the web, and the AI Agents course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the AI Agents course, upgrade to CoddyKit PRO.
What will I learn in “Handling Ambiguous Database Questions”?
Clarifying questions, schema disambiguation, and multi-table join reasoning. You practise AI Agents with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start AI Agents?
No prior experience is required. AI Agents on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Handling Ambiguous Database Questions” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this AI Agents lesson?
Yes. Every AI Agents lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- How NL-to-SQL Agents Work
- Schema Understanding and Injection
- Generating and Validating SQL Queries
- Handling Ambiguous Database Questions