Blog
Database DevOps

Database Refactoring: How to Safely Move a Database Column | Harness Blog

This blog post will walk you through a real-world example of how to safely alter a database schema to support new requirements with 0 downtime, data integrity, and the ability to rollback if needed.

TL;DR

Maintaining uptime and data consistency during database schema changes is essential. This example shows how to safely update a database to support new requirements without downtime or data loss. Using triggers, and incremental data migration, database changes can occur parallel to application updates, ensuring both old and new versions work seamlessly. Additionally, rollback mechanisms ensure quick recovery if issues arise. Tools like Harness Database DevOps integrate these changes into CI/CD pipelines for efficient, reliable deployments.

It is crucial to maintain uptime and data consistency while implementing database schema changes. This blog post will provide a real-world example of how to safely alter a database schema to support new requirements with zero downtime, data integrity, and the ability to roll back if needed. nIt expands on a larger scenario, but deep dives into the specifics of how that database migration works.

Our scenario involves an application that tracks inventory across multiple warehouses. Each warehouse is identified by a location field that simply has the name of the city. We’ve recently added a new warehouse in Boston, GA, but our database already contains Boston – the one in Massachusetts. To accommodate this, we need to update our Warehouses table to distinguish between cities and states.

These days, uptime is critical. We cannot afford even one second of downtime, which makes this more complicated. To maximize uptime, we will ensure our database schema can work with the old and new versions of the application simultaneously while ensuring data integrity. Since certain information is tracked in different places before and after migration, maintaining data integrity requires the use of database triggers to ensure that if one location is updated, both are updated. Each change has also been developed with rollback logic so that if something goes wrong, we can fully revert to the previous stable state– with no data loss.

Breaking Down the Changes

The full changes for this scenario can be found in this GitHub repo. These changes are packaged in change-sets, and each change-set can be independently deployed or rolled back. In this blog, we will talk through each of these change-sets and discuss how it ensures safety during the database migration. These changes can be applied using Harness Database Devops or liquibase.

Changeset: Refactor-1

This change-set introduces the new columns to track city and state. It also updates the location column so that if new rows are added that do not specify the location, a unique ID is generated for them. If we did not care about 0 downtime, the ability to rollback while maintaining database consistency, or copying over the existing data, then this would be the entire change.

Full Changeset:

- changeSet:
    id: refactor-1
    author: stephenatwell
    ignore: false
    changes:
    - addColumn:
            tableName: Warehouses
            columns:
              - column:
                  name: City
                  type: nvarchar(50)
              - column:
                  name: State
                  type: nvarchar(50)
    - addDefaultValue:
            columnName: Location
            tableName: Warehouses
            defaultValueComputed: newId()

Changeset: Refactor-1-data-migrate

The use of triggers and transactions in this change-set is what ensures that data remains consistent.

This change-set creates a trigger to automatically populate the new City field with the existing Location data. This ensures that if the old version of the application updates location midway through the migration, then the City field is updated with the new value. This ensures data consistency even if both the old and new versions of the application are running in parallel.

This change-set also copies data from the old ‘location’ column into the new ‘City’ column. This copy is performed in small, incremental batches to minimize locking and performance impact on the production database. During each batch, a set of rows in the table is locked, and the application will not be able to modify those rows until that batch completes. By using small batches, we ensure that only a small percentage of the table is locked at once so that locks are short-lived and do not noticeably impact the performance of our application.

This changelog also disables transactions. If we did not do this, we would lose the benefit of performing our small copies in batches, as each of their transactions would be a child of the main transaction. This transaction nesting prevents locks from being released until the outermost transaction is completed.

Full Changeset:

- changeSet:
    id: refactor-1-data-migrate
    author: stephenatwell
    ignore: false
    # disable transactions so that we can do small/incremental locks during the data copy.
    runInTransaction:  false
    changes:
    - sql:
            sql: |
              CREATE TRIGGER trg_Warehouses_Location_Update
              ON Warehouses
              AFTER INSERT, UPDATE
              AS 
              BEGIN
                  SET NOCOUNT ON;
                  
                  IF EXISTS (SELECT * FROM Inserted WHERE City IS NULL)
                  BEGIN
                      UPDATE Warehouses
                      SET City = i.Location
                      FROM Inserted i
                      WHERE Warehouses.ID = i.ID;
                  END
              END;  
    - sql:
            sql: |
              -- Begin transaction to minimize lock time
              BEGIN TRANSACTION;
              
              
              WHILE 1 = 1
              BEGIN
              
                  -- Update the City column in small batches to minimize lock time
                  UPDATE TOP (2) Warehouses
                  SET 
                      City = Location
                  WHERE 
                      City IS NULL;
                  
                  IF @@ROWCOUNT = 0
                      BREAK;
              END
              
              -- Commit transaction
              COMMIT TRANSACTION; 
    rollback:
    - sql:
            sql: IF OBJECT_ID('dbo.trg_Warehouses_Location_Update', 'TR') IS NOT NULL DROP TRIGGER dbo.trg_Warehouses_Location_Update;

