p.enthalabs

Postgres SELECT DISTINCT Does Not Scale

!Image 9: logo

[](https://www.cookiebot.com/en/what-is-behind-powered-by-cookiebot/?utm_source=banner_cb&utm_medium=referral&utm_content=v2)

- Consent

- Details

- [[#IABV2SETTINGS#]](https://www.dbos.dev/blog/postgres-select-distinct-does-not-scale#)

- About

This website uses cookies

We use cookies to personalise content and ads, to provide social media features and to analyse our traffic. We also share information about your use of our site with our social media, advertising and analytics partners who may combine it with other information that you’ve provided to them or that they’ve collected from your use of their services.

[#GPC_BANNER_ICON#]

[#GPC_TOAST_TEXT#]

Consent Selection

**Necessary**

- [x]

**Preferences**

- [x]

**Statistics**

- [x]

**Marketing**

- [x]

Show details

Details

* Necessary 15- [x] Necessary cookies help make a website usable by enabling basic functions like page navigation and access to secure areas of the website. The website cannot function properly without these cookies.

- Cookiebot 2Learn more about this provider![Image 10: opens in a new window](https://www.cookiebot.com/goto/privacy-policy/ "Learn more about this provider Cookiebot's privacy policy - opens in a new window")**CookieConsent[x2]**Stores the user's cookie consent state for the current domain**Maximum Storage Duration**: 1 year**Type**: HTTP Cookie

- HubSpot 1Learn more about this provider![Image 11: opens in a new window](https://legal.hubspot.com/privacy-policy "Learn more about this provider HubSpot's privacy policy - opens in a new window")**cookietest**This cookie is used to determine if the visitor has accepted the cookie consent box.**Maximum Storage Duration**: Session**Type**: HTTP Cookie

- LinkedIn 2Learn more about this provider![Image 12: opens in a new window](https://www.linkedin.com/legal/privacy-policy "Learn more about this provider LinkedIn's privacy policy - opens in a new window")**bcookie**Used in order to detect spam and improve the website's security. **Maximum Storage Duration**: 1 year**Type**: HTTP Cookie **li_gc**Stores the user's cookie consent state for the current domain**Maximum Storage Duration**: 180 days**Type**: HTTP Cookie

- hs-analytics.net hs-banner.com linkedin.com 9**__cf_bm[x9]**This cookie is used to distinguish between humans and bots. This is beneficial for the website, in order to make valid reports on the use of their website.**Maximum Storage Duration**: 1 day**Type**: HTTP Cookie

- www.dbos.dev 1**_cfuvid**This cookie is a part of the services provided by Cloudflare - Including load-balancing, deliverance of website content and serving DNS connection for website operators. **Maximum Storage Duration**: Session**Type**: HTTP Cookie

* Preferences 2- [x] Preference cookies enable a website to remember information that changes the way the website behaves or looks, like your preferred language or the region that you are in.

- Cookiebot 1Learn more about this provider![Image 13: opens in a new window](https://www.cookiebot.com/goto/privacy-policy/ "Learn more about this provider Cookiebot's privacy policy - opens in a new window")**CookieConsentBulkSetting-#**Enables cookie consent across multiple websites**Maximum Storage Duration**: 1 year**Type**: HTML Local Storage

- LinkedIn 1Learn more about this provider![Image 14: opens in a new window](https://www.linkedin.com/legal/privacy-policy "Learn more about this provider LinkedIn's privacy policy - opens in a new window")**lidc**Registers which server-cluster is serving the visitor. This is used in context with load balancing, in order to optimize user experience. **Maximum Storage Duration**: 1 day**Type**: HTTP Cookie

* Statistics 6- [x] Statistic cookies help website owners to understand how visitors interact with websites by collecting and reporting information anonymously.

- HubSpot 4Learn more about this provider![Image 15: opens in a new window](https://legal.hubspot.com/privacy-policy "Learn more about this provider HubSpot's privacy policy - opens in a new window")**__hssc**Identifies if the cookie data needs to be updated in the visitor's browser.**Maximum Storage Duration**: 1 day**Type**: HTTP Cookie **__hssrc**Used to recognise the visitor's browser upon reentry on the website.**Maximum Storage Duration**: Session**Type**: HTTP Cookie **__hstc**Sets a unique ID for the session. This allows the website to obtain data on visitor behaviour for statistical purposes.**Maximum Storage Duration**: 180 days**Type**: HTTP Cookie **hubspotutk**Sets a unique ID for the session. This allows the website to obtain data on visitor behaviour for statistical purposes.**Maximum Storage Duration**: 180 days**Type**: HTTP Cookie

- Matomo 2Learn more about this provider![Image 16: opens in a new window](https://matomo.org/privacy-policy/ "Learn more about this provider Matomo's privacy policy - opens in a new window")**_pk_id#**Collects statistics on the user's visits to the website, such as the number of visits, average time spent on the website and what pages have been read.**Maximum Storage Duration**: 1 year**Type**: HTTP Cookie **_pk_ses#**Used by Piwik Analytics Platform to track page requests from the visitor during the session.**Maximum Storage Duration**: 1 day**Type**: HTTP Cookie

* Marketing 14- [x] Marketing cookies are used to track visitors across websites. The intention is to display ads that are relevant and engaging for the individual user and thereby more valuable for publishers and third party advertisers.

- Google 2Learn more about this provider![Image 17: opens in a new window](https://business.safety.google/privacy/ "Learn more about this provider Google's privacy policy - opens in a new window")Some of the data collected by this provider is for the purposes of personalization and measuring advertising effectiveness. The provider may use the IP Addresses for ads measurement and ads personalization.

**_ga**Used to send data to Google Analytics about the visitor's device and behavior. Tracks the visitor across devices and marketing channels.**Maximum Storage Duration**: 2 years**Type**: HTTP Cookie **_ga_#**Used to send data to Google Analytics about the visitor's device and behavior. Tracks the visitor across devices and marketing channels.**Maximum Storage Duration**: 2 years**Type**: HTTP Cookie

- HubSpot 1Learn more about this provider![Image 18: opens in a new window](https://legal.hubspot.com/privacy-policy "Learn more about this provider HubSpot's privacy policy - opens in a new window")**__ptq.gif**Sends data to the marketing platform Hubspot about the visitor's device and behaviour. Tracks the visitor across devices and marketing channels.**Maximum Storage Duration**: Session**Type**: Pixel Tracker

- YouTube 11Learn more about this provider![Image 19: opens in a new window](https://business.safety.google/privacy/ "Learn more about this provider YouTube's privacy policy - opens in a new window")**__Secure-ROLLOUT_TOKEN**Used to track user’s interaction with embedded content.**Maximum Storage Duration**: 180 days**Type**: HTTP Cookie **__Secure-YEC**Stores the user's video player preferences using embedded YouTube video**Maximum Storage Duration**: Session**Type**: HTTP Cookie **__Secure-YNID**Used to track user’s interaction with embedded content.**Maximum Storage Duration**: 180 days**Type**: HTTP Cookie **LAST_RESULT_ENTRY_KEY**Used to track user’s interaction with embedded content.**Maximum Storage Duration**: Session**Type**: HTTP Cookie **LogsDatabaseV2:V#||LogsRequestsStore**Used to track user’s interaction with embedded content.**Maximum Storage Duration**: Persistent**Type**: IndexedDB **ServiceWorkerLogsDatabase#SWHealthLog**Necessary for the implementation and functionality of YouTube video-content on the website. **Maximum Storage Duration**: Persistent**Type**: IndexedDB **TESTCOOKIESENABLED**Used to track user’s interaction with embedded content.**Maximum Storage Duration**: 1 day**Type**: HTTP Cookie **VISITOR_INFO1_LIVE**Tries to estimate the users' bandwidth on pages with integrated YouTube videos.**Maximum Storage Duration**: 180 days**Type**: HTTP Cookie **YSC**Registers a unique ID to keep statistics of what videos from YouTube the user has seen.**Maximum Storage Duration**: Session**Type**: HTTP Cookie **yt-icons-last-purged**Necessary for the implementation and functionality of YouTube video-content on the website. **Maximum Storage Duration**: Persistent**Type**: HTML Local Storage **YtIdbMeta#databases**Used to track user’s interaction with embedded content.**Maximum Storage Duration**: Persistent**Type**: IndexedDB

- Unclassified 13 Unclassified cookies are cookies that we are in the process of classifying, together with the providers of individual cookies.

- Amazon 6Learn more about this provider![Image 20: opens in a new window](https://www.amazon.com/gp/help/customer/display.html/ref=footer_privacy?ie=UTF8&nodeId=468496 "Learn more about this provider Amazon's privacy policy - opens in a new window")**__test__**Pending**Maximum Storage Duration**: Session**Type**: HTTP Cookie **_reb2bgeo**Pending**Maximum Storage Duration**: Session**Type**: HTTP Cookie **_reb2bloaded**Pending**Maximum Storage Duration**: Session**Type**: HTTP Cookie **_reb2bsessionID**Pending**Maximum Storage Duration**: Session**Type**: HTTP Cookie **_reb2buid**Pending**Maximum Storage Duration**: Session**Type**: HTTP Cookie **_reb2buid**Pending**Maximum Storage Duration**: Persistent**Type**: HTML Local Storage

- cdn.jsdelivr.net 7**fs-consent-ad_personalization**Pending**Maximum Storage Duration**: 4 months**Type**: HTTP Cookie **fs-consent-ad_storage**Pending**Maximum Storage Duration**: 4 months**Type**: HTTP Cookie **fs-consent-ad_user_data**Pending**Maximum Storage Duration**: 4 months**Type**: HTTP Cookie **fs-consent-analytics_storage**Pending**Maximum Storage Duration**: 4 months**Type**: HTTP Cookie **fs-consent-functionality_storage**Pending**Maximum Storage Duration**: 4 months**Type**: HTTP Cookie **fs-consent-personalization_storage**Pending**Maximum Storage Duration**: 4 months**Type**: HTTP Cookie **fs-consent-security_storage**Pending**Maximum Storage Duration**: 4 months**Type**: HTTP Cookie

Cross-domain consent 2

Your consent applies to the following domains:

List of domains your consent applies to:console.dbos.devwww.dbos.dev

Cookie declaration last updated on 8/26/26 by Cookiebot

[#IABV2_TITLE#]

[#IABV2_BODY_INTRO#]

[#IABV2_BODY_LEGITIMATE_INTEREST_INTRO#]

[#IABV2_BODY_PREFERENCE_INTRO#]

[#IABV2_LABEL_PURPOSES#]

[#IABV2_BODY_PURPOSES_INTRO#]

[#IABV2_BODY_PURPOSES#]

[#IABV2_LABEL_FEATURES#]

[#IABV2_BODY_FEATURES_INTRO#]

[#IABV2_BODY_FEATURES#]

[#IABV2_LABEL_PARTNERS#]

[#IABV2_BODY_PARTNERS_INTRO#]

[#IABV2_BODY_PARTNERS#]

About

Cookies are small text files that can be used by websites to make a user's experience more efficient.

The law states that we can store cookies on your device if they are strictly necessary for the operation of this site. For all other types of cookies we need your permission.

This site uses different types of cookies. Some cookies are placed by third party services that appear on our pages.

You can at any time change or withdraw your consent from the Cookie Declaration on our website.

Learn more about who we are, how you can contact us and how we process personal data in our Privacy Policy.

Please state your consent ID and date when you contact us regarding your consent.

- [x]

**Do not sell or share my personal information**

Deny Allow selection Customize Allow all

Sept 10: DBOS for Rust - First Look

[](https://www.dbos.dev/blog/postgres-select-distinct-does-not-scale#)

![Image 21: DBOS - Logo](https://www.dbos.dev/)

Products

DBOS Transact Open source durable execution libraryDBOS Conductor Control plane for agents & workflows

PricingCustomers

Resources

About DBOS Meet the team simplifying reliability.Videos Demos, deep dives, and more DBOS.Partners Explore the DBOS ecosystem.

Docs

Quickstart Launch your first workflow in minutes.Docs Step-by-step guides & real-world use cases.Example Applications Ready-to-run code to spark your project.

Blog

[](https://discord.com/invite/jsmC6pXGgX)

Explore

4.7K

dbos/dbos-transact-ts Durable Execution Library - TypeScriptdbos/dbos-transact-py Durable Execution Library - Pythondbos-inc/dbos-transact-go Durable Execution Library - Godbos-inc/dbos-transact-java Durable Execution Library - Javadbos-inc/dbos-demo-apps Demo DBOS applicationsSee all repos

Sign inGet started

July 24:DBOS User Group Meeting

![Image 22: DBOS - Logo](https://www.dbos.dev/old-home-3)

Products

DBOS Transact Open source durable execution libraryDBOS Cloud Deploy with a click, scale to millions

CustomersPricingBlogDocs

Resources

About DBOS See our story, meet our team.Videos DBOS concepts and best practices

Start your project

Login

[](https://github.com/dbos-inc/dbos-transact-py)[](https://discord.com/invite/jsmC6pXGgX)Login

Start your project

Back to posts

Postgres SELECT DISTINCT Does Not Scale

!Image 23

Peter Kraft

August 10, 2026

Benchmarks

!Image 24

![Image 25 Add to Google](https://www.google.com/preferences/source?q=https://www.dbos.dev)

Share this post

[](https://www.linkedin.com/shareArticle?mini=true&url=https://www.dbos.dev/blog/postgres-select-distinct-does-not-scale&title=Postgres%20SELECT%20DISTINCT%20Does%20Not%20Scale)

[](https://twitter.com/intent/tweet?url=https://www.dbos.dev/blog/postgres-select-distinct-does-not-scale&hashtags=dbos)

[](https://www.facebook.com/sharer/sharer.php?u=https://www.dbos.dev/blog/postgres-select-distinct-does-not-scale)

[](mailto:?subject=Check%20out%20this%20article%20from%20DBOS&body=Check%20out%20this%20article%20from%20DBOS%3A%0Ahttps://www.dbos.dev/blog/postgres-select-distinct-does-not-scale)

SELECT DISTINCT is an innocuous-seeming clause that finds all unique values of a column. However, it has a dark secret: no matter how you index your table, no matter how few unique values there are to retrieve, SELECT DISTINCT will always scan every row that matches its predicates. We recently observed this when diagnosing the performance of a Postgres-backed queues workload, where SELECT DISTINCT turned out to be the most expensive query despite appearing to be the simplest and cheapest. In this blog post, we’ll explain what happened, what design decisions in Postgres make SELECT DISTINCT slow, and how to work around it.

Finding Unique Partitions with SELECT DISTINCT

The workload where we observed the slowdown was in partitioned Postgres-backed queues. Each queue is divided into partitions (for example, one per user) so flow control can be applied independently to each partition (for example, allowing each user to run at most one task at a time). The first step in dequeueing workflows from a partitioned queue is to find all “active” partitions, meaning partitions with an ENQUEUED workflow on them. We originally used `SELECT DISTINCT` to do this:

!Image 26

What SELECT DISTINCT does is find all unique values of a column given some condition. So this query finds all unique non-NULL partition keys among ENQUEUED workflows on a particular queue. We expected this query to be fast because it’s properly indexed using an index on queue name, workflow status, and partition key:

!Image 27

Intuitively, the index looks like this. Workflows are laid out in a tree structure first by queue name, then by status, then by partition key. This allows Postgres to efficiently locate workflows using all three fields.

!Image 28: Postgres SELECT DISTINCT query plan diagram

Because the index has this shape, we expected the performance of this query to be O(number of active partitions). After all, to satisfy this query Postgres only needs to seek a single row from each unique partition, then return the partition keys it found.

Initially, this appeared to be working as intended. Most of our queue workloads were “wide but shallow” with many partitions but few enqueued workflows per partition. For those, the query performed as expected. However, we soon encountered issues with “narrow but deep” workloads where there were few partitions, but they each contained many enqueued workflows. We expected the query to finish in under a millisecond because there were so few partitions, but instead it took seconds. We quickly realized this meant the query was scaling not with the number of active partitions, but with the total number of enqueued workflows, making it unacceptably slow.

We validated this observation with a benchmark fixing the number of partitions at 10 but scaling the number of rows per partition from 100 to 1M. As we can see, query latency scales linearly with the number of rows per partition.

!Image 29: Postgres SELECT DISTINCT performance benchmark

To understand why that was happening and how to fix it, we’ll have to examine how Postgres plans and executes this query.

The SELECT DISTINCT Query Plan

When we examined the query plan Postgres was using for the SELECT DISTINCT query, it looked like this (assuming 1M enqueued workflows across 3 partitions):

!Image 30

Essentially, Postgres is doing a full index scan: walking the index to retrieve every single enqueued workflow on a particular queue (in this case, 1M rows total) and checking if it contains a unique partition key. This explains the performance we saw: the reason run time scales with the total number of enqueued workflows is because Postgres is actually scanning every single enqueued workflow. This is supremely wasteful: in this example Postgres scanned 1M rows to find just three partition keys it could have directly retrieved from the index.

The Postgres query planner chooses this plan because it doesn’t have an alternative. Every operator implemented in Postgres for scanning an index performs a full index scan, retrieving all indexed values matching its predicates. Other relational databases do better: MySQL provides a “loose index scan” operator that retrieves only each unique value that satisfies its predicates.

Interestingly, Postgres 18 added something like a loose scan: a skip scan optimization that “skips” rows when searching a multicolumn index on a column other than its leftmost column. However, in our case, this still scans all rows that match its predicates, so it can’t be used to speed up SELECT DISTINCT. Separately, there was a significant attempt to add a loose index scan in 2018, but it was abandoned after four years of effort and maintainer churn.

Mitigating the Slowdown

Because the performance of SELECT DISTINCT scales with the size of the table and not the number of unique values it contains, it is not usable at scale. To efficiently count the number of unique values in a table, we instead need a workaround: a more elaborate query that effectively coerces Postgres into generating an efficient query plan.

!Image 31

This query is remarkably hard to read because it utilizes a recursive common table expression (CTE). To first approximation, this is a way to write imperative code in otherwise-declarative SQL. Essentially, this query evaluates as a loop whose first iteration finds the “smallest” partition key and whose subsequent iterations each find the “next” unique partition key after it. Here’s what that looks like:

!Image 32

Each loop iteration does a SELECT min() on a sorted index, so it only retrieves a single value instead of scanning the entire index. Therefore, because each loop iteration does fixed work and the total number of loop iterations is equal to the number of unique partitions, this query provides the O(number of partitions) performance we need.

To validate this performance, we benchmark the new query, fixing the number of partitions at 10 but varying the number of rows per partition from 1K to 1M. As we can see, median latency does not change no matter how large the partitions get:

!Image 33

Learn More

If you like building scalable, reliable systems, we’d love to hear from you. At DBOS, our goal is to make Postgres-backed durable execution as simple and performant as possible. Check it out:

- Quickstart:https://docs.dbos.dev/quickstart

- GitHub:https://github.com/dbos-inc

- Discord community: https://discord.gg/eMUHrvbu67

Insights

Recent articles

The latest in durable execution, AI workflows &more.

[](https://www.dbos.dev/blog/what-is-dbos-conductor)

!Image 34

DBOS Architecture

Aug 20, 2026

What is DBOS Conductor?

Alex Poliakov, head of DBOS Solutions Architecture. explains DBOS Conductor.

!Image 35

DBOS

[](https://www.dbos.dev/blog/postgres-select-distinct-does-not-scale)

!Image 36

Benchmarks

Aug 10, 2026

Postgres SELECT DISTINCT Does Not Scale

Postgres SELECT DISTINCT performs surprisingly poorly. We explain why and how we mitigated it.

!Image 37

Peter Kraft

[](https://www.dbos.dev/blog/postgres-listen-notify-scalability)

!Image 38

How To

Jul 24, 2026

Postgres LISTEN/NOTIFY Can Actually Scale

How we optimized Postgres LISTEN/NOTIFY-backed data streams at scale, achieving 60K writes per second with millisecond latency.

!Image 39

Peter Kraft

Back to insights

Postgres SELECT DISTINCT Does Not Scale

Peter Kraft

August 10, 2026

Benchmarks

!Image 40

SELECT DISTINCT is an innocuous-seeming clause that finds all unique values of a column. However, it has a dark secret: no matter how you index your table, no matter how few unique values there are to retrieve, SELECT DISTINCT will always scan every row that matches its predicates. We recently observed this when diagnosing the performance of a Postgres-backed queues workload, where SELECT DISTINCT turned out to be the most expensive query despite appearing to be the simplest and cheapest. In this blog post, we’ll explain what happened, what design decisions in Postgres make SELECT DISTINCT slow, and how to work around it.

Finding Unique Partitions with SELECT DISTINCT

The workload where we observed the slowdown was in partitioned Postgres-backed queues. Each queue is divided into partitions (for example, one per user) so flow control can be applied independently to each partition (for example, allowing each user to run at most one task at a time). The first step in dequeueing workflows from a partitioned queue is to find all “active” partitions, meaning partitions with an ENQUEUED workflow on them. We originally used `SELECT DISTINCT` to do this:

!Image 41

What SELECT DISTINCT does is find all unique values of a column given some condition. So this query finds all unique non-NULL partition keys among ENQUEUED workflows on a particular queue. We expected this query to be fast because it’s properly indexed using an index on queue name, workflow status, and partition key:

!Image 42

Intuitively, the index looks like this. Workflows are laid out in a tree structure first by queue name, then by status, then by partition key. This allows Postgres to efficiently locate workflows using all three fields.

!Image 43: Postgres SELECT DISTINCT query plan diagram

Because the index has this shape, we expected the performance of this query to be O(number of active partitions). After all, to satisfy this query Postgres only needs to seek a single row from each unique partition, then return the partition keys it found.

Initially, this appeared to be working as intended. Most of our queue workloads were “wide but shallow” with many partitions but few enqueued workflows per partition. For those, the query performed as expected. However, we soon encountered issues with “narrow but deep” workloads where there were few partitions, but they each contained many enqueued workflows. We expected the query to finish in under a millisecond because there were so few partitions, but instead it took seconds. We quickly realized this meant the query was scaling not with the number of active partitions, but with the total number of enqueued workflows, making it unacceptably slow.

We validated this observation with a benchmark fixing the number of partitions at 10 but scaling the number of rows per partition from 100 to 1M. As we can see, query latency scales linearly with the number of rows per partition.

!Image 44: Postgres SELECT DISTINCT performance benchmark

To understand why that was happening and how to fix it, we’ll have to examine how Postgres plans and executes this query.

The SELECT DISTINCT Query Plan

When we examined the query plan Postgres was using for the SELECT DISTINCT query, it looked like this (assuming 1M enqueued workflows across 3 partitions):

!Image 45

Essentially, Postgres is doing a full index scan: walking the index to retrieve every single enqueued workflow on a particular queue (in this case, 1M rows total) and checking if it contains a unique partition key. This explains the performance we saw: the reason run time scales with the total number of enqueued workflows is because Postgres is actually scanning every single enqueued workflow. This is supremely wasteful: in this example Postgres scanned 1M rows to find just three partition keys it could have directly retrieved from the index.

The Postgres query planner chooses this plan because it doesn’t have an alternative. Every operator implemented in Postgres for scanning an index performs a full index scan, retrieving all indexed values matching its predicates. Other relational databases do better: MySQL provides a “loose index scan” operator that retrieves only each unique value that satisfies its predicates.

Interestingly, Postgres 18 added something like a loose scan: a skip scan optimization that “skips” rows when searching a multicolumn index on a column other than its leftmost column. However, in our case, this still scans all rows that match its predicates, so it can’t be used to speed up SELECT DISTINCT. Separately, there was a significant attempt to add a loose index scan in 2018, but it was abandoned after four years of effort and maintainer churn.

Mitigating the Slowdown

Because the performance of SELECT DISTINCT scales with the size of the table and not the number of unique values it contains, it is not usable at scale. To efficiently count the number of unique values in a table, we instead need a workaround: a more elaborate query that effectively coerces Postgres into generating an efficient query plan.

!Image 46

This query is remarkably hard to read because it utilizes a recursive common table expression (CTE). To first approximation, this is a way to write imperative code in otherwise-declarative SQL. Essentially, this query evaluates as a loop whose first iteration finds the “smallest” partition key and whose subsequent iterations each find the “next” unique partition key after it. Here’s what that looks like:

!Image 47

Each loop iteration does a SELECT min() on a sorted index, so it only retrieves a single value instead of scanning the entire index. Therefore, because each loop iteration does fixed work and the total number of loop iterations is equal to the number of unique partitions, this query provides the O(number of partitions) performance we need.

To validate this performance, we benchmark the new query, fixing the number of partitions at 10 but varying the number of rows per partition from 1K to 1M. As we can see, median latency does not change no matter how large the partitions get:

!Image 48

Learn More

If you like building scalable, reliable systems, we’d love to hear from you. At DBOS, our goal is to make Postgres-backed durable execution as simple and performant as possible. Check it out:

- Quickstart:https://docs.dbos.dev/quickstart

- GitHub:https://github.com/dbos-inc

- Discord community: https://discord.gg/eMUHrvbu67

Share this post

[](https://www.linkedin.com/shareArticle?mini=true&url=https://www.dbos.dev/blog/postgres-select-distinct-does-not-scale&title=Postgres%20SELECT%20DISTINCT%20Does%20Not%20Scale)

[](https://twitter.com/intent/tweet?url=https://www.dbos.dev/blog/postgres-select-distinct-does-not-scale&hashtags=dbos)

[](https://www.facebook.com/sharer/sharer.php?u=https://www.dbos.dev/blog/postgres-select-distinct-does-not-scale)

[](mailto:?subject=Check%20out%20this%20article%20from%20DBOS&body=Check%20out%20this%20article%20from%20DBOS%3A%0Ahttps://www.dbos.dev/blog/postgres-select-distinct-does-not-scale)

!Image 49: DBOS - Logo

DBOS radically simplifies cloud application devops and deployment.

[](https://www.linkedin.com/company/dbos-inc/)[](https://github.com/dbos-inc)[](https://discord.com/invite/jsmC6pXGgX)[](https://twitter.com/DBOS_Inc)[](https://www.dbos.dev/contact)

Products

DBOS CloudDBOS TransactPricing PlansContact Us

Solutions

Cron Job PlatformDurable AI WorkflowsDurable Data PipelinesCloud Modernization

Developers

DocsQuickstart GuideExamplesTutorials

Company

About UsPrivacy PolicyTerms of ServiceCookies

Copyright © DBOS, Inc. 2025

Build your reliable backend.

Effortlessly.

Contact sales

Start your project

Get started with DBOS

Use the open source DBOS Transact library, free forever. Pair it with DBOS Pro for premium tooling and support.

Start for free

Explore plans

Subscribe to DBOS Insights

Monthly updates on durable workflow execution and observability.

By subscribing you agree to with our Privacy Policy

You've successfully subscribed!

Oops! Please check your email and try again.

Products

DBOS TransactDBOS ConductorPricing PlansDBOS vs. Temporal

Use Cases

Cron Job PlatformDurable AI WorkflowsDurable Data PipelinesCloud Modernization

Customer Stories

Bristol Myers SquibbYutoriDosuAll stories

Developers

DocsQuickstart GuideExamplesTutorialsLLMs.txt

Company

Contact SalesBlogAbout UsOur TeamPartnersCareers

![Image 50: DBOS - Logo](https://www.dbos.dev/)

© 2026 DBOS, Inc. All rights reserved.

Privacy PolicyTerms of ServiceCookies Settings

[](https://discord.com/invite/jsmC6pXGgX)[](https://github.com/dbos-inc)[](https://twitter.com/DBOS_Inc)[](https://www.linkedin.com/company/dbos-inc/)[](https://www.youtube.com/@DBOS-Inc)