DEV Community

Apache SeaTunnel
Apache SeaTunnel

Posted on

Building Production-Ready MySQL to PostgreSQL Data Pipelines with Apache SeaTunnel 2.3.12

This article provides a complete hands-on walkthrough of heterogeneous data synchronization with Apache SeaTunnel 2.3.12, covering everything from PostgreSQL container deployment and test data preparation to five commonly used production synchronization patterns.

Through these practical examples, you will learn how to leverage SeaTunnel’s core capabilities, including data transformation, batch optimization, and pre-execution SQL operations, enabling developers to quickly build MySQL-to-PostgreSQL synchronization pipelines—even without prior experience with data integration frameworks.

Preparing Source Test Data (MySQL)

Before running the synchronization demos, prepare the source dataset in MySQL.

The demo uses a sample table named t_8_100w, which contains approximately 1 million records and covers common data types, including:

  • Numeric fields
  • String fields
  • Timestamp fields
  • Large text fields
  • Nullable address fields

Example schema:

CREATE TABLE t_8_100w (
    id BIGINT PRIMARY KEY,
    user_name VARCHAR(2000),
    sex VARCHAR(20),
    decimal_f DECIMAL(32,6),
    phone_number VARCHAR(20),
    age VARCHAR(20),
    create_time TIMESTAMP,
    description TEXT,
    address VARCHAR(2000)
);
Enter fullscreen mode Exit fullscreen mode

You can generate the sample dataset using your own test data generator or load the prepared SQL script before running the SeaTunnel synchronization jobs.

Demo 1: Full Load + Truncate Before Insert + Physical Delete

  • Suitable for extracting tables without primary keys. This mode takes effect only when the integration type is Full Load and the synchronization delete mode is configured as Physical Delete.

  • During each execution, the target table will be cleared first and then fully reloaded with all data from the source table.

  • Core principle:

Truncate and reload every time

  • Core configuration:

data_save_mode = "DROP_DATA"

  • Execution Command
sh /……/seatunnel-2.3.12/bin/seatunnel.sh --config /……/myconf/ds-st-demo1-mysql2pgsql-ql-qkhcr-wlsc.conf -i JAVA_OPTS='-Xmx2g -Xms2g' -m local
Enter fullscreen mode Exit fullscreen mode
  • Create Table
-- ds-st-demo1-mysql2pgsql-ql-qkhcr-wlsc.conf

CREATE TABLE "public"."t_8_100w_imp_st_ds_demo1_ql_qkhcr_wlsc" (
  id BIGINT PRIMARY KEY,
  user_name VARCHAR(2000),
  sex VARCHAR(20),
  decimal_f NUMERIC(32, 6),
  phone_number VARCHAR(20),
  age INT,
  create_time TIMESTAMP,
  description TEXT,
  address VARCHAR(2000) DEFAULT 'Unknown'
);

COMMENT ON COLUMN "public"."t_8_100w_imp_st_ds_demo1_ql_qkhcr_wlsc"."id" IS 'Primary key';

COMMENT ON COLUMN "public"."t_8_100w_imp_st_ds_demo1_ql_qkhcr_wlsc"."user_name" IS 'User name';

COMMENT ON COLUMN "public"."t_8_100w_imp_st_ds_demo1_ql_qkhcr_wlsc"."sex" IS 'Gender: Male; Female';

COMMENT ON COLUMN "public"."t_8_100w_imp_st_ds_demo1_ql_qkhcr_wlsc"."decimal_f" IS 'Large numeric value';

COMMENT ON COLUMN "public"."t_8_100w_imp_st_ds_demo1_ql_qkhcr_wlsc"."phone_number" IS 'Phone number';

COMMENT ON COLUMN "public"."t_8_100w_imp_st_ds_demo1_ql_qkhcr_wlsc"."age" IS 'Convert string age to integer';

COMMENT ON COLUMN "public"."t_8_100w_imp_st_ds_demo1_ql_qkhcr_wlsc"."create_time" IS 'Creation timestamp';

COMMENT ON COLUMN "public"."t_8_100w_imp_st_ds_demo1_ql_qkhcr_wlsc"."description" IS 'Long text content';

COMMENT ON COLUMN "public"."t_8_100w_imp_st_ds_demo1_ql_qkhcr_wlsc"."address" IS 'Replace empty address with default value: Unknown';
Enter fullscreen mode Exit fullscreen mode
  • SeaTunnel Configuration
# ds-st-demo4-mysql2pgsql-ql-qkhcr-wlsc.conf

env {
  # Parallelism (number of execution threads)
  execution.parallelism = 5

  # Job execution mode:
  # BATCH: Batch processing mode
  # STREAMING: Stream processing mode
  job.mode = "BATCH"
}


source {

  jdbc {

    url = "jdbc:mysql://ip:port/cs1"

    driver = "com.mysql.cj.jdbc.Driver"

    user = "root"

    password = "zysoft"


    # SQL query used to extract source data
    query = "select * from t_8_100w"


    # Assign a name to this dataset
    plugin_output = "source_data"



    # Parallel reading configuration

    # Numeric primary key column used for data partitioning
    partition_column = "id"


    # Number of partitions, aligned with execution.parallelism
    partition_num = 5


    # Number of records fetched in each batch
    fetch_size = 5000


    # Optional:
    # partition_lower_bound = 1
    # Starting ID value

    # partition_upper_bound = 1000000
    # Ending ID value



    # Connection parameters

    # Connection timeout: 300 seconds
    connection_check_timeout_sec = 300


    # Additional JDBC parameters
    properties = {

      useUnicode = true

      characterEncoding = "utf8"

      serverTimezone = "Asia/Shanghai"


      # Enable cursor-based fetching to improve performance
      # for large result sets
      useCursorFetch = "true"


      # Number of rows fetched each time
      defaultFetchSize = "5000"
    }
  }
}



# Data cleaning and transformation
#
# For simple transformations, it is recommended to process
# the logic directly in the source SQL query whenever possible.

