Schema drift is the polite name for a common situation. The database in production has a shape that no file in your migrations folder describes. Sometimes it is one column, sometimes it is an index that quietly appeared during an incident, sometimes it is a trigger that was disabled during a maintenance window and never re-enabled. The team believes the schema is what the migration log says it is, and the runtime believes the schema is whatever the catalog currently returns. Both are internally consistent, and they disagree.
Most teams meet drift for the first time through a bug. A query that used to run in forty milliseconds now takes six seconds, because an index was dropped during a load test three weeks ago and nobody put it back. A report shows zero rows because a column was renamed and the view that read it silently kept using the old name. These bugs are hard to debug, not because the fix is complicated, but because the root cause was invisible until the moment it broke something.
Taavik detects this gap continuously by scanning the schema change history of the databases you connect. The scan classifies each change by severity and keeps enough context to reconstruct what happened, which is the part that makes drift actionable instead of just observable. If a change matters enough to page someone, schema change alerts route it to Slack or email.
The night nobody wrote a migration
An engineer is paged at half past two in the morning. A batch job is running late, the database is straining, and the incident channel is filling up with people looking at graphs. She logs in, tries a few things, and eventually runs a CREATE INDEX on a large table. The job finishes, the pager stops, and she updates the incident channel with a short note before going back to bed.
The next morning nobody mentions the index. She meant to open a pull request to add it to the migrations, but the incident retrospective was more urgent, and the retrospective focused on why the job was slow in the first place. Three weeks later a colleague is preparing a staging environment from the current migrations. The environment comes up without the index. Queries there behave differently from production, and nobody can explain why, because from the perspective of the migration files nothing is missing.
The index in production is real. The migration log does not know about it. This is schema drift.
Where the gap actually comes from
Drift is rarely the result of one careless engineer. It is a structural property of any database that more than one system can touch, and there are usually four sources feeding it.
The first is the incident scenario above. A manual change made under time pressure that never gets promoted into the migration workflow.
The second is auto-migrating frameworks. Some ORMs are configured to align the database to the model on startup, especially in development or staging. If a developer changes a model and runs the app locally, the ORM adds the column. If the same code runs against staging with a permissive configuration, the column appears there too, without a migration file anywhere.
The third is third-party tooling. Analytics platforms, feature flag services, replication agents, and even backup tools sometimes add columns, triggers, or schemas of their own. The team may not realize a vendor's onboarding script added a _last_synced_at column to every table, until a migration fails because of a permission the vendor tool needed and did not have.
The fourth is drift between environments, which is a category of its own. Staging and production accumulate manual changes at different rates, so the same code path can pass in staging and fail in production, or vice versa, for reasons that have nothing to do with the code.
None of these sources requires anyone to break the rules. The gap opens because production is a shared resource that many hands touch, and only some of those hands are wired to the migration workflow.
Why having a migration folder is not enough
The reflex answer to drift is procedural. Add a code review requirement, mandate that every change lands as a migration, refuse to merge anything that touches the schema outside the pipeline. These are all sensible policies, and none of them detect drift once it has already happened.
The reason is that migrations are a log of intentions. They record what somebody wanted the schema to become. The catalog of the running database is a log of reality. It records what the schema actually is right now. These two logs agree at the moment a migration runs successfully, and from then on they can diverge for any of the reasons above. A migration folder full of clean, reviewed files is compatible with a production database that has silently added or lost objects since the last deploy.
The consequence is that you cannot infer the current state of production by reading migrations. You can only infer what production would look like if every migration had been applied in order and nothing else had happened. Whether that assumption holds is exactly what drift detection is meant to answer.
What detection actually needs
Detecting drift is not conceptually complicated. It requires a periodic snapshot of the current catalog, a way to compare the latest snapshot to the previous one, and enough structure in the comparison to distinguish an intentional change from a suspicious one.
On PostgreSQL this means reading pg_class, pg_attribute, pg_index, pg_trigger, and a handful of other system tables into a normalized shape. On SQL Server it is sys.tables, sys.columns, sys.indexes, and the corresponding metadata views. On MySQL it is information_schema.tables, columns, statistics, and triggers. Every provider exposes the same underlying idea through different names.
The comparison then produces a list of differences. A table that appeared. A column that changed type. An index that was dropped. Each of these deserves a severity, because a new nullable column with a null default is not the same event as a NOT NULL constraint added to an existing column. A trigger that appeared out of nowhere is a different kind of signal from a routine that was replaced with an updated body.
The harder part is rename detection. A naive diff treats every rename as a drop followed by an add, which doubles the noise and loses the causal connection between the two events. A better diff compares the shape of dropped and added objects, and when the shapes match it emits a single rename event with a match score. This is the difference between a change log that reads like the actual history and a change log that reads like an unsorted list of surprises.
Doing this without a dedicated tool
If you want to build detection yourself, the shortest path on PostgreSQL is a nightly cron that runs pg_dump --schema-only and stores the output in a git repository. The daily commit becomes your snapshot, and git diff becomes your change list. This works, in the sense that you will see drift when it appears, but it produces a raw text diff that a human has to read and interpret. Rename detection is manual. Severity classification is manual. Routing to alerts is manual.
A step up is to write a small service that reads information_schema directly, serializes the interesting rows into a structured format, and compares consecutive versions in your own code. This gives you structured events instead of text diffs, and it lets you attach severity and metadata to each change. It also becomes a small ongoing engineering project of its own, with all that implies.
Taavik does the same thing continuously across PostgreSQL, MySQL, SQL Server, and Redshift, with rename detection, severity classification, and a queryable history. The reason it exists as a separate product is that the boundary of the problem is wider than most teams estimate at the start. It is not a hard project to begin. It is a hard project to keep running well after twelve months, as the team's attention moves elsewhere.
What drift detection does not solve
Detection makes drift visible. It does not prevent it. If the culture around production changes stays the same, the same causes will keep producing the same effects, and detection will keep flagging them. What changes is that the team stops finding out about drift through downstream bugs. The signal arrives when the change happens, not when the change hurts.
Detection also does not replace migrations. The migration log remains the source of truth for what the schema is supposed to be, and drift detection is the source of truth for what it currently is. Both are needed. If the migration log disappears, the database still runs, but nobody knows how to rebuild it. If drift detection disappears, the migration log stays clean, but silent changes keep accumulating.
Finally, detection does not fix the human workflow. If the incident engineer at half past two in the morning has no clean path to promote her fix into a migration the next day, she will keep doing what she did. A drift alert makes the invisible visible, and the team has to decide what to do with it. Detection is a prerequisite, not a solution.
An operating rhythm that works
Teams that handle drift well tend to converge on a similar rhythm. Scans run on a fixed cadence, more often on production than on staging, because production is where surprises are expensive. Changes are reviewed the same day they appear, even when the review is a two-line note in a channel that reads "yes, this was intentional, migration to follow this week". The review is not there to block the change. It is there to make the invisible visible while everyone still remembers the context.
Over time the team learns which sources produce most of the drift, and adjusts. Sometimes the answer is to tighten a vendor's permissions. Sometimes it is to disable ORM auto-migration in an environment. Sometimes it is to accept that a class of changes will always land manually, and to build a lightweight process to promote them into migrations within a week.
The specific case where two snapshots of the catalog disagree with the migration log is one instance of a broader idea, which is treating structural change as an observability signal in its own right. That is a topic on its own, and it is the subject of a separate piece on database observability.
Start by making drift visible on the databases that matter most. The rest of the practice grows from there.