他にもある。
階層的なフォーラム構造 (ボード > セクション > スレッドなど) の詳細な既読/未読データを保存する別の方法。これは、a) 既読/未読の情報を事前入力する必要がなく、b) 最悪の場合に U*(M/2) 行を超える行を保存する必要がなく、U はユーザー数、M はデータベース内の投稿の総数 (通常は、これよりはるかに少ない)
私はこのトピックについて少し前に調査しました。SMF/phpBB は、ユーザーの読み取り履歴を保存する方法を少し「ごまかす」ことがわかりました。それらのスキーマは、次のように、特定のボード、フォーラム、サブフォーラム、トピックで既読としてマークされた (またはブラウザーによって直接表示された) 最後のタイムスタンプまたはメッセージ ID のいずれかのストレージをサポートします。
[ user_id、ボード、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 がこのようなものを保存するのに役立ちます。これは、1 つの投稿だけを表示することはめったにないためです (トピックの最後のページにある 20 件以上の投稿に対してデフォルトのビューが設定されています)。ただし、よりスレッド化されたフォーラム (特に、一度に 1 つずつメッセージを表示するフォーラム) では、これは理想的とは言えません。このシステムのユーザーは、あるメッセージを読んで別のメッセージを読んでいないかどうかを非常に気にする可能性が高く、実際にはいくつかのメッセージを既読にしたいのに、セクション全体を既読にすることしかできないのは面倒だと思うかもしれません.
次のようなタプルにメッセージを保存します: [ 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 より 1 小さいか (current_msg_id == lower_msg_id-1)、または upper_msg_id より 1 大きいか (current_msg_id == upper_msg_id +1) を判別します。これは、「読み取り」または「表示」境界を 1 ずつ大きくする場合です。タプル範囲を拡大していない場合は、新しいタプル [ user_id, current_msg_id, current_msg_id ] を挿入します。
コーナー ケースは、2 つのタプル範囲が互いに近づく場合です。この場合、下位タプル境界と上位タプル境界の間を検索する際に、下位タプルの上位境界を上位タプルの上位境界に設定して 2 つの境界をマージし、上位タプルを削除します。
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
この特定の実装は、私自身のカスタム フォーラム以外のフォーラムでは見たことがありません。それは小さなものです。特に大規模および/または活発なフォーラムで、他の誰かが実装したか、これが他の場所で実装されているのを見た場合、私は興味があります。
よろしく、
カイデン