Current Migration Documentation for sqllite to postgresql

Looking for an up to date document for migrating sqllite to postgresql.
I have tried a couple different methods found here on the forum and from Google searches
but all have had some data issues which left a non-working system.

They all revolve around starting with an empty postgres database, start Grafana after adjusting
.ini to point to Postgresql, stop Grafana after DB is initialized and using pgload to upload data only/reset sequences from grafana.db.

OS: 
Debian13 Trixie

Postgresql:
pi@raspberry09:~ $ sudo -u postgres psql
psql (17.10 (Debian 17.10-0+deb13u1))
Type "help" for help.

postgres=# select version();
                                                          version
---------------------------------------------------------------------------------------------------------------------------
 PostgreSQL 17.10 (Debian 17.10-0+deb13u1) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit

Grafana:
pi@raspberry05:~ $ grafana -version
grafana version 13.1.1

Why the standard method fails

Grafana seeds default rows when it creates the schema

When you start Grafana against an empty Postgres DB to get it to build the schema, Grafana also inserts default rows (org id=1, default admin user). When pgloader then loads your real data with with data only, it collides with those seeded rows:

ERROR Database error 23505: duplicate key value violates unique constraint "org_pkey"
DETAIL: Key (id)=(1) already exists.
CONTEXT: COPY org, line 1

Some timestamp values in the SQLite database can be represented in a format that pgloader/PostgreSQL cannot parse

Some rows get timestamps in Go’s internal string format, including a monotonic clock suffix Postgres can’t parse:

ERROR Database error 22007: invalid input syntax for type timestamp: "2026-04-30 11:44:40.5456473 +0530 IST m=+98527.488670601"
CONTEXT: COPY alert_rule, line 1, column updated: "2026-04-30 11:44:40.5456473 +0530 IST m=+98527.488670601"

On small tables, this fails the entire table’s load, not just the bad row → I ended up with zero alert rules migrated, with no error surfaced anywhere in the Grafana UI, just a silently empty “Alert rules” page.

Dashboards live in resource, not dashboard, on recent Grafana

On 12.x/13.x, unified storage moved dashboards/folders into resource/resource_history/resource_blob tables. If you check the old dashboard table and it’s empty, that’s expected

Steps →

1. Create the Postgres user and empty database, point Grafana at it, start it once.

sql

CREATE USER grafana WITH PASSWORD '<password>';
CREATE DATABASE grafana OWNER grafana;

Set [database] in grafana.ini (or custom.ini — see note below) to type=postgres with these credentials, then start Grafana and let it fully initialize. Stop it once it’s up.

2. Dump the schema only, then rebuild the database clean.

bash

pg_dump -U grafana -h <host> -d grafana --schema-only -f grafana-schema.sql

sql

DROP DATABASE grafana;
CREATE DATABASE grafana OWNER grafana;

bash

psql -U grafana -h <host> -d grafana -f grafana-schema.sql

This gives you the correct schema with no seeded rows .

3. Run pgloader.

load database
  from sqlite:///path/to/grafana.db
  into postgresql://grafana:<password>@<host>:5432/grafana
with data only, reset sequences
set work_mem to '16MB', maintenance_work_mem to '512MB';

bash

pgloader main.load


Check the final summary → every table should show errors: 0. Example from a clean run:

Total import time          6       2444     4.1 MB          3.356s

If any table shows errors > 0

4. Fix any table that failed due to timestamp parsing.

For each table pgloader reported errors on, e.g. alert_rule:

a) Get its column list in order:

bash

sqlite3 grafana.db "PRAGMA table_info(alert_rule);"

b) Export its rows as SQL insert statements:

bash

sqlite3 grafana.db <<'EOF'
.mode insert alert_rule
SELECT * FROM alert_rule;
EOF

c) Fix the timestamp values: find any string matching <date> <time> <±HHMM> <TZ> m=+<number> and replace it with just <date> <time><+HH:MM> (drop the timezone name and monotonic-clock suffix). Example:

2026-04-30 11:44:40.5456473 +0530 IST m=+98527.488670601
     ↓ becomes
2026-04-30 11:44:40.5456473+05:30

You can do this with sed across the whole export, or by hand if it’s only a few rows.

d) Watch for column type mismatches between SQLite and Postgres while inserting common ones →

Boolean columns: SQLite stores 0/1, Postgres wants false/true → cast explicitly.
BLOB columns: wrap the hex data in decode('<hex>', 'hex') instead of inserting raw.

