PDA

Просмотр полной версии : как грамотно выбрать дерево из БД?


Дикс
06.02.2009, 23:02
есть таблица с комментариями, у некоторых комментариев так называемый parent_id - "комментарии на комментарии".

искал решение грамотной выборки без миллиона запросов к бд.

нашёл вот это:
http://habrahabr.ru/blogs/sql/43955/
там огорчили, что в mysql нет нормальных средств для этого и типа пользуйтесь пхп, и ссылка вот сюда:

http://www.codenet.ru/webmast/php/tree.php

это понятно - нерациональный говнокод.

собственная мысль (сонный мозг уже ниче не соображает):

-select comments where parent_id == null
$ids = все выбранные ид, через запятую
-select comments where parent_id in($ids)

фореач comments{
если in_array(текущий ид, $parent_ids)
то выбираем к нему все комментарии второго уровня

}

в результате мы делаем тока два запроса (может можно и один сделать через иннер джоин?)
и потом выводим средставми пхп в нужном порядке.

.:EnoT:.
07.02.2009, 01:05
Совершенно не понял про массив id шников.
Но я как-то переделывал вышеупомянутую ф-цию примерно таким образом (в виде рекурсии).

Возможно какие-то ошибки допустил, писал с ходу

<?php

function tree($id, $parent_id=0){

$sql = mysql_query('SELECT * FROM `comments` WHERE `id` = '.intval($id).'
AND`parent_id` = '.$parent_id);

if($sql && mysql_num_rows($sql)){

while($res = mysql_fetch_object($sql)){

if($res->parent_id) return tree($id, $res->parent_id);
else return $res->text;
}
}
}

?>

AkyHa_MaTaTa
07.02.2009, 04:24
Имхо для таких ситуаций использования join(и не обязательно инер, для той структуры даных вполне можно и left) являеться более рациональным решением чем прибегать к нескольким запросам и тем более создавать какие то непонятные рекурсии на php которые при порядочном объеме данных и частых запросах могут сыграть злую шутку с сервером.l

Shadow_p1raT
07.02.2009, 06:37
.:EnoT:. думаю вместо intval лучше заюзать is_numeric,так как цитирую

У intval() есть интересная особенность - она возвращает TRUE если первой в аргументе содержится хотя бы одна цифра.
И у разработчиков тоже есть интересная особенность =)) -- они периодически используют intval()/(int) в логичесих условиях,
допуская непростительные ошибки.
Ведь наличие цифр в строке вовсе не гарантирует отсутствие других символов.

Пример:
/index.php?id=1'"qwerty

$id=$_GET['id'];
if(intval($id) && (int)$id)
{
sql_query("select $id from table_name");
}
else die('Id not integer!');


Несмотря на кажущуюся незначительность баги, встречается она в диком тырнете достаточно регулярно.
Для безопасного сравнения используйте is_numeric()

взято от сюда
_ttps://forum.antichat.ru/threadnav56756-1-10.html

Дикс
07.02.2009, 08:16
по поводу интвал - что за странная проверка?
я всегда использовал if(intval($_GET['s']) > 0) $s = intval($_GET['s'])

Енот
ты похоже не открывал мою вторую ссылку - я же говорю что рекурсия в данном случае говнокод, потому что плодит запросы.


