还有另一种。
另一种存储分层论坛结构(板>部分>线程等)的详细已读/未读数据的方法。它这样做不需要 a) 必须预先填充已读/未读信息,并且 b) 在最坏的情况下不必存储超过 U*(M/2) 行,其中 U 是用户数,M 是数据库中的帖子总数(通常比这个少得多)
不久前我研究了这个话题。我发现 SMF/phpBB 在存储用户阅读历史记录方面有点“作弊”。他们的架构支持存储在给定版块、论坛、子论坛、主题(或由浏览器直接查看)中标记为已读的最后时间戳或消息 ID,如下所示:
[ user_id, board, last_msg_id, last_timestamp ]
[ user_id, board, forum, last_msg_id, last_timestamp ]
[ user_id, board, forum, subforum, last_msg_id, last_timestamp ]
[ user_id, board, forum, subforum, topic, 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,那么我们在该方向上将元组增加 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.
}
}
搜索未读帖子是查找给定用户的任何lower_msg_id 和upper_msg_id 之间不存在current_msg_id 的位置(SQL 术语中的NOT EXISTS 查询)。在关系数据库中实现时,这不是最有效的查询,但可以通过积极的索引来解决。例如,以下是一个 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
除了我自己的自定义论坛,我还没有在任何论坛中看到过这个特定的实现,而且它只是一个很小的实现。如果其他人已经实施或看到在其他地方实施,特别是在大型和/或活跃的论坛中,我会很感兴趣。
问候,
凯登