Before writing ON CONFLICT, check the table’s actual unique constraints

\d tablename

(run this in psql) → match your ON CONFLICT (…) target to whichever constraint you’re likely to collide with. I hit this myself with kv_store, where the real constraint was (org_id, namespace, key), not id.

e) Insert with ON CONFLICT (<primary key or unique columns>) DO NOTHING so you can safely re-run without duplicate-key errors if some rows already loaded:

sql

INSERT INTO alert_rule (id, org_id, title, ...) VALUES (...)
ON CONFLICT (id) DO NOTHING;

Run →

bash

psql -U grafana -h <host> -d grafana -f fix_rows.sql

Repeat for each affected table. In my case this was alert_rule, alert_rule_state, signing_key, user_stats, resource_last_import_timekv_store

5. Verify dashboards migrated correctly .

bash

sqlite3 grafana.db "SELECT resource, count(*) FROM resource GROUP BY resource;"
psql -U grafana -h <host> -d grafana -c "SELECT resource, count(*) FROM resource GROUP BY resource;"

Both outputs should match:

dashboards | 35
folders    | 3

If they match, your dashboards are intact even though the old dashboard table is empty.

6. Start Grafana and do a final check.

Log in with an existing user → confirms user/org/org_user/permission migrated correctly
Dashboards list populates
Alerting → Alert rules shows your rules (check per-org if you have multiple orgs — alert rules are org-scoped, so if you expect N rules and see fewer, you may just be viewing the wrong org)
Alerting → Contact points shows your notification config intact



Ok, so no updated migration documentation. It is left to users to sort out database issues
created by Grafana usage.

First attempt:

These are errors from pgload.

I believe I can skip these three since the objects do not exist in the schema creation generated
by running.

pg_dump -U grafana -h localhost -d grafana_db --schema-only -f grafana-schema.sql
2026-08-10T17:28:38.399997Z ERROR pgloader failed to find target table for source NIL."playlist" with name "playlist" in target catalog
2026-08-10T17:28:38.399997Z ERROR pgloader failed to find target table for source NIL."playlist_item" with name "playlist_item" in target catalog
2026-08-10T17:28:38.411997Z ERROR pgloader failed to find target table for source NIL."entity_event" with name "entity_event" in target catalog

Not sure what to do with the first error, but the remaining are all timestamp issues.

2026-08-10T17:28:40.267984Z ERROR Database error 22001: value too long for type character varying(100)
CONTEXT: COPY tag, line 14, column value: "2026-02-24T10_36_09.922644-05_00 xt8-down kernel - - [meta sequenceId="1042"] 0000_ bc 07 1d cf c6 b..."

2026-08-10T17:28:43.219963Z ERROR Database error 22007: invalid input syntax for type timestamp: "2026-04-14 11:08:59.300073954 +0000 UTC"
CONTEXT: COPY resource_last_import_time, line 2, column last_import_time: "2026-04-14 11:08:59.300073954 +0000 UTC"2026-08-10T17:28:47.179934Z ERROR Database error 22007: invalid input syntax for type timestamp: "2026-08-09 20:25:41.54773223 -0400 EDT m=+49109.633243837"
CONTEXT: COPY kv_store, line 4, column updated: "2026-08-09 20:25:41.54773223 -0400 EDT m=+49109.633243837"
2026-08-10T17:28:47.187934Z ERROR Database error 22007: invalid input syntax for type timestamp: "2026-04-14 07:09:00.670189986 -0400 EDT m=+2.562001333"
CONTEXT: COPY role, line 39, column updated: "2026-04-14 07:09:00.670189986 -0400 EDT m=+2.562001333"
2026-08-10T17:28:47.347933Z ERROR Database error 22007: invalid input syntax for type timestamp: "2026-05-30 20:05:08.114883615 -0400 EDT m=+218729.948410707"
CONTEXT: COPY signing_key, line 1, column added_at: "2026-05-30 20:05:08.114883615 -0400 EDT m=+218729.948410707"
2026-08-10T17:28:47.407933Z ERROR Database error 22007: invalid input syntax for type timestamp: "2026-08-10 13:12:05.006942959 -0400 EDT m=+109493.092454548"
CONTEXT: COPY alert_rule_state, line 1, column updated_at: "2026-08-10 13:12:05.006942959 -0400 EDT m=+109493.092454548"
2026-08-10T17:28:49.375919Z LOG report summary reset
                        table name     errors       rows      bytes      total time
