Skip to content

perf(database): index the message key for chat and message-id lookups - #2771

Open
mmabas77 wants to merge 1 commit into
evolution-foundation:developfrom
mmabas77:perf/message-key-indexes
Open

mmabas77 wants to merge 1 commit into
evolution-foundation:developfrom
mmabas77:perf/message-key-indexes

Conversation

@mmabas77

@mmabas77 mmabas77 commented Oct 28, 2026 •

Copy link
Copy Markdown

📋 Description

Message has only its primary key and Message_instanceId_idx, so every lookup by chat or by message id reads all messages of the instance. This adds a migration with five (instanceId, expression) indexes on key.

The key is filtered in two forms in the code, and PostgreSQL uses an expression index only for the exact same expression, so both are covered:

Form Where Indexes
"key" ->> 'id', "key" ->> 'remoteJid' raw SQL: getMessage, messages.update, updateMessagesReadedByTimestamp, updateChatUnreadMessages (whatsapp.baileys.service.ts), fetchChats (channel.service.ts), updateChatwootMessageId / getMessageByKeyId (chatwoot.service.ts) Message_instanceId_keyId_idx, Message_instanceId_keyRemoteJid_idx
"key" #> '{id}', #> '{remoteJid}', #> '{remoteJidAlt}' Prisma JSON path filters (key: { path: ['remoteJid'], equals }), e.g. findMessages Message_instanceId_keyIdPath_idx, Message_instanceId_keyRemoteJidPath_idx, Message_instanceId_keyRemoteJidAltPath_idx

Prisma cannot declare expression indexes, so they live in a raw SQL migration (Prisma's documented way for unsupported features). A comment on the Message model in both PostgreSQL schemas points to it.

🔗 Related Issue

Refs #2753 (the Message part of it; the IsOnWhatsapp lookup is not changed here).

🧪 Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🔧 Refactoring (no functional changes)
  • ⚡ Performance improvement
  • 🧹 Code cleanup
  • 🔒 Security fix

🧪 Testing

On a production install (v2.3.7, PostgreSQL, one busy instance with 69,210 messages and several clients polling findMessages): evolution_postgres was at 196% CPU on 4 cores and reads timed out under load. After creating the same indexes (built CONCURRENTLY, 1 to 2 s each on a 194 MB table):

Query Before After
messages of one chat, key #> '{remoteJid}' 88.7 ms (67,142 rows removed by filter) 8.6 ms
key #> '{remoteJidAlt}' 99.3 ms 5.9 ms
key ->> 'id' lookup full scan of the instance 0.15 ms

Database CPU went to 55% (one reading before, one after).

On this branch, against an empty PostgreSQL 16 with Prisma 7.8.0:

  • prisma migrate deploy applies all migrations, this one included (CONCURRENTLY, no error), all seven Message indexes are valid (indisvalid), and prisma migrate status reports the schema up to date.

  • EXPLAIN ANALYZE on 50,000 seeded messages: each of the five query forms uses its index.

  • Drift: prisma migrate diff --from-config-datasource --to-schema prisma/postgresql-schema.prisma returns an empty migration, so a later migrate dev does not try to drop these indexes. Control: a plain index that is not in the schema does show up there as DROP INDEX.

  • prisma validate passes for postgresql-schema.prisma and psql_bouncer-schema.prisma.

  • Manual testing completed

  • Functionality verified in development environment

  • No breaking changes introduced

  • Tested with different connection types (if applicable)

✅ Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have manually tested my changes thoroughly
  • I have verified the changes work with different scenarios
  • Any dependent changes have been merged and published

📝 Additional Notes

  • No write lock: the indexes are built CONCURRENTLY. prisma migrate deploy (7.8.0) runs the statements of a migration file one by one, outside a transaction, so this works inside the migration; I tested it with one and with two CONCURRENTLY statements in a file. If a build fails part-way, PostgreSQL leaves an INVALID index under that name: drop it, mark the migration rolled back and deploy again. Installs that already built these indexes by hand under the same names get a no-op (IF NOT EXISTS).
  • Write cost: five more indexes on Message add work to each insert and some disk space. I did not measure that overhead; on the install above the reads were the bottleneck.
  • MySQL is not covered: it needs generated columns for JSON paths, which would be a separate change.

Summary by Sourcery

Improve PostgreSQL message and chat lookup performance by indexing the JSON message key expressions used throughout the application.

Enhancements:

  • Add PostgreSQL expression indexes for common message-key lookups by instance, message ID, chat JID, and alternate chat JID.
  • Build the new indexes concurrently to improve lookup performance without blocking message writes.

Message had only its primary key and instanceId, so every lookup by chat or by message id read all messages of the instance. Prisma JSON path filters compile to "key" #> '{field}' and the raw queries use "key"->>'field', so the migration adds an (instanceId, expression) index for both forms: id and remoteJid as text, id, remoteJid and remoteJidAlt as jsonb path.

Prisma cannot declare expression indexes, so they live in a raw SQL migration, with a comment on the Message model in both PostgreSQL schemas. The indexes are built CONCURRENTLY, which prisma migrate deploy allows because it runs each statement outside a transaction, and IF NOT EXISTS keeps the migration a no-op where they already exist.

Refs evolution-foundation#2753
@sourcery-ai

sourcery-ai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This performance-focused change adds five concurrently built PostgreSQL expression indexes on Message.key, keyed by instanceId, to accelerate message-id, chat, and alternate-chat lookups across both raw SQL and Prisma JSON-path query forms; schema comments document the indexes because Prisma cannot declare them directly.

Entity relationship diagram for Message expression indexes

erDiagram
    MESSAGE {
        string instanceId
        json key
        index key_id_expression
        index key_remoteJid_expression
        index key_id_path_expression
        index key_remoteJid_path_expression
        index key_remoteJidAlt_path_expression
    }
Loading

File-Level Changes

Change Details Files
Adds five PostgreSQL expression indexes covering both raw SQL and Prisma JSON-path message-key lookups.
  • Indexes message instance plus key id, remote JID, and alternate remote JID expressions.
  • Uses separate ->> and #> indexes because PostgreSQL requires expression matching.
  • Builds indexes concurrently and conditionally to avoid write blocking and support existing manually created indexes.
prisma/postgresql-migrations/20261028200000_add_message_key_indexes/migration.sql
Documents the migration-managed expression indexes in both PostgreSQL Prisma schemas.
  • Adds comments linking the unsupported expression indexes to the raw SQL migration.
  • Keeps schema validation and Prisma migration drift behavior aligned with the migration.
prisma/postgresql-schema.prisma
prisma/psql_bouncer-schema.prisma

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!

Sourcery assessment

Approved.


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant