将列更新为连接列中的值

Posted

技术标签:

【中文标题】将列更新为连接列中的值【英文标题】:Update column to a value in joined column 【发布时间】:2015-07-25 06:33:07 【问题描述】:

我正在规范化数据库,需要运行约 630k 更新。这是我的表的基本结构:

身份证 姓名

统计数据

域 domain_id

以前,数据库没有域表,域存储在多个表中,有时作为列表(JSON 文本)。我将每个域迁移到domains 表,现在我需要建立与具有domain 列的stats 表的关系。我添加了domain_id 列并尝试以某种方式对其进行更新以匹配domains 表中域的idstats 表有超过 2300 万行和约 630k 的唯一域(统计数据是每小时一次)。我尝试运行一个 foreach,但每个域大约需要 2 秒,运行所有域大约需要 14 天。

到目前为止,这是我的代码:

首先,我从stats 表中找到domains 表中缺少的所有域,并将它们保存在domains 表中。

$statDomains = Stat::select('domain')->groupBy('domain')->lists('domain');
$domains = [];
foreach(array_chunk($statDomains , 1000) as $domains1k)
    $domains = array_merge($domains, Domain::whereIn('name', $domains1k)->lists('name'));

$missingDomains = [];
foreach(array_diff($statDomains , $domains) as $missingDomain)
    $missingDomains[] = ['name' => $missingDomain];


if(!empty($missingDomains))
    Domain::insert($missingDomains);

接下来,我从 stats 表中的 domains 表中获取所有域,并使用该域更新 stats 表中的所有行。

$domains = [];
foreach(array_chunk($statDomains, 1000) as $domains1k)
    $domains +=Domain::whereIn('name', $domains1k)->lists('name', 'id');

foreach($domains as $key => $domain)
        Stat::where('domain', $domain)->update(['domain_id' => $key]);

我会很欣赏 eloquent、查询构建器或只是原始 SQL 中的一些东西,它们可以更快地完成更新(最多两个小时?)。我做了一些谷歌搜索,发现了类似的问题,但无法适用于我的案例。

编辑

我现在正在运行建议的解决方案。与此同时,我发现迁移的另外两个部分大约需要 50 分钟。在第一个中,我有一张桌子domain_lists。它有一个带有 JSON 编码域的文本列 domains。我将这些域移动到domain 表并在domain_lists_domains_map 表上创建记录。代码如下:

foreach(DomainList::all() as $domainList)
    $attach = [];
    $domains = json_decode($domainList->domains, true);
    foreach($domains as $domain)
        $model = Domain::where('name', '=', $domain)->first();
        if(is_null($model) && !is_null($domain))
            $model = new Domain();
            $model->name = $domain;
            $model->save();
        
        if(!is_null($model))
            $attach[] = $model->id;
        
    
    if(!empty($attach))
        foreach(array_chunk(array_unique($attach), 1000) as $attach1k)
            $domainList->domains()->attach($attach1k);
        
    

我已经注意到,我可能应该首先找到所有唯一域并将它们插入到域表中,但是给出了上一个问题的解决方案,我觉得可能有更好的方法来做这一切SQL。第二部分非常相似,我大概可以通过查看第一部分的代码来弄清楚如何解决它。该表是类别,它还有一个带有 JSON 编码域的域文本列。非常感谢任何帮助。

编辑 2

这是我运行的查询,将现有表复制到一个新的表中,并填充了 domain_id 列:

CREATE TABLE "stats_new" AS SELECT
    "s"."domain",
    "d"."id" AS "domain_id"
FROM
    "stats" "s"
JOIN "domains" "d" ON ("s"."domain" = "d"."name")

【问题讨论】:

所有行中的stats.domain_id NULL 还是已经(正确)填充在某些行中? @ErwinBrandstetter 全部为空,我只是将其添加到表中 【参考方案1】:

原始 SQL 应该更快几个数量级

第一步:INSERT

所有个域名插入表domains,除非它们已经存在:

INSERT INTO domains (name)
SELECT DISTINCT s.domain
FROM   stats s
LEFT   JOIN domains d ON d.name = s.domain
WHERE  d.name IS NULL;
Select rows which are not present in other table

