Herd of elephants crossing an asphalt road, seen from behind, like a Protheus database switching database engines

SQL Server 2016 left extended support on July 14, 2026. 2017 leaves in October 2027, and TOTVS already flags it as EOL on its list of certified databases. Anyone running Protheus on either of those two versions will have to touch the database one way or another.

And that is when the question shows up in the meeting: since we have to touch it anyway, why not move to PostgreSQL and stop paying for licenses?

I get this question often. The honest answer is that it depends on things almost nobody checks before asking. Here I will lay out what TOTVS actually certifies, in their words and not mine, what it costs to stay, what changes for the DBA and for the developer, with tests run on a lab PostgreSQL, and how the migration really happens with the official tool. No rooting for either database. I have seen Protheus fly on both and crawl on both.

What TOTVS certifies today

Everything in this section I took from the DBAccess certified databases page and the Protheus certified databases page on TDN, consulted on October 15, 2026.

CERTIFIED DATABASES · DBACCESS What runs Protheus today, according to TOTVS PostgreSQL 15.2DBAccess 22.1.1.0+ 16DBAccess 24.1.0.0+ 18DBAccess 24.1.1.0+ 17not on the list WIN1252 · collation C · ODBC30=1 procedures supported · Linux in production Microsoft SQL Server 2016not even on the list 2017EOL 2019 · 2022 · 2025SUPPORTED Expressnever certified collation Latin1_General_BIN TOTVS recommends the Enterprise edition Oracle Database 19.3SUPPORTED 21.3SUPPORTED 26aiSUPPORTED Expressnever certified collation WE8MSWIN1252 no table partitioning Transcribed from the DBAccess and Protheus certified databases pages on TDN, on 10/15/2026. New minor is supported, new major requires new certification. DB2, Informix and OpenEdge: support ended.

So no, you do not lose the average cost procedures by moving to Postgres. That is a myth that still goes around, and TOTVS writes the opposite in plain letters on the Protheus page: “Stored Procedures are supported for the PostgreSQL database starting with Protheus version 12”.

There is a green box on the DBAccess page worth reading twice. Keep it in mind for the cost section.

TOTVS TDN notice: Choose Enterprise editions
DBAccess “Database” page on TOTVS TDN. Screenshot from 10/15/2026.

And there is the version policy, which a lot of people ignore. Certification covers the tested major version and the following minors. A new major requires a new certification. PostgreSQL 17 is not on the list, so there is no “upgrade to 17 because it came out”. You go from 16 to 18, when your DBAccess allows it.

Database certification policy on TOTVS TDN: a new minor version is supported, a new major version requires new certification
Certification policy, on the same TDN page.

For PostgreSQL, TOTVS requires three things when creating the database and one in DBAccess. In their words:

Encoding: WIN1252. Collation: C. Character type: *.CP1252 and/or *.1252.

DBAccess.ini: In the DBAccess configuration file, set the key ODBC30=1. The use of ODBC 1.0 functions from the Postgres ODBC is kept only for compatibility, and using them can cause instability and DBAccess crashes.

postgresql12-contrib package: this package is essential for identifier fields.

TOTVS, TDN: Certified Databases (Microsiga Protheus Line)

I tested it on a clean PostgreSQL 16, because there is a catch right on the first line: the database cannot be created from the default template, which is UTF8. Without template0, this is the error:

psql: CREATE DATABASE with WIN1252 fails without template0, works with template0, and the uuid-ossp extension generates the uuid
Lab PostgreSQL 16. The uuid-ossp extension ships in the contrib package and provides the uuid_generate_v4 that DBAccess uses for identifier fields.
-- o banco do Protheus precisa nascer assim (não dá pra mudar depois)
CREATE DATABASE tpprd
  WITH TEMPLATE = template0
       ENCODING = 'WIN1252'
       LC_COLLATE = 'C'
       LC_CTYPE   = 'C';

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
; dbaccess.ini
[General]
ODBC30=1

[POSTGRES]
environments=tpprd