tree($id, $parent_id=false){

$parent_id = $parent_id ? $parent_id : NULL;

это ваще че?))
не проще так:
tree($id, $parent_id=NULL){

AkyHa_MaTaTa
ну а как, как его использовать?

AkyHa_MaTaTa
07.02.2009, 17:14
2 Shadow_p1raT не пойму при чем здеся is_numeric() так как у .:EnoT:. в примере идет intval($id) - которая всегда вернет int значения $id даже если оно будет отричательным тоже самое касаеться и is_numeric() оно вернет true даже при отрицатенльном значении $id, но, имхо, ты прав в другом что перед гверей лутче бы проверить данные на то что они являються число(если нам нужно число) и как сказал Дикс что оно больше чем 0, но как я понял .:EnoT:. привел в качестве примера(хотя при чем тут это я не пойму так как вопрос у Дикса немного другого характера).
2 Дикс ну думаю гуглом ты умешь пользоваться не хуже чем я, там полно примеров с использование и описание JOIN в любых вариациях, вот неплохое обьяснения использования JOIN http://www.codinghorror.com/blog/archives/000976.html.

Дикс
07.02.2009, 18:14
нужны два запроса:
первый выдирает все нужные комменты с parent_id=null
второй все, у которых в parent_id - айдишники уже выбранных.

select * from comments where parent_id=null limit 0, 10
select * from comments where parent_id in(список id из первого запроса)

можно как-то объединить это в один запрос?
типа вот так:

select * from trazh_comment as c1 inner join trazh_comment as c2 on c1.parentid=null or c2.parentid = c1.cid limit 0, 10

выдаёт какую-то хрень с кучей одноименных столбцов.
прежде чем копать дальше, хочу узнать - реально ли вообще объединить те два запроса в один?

AkyHa_MaTaTa
07.02.2009, 18:49
В даном примере тебе не нужен JOIN так как у тебя 1 табличка, потому как я понял тебе надо что бы результат выборки одного запроса передовался в другой запрос для это легче и логичней использовать подзапрос, я не пойму что пытаешься зделать, у тебя 2 одинаковых запроса, то есть
select * from comments where parent_id=null limit 0, 10

вы берет все значения из comments parent_id =null а во втором запросе ты делаешь тоже самое

select * from comments where parent_id in(список id из первого запроса)

то есть фактически дублируешь 1 запрос, пытаешься
найти все значения comments у которых id из первого запроса, то есть с parent_id = null(вернее 10 значений), не совсем понятна мне структура данных которые тебе необходимо выбрать.

Pashkela
07.02.2009, 18:59
select * from comments where parent_id=null limit 0, 10
select * from comments where parent_id in(список id из первого запроса)


а вот так конкретно это одним запросом:


select * from comments where parent_id=null limit 0, 10


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

Helios
07.02.2009, 20:14
Как уже отмечали, средств для рекурсивных запросов в мускуле нет, но выбрать два уровня записей из дерева одним обращением к БД выбрать можно. Запрос будет выглядеть так:


SELECT *
FROM `comments`
WHERE `parent_id` = NULL
OR `parent_id` IN (
SELECT `id` FROM `comments` WHERE `parent_id` = NULL
)


Подзапросом мы вибираем идентификаторы тех элементов, которые не имеют родительских элементов, а основным запросом мы вибираем все те же корневые элементы и все их элементы-потомки.

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

VDShark
07.02.2009, 20:23
Для 2-х уровней Хелиос написал адекватный запрос (в отличии от большинства написанного в этом топике). Если же необходимо большее количество уровней (зачастую неопределенное) впринципе можно использовать хп, но понадобится минимум 2 курсора. Преимуществ здесь будет 2:
1) Сразу возвращаем полный и нужный нам результат
2) Сохраняется план выполнения данных запросов на серваке, что, при правильном подходе,
увеличит скорость работы.

Дикс
07.02.2009, 20:36
Helios
спасибо, как раз то, что нужно.
я не знал что в in() можно передавать запрос.

VDShark
что такое хп и курсоры?

AkyHa_MaTaTa
07.02.2009, 20:41
SELECT *
FROM `comments`
WHERE `parent_id` = NULL
OR `parent_id` IN (
SELECT `id` FROM `comments` WHERE `parent_id` =NULL)



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

VDShark
07.02.2009, 20:51
Я не вижу смысла выполнения этого запроса, критерии по которым выбераються данные при главном запросе идентичны тем которые идут в подзапросе, в итоге мы получаем тот же результат что и в основном запросе просто лишний раз проходим по таблице.

А если подумать немного? :) Выполни такие запросы и посмотри результат, м.б. натолкнет на ответ.

> что такое хп и курсоры?
ХП - сокращение от Хранимые Процедуры. Т.е. код хранящийся на стороне сервера и вызываемый по имени. А курсоры - это, грубо говоря, аналог циклов трационных языков программирования (другими словами - способ перемещения по кортежам и работы с ними, указатель на полученное отношение). Ну это я попытался рассказать попонятнее... Но можешь прочитать об этом в той же википедии (хоть там и не особо расписано) - http://ru.wikipedia.org/wiki/Курсор_(базы_данных)

AkyHa_MaTaTa
07.02.2009, 20:54
А если подумать немного? :) Выполни такие запросы и посмотри результат, м.б. натолкнет на ответ.

Может это тебе зделать(прежде чем что писать я всгда проверяю свои слова).

Helios
07.02.2009, 20:57
Я не вижу смысла выполнения этого запроса, критерии по которым выбераються данные при главном запросе идентичны тем которые идут в подзапросе, в итоге мы получаем тот же результат что и в основном запросе просто лишний раз проходим по таблице.
Запросы разные вообще-то и результаты выполнения их тоже разные, в чем можно убедиться, скормив запрос базе данных.

Подзапрос возвращает ID элементов в корне. А основной запрос делает выборку этих элементов и их потомков. Без подзапроса ты просто этих потомков не найдешь.

