将列更新为连接列中的值
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
表中域的id
。 stats
表有超过 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旁白:切勿使用非描述性术语,如 name
或 id
作为列名。这是一种普遍的反模式。 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 FULL
或CLUSTER
。如果并发事务锁定行,更新可能需要很长时间。我添加了指向更快替代方案的链接。也可以批量更新。我也为此添加了另一个链接【参考方案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 值中扣除值,扣除后的值必须存储在新列中