如果您具有并发写入访问权限,则可能存在竞争条件。最简单的解决方案是 lock the table domains 专门用于交易。否则,您可能会在操作中途遇到独特的违规,因为并发事务在两者之间提交了相同的域名。一切都会回滚。

BEGIN;
LOCK TABLE domains IN EXCLUSIVE MODE;

INSERT INTO domains (name)
SELECT DISTINCT s.domain
FROM   stats s
LEFT   JOIN domains d ON d.name = s.domain
WHERE  d.name IS NULL;

COMMIT;

domains.name 应该是 UNIQUE。该约束是通过列上的索引来实现的,这将有助于下一步的性能。

How does PostgreSQL enforce the UNIQUE constraint / what type of index does it use?

Does a Postgres UNIQUE constraint imply an index?

第二步:UPDATE

要更新部分行但不是全部: 更新所有domain_id,使其成为domains.name 的外键。 但不要使用相关子查询,使用UPDATE with a FROM clause。这里要快得多。

UPDATE stats s
SET    domain_id = d.id 
FROM   domains d
WHERE  d.name = s.domain
AND    domain_id IS NULL; -- assuming existing ids are correct.

然后你可以删除现在多余的列stats.domain

ALTER TABLE stats DROP column domain;

这是非常便宜的。该列在系统目录中被标记为失效。在更新或清除行之前,不会删除实际的列值。

为了进一步提高性能,直接删除操作不需要的所有索引并在之后创建它们 - 都在同一个事务中

,以 n 行为单位批量更新:

How to mark certain nr of rows in table on concurrent access

或者,由于您在评论中澄清您正在更新所有行,因此创建一个新表会便宜得多@ 987654327@ - 如果约束和访问模式允许的话。

要么创建一个全新的表,删除旧表并重命名新表:

Best way to populate a new column in a large table?

或者,如果您需要保留现有表(由于并发访问或其他限制):

Optimizing bulk update performance in PostgreSQL

旁白:切勿使用非描述性术语,如 nameid 作为列名。这是一种普遍的反模式。 Schema 应该是这样的:

CREATE TABLE domain (
   domain_id serial PRIMARY KEY
 , domain    text UNIQUE NOT NULL  -- guessing it should be UNIQUE
);

CREATE TABLE stats (
   stats_id  serial PRIMARY KEY
 , domain_id int REFERENCES domain
 -- , domain text  -- can be deleted after above normalization.
);

【讨论】:

很好的答案!我现在正在运行这个查询。我还用另一个有点慢的部分更新了我的问题。如果你能帮忙那就太好了。迁移完成后我会立即接受答案。 更新运行大约 20 分钟。可以分批 1k 或 10k 吗?更新过程占用了 20% 的 CPU,磁盘使用量增长非常缓慢。数据库中还没有更新任何行。 @PawelBieszczad:由于您要更新所有行,因此表在完成后将是两倍大小(除非它之前因死元组而变得臃肿)。当它通过时,您可能会运行VACUUM FULLCLUSTER。如果并发事务锁定行,更新可能需要很长时间。我添加了指向更快替代方案的链接。也可以批量更新。我也为此添加了另一个链接【参考方案2】:

忘记 php 来支持原始 sql - 循环处理记录和多个执行的语句使其变慢。而是直接在 db 中运行以下查询:

update stats s set domain_id=(select d.id from domains d where d.name=s.domain);

【讨论】:

【参考方案3】:

Erwin 的解决方案应该足够好,您应该可以在 2 小时内完成。

如果您有一个真正的大型统计表,您可能希望跳过最后一个更新步骤。只需创建一个 stats 主键和 domain_id 的新表。

【讨论】:

以上是关于将列更新为连接列中的值的主要内容,如果未能解决你的问题,请参考以下文章

将列中的值转换为现有数据框中的行名

从数据库列和 C# 中的 textBox 值中扣除值,扣除后的值必须存储在新列中

SQL:将列中的 Unicode 数据更新为重音字符

我想在列中的值中添加“%”单位

将我的 Access 表导出到 Excel,但将列中的不同值拆分到不同的工作表中

根据另一列中的值更新 BigQuery 中的嵌套数组