Есть ... другое.
Другой способ хранения подробных прочитанных / непрочитанных данных для иерархической структуры форума (доска объявлений> раздел> ветка и т. Д.). Это делается без а) необходимости предварительного заполнения прочитанной / непрочитанной информации и б) без необходимости когда-либо хранить более U * (M / 2) строк в худшем случае, где U - количество пользователей, а М - общее количество сообщений в базе данных (и, как правило, намного, намного меньше этого)
Я исследовал эту тему некоторое время назад. Я обнаружил, что SMF / phpBB немного «обманывают» в том, как они хранят историю чтения пользователя. Их схема поддерживает хранение последних отметок времени или идентификатора сообщения, которые были помечены как прочитанные на данной доске, форуме, подфоруме, в теме (или просмотрены непосредственно браузером), например:
[user_id, board, last_msg_id, last_timestamp]
[user_id, доска объявлений, форум, last_msg_id, last_timestamp]
[user_id, форум, форум, подфорум, last_msg_id, last_timestamp]
[user_id, форум, форум, подфорум, тема, last_msg_id, last_timestamp]
Это позволяет пользователям отмечать определенные форумы, форумы, темы и т. Д. Как «прочитанные». Однако для этого требуется либо действие со стороны пользователя (либо путем чтения, либо с активным нажатием «пометить как прочитанное»), а в случае с phpBB нет детализации, чтобы сказать: «Я видел это конкретное сообщение, но не это конкретное сообщение. " Вы также получаете ситуацию, когда вы сначала читаете последнее сообщение в теме (просматриваете последнее действие в цепочке), и вы сразу же предполагаете, что прочитали остальную часть цепочки.
Это работает для SMF и phpBB для хранения подобных вещей, потому что редко когда вы просматриваете только один пост (по умолчанию настроено более 20 постов на последней странице темы). Однако для многопоточных форумов (особенно форумов, где вы просматриваете сообщения по одному), это далеко не идеально. Пользователи этой системы, скорее всего, будут заботиться о том, прочитали ли они одно сообщение, но не другое, и могут посчитать трудным пометить только целый раздел как прочитанный, хотя на самом деле им просто нужно отметить несколько разделов как прочитанные.
Вы храните сообщения в кортежах следующим образом: [user_id, lower_msg_id, upper_msg_id]
Журнал истории пользователей ведется следующим образом:
При просмотре страницы функция проверяет, есть ли у user_id запись, где current_msg_id находится между lower_msg_id и upper_msg_id. Если да, то эта страница читается, и никаких действий не требуется. Если этого не произошло, то должен быть выполнен другой запрос, на этот раз определяющий, является ли current_msg_id либо на один меньше, чем lower_msg_id (current_msg_id == lower_msg_id-1), либо на один больше, чем upper_msg_id (current_msg_id == upper_msg_id +1). Это тот случай, когда мы увеличиваем нашу «читаемую» или «видимую» границу на 1. Если мы находимся на расстоянии от lower_msg_id или uppper_msg_id, то мы увеличиваем кортеж на 1 в этом направлении. Если мы не увеличиваем наш диапазон кортежей, то мы вставляем новый кортеж, [user_id, current_msg_id, current_msg_id].
Угловой случай, когда два диапазона кортежей приближаются друг к другу. В этом случае при поиске между нижней границей кортежа и верхней границей кортежа объедините две границы, установив верхнюю границу нижнего кортежа в верхнюю границу верхнего кортежа, и удалите верхний кортеж.
Пример кода в PHP:
function seen_bounds( $usr_id, $msg_id ) {
# mysql escape
$usr_id = mres( $usr_id );
$msg_id = mres( $msg_id );
$seen_query = "
SELECT
msb.id,
msb.lower_msg_id,
msb.upper_msg_id
FROM
msgs_seen_bounds msb
WHERE
$msg_id BETWEEN msb.lower_msg_id AND msb.upper_msg_id AND
msb.usr_id = $usr_id
LIMIT 1;
";
# See if this post already exists within a given
# seen bound.
$seen_row = query($seen_query, ROW);
if($seen_row == 0) {
# Has not been seen, try to detect if we're "near"
# another bound (and we can grow that bound to include
# this post).
$lower_query = "
SELECT
msb.id,
msb.lower_msg_id,
msb.upper_msg_id
FROM
msgs_seen_bounds msb
WHERE
msb.upper_msg_id = ($msg_id - 1) AND
msb.usr_id = $usr_id
LIMIT 1;
";
$upper_query = "
SELECT
msb.id,
msb.lower_msg_id,
msb.upper_msg_id
FROM
msgs_seen_bounds msb
WHERE
msb.lower_msg_id = ($msg_id + 1) AND
msb.usr_id = $usr_id
LIMIT 1;
";
$lower = query($lower_query, ROW);
$upper = query($upper_query, ROW);
if( $lower == 0 && $upper == 0 ) {
# No bounds exist for or near this. We'll insert a single-ID
# bound
$saw_query = "
INSERT INTO
msgs_seen_bounds
(usr_id, lower_msg_id, upper_msg_id)
VALUES
($usr_id, $msg_id, $msg_id)
;
";
query($saw_query, NONE);
} else {
if( $lower != 0 && $upper != 0 ) {
# Found "near" bounds both on the upper
# and lower bounds.
$update_query = '
UPDATE msgs_seen_bounds
SET
upper_msg_id = ' . $upper['upper_msg_id'] . '
WHERE
msgs_seen_bounds.id = ' . $lower['id'] . '
;
';
$delete_query = '
DELETE FROM msgs_seen_bounds
WHERE
msgs_seen_bounds.id = ' . $upper['id'] . '
;
';
query($update_query, NONE);
query($delete_query, NONE);
} else {
if( $lower != 0 ) {
# Only found lower bound, update accordingly.
$update_query = '
UPDATE msgs_seen_bounds
SET
upper_msg_id = ' . $msg_id . '
WHERE
msgs_seen_bounds.id = ' . $lower['id'] . '
;
';
query($update_query, NONE);
}
if( $upper != 0 ) {
# Only found upper bound, update accordingly.
$update_query = '
UPDATE msgs_seen_bounds
SET
lower_msg_id = ' . $msg_id . '
WHERE
msgs_seen_bounds.id = ' . $upper['id'] . '
;
';
query($update_query, NONE);
}
}
}
} else {
# Do nothing, already seen.
}
}
Поиск непрочитанных сообщений - это поиск того, где current_msg_id не существует между любым lower_msg_id и upper_msg_id для данного пользователя (запрос NOT EXISTS в терминах SQL). Это не самый эффективный запрос при реализации в реляционной базе данных, но может быть решен с помощью агрессивной индексации. Например, ниже приведен SQL-запрос для подсчета непрочитанных сообщений для данного пользователя с группировкой по области обсуждения («элемент»), в которой находятся сообщения:
$count_unseen_query = "
SELECT
msgs.item as id,
count(1) as the_count
FROM msgs
WHERE
msgs.usr != " . $usr_id . " AND
msgs.state != 'deleted' AND
NOT EXISTS (
SELECT 1
FROM
msgs_seen_bounds msb
WHERE
msgs.id BETWEEN msb.lower_msg_id AND msb.upper_msg_id
AND msb.usr_id = " . $usr_id . "
)
GROUP BY msgs.item
;
Чем больше пользователей читает на форуме, тем шире границы, помеченные как прочитанные каждым кортежем, и тем меньше нужно хранить кортежей. Пользователи могут получить точное количество прочитанных и непрочитанных, и их можно легко объединить, чтобы увидеть прочитанных и непрочитанных в каждом форуме, подфоруме, теме и т. Д.
Учитывая небольшой форум, содержащий около 2000+ сообщений, ниже приведена статистика использования по количеству сохраненных кортежей, отсортированная по количеству входов пользователей в систему (приблизительная активность пользователей). Столбец «num_bounds» - это количество кортежей, необходимое для хранения истории просмотра «num_posts_read» пользователя.
id num_log_entries num_bounds num_posts_read num_posts
479 584 11 2161 228
118 461 6 2167 724
487 119 34 2093 199
499 97 6 2090 309
476 71 139 481 82
480 33 92 167 26
486 33 256 757 154
496 31 108 193 51
490 31 80 179 61
475 28 129 226 47
491 22 22 1207 24
502 20 100 232 65
493 14 73 141 5
489 14 12 1517 22
498 10 72 132 17
Я не видел этой конкретной реализации ни на одном форуме, кроме моей собственной, и при этом она маленькая. Мне было бы интересно, если бы кто-то еще реализовал или видел, как это реализовано в другом месте, особенно на большом и / или активном форуме.
С уважением,
Кайден