I have a table which contains a location of all geographical locations in the world and their relationships.
Here is a example that shows the hierarchy. You will see tha
Typically, there are three kinds of queries in the hierarchies which cause troubles:
Here's a little table which shows the performance of different methods in MySQL
:
Ancestors Descendants Children Maintainability InnoDB
Adjacency list Good Decent Excellent Easy Yes
Nested sets (classic) Poor Excellent Poor/Excellent Very hard Yes
Nested sets (spatial) Excellent Very good Poor/Excellent Very hard No
Materialized path Excellent Very good Poor/Excellent Hard Yes
In children
, poor/excellent
means that the answer depends on whether you are mixing the method with adjacency list, i. e. storing the parentID
in each record.
For your task, you need all three queries:
I would go for materialized paths, since this kind of hierarchy rarely changes (only in case of war, revolt etc).
Create a varchar column called path
, index it and fill it with the value like this:
1:234:6345:45454:
where the numbers are primary keys of the appropriate parents, in correct order (1
for Europe, 234
for UK etc.)
You will also need a table called levels
to keep numbers from 1
to 20
(or whatever maximum nesting level you want).
To select all ancestors:
SELECT pa.*
FROM places p
JOIN levels l
ON SUBSTRING_INDEX(p.path, ':', l.level) <> p.path
JOIN places pa
ON pa.path = CONCAT(SUBSTRING_INDEX(p.path, ':', l.level), ':')
WHERE p.id = @id_of_place_in_devon
To select all children and counts of places within them:
SELECT pc.*, COUNT(pp.id)
FROM places p
JOIN places pc
ON pc.parentId = p.id
JOIN places pp
ON pp.path BETWEEN pc.path AND CONCAT(pc.path, ':')
AND pp.id NOT IN
(
SELECT parentId
FROM places
)
WHERE p.id = @id_of_europe
GROUP BY
pc.id
This is the query that I came up. It is an adaption of what you suggestion Quassnoi.
SELECT pa.*, level, SUBSTRING_INDEX(p.ancestry, '/', l.level), p.*
FROM geoplanet_places p
JOIN levels l
ON SUBSTRING_INDEX(p.ancestry, '/', l.level) <> p.ancestry
JOIN geoplanet_places pa
ON pa.woeid = SUBSTRING_INDEX( SUBSTRING_INDEX(p.ancestry, '/', l.level),'/',-1)
WHERE p.woeid = "13911"
This returns all of the parents of Brighton.
The problem with your query was that it wasn't return the path to parents, but instead any node which shared the same path.
SELECT pa.*, GROUP_CONCAT(pa.name ORDER BY pa.lft asc),group_concat( pa.lft ), pa.ancestry
FROM geo_places p
JOIN levels l
ON SUBSTRING_INDEX(CONCAT(p.ancestry, p.woeid,'/'), '/', l.level) <> p.ancestry
JOIN geo_places pa
ON pa.woeid = SUBSTRING_INDEX( SUBSTRING_INDEX(CONCAT(p.ancestry, p.woeid,'/'), '/', l.level),'/',-1)
WHERE p.woeid IN ("12767488","12832668","12844837","131390","131391","12846428","24534461")
GROUP BY p.woeid