Сложный иерархический запрос SQL нужна помощь, пожалуйста! - PullRequest
2 голосов
/ 12 апреля 2011

Вызов всех гуру mySQL!

Мне нужен сложный запрос для mySQL, но я не могу разобраться с ним. Есть 2 таблицы в вопросе:

Места
(столбцы: location_id, parent, location)
Данные иерархически разбиты на страну, регион, округ и город, таким образом:
1, 0, Англия ( страна )
2, 1, Юго-Запад ( регион )
3, 1, Юго-Восток ( регион )
4, 2, Дорсет ( округ )
5, 4, Борнмут ( город )
6, 4, Пул ( город )
7, 4, Уимборн ( город )
и т.д. до 400+ строк данных о местоположении

профили
(столбцы: profile_id, title, location_id)
Каждая строка имеет один идентификатор местоположения, который ВСЕГДА является городом (т. Е. Последним потомком). Например:
1, 'Этот профиль имеет местоположение, установленное как Борнмут', 5
2, 'Этот профиль имеет местоположение, заданное как Poole', 6
и т.д.

Что мне нужно сделать, так это вернуть все идентификаторы из таблицы Locations, где с ней или ее дочерними элементами связаны записи. Поэтому в приведенном выше примере мне понадобятся следующие идентификаторы местоположения: 1, 2, 4, 5, 6

Причины:
1 - ДА, Англия является родителем юго-запада, Дорсет и Борнмут, который имеет запись
2 - ДА, Юго-Запад является родителем Дорсета и Борнмута, который имеет запись
3 - НЕТ, на Юго-Востоке нет записей или его детей
4 - ДА, Дорсет является родителем Борнмута, у которого есть запись
5 - ДА, в Борнмуте есть запись
6 - ДА, у Пула есть запись
7 - НЕТ, Wimborne не имеет записей

Итак, это действительно возможно? Я пытался сделать это в PHP с вложенными запросами SQL, но время ожидания скрипта истекло, поэтому должен быть способ сделать это только в запросе SQL?

Заранее благодарю! :)

===========

UPDATE

Прочитав и поиграв со всеми этими решениями, я понял, что решаю проблему совершенно неправильно. Вместо того, чтобы проходить через все местоположения и возвращать те, у которых есть записи, это имеет больше смысла и гораздо эффективнее получить все записи и вернуть соответствующие местоположения, а затем пройти вверх по иерархии, чтобы получить каждого родителя местоположения, пока не будет достигнут корень.

Большое спасибо за вашу помощь, по крайней мере, я понял, что то, что я пытался, было ненужным.

Ответы [ 4 ]

0 голосов
/ 12 апреля 2011

Не уверен, что я полностью понимаю ваши требования, но следующий пример хранимой процедуры может быть хорошей отправной точкой для вас:

Пример звонков (обратите внимание на включенный столбец)

mysql> call location_hier(1);
+-------------+---------------------+--------------------+---------------------+-------+----------+
| location_id | location            | parent_location_id | parent_location     | depth | included |
+-------------+---------------------+--------------------+---------------------+-------+----------+
|           1 | England (country)   |               NULL | NULL                |     0 |        1 |
|           2 | South West (region) |                  1 | England (country)   |     1 |        1 |
|           3 | South East (region) |                  1 | England (country)   |     1 |        0 |
|           4 | Dorset (county)     |                  2 | South West (region) |     2 |        1 |
|           5 | Bournemouth (town)  |                  4 | Dorset (county)     |     3 |        1 |
|           6 | Poole (town)        |                  4 | Dorset (county)     |     3 |        1 |
|           7 | Wimborne (town)     |                  4 | Dorset (county)     |     3 |        0 |
+-------------+---------------------+--------------------+---------------------+-------+----------+
7 rows in set (0.00 sec)

Вы бы вызвали хранимую процедуру из php следующим образом:

$startLocationID = 1;
$result = $conn->query(sprintf("call location_hier(%d)", $startLocationID));

Полный скрипт:

http://pastie.org/1785995

drop table if exists profiles;
create table profiles
(
profile_id smallint unsigned not null auto_increment primary key,
location_id smallint unsigned null,
key (location_id)
)
engine = innodb;

insert into profiles (location_id) values (5),(6);

drop table if exists locations;
create table locations
(
location_id smallint unsigned not null auto_increment primary key,
location varchar(255) not null,
parent_location_id smallint unsigned null,
key (parent_location_id)
)
engine = innodb;

insert into locations (location, parent_location_id) values
('England (country)',null), 
    ('South West (region)',1),
    ('South East (region)',1),
        ('Dorset (county)',2),
            ('Bournemouth (town)',4),
            ('Poole (town)',4),
            ('Wimborne (town)',4);


drop procedure if exists location_hier;
delimiter #

create procedure location_hier
(
in p_location_id smallint unsigned
)
begin

declare v_done tinyint unsigned default 0;
declare v_depth smallint unsigned default 0;

