'Recursive/iterative reduce in PostgreSQL
I'm battling with recursive CTEs at the moment as I need to reference the resulting output of the CTE so far in each loop.
What I'm trying to do is take a table like this:
| suggestions |
| ----------- |
| id | label |
| ----------- |
| 1 | item-1 |
| 1 | item-2 |
| 2 | item-1 |
| 2 | item-2 |
| 3 | item-1 |
| 3 | item-2 |
And reduce it to one selected row per id:
| selected_suggestions |
| -------------------- |
| id | label | taken |
| -------------------- |
| 1 | item-1 | false |
| 2 | item-2 | false | (item-1 was taken by id=1, but item-2 was available)
| 3 | item-1 | true | (item-1 and item-2 both taken)
So for each id, it selects one of the suggestions that ideally hasn't already been taken by a prior id.
This is my attempt so far:
with recursive suggestions (id, label) as (
values
(1, 'item-1'),
(1, 'item-2'),
(2, 'item-1'),
(2, 'item-2'),
(3, 'item-1'),
(3, 'item-2')
),
chosen_suggestions as (
-- these are the rows we're iterating over
-- they are all set to selected = false so they can be filtered out at the end
(
select
*,
false as taken,
false as selected
from
suggestions
order by
id asc,
label asc
)
union
-- for each of the rows above, Postgres calls this section
(
select
suggestions.*,
count(already_chosen_suggestions) > 0 as taken,
true as selected
from
suggestions
-- join any suggestions already selected with the same label
left join chosen_suggestions already_chosen_suggestions on
already_chosen_suggestions.label = suggestions.label and
already_chosen_suggestions.selected = true
-- select all the suggestions for this row, where we don't already have a suggestion for this item (so we don't select multiple selections for a given `id`)
where
suggestions.id = chosen_suggestions.id and
suggestions.id not in (select id from chosen_suggestions where selected = true)
-- favour unused suggestions, then default to alphabetical
order by
case when count(already_chosen_suggestions) = 0 then 0 else 1 end asc,
label asc
-- we only want one suggestion per original row
limit 1
)
)
-- filter out all the unselected suggestions
select id, label, taken from chosen_suggestions where selected = true;
Unfortunately, this gives me a syntax error:
recursive reference to query "chosen_suggestions" must not appear within an outer join
I've tried replacing with subqueries, but this also is not allowed by PostgreSQL. At this point, I'm wondering if there's a wholly different approach I should be considering that I'm simply not aware of?
Solution 1:[1]
I've solved the problem with pgplsql:
create type suggestion as (id int, label text, selected boolean);
create function select_suggestions(suggestions suggestion[]) returns setof suggestion as $$
declare
current_record suggestion;
chosen_suggestion suggestion;
chosen_suggestions suggestion[];
begin
chosen_suggestions := array[]::suggestion[];
<<input_selections>>
foreach current_record in array suggestions
loop
foreach chosen_suggestion in array chosen_suggestions
loop
if chosen_suggestion.id = current_record.id or chosen_suggestion.label = current_record.label then
continue input_selections;
end if;
end loop;
chosen_suggestions := array_append(chosen_suggestions, current_record);
end loop;
return query
select
suggestions.id,
suggestions.label,
chosen_suggestions.label is not null as selected
from
unnest(suggestions) as suggestions(id, label)
left join
unnest(chosen_suggestions) as chosen_suggestions(id, label) on
chosen_suggestions.id = suggestions.id and
chosen_suggestions.label = suggestions.label;
end;
$$ language plpgsql;
with suggestions (id, label) as (
values
(1, 'item-1'),
(1, 'item-2'),
(2, 'item-1'),
(2, 'item-2'),
(3, 'item-1'),
(3, 'item-2'),
(4, 'item-4')
)
select distinct on (id)
id,
case
when selected = true then label
else null
end as label
from
select_suggestions(array(select (id, label, false)::suggestion from suggestions))
order by
id asc,
selected desc;
This outputs the rows as expected:
id | label
---+-------
1 | item-1
2 | item-2
3 | (null)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Ryan Townsend |
