- A data migration is finished when nobody is pointed at the old system, not when the rows land. Everything in between happens in the applications.
- Nobody has the list of what reads and writes a table, and it was never anywhere to start with. Reconstructing it by asking people is the day the schedule starts slipping.
- An impact analysis is only useful if it says who reads a table and when they last did, what writes it, what each consumer feeds, and what sits two hops out.
- The list has to be derived from artifacts that cannot misremember: repositories, transformation models, orchestration, ingestion config, BI metadata, infrastructure config. Then the query log confirms who still uses it.
- The output is an order of operations: retire the idle, cut over the isolated first, dual-run the finance close last. Every consumer is then specified, tested against old behaviour, built and cut over, with a person on the gate.
Migrations do not stall on technology. They stall on a question somebody asks in a meeting that nobody in the room can answer. This is about how to answer it.
Let’s get something out of the way first: the migration is probably a good idea.
Really. The evaluation went fine, the numbers were real, and the platform you picked is faster and cheaper for what you’re doing. None of that is in dispute here, and none of it is what this post is about.
Because the thing nobody warns you about is that the hard part of a data migration isn’t the data.
And the migration isn’t finished when the rows land. It’s finished when nobody is pointed at the old system anymore.
Everything between those two moments happens in the applications. Which means the dashboards, the jobs, the syncs and the services that read and write the old shape aren’t adjacent to the migration. They are the migration, whether or not anybody scoped them into it.
The meeting where it stalls
You know this meeting.
Three weeks of evaluation. A slice of production data loaded. Benchmarks for the ten queries anybody cares about. A slide with two bars on it, and the right bar is much shorter than the left one. Everybody nods.
Then somebody, usually whoever has to sign the change request, asks what happens to the orders table when you move it.
Crickets.
What comes back is almost never a list of systems. It’s a list of people:
- Ask Priya, she owns the finance dashboards
- isn’t there a job that updates it at four in the morning? is that still Marcus’s?
- somebody should check whether the health score pulls from there or off the replica
- I’ll ask around
I’ll ask around.
That’s the phrase. Write down the date when you hear it, because that’s the day your migration started slipping. (Though it won’t be until two quarters from now that somebody calls it that.)
Nothing technical went wrong. The data was always going to move; moving data is the easy part. We solved that years ago, when dinosaurs roamed the earth. What happened is that you discovered nobody has the list, and now you get to reconstruct it by walking around the building and asking.
Nobody has the list. And it’s not anybody’s fault.
I do a lot of documentation, so you probably think I’m about to talk about documentation discipline. I’m not. Bringing it up would be both unfair and useless.
I mean, think about how the things attached to a warehouse table pile up.
You bought a BI tool specifically so people would stop asking you for dashboards. Self-service is the entire value proposition. Over six years, a few hundred people took you up on it. Some of them even still work at the company.
Teams wrote scheduled jobs that read tables directly. Those teams got reorganized into other teams. Twice.
A revenue-ops person needed one field in the CRM on a Tuesday and set up a sync to get it there. Took her forty minutes. It was the right call. (That was years ago. Today she’d fire up Claude Code and have it in place in eight.)
Somebody wrapped an internal API around a query. Somebody else built a web front end with a live connection that has since been forwarded to half of finance.
And in 2021, under a deadline, an engineer pointed the application straight at the warehouse because it was the fastest way to ship. It worked. It’s still working.
And that is only the things reading it. Something has to put the rows there in the first place, and those piled up the same way: the original load, a change-feed somebody stood up when the product team wanted events, a vendor integration that writes its own table on a schedule nobody controls, and a backfill script that was supposed to run once.
Every single one of those choices was a reasonable decision made by a competent person.
Not one of them got registered anywhere central. Because there is no anywhere central.
Your orchestration tool knows about the jobs. Your BI tool knows about most of the dashboards, assuming its metadata is in good shape. (But it isn’t.) Nothing knows about the web front end, but it’s there, in the repo.
So the list isn’t lost. It was never anywhere to start with.
Three ways to live with not knowing
So here’s what teams do, roughly in descending order of optimism.
Move it all at once. Pick a weekend, cut over, find out what screams. It’s fast, and it’s over quickly one way or another. The catch: you meet every unknown consumer simultaneously, at 2am, with an audience. Everybody writes a rollback plan for this one, and almost everybody uses it.
Move none of it. Approved, funded, on the roadmap, and it slips a quarter. Then another. There’s never a quarter where the risk feels small enough to take blind. Meanwhile you’re paying for the platform you already decided to leave, which somebody in finance will eventually ask about.
Move the easy half. You do the migration to the new system, but both systems stay up, because you can’t demonstrate the old one is idle, and nobody turns off something they can’t prove is idle. This is where most teams end up, and it is worth naming what it shows: the rows moved on schedule, and the migration still didn’t finish. It’s also the most expensive of the three by a wide margin, and it deserves its own post, so I’ll leave it alone.
Three strategies. Same problem. All of them manage the unknown instead of removing it.
What you really need in an answer

