我最近有一个任务是使用 Propel 迭代 PostgreSQL 中的一个大表(~40KK 记录),并遇到性能问题,包括内存限制和执行速度。我的脚本已经运行了 22(!)小时。
任务是根据某些条件检索记录(过去 6 个月不活动)并将其存档(移至另一个表)以及其他表中的所有相关实体。
我的脚本正在处理的主表有几列:id
、device_id
、application_id
、last_activity_date
和其他,在这里没有任何重要意义。此表包含有关设备上安装的应用程序及其上次事件日期的信息。可能有多个记录具有相同的 device_id
和不同的 application_id
。以下是表中的示例:
id | device_id | application_id | last_init_date
----------+-----------+----------------+---------------------
1 | 1 | 1 | 2013-09-24 17:09:01
2 | 1 | 2 | 2013-09-19 20:36:23
3 | 1 | 3 | 2014-02-11 00:00:00
4 | 2 | 4 | 2013-09-29 20:12:54
5 | 3 | 5 | 2013-08-31 19:41:05
因此,如果此表中特定 device_id
的最大 last_activity_date
早于 6 个月,则该设备被视为足够旧,可以存档。这是查询:
SELECT device_id
FROM device_applications
GROUP BY device_id
HAVING MAX(last_init_date) < '2014-06-16 08:00:00'
在 Propel 中,它看起来像:
\DeviceApplicationsQuery::create()
->select('DeviceId')
->groupByDeviceId()
->having('MAX(device_applications.LAST_INIT_DATE) < ?', $date->format('Y-m-d H:i:s'))
->find();
正如您所知,结果集太大,无法放入内存,因此我必须以某种方式将其分成 block 。
问题是:在这种情况下选择什么策略来减少内存消耗并加快脚本速度? 在我的回答中,我将向您展示迄今为止我发现的内容。
最佳答案
我知道遍历大表的三种策略。
1。好的旧限制/偏移量
这种方法的问题在于,数据库实际上会检查您想要使用 OFFSET
跳过的记录。这是 doc 的引用:
The rows skipped by an OFFSET clause still have to be computed inside the server; therefore a large > OFFSET might be inefficient.
这是一个简单的示例(不是我最初的查询):
explain (analyze)
SELECT *
FROM device_applications
ORDER BY device_id
LIMIT 100
OFFSET 300;
执行计划:
Limit (cost=37.93..50.57 rows=100 width=264) (actual time=0.630..0.835 rows=100 loops=1)
-> Index Scan using device_applications_device_id_application_id_unique on device_applications (cost=0.00..5315569.97 rows=42043256 width=264) (actual time=0.036..0.806 rows=400 loops=1)
Total runtime: 0.873 ms
特别注意索引扫描部分中的实际结果。它显示 PostgreSQL 处理 400 条记录,即偏移量 (300) 加上限制 (100)。所以这种方法效率相当低,特别是考虑到初始查询的复杂性。
2。按某列排列
我们可以通过使查询与表的范围一起工作来避免限制/偏移方法的限制,这些范围是通过按列对表进行切片而实现的。
为了澄清,让我们想象一下您有一个包含 100 条记录的表,您可以将该表分为五个范围,每个范围有 20 条记录:0 - 20、20 - 40、40 - 60、60 - 80、80 - 100,然后处理较小的子集。就我而言,我们可以选择的列是 device_id
。查询如下所示:
SELECT device_id
FROM device_applications
WHERE device_id >= 1 AND device_id < 1000
GROUP BY device_id
HAVING MAX(last_init_date) < '2014-06-16 08:00:00';
它按device_id
对记录进行分组,提取范围并在last_init_date
上应用条件。当然,可能(并且在大多数情况下)不会有与条件匹配的记录。因此,这种方法的问题是您必须扫描整个表,即使您要查找的记录只是所有记录的 5%。
3。使用光标
我们需要的是 cursor 。游标允许迭代结果集,而无需立即获取整个数据。在 PHP 中,当您迭代 PDOStatement 时,您会使用游标。 。一个简单的例子:
$stmt = $dbh->prepare("SELECT * FROM table");
$stmt->execute();
// Iterate over statement using a cursor
foreach ($stmt as $row) {
// Do something
}
在 Propel 中,您可以通过 PropelOnDemandFormatter
类来使用此 PDO 的功能。所以,最终的代码:
$devApps = \DeviceApplicationsQuery::create()
->setFormatter('\PropelOnDemandFormatter')
->select('DeviceId')
->groupByDeviceId()
->having('MAX(device_applications.LAST_INIT_DATE) < ?', $date->format('Y-m-d H:i:s'))
->find();
/** @var \DeviceApplications $devApp */
foreach ($devApps as $devApp) {
// Do something
}
此处对 find()
的调用不会获取数据,而是会根据需要创建对象来创建一个集合。
关于php - 使用 Propel 遍历 PostgreSQL 中的大表,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27501914/