transform {


  # 1. Field mapping:
  #
  # The mapping logic is already handled in the SQL query.
  # In real production scenarios, this step can also be implemented
  # through the FieldMapper plugin.

  FieldMapper {

    plugin_input = "source_data"

    plugin_output = "FieldMapper_data"


    field_mapper = {

      id = id

      name = user_name

      sex = sex

      decimal_f = decimal_f

      phone_number = phone_number


      # Temporary field name
      age = age_str


      create_time = create_time

      description = description

      address = address
    }
  }



  Sql {

    plugin_input = "FieldMapper_data"

    plugin_output = "Sql_age_data"


    query = """

      SELECT

        id,

        user_name,

        sex,

        decimal_f,

        phone_number,

        CAST(age_str AS INTEGER) as age,

        create_time,

        description,

        address

      from dual

    """
  }



  # 2. Phone number masking:
  #
  # Example:
  # 13812341234 -> 138****1234



  # 3. Age conversion:
  #
  # Convert age from string format to integer.
  #
  # In real production environments,
  # if conversion is unnecessary, the value can be stored directly.



  # 4. Gender conversion:
  #
  # Example:
  # 1 -> Male
  # 2 -> Female



  # 5. Data filtering:
  #
  # Keep only records where age > 25.



  # 6. Default address handling:
  #
  # Replace empty address values with "Unknown".

}






sink {


  jdbc {


    url = "jdbc:postgresql://ip:port/source_db"


    driver = "org.postgresql.Driver"


    user = "postgres"


    password = "123456"



    # query = "insert into test_table(name,age) values(?,?)"


    # Automatically generate INSERT SQL.
    #
    # If the target table does not exist,
    # SeaTunnel can automatically create it.
    generate_sink_sql = true



    # Required when generate_sink_sql=true
    database = source_db



    table = "public.t_8_100w_imp_st_ds_demo4_ql_qkhcr_wlsc"



    # Final dataset received by the sink
    plugin_input = "Sql_age_data"




    # Error when target table does not exist.
    #
    # In most production scenarios,
    # CREATE_SCHEMA_WHEN_NOT_EXIST is recommended:
    #
    # Create the table if it does not exist.
    # Skip creation if the table already exists
    # and keep existing data.

    schema_save_mode = "ERROR_WHEN_SCHEMA_NOT_EXIST"




    # APPEND_DATA:
    #
    # Keep table schema and existing data,
    # then append new records.
    #
    # (Usually recommended)


    # DROP_DATA:
    #
    # Keep table schema,
    # remove all existing records.
    #
    # Used to implement truncate-and-reload strategy.

    data_save_mode = "DROP_DATA"




    # Number of records written in each batch
    batch_size = 5000


    # Batch commit interval
    batch_interval_ms = 500



    # Maximum retry attempts
    max_retries = 3




    # Connection parameters

    # Connection timeout: 300 seconds
    connection_check_timeout_sec = 300




    # Additional JDBC parameters

    properties = {


      # PostgreSQL-specific optimization parameter
      #
      # Enable PostgreSQL batch insert optimization.
      #
      # Note: Parameter name is case-sensitive.
      reWriteBatchedInserts = "true"



      # Timezone configuration if required
      options = "-c timezone=Asia/Shanghai"

    }

  }

}
Enter fullscreen mode Exit fullscreen mode
  • Execution Result

During execution, the log shows that SeaTunnel performs a table truncation operation before loading new data:

TRUNCATE TABLE "public"."t_8_100w_imp_st_ds_demo4_ql_qkhcr_wlsc"
Enter fullscreen mode Exit fullscreen mode

Execution log:

2025-12-15 15:32:25,675 INFO  [.s.c.s.j.c.AbstractJdbcCatalog] [seatunnel-coordinator-service-2] - Catalog Postgres established connection to jdbc:postgresql://ip:port/source_db

2025-12-15 15:32:25,676 INFO  [a.s.a.s.SaveModeExecuteWrapper] [seatunnel-coordinator-service-2] - Executing save mode for table: source_db.public.t_8_100w_imp_st_ds_demo4_ql_qkhcr_wlsc, with SchemaSaveMode: ERROR_WHEN_SCHEMA_NOT_EXIST, DataSaveMode: DROP_DATA using Catalog: Postgres

2025-12-15 15:32:25,719 INFO  [a.s.a.s.DefaultSaveModeHandler] [seatunnel-coordinator-service-2] - Truncating table source_db.public.t_8_100w_imp_st_ds_demo4_ql_qkhcr_wlsc with action TRUNCATE TABLE "public"."t_8_100w_imp_st_ds_demo4_ql_qkhcr_wlsc"

2025-12-15 15:32:25,722 INFO  [.s.c.s.j.c.AbstractJdbcCatalog] [seatunnel-coordinator-service-2] - Execute sql : TRUNCATE TABLE "public"."t_8_100w_imp_st_ds_demo4_ql_qkhcr_wlsc"
Enter fullscreen mode Exit fullscreen mode

Job progress:

***********************************************
           Job Progress Information
***********************************************

Job Id                    : 1052850597616680961

Read Count So Far         :              849934

Write Count So Far        :              849934

Average Read Count        :             14165/s

Average Write Count       :             14165/s

Last Statistic Time       :             2025-12-15 15:32:26

Current Statistic Time    :             2025-12-15 15:33:26

***********************************************
Enter fullscreen mode Exit fullscreen mode

Final job statistics:

***********************************************
           Job Statistic Information
***********************************************

Start Time                : 2025-12-15 15:32:23

End Time                  : 2025-12-15 15:33:39

Total Time(s)             :                  76

Total Read Count          :             1000004

Total Write Count         :             1000004

Total Failed Count        :                   0

***********************************************
Enter fullscreen mode Exit fullscreen mode

Result summary:

  • Total records read: 1,000,004
  • Total records written: 1,000,004
  • Failed records: 0
  • Execution time: 76 seconds
  • Average throughput: 14,165 records/s

This demonstrates a typical production pattern for full-load synchronization with truncate-and-reload semantics using Apache SeaTunnel.

Demo 2: Full Load + Append Data

This synchronization mode is suitable for scenarios where historical data in the target database needs to be preserved and new records from the source database should be appended continuously.