Changeset: boston-georgia:

This change-set updates the reference data in the Warehouses table by adding a new row for the new warehouse in Boston, GA. 

Full Changeset:

- changeSet:
    id: boston-georgia
    author: stephenatwell
    ignore: false
    preConditions:
       - onFail: MARK_RAN
       - sqlCheck:
          expectedResult: 0
          sql: SELECT COUNT(*) FROM Warehouses WHERE City='Boston' AND State='GA'
    changes:
    - insert:
            tableName: Warehouses
            columns:
              - column:
                  name: City
                  value: Boston
              - column:
                  name: State
                  value: GA
    rollback:
    - delete:
            tableName: Warehouses
            where: City='Boston' AND State='GA'

Conclusion

By carefully planning and implementing changes with considerations for consistency, uptime, and rollback, you can ensure smooth transitions and maintain integrity in your data systems. Database changes like these can be safely deployed as part of your CI/CD process using tools like Harness so that database changes no longer slow down your application delivery.

If you encounter similar scenarios and are interested in a seamless DB migration with minimal disruptions, reach out to Harness to learn more about how we can help you realize the benefits of Database DevOps

Learn More

This blog discusses part of a larger scenario that you can learn more about in our overview blog, Database DevOps: managing databases inside your CI/CD pipeline. This scenario leverages Harness Database DevOps and Harness Continuous Delivery to orchestrate this database change along side a change to application code as part of a larger CI/CD pipeline. if you are attending KubeCon 2024, a talk on this topic will also occur on thursday.

← Previous:
Next: →

Related Resources

How Changes to Database Schemas Slow Down Application Delivery

Harness Platform

How Changes to Database Schemas Slow Down Application Delivery

July 31, 2024

Stephen Atwell

+ more
Time to Read

What Makes Database Changes Different?

Database schema changes are inherently more complex than application code changes. In the world of software development, the ability to rapidly deliver new features and updates is crucial. However, one often overlooked bottleneck in this process is the management of database schema changes. While application code can be swiftly modified and deployed, Rapid changes to stateful applications are more complex because database schema changes introduce additional complexity that slows down the entire delivery pipeline.

Data Integrity and Consistency

Unlike application code, which can be versioned and rolled back relatively easily, database schema changes must maintain data integrity and consistency. Any alteration to the schema, such as adding or modifying tables, columns, or indexes, can have far-reaching implications on the existing data. If a rollback needs to occur, it is imperative that data is not lost.

An example schema change

Imagine a database change that splits one column into two based on a delimiter in the column data. Before the migration runs, data is present. During the migration, in addition to creating the two new columns, the data must be copied from the old column to the two new columns. At any point that both sets of columns exist, the question remains: which is the source of truth?

This situation can be safely handled, but what initially looks like a simple change may require multiple carefully considered changes to both the application and the database to be done safely. 

1. You start by setting up the database to have both sets of columns AND to maintain consistency between them:

a. Start by introducing the new columns

b. Now, set up a trigger to copy any change to the old column’s data into the new columns and vice versa. To avoid introducing a trigger cycle, these triggers must check whether the data matches before each update and only perform the update if there is a mismatch.

c. Copy all the data from the old to the new columns.

2. Now, you can deploy the new application version that uses the new columns. 

__wf_reserved_inherit
This is an example of a database schema change that splits one column into two. It shows the steps for introducing new columns, setting up data consistency triggers, copying data, deploying updates, and removing old columns.

Because the database is internally syncing changes bidirectionally via triggers, even if the application runs multiple concurrent pods, and some are on the old version while others are on the new, all pods will always see the same data. Similarly, if an application bug requires a rollback, doing so will not cause data loss. After all environments have been updated, a new data migration can occur to delete the old columns and remove the triggers.

