Database Migration Guide: Oracle, SQL Server, PostgreSQL, and MySQL

Database migration projects can be complex endeavors that require careful planning and execution. This guide provides a detailed roadmap for migrating between the four most popular relational database systems: Oracle, Microsoft SQL Server, PostgreSQL, and MySQL. We’ll explore the unique challenges and considerations for each migration path, along with practical solutions and code examples.

Contents

Quick Reference: Database System Comparison

Let’s begin with a comprehensive comparison of key features and considerations across these database systems:

FeatureOracleSQL ServerPostgreSQLMySQL
Data TypesNUMBER, VARCHAR2, CLOB, BLOBINT, VARCHAR, TEXT, VARBINARYINTEGER, VARCHAR, TEXT, BYTEAINT, VARCHAR, TEXT, BLOB
Auto-incrementSEQUENCEIDENTITYSERIAL/SEQUENCEAUTO_INCREMENT
Default SchemaNo defaultdbopublicNo schema support
Case SensitivityCase-sensitiveCase-insensitiveCase-sensitivePlatform-dependent
Stored ProceduresPL/SQLT-SQLPL/pgSQLSQL/PSM
NULL SortingNULLS LAST defaultNULLS FIRST defaultConfigurableNULLS FIRST default
Outer Join Syntax(+) or ANSIANSI onlyANSI onlyANSI only
CostCommercialCommercialFreeFree
JSON SupportNativeNativeNative (JSONB)Native
PartitioningNativeNativeNativeAvailable in Enterprise
Full-Text SearchOracle TextFull-Text SearchBuilt-inBuilt-in

General Migration Process

Before diving into specific migration paths, let’s understand the general migration workflow that applies to all scenarios:

def general_migration_workflow(source_db, target_db):
    """
    A general framework for database migration
    """
    # Phase 1: Assessment and Planning
    schema_analysis = analyze_source_schema(source_db)
    data_volume = calculate_data_volume(source_db)
    dependencies = identify_dependencies(source_db)

    # Phase 2: Schema Conversion
    target_schema = convert_schema(schema_analysis, source_db, target_db)

    # Phase 3: Data Migration
    migration_batches = plan_data_migration(data_volume)

    # Phase 4: Object Migration
    stored_procedures = convert_stored_procedures(source_db, target_db)
    views = convert_views(source_db, target_db)
    triggers = convert_triggers(source_db, target_db)

    # Phase 5: Testing and Validation
    validate_schema(target_schema)
    validate_data_integrity(source_db, target_db)

    return MigrationPlan(
        schema=target_schema,
        procedures=stored_procedures,
        batches=migration_batches
    )

Oracle to PostgreSQL Migration

Oracle to PostgreSQL is one of the most common migration paths due to cost considerations. Here’s how to handle key differences:

Data Type Mapping

-- Oracle to PostgreSQL type mapping function
CREATE OR REPLACE FUNCTION map_oracle_to_pg_type(
    oracle_type VARCHAR2,
    data_length NUMBER,
    data_precision NUMBER,
    data_scale NUMBER
) RETURN VARCHAR2 IS
BEGIN
    CASE oracle_type
        WHEN 'NUMBER' THEN
            IF data_precision IS NULL THEN
                RETURN 'double precision';
            ELSIF data_scale = 0 THEN
                IF data_precision <= 4 THEN
                    RETURN 'smallint';
                ELSIF data_precision <= 9 THEN
                    RETURN 'integer';
                ELSIF data_precision <= 18 THEN
                    RETURN 'bigint';
                ELSE
                    RETURN 'numeric';
                END IF;
            ELSE
                RETURN 'numeric(' || data_precision || ',' || data_scale || ')';
            END IF;
        WHEN 'VARCHAR2' THEN
            RETURN 'varchar(' || data_length || ')';
        WHEN 'DATE' THEN
            RETURN 'timestamp';
        WHEN 'CLOB' THEN
            RETURN 'text';
        WHEN 'BLOB' THEN
            RETURN 'bytea';
        ELSE
            RETURN 'text';
    END CASE;
END;
/

PL/SQL to PL/pgSQL Conversion

-- Oracle PL/SQL
CREATE OR REPLACE PROCEDURE update_customer_status(
    p_customer_id IN NUMBER,
    p_status IN VARCHAR2
) IS
BEGIN
    UPDATE customers
    SET status = p_status
    WHERE customer_id = p_customer_id;

    IF SQL%ROWCOUNT = 0 THEN
        raise_application_error(-20001, 'Customer not found');
    END IF;
    COMMIT;
END;
/

-- PostgreSQL PL/pgSQL
CREATE OR REPLACE FUNCTION update_customer_status(
    p_customer_id INTEGER,
    p_status VARCHAR
) RETURNS VOID AS $$
BEGIN
    UPDATE customers
    SET status = p_status
    WHERE customer_id = p_customer_id;

    IF NOT FOUND THEN
        RAISE EXCEPTION 'Customer not found'
            USING ERRCODE = '23505';
    END IF;
END;
$$ LANGUAGE plpgsql;

SQL Server to PostgreSQL Migration

SQL Server to PostgreSQL migrations require careful handling of T-SQL specific features:

Handling Identity Columns

-- SQL Server
CREATE TABLE orders (
    order_id INT IDENTITY(1,1) PRIMARY KEY,
    customer_id INT NOT NULL,
    order_date DATETIME DEFAULT GETDATE()
);

