将数据库内容从一种非常糟糕的结构迁移到一种非常合乎逻辑的结构的最佳实践?

Posted

技术标签:

【中文标题】将数据库内容从一种非常糟糕的结构迁移到一种非常合乎逻辑的结构的最佳实践?【英文标题】:Best practices for migrating database content from one very poor structure to one very logical? 【发布时间】:2011-10-21 20:30:13 【问题描述】:

TL;DR 在一个结构非常糟糕的数据库(有很多列重复、没有相互关系和重复数据)之间迁移大量数据到另一个高度组织和关系结构的最佳方法是什么? - 抱歉读了这么久!

我最近接手了一份非常复杂的工作。它正在重写整个公司的基于 Web 的 IT 平台。恐怕我不能提供太多细节,因为我们不能让老开发人员知道(他有一把隐喻的枪指着公司的脑袋,因为他是唯一一个知道如何做发票生成等关键事情的人,并且要求越来越多的钱)。

主要问题是整个网络平台(由所有员工和所有客户使用)是由一个技术水平低于业余水平的人编写的。它由约 300 个单独的代码文件组成。没有模板库 - 它全部硬编码到每个文件中。没有逻辑的数据库结构——它实际上是在他进行的过程中组成的。没有安全保障——这令人震惊。无论如何,我们将在大约 3 个月的时间内重写整个平台。

但是老板说,在上线的那天早上,任何地方都不会丢失任何客户数据。必须直接复制整个数据库内容。数据库的结构目前非常糟糕,几乎无法使用,但本周我们将(尝试!)编写一些脚本,将其迁移到我们新的、高度相关的结构中,这种结构更加合乎逻辑。 问题是,最好的方法是什么?

一个例子是地址。在旧数据库中,大约 12 个表(总共 44 个......)中使用了地址。在我们的表中,我们有一个 addresses 表,它将被其他表(例如 address_id)交叉引用以保持干净。主要问题是,在他大约一半的表中,地址存储为line1line2towncity 等,这很好,但在另一半他只有一个@ 987654329@ 存储整个内容的字段!

第二个例子是日期——在一些表中,他有秒-since-Epoch 日期,在其他 mysql NOW() 日期中,在其他表中,他将其存储在每行 6 列中 - yearmonthdayhourminutesecond - 哎哟......

尝试解决此问题的好方法是什么? 我们应该查看 我们的 表并找出我们需要将 他的 数据从哪里提取到我们的表中,还是应该反过来看 他的 表并找出他的数据需要进入我们的哪里?

从编程的角度来看,我们应该如何解决这个问题?很多数据需要动态格式化(例如日期),所以我们考虑一次提取一行数据,格式化它正确,然后将其重新插入到我们脚本中的正确位置。

查询的速度和效率对我们来说不是问题,因为我们只需要在本地机器上运行一次(测试后)。当 SQL 转储时,他的数据库目前约为 800MB,但其中很多都是他无用的测试数据,或者完全没有必要。

关于解决这个问题的最佳方法有什么想法吗?作为参考,我们的系统将用 php 重新编写,因此任何基于 PHP 的建议都会很好。该数据库目前(并将仍然)在 MySQL 中。

【问题讨论】:

+1 我目前正在为我工​​作的一家公司重写网站,他们大约 500,000 行的数据库绝对是一团糟,所以如果有人有答案,那就太好了。 一篇“文章”有一半是关于坏人的 :) Question 3094126 讨论如何存储地址。保留主地址表最终会咬你,原因与主客户表会咬你answer 2995299 to question 648463 的原因相同。 【参考方案1】:

这里没有解决方案。没有魔法。只是单纯的努力。

你有你的新模型,而完成这项工作的唯一方法是找到每个表格,并在纸上、白板上等逻辑上将它们单独转换为新模型。

您需要处理的不仅仅是简单的格式问题。您还将需要处理数据重复问题。如果您有 12 个带有地址的表,但只有 1 个客户端,那么哪个地址会胜出?

仅此一项决定就可以简化很多处理过程(例如,除了链接到主客户记录的一个受祝福的地址之外,也许您可​​以忽略其他地址)。

这就引出了最后一个问题。转换过程中“不丢失任何数据”。

根据“不丢失任何数据”的含义,这很可能从第一天开始就无法启动。例如,如果您要丢弃地址,就会有数据丢失。当然,每个组件“都有一个地址”,但不一定是它们以前的地址。之前它们可能都是相同的,但也可能不是。会很乱。

