Ok let's look at our table first. Each row has assessment results as a jsonb field. This is a special Postgres data type that allows you to query within the table. Learn more about JSONB
assessment_result field looks like this:
{
"Battle Planning": {
"Assigning heroic roles based on each hero's powers": 3,
"Clearly defining how the villain would be defeated": 1,
"Planning to minimize collateral damage to the city": 2,
"Anticipating villain traps, twists, and dirty tricks": 1,
"Reviewing the fight afterward to level up for next time": 2,
"Preparing backup plans when the first plan inevitably failed": 2
},
"Hero Team Synergy": {
"Communicating clearly during high-stakes fights": 1,
"Training and supporting new heroes joining the team": 5,
"Trusting teammates to handle their part of the fight": 2,
"Handling ego clashes and dramatic tension constructively": 2,
"Keeping morale high even when the villain had the upper hand": 5,
"Executing multi-step plans without stepping on each other's capes": 1
},
"Gadgets & Combat Tricks": {
"Keeping hero comms secure during the showdown": 5,
"Defending against villain tech, hacks, or sabotage": 5,
"Using sensors, scans, or intel to track villain activity": 1,
"Using standard battle playbooks effectively in the field": 4,
"Improvising heroically when gadgets broke or went missing": 1,
"Building, upgrading, or repurposing gadgets under pressure": 1
},
...
}
Our goal is to get average responses for each question, then choose the question that scores the best and the one that scores the worst for each category:
| category | best | worst |
|---|---|---|
| Battle Planning | (best question result) | (worst question result) |
| Gadgets & Combat Tricks | (best question result) | (worst question result) |
| Hero Team Synergy | (best question result) | (worst question result) |
jsonb_each_text() converts the JSON object into multiple rows
SELECT jsonb_each_text(assessment_result->'Battle Planning')
FROM assessments
CROSS JOIN LATERAL converts the results into columns for key and value
SELECT key, value
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Battle Planning')
Step 3 - now let's group those questions together and get average score for each question for a given category
SELECT key AS question, AVG(value::numeric) AS avg_score
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Battle Planning')
GROUP BY key
SELECT key AS question, AVG(value::numeric) AS avg_score
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Gadgets & Combat Tricks')
GROUP BY key
SELECT key AS question, AVG(value::numeric) AS avg_score
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Hero Team Synergy')
GROUP BY key
Ok now we have three separate queries. We don't want to have to hit the database 3 times for this data. What do you think we want to do next?
Use UNION ALL to join data from multiple queries
SELECT key AS question, AVG(value::numeric) AS avg_score
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Battle Planning')
GROUP BY key
UNION ALL
SELECT key AS question, AVG(value::numeric) AS avg_score
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Gadgets & Combat Tricks')
GROUP BY key
UNION ALL
SELECT key AS question, AVG(value::numeric) AS avg_score
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Hero Team Synergy')
GROUP BY key
Nice! Now all the results are together. But I can't tell what category they belong to anymore. What should I do next?
We're doing this manually so that we don't have to do an extra query to get that value. We could technically name it anything we want. Here's an example with just one category.
SELECT 'My Category' AS category, key AS question, AVG(value::numeric) AS avg_score
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Battle Planning')
GROUP BY key
And now the whole query:
SELECT 'Battle Planning' as category, key AS question, AVG(value::numeric) AS avg_score
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Battle Planning')
GROUP BY key
UNION ALL
SELECT 'Gadgets & Combat Tricks' as category, key AS question, AVG(value::numeric) AS avg_score
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Gadgets & Combat Tricks')
GROUP BY key
UNION ALL
SELECT 'Hero Team Synergy' as category, key AS question, AVG(value::numeric) AS avg_score
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Hero Team Synergy')
GROUP BY key
Ok now we want to query on our query to get the best question. This is where we need a CTE - Common Table Expression.
WITH question_avgs AS (query) allows you to save the results of your query as a temporary result set. You can then query on this set as if you were querying a table in your database. In our case, we're saving it as question_avgs.
WITH question_avgs AS (
SELECT 'Battle Planning' AS category, key AS question, AVG(value::numeric) AS avg_score
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Battle Planning')
GROUP BY key
UNION ALL
SELECT 'Gadgets & Combat Tricks', key, AVG(value::numeric)
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Gadgets & Combat Tricks')
GROUP BY key
UNION ALL
SELECT 'Hero Team Synergy', key, AVG(value::numeric)
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Hero Team Synergy')
GROUP BY key
)
SELECT category, question, avg_score
FROM question_avgs
ROW_NUMBER assigns a sequential number to each row.
PARITION BY groups rows by category name.
This combination means that each category will be sorted by avg_score and assigned a number, where 1 is the best score for each category.
WITH question_avgs AS (
SELECT 'Battle Planning' AS category, key AS question, AVG(value::numeric) AS avg_score
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Battle Planning')
GROUP BY key
UNION ALL
SELECT 'Gadgets & Combat Tricks', key, AVG(value::numeric)
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Gadgets & Combat Tricks')
GROUP BY key
UNION ALL
SELECT 'Hero Team Synergy', key, AVG(value::numeric)
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Hero Team Synergy')
GROUP BY key
)
SELECT category, question, avg_score,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY avg_score DESC, question ASC) rn
FROM question_avgs
Which one is the best? Where row number (saved as rn) is equal to 1.
WITH question_avgs AS (
SELECT 'Battle Planning' AS category, key AS question, AVG(value::numeric) AS avg_score
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Battle Planning')
GROUP BY key
UNION ALL
SELECT 'Gadgets & Combat Tricks', key, AVG(value::numeric)
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Gadgets & Combat Tricks')
GROUP BY key
UNION ALL
SELECT 'Hero Team Synergy', key, AVG(value::numeric)
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Hero Team Synergy')
GROUP BY key
)
SELECT category, question AS best_question
FROM (
SELECT category, question, avg_score,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY avg_score DESC, question ASC) rn
FROM question_avgs
) s WHERE rn = 1
This way we can start to query for worst questions too. Don't forget your comma before best!
WITH question_avgs AS (
SELECT 'Battle Planning' AS category, key AS question, AVG(value::numeric) AS avg_score
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Battle Planning')
GROUP BY key
UNION ALL
SELECT 'Gadgets & Combat Tricks', key, AVG(value::numeric)
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Gadgets & Combat Tricks')
GROUP BY key
UNION ALL
SELECT 'Hero Team Synergy', key, AVG(value::numeric)
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Hero Team Synergy')
GROUP BY key
),
best AS (
SELECT category, question AS best_question
FROM (
SELECT category, question,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY avg_score DESC, question ASC) rn
FROM question_avgs
) s WHERE rn = 1
)
SELECT
best.category,
best.best_question
FROM best
Use best format to create a worst CTE. Only difference is the ORDER BY direction is ASC. And we'll name our selected question worst_question
WITH question_avgs AS (
SELECT 'Battle Planning' AS category, key AS question, AVG(value::numeric) AS avg_score
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Battle Planning')
GROUP BY key
UNION ALL
SELECT 'Gadgets & Combat Tricks', key, AVG(value::numeric)
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Gadgets & Combat Tricks')
GROUP BY key
UNION ALL
SELECT 'Hero Team Synergy', key, AVG(value::numeric)
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Hero Team Synergy')
GROUP BY key
),
best AS (
SELECT category, question AS best_question
FROM (
SELECT category, question,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY avg_score DESC, question ASC) rn
FROM question_avgs
) s WHERE rn = 1
),
worst AS (
SELECT category, question AS worst_question
FROM (
SELECT category, question,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY avg_score ASC, question ASC) rn
FROM question_avgs
) s WHERE rn = 1
)
SELECT
worst.category,
worst.worst_question
FROM worst
JOIN worst USING (category) matches the two CTE results where category is the same. This way we can have a row for each one.
WITH question_avgs AS (
SELECT 'Battle Planning' AS category, key AS question, AVG(value::numeric) AS avg_score
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Battle Planning')
GROUP BY key
UNION ALL
SELECT 'Gadgets & Combat Tricks', key, AVG(value::numeric)
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Gadgets & Combat Tricks')
GROUP BY key
UNION ALL
SELECT 'Hero Team Synergy', key, AVG(value::numeric)
FROM assessments CROSS JOIN LATERAL jsonb_each_text(assessment_result->'Hero Team Synergy')
GROUP BY key
),
best AS (
SELECT category, question AS best_question
FROM (
SELECT category, question,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY avg_score DESC, question ASC) rn
FROM question_avgs
) s WHERE rn = 1
),
worst AS (
SELECT category, question AS worst_question
FROM (
SELECT category, question,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY avg_score ASC, question ASC) rn
FROM question_avgs
) s WHERE rn = 1
)
SELECT
worst.category,
worst.worst_question,
best.best_question
FROM best
JOIN worst USING (category)