JSONB and extensions
Work with semi-structured data: read and search the adjusters' JSONB files, index them with GIN, and add capabilities with extensions such as pg_trgm for fuzzy search.
Theory for today: Fraud and data quality, Losses and claims, JSON and extensions.
The lesson
Not everything fits neatly in columns. An adjuster's file on a claim holds notes, a list of damaged items with their costs, the number of photos, and any fraud flags; a burglary file lists many items, a windscreen file none. Harbour Mutual keeps these files in claim_documents.doc, a JSONB column: JSON, stored in a binary form PostgreSQL can search and index.
SELECT doc
FROM claim_documents
LIMIT 3;
adjuster, channel, notes, items, photos and fraud_flags.
Reading JSONB
| Operator | Returns | Example |
|---|---|---|
-> | a JSON value | doc -> 'items' |
->> | TEXT | doc ->> 'adjuster' |
@> | does the left contain the right? | doc @> '{"channel": "broker"}' |
? | does the array or object contain this string? | doc -> 'fraud_flags' ? 'reported late' |
SELECT doc ->> 'adjuster' AS adjuster, COUNT(*) AS files
FROM claim_documents
GROUP BY 1
ORDER BY files DESC;
->> returns text, which can be grouped, filtered and joined like any column. Cast it for arithmetic: (doc ->> 'photos')::INT.
Arrays into rows
JSONB_ARRAY_ELEMENTS turns a JSON array into one row per element (and JSONB_ARRAY_ELEMENTS_TEXT into text), so the items in every file can be counted and summed. Used in FROM after the table, it runs once per row, like a join.
SELECT c.claim_number, i ->> 'item' AS item, (i ->> 'cost')::NUMERIC AS cost
FROM claim_documents d
JOIN claims c USING (claim_id),
JSONB_ARRAY_ELEMENTS(d.doc -> 'items') i
LIMIT 10;
Changing JSONB
JSONB_SET(doc, '{key}', value) replaces one key's value; || joins two JSON arrays or merges two objects; - removes a key. The column is updated as a whole, with a normal UPDATE.
GIN indexes
A normal (B-tree) index sorts whole values, which does not help to find the documents that contain a key or value. A GIN index (generalised inverted index) records, for every key and value, the rows that contain it, like the index of a book listing every page a word appears on. It serves @> and ?.
CREATE INDEX claim_documents_doc ON claim_documents USING GIN (doc jsonb_path_ops);
SELECT COUNT(*)
FROM claim_documents
WHERE doc @> '{"channel": "broker"}';
jsonb_path_ops makes a smaller index that serves @>. On 1,534 files PostgreSQL may still prefer to read the whole table; on millions, it would not.
Extensions
PostgreSQL grows through extensions, added to a database with CREATE EXTENSION. pg_trgm compares text by its trigrams (sequences of three letters), so it can find names that are spelt nearly the same: a caller's 'Schafer' still finds 'Schäfer'. SIMILARITY(a, b) scores two texts from 0 to 1, and a % b is true when the score is above 0.3.
CREATE EXTENSION IF NOT EXISTS pg_trgm;
SELECT name, SIMILARITY(name, 'Sofia Schafer') AS score
FROM customers
WHERE name % 'Sofia Schafer'
ORDER BY score DESC
LIMIT 5;
gin_trgm_ops makes this fast on a large table.
Other extensions you will meet: pgcrypto for hashing and encryption, uuid-ossp for identifiers (plain GEN_RANDOM_UUID() is now built in), PostGIS for maps. In this lab pg_trgm is installed; managed cloud databases list the extensions they allow.
Briefs from the business
0 of 6 done
Write SQL to answer each brief, run it, then check your answer against Harbour Mutual's. Your database is the same one as in the playground.
Starting PostgreSQL in your browser…
Brief 9.1 · The adjusters' workload
The claims manager wants to know how many claim files each adjuster holds. Show the adjuster (from each file's adjuster key) and the number of files.
Tables: claim_documents
Brief 9.2 · Which red flags come up?
The fraud team wants to know how often each fraud flag is raised. Show each flag and the number of claims with it.
Tables: claim_documents
Brief 9.3 · What gets damaged most?
Which five damaged items cost the most in total across all claim files? Show the item, how many times it was claimed, and the total cost.
Tables: claim_documents
Brief 9.4 · Index the claim files
The fraud team searches the files by containment all day, such as doc @> '{"channel": "broker"}'. Create a GIN index on claim_documents.doc to serve these searches.
Tables: claim_documents
Brief 9.5 · Who is on the phone?
A caller says she is Sofia Schafer, without the umlaut, and she is not sure how the name was recorded. Using pg_trgm, find the five customers whose names are most similar to 'Sofia Schafer'. Show the customer id, name and similarity, to 2 decimals, most similar first.
Tables: customers
Brief 9.6 · Mark a claim for manual review
The largest open claim, CL-2025-01525, is to be reviewed by hand. Add the flag 'manual review' to its file's fraud_flags, keeping any flags already there. Running your update twice must not add it twice.
Tables: claim_documents, claims