一旦您完成了映射和其他流程,使用几乎任何语言对其进行编码都非常简单。脚本语言可以很好地解决这个问题。您可以将每个表“按原样”批量加载到新数据库中,并编写存储过程来进行转换。无论你熟悉什么。您的转换可能需要几个步骤,并且大部分代码可能是“一次性”的,仅用于促进转换。

这会很乏味。这些东西一直都是。简直是太详细了。这是一个可怕的系统的所有原因都是转换将是可怕的原因。如果您没有预算足够的时间来完成它,请不要感到惊讶。

最后,如果您有大量数据,如果您无法在业务停机期间(周末、晚上等)执行切换,您可能需要处理一些时间限制。如果您在运行时更新数据,那将是另一回事。如果可能的话,我强烈建议不要这样做。

【讨论】:

【参考方案2】:

我最近进行了几次更大规模的迁移,并在此期间逐渐为自己开发了一些实用的最佳实践。这并不是真正的开创性,但您可能会发现其中一些有用:

一般提示

在开始之前,请确保您了解现有数据模型以及新版本系统的要求。 尽可能设计新的数据库架构,并尽量不要因为需要迁移旧内容而给自己带来压力。 使用具有可靠 ORM 的框架。不仅开发新版本更容易,而且迁移也更容易。

迁移

处理数据迁移的代码将在一段时间内成为您项目的一部分,因此最好将其专用于一个包/文件夹(即legacy)。在这个包中保留您的转换脚本和与旧系统相关的其他文件。一段时间后,您将可以通过简单的rm -rf legacy 摆脱它。

脚本应该分小步进行转换。最好在一个表上循环多次并保持步骤小、简单和可调试,而不是拥有一个可以完成所有事情的大脚本,尽管速度更快。

在自己的事务中运行每个步骤并仅在成功完成后提交也是一个好主意,这样您就不需要在一个步骤失败时再次重新运行整个迁移。

整个迁移过程以及特定步骤或步骤组应该可以使用命令行中的一个命令运行,因为您将多次运行它直到达到最终版本,因此您的自动化程度越高更好。

主脚本(即legacy/bin/full-migration)应该执行整个过程(即获取旧生产数据库的新副本,(重新)在其中创建新数据库和表,运行整个迁移)并且它应该与在生产服务器中部署新版本后最终运行的过程完全相同(仅具有不同的配置)。它将允许您在开发环境中对其进行彻底测试。

因为转换可能需要很长时间,所以记录每个操作是有益的(普通的print action + object_id 应该这样做)。通常有几行有一些意想不到的差异,这会使您的脚本崩溃或导致引用完整性错误。在这种情况下,最好查看它是哪个对象,以便您可以立即转到数据库,检查数据,相应地更新脚本并再次运行失败的步骤。

事实证明对我非常有用的一件事是使用 ORM 为遗留数据库表定义模型类。我在 Django 中做过几次,它支持多个数据库连接和每个模型的路由,所以我能够编写看起来大致像这样的脚本(Python):

from legacy import models as old
from catalog import models as new

# Loop through all products from the legacy DB
for old_product in old.Product.objects.all():  
    # Create an instance of the new product model class
    new_product = new.Product() 
    # Copy and modify attributes as needed
    new_product.name = old_product.product_name.strip()
    # ...
    # Save it to the new database
    new_product.save()

此外,新架构越严格越好(即,在可能的情况下不为空,外键检查等),因为它可以帮助您了解您对旧架构的假设在哪些地方是错误的,并防止不正确数据进入你的新系统(InnoDB 作为 MySQL 的后端是个好主意)。

其他好的做法是尽可能在新数据库中保留旧的主键。如果您在迁移后在新数据中看到一些奇怪的东西,您可以返回并在旧系统中通过其 ID 查找该项目。

【讨论】:

+1 用于保留主键。这是一个好主意,因为对于这样的项目,您肯定会错过一些在正式迁移完成后会出现的问题,届时您将乞求数据迁移的完整审计跟踪。 【参考方案3】:

进行重写的第一步是充分理解当前的数据结构和在其上运行的代码。可能有一些数据看起来是多余的,但出于某种奇怪的原因,代码要求它是多余的。是不是设计很差?可能 - 但请确保您完全理解写入或访问数据的每一段代码,以便确定可以删除哪些内容、必须重构哪些内容以及必须保留哪些内容。

