Хранение иерархических данных (MySQL) для реферального маркетинга

Мне нужно иметь 5-уровневую иерархию для пользователей, зарегистрированных на веб-сайте. Каждый пользователь приглашен другим, и мне нужно знать всех потомков пользователя. А также предков для пользователя.

Я имею в виду 2 решения.

  1. Ведение таблицы с отношениями таким образом. Заключительная таблица:

    ancestor_id  descendant_id  distance
    1            1              0
    2            2              0
    3            3              0
    4            4              0
    5            5              0
    6            6              0
    2            3              1
  1. Имея эту таблицу для отношений. Хранение в таблице 5 уровней предков. Таблица "предков":

   user_id ancestor_level1_id ancestor_level2_id ancestor_level3_id ancestor_level4_id ancestor_level5_id
   10      9                  7                  4                  3                  2
   9       7                  4                  3                  2                  1

Это хорошие идеи?

Я знаю о «модели списка смежности» и «модифицированном алгоритме обхода дерева предварительного заказа», но являются ли они хорошими решениями для «реферальной» системы?

Запросы, которые мне нужно выполнить для этого дерева:

  • частое добавление новых пользователей
  • когда пользователь что-то покупает, его рефереры получают процентную комиссию
  • каждый пользователь должен иметь возможность узнать, сколько людей они порекомендовали (и сколько людей порекомендовали люди, которых они порекомендовали....) на каждом уровне

person morandi3    schedule 13.05.2011    source источник
comment
Было бы полезно, если бы вы определили «хорошо» — вы ищете скорость, гибкость, простоту обслуживания?   -  person Neville Kuyt    schedule 13.05.2011
comment
@Neville Я ищу скорость, простоту обслуживания, но также и некоторую гибкость.   -  person morandi3    schedule 14.05.2011
comment
@Neville, @morandi3: нам нужно точно знать, какие запросы вы хотите выполнить.   -  person Ken Bloom    schedule 16.05.2011


Ответы (4)


Таблица закрытия

ancestor_id  descendant_id  distance
    1            1              0
    2            2              0
    3            3              0
    4            4              0
    5            5              0
    6            6              0
    2            3              1

Чтобы добавить пользователя 10, на который ссылается пользователь 3. (я не думаю, что вам нужно блокировать таблицу между этими двумя вставками):

insert into ancestor_table
select ancestor_id, 10, distance+1
from ancestor_table
where descendant_id=3;

insert into ancestor_table values (10,10,0);

Чтобы найти всех пользователей, которых привел пользователь 3.

select descendant_id from ancestor_table where ancestor_id=3;

Чтобы подсчитать этих пользователей по глубине:

select distance, count(*) from ancestor_table where ancestor_id=3 group by distance;

Чтобы найти предков пользователя 10.

select ancestor_id, distance from ancestor_table where descendant_id=10;

Недостатком этого метода является объем памяти, который займет эта таблица.

person Ken Bloom    schedule 16.05.2011
comment
Все ваши решения выглядят хорошо: D Трудно найти лучшее. Я думаю, что я выберу это на данный момент. Но механизм хранения OQGRAPH также кажется надежным решением. Спасибо - person morandi3; 16.05.2011

Используйте механизм хранения OQGRAPH.

Возможно, вы захотите отслеживать произвольное количество уровней, а не только 5 уровней. Получите один из форков MySQL, который поддерживает движок QGRAPH (например, как MariaDB или OurDelta) и используйте его для хранения своего дерева. Он реализует модель списка смежности, но, используя специальный столбец с именем latch для отправки команды механизму хранения, сообщая ему, какой запрос выполнять, вы получаете все преимущества замыкающей таблицы без необходимости выполнять бухгалтерскую работу. каждый раз, когда кто-то регистрируется на вашем сайте.

Вот запросы, которые вы бы использовали в OQGRAPH. См. документацию по адресу http://openquery.com/graph-computation-engine-documentation.

Мы собираемся использовать origid в качестве реферера и destid в качестве реферера.

Чтобы добавить пользователя 11, привлеченного пользователем 10

insert into ancestors_table (origid,destid) values (10,11)

Чтобы найти всех пользователей, которых привел пользователь 3.

SELECT linkid FROM ancestors_table WHERE latch = 2 AND origid = 3;