create temporary table hier(
 parent_location_id smallint unsigned, 
 location_id smallint unsigned, 
 depth smallint unsigned default 0,
 included tinyint unsigned default 0,
 primary key (location_id),
 key (parent_location_id)
)engine = memory;

insert into hier select parent_location_id, location_id, v_depth, 0 from locations where location_id = p_location_id;
create temporary table tmp engine=memory select * from hier;

/* http://dev.mysql.com/doc/refman/5.0/en/temporary-table-problems.html */

while not v_done do

    if exists( select 1 from locations c
        inner join tmp on c.parent_location_id = tmp.location_id and tmp.depth = v_depth) then

        insert into hier select c.parent_location_id, c.location_id, v_depth + 1, 0 from locations c
            inner join tmp on c.parent_location_id = tmp.location_id and tmp.depth = v_depth;

    update hier inner join tmp on hier.location_id = tmp.parent_location_id
    set hier.included = 1;

        set v_depth = v_depth + 1;          

        truncate table tmp;
        insert into tmp select * from hier where depth = v_depth;

    else
        set v_done = 1;
    end if;

end while;

update hier inner join tmp on hier.location_id = tmp.parent_location_id
set hier.included = 1;

-- include any locations that have profiles ???

update hier inner join profiles on hier.location_id = profiles.location_id
set hier.included = 1;

-- output the results

select 
 c.location_id,
 c.location as location,
 p.location_id as parent_location_id,
 p.location as parent_location,
 hier.depth,
 hier.included
from 
 hier
inner join locations c on hier.location_id = c.location_id
left outer join locations p on hier.parent_location_id = p.location_id
-- where included = 1 -- filter in your php or here up to you !
order by
 hier.depth;

-- clean up

drop temporary table if exists hier;
drop temporary table if exists tmp;

end #

delimiter ;

call location_hier(1);

Надеюсь, это поможет:)

0 голосов
/ 12 апреля 2011

Если ваш php-код был хорошим, вы могли бы иметь вложенный цикл в fd [location -> parent]. Я бы начал там сначала и просто использовал PHP. Я не думаю, что SQL имеет рекурсивную функцию.

Если вам НУЖЕН вложенный родительский цикл, вы должны написать мутацию алгоритма слияния | объединения, чтобы решить эту проблему.

Чтобы найти вложенный цикл в PHP

$ids = array();
function nestedLoopFinder($parent)
{
    global $ids;
    $result = mysql_query("SELECT location_id FROM locations WHERE parent=$parent");
    while($row = mysql_fetch_object($result))
    {
        if(in_array($row->location_id, $ids)) {
            die("duplicate found: $row->location_id");
        }
        $ids[] = $row->location_id;

        //recurse
        nestedLoopFinder($row->location_id);
    }
}
0 голосов
/ 12 апреля 2011

То, как я справился с этим, выполняет только одну загрузку SQL, а затем помещает ссылки внутри родительских объектов.

    $locations = array();

        $obj_query = "SELECT * from locations";
        $result_resource = mysql_query($obj_query);
        while ($row = mysql_fetch_assoc($result_resource) {
            $locations[$row['location_id'] = (object) $row;
        }

        foreach ($locations as $location) {
            if (isset($location->parent) {
                $locations[$location->parent]->children[] = $location;
            }
        }

Вашему объекту потребуется такой метод, чтобы определить, является ли местоположение потомком:


function IsAnscestorOF ($location) {
    if (empty($children)) { return false; }
    if (in_array($location, keys($this->children) {
        return true;
    } else {
        foreach ($children as $child) {
             if ($child->isAnscestor) {
                 return true;
             }
         }
     }
     return false;
}

0 голосов
/ 12 апреля 2011

Тот факт, что время ожидания вашего скрипта будет указывать на бесконечный цикл где-то

Учитывая, что вы делаете ссылку на таблицу расположений на основе дочерней области, плюс еще одну ссылку на родительскую область, вам, вероятно, придется использовать комбинацию PHP и Mysql для прокрутки всего этого - простой оператор JOIN будет не работает в этом случае, я не думаю.

Также вам нужно изменить таблицу так, чтобы, если это страница верхнего уровня, она имела parent_id NULL, а не 0. После того, как вы это сделали ..

$sql = "SELECT * FROM locations WHERE parent =''";
$result = mysql_query($sql);
while($country = mysql_fetch_array($result)) {

$subsql = "SELECT * FROM locations WHERE parent='".$country['id']."'";
$subresult = mysql_query($subsql);
while($subregion = mysql_fetch_array($subresult)) {

$profilesql = "SELECT * FROM profiles WHERE location_id='".$subregion['id']."'";
$profileresult = mysql_query($profilesql); 

echo mysql_num_rows($profileresult).' rows under '.$subregion['location'].'.<br />';
}

}

Базовый код есть ... у кого-нибудь есть умная идея заставить его работать с различными подуровнями? Но, честно говоря, если бы это был мой проект, я бы сделал отдельные таблицы для страны, а затем регионов, а затем города / города. 3 таблицы значительно облегчили бы навигацию по данным.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...