我最近有一个任务是使用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
因此,如果此表中特定last_activity_date
的最大device_id
超过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();
如您所知,结果集太大而无法放入内存中,因此我必须以某种方式将其拆分为块。
问题是:在这种情况下,选择哪种方法来减少内存消耗并加快脚本速度? 在我的回答中,我会告诉你到目前为止我发现了什么。
答案 0 :(得分:3)
我知道三种穿越大桌子的策略。
此方法的问题在于数据库实际检查您要使用OFFSET
跳过的记录。以下是doc:
OFFSET子句跳过的行仍然必须在服务器内计算;因此大&gt; OFFSET可能效率低下。
这是一个简单的例子(不是我最初的查询):
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)。因此,这种方法效率很低,特别是考虑到初始查询的复杂性。
我们可以通过使查询使用表的范围来避免限制/偏移方法的限制,这些范围是通过用列切割表来实现的。
为了澄清,让我们想象你有一张包含100条记录的表,你可以将这个表分为5个范围,每个记录分为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%。
我们需要的是cursor。游标允许迭代结果集而无需立即获取整个数据。在PHP中,当您遍历PDOStatement时,可以使用游标。一个简单的例子:
$stmt = $dbh->prepare("SELECT * FROM table");
$stmt->execute();
// Iterate over statement using a cursor
foreach ($stmt as $row) {
// Do something
}
在Propel中,您可以使用此{+ 1}}类的此PDO功能。所以,最终的代码:
PropelOnDemandFormatter
此处对$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
}
的调用不会获取数据,而是会创建一个按需创建对象的集合。
答案 1 :(得分:1)
如果您使用PHP并且不需要将结果保存到PHP实体(对象),则可以使用PommProject / Foundation包。该脚本将简单地类似于
<?php
$loader = require __DIR__.'/vendor/autoload.php';
$pomm = new PommProject\Foundation\Pomm(
[
'project_name' => ['dsn' => 'pgsql://user:pass@host:port/db_name']
]
);
$sql = <<<SQL
with
removed as (delete from a_table where val1 = $* and … returning *)
insert into another_table select * from removed
SQL;
$pomm['your_project']
->getQueryManager()
->query($sql, [$value1, …]);
确保为删除查询正确设置索引,并且速度应该更快。