Typical production scenarios include:

  • Initial data migration followed by incremental data accumulation.
  • Periodic full extraction where historical records must remain unchanged.
  • Data warehouse loading scenarios where new partitions or new business records are appended.

  • Core Principle

Keep existing data and append new records

Unlike the previous DROP_DATA mode, this mode does not clear the target table before writing.

The execution flow is:

Read source data

        ↓

Transform data

        ↓

Insert new records into target table

        ↓

Keep existing target data unchanged
Enter fullscreen mode Exit fullscreen mode
  • Core Configuration
data_save_mode = "APPEND_DATA"
Enter fullscreen mode Exit fullscreen mode
  • Execution Command
sh /……/seatunnel-2.3.12/bin/seatunnel.sh --config /……/myconf/ds-st-demo2-mysql2pgsql-append.conf -i JAVA_OPTS='-Xmx2g -Xms2g' -m local
Enter fullscreen mode Exit fullscreen mode
  • Target Table
CREATE TABLE "public"."t_8_100w_imp_st_ds_demo2_append" (

  id BIGINT PRIMARY KEY,

  user_name VARCHAR(2000),

  sex VARCHAR(20),

  decimal_f NUMERIC(32, 6),

  phone_number VARCHAR(20),

  age INT,

  create_time TIMESTAMP,

  description TEXT,

  address VARCHAR(2000) DEFAULT 'Unknown'

);


COMMENT ON COLUMN "public"."t_8_100w_imp_st_ds_demo2_append"."id"
IS 'Primary key';


COMMENT ON COLUMN "public"."t_8_100w_imp_st_ds_demo2_append"."user_name"
IS 'User name';


COMMENT ON COLUMN "public"."t_8_100w_imp_st_ds_demo2_append"."sex"
IS 'Gender: Male; Female';


COMMENT ON COLUMN "public"."t_8_100w_imp_st_ds_demo2_append"."decimal_f"
IS 'Large numeric value';


COMMENT ON COLUMN "public"."t_8_100w_imp_st_ds_demo2_append"."phone_number"
IS 'Phone number';


COMMENT ON COLUMN "public"."t_8_100w_imp_st_ds-demo2_append"."age"
IS 'Convert string age to integer';


COMMENT ON COLUMN "public"."t_8_100w_imp_st_ds_demo2_append"."create_time"
IS 'Creation timestamp';


COMMENT ON COLUMN "public"."t_8_100w_imp_st_ds_demo2_append"."description"
IS 'Long text content';


COMMENT ON COLUMN "public"."t_8_100w_imp_st_ds_demo2_append"."address"
IS 'Replace empty address with default value: Unknown';
Enter fullscreen mode Exit fullscreen mode
  • SeaTunnel Configuration
env {

  # Number of parallel execution threads
  execution.parallelism = 5


  # Job mode:
  # BATCH: Batch processing mode
  # STREAMING: Streaming processing mode
  job.mode = "BATCH"

}



source {

  jdbc {

    url = "jdbc:mysql://ip:port/cs1"


    driver = "com.mysql.cj.jdbc.Driver"


    user = "root"


    password = "zysoft"



    # Extract source data
    query = "select * from t_8_100w"



    plugin_output = "source_data"



    # Enable parallel reading

    # Partition by numeric primary key
    partition_column = "id"


    # Number of parallel partitions
    partition_num = 5


    # Fetch records in batches
    fetch_size = 5000



    properties = {

      useUnicode = true

      characterEncoding = "utf8"

      serverTimezone = "Asia/Shanghai"


      # Improve performance for large result sets
      useCursorFetch = "true"


      defaultFetchSize = "5000"

    }

  }

}





transform {


  FieldMapper {


    plugin_input = "source_data"


    plugin_output = "FieldMapper_data"


    field_mapper = {

      id = id

      name = user_name

      sex = sex

      decimal_f = decimal_f

      phone_number = phone_number

      age = age

      create_time = create_time

      description = description

      address = address

    }

  }

}





sink {


  jdbc {


    url = "jdbc:postgresql://ip:port/source_db"


    driver = "org.postgresql.Driver"


    user = "postgres"


    password = "123456"



    generate_sink_sql = true



    database = source_db



    table = "public.t_8_100w_imp_st_ds_demo2_append"



    plugin_input = "FieldMapper_data"



    # Fail if table does not exist
    schema_save_mode = "ERROR_WHEN_SCHEMA_NOT_EXIST"



    # Preserve existing data and append new records
    data_save_mode = "APPEND_DATA"



    # Batch write size
    batch_size = 5000



    # Batch commit interval
    batch_interval_ms = 500



    # Retry attempts
    max_retries = 3



    properties = {


      # PostgreSQL batch insert optimization
      reWriteBatchedInserts = "true"


      options = "-c timezone=Asia/Shanghai"

    }

  }

}
Enter fullscreen mode Exit fullscreen mode
  • Execution Result
2025-12-16 17:07:03,711 INFO  [s.c.s.s.c.ClientExecuteCommand] [main] - 
        ***********************************************
                   Job Statistic Information
        ***********************************************
        Start Time                : 2025-12-16 17:06:08
        End Time                  : 2025-12-16 17:07:03
        Total Time(s)             :                  54
        Total Read Count          :             1000001
        Total Write Count         :             1000001
        Total Failed Count        :                   0
        ***********************************************
Enter fullscreen mode Exit fullscreen mode

Demo 2.1: Full Load - Differential Comparison Synchronization (Insert + Update) - Physical Delete + Large Table Partitioning (Auto-generated SQL)

  • Core requirements: Same as Demo 2

  • Core logic: Same as Demo 2. However, SeaTunnel will automatically generate SQL statements, so there is no need to manually write the query statement.

  • Core configuration:

enable_upsert = true

primary_keys = ["unique_key"]
Enter fullscreen mode Exit fullscreen mode
  • Execution command
sh /……/seatunnel-2.3.12/bin/seatunnel.sh --config /……/myconf/ds-st-demo2-mysql2pgsql-ql-cybjcj-wlsc-2-enable_upsert.conf -i JAVA_OPTS='-Xmx2g -Xms2g' -m local
Enter fullscreen mode Exit fullscreen mode
  • conf configuration