VDShark
07.02.2009, 20:58
Может это тебе зделать(прежде чем что писать я всгда проверяю свои слова).
Видимо данные на которых ты проверял были не учитывающими данную ситуацию... в чем я сильно сомневаюсь. Поэтому необходимо включать соображалку хотя бы иногда, а не безосновательно буровить на людей.

Дикс
07.02.2009, 21:01
а всё-таки есть проблема с таким подходом.

вот мой запрос:

SELECT *
FROM comment
WHERE tid =1
AND parentid IS NULL
OR parentid
IN (

SELECT cid
FROM comment
WHERE tid =1
AND parentid IS NULL
ORDER BY ctime ASC
)
ORDER BY ctime ASC
LIMIT 0 , 10


таким образом: сначала выполняется подзапрос. он возвращает ВСЕ id у которых tid=1 - ну типа комменты относятся к определенной теме.

а потом уже мы выбираем вложенные комменты к этим комментам.

но мне надо выбрать от n до m комментов в подзапросе, т.к. у меня пейджинг.
а mysql говорит что её версия (5.0.15-nt) не поддерживает limit в подзапросах..

как это исправить? есть ещё какие-то способы выполнить подзапрос, но с лимитом?

AkyHa_MaTaTa
07.02.2009, 21:02
Ну вобшето я даже и не предполагал на кого то буровить, если это так воспринял то извеняй, а теперь по делу, если тебе несложно покажи ту структуру данных при которых даный запрос будет работать, ибо как ты заметил моей сображалки не хватает, покажи плиз свою.

VDShark
07.02.2009, 21:05
Ну вобшето я даже и не предполагал на кого то буровить, если это так воспринял то извеняй, а теперь по делу, если тебе несложно покажи ту структуру данных при которых даный запрос будет работать, ибо как ты заметил моей сображалки не хватает, покажи плиз свою.
Извиняю, надеюсь ты действительно не имел ввиду ничего дурного :)
А по делу - если вообще не будет потомков (т.е. у всех записей будет pid=0), тогда вернутся идентичные рзультаты, иначе же будут утеряны родители (если запрос без where pid=0).

Дикс
07.02.2009, 21:11
SELECT *
FROM `comments`
WHERE `parent_id` = NULL
OR `parent_id` IN (
SELECT `id` FROM `comments` WHERE `parent_id` =NULL)

AkyHa_MaTaTa
ты и меня смутил. но я понял в чем суть.
сначала вложенный запрос выбирает все комментарии первого уровня (как раз лимита на это я и не могу поставить)
затем основной запрос выбирает опять комментарии первого уровня (где парент_ид НУЛЛ) - это кстати совсем лишнее И выбирает комментарии второго уровня, у которых парент_ид лежит в области тех ид, что выбраны в самом начале.

убираем из основного запроса "WHERE `parent_id` = NULL", и каким-то образом добавляем лимит во вложенный запрос - вот то что необходимо для того, чтобы выбрать только нужные комменты и ВСЕ их подкомменты.

oRb
07.02.2009, 21:17
Дикс, если есть возможность изменить структуру таблицы, тогда советую почитать про nested sets. Этот метод очень хорош для выборки поддерева.

Helios
07.02.2009, 21:25
Если убрать проверку на `parent_id` = NULL из основного запроса, то ты получишь только комментырии второго уровня.

Подзапрос не выбирает комментарии первого уровня, он только находит их идентификаторы, по которым мы выбираем комменты второго уровня.

Как я понял, ты берешь на страницу N комментариев первого уровня и всех их потомков. В таком случае вижу два выхода:
Первый из них - выбирать все и брать нужное средствами PHP, что совсем не айс. Потому метод отбрасываем.

Второй - все таки разделить на два запроса:

SELECT * FROM `comments` WHERE `parent_id` = NULL AND `t` = '1' LIMIT 0,5

Для выбора комментов первого уровня. Также сз полученных данных выбираешь id этих комментов и составляешь второй запрос:

SELECT *
FROM `comments`
WHERE `parent_id` IN ( $id_list )

И уже отсюда выбираешь комменты второго уровня.

Решение не из самых красивых, но это все же не рекурсия - влаживаем все в два запроса.

astrologer
07.02.2009, 21:27
Дикс, если есть возможность изменить структуру таблицы, тогда советую почитать про nested sets. Этот метод очень хорош для выборки поддерева. Этот метод действительно хорош для статичных деревьев, а дерево комментариев, очевидно, таким не является.

Дикс
08.02.2009, 10:02
http://forum.ixbt.com/topic.cgi?id=26:39140