# Postgres Schema Expert

> Instructions for managing the PostgreSQL schema, writing SQL queries, and maintaining data integrity for the insurance analytics project.

- Skill: `fdu-ins/postgres-schema-expert` (Agent Skill)
- Install (CLI): `npx skillmds@latest add fdu-ins/postgres-schema-expert`
- Raw SKILL.md: https://api.skillmd.com/api/skills/fdu-ins/postgres-schema-expert/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Data & Analytics
- Author: FDU-INS (https://skillmd.com/u/fdu-ins)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/fdu-ins/postgres-schema-expert

---


# PostgreSQL Schema Expert Skill

This skill covers the database layer of the insurance analytics project: the `insurance` schema, table definitions, indexes, SQL patterns, and data-integrity rules.

## Schema overview

All tables live in the `insurance` schema inside the `insurdb` database.

```
insurance
├── member      – one row per enrolled health-plan member
├── provider    – one row per healthcare provider
└── claim       – one row per insurance claim (child of member + provider)
```

The DDL lives in `src/sql/ddl_create_tables.sql` and is applied with:

```bash
# DATABASE_URL uses the SQLAlchemy format (postgresql+psycopg2://...).
# psql requires a driver-free URL — strip the +psycopg2 prefix:
PSQL_URL="${DATABASE_URL/+psycopg2/}"
psql "$PSQL_URL" -f src/sql/ddl_create_tables.sql
# or use the Makefile shortcut (handles the URL conversion for you):
make db-init
```

## Table definitions

### `insurance.member`

| Column | Type | Notes |
|---|---|---|
| `member_id` | `BIGSERIAL` PRIMARY KEY | Auto-incrementing surrogate key |
| `person_id` | `BIGINT` | External identifier |
| `dob` | `DATE` | Date of birth — used to derive age in Python |
| `gender` | `VARCHAR(10)` | `'male'` / `'female'` / `'U'` — Kaggle datasets map the source `sex` column to `gender`; `'U'` is the default when gender is absent |
| `region` | `VARCHAR(50)` | Geographic region (e.g. `'Northeast'`) |
| `effective_date` | `DATE` | Coverage start date |
| `termination_date` | `DATE` | Coverage end date (`NULL` = still active) |

### `insurance.provider`

| Column | Type | Notes |
|---|---|---|
| `provider_id` | `BIGSERIAL` PRIMARY KEY | Auto-incrementing surrogate key |
| `specialty` | `VARCHAR(80)` | Medical specialty (e.g. `'Cardiology'`) |
| `in_network` | `BOOLEAN` | `TRUE` = in-network, `FALSE` = out-of-network |
| `region` | `VARCHAR(50)` | Geographic region of the provider |

### `insurance.claim`

| Column | Type | Notes |
|---|---|---|
| `claim_id` | `BIGSERIAL` PRIMARY KEY | Auto-incrementing surrogate key |
| `member_id` | `BIGINT` FK → `member` | Required — every claim belongs to a member |
| `provider_id` | `BIGINT` FK → `provider` | Required — every claim belongs to a provider |
| `service_date` | `DATE` | Date the service was rendered |
| `diagnosis_code` | `VARCHAR(8)` | ICD-10 code (e.g. `'Z00.00'`) |
| `procedure_code` | `VARCHAR(8)` | CPT code (e.g. `'99213'`) |
| `billed_amount` | `NUMERIC(12,2)` | Amount the provider billed |
| `allowed_amount` | `NUMERIC(12,2)` | Maximum amount the plan will pay |
| `paid_amount` | `NUMERIC(12,2)` | Amount actually paid by the plan |
| `place_of_service` | `VARCHAR(20)` | E.g. `'Office'`, `'Hospital'` |

## Indexes

```sql
-- Speeds up joins from claim → member
CREATE INDEX IF NOT EXISTS idx_claim_member      ON insurance.claim(member_id);
-- Speeds up date-range queries on claims
CREATE INDEX IF NOT EXISTS idx_claim_service_date ON insurance.claim(service_date);
```

Add new indexes only when a slow query is identified. Over-indexing slows down INSERT/UPDATE.

## Common SQL query patterns

### Monthly paid totals by region

```sql
SELECT
    DATE_TRUNC('month', c.service_date) AS month,
    m.region,
    COUNT(*)                            AS claim_count,
    SUM(c.paid_amount)                  AS paid_total
FROM insurance.claim c
JOIN insurance.member m ON c.member_id = m.member_id
GROUP BY 1, 2
ORDER BY 1, 2;
```

### In-network vs out-of-network comparison

```sql
SELECT
    p.in_network,
    COUNT(*)           AS claims,
    AVG(c.paid_amount) AS avg_paid,
    SUM(c.paid_amount) AS total_paid
FROM insurance.claim c
JOIN insurance.provider p ON c.provider_id = p.provider_id
GROUP BY p.in_network;
```

### Claims for currently active members only

```sql
SELECT c.*
FROM insurance.claim c
JOIN insurance.member m ON c.member_id = m.member_id
WHERE m.termination_date IS NULL
   OR m.termination_date > CURRENT_DATE;
```

### Top 10 providers by paid amount

```sql
SELECT
    c.provider_id,
    p.specialty,
    SUM(c.paid_amount) AS paid_total
FROM insurance.claim c
JOIN insurance.provider p ON c.provider_id = p.provider_id
GROUP BY 1, 2
ORDER BY paid_total DESC
LIMIT 10;
```

## Resetting and re-initialising the database

```bash
# Drop and recreate the schema, then re-apply DDL
make db-reset

# Equivalent raw SQL (strip the +psycopg2 driver prefix for psql compatibility)
PSQL_URL="${DATABASE_URL/+psycopg2/}"
psql "$PSQL_URL" -c "DROP SCHEMA IF EXISTS insurance CASCADE;"
psql "$PSQL_URL" -c "CREATE SCHEMA insurance;"
psql "$PSQL_URL" -f src/sql/ddl_create_tables.sql
```

## Load behaviour and idempotency

`src/load.py` uses a **truncate-then-load** strategy so repeated runs are safe:

```sql
-- Runs inside a single transaction
TRUNCATE insurance.claim, insurance.provider, insurance.member CASCADE;
```

This means every `make kaggle-load` call replaces all data. Do not run it on a production database with live data unless you intend to replace everything.

## Data-integrity rules

1. **Foreign keys** – Every `claim` must reference an existing `member` and `provider`. PostgreSQL enforces this automatically.
2. **Numeric scale** – `NUMERIC(12,2)` stores up to 10 digits before the decimal and exactly 2 after. Never store amounts as `FLOAT` — floating-point rounding causes accounting errors.
3. **Date columns** – Always store dates as `DATE`, never as `VARCHAR`. This enables range comparisons and `DATE_TRUNC` without casting.
4. **Schema isolation** – All objects live in the `insurance` schema, not `public`. This keeps the database clean if other schemas are added later.

## Environment variable

The application connects using the `DATABASE_URL` environment variable:

```
DATABASE_URL=postgresql+psycopg2://postgres:postgres@localhost:5432/insurdb
```

The `psql` CLI accepts a slightly different format (no driver prefix):

```bash
psql "postgresql://postgres:postgres@localhost:5432/insurdb"
```