[POSTGRES/tpprd]
user=tpprd
password=...
TableSpace=tpprd_data
IndexSpace=tpprd_data

The ODBC30=1 goes in the General section, not the database section. And if DBAccess runs on Windows, skip version 16 of the Postgres ODBC driver. The DBAccess page says it has a known bug and points you to 17.00.0003 or later.

One more, on the migration tips page. If your team only knows Windows, this is the first hidden cost of the conversation:

Excerpt from TOTVS TDN: we do not recommend using PostgreSQL in production on Windows
“DBTools: Migration tips” page, TOTVS TDN.

What it costs to stay on SQL Server

Let’s do the math with Microsoft list prices for SQL Server 2025, per-core licensing, which is what you use with Protheus. 2-core pack, Standard: US$ 3,945. 2-core pack, Enterprise: US$ 15,123. Minimum of 4 cores per server. An 8-core database server, which is a common size for a mid-sized Protheus:

LICENSING · 8-CORE SERVER List price, in US dollars, per server SQL Server 2025 Standard 4 packs of 2 cores × 3,945 15,780 perpetual, without Software Assurance or 5,672/year on subscription no full Always On, 128 GB buffer pool SQL Server 2025 Enterprise 4 packs of 2 cores × 15,123 60,492 perpetual, without Software Assurance or 21,736/year on subscription the edition TOTVS recommends PostgreSQL 16 / 18 database license 0 the cost moves elsewhere: migration project, people who operate Linux and Postgres, backup, HA, support and it is not recurring like the license Multiply by production + QA + development + DR replica. On Postgres, the fourth environment costs the same as the first. Source: Microsoft, SQL Server 2025 Pricing Datasheet (open NL). CAL, Windows Server and Software Assurance are left out of the math.

Look at the second box. If you follow the TOTVS recommendation and go Enterprise, an 8-core server is sixty thousand dollars at list price. If your QA environment runs the same engine, double it. On subscription, that is twenty-one thousand a year for production alone, forever.

Plenty of people run Standard and live fine. But then there is no full Always On, no TDE before 2019, and there is a memory cap per instance. Each of those “no”s becomes an architecture decision you make to save on licensing, and that is a cost too, it just does not show up on the invoice.

PostgreSQL costs zero in licensing. It does not cost zero. The cost moves somewhere else: a migration project with three rounds, people who know how to operate Postgres on Linux, a backup tool, a high availability architecture, and, if you want someone to call in the middle of the night, a support contract.

What changes for the DBA

This is the part I most want you to read, because it is where the migration goes wrong six months after it went right.

SQL Server updates the row in place. PostgreSQL does not. Every UPDATE in Postgres writes a new version of the row and leaves the old one behind, marked dead, until VACUUM comes by and frees the space. That is how its MVCC works, and it is why readers do not block writers.

Now think about Protheus. A delete in Protheus is an UPDATE on D_E_L_E_T_. SB2 gets hammered with updates all night long during the cost recalculation. The accounting balance tables take inserts, updates and deletes at every closing. All of that becomes dead rows in Postgres.

THE SAME UPDATE ON SB2 Where the row ends up in each database SQL Server SB2010 B2_COD = MP00001 B2_QATU = 120 the row, in its place on the page UPDATE B2_QATU = 115 SB2010 B2_COD = MP00001 B2_QATU = 115 same row, overwritten, one more record in the log the table does not grow with updates the bill goes to the transaction log PostgreSQL sb2010 b2_cod = MP00001 b2_qatu = 120 version 1 of the row UPDATE b2_qatu = 115 sb2010 b2_cod = MP00001 b2_qatu = 120 version 1 is still there, dead, taking up space sb2010 b2_cod = MP00001 b2_qatu = 115 version 2, written somewhere else table and index grow with every update until autovacuum comes by and collects the dead ones Delete in Protheus is an UPDATE on D_E_L_E_T_. Cost recalculation updates SB2 all night. The database does not break, it gets slow.