Чтобы найти предков пользователя 10.

SELECT linkid FROM ancestors_table WHERE latch = 2 AND destid = 10;

Чтобы найти количество пользователей на каждом уровне, переданных пользователем 3:

SELECT count(linkid), weight
FROM ancestors_table
WHERE latch = 2 AND origid = 3
GROUP BY weight;
person Ken Bloom    schedule 15.05.2011
comment
@Ken Я не знаю, стоит ли сохранять произвольное количество уровней, скажем, у нас будет около 100 уровней. Стоит ли хранить все эти уровни в базе данных? - person morandi3; 16.05.2011
comment
@morandi3: Глядя только на технические ограничения (без обсуждения последствий для конфиденциальности), все зависит от того, как вы это делаете. Ваша таблица предков использует пространство для хранения, пропорциональное максимальному количеству отслеживаемых уровней. OQGRAPH — это механизм хранения, предназначенный специально для выполнения графовых алгоритмов. Он предназначен для выполнения таких операций, как алгоритм кратчайшего пути Дейкстры, которые обычно сложны или невозможны в базе данных SQL, за счет наличия в таблице специального столбца для выдачи команды механизму хранения. У него нет такого же штрафа за пространство. - person Ken Bloom; 16.05.2011
comment
@Ken Я не уверен, что смогу использовать MariaDB или OurDelta на своем сервере. Можно ли использовать этот движок только для таблицы из базы данных, или мне нужно изменить хранилище для всех таблиц? Какая из моих идей кажется вам самой быстрой/надежной? - person morandi3; 16.05.2011
comment
@morandi3: вам не нужно менять хранилище для всех ваших таблиц. Механизм хранения в MySQL — это плагин для создания и управления определенным типом таблиц. Типом таблицы по умолчанию в MySQL является MyISAM, и дистрибутив по умолчанию также позволяет создавать определенные таблицы с помощью механизма хранения InnoDB (для лучшего параллелизма в транзакциях). OQGRAPH — это еще один тип таблиц, которые вы можете создать, когда возникнет необходимость. Он не меняет значение по умолчанию, не меняет механизм хранения, используемый для существующих таблиц, и не заменяет механизмы хранения по умолчанию. - person Ken Bloom; 16.05.2011
comment
@morandi3: Чтобы знать, что лучше, мне нужно знать, какие запросы вы хотите выполнить. Я не думаю, что модель вложенного набора подойдет вам, так как у вас много вставок. Если вы серьезно рассматриваете 5-уровневую таблицу отношений, это может упростить использование строки предков с разделителями вместо 5 отдельных столбцов. - person Ken Bloom; 16.05.2011
comment
@Ken У нас будут запросы на: добавление нового пользователя, предоставление комиссий (процентов) предкам, когда потомок что-то покупает, каждый пользователь может знать, сколько потомков есть на каждом уровне (для этого я думаю использовать некоторые поля в база данных: count_level1, count_level2, где хранить эту информацию.Да, строка предков с разделителями должна подойти, когда мне нужно узнать предков, но мне нужно также найти потомков для каждого пользователя. - person morandi3; 16.05.2011
comment
@morandi3: теперь у меня есть 2 ответа, в которых точно указано, как выполнять каждый из ваших запросов. Ни одна из этих моделей не является очень сложной. - person Ken Bloom; 16.05.2011
comment
@morandi3: не забудьте принять тот ответ, который вы решили использовать, нажав на галочку рядом с ответом. - person Ken Bloom; 16.05.2011

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

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

Кажется, это хорошо подходит для вашей проблемы - в реферальной модели вам нужно найти дерево рефереров, которое быстро работает в модели вложенного множества; вам также необходимо знать, кто является ~детями@ данного пользователя, и глубину их отношений; это тоже быстро.

person Neville Kuyt    schedule 15.05.2011
comment
Я не думаю, что это хороший подход для моей системы, потому что вложенный набор нужно обновлять каждый раз, когда регистрируется новый пользователь, не так ли?! - person morandi3; 16.05.2011

Строка предков с разделителями

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

user_id  depth   ancestors
10       7       9,7,4,3,2,1
9        6       7,4,3,2,1
...
2        2       1
1        1       (empty string)

Вот некоторые команды SQL, которые вы могли бы использовать с этой моделью:

Чтобы добавить пользователя 11, привлеченного пользователем 10

insert into ancestors_table (user_id, depth, ancestors)
select 11, depth+1, concat(10,',',ancestors)
from ancestors_table
where user_id=10;

Чтобы найти всех пользователей, указанных пользователем 3. (Обратите внимание, что этот запрос не может использовать индекс.)

select user_id
from ancestors_table
where ancestors like '%,3,%' or ancestors like '3,%' or ancestors like '%,3';

Чтобы найти предков пользователя 10. Вам нужно разбить строку в вашей клиентской программе. В Ruby код будет ancestorscolumn.split(",").map{|x| x.to_i}. В SQL нет хорошего способа разбить строку.

select ancestors from ancestors_table where user_id=10;

Чтобы найти количество пользователей на каждом уровне, переданных пользователем 3:

select
   depth-(select depth from ancestors_table where user_id=3),
   count(*)
from ancestors_table
where ancestors like '%,3,%' or ancestors like '3,%' or ancestors like '%,3'
group by depth;

Вы можете избежать атак SQL-инъекций в like '%,3,%' частях этих запросов, используя вместо этого like concat('%,', ?, ',%') и привязывая целое число для номера пользователя к заполнителю.

person Ken Bloom    schedule 16.05.2011
comment
Поле предков @Ken не должно быть в этом случае потомками? Или для чего мы используем глубину? Потому что у каждого пользователя есть только один предок на каждом уровне. - person morandi3; 16.05.2011
comment
@morandi3: в последнем запросе вы ищете узлы, предком которых является 3, и группируете их по тому, насколько они ниже этого узла 3. Столбец depth отслеживает, сколько предков имеет определенный узел, группируя их по уровням в базе данных. - person Ken Bloom; 16.05.2011
comment
@Ken Да, но у пользователя есть только один предок/уровень - person morandi3; 16.05.2011
comment
@morandi3: я думаю, вы путаете родителя с предком. У пользователя 10 есть только один родитель (пользователь 9). А у пользователя 9 есть один родитель (пользователь 7). У пользователя 7 также есть один родитель (пользователь 4). Все эти родители и родители родителей называются предками пользователя 10. Таким образом, глубина — это количество прыжков, которое вам нужно сделать, прежде чем вы достигнете того, кого не порекомендовал друг (кто-то, кто нашел ваш сайт по рекомендации пользователя). реклама или случайный поиск). - person Ken Bloom; 16.05.2011
comment
@Ken Хорошо, теперь я понимаю, глубина - это длина цепочки. Хм, но с этим решением трудно найти потомков для пользователя. - person morandi3; 16.05.2011
comment
@morandi: поиск потомков - это второй запрос. (Помните, как и предки, потомки — это дети и дети детей и т. д.) Вы имели в виду найти только детей? - person Ken Bloom; 16.05.2011
comment
@Ken, я хотел сказать следующее: поиск потомков для определенного уровня будет медленным, потому что мы не можем использовать для этого индекс - person morandi3; 16.05.2011
comment
@morandi3: ммм да. Это минус данной модели. (Вы могли поместить индекс в поле depth, но я сомневаюсь, что это сильно поможет.) Вот вкратце, почему SQL и графики/деревья несовместимы, и почему механизм хранения OQGRAPH было изобретено. - person Ken Bloom; 16.05.2011
comment
@Ken Хм, да, OQGRAPH кажется хорошим решением. Считаете ли вы, что замыкающая таблица (расстояние ancestor_id, потомок_ид) или использование 5 полей, по одному на каждый уровень, сильно замедлит работу? - person morandi3; 16.05.2011
comment
@morandi3: я думаю, что использование 5 полей будет очень громоздким для работы. Я опубликую ответ о том, как что-то делать с таблицей закрытия, и вы можете решить, будет ли это работать. - person Ken Bloom; 16.05.2011
comment
@ morandi3: Замыкающая таблица на самом деле выглядит работоспособной, если вас не беспокоит увеличение пространства, возникающее из-за многократного перечисления каждого предка. - person Ken Bloom; 16.05.2011
comment
@Ken: я немного беспокоюсь, потому что для 50 000 пользователей у нас будет таблица с примерно 300 000 строк. - person morandi3; 16.05.2011