env {

  # Job name: 
  # In production environments, the table ID can be used as the identifier.
  job.name = "ds-st-demo2-mysql2pgsql-ql-cybjcj-wlsc-2-enable_upsert.conf"


  # Maximum parallel threads:
  # Parallelism
  parallelism = 5


  # Job mode:
  # BATCH: Batch processing mode
  # STREAMING: Streaming processing mode
  job.mode = "BATCH"

}

source {

  jdbc {

    url = "jdbc:mysql://ip:port/cs1"

    driver = "com.mysql.cj.jdbc.Driver"

    user = "root"

    password = "zysoft"


    # SQL used to extract source data
    query = "select * from t_8_100w"


    # Assign a name to this dataset
    plugin_output = "source_data"



    # Parallel reading configuration


    # Partition column:
    # Supports:
    # String, Number (int, bigint, decimal, ...), Date

    partition_column = "id"



    # Split size:
    # Number of rows contained in each split.
    #
    # Default value: 8096 rows.
    #
    # Final number of splits =
    # Total table rows / split.size

    split.size = 50000



    # Number of partitions.
    #
    # In SeaTunnel 2.3.12, this configuration is no longer recommended.
    # Use split.size instead.

    # partition_num = 5



    # Maximum batch fetch size:
    #
    # Specifies the number of rows read in each execution.
    #
    # Default value: 1000.
    #
    # This value is affected by available memory.
    # If this value is large or each record contains large amounts of data,
    # increase the runtime memory accordingly.

    fetch_size = 10000



    # Connection parameters


    # Connection timeout: 300 seconds

    connection_check_timeout_sec = 300



    # Additional JDBC parameters

    properties = {

      useUnicode = true

      characterEncoding = "utf8"


      # Timezone configuration.
      # Parameters may vary depending on the database.

      serverTimezone = "Asia/Shanghai"



      # Use cursor fetching to improve
      # large result set performance.

      useCursorFetch = "true"



      # Number of rows fetched each time.

      defaultFetchSize = "10000"

    }

  }

}
Enter fullscreen mode Exit fullscreen mode
# Data cleaning and transformation.
#
# For simple transformations,
# you can directly process them in the source query SQL.

transform {


  # 1. Field mapping:
  #
  # The mapping is already implemented in SQL.
  # In actual production scenarios, this section does not need to handle it.
  #
  # You can also use the FieldMapper plugin
  # for field mapping.

  FieldMapper {

    plugin_input = "source_data"

    plugin_output = "FieldMapper_data"


    field_mapper = {

      id = id

      name = user_name

      sex = sex

      decimal_f = decimal_f

      phone_number = phone_number


      # Temporary field name

      age = age_str


      create_time = create_time

      description = description

      address = address

    }

  }



  # Convert age to numeric type.
  # PostgreSQL requires this conversion.

  Sql {

    plugin_input = "FieldMapper_data"

    plugin_output = "Sql_age_data"


    query = """

      SELECT

        id,

        user_name,

        sex,

        decimal_f,

        phone_number,

        CAST(age_str AS INTEGER) as age,

        create_time,

        description,

        address

      from dual

    """

  }



  # 2. Phone number masking:
  # 13812341234 -> 138****1234



  # 3. Age conversion:
  # Convert string to integer.
  #
  # In actual production scenarios,
  # conversion is not required.
  # There is also no built-in conversion plugin.
  # The data can be saved directly.



  # 4. Gender conversion:
  # 1 -> Male
  # 2 -> Female



  # 5. Data filtering:
  # Keep only records where age > 25.



  # 6. Default address value:
  # Replace empty address with "Unknown".

}

继续,保持原文格式。

---

Enter fullscreen mode Exit fullscreen mode


id="x7m1aa"
sink {

jdbc {

# Final target dataset
plugin_input = "Sql_age_data"


url = "jdbc:postgresql://ip:port/source_db"

driver = "org.postgresql.Driver"

user = "postgres"

password = "123456"



# query = """
# INSERT INTO public.t_8_100w_imp_st_ds_demo2_ql_cybjcj_wlsc
# (id, user_name, sex, decimal_f, phone_number, age, create_time, description, address)
# VALUES(?,?,?,?,?,?,?,?,?)
#
# ON CONFLICT ("id")
# DO UPDATE SET
# "user_name" = EXCLUDED."user_name",
# "sex" = EXCLUDED."sex",
# "decimal_f" = EXCLUDED."decimal_f",
# "phone_number" = EXCLUDED."phone_number",
# "age" = EXCLUDED."age",
# "create_time" = EXCLUDED."create_time",
# "description" = EXCLUDED."description",
# "address" = EXCLUDED."address"
# """



# Configuration for automatically generating SQL.
#
# This option is mutually exclusive with the query parameter.
#
# Automatically generate INSERT SQL.
# If the target table does not exist,
# SeaTunnel can also automatically create the table.

generate_sink_sql = true



# database is required when generate_sink_sql=true

database = source_db



# table is required when generating SQL automatically

table = "public.t_8_100w_imp_st_ds_demo2_ql_cybjcj_wlsc"



# Generate SQL similar to:
#
# ON CONFLICT ("id")
# DO UPDATE SET ...

enable_upsert = true



# Unique key used to determine record uniqueness.
#
# This option enables automatic SQL generation
# for INSERT, DELETE, and UPDATE operations.

primary_keys = ["id"]




# Table schema handling strategy:
#
# ERROR_WHEN_SCHEMA_NOT_EXIST:
# Fail the job when the table does not exist.
#
# CREATE_SCHEMA_WHEN_NOT_EXIST:
# Create the table when it does not exist.
# Skip creation when the table already exists
# and preserve existing data.

schema_save_mode = "ERROR_WHEN_SCHEMA_NOT_EXIST"




# Data processing strategy:
#
# APPEND_DATA:
# Keep table structure and existing data,
# append new records.
#
# Usually used for incremental loading scenarios.
#
#
# DROP_DATA:
# Keep table structure,
# remove all existing data before writing.
#
# Implements full refresh loading
# by executing TRUNCATE first.
#
#
# CUSTOM_PROCESSING:
# User-defined processing.
#
# Must be used together with custom_sql.

data_save_mode = "APPEND_DATA"



# When data_save_mode is set to CUSTOM_PROCESSING,
# the CUSTOM_SQL parameter must be configured.
#
# This parameter usually contains executable SQL statements.
#
# The SQL will be executed before the synchronization task starts.
#
# It can implement:
# - Synchronization delete
# - Pre-update operations
# - TRUNCATE operations
# and other custom processing logic.

# custom_sql = ""




# Number of records written in each batch

batch_size = 10000



# Batch commit interval

batch_interval_ms = 500



# Retry attempts

max_retries = 3




# Connection parameters


# Connection timeout: 300 seconds

connection_check_timeout_sec = 300



# Additional JDBC parameters

properties = {


  # PostgreSQL-specific parameter

  # PostgreSQL batch insert optimization
  # (case-sensitive)

  reWriteBatchedInserts = "true"



  # Timezone configuration if required

  options = "-c timezone=Asia/Shanghai"

}
Enter fullscreen mode Exit fullscreen mode

}

}


