通过命中优化内容流行度的查询

时间:2011-06-02 08:32:38

标签: php mysql sql performance database-design


我已经做了一些搜索,但没有想出任何东西,也许有人可以指出我正确的方向 我在MySQL数据库中有一个包含大量内容的网站,还有一个PHP脚本,可以按点击加载最受欢迎的内容。它通过记录表中的每个内容以及访问时间来实现此目的。然后运行选择查询以查找过去24小时,7天或最长30天内最受欢迎的内容。 cronjob会删除日志表中超过30天的任何内容。

我现在面临的问题是,随着网站的增长,日志表有1m +点击记录,这实际上减慢了我的选择查询(10-20秒)。起初我虽然问题是我在查询中获得内容标题,网址等的连接但是现在我不确定在测试中删除连接不会像我那样加速查询。

所以我的问题是这种流行度存储/选择的最佳做法是什么?它们是否为此提供了良好的开源脚本?或者你会建议什么?

表格方案

  

“人气”点击日志表
  nid | insert_time | TID
  nid:内容的节点ID   insert_time:timestamp(2011-06-02 04:08:45)
  tid:术语/类别ID

     

“节点”内容表
  nid |标题|状态| (还有更多,但这些是重要的)
  nid:节点ID
  标题:内容标题
  status:是发布的内容(0 = false,1 = true)

SQL

SELECT node.nid, node.title, COUNT(popularity.nid) AS count  
FROM `node` INNER JOIN `popularity` USING (nid)  
WHERE node.status = 1  
  AND  popularity.insert_time >= DATE_SUB(CURDATE(),INTERVAL 7 DAY)  
GROUP BY popularity.nid  
ORDER BY count DESC  
LIMIT 10;

5 个答案:

答案 0 :(得分:2)

我们遇到了类似的情况,这就是我们如何解决这个问题。我们决定我们并不真正关心事情发生的确切时间,只发生在它发生的那一天。然后我们这样做了:

  1. 每条记录都有一个'总命中'记录,每次发生某事时都会增加
  2. 日志表记录每天每条记录的“总点击次数”(在cron作业中)
  3. 通过选择此日志表中两个给定日期之间的差异,我们可以非常快速地推断出两个日期之间的“点击”。
  4. 这样做的好处是你的日志表的大小只有NumRecords * NumDays那么大,在我们的例子中非常小。此日志表上的任何查询都非常快。

    缺点是你无法在一天中的时间内推断出点击次数,但如果你不需要,那么可能值得考虑。

答案 1 :(得分:1)

你实际上有两个问题需要进一步解决。

其中一个,你还没有遇到但你可能比你想要的更早,将在你的统计表中插入吞吐量。

您在问题中概述的另一个实际上是使用统计数据。


让我们从输入吞吐量开始。

首先,如果您这样做,请不要跟踪可以使用缓存的网页上的统计信息。使用php脚本将自己宣传为空的javascript或单像素图像,并将后者包含在您正在跟踪的页面上。这样做可以轻松缓存您网站的剩余内容。

在电信业务中,不是在电话呼叫中进行与计费相关的实际插入,而是将内容放在内存中并定期与磁盘同步。这样做可以在保持硬盘驱动器满意的同时管理巨大的吞吐量。

要在您的结尾进行类似的操作,您需要一个原子操作和一些内存存储。这是第一部分基于memcache的伪代码......

对于每个页面,您需要一个Memcache变量。在Memcache中,increment()是原子的,但add(),set()等不是。因此,当并发​​进程同时添加同一页时,您需要警惕不要错过计数命中:

$ns = $memcache->get('stats-namespace');
while (!$memcache->increment("stats-$ns-$page_id")) {
  $memcache->add("stats-$ns-$page_id", 0, 1800); // garbage collect in 30 minutes
  $db->upsert('needs_stats_refresh', array($ns, $page_id)); // engine = memory
}

定期地说,每隔5分钟(相应地配置超时),您需要将所有这些同步到数据库,而不会有任何并发​​进程相互影响或现有命中计数的可能性。为此,在执行任何操作之前增加命名空间(这使您可以锁定所有意图和目的的现有数据),并稍微休眠一下,以便在需要时引用先前命名空间的现有进程完成:

