Run read-only SQL against the local Postgres app database (SELECT, EXPLAIN, EXPLAIN ANALYZE on SELECT). Default local URL postgres://posthog:posthog@localhost:5432/posthog; else DATABASE_URL. Use when querying the local DB, inspecting tables, debugging data, or analyzing query plans. Mutations are strictly forbidden.
Scanned 9/1/2026
Install to Claude Code
npx -y skills add PostHog/posthog --skill querying-local-postgres --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Querying Local Postgres?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/posthog-querying-local-postgres-posthog)More formats (shields.io, HTML) on the badges page.
---
name: querying-local-postgres
description: Run read-only SQL against the local Postgres app database (SELECT, EXPLAIN, EXPLAIN ANALYZE on SELECT). Default local URL postgres://posthog:posthog@localhost:5432/posthog; else DATABASE_URL. Use when querying the local DB, inspecting tables, debugging data, or analyzing query plans. Mutations are strictly forbidden.
allowed-tools: Bash
---
# Querying local Postgres (READ-ONLY) — PostHog repo
User's query: $ARGUMENTS
**Scope:** This repo uses **PostgreSQL** for app metadata (teams, projects, flags, Django models, etc.). **Analytics event data** lives in **ClickHouse**, not Postgres — use HogQL / ClickHouse tools for `events`-style questions unless the user explicitly wants Postgres.
## When to use
- User asks to query the database, inspect tables, or run SQL against **Postgres**
- **Debugging**: Row-level checks (e.g. why a team/project/flag row looks wrong), migrations, constraints, duplicate keys
- **Performance**: `EXPLAIN` / `EXPLAIN (ANALYZE, …)` on **read-only `SELECT`** against Django or app tables
## Instructions
1. **Strictly forbid mutations** — See "Mutations strictly forbidden" below. If the user asks for any write or mutation, refuse and explain the skill is read-only.
2. **Translate** the user's question into one or more read-only SQL statements.
3. **Show the SQL** in a code block before running.
4. **Run** using the command pattern below (always with `PGOPTIONS='-c default_transaction_read_only=on'` to force a read-only connection).
5. **Show results** and give a brief interpretation (especially when used for debugging or plan review).
## Mutations strictly forbidden
**Do not run, suggest, or generate any of the following.** Refuse and state that this skill is read-only.
- **DML**: `INSERT`, `UPDATE`, `DELETE`, `MERGE`, `TRUNCATE`
- **DDL**: `CREATE`, `DROP`, `ALTER`, `RENAME`
- **Other writes**: `COPY ... TO program`, `CALL` (if it mutates), `GRANT`/`REVOKE`
- **`EXPLAIN ANALYZE` on anything other than a read-only `SELECT`** (including `WITH … SELECT`). Do not wrap DML in `EXPLAIN ANALYZE` — it would execute the write. The read-only connection below rejects writes, but the agent must not attempt this pattern.
- Any statement that modifies data, schema, or roles
**Allowed:**
- `SELECT` (including `WITH … SELECT`)
- `EXPLAIN` … `SELECT` (estimate-only plan; no execution)
- `EXPLAIN (ANALYZE, …) SELECT` — **executes** the `SELECT` once; use only for performance analysis. Must run on the read-only connection below.
- `SHOW`, `SELECT` from catalog views (`pg_stat_*`, `information_schema`, etc.) when read-only
If the user requests a write operation, say: "This skill is read-only. I can't run INSERT/UPDATE/DELETE or other mutations. Use a DB client or migration tool for writes."
## EXPLAIN and EXPLAIN ANALYZE (performance)
| Goal | What to use |
| ----------------------------------------- | ------------------------------------------------------------------------------- |
| Plan shape, estimated costs, no execution | `EXPLAIN (FORMAT TEXT, COSTS)` or add `VERBOSE` |
| Actual timings, row counts, buffer hits | `EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)` on the **`SELECT`** |
| Buffer + WAL stats | `BUFFERS` requires **`ANALYZE`**; `WAL` requires **`ANALYZE`** (PostgreSQL 13+) |
**Safe pattern:** the analyzed statement must be **only** a `SELECT` (or `WITH … SELECT`), run on the read-only connection (see Usage below). Example:
```bash
PGOPTIONS='-c default_transaction_read_only=on' psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c "
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, FORMAT TEXT)
SELECT … LIMIT 100;"
```
Optional flags (when useful): `SETTINGS` (show non-default GUCs), `WAL` (with `ANALYZE`), `TIMING` (default on in recent versions for `ANALYZE`).
**Caveats:**
- **`EXPLAIN ANALYZE` runs the query** — can be slow or heavy on large scans; prefer a bounded `SELECT` (e.g. realistic `WHERE`, `LIMIT` matching production shape) when exploring.
- **Production / shared DBs** — analyzing hot or wide queries can add load; prefer staging, a replica, or off-peak when the user cares about impact.
- **`EXPLAIN` without `ANALYZE`** — does not execute the inner statement (except some special cases); still only wrap **read-only** SQL.
## PostHog: connection and `DATABASE_URL`
### Local Postgres (host machine) — default for this skill
Use this **hardcoded** URL for day-to-day local queries (matches typical Docker Compose + port `5432` on localhost, SSL off):
| Setting | Value |
| -------- | ----------- |
| Host | `localhost` |
| Port | `5432` |
| User | `posthog` |
| Password | `posthog` |
| Database | `posthog` |
| SSL | off |
```bash
# Prefer this unless the user says their local password/db differs
LOCAL_POSTGRES_URL='postgres://posthog:posthog@localhost:5432/posthog'
```
Equivalent: `postgresql://posthog:posthog@localhost:5432/posthog`
**Other local DBs** on the same server: swap the path only, e.g. `...5432/posthog_persons`.
---
**Configuration source of truth (app):** `posthog/settings/data_stores.py` (Django `DATABASES`, optional replica `POSTHOG_POSTGRES_READ_HOST`, direct `POSTHOG_POSTGRES_DIRECT_HOST`, `PERSONS_DB_WRITER_URL`, product DB routing from `products/db_routing.yaml`).
**When not using the hardcoded URL:** Connecting **from the host** with the same credentials is documented in [Developing locally](../../../docs/published/handbook/engineering/developing-locally.md) (`fe_sendauth` troubleshooting). Ensure containers are running.
**Default env when `DEBUG` is on:** Django builds a default `DATABASE_URL` from `PGHOST` (default **`db`**), `PGUSER` / `PGPASSWORD`, `PGPORT`, `PGDATABASE` — matching **in-container** hostnames. From the **host**, use `localhost` and the same user/password/database name unless your shell already exports `DATABASE_URL`.
**Multiple PostgreSQL databases** (same server in local compose; separate logical DBs):
- Main app DB: usually `posthog`
- Persons DB: `posthog_persons` (`PERSONS_DB_WRITER_URL` / `PERSONS_DB_READER_URL`)
- Product-isolated DBs: `posthog_<name>` per `products/db_routing.yaml` (created by `docker/postgres-init-scripts/create-product-dbs.sh`)
- Other init scripts may create additional DBs (e.g. cyclotron) — inspect `docker/postgres-init-scripts/` if needed
Point `psql` at the right database by changing the path in `DATABASE_URL` (e.g. `.../posthog_persons`).
**Rust / sqlx:** Some services use `rust/.env` for `DATABASE_URL` when working from `posthog/rust` — see `rust/README.md`.
## Usage (command pattern)
**Always** force the connection read-only via `PGOPTIONS='-c default_transaction_read_only=on'` so Postgres rejects writes even if the generated SQL is wrong.
> **Why `PGOPTIONS`, not `SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY`?**
> A `psql -c "..."` string with multiple statements runs as a **single implicit transaction**.
> `SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY` only sets the default for _subsequent_
> transactions — the in-progress one keeps the read-write mode it was given at `BEGIN`, so a write
> in the same `-c` would **not** be rejected. `PGOPTIONS='-c default_transaction_read_only=on'` sets
> the GUC at connection startup, so every transaction (including the implicit `-c` one) starts
> read-only. The inline equivalent is `SET TRANSACTION READ ONLY;` **as the first statement** of the
> `-c` string (it affects the current transaction, unlike `SET SESSION CHARACTERISTICS`).
**Run from the PostHog repo root** so relative env paths resolve.
**Default — local hardcoded URL** (`posthog` / `posthog` @ `localhost:5432` / db `posthog`):
```bash
PGOPTIONS='-c default_transaction_read_only=on' psql "postgres://posthog:posthog@localhost:5432/posthog" -v ON_ERROR_STOP=1 -c "SELECT 1;"
```
**Option A — `DATABASE_URL` already in the shell** (e.g. after `flox activate` or manual `export`):
```bash
PGOPTIONS='-c default_transaction_read_only=on' psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c "SELECT 1;"
```
**Option B — load from a gitignored env file at repo root** (if `DATABASE_URL` is set there):
```bash
npx dotenv -e .env -- bash -c "PGOPTIONS='-c default_transaction_read_only=on' psql \"\$DATABASE_URL\" -c 'SELECT ...'"
```
- Use single quotes for string literals in SQL inside the shell as usual; escape carefully when nesting quotes in `-c`.
- Default `LIMIT 100` unless the user specifies otherwise.
- For wide rows use `-x`: `psql ... -x -c "..."`.
## Schema reference (PostHog)
- **Django models → tables:** see `posthog/models/` (and product packages under `products/`). Table names are usually prefixed with `posthog_` and snake-cased (e.g. `posthog_team`, `posthog_user`). Confirm with `\dt posthog_*` in psql, or check the model's `Meta.db_table` if nonstandard.
- **Migrations:** `posthog/migrations/` (and product migration paths) define the authoritative DDL over time.
- **Person table name:** configurable via `PERSON_TABLE_NAME` (see `data_stores.py`); default `posthog_person`.
## Debugging with the query runner (PostHog-flavored)
- Confirm a row exists for a team, project, user, or feature-flag linkage; check soft-delete / `deleted` fields where applicable.
- Compare counts and joins to what the app assumes (e.g. membership, project access).
- Validate replica vs primary read differences only if the user is connected to the right host (replica: `POSTHOG_POSTGRES_READ_HOST`).
- Use `EXPLAIN ANALYZE` on `SELECT` for slow Django queries **replicated as SQL** — mind loading production-sized data.
## Cross-reference
- Local setup and DB gotchas: `docs/published/handbook/engineering/developing-locally.md`
- Repo CLI: `hogli` (see `.agents/skills/hogli/SKILL.md`)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!