Enough tech-PTSD, let’s skip ahead to the useful part. Please take this with our blessing whether or not you ever talk to us.
For one table you want to move, an impact analysis is worth nothing unless it tells you:
Who reads it, and when they last read it. Dashboards, jobs, models, syncs, notebooks, services, preferably with the query attached.
“When they last read it” is not a nice-to-have. It’s the whole thing. A consumer that hasn’t run since you started watching is usually a cleanup ticket, not a risk. From a list of names you can’t tell those apart, so you’ll treat them identically, and you’ll spend all your caution in the wrong places.
What writes it, and in what shape. Ingestion jobs, change feeds, reverse syncs, an application inserting rows directly. Readers break loudly: somebody opens a dashboard and it is empty, and you hear about it that morning. Writers break quietly, and worse, a writer you miss keeps the old table current, which means the old table stays alive, and nobody turns off a system that is still being fed.
What each consumer feeds. A dashboard nobody has opened in months and a dashboard that goes in the board pack can have byte-identical queries. What makes one of them frightening happens downstream of the query. Which is exactly why query-level tooling keeps missing it.
What’s two and three hops out. Not just who reads the table. Who reads the things derived from the table. This is where the genuinely nasty surprises live, because nobody’s mental model of their own system reaches that far. Mine doesn’t, I can tell you that, and yours probably doesn’t either.
What’s safe to cut over first, the consumers with nothing hanging off them. Every migration should start there, rather than where most start, which is whichever table is annoying the loudest person.
What shouldn’t be cut over yet, with the reason written down.
Those last two are the point of the exercise. What you want out of an impact analysis isn’t a warning.
It’s an order of operations.
You can’t ask for this. You have to derive it.
Which is the actual problem, when you sit with it for a second.
Asking requires knowing who to ask. Knowing who to ask is the thing you don’t have. That’s not a process gap you fix with a better intake form. And when you do find out who to ask, half the time they’re long gone.
So it has to come out of the artifacts that can’t misremember what they do:
- repositories → the queries buried in application code
- the transformation project → models and their lineage
- orchestration definitions → what runs on a schedule, and what it touches
- ingestion and change-feed config → what lands rows in the table, and from where
- BI metadata → dashboards and the queries under them
- infrastructure config → which services hold warehouse credentials
Put those together and you get something you can interrogate the way you’d interrogate a colleague who happened to have read everything:
- what reads this
- what writes this
- what does that feed
- what happens two hops out
- what breaks if this column changes type
Building that model, and then keeping it current while the system moves underneath it, is the first thing AI-MSL does on any system we take on. We call it the AppGraph, and nothing gets proposed until it exists. Migration is just the most obvious case, because it comes with a budget and a date. A version upgrade, a schema change, splitting one table in two, retiring a pipeline: every one of those asks the same question about the same unwritten list, usually with less warning and nobody watching. The underlying rule is more boring: you can’t govern changes to a system nobody can describe.
Structure tells you who could read the table
It doesn’t tell you who still does. That’s the other half of the job, and it’s where the query log comes in.
The query log is sitting right there in every warehouse I’ve ever looked at, and almost nobody uses it for this. Every other source tells you what somebody meant to happen. The query log tells you what did happen: last Tuesday, from which service account, against which table.
But here’s why almost nobody does this. On its own, a query log is a firehose. Millions of rows of query text and no idea which of them you care about. As a starting point it’s close to useless.
Run it the other way and it’s the most valuable thing you own. The structure hands you a specific list of candidates, these forty-seven things can read this table, and now you go to the log with forty-seven narrow questions instead of one impossible one. Same question each time. Any sign of life?
That’s how you get to stand up in a room and say, with a straight face, that the dashboard three people have described to you as business-critical hasn’t run in months.
It answers the write side the same way, and that is the half people forget to ask about. The log knows what has been landing rows in the table, not just what has been reading them, which is the question standing between you and turning anything off.
(Check how far back your log goes before you lean on it, by the way. Retention varies a lot by platform, and the default is almost always shorter than you’d like.)
Structure first. Then evidence.
It’s going to tell you things you don’t want to hear
They never come back clean. Not once, in my experience.
There’s a sync still shipping data into a system you retired last year, failing into a retry queue quietly enough that nobody noticed.
There’s a dashboard that’s been wrong since a schema change in the spring. It’s in somebody’s Monday review.
There’s a scheduled job nobody can account for, running under credentials belonging to a person who left two years ago.
And more often than you’d like, there’s a customer-facing report reading a table that three separate people have confidently described as internal-only.
None of that was caused by looking, of course.
It was already true, and it was already yours. The migration is just the first time anybody had a concrete reason to go check. Honestly, the unpleasant findings are a good portion of the return on the exercise, you were going to meet them eventually, and this way you meet them on a Tuesday afternoon instead of at 2am on cutover weekend with the CTO on the phone.
The same meeting, six weeks later
Somebody asks what breaks if we move the orders table.
This time the discussion takes ninety seconds.
Fifty-six things touch it. Forty-seven read it, nine write to it.
- 19 haven’t run in six months → confirm and retire. Housekeeping you owed yourself anyway.
- 22 are internal dashboards with nothing hanging off them → first wave cut over. If you’ve got one wrong, the blast radius is one team’s morning.
- 5 feed the finance close → last, after a dual run across a full reporting period with the numbers reconciled.
- 1 customer-facing report → not this phase, but you’re postponing it rationally instead of just being afraid.
- 9 write into it → the original load, two vendor feeds, a change feed and five jobs. These don’t get a wave of their own. Each one cuts over to the new shape alongside the readers it feeds, and until the last of them does, the old table is still current and the old system is still running.
That is not an easier migration. It’s exactly the same migration, with the same work in it.
What changed is that somebody can defend it now, in front of a person who’s accountable. Scoped, sequenced, estimated. The frightening parts got named instead of discovered in production.
What it does not do is make the work go away. Every one of those consumers still has to be changed, tested and cut over, one at a time.
Nothing clever about that. It’s just what becomes possible once you know the order in which to do things.
The part after “move the data”
Most migration plans have a line near the bottom that reads something like update downstream consumers. One line. Underneath it is the nine months.
Here is what is under it.
The new schema is not the old schema. It never is. Orders came across with the line items split into a table of their own, because that is the shape the new engine wants. Status is an enum now instead of a string. Every timestamp is UTC, where the old ones were whatever the machine that wrote them happened to be set to. And the column you have called customer_id since 2019 is account_id on the other side, because somebody sensibly used the migration to fix a name that was always wrong.
The connection changes too. New driver, new dialect, new auth. The date function in that dashboard’s query takes different arguments. The window function is spelled differently. The client library your application has been importing for six years is not the one it imports now.
None of that is hard. That is the thing about it. Every one of those changes is small, well understood, and, frankly, boring.
There are just four hundred of them across the migration. Fifty-six on the orders table alone. And each one has to be right.
So what does one of them take? Take the forecast dashboard from the finance group, one of the five that feed the close.
It reads the orders table today. Afterwards it has to read the new shape, through the new driver, and produce exactly the number it produces now. That last part is the entire job. Nobody is going to admire the query. They want Q3 revenue to be the same figure on Thursday that it was on Wednesday.
So the change gets specified before anybody writes it:
- what this consumer must still produce, in figures rather than adjectives
- which figures are allowed to change, and why
- what it reads now, and what it will read instead
- who signs that off
Then the tests come from the old behaviour rather than from the new code. You capture what the current query returns across a full reporting period, and that becomes the thing the new one has to match. Then the code. Then QA against those same numbers, a dual run, and any difference explained rather than explained away.
Then it cuts over, and the whole record stays attached to it: what was asked for, what was built, what was checked, who approved it.
Four hundred times. That is the part of the migration nobody scoped, and it is the part that takes the nine months.
Where the judgment goes
This is the loop AI-MSL runs, and it runs it per consumer, because a migration is not one change. It is several hundred changes that happen to share a reason.
The mechanical half is genuinely mechanical. Translating a query into the new dialect, remapping columns, porting a transformation, generating tests from behaviour you can observe: AI is good at that work, and that work is most of the volume.
The other half is judgment, and it does not get automated:
- what to reshape for the new engine rather than carry across unchanged
- what to stop carrying forward at all
- which numbers are allowed to change, because some of them should
Those are decisions with owners, enforced by an AI Lifecycle Manager. Routine changes are reviewed against standards your engineers wrote. Anything that touches the finance close, customer data or a public interface routes to a named senior engineer every time, with an AI Lifecycle Manager holding that gate. Your engineers set the criteria, and a change can only ever be escalated into that lane, never quietly out of it.
Which answers the question you should be asking, which is what happens when one of the four hundred is wrong. It gets caught the way it would if a person had got it wrong: by the tests that came from the old behaviour, at the gate, before anybody’s close depends on it.
And the writers finish the same way. Each one lands in the new shape, the old table stops being current, and the count you have been tracking since that meeting reaches zero. That is the day the migration is over, and it is a long way from the day the rows landed.
The map is not the deliverable
There’s a temptation to treat the dependency graph as the thing you bought, because it screenshots well and it looks great in a steering committee.
It isn’t. A graph nobody reads is a more expensive way of not knowing.
The deliverable is that somebody understands the system again, and specifically, that it doesn’t have to be somebody who was there when it was built. That’s the part that survives people leaving.
So: if we move this, what stops working?
Perfectly fair question. Now you can answer it.

