Day 9 of 10 · Harbour Mutual

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;
One JSON object per claim: adjuster, channel, notes, items, photos and fraud_flags. Try it in the playground

Reading JSONB

OperatorReturnsExample
->a JSON valuedoc -> 'items'
->>TEXTdoc ->> '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. Try it in the playground

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;
Each claim appears once per damaged item. Try it in the playground

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. Try it in the playground

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;
Three customers called Sofia Schäfer score 0.63. A GIN index with gin_trgm_ops makes this fast on a large table. Try it in the playground

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.

JSONB suits data whose shape varies, such as a claim file. Anything you filter, join or total on every day belongs in a proper column, with a type and constraints.

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

An unhandled error has occurred. Reload 🗙

Reconnecting…

Your connection to Data Analytics SQL Lab dropped, probably a Wi-Fi blip. Hold on while it reconnects. Your work is kept.

The connection could not be restored. The page will reload in a few seconds.