-- PostgreSQL equivalent
CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    customer_id INTEGER NOT NULL,
    order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Converting Table-Valued Functions

-- SQL Server
CREATE FUNCTION get_customer_orders(@customer_id INT)
RETURNS TABLE
AS
RETURN
(
    SELECT order_id, order_date, total_amount
    FROM orders
    WHERE customer_id = @customer_id
);

-- PostgreSQL equivalent
CREATE OR REPLACE FUNCTION get_customer_orders(p_customer_id INTEGER)
RETURNS TABLE (
    order_id INTEGER,
    order_date TIMESTAMP,
    total_amount NUMERIC
) AS $$
BEGIN
    RETURN QUERY
    SELECT o.order_id, o.order_date, o.total_amount
    FROM orders o
    WHERE o.customer_id = p_customer_id;
END;
$$ LANGUAGE plpgsql;

MySQL to PostgreSQL Migration

MySQL to PostgreSQL migration requires attention to data type differences and syntax variations:

Handling AUTO_INCREMENT

def convert_mysql_auto_increment(mysql_schema):
    """
    Converts MySQL AUTO_INCREMENT to PostgreSQL SERIAL
    """
    converted_schema = re.sub(
        r'int.*AUTO_INCREMENT',
        'SERIAL',
        mysql_schema,
        flags=re.IGNORECASE
    )
    return converted_schema

Converting Group Concatenation

-- MySQL
SELECT customer_id,
       GROUP_CONCAT(order_id ORDER BY order_date DESC) as order_list
FROM orders
GROUP BY customer_id;

-- PostgreSQL equivalent
SELECT customer_id,
       STRING_AGG(order_id::text, ',' ORDER BY order_date DESC) as order_list
FROM orders
GROUP BY customer_id;

Special Considerations for Each Migration Path

Oracle to PostgreSQL

When migrating from Oracle to PostgreSQL, pay special attention to:

-- Sequence handling
-- Oracle
CREATE SEQUENCE order_seq START WITH 1 INCREMENT BY 1;
CREATE TRIGGER order_id_trigger
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
    SELECT order_seq.NEXTVAL INTO :NEW.order_id FROM dual;
END;

-- PostgreSQL
CREATE SEQUENCE order_seq START 1;
ALTER TABLE orders ALTER COLUMN order_id SET DEFAULT nextval('order_seq');

SQL Server to PostgreSQL

SQL Server migrations require careful handling of:

-- DateTime conversion
-- SQL Server
SELECT DATEADD(day, 1, order_date) FROM orders;

-- PostgreSQL
SELECT order_date + INTERVAL '1 day' FROM orders;

-- Full-text search conversion
-- SQL Server
SELECT * FROM products 
WHERE CONTAINS(description, 'searchterm');

-- PostgreSQL
SELECT * FROM products 
WHERE to_tsvector('english', description) @@ to_tsquery('english', 'searchterm');

MySQL to PostgreSQL

MySQL migrations need special attention for:

-- ENUM handling
-- MySQL
CREATE TABLE orders (
    status ENUM('pending', 'processing', 'completed')
);

-- PostgreSQL
CREATE TYPE order_status AS ENUM ('pending', 'processing', 'completed');
CREATE TABLE orders (
    status order_status
);

Post-Migration Tasks

After completing any migration, perform these essential tasks:

-- Update statistics
ANALYZE VERBOSE;

-- Rebuild indexes
REINDEX DATABASE your_database;

-- Validate foreign keys
SELECT conname, contype, 
       pg_catalog.pg_get_constraintdef(r.oid, true) as condef
FROM pg_catalog.pg_constraint r
WHERE r.contype = 'f'
ORDER BY conname;

-- Check for invalid indexes
SELECT schemaname, tablename, indexname, 
       pg_size_pretty(pg_relation_size(indexname::regclass))
FROM pg_indexes
WHERE indexname IN (
    SELECT indexrelname 
    FROM pg_stat_user_indexes 
    WHERE idx_scan = 0
);

Performance Monitoring and Optimization

After migration, monitor and optimize performance:

-- Check for slow queries
SELECT pid, age(clock_timestamp(), query_start), usename, query
FROM pg_stat_activity
WHERE query != '<IDLE>'
AND query NOT ILIKE '%pg_stat_activity%'
ORDER BY query_start desc;

-- Identify missing indexes
SELECT schemaname, tablename, 
       round(heap_blks_hit * 100.0 / nullif(heap_blks_hit + heap_blks_read, 0), 1) as cache_hit_ratio
FROM pg_statio_user_tables
WHERE heap_blks_hit + heap_blks_read > 0
ORDER BY cache_hit_ratio DESC;

Conclusion

Database migration is a complex process that requires careful planning and execution. The key to success lies in understanding the unique characteristics of both the source and target databases. This guide has covered the essential aspects of migrating between major database systems, but remember that each migration project may have its own specific requirements and challenges.

Remember these key points for a successful migration:

  1. Always maintain comprehensive backups throughout the migration process
  2. Test the migration thoroughly in a staging environment
  3. Plan for adequate downtime during the final migration
  4. Monitor performance closely after migration
  5. Keep application code updated to leverage target database features
  6. Document all changes and maintain detailed migration logs

By following this guide and adapting it to your specific needs, you can ensure a successful database migration project while minimizing risks and downtime.