Course Wiki
Storing Data in PostgreSQL — LLM Zoomcamp Module 5
--- video_url: 'https://www.youtube.com/watch?v=iXRu_AbMtuU&list=PL3MmuxUbc_hLZFNgSad56pDBKK8KO0XIv' --- # Storing Data in PostgreSQL
Related Pages
LLM Zoomcamp › Module 5: Monitoring › Storing Data in PostgreSQL
Notes
video_url: “https://www.youtube.com/watch?v=iXRu_AbMtuU&list=PL3MmuxUbc_hLZFNgSad56pDBKK8KO0XIv”
Storing Data in PostgreSQL
The metrics disappear when we close the app, so we need somewhere to keep them. We store every conversation in PostgreSQL, which we run only for monitoring. No other part of the system touches this database. We pick Postgres for two reasons: it handles structured data well, and Grafana connects to it easily later on.
Starting PostgreSQL with Docker
First we create a Docker network. Postgres and Grafana both run in containers, and Grafana reaches Postgres by name, so they need to share a network.
Create the network:
docker network create monitoring
Start PostgreSQL with a volume for data persistence and connect it to the network:
docker run -it \
--name course-assistant-pg \
--network monitoring \
-e POSTGRES_USER=user \
-e POSTGRES_PASSWORD=password \
-e POSTGRES_DB=course_assistant \
-p 5432:5432 \
-v pgdata:/var/lib/postgresql/data \
postgres:17
If the commands feel opaque, paste them into ChatGPT and ask it to walk you through each flag. The first module of our data-engineering-zoomcamp goes deeper into Docker if you want the longer version.
These are long commands we’ll run again and again, so they go in the
Makefile from lesson 02. The postgres target depends on network,
so make postgres creates the network first and then starts the
container.
Add these targets:
network:
docker network create monitoring
postgres: network
docker run -it \
--name course-assistant-pg \
--network monitoring \
-e POSTGRES_USER=user \
-e POSTGRES_PASSWORD=password \
-e POSTGRES_DB=course_assistant \
-p 5432:5432 \
-v pgdata:/var/lib/postgresql/data \
postgres:17
Now we can just run:
make postgres
To reach Postgres from Python, we install the psycopg driver:
uv add "psycopg[binary]"
Initializing the database
The table stores everything from our LLMCallRecord, plus the question
and the course. I call it conversations, which isn’t the best name. I
carried it over from materials I recorded a couple of years ago.
Something like llm_call_records would describe it better, but the name
stuck. Name yours however you like.
Two fields are worth a word. We store the course because the same
assistant can serve more than one course. Right now everything is
llm-zoomcamp, but keeping the column means we don’t have to reshape the
table when we add others. The timestamp is timezone-aware
(TIMESTAMP WITH TIME ZONE) on purpose. Without the time zone, Grafana
won’t line the data up correctly on its time axis later.
The SQL to create the table:
CREATE TABLE conversations (
id SERIAL PRIMARY KEY,
question TEXT NOT NULL,
answer TEXT NOT NULL,
course TEXT NOT NULL,
model TEXT NOT NULL,
instructions TEXT NOT NULL,
prompt TEXT NOT NULL,
prompt_tokens INTEGER NOT NULL,
completion_tokens INTEGER NOT NULL,
total_tokens INTEGER NOT NULL,
response_time FLOAT NOT NULL,
cost FLOAT NOT NULL,
timestamp TIMESTAMP WITH TIME ZONE NOT NULL
)
We can run this via psql or any other tool, but let’s create a
Python script.
Create db_init.py.
Imports:
import os
import psycopg
from datetime import datetime
DB_TIMEZONE = datetime.now().astimezone().tzinfo
print(f"Using timezone: {DB_TIMEZONE}")
A helper to connect to the database.
It uses environment variables with defaults matching the Docker container we just started:
def get_db_connection():
return psycopg.connect(
host=os.getenv("POSTGRES_HOST", "localhost"),
dbname=os.getenv("POSTGRES_DB", "course_assistant"),
user=os.getenv("POSTGRES_USER", "user"),
password=os.getenv("POSTGRES_PASSWORD", "password"),
)
The init function creates the table.
Key concepts
- OpenAI API
- RAG
- Docker
- PostgreSQL
- Prompt Engineering
- Model Monitoring
- Streamlit
- Prometheus and Grafana
Related notes
- llmz-m01-agents
- llmz-m01-building-the-prompt
- llmz-m01-data-ingestion
- llmz-m01-environment
- llmz-m01-function-calling
- llmz-m01-introduction
- llmz-m01-other-frameworks
- llmz-m01-quick-rag-revision-optional
- llmz-m01-rag
- llmz-m01-rag-helper
- llmz-m01-search
- llmz-m01-the-agentic-loop
- llmz-m01-the-course-faq-dataset
- llmz-m01-the-llm
- llmz-m01-toyaikit
- llmz-m01-wrap-up-of-part-1
- llmz-m02-embeddings
- llmz-m02-next-steps
- llmz-m02-rag-with-vector-search
- llmz-m02-using-onnx-runtime-instead-of-pytorch
- llmz-m02-vector-search
- llmz-m02-vector-search-with-minsearch
- llmz-m02-vector-search-with-pgvector
- llmz-m02-vector-search-with-sqlitesearch
- llmz-m03-ai-agents
- llmz-m03-ai-copilot
- llmz-m03-ai-orchestration
- llmz-m03-best-practices
- llmz-m03-context-engineering
- llmz-m03-multi-agent-systems
- llmz-m03-next-steps
- llmz-m03-retrieval-augmented-generation
- llmz-m03-setting-up-kestra
- llmz-m04-agent-evaluation
- llmz-m04-evaluation
- llmz-m04-generating-ground-truth-data
- llmz-m04-generating-ground-truth-for-all-documents
- llmz-m04-generating-rag-answers
- llmz-m04-llm-as-a-judge
- llmz-m04-next-steps
- llmz-m04-rag-and-agent-evaluation
- llmz-m04-search-evaluation
- llmz-m04-search-parameter-tuning
- llmz-m05-assistant
- llmz-m05-built-in-judge
- llmz-m05-capturing-metrics
- llmz-m05-chat-app
- llmz-m05-docker-compose
- llmz-m05-feedback-dashboard
- llmz-m05-grafana-dashboards
- llmz-m05-monitoring
- llmz-m05-next-steps
- llmz-m05-querying-data
- llmz-m05-streamlit-dashboard
- llmz-m05-synthetic-data-generation
- llmz-m05-user-feedback
- llmz-m06-best-practices-for-rag
- llmz-m06-next-steps
- llmz-m07-chunking-for-longer-texts
- llmz-m07-content-processing-cases-and-steps
- llmz-m07-end-to-end-project-example
- llmz-m07-evaluating-rag
- llmz-m07-evaluating-retrieval
- llmz-m07-interface-and-ingestion-pipeline
- llmz-m07-monitoring-and-containerization
- llmz-m07-summary-and-closing-remarks