This is not theory. I created an sb2010 shaped like the Protheus one (CHAR fields, D_E_L_E_T_, R_E_C_N_O_, composite index), 300 thousand rows, and ran five passes of UPDATE on the balance, which is what a recalculation does. Look at the size before and after:

psql: table sb2010 with 300 thousand rows grows from 52 MB to 224 MB and 1.5 million dead rows after five UPDATE passes
From 52 MB to 224 MB without inserting a single row. One and a half million dead rows, and autovacuum (turned off on purpose on this table, to show the effect) never came by.

Then run VACUUM. It collects the dead rows, frees the space for reuse inside the table, and the table stays at 224 MB. A regular VACUUM does not return space to the operating system. This matters for anyone coming from SQL Server and expecting a shrink:

psql: VACUUM VERBOSE on sb2010 removes the dead rows, but the table stays at 224 MB
“tuples: 300526 removed”, 83% of the table’s pages had dead rows, and the size did not change.

Autovacuum exists to take care of this, and with factory defaults it is lazy on large tables. The trigger is 20% of the table in dead rows. On an SD3 with 100 million rows, it waits for 20 million dead rows before starting. On an SB2 that gets updated in a loop, that is far too late. First thing I look at on a Protheus running on Postgres:

-- quem está inchando e quando o autovacuum passou pela última vez
SELECT relname,
       n_live_tup,
       n_dead_tup,
       ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 1) AS pct_mortas,
       last_autovacuum,
       last_autoanalyze
  FROM pg_stat_user_tables
 WHERE relname IN ('sb2010', 'sb9010', 'sd3010', 'sd1010', 'cq1010', 'se2010', 'se1010')
 ORDER BY n_dead_tup DESC;

And the tuning is per table, not global. The tables Protheus hammers get their own trigger. The fillfactor leaves spare room on the page so the new version lands on the same page as the old one, which is what Postgres calls a HOT update and spares the index:

psql: ALTER TABLE sb2010 with autovacuum_vacuum_scale_factor 0.01 and fillfactor 80, while the global default stays at 0.2
The global parameter stays at 0.2. SB2 now fires at 1% dead rows.
ALTER TABLE sb2010 SET (
  autovacuum_vacuum_scale_factor  = 0.01,
  autovacuum_analyze_scale_factor = 0.02,
  fillfactor = 80
);
ALTER TABLE sd3010 SET (autovacuum_vacuum_scale_factor = 0.02);
ALTER TABLE cq1010 SET (autovacuum_vacuum_scale_factor = 0.01);

A note on TOTVS’s own maintenance page, which shows a vacuumdb -z -f -v on databases called tpprd. The -f is VACUUM FULL. That one does return space, because it rewrites the whole table, but it holds an exclusive lock the entire time. It is fine for a weekend window after a large purge. It is not for routine use, and not with users logged in.

Second thing: connections. In Postgres every connection is an operating system process, with its own memory. DBAccess opens one connection per Protheus thread. The dbaccess.ini example TOTVS publishes on the migration page has ThreadMax=10000. Ten thousand Postgres processes do not exist. So max_connections and work_mem have to be sized together, because work_mem is per sort operation, per connection, and the math blows up fast.

-- quantas conexões o DBAccess está segurando agora, e em que estado
SELECT state, COUNT(*) AS conexoes,
       ROUND(MAX(EXTRACT(EPOCH FROM (now() - state_change))) / 60) AS parada_ha_min
  FROM pg_stat_activity
 WHERE backend_type = 'client backend'
 GROUP BY state
 ORDER BY conexoes DESC;

And do not count on a transaction-mode pooler (PgBouncer and the like) to solve this. DBAccess relies on session state, uses temporary tables, and transaction-mode pooling breaks exactly that. Session mode does work, but then it saves no processes at all. The solution is to size ThreadMax for the real number of concurrent users and give the server memory.

Third: size. Many Protheus databases on SQL Server use data compression, TOTVS even has a page about it, and their migration example started from 600 GB compressed. PostgreSQL has no row compression. A CHAR field padded with spaces, which is everything in Protheus, will take up its declared size. Expect the database to grow at the destination. By how much, only the test round will tell. I have no formula and I distrust anyone who has one.