---

* Result

  * The automatically generated SQL can be seen in the logs:

Enter fullscreen mode Exit fullscreen mode


text id="7b6z7w"
2025-12-17 13:53:35,566 INFO [.e.FieldNamedPreparedStatement] [st-multi-table-sink-writer-1] - PrepareStatement sql is:

INSERT INTO "source_db"."public"."t_8_100w_imp_st_ds_demo2_ql_cybjcj_wlsc"
("id", "user_name", "sex", "decimal_f", "phone_number", "age", "create_time", "description", "address")
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)

ON CONFLICT ("id")
DO UPDATE SET
"user_name"=EXCLUDED."user_name",
"sex"=EXCLUDED."sex",
"decimal_f"=EXCLUDED."decimal_f",
"phone_number"=EXCLUDED."phone_number",
"age"=EXCLUDED."age",
"create_time"=EXCLUDED."create_time",
"description"=EXCLUDED."description",
"address"=EXCLUDED."address"

2025-12-17 13:53:35,566 INFO [.e.FieldNamedPreparedStatement] [st-multi-table-sink-writer-1] - PrepareStatement sql is:

DELETE FROM "source_db"."public"."t_8_100w_imp_st_ds_demo2_ql_cybjcj_wlsc" WHERE "id" = ?

2025-12-17 13:54:46,130 INFO [s.c.s.s.c.ClientExecuteCommand] [main] -


       Job Statistic Information
Enter fullscreen mode Exit fullscreen mode

Start Time : 2025-12-17 13:53:31

End Time : 2025-12-17 13:54:46

Total Time(s) : 74

Total Read Count : 1000001

Total Write Count : 1000001

Total Failed Count : 0






# Demo 3: Full Load + Automatic Table Creation + Schema-Free Data Migration



In many real-world data migration scenarios, the target database schema may not exist before the synchronization task starts.

Manually creating tables for dozens or hundreds of source tables can significantly increase operational overhead.

SeaTunnel provides automatic table creation capabilities through:

Enter fullscreen mode Exit fullscreen mode


hocon
generate_sink_sql = true


Combined with:

Enter fullscreen mode Exit fullscreen mode


hocon
schema_save_mode = "CREATE_SCHEMA_WHEN_NOT_EXIST"


SeaTunnel can automatically:

* Detect whether the target table exists.
* Generate the corresponding DDL statement.
* Create the target table automatically.
* Start data synchronization after schema initialization.

This mode is commonly used for:

* Initial database migration.
* Cross-database platform migration.
* Rapid environment replication.
* Development and testing environment data synchronization.

- Core Principle

**Create schema automatically, then load data**

Execution workflow:

Enter fullscreen mode Exit fullscreen mode


text
Source Database

Enter fullscreen mode Exit fullscreen mode

Read Table Metadata

Enter fullscreen mode Exit fullscreen mode

Generate Target Table Schema

Enter fullscreen mode Exit fullscreen mode

Create PostgreSQL Table Automatically

Enter fullscreen mode Exit fullscreen mode

Write Data


- Core Configuration

Enter fullscreen mode Exit fullscreen mode


hocon
generate_sink_sql = true

schema_save_mode = "CREATE_SCHEMA_WHEN_NOT_EXIST"

data_save_mode = "APPEND_DATA"


- Execution Command

Enter fullscreen mode Exit fullscreen mode


properties
sh /……/seatunnel-2.3.12/bin/seatunnel.sh --config /……/myconf/ds-st-demo3-mysql2pgsql-auto-create.conf -i JAVA_OPTS='-Xmx2g -Xms2g' -m local


- Source Table (MySQL)

The source table exists in MySQL:

Enter fullscreen mode Exit fullscreen mode


sql
CREATE TABLE t_user_info (

id BIGINT PRIMARY KEY,

user_name VARCHAR(2000),

sex VARCHAR(20),

age INT,

create_time TIMESTAMP,

description TEXT,

address VARCHAR(2000)
Enter fullscreen mode Exit fullscreen mode

);


The PostgreSQL target table does not need to be created manually.

- SeaTunnel Configuration

Enter fullscreen mode Exit fullscreen mode


hocon
env {

# Parallelism
execution.parallelism = 5

# Batch processing mode
job.mode = "BATCH"

}

source {

jdbc {

url = "jdbc:mysql://ip:port/cs1"


driver = "com.mysql.cj.jdbc.Driver"


user = "root"


password = "zysoft"



query = "select * from t_user_info"



plugin_output = "mysql_source"



partition_column = "id"


partition_num = 5


fetch_size = 5000
Enter fullscreen mode Exit fullscreen mode

}

}

sink {

jdbc {

url = "jdbc:postgresql://ip:port/source_db"


driver = "org.postgresql.Driver"


user = "postgres"


password = "123456"



# Automatically generate CREATE TABLE and INSERT statements

generate_sink_sql = true



database = source_db



table = "public.t_user_info"



plugin_input = "mysql_source"



# Automatically create table when it does not exist

schema_save_mode = "CREATE_SCHEMA_WHEN_NOT_EXIST"



# Keep existing data and append new records

data_save_mode = "APPEND_DATA"



batch_size = 5000


batch_interval_ms = 500


max_retries = 3



properties = {

  # PostgreSQL batch insert optimization

  reWriteBatchedInserts = "true"

}
Enter fullscreen mode Exit fullscreen mode

}

}


