Как я могу найти всех братьев и сестер для моего узла и его предков в иерархическом дереве категорий? - PullRequest
4 голосов
/ 22 августа 2009

Это мой стол:

CREATE TABLE IF NOT EXISTS  `Category` (
`Name` varchar(25) NOT NULL,
`lft` INT UNSIGNED NOT NULL,
`rgt` INT UNSIGNED NOT NULL,
`CategoryId` int UNSIGNED auto_increment NOT NULL,
PRIMARY KEY (`CategoryId`)
) Engine = InnoDb;

У меня есть URL, который выглядит следующим образом: products.php?category=5

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

CREATE VIEW category_tree AS
SELECT node.name as name,
    node.categoryId as categoryId,
    node.lft as lft,
    node.rgt as rgt,
    (COUNT(parent.categoryId) - 1) AS depth
FROM Category AS node,
Category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.categoryId;

SELECT tree.name, tree.depth, tree.categoryId,
(node.lft BETWEEN tree.lft AND tree.rgt) AS is_selected
FROM category_tree as tree
JOIN category_tree AS node ON node.categoryId = :selectedCategory
JOIN (
    SELECT MAX(tree.lft) as lft
    FROM category_tree as tree
    JOIN category_tree AS node ON node.categoryId = :selectedCategory
    WHERE
    tree.depth = node.depth -1
    AND tree.lft < node.lft
) AS parent_finder
LEFT JOIN category_tree AS parent ON parent.lft = parent_finder.lft
WHERE tree.depth < node.depth
OR (
  tree.depth = node.depth
  AND tree.lft BETWEEN parent.lft AND parent.rgt
)
OR (
  tree.lft BETWEEN node.lft AND node.rgt
  AND tree.depth <= node.depth + 1
)
ORDER BY tree.depth, tree.name

Например, допустим, мое дерево категорий выглядит так:


(из Управление иерархическими данными в MySQL )

Допустим, пользователь выбрал «проигрыватели компакт-дисков», я хочу получить следующую информацию:

name                 depth    is_selected
electronics           0            1
portable electronics  1            1
televisions           1            0
mp3 players           2            0
cd players            2            1
2 way radios          2            0

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

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

Ответы [ 2 ]

2 голосов
/ 22 августа 2009

Я не уверен, что следую всему этому, но, похоже, вы хотите, чтобы все непосредственные дети категории 5.

Вот способ сделать это:

SELECT child.*
FROM Category parent
  JOIN Category child 
    ON (child.lft BETWEEN parent.lft AND parent.rgt)
  LEFT JOIN Category intermediate 
    ON (intermediate.lft > parent.lft AND intermediate.rgt < parent.rgt
      AND child.lft > intermediate.lft AND child.rgt < intermediate.rgt)
WHERE intermediate.CategoryId IS NULL
  AND parent.CategoryId = ?;

edit: Хорошо, теперь я понимаю, что приведенное выше решение является лишь частью того, что вы хотите. Вы хотите:

  • Прямые предки проигрывателей компакт-дисков
  • «Дяди» проигрывателей компакт-дисков (братьев и сестер предков)
  • Братья и сестры проигрывателей компакт-дисков
  • Дети CD-плееров

Позвольте мне поработать над этим несколько минут.


Вот что я придумала:

SELECT descendant.*,
  (current.lft BETWEEN descendant.lft AND descendant.rgt) AS is_selected,
  COUNT(DISTINCT c.CategoryId) AS depth
FROM Category current
JOIN Category selected 
  ON (current.lft BETWEEN selected.lft AND selected.rgt)
JOIN Category descendant 
  ON (descendant.lft BETWEEN selected.lft AND selected.rgt)
LEFT JOIN Category intermediate 
  ON (intermediate.lft > selected.lft AND intermediate.rgt < selected.rgt
    AND descendant.lft > intermediate.lft AND descendant.lft < intermediate.rgt)
JOIN Category c
  ON (descendant.lft BETWEEN c.lft AND c.rgt)
WHERE intermediate.CategoryId IS NULL
  AND current.CategoryId = ?
GROUP BY descendant.CategoryId
ORDER BY depth, descendant.name;
  • current - проигрыватели компакт-дисков
  • selected - предки CD-плееров (электроника, портативная электроника, CD-плееры)
  • descendant - это любой ребенок, внук и т. Д. Каждого selected предка
  • intermediate является потомком каждого selected предка, который также является родителем descendant - не должно быть ни одного из них, следовательно, ограничение IS NULL.
  • c - это цепочка предков от descendant до вершины для определения глубины.

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

1 голос
/ 22 августа 2009

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

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

SELECT
  Name,
  len(MyPath)/4 AS Depth,
  CASE WHEN :PathSelected LIKE MyPath + '%' THEN 1 ELSE 0 END AS Selected
FROM Category
WHERE MyPath = ''
OR (
  MyPath <> ''
  AND MyPath LIKE SUBSTRING(:PathSelected,1,ABS(LEN(MyPath) - 4)) + '____'
);

: PathSelected - это (SELECT MyPath FROM Categories WHERE ID =: CategorySelected)), которое можно предварительно вычислить или включить в запрос. Кроме того, АБС защищает от плана запроса, в котором SUBSTRING вычисляется, даже когда MyPath = '', и, следовательно, вызывает SUBSTRING с параметром отрицательной длины.

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

create table Category(
  Name varchar(25) NOT NULL,
  ID int NOT NULL PRIMARY KEY,
  MyPath varchar(200) NOT NULL
);
GO

insert into Category values
  ('ELECTRONICS',1,''),
  ('TELEVISIONS',2,'/001'),
  ('PORTABLE ELECTRONICS',3,'/002'),
  ('FLASH',4,'/002/001/001'),
  ('MP3 PLAYERS',5,'/002/001'),
  ('2 WAY RADIOS',6,'/002/002'),
  ('CD PLAYERS',7,'/002/003'),
  ('PLASMA',8,'/001/003'),
  ('LCD',9,'/001/002'),
  ('TUBE',10,'/001/001');
GO

DECLARE @Selected int = 7;
DECLARE @MyPath varchar(200) = (
  SELECT MyPath FROM Category
  WHERE ID = @Selected
);

SELECT
  Name,
  len(MyPath)/4 AS Depth,
  CASE WHEN @MyPath LIKE MyPath + '%' THEN 1 ELSE 0 END AS Selected
FROM Category
WHERE MyPath = ''
OR (
  MyPath <> ''
  AND MyPath LIKE SUBSTRING(@MyPath,1,LEN(MyPath) - 4) + '____'
);
GO

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