Fourth: backup and high availability. There is no one-line “backup database to disk”. There is physical backup with WAL archiving, and the tool is pgBackRest or Barman (TOTVS mentions Barman in its documentation). Always On becomes streaming replication, and failover is orchestrated by Patroni, repmgr or your cloud operator. All of this is mature. All of this is different. If your disaster runbook is a .bak in a folder, it will have to be rewritten from scratch.

Fifth, and this one touches the LGPD, Brazil’s data protection law: there is no TDE in community PostgreSQL. TOTVS even warns about it on the certification page:

With the start of the General Data Protection Law (LGPD), if the database needs to be encrypted, we recommend contacting the vendor of the chosen database to check whether the version to be purchased includes native data encryption.

TOTVS, TDN: DBAccess, Database page

In Postgres the path is to encrypt the disk (LUKS, or the cloud storage) or a commercial distribution that ships TDE. On SQL Server I have already shown how to enable TDE, and it is an afternoon of work. If encryption at rest is an audit requirement at your company, settle it in the design, before migrating.

What changes for the developer

TOTVS standard code already ships with queries adjusted for the three databases. The migration page itself says so, and in the same paragraph tells you to recompile the procedures afterwards:

Excerpt from TOTVS TDN on the acceptance testing phase: compile the procedures again, since the migration results in a new database
Phase 2 (acceptance testing) on the “DBTools: Migration tips” page, TOTVS TDN.

The problem is never the standard code. It is the customization. Ten years of entry points and custom routines written by people who only ever knew SQL Server leave T-SQL scattered across the source. WITH (NOLOCK) is the most common, so much so that TOTVS calls it out by name:

TOTVS TDN notice about the With (NOLOCK) clause in SQL Server customizations
Same TDN page. In Postgres readers already do not block writers, so NOLOCK makes no sense, and the syntax simply breaks.

But there is more: GETDATE(), ISNULL(), CONVERT(), TOP 10, DATEADD(), concatenation with +, column names in square brackets, temporary tables with #. Before any application test, I run a dumb scan of the source. Dumb and effective:

# no diretório dos fontes customizados (.prw, .prx, .tlpp)
grep -ril --include='*.prw' --include='*.prx' --include='*.tlpp' \
  -e 'nolock' -e 'getdate' -e 'isnull(' -e 'convert(' -e 'dateadd(' \
  -e ' top ' -e 'datediff(' -e 'charindex(' -e 'with (' . | sort > suspeitos.txt

wc -l suspeitos.txt

That number is the size of your project on the development side. It may be ten files, it may be four hundred. It is what decides whether the migration takes two months or a year, not the size of the database.

Two catches that do not show up in the grep and that bite. I tested both in the lab.

Trailing spaces. Protheus pads every character field with spaces up to its declared length. On SQL Server, concatenating two CHARs with + carries the spaces along. In Postgres, || converts the CHAR to text first, and the conversion drops the trailing spaces. The query does not fail. It returns something else:

psql: concatenating CHAR in PostgreSQL drops the trailing spaces; length returns 7 and octet_length returns 15
B2_COD is CHAR(15) and stores 15 bytes, but || delivers “MP0000101” squeezed together and LENGTH says 7. With RPAD the key is back to the length Protheus expects.

Names in double quotes. In Postgres, an unquoted identifier is folded to lowercase. Everything Protheus sends unquoted works, because the database folds to lowercase on both sides. Now a query someone wrote with "SB2010" in quotes works on SQL Server and breaks on Postgres:

psql: SELECT on SB2010 in double quotes fails with relation does not exist; without quotes it works
Same table, same session. With quotes, “relation does not exist”. Without quotes, 300 thousand rows.

