You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When using Kysely with a Postgres database containing many schemas and tables, migrateToLatest() takes ~500 seconds to start up, even when no migrations need to be applied.
My production database has ~40 schemas with ~200 tables each (~8,000 tables total). Just calling migrateToLatest() blocks the application for minutes.
Root cause
After profiling, I found two main bottlenecks:
1. #parseTableMetadata is O(n²)
PostgresIntrospector.#parseTableMetadata uses Array.find() to look up each table by name and schema for every column row:
For each of the n columns, it linearly scans the growing tables array, resulting in O(n²) time complexity. With ~50,000 columns, this in-memory step alone takes ~183 ms per call.
2. #doesTableExist fetches all tables in the database
The migrator's #doesTableExist calls getTables({ withInternalKyselyTables: true }) which introspects every table in every schema, then filters in JavaScript:
With 8,000 tables, this fetches tens of thousands of column rows from pg_catalogtwice (once for the migration table, once for the lock table), just to check if two specific tables exist. This is the dominant contributor to the ~500s startup.
Benchmark: #parseTableMetadata (in-memory only)
Scenario
Total Columns
Before (ms)
After (ms)
Speedup
Small (1 schema, 5 tables, 5 cols)
25
0.002
0.002
1.1x
Medium (2 schemas, 20 tables, 10 cols)
400
0.051
0.026
2.0x
Large (3 schemas, 50 tables, 15 cols)
2,250
0.731
0.133
5.5x
XL (5 schemas, 100 tables, 20 cols)
10,000
11.46
0.61
18.9x
XXL (10 schemas, 200 tables, 25 cols)
50,000
182.69
2.85
64x
Benchmark: getTables() full roundtrip (SQL → parse)
PR feat: add introspector filtering #1706: feat/add-introspector-filtering: Add optional schemas?: string[] and tables?: string[] filters to DatabaseMetadataOptions. This allows pushing filtering down to SQL (via WHERE ... IN (...)) in all 4 dialect introspectors. The migrator's #doesTableExist is updated to query only the specific table it needs instead of fetching everything.
Environment
Kysely 0.28.x
PostgreSQL 16
Node.js v25
Database: ~40 schemas, ~200 tables each (~8,000 tables, ~50,000+ columns)
Disclosure: This issue was written with AI assistance. The problem identification, solution design, benchmarks, and code review were done manually.
When using Kysely with a Postgres database containing many schemas and tables,
migrateToLatest()takes ~500 seconds to start up, even when no migrations need to be applied.My production database has ~40 schemas with ~200 tables each (~8,000 tables total). Just calling
migrateToLatest()blocks the application for minutes.Root cause
After profiling, I found two main bottlenecks:
1.
#parseTableMetadatais O(n²)PostgresIntrospector.#parseTableMetadatausesArray.find()to look up each table by name and schema for every column row:For each of the n columns, it linearly scans the growing
tablesarray, resulting in O(n²) time complexity. With ~50,000 columns, this in-memory step alone takes ~183 ms per call.2.
#doesTableExistfetches all tables in the databaseThe migrator's
#doesTableExistcallsgetTables({ withInternalKyselyTables: true })which introspects every table in every schema, then filters in JavaScript:With 8,000 tables, this fetches tens of thousands of column rows from
pg_catalogtwice (once for the migration table, once for the lock table), just to check if two specific tables exist. This is the dominant contributor to the ~500s startup.Benchmark:
#parseTableMetadata(in-memory only)Benchmark:
getTables()full roundtrip (SQL → parse)Proposed fix
I've split the fix into two PRs:
PR perf: optimize PostgresIntrospector
#parseTableMetadatawith Map-based lookups #1705:perf/pg-parse-table-metadata: ReplaceArray.find()with a nestedMap<schema, Map<table, TableMetadata>>for O(1) lookups → O(n) total complexity. No API changes.PR feat: add introspector filtering #1706:
feat/add-introspector-filtering: Add optionalschemas?: string[]andtables?: string[]filters toDatabaseMetadataOptions. This allows pushing filtering down to SQL (viaWHERE ... IN (...)) in all 4 dialect introspectors. The migrator's#doesTableExistis updated to query only the specific table it needs instead of fetching everything.Environment
0.28.x