----------------------------------  ---------  ---------  ---------  --------------
                             fetch          0          0                     0.000s
                   fetch meta data          0         90                     0.080s
                 Drop Foreign Keys          0          0                     0.000s
                          Truncate          0         87                     0.060s
----------------------------------  ---------  ---------  ---------  --------------
                     migration_log          0        725   117.1 kB          0.096s
                         temp_user          0          0                     0.048s
                               org          0          1     0.1 kB          0.100s
                     dashboard_tag          0         24     0.7 kB          0.160s
            dashboard_provisioning          0          0                     0.228s
                           api_key          0          1     0.2 kB          0.280s
                             quota          0          0                     0.344s
                           session          0          0                     0.400s
                             alert          0          0                     0.528s
                alert_notification          0          0                     0.580s
                        annotation          0     285636   202.3 MB         10.700s
                            "user"          0          2     0.4 kB          0.004s
                              star          0          0                     0.064s
                          org_user          0          2     0.1 kB          0.112s
                         dashboard          0         14   493.2 kB          0.248s
                       data_source          0          5     1.6 kB          0.312s
                dashboard_snapshot          0          0                     0.364s
                    plugin_setting          0          2     0.2 kB          0.496s
                       preferences          0          1     0.2 kB          0.568s
                    alert_rule_tag          0          0                     0.640s
          alert_notification_state          0          0                     0.688s
                    annotation_tag          0      32777   426.4 kB          0.984s
                 dashboard_version          0        141     3.9 MB          1.200s
                       team_member          0          0                     1.172s
                               tag          1          0                     1.552s
                         user_auth          0          0                     1.432s
                   user_auth_token          0          6     1.7 kB          1.588s
                         short_url          0         14     3.4 kB          1.664s
                        alert_rule          0          6     7.0 kB          1.728s
               alert_configuration          0          1     1.0 kB          1.884s
                   provenance_type          0          4     0.1 kB          1.940s
       alert_configuration_history          0          9     6.3 kB          2.016s
        library_element_connection          0          0                     2.068s
                           secrets          0          5     0.9 kB          2.148s
                        permission          0        859    89.9 kB          2.292s
                         team_role          0          0                     2.336s
                      builtin_role          0          7     0.4 kB          2.396s
             query_history_details          0        108     3.6 kB          2.488s
                       correlation          0          0                     2.460s
                              file          0          0                     2.516s
                   seed_assignment          0          0                     2.568s
                       anon_device          0          0                     2.708s
                       sso_setting          0          0                     2.756s
          cloud_migration_snapshot          0          0                     2.824s
             user_external_session          0          0                     2.872s
              secret_migration_log          0         35     7.2 kB          3.112s
                     secret_keeper          0          0                     3.164s
            resource_migration_log          0         49     8.2 kB          3.220s
                  resource_history          0       1850    13.9 MB          4.080s
         resource_last_import_time          1          0                     3.936s
            secret_encrypted_value          0          0                     4.008s
                   resource_events          0          0                     4.056s
          pending_tenant_deletions          0          0                     4.232s
          search_snapshot_manifest          0          0                     4.288s
                         test_data          0          0                     0.008s
                              team          0          0                     0.008s
                     dashboard_acl          0          2     0.1 kB          0.080s
                     login_attempt          0          0                     0.072s
                       server_lock          0          5     0.2 kB          0.132s
                        cache_data          0          0                     0.120s
                    alert_instance          0          9     5.1 kB          0.292s
                alert_rule_version          0         77    88.9 kB          0.344s
             ngalert_configuration          0          0                     0.296s
                       alert_image          0          0                     0.312s
                   library_element          0          0                     0.320s
                         data_keys          0         26     5.4 kB          0.380s
                          kv_store          1          0                     0.376s
                              role          1          0                     0.376s
                         user_role          0          1     0.0 kB          0.420s
                     query_history          0        108    32.3 kB          0.428s
                query_history_star          0          0                     0.420s
                  dashboard_public          0          0                     0.472s
                         file_meta          0          0                     0.476s
                            folder          0          1     0.1 kB          0.472s
                       signing_key          1          0                     0.516s
           cloud_migration_session          0          0                     0.520s
          cloud_migration_resource          0          0                     0.520s
                  alert_rule_state          1          0                     0.568s
               secret_secure_value          0          0                     0.572s
                   secret_data_key          0          0                     0.580s
                          resource          0         82     1.1 MB          0.732s
                     resource_blob          0          0                     0.660s