On sorting and case, good news: Latin1_General_BIN on SQL Server and collation C on Postgres are both binary orderings. ORDER BY comes out the same, LIKE is case-sensitive the same way, the index behaves the same. That is exactly why TOTVS requires collation C. Whoever tries to be clever and creates the database with pt_BR.UTF-8 to “sort nicely” finds Protheus behaving differently on lookup screens, and by then it cannot be changed without recreating the database.

How the migration really happens

The tool is from TOTVS, it is called DBTools, and it lives inside the DBAccess installation folder. It does not talk to any database directly: it reads the source through one DBAccess and writes to the destination through another DBAccess. It is a copy, table by table. It is not replication, it is not CDC, there is no “sync the delta later”. The diagram is TOTVS’s own:

TOTVS diagram: migration VM with DBAccess SQL Server, DBAccess PostgreSQL and DBTools between the source SQL Server and the destination PostgreSQL
Architecture used by TOTVS in the documented migration: one Windows VM with two DBAccess instances (one per database) and DBTools. Source: TDN, “DBTools: Migration tips”.

Three rules of the migrator that change your planning, in their words:

TOTVS TDN note: only tables containing the DBAccess control fields will be migrated
A table without R_E_C_N_O_ and D_E_L_E_T_ does not go. An integration table created by hand outside the dictionary gets left behind.

The source DBMS must not be accessed or updated while the data is being read for the copy.

TOTVS, TDN: DBTools, Environment Migrator (requirements)
TOTVS TDN notice: there is no data comparison, only the table row count
On resuming an interrupted copy. It counts rows, it does not check content.

This is the parameters screen. It is text mode, you navigate by typing the option number:

TOTVS DBTools screen: Migrator, Set Migration Parameters, with the default values
DBTools 22.1.1.2, copy parameters with the default values. TOTVS screenshot from TDN.

In production I run it in batch mode, with a file. This is my starting point, adapted from the example TOTVS published:

; dbtools.ini
[general]
threads=0          ; deixa ele escalar sozinho; depois da rodada 1 você fixa
overwrite=0
resume=0
copydeleted=1      ; leva os registros com D_E_L_E_T_ = '*' (expurga ANTES, não durante)
copyempty=1
createindex=1
errorstop=0        ; não para na primeira tabela com erro; o log diz quais falharam
keeprecno=1        ; mantém o R_E_C_N_O_ de origem nas tabelas sem numeração automática
copymask=%
ignoremask=%BKP,%LOG

[sourcedb]
dbserver=10.0.0.10
dbport=7898
dbdatabase=MSSQL
dbalias=TPPRDSQL
dbuser=
dbpsw=

[targetdb]
dbserver=10.0.0.10
dbport=7899
dbdatabase=POSTGRES
dbalias=TPPRD
dbuser=
dbpsw=
C:\totvs\dbaccess\dbtools>dbtools.exe /batch /log migracao-rodada1.log

Two parameters deserve a minute. copydeleted=1 carries the garbage along. If the database has 30% dead records, you will copy 30% garbage and build indexes on top of it. The right path is to purge at the source, the right way, with the module and the dictionary, weeks before round 1. Migration is not the time to clean up a database. And keeprecno=1: by default it comes off and TOTVS says it is not necessary. I turn it on, because there are integrations out there that stored R_E_C_N_O_ as if it were a key, even with TOTVS saying, on the UUID fields page, that “the R_E_C_N_O_ value must not be used as a foreign key”.

Running, this is the screen. Note the “Threads”: with threads=0 it starts one process, measures for 90 seconds, starts another, and only stops adding when the next one does not increase throughput, which is usually when the network or the destination disk saturates:

TOTVS DBTools screen during the copy: table CDYT10, 258 thousand records, 82 MB of 1.43 GB, 6% complete
Copy in progress, largest table first, reading in R_E_C_N_O_ order. TOTVS screenshot from TDN.
Two TOTVS DBMonitor windows side by side during the migration, DBAccess on port 7898 and on 7899
The two migration DBAccess instances seen through DBMonitor: 7898 reading SQL Server, 7899 writing to PostgreSQL. TOTVS screenshot from TDN.

On the Postgres side, the initial load is the only time I touch durability parameters. Put everything back before opening to users:

# postgresql.conf, SÓ durante a carga do DBTools
maintenance_work_mem = 2GB          # criação de índice depois de cada tabela
max_wal_size = 16GB                 # menos checkpoint no meio do insert em massa
checkpoint_timeout = 30min
synchronous_commit = off            # commit não espera o fsync do WAL
autovacuum = off                    # ele só atrapalha enquanto a tabela está nascendo
wal_level = minimal                 # exige max_wal_senders = 0; sem réplica durante a carga
max_wal_senders = 0

# depois da carga: volta tudo, reinicia, e roda
# VACUUM (ANALYZE, VERBOSE);   -- estatística do banco inteiro antes do primeiro teste

How long does it take? TOTVS published their lab: “the expected time was around 5h38min to migrate around 600GB”, with 8-core machines, a 4 Gbit network and disks between 800 and 2,000 IOPS. Your numbers will be different. Round 1 exists to find out what they are. And TOTVS tells you to measure the network with iperf beforehand, because the copy bottleneck is usually the network or destination IOPS, not CPU.

That time is your production window, because the source has to stay down from start to finish. If the window does not fit in a weekend, the options are to slim down the database first (purge, backup tables nobody uses, five years of integration logs) or accept a longer outage. What does not exist is “copy during the week and sync the rest at cutover”. Not with this tool.

And how many times? TOTVS answers:

Excerpt from TOTVS TDN: the recommended minimum is 3 migration rounds
“DBTools: Migration tips” page, TOTVS TDN.
THE THREE ROUNDS 1 Test measures the real copy time, catches data errors, tunes parameters, finds the size at the destination 2 Acceptance business tests module by module, procedures recompiled, no patches on the ERP or the database 3 Production source down from start to finish, window calculated in round 1, ANALYZE before releasing A minimum of three is the TOTVS recommendation. Do not do it with fewer.

The phase 2 warning is the one I most often see ignored. Someone applies a fix patch in the middle of acceptance testing, a table structure changes, and the production round migrates a different database from the one that was tested:

TOTVS TDN notice: during acceptance testing do not make changes to the ERP or the database, including patches
“DBTools: Migration tips” page, TOTVS TDN.

When I would not migrate

If the only reason is the end of support for 2016 or 2017. Running from a deadline is the worst reason to switch DBMS. In that case the short path is to upgrade to SQL Server 2022 or 2025, which is a version upgrade, and decide on PostgreSQL calmly, next year, with a test round done without pressure.

If the infrastructure team is 100% Windows and there is nobody to operate Linux. TOTVS does not recommend Postgres on Windows for production, and neither do I. You would be trading licensing for dependency.

If half the world reads the database directly. Power BI pointed at SB2, a linked server from another system, an SSIS package, an integration that runs SELECT on SA1 in the middle of the night. Each of those is a parallel project nobody budgeted. Take the inventory before promising a date.

If suspeitos.txt came back with hundreds of files and there is no developer to review them. The database migrates in a weekend. The customization does not.

Now, if you have four environments on the same engine, the license renewal is on the table, the team already operates Linux for other things, and the grep came back small, then the math closes fast. And it closes again every year.

When to call a specialist

If your SQL Server renewal is coming up, or your 2016 is already out of support, the right step is not to pick a database. It is to take the inventory: DBAccess version, a scan of the customizations, who reads the database from outside, the real size of the database without the garbage, and a round 1 copy to measure the window. With that in hand the decision becomes math, not a bet. That is what Furushima does in its database consulting and Protheus managed support practices: an assessment of the environment, the three migration rounds with the official TOTVS tool, and the DBA on the other end when autovacuum decides to show up.

Cover photo: Bernard Dupont, CC BY-SA 2.0, via Wikimedia Commons (cropped). Screenshots marked as TDN are from TOTVS public documentation, reproduced for citation, with links in the list below. psql screenshots are from a lab PostgreSQL 16.

References

Leave a Reply

Your email address will not be published. Required fields are marked *