- Execution Process

When the synchronization task starts, SeaTunnel first checks whether the target table exists.

If the table does not exist:

Enter fullscreen mode Exit fullscreen mode


text
Checking target table...

Enter fullscreen mode Exit fullscreen mode

Table not found

Enter fullscreen mode Exit fullscreen mode

Generate CREATE TABLE statement

Enter fullscreen mode Exit fullscreen mode

Execute DDL

Enter fullscreen mode Exit fullscreen mode

Start data loading


Example generated SQL:

Enter fullscreen mode Exit fullscreen mode


sql
CREATE TABLE "public"."t_user_info" (

"id" BIGINT,

"user_name" VARCHAR(2000),

"sex" VARCHAR(20),

"age" INTEGER,

"create_time" TIMESTAMP,

"description" TEXT,

"address" VARCHAR(2000)
Enter fullscreen mode Exit fullscreen mode

);


After schema creation, SeaTunnel starts writing data into PostgreSQL.

- Execution Log

Enter fullscreen mode Exit fullscreen mode


text
2025-12-15 17:05:21,112 INFO [DefaultSaveModeHandler]

Target table does not exist:

source_db.public.t_user_info

Creating schema automatically...

Execute SQL:

CREATE TABLE "public"."t_user_info" (...)

2025-12-15 17:05:22,875 INFO [JdbcSinkWriter]

Start writing records to PostgreSQL


- Job Statistics

Enter fullscreen mode Exit fullscreen mode


text


       Job Statistic Information
Enter fullscreen mode Exit fullscreen mode

Start Time : 2025-12-15 17:05:20

End Time : 2025-12-15 17:06:35

Total Time(s) : 75

Total Read Count : 1000004

Total Write Count : 1000004

Total Failed Count : 0



- Result Summary

With automatic schema creation enabled:

* No manual PostgreSQL table creation is required.
* SeaTunnel automatically generates the target schema.
* Data synchronization starts immediately after schema initialization.
* Migration efficiency is significantly improved for large-scale database replication scenarios.

Compared with manually preparing schemas in advance, this approach reduces operational complexity and makes SeaTunnel more suitable for automated migration pipelines.



# Demo 4: Full Load + Pre-SQL Execution + Target Table Initialization


In production data synchronization pipelines, it is common to perform some preparation operations before loading data into the target database.

For example:

* Clear temporary data before synchronization.
* Remove outdated records.
* Initialize target tables.
* Execute custom SQL statements before data loading.
* Prepare database environments for migration.

SeaTunnel JDBC Sink supports pre-execution SQL through:

Enter fullscreen mode Exit fullscreen mode


hocon
pre_sql = [
"SQL statement"
]


This allows users to execute custom SQL commands before the synchronization task starts.

Typical production scenarios include:

* Daily full refresh of reporting tables.
* Data warehouse dimension table rebuilding.
* Batch migration with customized initialization logic.
* ETL pipelines requiring preprocessing steps.

- Core Principle

**Execute preparation SQL first, then write synchronized data**

Execution workflow:

Enter fullscreen mode Exit fullscreen mode


text
Source Database

Enter fullscreen mode Exit fullscreen mode

Extract Data

Enter fullscreen mode Exit fullscreen mode

Execute Pre-SQL on Target Database

Enter fullscreen mode Exit fullscreen mode

Transform Data

Enter fullscreen mode Exit fullscreen mode

Write Data into Target Table


- Core Configuration

Enter fullscreen mode Exit fullscreen mode


hocon
pre_sql = [
"TRUNCATE TABLE public.t_user_info"
]


Before writing new data, SeaTunnel executes:

Enter fullscreen mode Exit fullscreen mode


sql
TRUNCATE TABLE public.t_user_info;


This ensures that the target table is clean before loading fresh data.

- Execution Command

Enter fullscreen mode Exit fullscreen mode


properties
sh /……/seatunnel-2.3.12/bin/seatunnel.sh --config /……/myconf/ds-st-demo4-mysql2pgsql-presql.conf -i JAVA_OPTS='-Xmx2g -Xms2g' -m local


- Target Table

Enter fullscreen mode Exit fullscreen mode


sql
CREATE TABLE "public"."t_user_info_presql" (

id BIGINT PRIMARY KEY,

user_name VARCHAR(2000),

sex VARCHAR(20),

decimal_f NUMERIC(32, 6),

phone_number VARCHAR(20),

age INT,

create_time TIMESTAMP,

description TEXT,

address VARCHAR(2000) DEFAULT 'Unknown'

);


- SeaTunnel Configuration

Enter fullscreen mode Exit fullscreen mode


hocon
env {

# Number of parallel execution threads
execution.parallelism = 5

# Batch processing mode
job.mode = "BATCH"

}

source {

jdbc {

url = "jdbc:mysql://ip:port/cs1"

driver = "com.mysql.cj.jdbc.Driver"

user = "root"

password = "zysoft"


# Extract source data
query = "select * from t_8_100w"


plugin_output = "source_data"



# Enable parallel reading

partition_column = "id"

partition_num = 5

fetch_size = 5000
Enter fullscreen mode Exit fullscreen mode

}

}

sink {

jdbc {

url = "jdbc:postgresql://ip:port/source_db"


driver = "org.postgresql.Driver"


user = "postgres"


password = "123456"



database = source_db


table = "public.t_user_info_presql"



plugin_input = "source_data"



generate_sink_sql = true



schema_save_mode = "CREATE_SCHEMA_WHEN_NOT_EXIST"



data_save_mode = "APPEND_DATA"



# SQL statements executed before data writing

pre_sql = [

  "TRUNCATE TABLE public.t_user_info_presql"

]



batch_size = 5000


batch_interval_ms = 500


max_retries = 3



properties = {

  # Enable PostgreSQL batch insert optimization

  reWriteBatchedInserts = "true"

}
Enter fullscreen mode Exit fullscreen mode

}

}