Performance Issues

Poorly optimized schema changes can degrade database performance, affecting overall application performance. Identifying and resolving these performance issues can further delay the delivery process. Database performance problems are often dependent on both the data and the database sizing, making detection of performance issues before they reach production more difficult.

As shown above, ensuring uptime and consistent data during a migration often requires using database triggers to synchronize data if it changes. Since the database is doing twice as many writes, this will also slow down the database.

Additionally, database migrations frequently require locking part or all of the database in order to run. If a lock is held for a significant amount of time, this can result in a database migration, causing a missed SLA when the database cannot process a request because of an in-progress migration. Because of this, you may need to copy rows in batches to minimize locking on large databases.

What Slows Down Database Changes?

Database changes are often slow because of manual bottlenecks in the change process.

Approval Bottlenecks

Database changes often require close coordination between development and database administration (DBA) teams. Developers may write the initial SQL scripts, but DBAs must review and approve these changes to ensure they do not negatively impact database performance, violate data governance policies, or cause data inconsistencies.

The need for DBA approval can create bottlenecks, especially if the DBA team is small or overburdened. This approval process can delay the deployment of new features that depend on the schema changes.

Manual Processes

Many organizations rely on manual processes for deploying database changes. This can involve creating and running SQL scripts manually, which is not only time-consuming but also prone to human error. While Continuous Integration/Continuous Deployment (CI/CD) pipelines have accelerated application delivery, database changes often lag behind in terms of automation. Integrating database changes into CI/CD pipelines has not been as widely adopted as delivery, for example, the delivery of application docker images.

Manual deployment processes increase the risk of errors, such as running scripts in the wrong order or missing critical steps. These errors can lead to deployment failures, requiring time-consuming troubleshooting and rollback procedures.

Without proper tools to track and manage database changes, it can be challenging to understand the state of the database across different environments. This lack of visibility can lead to inconsistencies and unexpected issues, where two environments behave differently because a database change was accidentally not applied to one of them. 

Strategies for Mitigating the Slowdown

To address these challenges and accelerate application delivery, organizations can adopt several strategies:

Automate Database Deployments

Integrate database schema changes into your CI/CD pipeline using tools like Liquibase or Flyway. These tools can help automate the deployment process, reducing the risk of errors and speeding up the approval process. Integrating them into your deployment pipeline can also ensure consistency between the rollout and rollback of your database and application change. 

Many CI/CD tools have features that enforce configuration policies during deployment. Use these tools to replace manual DBA team approvals while ensuring that database changes adhere to governance and compliance requirements. 

Implement Version Control for Database Changes

Like application code, database schema changes should be versioned and stored in a version control system. This ensures that all changes are tracked. It also makes it easy to inspect the code, and the comment history can help you understand the intent of a database change alongside who made it. 

Adopt a DevOps Approach

Encourage collaboration between development and DBA teams by adopting a DevOps approach. This can help streamline the approval process and ensure that database changes are reviewed and tested early in the development cycle.

Final Thoughts

Database schema changes are a critical but often challenging aspect of application delivery. By understanding the complexities involved and adopting strategies to automate and streamline the process, organizations can mitigate the slowdown caused by these changes. Embracing a DevOps culture, leveraging automation tools, and ensuring proper version control and monitoring can help accelerate application delivery and maintain the integrity and performance of the database

Join Us on This Journey

Harness has been discussing the challenges unique to stateful applications with our customers for several years. Since we started building Database DevOps we’ve worked with over 30 customers to define the ideal solution. These customers have helped design our UIs, provided valuable feedback throughout our prototype and alpha phases, and regularly use Database DevOps to deploy their schema migrations. 

As we enter public beta, we invite you to join us as a design partner to shape the future of Database DevOps. Your insights and feedback are invaluable as we refine and expand the capabilities of this module. Together, we can make database changes as seamless and reliable as application code deployments. To join the beta, sign up here for a demo, and we’ll get you started. To learn more, view our product page, check out the documentation on the developer hub, watch brand new demo videos on our YouTube playlist, or come to our session at KubeCon this November.

Get Started

Get Started with Harness AI

Try the full platform free. No module restrictions, no credit card.

Stephen Atwell
Principal Product Manager
Stephen Atwell develops products to improve the life of technologists.
stephen-atwell
Stephen Atwell
https://www.linkedin.com/in/stephen-atwell/