工具可以帮助自动化流程 - 但如果不深入了解当前系统,它们可以将您自动化到一个角落。

我会设计新的数据结构,编写脚本将旧结构转换为新结构,然后测试功能。如果有问题,更改新结构和/或导入脚本,然后再次运行数据传输例程并重复整个过程,直到确保没有数据或功能丢失。此时,安排一个日期,关闭旧系统,进行数据迁移,然后启动新系统。

当然,所有这些都缺少培训用户使用新/改进的系统。这是至关重要的!不要把它排除在你的计划之外,否则最好的新的闪亮改进系统会因为用户的不满而沉没。

【讨论】:

博克+1。我要强调一点 - 在回答 OP 的问题时,最好的解决方案是使用可以在调整后测试和重新测试的脚本。对此没有灵丹妙药。需要编写脚本以从各种低质量来源中找到可用的最佳数据。您可能还必须包含一种机制,用于保存需要手动修复的记录日志,一旦新系统启动并运行得足够好,以至于枪从旧程序员手中拿走。 同意。我还要补充一点,您应该尽快开始编写实际脚本(即使数据库模式尚未完全确定,也请开始为您有信心的部分编写脚本)。在编写完这些之后,开始运行所有脚本并查看目标数据库中发生了什么。我过去经历过类似的练习,能够在生产数据上试用脚本提供了一个不断提高质量的机会(最终导致迁移后需要人工干预的记录数量非常少)。【参考方案4】:

要考虑的一件事......

为什么不将新的、固定的、闪亮的模式隐藏在视图后面,让它看起来像旧模式?

这意味着您有 2 个基于相同数据的客户端代码,但每个在数据库中都有自己的“API”。

这也意味着旧系统从未真正在“上线”时关闭。

【讨论】:

视图是正确的工具。但是在一个设计糟糕到足以开始的数据库中,视图将无法工作。使新结构看起来像旧结构并不总是可能的。有时,可能的部分无法使用,因为它们太慢了。 @Catcall:是的,平衡事情总是很棘手【参考方案5】:

在设计新结构时,首先要包含用于保存旧系统的记录标识符和它来自的表的列。您可以在移动被证明成功后放弃这些,但它们将极大地帮助迁移数据并在迁移后测试数据是否正确,并在用户对他们看到的内容感到惊讶时回答有关数据来自何处的问题。如果旧数据没有 PK,则使用某种类型的 automnumber 字段创建它们。

从父表向下工作。如果地址存储在多个位置,请确定要从哪个顺序获取地址,如果有多个不同的记录,则优先顺序。您可能还想存储不同的地址(地址表与人员表是一对多的,是吗?),但您可能需要其他可用的地址类型。

您需要处理旧数据与新数据类型或大小或约束不匹配的问题(例如,您需要某些东西但它们没有值)。在开始之前决定你想如何处理,并从利益相关者那里得到收买。如果需要街道 1 并且您只有城市和州,您可能希望使用“未知”的值。

将任何经过转换以符合新标准的数据或您根本无法弄清楚如何更改的数据发送到例外表。利益相关者或用户可能必须与他们打交道以获取新的所需数据或告诉您要更改的内容。

您可能需要多次运行此程序。首先在开发盒上,然后在 QA 盒上。迁移到 prod 时,如果转换所需的时间超过了您可以承受的停机时间,您可能需要在启动前移动大量数据,然后在启动时只移动新的或更改的数据。

有很多工作要做,3 个月的时间对于这种迁移来说非常紧迫。祝你好运。

【讨论】:

以上是关于将数据库内容从一种非常糟糕的结构迁移到一种非常合乎逻辑的结构的最佳实践?的主要内容,如果未能解决你的问题,请参考以下文章

如何将 AES 模式从一种迁移到另一种?

计算从一种状态到另一种状态需要多少天:SQL

如何将大型 MySQL 数据库从一台服务器迁移到另一台服务器?

尝试从一种形式发布到两个数据库表 - Laravel 8

python 重命名具有迁移功能的Django应用程序已经很糟糕。这是我发现的一种方法,可以保留您的旧迁移历史记录并保留

一种将数据从非常大的 csv 写入 SQL 数据库的方法