{"id":4455,"date":"2026-11-10T09:35:00","date_gmt":"2026-11-10T09:35:00","guid":{"rendered":"https:\/\/dataopsschool.com\/blog\/?p=4455"},"modified":"2026-11-10T09:35:00","modified_gmt":"2026-11-10T09:35:00","slug":"data-change-undo-compared","status":"publish","type":"post","link":"https:\/\/dataopsschool.com\/blog\/data-change-undo-compared\/","title":{"rendered":"Four Ways to Change Live Data, Ranked by How Easily You Can Undo Them"},"content":{"rendered":"<p>Data changes have a nasty property that code changes mostly lack: the damage persists. Redeploy an old build and the bug is gone; restore an old table and you may have lost a day of legitimate writes. That is why reversible decisions in DataOps deserve a blunt comparison rather than a slogan. Below, four common ways of changing a live dataset are set side by side, judged on one thing only: how hard is it to get back to where you started?<\/p>\n<h2>The yardstick: cost of getting back<\/h2>\n<p>Forget elegance and speed for a moment. For each approach, ask three plain questions. How long until I can undo it? What do I lose if I do? And how soon does the undo option expire? That third question is the one people forget. Many &#8220;reversible&#8221; changes are only reversible for a window, until old files are vacuumed, a retention period lapses or consumers start depending on the new shape.<\/p>\n<h2>Option one: change it in place<\/h2>\n<p>You rename the column, rewrite the table, or overwrite the partition, and everything downstream sees the new state at once.<\/p>\n<ul>\n<li><strong>Undo time:<\/strong> usually hours, sometimes days, because it means restoring from a backup and replaying writes.<\/li>\n<li><strong>What you lose:<\/strong> any writes since the backup, unless you replay carefully.<\/li>\n<li><strong>When the window closes:<\/strong> the moment the first downstream job or dashboard consumes the new shape.<\/li>\n<li><strong>Verdict:<\/strong> fine for scratch tables nobody else reads, risky anywhere else. It is the closest thing to a locking door in this list.<\/li>\n<\/ul>\n<h2>Option two: expand, migrate, then contract<\/h2>\n<p>You add the new column or table next to the old one, write to both, move readers over one at a time, and remove the old structure only when nobody touches it.<\/p>\n<ul>\n<li><strong>Undo time:<\/strong> minutes during the first two phases, since readers just point back at the old structure.<\/li>\n<li><strong>What you lose:<\/strong> almost nothing, apart from some storage and extra write work.<\/li>\n<li><strong>When the window closes:<\/strong> at the contract step, when you drop the old structure. That step is the one irreversible moment, and it comes late, with evidence.<\/li>\n<li><strong>Verdict:<\/strong> the workhorse. It converts one scary jump into several small hops, and only the last hop is a one-way one.<\/li>\n<\/ul>\n<h2>Option three: build the new thing alongside and compare<\/h2>\n<p>Instead of altering anything, you produce a second version of the output dataset through a new pipeline and diff it against the existing one for a while. Consumers keep reading the original until the diff is boring.<\/p>\n<ul>\n<li><strong>Undo time:<\/strong> none needed, because nothing was changed. You simply never switch.<\/li>\n<li><strong>What you lose:<\/strong> compute and storage for the duration, plus the effort of reading diffs.<\/li>\n<li><strong>When the window closes:<\/strong> never, until you retire the old pipeline.<\/li>\n<li><strong>Verdict:<\/strong> the safest option for transformation logic you do not fully trust, though it costs money and attention. Worth it for revenue, compliance or customer-facing tables; overkill for internal helper tables.<\/li>\n<\/ul>\n<h2>Option four: lean on versioned storage<\/h2>\n<p>Many modern table formats and warehouses keep history, letting you query or restore a table as it was at an earlier point. If your platform offers it, a bad write becomes a restore to a recent version rather than a recovery project.<\/p>\n<ul>\n<li><strong>Undo time:<\/strong> minutes, if you have rehearsed it.<\/li>\n<li><strong>What you lose:<\/strong> writes made after the version you restore to, unless you reapply them.<\/li>\n<li><strong>When the window closes:<\/strong> when retention expires or a cleanup job removes old versions. Check the retention setting before you rely on it, because the default is often shorter than people assume.<\/li>\n<li><strong>Verdict:<\/strong> an excellent safety net, but a net with an expiry date. It complements the expand-contract pattern rather than replacing it.<\/li>\n<\/ul>\n<h2>Side by side<\/h2>\n<p>Put plainly, the options rank like this for reversibility: building alongside is easiest to walk back, expand-and-contract is nearly as good, versioned storage is good while the history lasts, and in-place edits come last. The order flips if you rank by effort, which is exactly why teams drift toward the riskiest option under deadline pressure. A reversible design costs more at the start and pays back the first time something goes wrong.<\/p>\n<h2>Reversibility in the pipeline, not just the table<\/h2>\n<p>The same sorting applies to pipeline behaviour. A few practical examples:<\/p>\n<ul>\n<li><strong>Idempotent loads.<\/strong> If rerunning a job produces the same result rather than duplicate rows, a failed run is a rerun, not an investigation. That one property turns most incidents into minor ones.<\/li>\n<li><strong>Append, then publish.<\/strong> Land new data in a staging area, check it, then swap it into the published location. The swap is the commitment; everything before it is disposable.<\/li>\n<li><strong>Quarantine, not delete.<\/strong> When a validation rule rejects rows, move them to a holding table. Deleted rows cannot be reviewed; quarantined rows can be released once the rule is fixed.<\/li>\n<li><strong>Soft deletes.<\/strong> Mark records as removed before physically removing them, with a grace period.<\/li>\n<\/ul>\n<p>Validation tooling helps here because it makes the &#8220;publish&#8221; step conditional. <a href=\"https:\/\/dataopsschool.com\/blog\/great-expectations-for-data-validation-in-dataops-a-practical-guide\/\">The hands-on piece on declaring data expectations with Great Expectations<\/a> shows how to gate a load on checks, which is the pipeline equivalent of a door that only opens when conditions are met. Likewise, <a href=\"https:\/\/dataopsschool.com\/blog\/master-data-transformations-with-dbt-in-modern-dataops-pipelines\/\">mastering transformations with dbt<\/a> gives you versioned, reviewable models, so a logic change is a diff you can revert in version control instead of a mystery edit.<\/p>\n<h2>The irreversible list: be strict about it<\/h2>\n<p>Some data decisions really are one-way, and pretending otherwise is how teams get hurt. Treat these with evidence and a second reader:<\/p>\n<ol>\n<li>Physically deleting source data with no copy elsewhere.<\/li>\n<li>Sending data to an external party, whether a partner feed, a published dataset or a customer export.<\/li>\n<li>Changing identifiers that other systems have stored, such as customer keys.<\/li>\n<li>Dropping the old structure at the end of an expand-contract migration.<\/li>\n<li>Anonymising or aggregating raw records in a way that cannot be rebuilt, which can be the right move for privacy but should be taken on purpose.<\/li>\n<\/ol>\n<p>The rest of your week is probably made up of choices that can be tried cheaply. Spend your caution on that short list and let the rest move.<\/p>\n<h2>A worked example: splitting a name field<\/h2>\n<p>Suppose a customer table holds a single full-name column, and the marketing team wants first and last names separately. The in-place route rewrites the column and breaks every report that reads it. The reversible route adds two new columns, fills them from the old one with a script you can rerun, and updates the loader to write all three for a few weeks. Reports move across one at a time, each with its own small announcement. When the last reader has switched and a week of quiet has passed, you schedule the drop of the old column and take that step deliberately, with a snapshot taken minutes before. If someone discovers a report that was missed, the old column is still there and nobody has to explain a gap. The total extra effort is some duplicated writes and a calendar reminder, which is a small fee for removing a whole category of surprise.<\/p>\n<h2>Where outside help fits<\/h2>\n<p>Teams sometimes ask for help working out which of their migrations are the locking kind. A consultancy and training firm in Amsterdam, <a href=\"https:\/\/www.devopsschool.nl\/\">DevOpsSchool Netherlands<\/a>, works on DevOps, SRE and cloud delivery and runs corporate and instructor-led courses, which is the sort of place engineers go when they want release and rollback habits taught properly alongside the data tooling.<\/p>\n<h2>A practical checklist before any change<\/h2>\n<ul>\n<li>Write down the exact command or step that undoes this change, before you make it.<\/li>\n<li>Check how long that undo remains possible (retention, vacuum jobs, expiring snapshots).<\/li>\n<li>Note who or what reads this data. If an outside consumer does, treat it as the locking kind.<\/li>\n<li>Run the undo once on a copy. An untested rollback is a hope.<\/li>\n<li>Decide the contract date in advance, and calendar it, so old structures do not linger forever.<\/li>\n<\/ul>\n<h2>Bottom line<\/h2>\n<p>Do not try to make every data change reversible; that way lies endless parallel pipelines and a budget nobody approved. Sort changes by who consumes them and how long the undo stays alive. Use expand-and-contract as your default, versioned storage as your net, and quarantine instead of deletion. Keep your irreversible list short, written down and reviewed by a second person. Everything else, try it, watch it, and be ready to step back.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In-place edits, expand-and-contract, parallel builds and versioned storage compared on one question: how hard is it to get back to where you started?<\/p>\n","protected":false},"author":6,"featured_media":4456,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[814],"tags":[],"class_list":["post-4455","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dataops"],"_links":{"self":[{"href":"https:\/\/dataopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/4455","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dataopsschool.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/dataopsschool.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/dataopsschool.com\/blog\/wp-json\/wp\/v2\/users\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/dataopsschool.com\/blog\/wp-json\/wp\/v2\/comments?post=4455"}],"version-history":[{"count":1,"href":"https:\/\/dataopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/4455\/revisions"}],"predecessor-version":[{"id":4515,"href":"https:\/\/dataopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/4455\/revisions\/4515"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/dataopsschool.com\/blog\/wp-json\/wp\/v2\/media\/4456"}],"wp:attachment":[{"href":"https:\/\/dataopsschool.com\/blog\/wp-json\/wp\/v2\/media?parent=4455"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dataopsschool.com\/blog\/wp-json\/wp\/v2\/categories?post=4455"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dataopsschool.com\/blog\/wp-json\/wp\/v2\/tags?post=4455"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}