状況は次のとおりです。
シンプルな RSS フィード リーダーである SAAS アプリケーションがあります。ほとんどの人はこれが何であるかを知っていると思います - ユーザーは RSS フィードを購読し、そこからアイテムを読んでいます。新しいものは何もありません。1 つのフィードに多数のサブスクライバーを含めることができます。
ユーザーの統計をいくつか実装しましたが、正しいアプローチを選択したとは思いません。ユーザーとフィードの数が増えるにつれて、時間ごとに処理が遅くなるためです。
これが私が今していることです:
1 時間ごとに、各フィードの記事の総数を取得します。
SELECT COUNT(*) FROM articles WHERE feed_id=?
前の値を取得してデルタを計算します (これは少し遅くなります)。
SELECT value FROM feeds_stats WHERE feed_id=? AND name='total_articles' ORDER BY date DESC LIMIT 1
新しい値とデルタを挿入します。
INSERT INTO feeds_stats (date,feed_id,name,value,delta) VALUES ('".date("Y-m-d H:i:s",$global_timestamp)."','".$feed_id','total_articles','".$value."','".($value-$old_value)."')
すべてのユーザーがフィードを取得し、フィードごとにユーザーが読んだ記事の数を取得します。
SELECT COUNT(*) FROM users_articles ua JOIN articles a ON a.id=ua.article_id WHERE a.feed_id='%s' AND ua.user_id='%s' AND ua.read=1
users_articles は、ユーザーごとの各記事の読み取り状態を保持するテーブルです
次に、再びデルタを取得します。
SELECT value FROM users_feeds_stats WHERE user_id='?' AND feed_id='?' AND name='total_reads' ORDER BY date DESC LIMIT 1
新しい値 + デルタを挿入します。
INSERT INTO users_feeds_stats (date,user_id,feed_id,name,value,delta) VALUES ('".date("Y-m-d H:i:s",$global_timestamp)."','".$user_id."','".$feed_id."','total_reads','".$value."','".($value-$old_value)."')
ユーザーのすべてのフィードが処理されると、集計部分が表示されます。
これは少しトリッキーで、ここには最適化の余地がたくさんあるはずです。PHP での実際の集計関数は次のとおりです。
<?php
function aggregate_user_stats($user_id=false,$feed_id=false){
global $global_timestamp;
// defined dimensions
$feed_types[0] = array("days_back" => 31, "group_by" => "DATE_FORMAT(date, '%Y-%m-%d')");
$feed_types[1] = array("days_back" => 31, "group_by" => "WEEKDAY(date)+1");
$feed_types[2] = array("days_back" => 31, "group_by" => "HOUR(date)");
if($user_id){
$where = " WHERE id=".$user_id;
}
$feed_where = "";
$getusers = mysql_query("SELECT id FROM users".$where)or die(__LINE__." ".mysql_error());
while($user = mysql_fetch_assoc($getusers)){
if($feed_id){
$feed_where = " AND feed_id=".$feed_id;
}
$user_feeds = array();
$getfeeds = mysql_query("SELECT feed_id FROM subscriptions WHERE user_id='".$user["id"]."' AND active=1".$feed_where)or die(__LINE__." ".mysql_error());
while($row = mysql_fetch_assoc($getfeeds)){
foreach($feed_types as $tab => $type){
$getdata = mysql_query("
SELECT ".$type["group_by"]." AS date, name, SUM(delta) AS delta FROM feeds_stats WHERE feed_id = '".$row["feed_id"]."' AND name='total_articles' AND date > DATE_SUB(NOW(), INTERVAL ".$type["days_back"]." DAY) GROUP BY name, ".$type["group_by"]."
UNION
SELECT ".$type["group_by"]." AS date, name, SUM(delta) AS delta FROM users_feeds_stats WHERE user_id = '".$user["id"]."' AND feed_id = '".$row["feed_id"]."' AND name='total_reads' AND date > DATE_SUB(NOW(), INTERVAL ".$type["days_back"]." DAY) GROUP BY name, ".$type["group_by"]."
")or die(__LINE__." ".mysql_error());
$data = array();
while($row = mysql_fetch_assoc($getdata)){
$data[$row["date"]][$row["name"]] = $row["delta"];
}
if(count($data)){
db_start_trx();
mysql_query("DELETE FROM stats_feeds_over_time WHERE feed_id='".$row["feed_id"]."' AND user_id='".$user["id"]."' AND tab='".$tab."'")or die(__LINE__." ".mysql_error());
foreach($data as $time => $keys){
mysql_query("REPLACE INTO stats_feeds_over_time (feed_id,user_id,tab,date,total_articles,total_reads,total_favs) VALUES ('".$row["feed_id"]."','".$user["id"]."','".$tab."','".$time."','".$keys["total_articles"]."','".$keys["total_reads"]."','".$keys["total_favs"]."')")or die(__LINE__." ".mysql_error());
}
db_commit_trx();
}
}
}
}
}
いくつかのメモ:
編集:関連するテーブルのDDLは次のとおりです。
CREATE TABLE `articles` (
`id` INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`feed_id` INTEGER(11) UNSIGNED NOT NULL,
`date` INTEGER(10) UNSIGNED NOT NULL,
`date_updated` INTEGER(11) UNSIGNED NOT NULL,
`title` VARCHAR(1000) COLLATE utf8_general_ci NOT NULL DEFAULT '',
`url` VARCHAR(2000) COLLATE utf8_general_ci NOT NULL DEFAULT '',
`author` VARCHAR(200) COLLATE utf8_general_ci NOT NULL DEFAULT '',
`hash` CHAR(32) COLLATE utf8_general_ci NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `feed_id_hash` (`feed_id`, `hash`),
KEY `date` (`date`),
KEY `url` (`url`(255))
)ENGINE=InnoDB
AUTO_INCREMENT=0
CHARACTER SET 'utf8' COLLATE 'utf8_general_ci'
COMMENT='';
CREATE TABLE `users_articles` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`user_id` INTEGER(11) UNSIGNED NOT NULL,
`article_id` INTEGER(11) UNSIGNED NOT NULL,
`subscription_id` INTEGER(11) UNSIGNED NOT NULL,
`read` TINYINT(4) UNSIGNED NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `user_id` (`user_id`, `article_id`),
KEY `article_id` (`article_id`),
KEY `subscription_id` (`subscription_id`)
)ENGINE=InnoDB
CHECKSUM=1 AUTO_INCREMENT=0
CHARACTER SET 'utf8' COLLATE 'utf8_general_ci'
COMMENT='';
CREATE TABLE `feeds_stats` (
`id` INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`feed_id` INTEGER(11) UNSIGNED NOT NULL,
`date` DATETIME NOT NULL,
`name` VARCHAR(50) COLLATE utf8_general_ci NOT NULL DEFAULT '',
`value` INTEGER(11) NOT NULL,
`delta` INTEGER(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `name` (`name`),
KEY `feed_id` (`feed_id`),
KEY `date` (`date`)
)ENGINE=InnoDB
AUTO_INCREMENT=0
CHARACTER SET 'utf8' COLLATE 'utf8_general_ci'
COMMENT='';
CREATE TABLE `users_feeds_stats` (
`id` INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` INTEGER(11) UNSIGNED NOT NULL DEFAULT '0',
`feed_id` INTEGER(11) UNSIGNED NOT NULL,
`date` DATETIME NOT NULL,
`name` VARCHAR(50) COLLATE utf8_general_ci NOT NULL DEFAULT '',
`value` INTEGER(11) NOT NULL,
`delta` INTEGER(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `name` (`name`),
KEY `feed_id` (`feed_id`),
KEY `user_id` (`user_id`),
KEY `date` (`date`)
)ENGINE=InnoDB
AUTO_INCREMENT=0
CHARACTER SET 'utf8' COLLATE 'utf8_general_ci'
COMMENT='';
CREATE TABLE `stats_feeds_over_time` (
`feed_id` INTEGER(11) UNSIGNED NOT NULL,
`user_id` INTEGER(11) NOT NULL,
`tab` INTEGER(11) NOT NULL,
`date` VARCHAR(30) COLLATE utf8_general_ci NOT NULL DEFAULT '',
`total_articles` DOUBLE(9,2) UNSIGNED NOT NULL,
`total_reads` DOUBLE(9,2) UNSIGNED NOT NULL,
`total_favs` DOUBLE(9,2) UNSIGNED NOT NULL,
PRIMARY KEY (`feed_id`, `user_id`, `tab`, `date`)
)ENGINE=InnoDB
AUTO_INCREMENT=0
CHARACTER SET 'utf8' COLLATE 'utf8_general_ci'
COMMENT='';
集計関数の最後に、テーブル stats_feeds_over_time に REPLACE があります。このテーブルには、グラフに表示されるレコードのみが保持されるため、実際のグラフ作成プロセスには重いクエリは必要ありません。
最後に、これによって生成されたグラフを次に示します。
統計のためにMySQLを捨てることを意味するとしても、誰かがこのソリューションをどこでどのように最適化するかについて正しい方向に向けてくれるとうれしいです.
私はRRDToolで長い経験を持っていますが、「時刻」、「曜日」の集計のため、ここでは状況が異なります。