我正常化数据库并需要运行〜630k更新。这是我的表的基本结构:
结构域
统计
之前,数据库没有域表,域存储在多个表中,有时作为列表(JSON文本)。我将每个域迁移到domains
表,现在我需要建立与stats
表的关系,该表具有domain
列。我添加了domain_id
列并试图以某种方式更新它以匹配id
表中域的domains
。 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);
}
接下来,我从domains
表中获取stats
表中存在的所有域,并使用该域更新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]);
}
我很欣赏在雄辩,查询构建器或原始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")
答案 0 :(得分:1)
忘记php支持原始sql - 处理循环中的记录和多个执行的语句使它变慢。而是直接在db:
中运行以下查询update stats s set domain_id=(select d.id from domains d where d.name=s.domain);
答案 1 :(得分: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;
如果您具有并发写入权限,则存在潜在的竞争条件。最简单的解决方案是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
。该约束是通过列上的索引实现的,这将有助于下一步的性能。
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 行:
或,因为您在评论中澄清了您正在更新 所有 行,所以创建新表格要便宜得多{ {3}} - 如果约束和访问模式允许这样做。
创建一个全新的表,删除旧表并重命名新表:
或,如果您需要保留现有表(由于并发访问或其他限制):
旁白:绝不要使用name
或id
等非描述性字词作为列名。这是一种广泛的反模式。架构应该是这样的:
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.
);
答案 2 :(得分:1)
Erwin的解决方案应该足够好,你应该能够在2小时内完成。
如果您有一个真正的大统计表,您可能想跳过上一个更新步骤。只需创建一个stats主键和domain_id的新表。