Home > Software design >  Query a nested data structure for locations
Query a nested data structure for locations

Time:09-14

I have this locations table:

id type slug parent_location_id
1 Country Germany nil
2 FederalState Hessen 1
3 City Frankfurt 2
4 Street Example 3

I have a tags table which has a belongs_to logic to the locations table:

id name location_id
1 blue 1
2 yellow 2
3 red 2
4 orange 3
5 white 4
6 black 4

All tags of a location are displayed on webpages with this URL schema:

  • http://localhost:4000/Germany
  • http://localhost:4000/Germany/Hessen
  • http://localhost:4000/Germany/Hessen/Frankfurt
  • http://localhost:4000/Germany/Hessen/Frankfurt/Example
  • http://localhost:4000/:country_slug/:federal_state_slug/:city_slug/:street_slug

To fetch all the tags for the last URL which would include all the tags for the Country, the FederalState, the City and the Street I run down the tree and query for the :country_slug first, then for the :federal_state_slug, then for the :city_slug and lastly for the :street_slug. Always in combination with the parent_location_id to make sure that it makes sense within the location tree. After that I combine all the location ids and run a select on tags. Needless to say that this takes for ever since I run 5 SQL queries in the worst case (for a street) and 2 in the best case (for a country).

I can't help but think that there must be a better data structure (I am happy to restructure the database) or some SQL magic so get this done with one SQL query. Is there?

CodePudding user response:

You can use a recursive cte, in which you join your tags table onto each recursive query. The cte builds a JSON array of tag ids, which you can query later on:

with recursive cte(id, url, tag_path) as (
   select l.id, 'http://localhost:4000/'||l.slug, ('['||t.id||']')::jsonb 
   from locations l join tags t on t.location_id = l.id where l.parent_location_id is null
   union all
   select l.id, c.url||'/'||l.slug, c.tag_path||(t.id::text::jsonb) 
   from locations l join cte c on c.id = l.parent_location_id 
   join tags t on t.location_id = l.id
)
select * from cte;

See fiddle.

  • Related