MySQL включение / исключение постов - PullRequest
4 голосов
/ 11 октября 2010

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

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

ID | owner_id | post | other_info | privacy_level (int value)

Оттуда пользователи могут добавлять свои данные о конфиденциальности, позволяя просматривать их всем [privacy_level = 0), друзьям (privacy_level = 1), никому (privacy_level = 3) или конкретным людям.или фильтры (privacy_level = 4).Для уровней конфиденциальности, указывающих конкретных людей (4), запрос будет ссылаться на таблицу "post_privacy_include_for" в подзапросе, чтобы увидеть, существует ли пользователь (или фильтр, которому принадлежит пользователь) в строке в таблице.

ID | post_id | user_id | list_id

Кроме того, пользователь имеет возможность запретить некоторым людям просматривать свои посты в рамках большой группы, исключая их (например, установив его на всеобщее обозрение, но скрывая его от преследователя).Для этого добавлена ​​еще одна справочная таблица, "post_privacy_exclude_from" - она ​​выглядит идентично настройке как "post_privacy_include_for".

Моя проблема в том, что это не масштабируется.Совсем.На данный момент существует около 1-2 миллионов сообщений, большинство из которых должны быть доступны для просмотра всем.Для каждого поста на странице он должен проверить, есть ли строка, которая исключает пост для показа пользователю - это продвигается очень медленно на странице, которая может быть заполнена 100-200 постами.Это может занять до 2-4 секунд, особенно когда к запросу добавляются дополнительные ограничения.

Это также создает чрезвычайно большие и сложные запросы, которые просто ... неудобны.