cloud_migration_snapshot_partition          0          0                     0.612s
                  resource_version          0          6     0.3 kB          0.680s
      unifiedstorage_migration_log          0          3     0.5 kB          0.728s
                         kv_leases          0          0                     0.732s
              search_snapshot_data          0          0                     0.760s
----------------------------------  ---------  ---------  ---------  --------------
           COPY Threads Completion          0          4                    10.720s
                   Reset Sequences          0         63                     0.076s
               Create Foreign Keys          0          0                     0.000s
                  Install Comments          0          0                     0.000s
----------------------------------  ---------  ---------  ---------  --------------
                 Total import time          6     322604   222.5 MB         10.796s

Need to sort out how to fix these items and move on to attempt 2.

How many dashboards do you have? Any other artifacts such as alerts, etc?

19 Dashboards
5 Rule Groups
6 Alerts
5 Datasources

Using some python scripts to generate counts for all tables in both sqlite and postpgres.

pi@raspberry09:~/migrate_grafana $ diff -ZB sqlite_counts.log postgres_counts.log
9c9
< alert_rule_state,6
---
> alert_rule_state,0
32d31
< entity_event,0
37c36
< kv_store,12
---
> kv_store,0
47,48d45
< playlist,0
< playlist_item,0
60c57
< resource_last_import_time,3
---
> resource_last_import_time,0
63c60
< role,128
---
> role,0
76c73
< signing_key,4
---
> signing_key,0
79c76
< tag,38191
---
> tag,0

another, maybe less painful, approach you might want to look into is using the provisioning route. you provision all of these artifacts using python and then copy these to the new grafana instance and restart.

I have yet to see any one soul successfully migrate from sqlite to another db using the psql and such tools.

I am absolutely terrified you may be correct about the migration path. I fail to understand
why Grafana does not have a more painless approach to allow migrations between
supported databases.

I did look at using the api to get data sources out in a yaml so they could be provisioned in
a new deployment. I believe everything else can be exported/imported to move.
I would rather not take that approach unless forced to do so.

Looking at what tables require data fixes.

The following are timestamp issues.

alert_rule_state,6 rows
kv_store,12 rows
resource_last_import_time,3 rows
signing_key,4 rows
role,128 rows

I need more information on the tag table. Primarily what is it storing.

tag,38191 rows.

sqlite> .schema tag

CREATE TABLE `tag` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
, `key` TEXT NOT NULL
, `value` TEXT NOT NULL
);
CREATE UNIQUE INDEX `UQE_tag_key_value` ON `tag` (`key`,`value`);

From creating insert statements it looks like it is old alert data.
I would like to see if this data could be purged in place
prior to attempting a migration to Postgresql

