Nobody puts off a database migration because they think the old version is fine. They put it off because the database is the one system where a mistake cannot be fixed by redeploying. That fear is rational. The answer is a method where every step is reversible and every claim is verified against numbers.
End of support is a security event, not a calendar entry
When a database version leaves support, patches stop. The bugs do not. Researchers keep finding flaws, advisories keep landing, and yours becomes the version that will never receive a fix. Everything else you worry about (compliance, insurance, uptime, audit questions) follows from that fact.
- ▸The exposure only grows. Every month past end of support adds newly published issues that will never be patched on your build.
- ▸It drags the whole stack backwards. An old database pins an old client library, which pins an old language runtime, which pins an old operating system. Check the dates in the Microsoft product lifecycle and PHP supported versions pages against what your servers run.
- ▸Attackers prioritise what works. Flaws exploited in the wild are catalogued in the CISA Known Exploited Vulnerabilities catalogue, and unsupported data layer software is a recurring theme.
- ▸It becomes a regulatory problem. Under the EU NIS2 Directive, organisations in scope carry explicit duties around risk management and incident handling. Unsupported software holding personal data is hard to defend there.
What actually goes wrong in a database migration
Rows rarely vanish. They change meaning. That is why teams who check only row counts declare success and then discover a year of corrupted invoices. The real failures are quiet type and encoding problems that pass every superficial test and surface later in money, dates and names.
- ▸Character encoding. Moving an old single byte character set to full Unicode is where accented names, Arabic text and emoji get mangled, especially if the source stored UTF-8 bytes in a column declared otherwise.
- ▸Silent truncation. The target column is shorter or stricter and the load tool trims instead of failing. Nothing errors. Data is simply shorter than it was.
- ▸Numeric types for money. Floating point columns migrated into floating point columns keep their rounding errors. Move currency to fixed precision decimal instead, deliberately, with finance informed.
- ▸Dates and time zones. Naive timestamps, zero dates and implicit server time zone conversion produce records that shift by hours or land on the wrong day, quietly breaking reporting.
- ▸Permissive versus strict engines. Old engines tolerate loose comparison, empty strings where NULL was meant, and automatic type conversion. Strict ones do not, so identical data can return different query results.
- ▸Everything that is not a table. Stored procedures, triggers, views, scheduled events, permissions and collation rules do not travel in a data dump. They must be rewritten and tested.
Step one: schema parity before anything moves
Build the target schema by hand, column by column, with an explicit type mapping. Do not generate it from a dump and hope. This step is where you decide what the data means, and it always uncovers columns that store three different things depending on which part of the application wrote them.
Write a mapping document listing every table and column, source type, target type, and the decision for anything ambiguous. Then profile the source data: count NULLs per column, find maximum lengths, find values outside the range the new type allows, find duplicates in columns you are about to make unique. Old databases enforced very little, so your new constraints will reject real rows.
Decide the policy per column in advance: fix at source, coerce with a documented rule, or park in a quarantine table for review. Making that call under pressure during cutover is how bad data gets waved through.
Step two: dual write, so the new database earns trust
Dual write means every change is applied to both databases while the old one remains authoritative. It is the difference between a migration and a leap of faith, because the new system runs under real production load for weeks while you compare its behaviour against a system you already trust.
- ▸Prefer a change data capture stream over duplicated application code. Reading the transaction log (binary log or write ahead log) keeps the logic in one place and captures writes from jobs, scripts and admin tools the application layer never sees.
- ▸Make writes idempotent. Key every applied change by primary key and version so a replay or a restart cannot double apply.
- ▸Never let the new database fail a user request. The secondary write path is asynchronous and best effort. It alarms loudly on failure, it does not return an error to the customer.
- ▸Measure divergence continuously. Track replication lag and a rolling count of mismatched rows. Divergence trending to zero is the signal that lets you schedule cutover.
Step three: backfill the history without locking the business
Dual write handles new changes. Backfill moves everything that existed before you started. Do it in chunks by primary key range, throttled, resumable, and against a replica where possible, so a large table never holds a lock long enough for customers to notice.
Keep a checkpoint table recording the last completed range per table, so an interrupted run resumes rather than restarting. The critical rule is the watermark: a backfilled historical row must never overwrite a newer live change that arrived through dual write. Enforce it with a conditional write comparing an updated timestamp or version column, and test that rule deliberately by changing a row mid backfill.
Expect several passes: the first reveals the data problems, the second runs against the fixed mapping, the third produces the numbers you report.
Step four: verification that would survive an audit
Verification means producing evidence, not confidence. Before cutover you should be able to hand someone a report showing the two databases agree, with the method written down and repeatable. Row counts are the beginning of that process, never the end.
- ▸Counts per table, and per meaningful partition. Orders per month, users per status. A single total hides two errors that cancel out.
- ▸Checksums per chunk. Hash a canonical concatenation of columns for each key range on both sides and compare. This catches value level corruption that counting never will.
- ▸Business invariants. The sum of ledger entries, the total of open invoices, the count of active subscriptions. Those are the numbers the business notices.
- ▸Boundary sampling. Compare the oldest rows, the newest rows, rows with NULLs, the longest text values and rows containing non Latin characters. Bugs live at the edges.
- ▸Shadow reads. Serve production traffic from the old database while issuing the same query to the new one, then diff the responses in the background. That validates query behaviour and index performance, not just stored bytes.
Step five: cutover in minutes, with a rollback you have rehearsed
Cutover should be a short, boring, scripted window. Everything difficult was done in the previous steps. The rule that keeps it boring is simple: define the rollback criteria in numbers before you start, and rehearse the rollback at least once against production sized data.
1. Announce the window, then put the application into read only mode. 2. Wait for the replication stream to reach zero lag, and confirm it. 3. Run the final verification pass and compare it against the previous run. 4. Flip the connection configuration, through one environment change rather than a code deployment. 5. Run a scripted smoke test covering login, checkout, search, reporting and one write per critical table. 6. Re enable writes and watch error rates and latency against a pre agreed threshold.
Keep the old database running, read only, for at least a fortnight, and reverse the replication direction so it stays current and remains a real option, not a stale snapshot. Take a verified backup before the window that you have restored elsewhere at least once, because an untested backup is a belief, not a control. The guidance under CISA Stop Ransomware makes the same point about tested restores.
The security work to do while you are already in there
A migration is the only moment when changing database defaults is cheap, because every connection string, credential and permission is being touched anyway. Skip it and you carry a decade of bad defaults into the system that was meant to be a fresh start.
- ▸Least privilege for application accounts. The application does not need schema modification rights. Separate the migration, application and read only reporting users.
- ▸No public listener. The database accepts connections from the application network only, never the open internet.
- ▸Encryption in transit and at rest, with certificate validation actually enabled rather than switched off to clear an error.
- ▸Rotate every credential, assuming the old ones sit in a config file, a wiki page and a shell history.
- ▸Turn on audit logging and ship it off the box, so a future investigation has something to read.
Map these to a framework so they survive staff turnover: the NIST Cybersecurity Framework is the pragmatic choice for a small team. For the surface in front of the database, see The Small Business Website Security Checklist for 2026 and What Website Maintenance Should Actually Include (and What You Are Probably Paying For).
If the database is one symptom of a platform you have outgrown, the same method scales to the whole system. Escaping SaaS: The Complete Guide to Migrating Your Business to Custom Software and Leaving WordPress: A Migration Playbook That Does Not Lose Your SEO show how to sequence it.
How TuniCyberLabs helps
We move businesses off unsupported databases without downtime drama: schema mapping and data profiling first, change data capture and dual write in the middle, chunked backfill with checksum verification, then a scripted cutover with a rehearsed rollback. You get an evidence pack showing your data arrived intact, and a hardened data layer instead of a copy of the old defaults.
Tell us which version you are stuck on and we will scope the migration honestly: talk to TuniCyberLabs.