SELECT t.*
FROM posts t
WHERE ( (t.privacy_level = 3
         AND t.owner_id = ?)
       OR (t.privacy_level = 4
           AND EXISTS
             ( SELECT i.id
              FROM PostPrivacyIncludeFor i
              WHERE i.user_id = ?
                AND i.thought_id = t.id)
           OR t.privacy_level = 4
           AND t.owner_id = ?)
       OR (t.privacy_level = 4
           AND EXISTS
             (SELECT i2.id
              FROM PostPrivacyIncludeFor i2
              WHERE i2.thought_id = t.id
                AND EXISTS
                  (SELECT r.id
                   FROM FriendFilterIds r
                   WHERE r.list_id = i2.list_id
                     AND r.friend_id = ?))
           OR t.privacy_level = 4
           AND t.owner_id = ?)
       OR (t.privacy_level = 1
           AND EXISTS
             (SELECT G.id
              FROM Following G
              WHERE follower_id = t.owner_id
                AND following_id = ?
                AND friend = 1)
           OR t.privacy_level = 1
           AND t.owner_id = ?)
       OR (NOT EXISTS
             (SELECT e.id
              FROM PostPrivacyExcludeFrom e
              WHERE e.thought_id = t.id
                AND e.user_id = ?
                AND NOT EXISTS
                  (SELECT e2.id
                   FROM PostPrivacyExcludeFrom e2
                   WHERE e2.thought_id = t.id
                     AND EXISTS
                       (SELECT l.id
                        FROM FriendFilterIds l
                        WHERE l.list_id = e2.list_id
                          AND l.friend_id = ?)))
           AND t.privacy_level IN (0, 1, 4))
  AND t.owner_id = ?
ORDER BY t.created_at LIMIT 100

(смоделируйте запрос, похожий на запрос, который я сейчас использую в Doctrine ORM. Это беспорядок, но вы понимаете, о чем я говорю.)

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

Спасибо, ребята.

Обновлено: исправлен запрос, отражающий значенияЯ определил для уровня конфиденциальности выше (я забыл обновить его, потому что я упростил значения)

Ответы [ 2 ]

1 голос
/ 11 октября 2010

Ваш запрос слишком длинный, чтобы дать окончательное решение, но подход, который я бы использовал, заключается в простом поиске данных путем преобразования подзапросов в объединения, а затем встраивания логики в предложение where и список столбцов спискаоператор SELECT:

select t.*, i.*, r.*, G.*, e.* from posts t
left join PostPrivacyIncludeFor i on i.user_id = ? and i.thought_id = t.id
left join FriendFilterIds r on r.list_id = i.list_id and r.friend_id = ?
left join Following G on follower_id = t.owner_id and G.following_id = ? and G.friend=1
left join PostPrivacyExcludeFrom e on e.thought_id = t.id and e.user_id = ? 

(Возможно, потребуется расширить: я не мог следовать логике заключительного предложения.)

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

0 голосов
/ 11 октября 2010

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

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

call list_user_filtered_posts( <user_id>, <day_interval> );

Весь сценарий можно найти здесь: http://pastie.org/1212812

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

Таблицы

Удалил вашу таблицу post_privacy_exclude_from и добавил таблицу user_stalkers, которая работает довольноочень похоже на инверсию user_friends.Сохраняйте исходную таблицу post_privacy_includes_for в соответствии с вашим дизайном, поскольку это позволяет пользователю ограничить конкретную публикацию подмножеством людей.

drop table if exists users;
create table users
(
user_id int unsigned not null auto_increment primary key,
username varbinary(32) unique not null
)
engine=innodb;


drop table if exists user_friends;
create table user_friends
(
user_id int unsigned not null,
friend_user_id int unsigned not null,
primary key (user_id, friend_user_id)
)
engine=innodb;


drop table if exists user_stalkers;
create table user_stalkers
(
user_id int unsigned not null,
stalker_user_id int unsigned not null,
primary key (user_id, stalker_user_id)
)
engine=innodb;


drop table if exists posts;
create table posts
(
post_id int unsigned not null auto_increment primary key,
user_id int unsigned not null,
privacy_level tinyint unsigned not null default 0,
post_date datetime not null,
key user_idx(user_id),
key post_date_user_idx(post_date, user_id)
)
engine=innodb;


drop table if exists post_privacy_includes_for;
create table post_privacy_includes_for
(
post_id int unsigned not null,
user_id int unsigned not null,
primary key (post_id, user_id)
)
engine=innodb;

Хранимые процедуры

Хранимая процедураОтносительно прост: сначала он выбирает ВСЕ сообщения за указанный период, а затем отфильтровывает сообщения в соответствии с вашими первоначальными требованиямиЯ не тестировал производительность этого sproc с большими объемами, но поскольку первоначальный выбор относительно невелик, он должен быть достаточно производительным, а также упрощать код приложения / среднего уровня.

drop procedure if exists list_user_filtered_posts;

delimiter #

create procedure list_user_filtered_posts
(
in p_user_id int unsigned,
in p_day_interval tinyint unsigned
)
proc_main:begin

 drop temporary table if exists tmp_posts;
 drop temporary table if exists tmp_priv_posts;

 -- select ALL posts in the required date range (or whatever selection criteria you require)

 create temporary table tmp_posts engine=memory 
 select 
  p.post_id, p.user_id, p.privacy_level, 0 as deleted 
 from 
  posts p
 where
  p.post_date between now() - interval p_day_interval day and now()  
 order by 
  p.user_id;

 -- purge stalker posts (0,1,3,4)

 update tmp_posts 
 inner join user_stalkers us on us.user_id = tmp_posts.user_id and us.stalker_user_id = p_user_id
 set
  tmp_posts.deleted = 1
 where
  tmp_posts.user_id != p_user_id;

 -- purge other users private posts (3)

 update tmp_posts set deleted = 1 where user_id != p_user_id and privacy_level = 3;

 -- purge friend only posts (1) i.e where p_user_id is not a friend of the poster

 /*
  requires another temp table due to mysql temp table problem/bug
  http://dev.mysql.com/doc/refman/5.0/en/temporary-table-problems.html
 */

 -- the private posts (1) this user can see

 create temporary table tmp_priv_posts engine=memory 
 select
  tp.post_id
 from
  tmp_posts tp
 inner join user_friends uf on uf.user_id = tp.user_id and uf.friend_user_id = p_user_id
 where
  tp.user_id != p_user_id and tp.privacy_level = 1;

 -- remove private posts this user cant see

 update tmp_posts 
 left outer join tmp_priv_posts tpp on tmp_posts.post_id = tpp.post_id 
 set 
  tmp_posts.deleted = 1
 where 
  tpp.post_id is null and tmp_posts.privacy_level = 1;

 -- purge filtered (4)

 truncate table tmp_priv_posts; -- reuse tmp table

 insert into tmp_priv_posts
 select
  tp.post_id
 from
  tmp_posts tp
 inner join post_privacy_includes_for ppif on tp.post_id = ppif.post_id and ppif.user_id = p_user_id
 where
  tp.user_id != p_user_id and tp.privacy_level = 4;

 -- remove private posts this user cant see

 update tmp_posts 
 left outer join tmp_priv_posts tpp on tmp_posts.post_id = tpp.post_id 
 set 
  tmp_posts.deleted = 1
 where 
  tpp.post_id is null and tmp_posts.privacy_level = 4;

 drop temporary table if exists tmp_priv_posts;

 -- output filtered posts (display ALL of these on web page)

 select 
  p.* 
 from 
  posts p
 inner join tmp_posts tp on p.post_id = tp.post_id
 where
  tp.deleted = 0
 order by
  p.post_id desc;

 -- clean up

 drop temporary table if exists tmp_posts;

end proc_main #

delimiter ;

Тестовые данные

Некоторые основные данные испытаний.

insert into users (username) values ('f00'),('bar'),('alpha'),('beta'),('gamma'),('omega');

insert into user_friends values 
(1,2),(1,3),(1,5),
(2,1),(2,3),(2,4),
(3,1),(3,2),
(4,5),
(5,1),(5,4);

insert into user_stalkers values (4,1);

insert into posts (user_id, privacy_level, post_date) values

-- public (0)

(1,0,now() - interval 8 day),
(1,0,now() - interval 8 day),
(2,0,now() - interval 7 day),
(2,0,now() - interval 7 day),
(3,0,now() - interval 6 day),
(4,0,now() - interval 6 day),
(5,0,now() - interval 5 day),

-- friends only (1)

(1,1,now() - interval 5 day),
(2,1,now() - interval 4 day),
(4,1,now() - interval 4 day),
(5,1,now() - interval 3 day),

-- private (3)

(1,3,now() - interval 3 day),
(2,3,now() - interval 2 day),
(4,3,now() - interval 2 day),

-- filtered (4)

(1,4,now() - interval 1 day),
(4,4,now() - interval 1 day),
(5,4,now());

insert into post_privacy_includes_for values (15,4), (16,1), (17,6);

Тестирование

Как я уже говорил, я не полностью протестировал это, но на поверхности этоКажется, работает.

select * from posts;

call list_user_filtered_posts(1,14);
call list_user_filtered_posts(6,14);

call list_user_filtered_posts(1,7);
call list_user_filtered_posts(6,7);

Надеюсь, вы найдете некоторые из этого использования.

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