Database Benchmark: MySQL vs. PostgreSQL vs. SingleStore Performance in Docker (100M+ Records)

Clock Icon

4 min read

Pencil Icon

May 20, 2025

Selecting the right database engine isn’t just a checkbox in your architecture diagram —   it directly shapes both the user experience and infrastructure cost at scale. To see what really happens when you throw 100 million+ transactions at modern SQL engines, we pitted MySQL, PostgreSQL and SingleStore head‑to‑head in identical Docker containers, generated realistic fintech‑style workloads and measured how each platform handles heavy joins, rolling‑window sums and hot analytic queries. This article walks you through our open‑source benchmarking setup, shares the raw query times and highlights the architectural trade‑offs you should weigh before committing to an engine.

project-setupProject setup

To keep things realistic and on equal ground, we created a project and workloads that could be used across all three platforms being tested. The benchmark project we built here is based on Turborepo, and includes several applications and packages. These include:

Applications

  • ./apps/db.   Application for running Docker containers.

  • ./apps/data-generator.  Node.js application for generating and loading data into databases and creating indexes

  • ./apps/client. Next.js application to start the client and execute queries

Packages

  • ./packages/singlestore. Drizzle instance connected to the SingleStore image

  • ./packages/mysql. Drizzle instance connected to the MySQL image

  • ./packages/postgres. Drizzle instance connected to the PostgreSQL image

Docker images

  • SingleStore. ghcr.io/singlestore-labs/singlestoredb-dev:latest

  • MySQL. mysql:latest

  • Postgres. postgres:latest

The great part about this setup is you can run the project locally by cloning the docker-benchmarking-battle repository and following the setup instructions in the included README.md.

dataset-schemaDataset schema

All three databases share the exact same schema and data volume. This includes the following tables and row counts for every database instance:

Database Benchmark: MySQL vs. PostgreSQL vs. SingleStore Performance in Docker (100M+ Records)
By creating data and schemas this way, we are able to run many high volume and realistic scenarios that include multiple joins. Next, let’s look at how this data is used throughout the benchmark scenarios we ran.

benchmark-scenariosBenchmark scenarios

Each query below simulates a real-world use case and is executed on the full dataset of 100 million transactions, ten million accounts and one million users. There are three scenarios/queries used in this benchmark to test out-of-the-box capabilities of all three platforms. Let’s break down the scenarios further, and go over the queries in detail.

1-total-transaction-volume-last-30-days1. Total transaction volume (last 30 days)

The following query calculates the sum of all successful transactions made within the past 30 days across the entire system, joining the transactions and transaction_statuses tables, filtering for status = "success" and createdAt > NOW() - 30 days.

This type of query could be used by financial dashboards and revenue reporting tools that frequently need to calculate totals over dynamic time windows. Using TypeScript, the query looks like this:

1

// ./packages/singlestore/transaction/get-sum.ts

2

3

export async function getTransactionsSum()

4

{

5

const cutoffDate = subDays(new Date(), 30);

6

7

const result = await singlestore

8

.select({ sum: sum(transactionsTable.amount) })

9

.from(transactionsTable)

10

.innerJoin(transactionStatusesTable, eq(transactionStatusesTable.id, transactionsTable.statusId))

11

.where(and(gt(transactionsTable.createdAt, cutoffDate), eq(transactionStatusesTable.name, "success")));

12

13

return result;

14

}

When running the query across all three platforms, here is the result:

In these results, we can see that SingleStore executed the query in 382 ms, which is 116 times faster than MySQL (44s 561ms) and 20 times faster than Postgres (7s 734ms). This shows the speed in which SingleStore is able to query against large datasets extremely efficiently.

2-top-transfer-recipients2. Top transfer recipients

Next, we’ll look at another query that finds the most common recipients of successful transfers by counting how many times each account received a transfer, joining the transactions, transaction_types and transaction_statuses tables. Filters for type = "transfer" and status = "success" then count how many times each accountIdTo received a transfer, which are then sorted by transferCount.

This type of query can be useful in detecting VIP recipients, fraud patterns or frequent business payees in a fintech app. When boiled into TypeScript, this is the resulting query:

1

// ./packages/singlestore/account/list-top-recipients.ts

2

3

export type ListTopRecipientsParams = {

4

limit?: number;

5

};