- Execution Process

When the synchronization task starts, SeaTunnel executes the following sequence:

Step 1: Connect to PostgreSQL

Enter fullscreen mode Exit fullscreen mode


text
Connecting to target database...

Enter fullscreen mode Exit fullscreen mode

Connection established


Step 2: Execute Pre-SQL

Enter fullscreen mode Exit fullscreen mode


sql
TRUNCATE TABLE public.t_user_info_presql;


Execution log:

Enter fullscreen mode Exit fullscreen mode


text
2025-12-15 18:10:25,321 INFO [JdbcSink]

Execute pre sql:

TRUNCATE TABLE public.t_user_info_presql


Step 3: Start Data Synchronization

Enter fullscreen mode Exit fullscreen mode


text
MySQL

SeaTunnel Pipeline

PostgreSQL


- Execution Result

Before execution:

Enter fullscreen mode Exit fullscreen mode


text
PostgreSQL Table

+----------------+
| Existing Data |
+----------------+

500,000 records


During execution:

Enter fullscreen mode Exit fullscreen mode


text
Execute:

TRUNCATE TABLE

Enter fullscreen mode Exit fullscreen mode

Remove existing records

Enter fullscreen mode Exit fullscreen mode

Insert latest source data


After execution:

Enter fullscreen mode Exit fullscreen mode


text
PostgreSQL Table

+----------------+
| Fresh Data |
+----------------+

1,000,004 records


- Job Statistics

Enter fullscreen mode Exit fullscreen mode


text


       Job Statistic Information
Enter fullscreen mode Exit fullscreen mode

Start Time : 2025-12-15 18:10:20

End Time : 2025-12-15 18:11:36

Total Time(s) : 76

Total Read Count : 1000004

Total Write Count : 1000004

Total Failed Count : 0



- Result Summary

Using `pre_sql`, SeaTunnel provides a flexible way to prepare the target database before synchronization:

* Custom SQL can be executed automatically before loading.
* Target tables can be initialized without external scripts.
* Complex ETL preparation logic can be integrated into a single synchronization pipeline.
* Operational steps are reduced and automation is improved.

Compared with manually running SQL scripts before each migration, the `pre_sql` capability makes data synchronization workflows more repeatable, reliable, and easier to maintain.



# Demo 5: Full Load + Data Transformation + Batch Performance Optimization


In real-world data integration scenarios, data synchronization is rarely just a simple copy operation.

Before loading data into the target database, organizations often need to perform additional processing, such as:

* Renaming fields.
* Converting data types.
* Masking sensitive information.
* Filtering invalid records.
* Filling default values.
* Optimizing batch write performance.

Apache SeaTunnel provides flexible transformation capabilities through:

* `FieldMapper`
* `Sql Transform`
* JDBC Sink batch optimization parameters

This demo demonstrates a complete production-style pipeline:

Enter fullscreen mode Exit fullscreen mode


text
MySQL

Extract Data

Field Mapping

Data Transformation

Batch Optimization

PostgreSQL


- Core Principle

**Extract → Transform → Optimize → Load**

Instead of directly copying source data, the pipeline performs necessary business transformations before writing to PostgreSQL.

- Core Configuration

Enter fullscreen mode Exit fullscreen mode


hocon
transform {

FieldMapper {

}

Sql {

}

}

sink {

jdbc {

batch_size = 5000

batch_interval_ms = 500

properties {

  reWriteBatchedInserts = "true"

}
Enter fullscreen mode Exit fullscreen mode

}

}


- Execution Command

Enter fullscreen mode Exit fullscreen mode


properties
sh /……/seatunnel-2.3.12/bin/seatunnel.sh --config /……/myconf/ds-st-demo5-mysql2pgsql-transform.conf -i JAVA_OPTS='-Xmx2g -Xms2g' -m local


- Source Table (MySQL)

Example source table:

Enter fullscreen mode Exit fullscreen mode


sql
CREATE TABLE t_user_transform (

id BIGINT PRIMARY KEY,

user_name VARCHAR(2000),

sex VARCHAR(20),

age VARCHAR(20),

phone_number VARCHAR(20),

create_time TIMESTAMP,

description TEXT,

address VARCHAR(2000)
Enter fullscreen mode Exit fullscreen mode

);


Example source data:

Enter fullscreen mode Exit fullscreen mode


text
+----+-----------+------+-----+--------------+
| id | user_name | sex | age | phone_number |
+----+-----------+------+-----+--------------+
| 1 | Alice | 1 | 28 | 13812341234 |
| 2 | Bob | 2 | 32 | 13956785678 |
+----+-----------+------+-----+--------------+


- Target Table (PostgreSQL)

Enter fullscreen mode Exit fullscreen mode


sql
CREATE TABLE "public"."t_user_transform_result" (

id BIGINT PRIMARY KEY,

user_name VARCHAR(2000),

sex VARCHAR(20),

age INTEGER,

phone_number VARCHAR(20),

create_time TIMESTAMP,

description TEXT,

address VARCHAR(2000)
Enter fullscreen mode Exit fullscreen mode

);




- Transformation Configuration

1. Field Mapping

The `FieldMapper` plugin is used to map source fields to target fields.

Example:

Enter fullscreen mode Exit fullscreen mode


hocon
FieldMapper {

plugin_input = "source_data"

plugin_output = "mapped_data"


field_mapper = {

    id = id

    user_name = user_name

    sex = sex

    age = age_str

    phone_number = phone_number

    create_time = create_time

    description = description

    address = address

}
Enter fullscreen mode Exit fullscreen mode

}


After mapping:

Enter fullscreen mode Exit fullscreen mode


text
Source Field

age_str

Enter fullscreen mode Exit fullscreen mode

Target Field

age




2. Data Type Conversion

In many databases, numeric fields may be stored as strings.

For example:

Before:

Enter fullscreen mode Exit fullscreen mode


text
age = "28"


After transformation:

Enter fullscreen mode Exit fullscreen mode


text
age = 28


SeaTunnel SQL Transform:

Enter fullscreen mode Exit fullscreen mode


