From SQL Server to PostgreSQL with dbSlice
A practical guide to installation, GUI synchronization, saved jobs, and CLI automation
Lab notes and screenshots: October 8, 2026 · Audience: developers, DBAs, DevOps engineers, and SREs
Moving a table between database engines is only part of the task. You also need to preserve keys, respect relationships, choose how existing rows should be handled, and make the operation repeatable.
This guide follows a sales database from a SQL Server source to a PostgreSQL destination. We start with installation, register the connections, run a synchronization visually, and turn the same operation into a saved job that can run from PowerShell. The worked example uses 245,014 service records from a synthetic database containing 2,250,837 records across 11 tables.
dbSlice is a Windows application for data migration, synchronization, and reconciliation. It supports a graphical workflow and saved jobs that run without opening the main interface. See the official product overview for current capabilities.
The recorded run copied the service table through the Upsert path in 10.6 seconds. That is an observation from this lab, not a performance guarantee or a full-database migration result.
1. Install dbSlice
Option A: Microsoft Store
Open the dbSlice Microsoft Store listing, confirm the publisher is Thinwork Tecnologia da Informação, and choose the installation action shown by the Store. After installation, launch dbSlice from the Windows Start menu.
The product ID is 9PMS69X1T53Z. It is also the identifier used in the WinGet commands below. The official website links to this listing.
Option B: WinGet
Open PowerShell or Windows Terminal and check that WinGet is available:
winget --version
If the command is unavailable, install or update Microsoft's App Installer. WinGet is distributed with App Installer on supported Windows desktop systems. See Microsoft's WinGet documentation.
Inspect the package, then install it from the Microsoft Store source:
winget show --id 9PMS69X1T53Z --source msstore
winget install --id 9PMS69X1T53Z --source msstore
Review any source or package agreements displayed by WinGet. Using the product ID selects the intended package directly. The --source msstore option selects the Store catalog; this guide does not depend on a separate community repository package name. Microsoft documents both identifier selection and source selection in the WinGet install reference.
If a name search does not find the product, use the direct Store link or inspect the product ID above. If WinGet reports a certificate or network error, resolve that connectivity issue before retrying.
Activation and the scope of this walkthrough
The official site describes a feature-limited Store evaluation. To activate a purchased license, open Help → About dbSlice, enter the purchase email and activation code, and select Activate. The current FAQ states that command-line use requires active maintenance. Use an eligible activated installation for the full-volume job in this article; the evaluation should not be assumed to reproduce this workload. Consult the current licensing and activation FAQ.
The supplied application screenshots identify build 2.3.20260913.1655. Website and installation references were checked on October 8, 2026. Labels and availability may differ in later builds.
2. Understand the lab
The logical database is named db_vendas. Its table and column names are in English. MASTER is a connection profile name for the source; it does not mean the SQL Server system database named master.
| Connection profile | Engine | Role | Object naming |
|---|---|---|---|
MASTER |
SQL Server | Populated source | dbo.service |
Server-dbPostg |
PostgreSQL | Main destination in this guide | public.service |
Server-dbMaria |
MariaDB | Additional prepared destination | service in db_vendas |
Server-dbLite |
SQLite | Additional prepared destination | service in a local file |
All profiles belong to the article environment. Actual server addresses, database usernames, and passwords are intentionally absent from the article and example jobs. Enter those values locally when creating connection profiles.
The initial preparation created empty destination schemas. Once synchronization begins, those destinations are no longer empty. The timing logs alone do not establish the exact contents of each target before each run.
Dataset and relationships
| Table | Source rows | Purpose |
|---|---|---|
company |
25 | Companies |
permission |
8 | User permission profiles |
entity_type |
6 | Shared classification types |
category |
120 | Item and service categories |
app_user |
250 | Application users |
person |
20,423 | Customer/person records |
item |
11,172 | Item catalog |
service |
245,014 | Service catalog |
item_service |
402,780 | Item-to-service associations |
sale |
550,023 | Sales headers |
sale_item |
1,021,016 | Sales lines |
| Total | 2,250,837 | 11 related tables |
The model contains 11 primary keys, 14 foreign keys, 12 unique constraints, and 14 explicitly defined secondary indexes. IDs are supplied by the source rather than generated with identity columns. Data is synthetic; example emails use example.invalid.
For the service example, the dependency chain is:
entity_type → category → service
Load entity_type and category before service so that its foreign keys can be satisfied. A table-level synchronization is not a promise that dependent tables will be copied automatically.
For a complete initial load of this model with foreign keys active, use:
company → permission → entity_type → category → app_user → person
→ item → service → item_service → sale → sale_item
This is one valid insertion order, not a claim that every adjacent table is directly related. The schema and seed scripts are provided in the companion sql/ folder. Only SQL Server receives the synthetic seed data; the other scripts create empty structures. SQL Server's seed script requires all demo tables to be empty.
3. Register and test the connections
Use File → New Connection to create each connection profile. Select the article environment, choose the engine, and enter its local connection details.
For the source, use the profile name MASTER, database type SQL Server, and application database db_vendas. For the destination, use Server-dbPostg, type PostgreSQL, and db_vendas. Click Test connection before saving each profile.
For MariaDB, select MariaDB and supply an account that can access the prepared target tables. A database visible to an administrator may still be inaccessible to another account. For SQLite, select SQLite and use the Database / File field to choose the prepared .db file; a server login is not needed for this lab file.
The source account needs read access. The target account needs access appropriate to the selected operation, such as inserting and updating rows for Upsert. Creating missing target tables also requires DDL privileges. This lab uses pre-created schemas.
Figure 1. Source and target connections are selected independently. This initial screenshot still has an empty source-table field; select the table and its key before running.
4. Run the first synchronization in the GUI
Start with the small company table to verify the end-to-end workflow. Then load the dependencies needed by service.
- Open the Synchronize tab.
- In Source, choose
MASTERand click Connect. - Set the source kind to Table, select
company, and selectidas Key column (unique). - In Target, choose
Server-dbPostgand click Connect. - Select
public.companyand target keyid. - Choose Upsert — insert new and update existing.
- Inspect Map columns…. Matching names support the automatic mapping used in this lab.
- Click Run synchronization and inspect the execution log.
The supplied company screenshot reports 25 rows read and 25 in the combined upsert/update counter. Its displayed duration is 0.1 seconds. Use this as a connectivity check rather than a meaningful throughput benchmark.
Configure the larger service run
After loading the required category/type records, use these settings:
| Setting | Value |
|---|---|
| Source connection | MASTER |
| Source table | service (SQL Server dbo schema) |
| Source key | id |
| Target connection | Server-dbPostg |
| Target table | public.service |
| Target key | id |
| Mode | Upsert — insert new and update existing |
| Page (rows) | 100,000 |
| Columns | Match by name; review the mapping |
| WHERE filter | Disabled for the complete table |
| Create table on target if missing | Unchecked: the target already exists |
| Keep source IDENTITY | Unchecked: this model has no identity columns |
With 245,014 source rows and a page size of 100,000, the expected progress is 100,000, 200,000, and 245,014 processed. Page size is an application processing setting; it should not be interpreted as an exact count of individual SQL statements or network requests.
Choose the write behavior deliberately
| Mode | Intended behavior | Suitable use |
|---|---|---|
| Upsert | Insert absent keys and update matching keys; retain target-only rows | Repeatable synchronization |
| Update Only | Update matching keys; do not insert absent keys | Refresh rows already present |
| Insert New | Insert keys missing from the destination | Additions without refreshing existing rows |
| Insert Only | Attempt to insert every selected row | Initial load into an empty compatible target |
| Force Load | Clear and reload the target | Explicit replacement of a disposable target |
This walkthrough uses Upsert and Update Only. Upsert does not delete target-only rows, so it does not by itself establish an exact replica. Update Only will not populate an empty target. Force Load changes the operation to replacement and may be incompatible with referencing foreign keys; it is not used here.
5. Read the GUI results correctly
The supplied Upsert log records:
[09:07:34.95] Starting: MASTER.service -> Server-dbPostg.public.service
[Upsert — insert new and update existing]
[09:07:35.06] Source total: 245,014 rows
[09:07:39.49] Page 1: 100,000/245,014 processed
[09:07:43.74] Page 2: 200,000/245,014 processed
[09:07:45.56] Page 3: 245,014/245,014 processed
[09:07:45.57] Done: 245,014 read, 0 inserted, 245,014 upsert/update in 10.6s
read 0.5s / transform 0.2s / write 10.2s
The subsequent Update Only run records:
[09:07:51.81] Starting: MASTER.service -> Server-dbPostg.public.service
[Update existing only (UPDATE)]
[09:07:51.88] Source total: 245,014 rows
[09:08:38.85] Page 1: 100,000/245,014 processed
[09:09:24.73] Page 2: 200,000/245,014 processed
[09:09:44.34] Page 3: 245,014/245,014 processed
[09:09:44.36] Done: 245,014 read, 0 inserted, 245,014 upsert/update in 112.5s
read 0.6s / transform 0.4s / write 111.9s
Excerpts preserve the reported measurements; repeated completion lines are omitted and long lines are wrapped for readability.
Figure 2. The completed Update Only run processed 245,014 rows. The log also contains the earlier Upsert execution.
The combined counter matters. In the implementation reviewed for this article, Upsert writes are accumulated in the upsert/update counter. They are not split into independently measured inserts and updates. Consequently, 0 inserted in an Upsert summary does not prove that no new rows were inserted, and 245,014 upsert/update does not prove that 245,014 previously existing rows changed value. Treat this as operational progress and validate the destination separately.
The stage timers are also different from elapsed wall-clock time. Reading can overlap processing/writing, and reported values are rounded. Do not sum read, transform, and write to reconstruct the total duration.
6. Save the operation as a job
Open Tools → CLI jobs and choose New. Configure:
| Job field | Value |
|---|---|
| Name | Transf_PostgreSQL_dbstoreSite |
| Type | Sync — source to target, choose the mode |
| Sync mode | Upsert |
| Source connection / table / key | MASTER / service / id |
| Target connection / table / key | Server-dbPostg / public.service / id |
| Page (rows) | 10,000 |
| WHERE filter | Disabled |
Select Save and use Run now to execute the saved job from the editor. This is distinct from launching the standalone CLI in a terminal.
The supplied job-editor log shows 25 pages: 24 pages of 10,000 rows, followed by 5,014 rows. Its conclusion is:
[09:18:33] --- Transf_PostgreSQL_dbstoreSite (Sync · Upsert) ---
[09:18:33] source : MASTER [SQL Server] · service · key: id
[09:18:33] target: Server-dbPostg [PostgreSQL] · public.service · mode: Upsert
[09:18:33] impact: writes to the target (INSERT + UPDATE)
...
[09:18:44] Page 25: 245,014/245,014 processed
[09:18:44] OK Done: 245,014 read, 0 inserted, 245,014 upsert/update in 10.7s
read 0.8s / transform 0.3s / write 10.2s
[09:18:44] Sync OK | MASTER -> Server-dbPostg | 245,014 Rows | 10.7s
Click Copy CLI command to obtain the command for the saved file. A job references stored connection profiles by environment and name; no database password is needed in the job JSON.
The companion file service-upsert.job.json expresses the same configuration. Its field names match the current dbSlice file format, including nome, tipo, origem, and destino. These are API/schema identifiers and must not be translated even though the article and interface are in English.
{
"nome": "Transf_PostgreSQL_dbstoreSite",
"tipo": "Sync",
"mode": "Upsert",
"origem": {
"environment": "article",
"db": "MASTER",
"type": "TABLE",
"object": "service",
"key": "id"
},
"destino": {
"environment": "article",
"db": "Server-dbPostg",
"type": "TABLE",
"object": "public.service",
"key": "id"
},
"pageSize": 10000,
"createTargetIfMissing": false,
"ofuscar": false,
"alertemail": false
}
The names must match profiles saved under the Windows user running the job. The source object is service as shown in the screenshots; use the schema-qualified name if required by your database user's default schema.
7. Run the job from PowerShell
The command-line format is positional:
dbSlice <job.json> [job.log]
The optional second argument writes a timestamped log. That file is recreated for each run, so give each execution a distinct log filename if you need history.
First locate the executable. If dbSlice.exe is already available through your command path, use that. For a Store installation, the following resolves the installed application package for the current Windows user without hard-coding its versioned installation directory:
$command = Get-Command dbSlice.exe -ErrorAction SilentlyContinue
if ($command) {
$dbSliceExe = $command.Source
} else {
$package = Get-AppxPackage -Name '*dbslice*' |
Where-Object { Test-Path (Join-Path $_.InstallLocation 'dbSlice.exe') } |
Select-Object -First 1
if (-not $package) {
throw 'Locate your installed dbSlice.exe and set $dbSliceExe to its full path.'
}
$dbSliceExe = Join-Path $package.InstallLocation 'dbSlice.exe'
}
Do not assume that installation automatically adds a shell alias. You can also set $dbSliceExe explicitly to your installed executable. Check --help and --version when verifying a different build.
From this article's folder, run the companion wrapper:
.\examples\Invoke-ServiceSync.ps1 -DbSliceExe $dbSliceExe
The wrapper waits for the GUI-subsystem executable to finish, creates a separate log for each run, and returns its exit code. It invokes the existing CLI; it does not create connections, grant permissions, or populate prerequisite tables.
To run a job saved by the graphical editor instead, pass its path explicitly:
$savedJob = Join-Path $env:APPDATA 'dbSlice\jobs\Transf_PostgreSQL_dbstoreSite_job.json'
.\examples\Invoke-ServiceSync.ps1 -DbSliceExe $dbSliceExe -JobPath $savedJob
In the supplied terminal screenshot, the standalone CLI processes the same 245,014 rows in 13.8 seconds. Its stage report is read 0.8s / transform 0.2s / write 9.9s. This is a separate execution from the 10.7-second run in the job editor.
| Exit code | Meaning in the reviewed CLI |
|---|---|
0 |
Job completed successfully |
1 |
Job execution failed |
2 |
Arguments, job-file loading, or license eligibility prevented execution |
For scheduling, run under a Windows account with access to the application's saved profiles and the appropriate license. A copied JSON file alone does not transfer its referenced credentials. Set the task's working directory and use absolute executable/job paths. The example wrapper has a -LogDirectory parameter for operational log storage.
8. Compare the observed executions
All four measurements below refer to service, with 245,014 rows read from SQL Server and PostgreSQL as the destination.
| Execution | Mode | Page size | Pages | Elapsed | Approx. rows/s |
|---|---|---|---|---|---|
| Synchronize tab | Upsert | 100,000 | 3 | 10.6 s | 23,115 |
| Synchronize tab | Update Only | 100,000 | 3 | 112.5 s | 2,178 |
| CLI jobs editor, Run now | Upsert | 10,000 | 25 | 10.7 s | 22,899 |
| Standalone terminal run | Upsert | 10,000 | 25 | 8.9 s | 26,213 |
Throughput is calculated as 245,014 / reported elapsed seconds and rounded to the nearest whole row. The inputs are already rounded durations.
Figure 3. Observed elapsed durations for this lab. These are individual executions, not averages from a controlled benchmark.
The GUI Update Only run took approximately 10.6 times as long as the GUI Upsert run. This does not mean Upsert is universally faster, or that the two modes are interchangeable. They have different write semantics. Hardware, database versions, network latency, server load, cache state, and target contents were not captured as a controlled benchmark specification. The two page sizes were also not tested through repeated, controlled trials.
The result supports a narrower conclusion: the supplied run completed a 245,014-row Upsert operation in 10.6 seconds, and the saved-job workflow successfully repeated the operation from both the job editor and the terminal.
9. Validate the destination
A successful log is the starting point for verification. Run the following queries in the appropriate database clients after a full-table service synchronization.
SQL Server source:
SELECT COUNT_BIG(*) AS row_count, MIN(id) AS first_id, MAX(id) AS last_id
FROM dbo.service;
PostgreSQL destination:
SELECT COUNT(*) AS row_count, MIN(id) AS first_id, MAX(id) AS last_id
FROM public.service;
SELECT COUNT(*) AS orphan_services
FROM public.service AS s
LEFT JOIN public.category AS c ON c.id = s.category_id
WHERE c.id IS NULL;
For the unfiltered synthetic service dataset with no target-only records, expect 245,014 rows, IDs from 1 to 245,014, and zero orphan services. Count/min/max checks are useful but do not prove that all values match.
Use dbSlice's Compare functionality to examine key differences, and enable Also compare column values when validating content. Inspect the configured column mappings and review any differences. Remember that an Upsert run intentionally retains target-only rows.
To demonstrate a data slice later, enable the source WHERE filter and use an expression such as category_id = 1. Include the required parent records in the destination and validate the filtered set, rather than expecting the full-table row count. Document each table included in the slice.
10. Extend the workflow to MariaDB and SQLite
Use the prepared MariaDB or SQLite connection as the target, choose the matching destination table and id key, and review the column mapping. Keep the same dependency order when loading an empty mirror.
The MariaDB and SQLite structures are ready for these exercises, but the supplied performance evidence covers SQL Server → PostgreSQL only. No timings for the other engines are claimed here.
For SQLite, foreign-key enforcement is a connection setting. Ensure the connection used for copying and validation enables it. SQLite's numeric affinity also differs from the fixed-precision decimal types in the server databases; validate monetary values to the precision required by your scenario.
Troubleshooting the walkthrough
| Symptom | Check |
|---|---|
| A required field is highlighted | Select both table names and both unique keys before running |
| PostgreSQL rejects a foreign key | Load the referenced type/category records first |
| MariaDB reports access denied for the database | Verify privileges for the actual connecting account and host |
| CLI cannot find a profile | Match both environment and db, and run under the expected Windows account |
dbSlice is not recognized in the terminal |
Resolve or provide the installed executable's full path |
| Job stops before synchronization | Inspect the log for job-file, profile, or license eligibility errors |
| Update Only leaves an empty target empty | Choose a mode that inserts rows when an initial load is intended |
| Counts match but data may differ | Compare keys and column values; counts alone are insufficient |
Companion files and references
- Example Upsert job: no connection strings or passwords.
- PowerShell runner: waits for completion and preserves the CLI exit code.
sql/: schema scripts for all four engines and the SQL Server synthetic seed script.- Official dbSlice website: product capabilities and installation entry point.
- dbSlice FAQ: activation and current licensing eligibility.
- Microsoft Store listing: product
9PMS69X1T53Z. - Microsoft WinGet documentation: WinGet and App Installer.
- WinGet install reference: package ID and source options.
The numerical results and interface illustrations come from the supplied October 8 lab logs and screenshots. CLI syntax, job fields, and counter interpretation were cross-checked against the local product implementation. The article does not contain live credentials, personal profile paths, or private server addresses.