6

7

export async function listTopRecipients(params: ListTopRecipientsParams) {

8

const { limit = 10 } = params;

9

10

const result = await singlestore

11

.select({

12

accountIdTo: transactionsTable.accountIdTo,

13

transferCount: count(transactionsTable.id).as("transferCount"),

14

})

15

.from(transactionsTable)

16

.innerJoin(transactionTypesTable, eq(transactionTypesTable.id, transactionsTable.typeId))

17

.innerJoin(transactionStatusesTable, eq(transactionStatusesTable.id, transactionsTable.statusId))

18

.where(and(eq(transactionTypesTable.name, "transfer"), eq(transactionStatusesTable.name, "success")))

19

.groupBy(transactionsTable.accountIdTo)

20

.orderBy(desc(sql`transferCount`))

21

.limit(limit);

22

23

return result;

24

}

When the query is executed, we can see once again that SingleStore, when used in queries with multiple joins, reigns supreme.

When looking closer at the numbers, we see SingleStore executed the query in 1s 964ms, which is 147 times faster than MySQL (4m 49s 353ms) and six times faster than Postgres (12s 334ms). Again, an impressive result.

3-join-users-accounts-and-transactions3. Join users, accounts and transactions

Lastly, we will run a query that retrieves recent transactions from the past 7 days and includes associated user and account information using multi-table joins. Joins users, accounts, transactions and transaction_types tables to retrieve transactions from the last seven days, including user name, account ID, amount and transaction type.

This type of query replicates a real-time activity feed or recent transaction page, common in banking apps and a sweet spot for SingleStore . With the query parsed into TypeScript, here is the resulting code:

1

// ./packages/singlestore/transaction/list-recent-with-info.ts

2

3

export async function listRecentTransactionsWithInfo() {

4

const cutoffDate = subDays(new Date(), 7);

5

6

const result = await singlestore

7

.select({

8

userId: usersTable.id,

9

name: usersTable.name,

10

accountId: accountsTable.id,

11

amount: transactionsTable.amount,

12

type: transactionTypesTable.name,

13

createdAt: transactionsTable.createdAt,

14

})

15

.from(usersTable)

16

.innerJoin(accountsTable, eq(accountsTable.userId, usersTable.id))

17

.innerJoin(transactionsTable, eq(transactionsTable.accountIdFrom, accountsTable.id))

18

.innerJoin(transactionTypesTable, eq(transactionTypesTable.id, transactionsTable.typeId))

19

.where(gte(transactionsTable.createdAt, cutoffDate))

20

.orderBy(desc(transactionsTable.createdAt))

21

.limit(100);

22

23

return result;

24

}

25

When the query runs, we see a really interesting result between SingleStore and Postgres. However, more shockingly, we can see that MySQL still hasn’t returned a result after five minutes!

Looking closer, we see that SingleStore executed the query in 20s 38ms, which is two times faster than Postgres (40s 58ms), while MySQL could not execute the query even within five minutes. This puts SingleStore and Postgres on a similar footing, with SingleStore still being twice as quick. However, this also highlights the fact that MySQL struggles with these queries.

key-takeawaysKey takeaways

When reflecting on this short benchmarking exercise, we  see a few strong themes emerge:

  • SingleStore delivered the best performance across all benchmark scenarios

  • MySQL struggled with large queries, joins, and aggregations

  • PostgreSQL was more performant than MySQL but couldn’t match SingleStore’s analytical speeds

try-it-yourselfTry it yourself

Wondering if your own setup will make a difference? You can easily run the benchmark on your own machine   just follow a few simple steps and start experimenting with the data or queries.

  1. Start with $600 in free credits

  2. Clone the GitHub repository

  3. Follow the setup steps and run the benchmarks on your own system

  4. Adjust query patterns, dataset size or DB configs to explore even more scenarios

When it comes to handling large-scale, real-time data, not all SQL databases are created equal. In our Dockerized benchmark, SingleStore consistently outperformed MySQL and PostgreSQL across common query patterns in fintech and analytics applications.

Don't forget to share this post with your team if you're comparing database performance for your next project. We hope you found the information and benchmark setup displayed in this post helpful! Stay tuned for more in-depth benchmarking content coming soon.

resourcesResources


Share