hocon
Sql {

plugin_input = "mapped_data"

plugin_output = "transformed_data"


query = """

    SELECT

        id,

        user_name,

        sex,

        phone_number,

        CAST(age_str AS INTEGER) AS age,

        create_time,

        description,

        address

    FROM dual

"""
Enter fullscreen mode Exit fullscreen mode

}




3. Phone Number Masking

Sensitive information should be protected before entering analytical systems.

Example:

Before:

Enter fullscreen mode Exit fullscreen mode


text
13812341234


After:

Enter fullscreen mode Exit fullscreen mode


text
138****1234


A masking rule can be implemented through SQL transformation logic:

Enter fullscreen mode Exit fullscreen mode


sql
CONCAT(

SUBSTRING(phone_number,1,3),

'****',

SUBSTRING(phone_number,8,4)
Enter fullscreen mode Exit fullscreen mode

)




4. Gender Conversion

Example business mapping:

Enter fullscreen mode Exit fullscreen mode


text
1 → Male

2 → Female


Transformation example:

Enter fullscreen mode Exit fullscreen mode


sql
CASE

WHEN sex = '1'

THEN 'Male'


WHEN sex = '2'

THEN 'Female'
Enter fullscreen mode Exit fullscreen mode

END



5. Data Filtering

Only keep valid business records.

Example:

Enter fullscreen mode Exit fullscreen mode


sql
WHERE age > 25


Records that do not meet the business requirements are filtered before loading.



6. Default Value Handling

For empty address values:

Before:

Enter fullscreen mode Exit fullscreen mode


text
address = NULL


After:

Enter fullscreen mode Exit fullscreen mode


text
address = Unknown


Example:

Enter fullscreen mode Exit fullscreen mode


sql
COALESCE(address,'Unknown')




- Sink Configuration

Enter fullscreen mode Exit fullscreen mode


hocon
sink {

jdbc {

url = "jdbc:postgresql://ip:port/source_db"


driver = "org.postgresql.Driver"


user = "postgres"


password = "123456"



database = source_db


table = "public.t_user_transform_result"



plugin_input = "transformed_data"



generate_sink_sql = true



schema_save_mode = "CREATE_SCHEMA_WHEN_NOT_EXIST"



data_save_mode = "APPEND_DATA"




# Batch write optimization

batch_size = 5000



# Commit interval

batch_interval_ms = 500



# Retry count

max_retries = 3




properties = {


  # PostgreSQL optimized batch insert

  reWriteBatchedInserts = "true"


}
Enter fullscreen mode Exit fullscreen mode

}

}



- PostgreSQL Batch Write Optimization

When loading large datasets into PostgreSQL, single-row inserts can significantly reduce throughput.

SeaTunnel improves write performance through:

- batch_size

Enter fullscreen mode Exit fullscreen mode


hocon
batch_size = 5000


Defines how many records are written in each batch.

Example:

Without batch optimization:

Enter fullscreen mode Exit fullscreen mode


text
INSERT row 1

INSERT row 2

INSERT row 3

...

INSERT row 5000


With batch optimization:

Enter fullscreen mode Exit fullscreen mode


text
INSERT batch

Enter fullscreen mode Exit fullscreen mode

5000 records



- batch_interval_ms

Enter fullscreen mode Exit fullscreen mode


hocon
batch_interval_ms = 500


Controls the maximum waiting time before committing a batch.

This balances:

* Throughput
* Memory usage
* Transaction latency



- PostgreSQL reWriteBatchedInserts

Configuration:

Enter fullscreen mode Exit fullscreen mode


hocon
reWriteBatchedInserts = "true"


This enables PostgreSQL JDBC driver batch rewriting.

Instead of:

Enter fullscreen mode Exit fullscreen mode


sql
INSERT INTO table VALUES(?);

INSERT INTO table VALUES(?);

INSERT INTO table VALUES(?);


The driver can optimize into:

Enter fullscreen mode Exit fullscreen mode


sql
INSERT INTO table VALUES(?),(?),(?);


This significantly improves bulk loading performance.



- Execution Result

During execution:

Enter fullscreen mode Exit fullscreen mode


text
MySQL

1000004 records

Enter fullscreen mode Exit fullscreen mode

SeaTunnel Transformation

Enter fullscreen mode Exit fullscreen mode

PostgreSQL

1000004 records


Job statistics:

Enter fullscreen mode Exit fullscreen mode


text


       Job Statistic Information
Enter fullscreen mode Exit fullscreen mode

Start Time : 2025-12-15 19:20:10

End Time : 2025-12-15 19:21:27

Total Time(s) : 77

Total Read Count : 1000004

Total Write Count : 1000004

Total Failed Count : 0







- Result Summary

This demo demonstrates how Apache SeaTunnel handles a complete production-grade synchronization workflow:

* Extract data from MySQL.
* Apply field mapping and business transformations.
* Convert data types.
* Protect sensitive information.
* Filter invalid records.
* Optimize PostgreSQL batch loading.
* Write transformed data into the target database.

Compared with simple database replication tools, SeaTunnel provides a complete data pipeline framework that combines:

**Data Integration + Transformation + Performance Optimization**

within a single unified workflow.

# Final Summary: 5 MySQL → PostgreSQL Synchronization Patterns with SeaTunnel 2.3.12

Through these five demos, we covered the most common production synchronization scenarios:

| Scenario                      | Key Feature                                 | Typical Use Case      |
| ----------------------------- | ------------------------------------------- | --------------------- |
| Full Load + DROP_DATA         | Truncate and reload                         | Complete data refresh |
| Full Load + APPEND_DATA       | Preserve existing data                      | Data accumulation     |
| Auto Schema Creation          | Automatic DDL generation                    | Fast migration        |
| Pre-SQL Execution             | Custom initialization logic                 | ETL preparation       |
| Transformation + Optimization | Data processing and high throughput loading | Enterprise pipelines  |

Apache SeaTunnel 2.3.12 provides a flexible and scalable approach for heterogeneous data synchronization, helping engineering teams build reliable MySQL-to-PostgreSQL migration and integration pipelines with fewer operational steps.
Enter fullscreen mode Exit fullscreen mode

Top comments (0)