【问题标题】:Traverse a big table in PostgreSQL with Propel使用 Propel 遍历 PostgreSQL 中的大表
【发布时间】:2014-12-16 09:58:52
【问题描述】:

我最近有一个任务是使用 Propel 在 PostgreSQL 中迭代一个大表(约 40KK 记录),并遇到了性能问题,包括内存限制和执行速度。我的脚本已经运行了 22(!) 小时。

任务是根据某些条件(在过去 6 个月内不活跃)检索记录并将它们存档(移动到另一个表)以及来自其他表的所有相关实体。

我的脚本正在处理的主表有几列:iddevice_idapplication_idlast_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();

如您所知,结果集太大而无法放入内存,因此我必须以某种方式将其拆分为块。

问题是:在这种情况下,减少内存消耗和加快脚本速度的最佳策略是什么? 在我的回答中,我将向您展示我到目前为止发现的内容。

【问题讨论】:

  • 为什么不能通过一次查询将所有记录移动到档案中? 40k 条记录并不多。
  • 40KK,是4000万
  • 4000 万条记录要多得多,但当您的硬件可以处理这么多数据时可能会很快。您是否在单个查询中检查了包含 DELETE 和 INSERT 的公用表表达式? ORM(任何类型的 ORM)和性能始终是一个挑战,ORM 不是为此而生的。
  • 是的,我知道ORM一般不适合这种情况,但问题是主要实体(设备)有很多相关实体(实际上是5个其他表,有外键约束)也必须存档。并且归档相关记录的逻辑是在ORM类中实现的。
  • 这就是我所做的,我在回答中指出了这一点。

标签: php postgresql pdo propel


【解决方案1】:

我知道遍历大表的三种策略。

1。旧的限制/偏移量

这种方法的问题在于,数据库实际上检查了您想用OFFSET 跳过的记录。这是doc的引述:

OFFSET 子句跳过的行仍然需要在服务器内部计算;因此,较大的 > 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)。所以这种方法效率很低,尤其是考虑到初始查询的复杂性。

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() 的调用不会获取数据,而是会创建一个按需创建对象的集合。

【讨论】:

【解决方案2】:

如果您使用 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, …]);

确保为您的删除查询正确设置了索引,它应该更快。

【讨论】:

  • 感谢提示,我不知道 Pomm 项目。
猜你喜欢
  • 1970-01-01
  • 2019-05-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-19
  • 2014-06-10
  • 1970-01-01
相关资源
最近更新 更多