INSERT INTO tag VALUES (118, 'application', 'pvestatd');
INSERT INTO tag VALUES (119, 'connect_host', 'rp5-util07.dbonenet.com.');
INSERT INTO tag VALUES (120, 'facility', 'daemon');
INSERT INTO tag VALUES (121, 'host', 'rp5-util07');
INSERT INTO tag VALUES (122, 'hostname', 'rp5-util07');
INSERT INTO tag VALUES (123, 'message', '2026-02-24T23_21_17.041011-05_00 rp5-util07 pvestatd 1482 - -  auth key pair too old, rotating..');
INSERT INTO tag VALUES (124, 'message', '2026-02-24T23_36_09.97476-05_00 xt8-down kernel - - [meta sequenceId="1173"] kck_');
INSERT INTO tag VALUES (125, 'message', '2026-02-24T23_36_09.97476-05_00 xt8-down kernel - - [meta sequenceId="1174"] 0000_ bc 07 1d cf c6 b4 46 97 bd 3b 3b 22 7e df e5 35');
INSERT INTO tag VALUES (126, 'message', '2026-02-24T23_36_09.97476-05_00 xt8-down kernel - - [meta sequenceId="1175"] kek_');
INSERT INTO tag VALUES (127, 'message', '2026-02-24T23_36_09.980348-05_00 xt8-down kernel - - [meta sequenceId="1176"] 0000_ fa 93 e6 b8 b9 d2 44 50 91 bc 3e b8 9f 3b ef 08');
INSERT INTO tag VALUES (128, 'message', '2026-02-24T23_36_09.980665-05_00 xt8-down kernel - - [meta sequenceId="1177"] replay_ctr_');
INSERT INTO tag VALUES (129, 'message', '2026-02-24T23_36_09.984636-05_00 xt8-down kernel - - [meta sequenceId="1178"] 0000_ 00 00 00 00 00 00 00 29');
INSERT INTO tag VALUES (130, 'application', 'MerlinAU_');
INSERT INTO tag VALUES (131, 'message', '2026-02-24T23_45_01-05_00 rt-be88u MerlinAU_ 15994 - [meta sequenceId="4889"] Cron job hook already exists in '/jffs/scripts/services-start' script.');
INSERT INTO tag VALUES (132, 'message', '2026-02-25T00_00_02-05_00 rt-be88u MerlinAU_ 13221 - [meta sequenceId="4908"] Cron job hook already exists in '/jffs/scripts/services-start' script.');
INSERT INTO tag VALUES (133, 'message', '2026-02-25T00_00_03-05_00 rt-be88u MerlinAU_ 13221 - [meta sequenceId="4909"] MerlinAU v1.5.9');
INSERT INTO tag VALUES (134, 'message', '2026-02-25T00_00_03-05_00 rt-be88u MerlinAU_ 13221 - [meta sequenceId="4910"] Running the update task now. Checking for F/W updates...');
INSERT INTO tag VALUES (135, 'message', '2026-02-25T00_00_03-05_00 rt-be88u MerlinAU_ 13221 - [meta sequenceId="4911"] Successful Login for AiMesh Node [XXX.XXX.XXX.XXX].');
INSERT INTO tag VALUES (136, 'message', '2026-02-25T00_00_03-05_00 rt-be88u MerlinAU_ 13221 - [meta sequenceId="4912"] Successful Login for AiMesh Node [XXX.XXX.XXX.XXX].');
INSERT INTO tag VALUES (137, 'message', '2026-02-25T00_00_03-05_00 rt-be88u MerlinAU_ 13221 - [meta sequenceId="4913"] Successful Login for AiMesh Node [XXX.XXX.XXX.XXX].');
INSERT INTO tag VALUES (138, 'message', '2026-02-25T00_00_12-05_00 rt-be88u MerlinAU_ 13221 - [meta sequenceId="4915"] No new firmware version update is found for [RT-BE88U] router model.');
INSERT INTO tag VALUES (139, 'message', '2026-02-25T00_05_02-05_00 xt8-down MerlinAU_ 21225 - [meta sequenceId="1183"] Cron job hook already exists in '/jffs/scripts/services-start' script.');

It looks like the data in tag ranges from 02/24/2026 to 06/24/2026.
June is when I switched to unified_alerting.state_history going into Loki.
So if this is just old alert data it is definitly a candidate to purge

Somehow it looks like I have successfully stumbled my way thru
migrating Grafana from sqllite to Postgres. Dashboards are working, Alerts are firing and email
from Alerts are being received. Not seeing anything unusual in either Grafana or Postgres logs.

Still disappointed that Grafana sticks the data correction of Grafana generated data
on the backs of their users.

Will keep an eye on things to make sure my initial assessment of the migration holds water.

Well it’s been over 24 hours and all looks to be working well.

I mostly used the process described in many of the documents available to run this migration.
Did use a sightly modified pgload config so I could skip a step.

LOAD DATABASE
     FROM sqlite:///path to/grafana.db
     INTO postgresql://grafana:password@localhost:5432/grafana_db

WITH truncate, data only, reset sequences

SET work_mem to '16MB', maintenance_work_mem to '512 MB';

The biggest PITA was fixing Grafana generated data. These are the tables I had to fix.

TAG: deleted all records in /var/lib/grafana/grafana.db source database prior to migration.
ROLE: create insert statements. fixed timestamp, boolean(0::boolen/1:boolean) and escaped apostrophes (Grafana's)
RESOURCE_LAST_IMPORT_TIME: create insert statements. fixed timestamp
KV_STORE: create insert statements. fixed timestamp and fixed one insert with dollar quoting.
SIGNING_KEY:create insert statements. fixed timestamp
ALERT_RULE_STATE:create insert statements. fixed timestamp and change python script to convert binary data to pure hexadecimal format (\x...).

I am not planning to mark this post as solved since the question I asked has not been resolved.
Very grateful to @infofcc3 for the information provided as that did help greatly.

Here are a couple of dashboards of my small but working environment.


Nice! I would create a github issue in grafana documentation

I doubt this forum would be the route one would take to get visibility to this issue