$ns = $memcache->get('stats-namespace');
$memcache->increment('stats-namespace');
sleep(60); // allow concurrent page loads to finish

完成后,您可以安全地遍历页面ID,相应地更新统计信息,并清理needs_stats_refresh表。后者只需要两个字段:page_id int pkey,ns_id int)。除了从脚本运行的简单选择,插入,更新和删除语句之外,还有更多内容,所以继续......

正如另一位回复者所说,为你的目的维护中间统计数据是非常合适的:存储批量命中而不是单独命中。最重要的是,我假设你需要每小时的统计数据或每小时的统计数据,所以处理每15分钟批量加载的小计是很好的。

更重要的是,为了您的利益,因为您使用这些总计订购帖子,您希望存储汇总的总计并对后者有一个索引。 (我们会到达更远的地方。)

维持总计的一种方法是添加一个触发器,在插入或更新统计表时,将根据需要调整统计数据。

这样做时,要特别警惕死锁。虽然没有两个$ns运行将混合它们各自的统计数据,但仍有一个(但是很小)的可能性,即两个或多个进程同时启动上述“增量$ ns”步骤,并随后发出寻求同时更新计数。获取advisory lock是避免与此相关问题的最简单,最安全,最快捷的方法。

假设你使用了一个咨询锁,那么在更新语句中使用:total = total + subtotal是完全可以的。

关于锁的主题,请注意更新总计将需要对每个受影响的行进行独占锁定。由于您是由他们订购的,因此您不希望它们一次性处理,因为这可能意味着在较长时间内保持独占锁定。这里最简单的是将插入处理成较小批量的统计数据(比如1000),每个批次后面都有一个提交。

对于中间统计(每月,每周),在统计表中添加一些布尔字段(MySQL中的bit或tinyint)。让这些存储中的每一个都是按月计算,每周计算,每日统计数据等等。还可以触发它们,以便它们增加或减少stat_totals表中适用的总计。

作为结束语,请考虑一下您希望存储实际计数的位置。它需要是一个索引字段,而后者将会大量更新。通常,您需要将它存储在自己的表中,而不是存储在页面表中,以避免使用更大的(大得多的)死行来破坏页面表。


假设您完成了以上所有操作,您的最终查询将变为:

select p.*
from pages p join stat_totals s using (page_id)
order by s.weekly_total desc limit 10

每周_total上的索引应该足够快。

最后,让我们不要忘记最明显的一点:如果你一遍又一遍地运行这些相同的总/月/周/等查询,他们的结果也应该放在内存中。

答案 2 :(得分:0)

你可以添加索引并尝试调整你的SQL,但这里的真正解决方案是缓存结果。

你应该真的只需要每天一次的最后7/30天的流量计算

你可以每小时过去24小时吗?

即使你每5分钟做一次,这仍然比为每个用户的每次点击运行(昂贵的)查询节省了大量的费用。

答案 3 :(得分:0)

<强> RRDtool的

许多工具/系统不构建自己的日志记录和日志聚合,而是使用RRDtool(循环数据库工具)来有效处理时间序列数据。 RRDtools还带有强大的图形子系统,并且(根据Wikipedia)有PHP和其他语言的绑定。

根据您的问题,我假设您不需要任何特殊和奇特的分析,RRDtool可以高效地完成您的需要,而无需您实施和调整自己的系统。

答案 4 :(得分:0)

您可以在后台执行某些“聚合”,例如通过con job。一些可能有所帮助的建议(没有特别的顺序):

<强> 1。创建一个包含每小时结果的表格。这意味着您仍然可以创建所需的统计数据,但是将数据量减少到(24 * 7 * 4 =每页每月约672条记录)。

你的桌子可以在某个地方:

hourly_results (
nid integer,
start_time datetime,
amount integer
)

将它们解析到聚合表后,您可以或多或少地删除它们。

2.使用结果缓存(memcache,apc) 您可以轻松地存储结果(不应每分钟更改,而不是每小时更改一次?),在memcache database中(您可以再次从cronjob更新),使用apc user cache(您如果内存不足,则无法通过cronjob更新或使用file caching序列化对象/结果。

第3。优化您的数据库 10秒是很长一段时间。试着找出你的数据库发生了什么。内存不足吗?你需要更多的索引吗?