{"templateName":"blog-page","canonicalLink":"https://www.snowflake.com/en/blog/engineering/postgres-count-distinct-approximation/","robotsTags":["index","follow"],"cssClassNames":"blog-page page basicpage summit-page","allowedRenditionsWidth":["320","480","640","768","960","1200","1440","1920"],"language":"en","description":"Learn how to replace slow Postgres COUNT(DISTINCT) queries using TABLESAMPLE, HyperLogLog, and Apache DataSketches for sub-millisecond analytics.","title":"Postgres COUNT(DISTINCT) Too Slow? Fast Approximation Guide","analyticsPageType":"homepage","analyticsCategory":"general","analyticsSubCategory":"","excludeFromAnalytics":false,":mappedPath":"/en/blog/engineering/postgres-count-distinct-approximation/",":type":"snowflake-site/components/structure/page","analyticsDebugMode":false,"analyticsData":{"excludeFromAnalytics":false,"subCategory":"","pageType":"homepage","templateName":"blog-page","siteName":"snowflake","pageUrl":"/content/snowflake-site/global/en/blog/engineering/postgres-count-distinct-approximation","language":"en","category":"general","pageName":"Your COUNT(DISTINCT) Is Too Slow: Approximations and Sampling in Postgres","contentTags":["snowflake-site:taxonomy/blog/engineering-blog/open-source"]},":items":{"root":{"columnCount":12,"columnClassNames":{"experiencefragment-banner":"aem-GridColumn aem-GridColumn--default--12","experiencefragment-sub-header":"aem-GridColumn aem-GridColumn--default--12","experiencefragment-pre-footer":"aem-GridColumn aem-GridColumn--default--12","experiencefragment-header":"aem-GridColumn aem-GridColumn--default--12","markup_editor-table":"aem-GridColumn aem-GridColumn--default--12","responsivegrid":"aem-GridColumn aem-GridColumn--default--12","experiencefragment-footer":"aem-GridColumn aem-GridColumn--default--12","markup_editor":"aem-GridColumn aem-GridColumn--default--12","container_47873732":"aem-GridColumn aem-GridColumn--default--12"},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12",":items":{"experiencefragment-banner":{"id":"experiencefragment-64f367c07e","localizedFragmentVariationPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/pushdown-banner/pushdown-banner-blank/jcr:content","configured":true,":type":"snowflake-site/components/experiencefragment","xfModelPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/pushdown-banner/pushdown-banner-blank.xfmodel.json"},"experiencefragment-header":{"id":"experiencefragment-e9fde393fb","localizedFragmentVariationPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/mega-nav-header/master/jcr:content","configured":true,":type":"snowflake-site/components/experiencefragment","languageNavPath":"/content/snowflake-site/global/en/blog/engineering/postgres-count-distinct-approximation.languagenav.json","xfModelPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/mega-nav-header/master.xfmodel.json","appliedCssClassNames":"snowflake-sticky-nav-host"},"experiencefragment-sub-header":{"id":"experiencefragment-c95862fe9e","localizedFragmentVariationPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/sub-navigation/engineering-blog-sub-nav/jcr:content","configured":true,":type":"snowflake-site/components/experiencefragment","xfModelPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/sub-navigation/engineering-blog-sub-nav.xfmodel.json"},"responsivegrid":{"columnCount":12,"columnClassNames":{"container_breadcrumb":"aem-GridColumn aem-GridColumn--default--12","container_main_content":"aem-GridColumn aem-GridColumn--default--12"},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12",":items":{"container_breadcrumb":{"layout":"RESPONSIVE_GRID","columnCount":12,"columnClassNames":{"breadcrumb":"aem-GridColumn aem-GridColumn--default--12"},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12","id":"blog-page-breadcrumb-indentation",":type":"snowflake-site/components/container",":items":{"breadcrumb":{"id":"breadcrumb-125895edd6","breadcrumbItems":[{"title":"Blog","path":"/en/blog/engineering/","active":false},{"title":"Open Source","path":"/en/blog/engineering/open-source/","active":false},{"title":"Your COUNT(DISTINCT) Is Too Slow: Approximations and Sampling in Postgres","path":"/en/blog/engineering/postgres-count-distinct-approximation/","active":false}],":type":"snowflake-site/components/blog/breadcrumb"}},":itemsOrder":["breadcrumb"],"appliedCssClassNames":"snowflake-container"},"container_main_content":{"layout":"RESPONSIVE_GRID","columnCount":12,"columnClassNames":{"flexible_column_container":"aem-GridColumn aem-GridColumn--default--12","related_content":"aem-GridColumn aem-GridColumn--default--12"},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12","id":"main-content",":type":"snowflake-site/components/container",":items":{"flexible_column_container":{"id":"flexible-column-container-6398789130","propertiesId":"snowflake-blog-template-main-container","type":"2-column-60-40","alignColumns":"top","containerMaxWidth":"extra-large","topPadding":"none","bottomPadding":"none","spaceBetween":"none","reverseOnMobile":true,"carouselOnMobile":false,"backgroundImageOption":"none","flexible_column_content_container_1":{"layout":"SIMPLE","id":"container-9781f7f39b",":type":"snowflake-site/components/flexible-column-container/flexible-column-content-container",":items":{"container_hero":{"layout":"RESPONSIVE_GRID","columnCount":12,"columnClassNames":{"blog_hero":"aem-GridColumn aem-GridColumn--default--12"},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12","id":"container-4f1211234a",":type":"snowflake-site/components/container",":items":{"blog_hero":{"id":"blog-hero-96483e29be","linkedInShareUrl":"https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Fwww.snowflake.com%2Fcontent%2Fsnowflake-site%2Fglobal%2Fen%2Fblog%2Fengineering%2Fpostgres-count-distinct-approximation&title=Your+COUNT%28DISTINCT%29+Is+Too+Slow%3A+Approximations+and+Sampling+in+Postgres","twitterShareUrl":"https://x.com/intent/post?url=https%3A%2F%2Fwww.snowflake.com%2Fcontent%2Fsnowflake-site%2Fglobal%2Fen%2Fblog%2Fengineering%2Fpostgres-count-distinct-approximation&text=Your+COUNT%28DISTINCT%29+Is+Too+Slow%3A+Approximations+and+Sampling+in+Postgres","facebookShareUrl":"https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fwww.snowflake.com%2Fcontent%2Fsnowflake-site%2Fglobal%2Fen%2Fblog%2Fengineering%2Fpostgres-count-distinct-approximation","showClaude":true,"showChatGpt":true,"authors":[{"authorImage":{"id":"image-e7f862dfa8","height":"675","isLcpImage":false,"src":"https://www.snowflake.com/adobe/dynamicmedia/deliver/dm-aid--8e4d14e7-49fc-4d3a-9899-8848b9f2bb1d/echristensen.png?preferwebp=true&quality=85","lazyEnabled":true,"width":"675",":type":"snowflake-site/components/image"},"authorCta":{"id":"button-0f9f832469","showOutboundIcon":false,"buttonLink":{"valid":true,"url":"/en/blog/authors/elizabeth-garrett-christensen/"},"linkTargetContentType":"DOCUMENT_LEARN",":type":"snowflake-site/components/button","linkType":"SNOWFLAKE_INTERNAL","text":"Elizabeth Garrett Christensen"}}],"image":{"id":"image-e9265f9daa","height":"720","isLcpImage":false,"src":"https://www.snowflake.com/adobe/dynamicmedia/deliver/dm-aid--8356a77b-79b0-4938-b4c2-dc928b4ace8d/sf-eng-blog-ml-4-1.png?preferwebp=true&quality=85","lazyEnabled":true,"width":"1680",":type":"snowflake-site/components/image"},"timeToRead":"16","publicationDate":"JUL 31, 2026","tag":{"tagText":"Open Source","tagColor":"#29B5E8"},"title":{"lines":["Your COUNT(DISTINCT) Is Too Slow: Approximations and Sampling in Postgres"],"type":"heading2",":type":"snowflake-site/components/title-v2"},":type":"snowflake-site/components/blog/blog-hero"}},":itemsOrder":["blog_hero"]},"responsivegrid_content":{"columnCount":12,"columnClassNames":{"code_snippet_547472791":"aem-GridColumn aem-GridColumn--default--12","code_snippet_164400665":"aem-GridColumn aem-GridColumn--default--12","code_snippet_1652045645":"aem-GridColumn aem-GridColumn--default--12","blog_text_1390155145":"aem-GridColumn aem-GridColumn--default--12","code_snippet_1757267569":"aem-GridColumn aem-GridColumn--default--12","blog_text_1590427254":"aem-GridColumn aem-GridColumn--default--12","blog_text_1608570159":"aem-GridColumn aem-GridColumn--default--12","blog_text_205358017":"aem-GridColumn aem-GridColumn--default--12","blog_text":"aem-GridColumn aem-GridColumn--default--12","blog_text_820253149":"aem-GridColumn aem-GridColumn--default--12","blog_text_180620154":"aem-GridColumn aem-GridColumn--default--12","blog_text_1014003239":"aem-GridColumn aem-GridColumn--default--12","blog_text_1704642617":"aem-GridColumn aem-GridColumn--default--12","code_snippet_355572341":"aem-GridColumn aem-GridColumn--default--12","code_snippet_1274423142":"aem-GridColumn aem-GridColumn--default--12","blog_text_155102234":"aem-GridColumn aem-GridColumn--default--12","blog_text_226390574":"aem-GridColumn aem-GridColumn--default--12","blog_text_824961338":"aem-GridColumn aem-GridColumn--default--12","code_snippet_1632883888":"aem-GridColumn aem-GridColumn--default--12","code_snippet_640135930":"aem-GridColumn aem-GridColumn--default--12","blog_text_744703937":"aem-GridColumn aem-GridColumn--default--12","blog_text_1368028614":"aem-GridColumn aem-GridColumn--default--12","blog_text_2006340389":"aem-GridColumn aem-GridColumn--default--12","code_snippet_882258116":"aem-GridColumn aem-GridColumn--default--12","blog_text_1984430024":"aem-GridColumn aem-GridColumn--default--12","blog_text_1452300736":"aem-GridColumn aem-GridColumn--default--12","blog_text_1563551537":"aem-GridColumn aem-GridColumn--default--12","code_snippet_157475885":"aem-GridColumn aem-GridColumn--default--12","blog_text_1883468919":"aem-GridColumn aem-GridColumn--default--12","blog_text_892720800":"aem-GridColumn aem-GridColumn--default--12","blog_text_1140811835":"aem-GridColumn aem-GridColumn--default--12","blog_text_1039412481":"aem-GridColumn aem-GridColumn--default--12","blog_text_811621137":"aem-GridColumn aem-GridColumn--default--12","blog_text_795054801":"aem-GridColumn aem-GridColumn--default--12","code_snippet_416839381":"aem-GridColumn aem-GridColumn--default--12","code_snippet_1468592557":"aem-GridColumn aem-GridColumn--default--12","code_snippet_312769523":"aem-GridColumn aem-GridColumn--default--12","code_snippet_780365281":"aem-GridColumn aem-GridColumn--default--12","blog_text_1380109897":"aem-GridColumn aem-GridColumn--default--12","code_snippet_553256594":"aem-GridColumn aem-GridColumn--default--12","code_snippet_183700051":"aem-GridColumn aem-GridColumn--default--12","code_snippet_1697379323":"aem-GridColumn aem-GridColumn--default--12","blog_text_390676450":"aem-GridColumn aem-GridColumn--default--12","code_snippet_285538223":"aem-GridColumn aem-GridColumn--default--12","blog_text_258456775":"aem-GridColumn aem-GridColumn--default--12","blog_text_1795912919":"aem-GridColumn aem-GridColumn--default--12","blog_text_1492962835":"aem-GridColumn aem-GridColumn--default--12","blog_text_1490876437":"aem-GridColumn aem-GridColumn--default--12","blog_text_1237104672":"aem-GridColumn aem-GridColumn--default--12","blog_text_2138710847":"aem-GridColumn aem-GridColumn--default--12","code_snippet_1314625764":"aem-GridColumn aem-GridColumn--default--12","code_snippet_1682867120":"aem-GridColumn aem-GridColumn--default--12","code_snippet":"aem-GridColumn aem-GridColumn--default--12","code_snippet_304759495":"aem-GridColumn aem-GridColumn--default--12","blog_text_2005723149":"aem-GridColumn aem-GridColumn--default--12","code_snippet_87021746":"aem-GridColumn aem-GridColumn--default--12","blog_text_507496595":"aem-GridColumn aem-GridColumn--default--12","code_snippet_1299592688":"aem-GridColumn aem-GridColumn--default--12","code_snippet_246204236":"aem-GridColumn aem-GridColumn--default--12"},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12","appliedCssClassNames":"snowflake-layout-container-inner-padding-small",":items":{"blog_text":{"id":"blog-text-6a067919c5","text":"\u003Cp\u003EI'm very impatient. I don't always care about the exact right answer. When I'm checking how many unique visitors hit a page, I don't need 497,536 — &quot;about 497,000&quot; is fine. If getting that answer takes 3 milliseconds instead of a second, I'll take the trade every time.\u003C/p\u003E\r\n\u003Cp\u003EPostgres has several features for approximation and sampling that are worth knowing about, especially if you're working with huge data sets. Approximation options in Postgres range from built-in table sampling to algorithms for probabilistic data structures. This post walks through all of them using the same test data so you can compare and see when each one makes sense.\u003C/p\u003E\r\n\u003Ch4\u003EFollow along with a sample web analytics table\u003C/h4\u003E\r\n\u003Cp\u003EEverything in this post uses the same 10 million row test table. You can generate it yourself on Postgres 17 or 18+ with the \u003Ccode\u003Ehll\u003C/code\u003E and \u003Ccode\u003Edatasketches\u003C/code\u003E extensions installed. The benchmarks here were run on Postgres 18, and an Apple M-series machine with \u003Ccode\u003Eshared_buffers = 256MB\u003C/code\u003E, \u003Ccode\u003Ework_mem = 64MB\u003C/code\u003E, and \u003Ccode\u003Emax_parallel_workers_per_gather = 2\u003C/code\u003E (the Postgres default) — a pretty standard laptop setup. Parallel scans are enabled for all queries equally. I also made \u003Ca href=\"https://gist.github.com/sfc-gh-echristensen/21295b1c50ed35aa4580d05e01895f1b\" target=\"_blank\" rel=\"noopener noreferrer\"\u003Ea companion SQL gist\u003C/a\u003E with additional details. I think &quot;too much SQL&quot; wins me some kind of Snowflake employee award.\u003C/p\u003E\r\n\u003Cp\u003EWe're simulating a web analytics table, the kind of data you'd have from a product analytics tracker, or really any event-driven application. Each row here is a single page view with information about who visited, where they came from, and how fast the page loaded.\u003C/p\u003E\r\n\u003Cp\u003EI built this test data set specifically so it has properties that exercise each approximation technique.\u003C/p\u003E\r\n\u003Cul\u003E\r\n\u003Cli\u003EThere are lots of distinct users for testing distinct counting\u003C/li\u003E\r\n\u003Cli\u003EA few categorical columns like traffic channel for set operations (that is, comparing overlapping groups)\u003C/li\u003E\r\n\u003Cli\u003EA numeric response time column with a skewed distribution for testing percentile calculations\u003C/li\u003E\r\n\u003C/ul\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet":{"id":"code-snippet-2acbdd035d","language":"sql","codeSnippet":"CREATE TABLE page_views (\r\n    id            bigint GENERATED ALWAYS AS IDENTITY,\r\n    event_time    timestamptz NOT NULL,       -- spread over 90 days\r\n    user_id       int NOT NULL,               -- 500K distinct users, power-law activity\r\n    session_id    bigint NOT NULL,            -- ~5 events per session\r\n    channel       text NOT NULL,              -- 5 traffic sources: direct, google, facebook, email, tiktok\r\n    region        text NOT NULL,              -- 8 geographic regions\r\n    page_path     text NOT NULL,              -- ~10K distinct URLs, Zipfian (some pages much hotter)\r\n    response_ms   numeric(8,2) NOT NULL,      -- log-normal: median ~120ms, p99 ~1s, long tail to ~5s\r\n    cost_cents    int                         -- ad spend per click; NULL for organic channels\r\n);","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_1452300736":{"id":"blog-text-b82fd1abf4","text":"\u003Cp\u003E\u003Cb\u003EGenerating 10 million rows\u003C/b\u003E\u003C/p\u003E\r\n\u003Cp\u003EThis \u003Ccode\u003EINSERT\u003C/code\u003E uses \u003Ccode\u003Egenerate_series\u003C/code\u003E and random functions to fill the table which takes about 90 seconds on my laptop.\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_304759495":{"id":"code-snippet-4f3cd23e0e","language":"sql","codeSnippet":"INSERT INTO page_views (event_time, user_id, session_id, channel, region, page_path, response_ms, cost_cents)\r\nSELECT\r\n    now() - (random() * 90)::int * interval '1 day'\r\n          - (random() * 86400) * interval '1 second',\r\n    least(floor(power(random(), 1.5) * 500000)::int + 1, 500000),\r\n    floor(i / 5.0)::bigint + floor(random() * 1000000)::bigint,\r\n    (ARRAY['direct','google','facebook','email','tiktok'])[\r\n        CASE WHEN r \u003C 0.35 THEN 1 WHEN r \u003C 0.60 THEN 2\r\n             WHEN r \u003C 0.80 THEN 3 WHEN r \u003C 0.92 THEN 4 ELSE 5 END\r\n    ],\r\n    (ARRAY['us_east','us_west','eu_west','eu_central','apac_east','apac_south','latam','africa'])[\r\n        CASE WHEN r2 \u003C 0.25 THEN 1 WHEN r2 \u003C 0.45 THEN 2 WHEN r2 \u003C 0.60 THEN 3\r\n             WHEN r2 \u003C 0.72 THEN 4 WHEN r2 \u003C 0.82 THEN 5 WHEN r2 \u003C 0.90 THEN 6\r\n             WHEN r2 \u003C 0.96 THEN 7 ELSE 8 END\r\n    ],\r\n    '/' || (ARRAY['blog','docs','pricing','product','about','api','demo','support'])[\r\n        floor(random() * 8)::int + 1\r\n    ] || '/page-' || floor(power(random(), 2) * 1250)::int,\r\n    round(exp(4.8 + sqrt(-2.0 * ln(greatest(random(), 1e-10))) * cos(2.0 * pi() * random()) * 0.9)::numeric, 2),\r\n    CASE WHEN r \u003E= 0.35 AND r \u003C 0.92 THEN floor(random() * 496)::int + 5 ELSE NULL END\r\nFROM (\r\n    SELECT i, random() AS r, random() AS r2\r\n    FROM generate_series(1, 10000000) AS g(i)\r\n) sub;\r\n\r\nANALYZE page_views;","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_811621137":{"id":"blog-text-465621c8db","text":"\u003Cp\u003EThe table ends up at about 1.4 GB on disk with 500,000 distinct users. The distribution should be somewhat realistic — a few power users generate way more traffic than everyone else, a handful of pages get most of the views, and response times have a long tail where most pages load fast but some are much slower. This makes it a good test for everything we'll cover.\u003C/p\u003E\r\n\u003Ch2\u003EThe problem: Exact is expensive\u003C/h2\u003E\r\n\u003Cp\u003EExact aggregates on big tables are slow in two different ways, and knowing which kind of slow you're dealing with determines which tool to reach for.\u003C/p\u003E\r\n\u003Cp\u003E\u003Cb\u003EAggregate stats\u003C/b\u003E — averages, sums, percentiles — are expensive because they have to read every row:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_1757267569":{"id":"code-snippet-4e7e27a2b6","language":"sql","codeSnippet":"SELECT avg(response_ms) FROM page_views;\r\n-- Time: 352 ms\r\n\r\nSELECT percentile_cont(0.99) WITHIN GROUP (ORDER BY response_ms) FROM page_views;\r\n--  986.39\r\n-- Time: 2,617 ms","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_795054801":{"id":"blog-text-7e8b130a42","text":"\u003Cp\u003E\u003Ccode\u003Epercentile_cont\u003C/code\u003E needs to sort all 10 million response time values to find the 99th percentile. Even with 64MB of \u003Ccode\u003Ework_mem\u003C/code\u003E and parallel workers, that's 2.6 seconds. Drop \u003Ccode\u003Ework_mem\u003C/code\u003E to 4MB (a common default) and it gets much worse — the sort spills to disk with over 200 MB of temp file I/O just to answer one question.\u003C/p\u003E\r\n\u003Cp\u003EThese queries are fine when you're doing a one-off analysis. They're probably not fine for dashboards that refresh every few seconds, or for tables that are 10 or 100 times larger than our test set. The rest of this post covers the tools that fix each case — starting with the simplest.\u003C/p\u003E\r\n\u003Ch2\u003ETABLESAMPLE: zero-setup sampling\u003C/h2\u003E\r\n\u003Cp\u003EFor aggregate stats like averages and percentiles, you have a super simple option: just read less data. Postgres has a built-in \u003Ccode\u003ETABLESAMPLE\u003C/code\u003E clause that returns a random subset of your table without needing any extensions or setup.\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_285538223":{"id":"code-snippet-ddc176048f","language":"sql","codeSnippet":"SELECT avg(response_ms) FROM page_views TABLESAMPLE SYSTEM(10);\r\n-- Result: ~183   (actual: 182.25)\r\n-- Time: 111 ms\r\n-- Standard Postgres time: 352 ms","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_1490876437":{"id":"blog-text-2545c26b90","text":"\u003Cp\u003EIn our example, a 10% sample gives you a nearly identical average in about a third the time. Percentiles work too:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_164400665":{"id":"code-snippet-eb2aef90e1","language":"sql","codeSnippet":"SELECT percentile_cont(0.99) WITHIN GROUP (ORDER BY response_ms)\r\nFROM page_views TABLESAMPLE SYSTEM(10);\r\n-- Result: ~985   (actual: 986.39)\r\n-- Time: 250 ms\r\n-- Standard Postgres time: 2,617 ms","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_1984430024":{"id":"blog-text-493b7a57bf","text":"\u003Cp\u003EThe p99 estimate is within 0.3% of exact, and it ran 10x faster because Postgres only sorted ~1 million values instead of 10 million. You put \u003Ccode\u003ETABLESAMPLE\u003C/code\u003E right after the table name and give it a percentage.\u003C/p\u003E\r\n\u003Cp\u003ETABLESAMPLE has two sampling methods you can choose from.\u003C/p\u003E\r\n\u003Cp\u003E\u003Cb\u003ESYSTEM\u003C/b\u003E samples at the page level. Postgres data is stored in 8KB pages on disk, and SYSTEM randomly includes or excludes entire pages. This is fast because it can skip whole chunks of the table without reading them. The downside is that if your data is physically clustered, you will get a biased sample, and there's no way to ensure it is correctly distributed.\u003C/p\u003E\r\n\u003Cp\u003E\u003Cb\u003EBERNOULLI\u003C/b\u003E evaluates each row individually with a random number generator. This gives you a more evenly distributed sample but it's slower because it can't skip pages. The method is named after Jacob Bernoulli, the 17th-century Swiss mathematician who developed some of the math for probability.\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_1652045645":{"id":"code-snippet-fcf4731130","language":"sql","codeSnippet":"-- SYSTEM: reads ~10% of pages, very fast\r\nSELECT count(DISTINCT user_id) FROM page_views TABLESAMPLE SYSTEM(10);\r\n-- Time: 217 ms\r\n-- Standard Postgres time: 671 ms\r\n\r\n-- BERNOULLI: evaluates every row, keeps ~10%\r\nSELECT count(DISTINCT user_id) FROM page_views TABLESAMPLE BERNOULLI(10);\r\n-- Time: 404 ms\r\n-- Standard Postgres time: 671 ms","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_1140811835":{"id":"blog-text-49057372e7","text":"\u003Ch3\u003EWhere TABLESAMPLE falls down\u003C/h3\u003E\r\n\u003Cp\u003ESampling can be a good fix for quicker aggregates. But what about distinct counts? We all know how slow full distinct counts are on huge tables.\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_1697379323":{"id":"code-snippet-de1ddc09ea","language":"sql","codeSnippet":"SELECT count(DISTINCT user_id) * 10 FROM page_views TABLESAMPLE SYSTEM(10);\r\n-- Result: 4,150,040   (actual: 500,000)\r\n-- Time: 217 ms\r\n-- Standard Postgres time: 671 ms","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_1795912919":{"id":"blog-text-067936a144","text":"\u003Cp\u003EYou might think &quot;I sampled 10%, so multiply by 10 to get the full number.&quot; But this massively overcounts. Here's why: if a user appears in 20 rows across the full table, even a 10% sample is almost certain to catch at least one of those rows. So nearly all 500K users show up in your 10% sample. Multiplying by 10 gives you a number that's eight times too high.\u003C/p\u003E\r\n\u003Cp\u003ETABLESAMPLE is best for: averages, sums, ballpark histograms, spot-checking your data during development, or as a quick smoke test. It may have some clustered data bias. If you need accurate distinct counts, you need a different approach.\u003C/p\u003E\r\n\u003Ch2\u003EHyperLogLog: streaming distinct counts\u003C/h2\u003E\r\n\u003Cp\u003EHyperLogLog (HLL) is a probabilistic algorithm that's been around since 2007. The core idea is simple: instead of storing every unique value, it observes patterns in the binary representation of hashed values to estimate how many distinct values it's seen.\u003C/p\u003E\r\n\u003Cp\u003EThe Postgres \u003Ccode\u003Ehll\u003C/code\u003E extension managed by Citus Data implements HLL as a native data type. When you use it, you'd typically store it as a column in a rollup table alongside your regular columns. It's a compact binary blob, so pretty space efficient. You can install it with:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_1299592688":{"id":"code-snippet-87b668bc04","language":"sql","codeSnippet":"CREATE EXTENSION IF NOT EXISTS hll;","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_1014003239":{"id":"blog-text-ff76cac7ea","text":"\u003Ch3\u003EBasic distinct count\u003C/h3\u003E\r\n\u003Cp\u003EHere's the simplest use — getting an approximate distinct count:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_1274423142":{"id":"code-snippet-ed7cefa1f2","language":"sql","codeSnippet":"SELECT hll_cardinality(hll_add_agg(hll_hash_integer(user_id)))::int AS unique_users\r\nFROM page_views;\r\n-- Result: 497,209   (actual: 500,000, error: 0.6%)\r\n-- Time: 320 ms\r\n-- Standard Postgres time: 671 ms","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_1380109897":{"id":"blog-text-6f4ffc2cb0","text":"\u003Cp\u003EThat's 320 milliseconds vs 671 ms for exact \u003Ccode\u003ECOUNT(DISTINCT)\u003C/code\u003E which is about twice as fast on a single scan. Also HLL isn't just about raw speed on one query. The function names are a bit verbose but the pattern is always the same: hash the value, aggregate the hashes into an HLL, then ask for the cardinality.\u003C/p\u003E\r\n\u003Ch3\u003EPreaggregate for instant queries\u003C/h3\u003E\r\n\u003Cp\u003EThe real power of HLL is that sketches are mergeable. This is the key concept that makes them different from just &quot;a faster COUNT(DISTINCT).&quot; You can build a separate HLL sketch for each day, store those sketches in a table, and then union them together at query time to get distinct counts across any date range.\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_547472791":{"id":"code-snippet-bea04d22a3","language":"sql","codeSnippet":"-- Build daily HLL sketches (do this once a day in your pipeline)\r\nCREATE TABLE daily_hll AS\r\nSELECT\r\n    date_trunc('day', event_time)::date AS event_date,\r\n    hll_add_agg(hll_hash_integer(user_id)) AS users_hll\r\nFROM page_views\r\nGROUP BY 1;\r\n\r\n-- Unique users over any date range — instant, doesn't touch the raw table\r\nSELECT hll_cardinality(hll_union_agg(users_hll))::int AS unique_users_7d\r\nFROM daily_hll\r\nWHERE event_date \u003E= current_date - 7;\r\n-- Time: 0.1 ms\r\n-- Standard Postgres time: 315 ms","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_824961338":{"id":"blog-text-2e98edcbbc","text":"\u003Cp\u003EThe daily_hll table has one row per day. Each row is tiny — the HLL column in this example is about 1.3 KB per row regardless of how many users it represents. And querying this small rollup table instead of scanning millions of raw rows is what gets you from hundreds of milliseconds to sub-millisecond.\u003C/p\u003E\r\n\u003Cp\u003EHLL sketches work as window functions too, making rolling unique counts trivial:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_312769523":{"id":"code-snippet-689e3732a0","language":"sql","codeSnippet":"SELECT\r\n    event_date,\r\n    hll_cardinality(users_hll)::int AS daily_uniques,\r\n    hll_cardinality(\r\n        hll_union_agg(users_hll) OVER (\r\n            ORDER BY event_date\r\n            ROWS BETWEEN 6 PRECEDING AND CURRENT ROW\r\n        )\r\n    )::int AS rolling_7_day_uniques\r\nFROM daily_hll\r\nORDER BY event_date;\r\n-- Time: 4 ms (all 91 days)\r\n-- Standard Postgres time: 5,200 ms","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_2138710847":{"id":"blog-text-4cee30b605","text":"\u003Cp\u003ETry computing a rolling 7-day unique user count with exact SQL — you'd need a correlated subquery that re-scans raw data for every output row. That took 7 seconds for just 5 days in our test — at that rate it would be over 2 minutes for all 91 days. With HLL, it's just a window function over a tiny column, done in 4 milliseconds.\u003C/p\u003E\r\n\u003Ch2\u003EApache DataSketches: the full toolkit for probability\u003C/h2\u003E\r\n\u003Cp\u003E\u003Ca href=\"https://datasketches.apache.org/\" target=\"_blank\" rel=\"noopener noreferrer\"\u003EApache DataSketches\u003C/a\u003E is a library of streaming algorithms originally developed at Yahoo for analytics at web scale. &quot;Sketch&quot; is a general term for any compact probabilistic data structure that summarizes a stream of data. Think of it like a compressed fingerprint of your dataset that you can still ask questions about.\u003C/p\u003E\r\n\u003Cp\u003EThe Postgres extension brings several sketch types, each designed for a different kind of question:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_892720800":{"id":"blog-text-23494940a3","text":"\u003Ctable\u003E\r\n\u003Cthead\u003E\u003Ctr\u003E\u003Cth\u003ESketch\u003C/th\u003E\r\n\u003Cth\u003EQuestion it answers\u003C/th\u003E\r\n\u003C/tr\u003E\u003C/thead\u003E\u003Ctbody\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Cb\u003ECPC\u003C/b\u003E (Compressed Probabilistic Counting)\u003C/td\u003E\r\n\u003Ctd\u003EHow many distinct values?\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Cb\u003ETheta\u003C/b\u003E\u003C/td\u003E\r\n\u003Ctd\u003EHow many distinct values overlap?\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Cb\u003EKLL\u003C/b\u003E\u003C/td\u003E\r\n\u003Ctd\u003EWhat's the median? The p99? Show me a histogram.\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Cb\u003EFrequent Strings\u003C/b\u003E\u003C/td\u003E\r\n\u003Ctd\u003EWhat are the heaviest items by count?\u003C/td\u003E\r\n\u003C/tr\u003E\u003C/tbody\u003E\u003C/table\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_882258116":{"id":"code-snippet-910fcbdc3f","language":"sql","codeSnippet":"CREATE EXTENSION IF NOT EXISTS datasketches;","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_1590427254":{"id":"blog-text-b6071f7bfb","text":"\u003Ch3 style=\"text-align: left;\"\u003ECPC: compact distinct counting\u003C/h3\u003E\r\n\u003Cp\u003ECPC is the DataSketches answer to &quot;how many distinct values?&quot; — the same question HLL answers, but CPC sketches can be more compact when stored and are thought to have slightly better accuracy at the same memory budgets.\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_1314625764":{"id":"code-snippet-c910bdd310","language":"sql","codeSnippet":"SELECT cpc_sketch_get_estimate(cpc_sketch_build(user_id))::int AS approx_uniques\r\nFROM page_views;\r\n-- Result: 494,394   (actual: 500,000, error: 1.1%)\r\n-- Time: 389 ms\r\n-- Standard Postgres time: 671 ms","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_226390574":{"id":"blog-text-5602ce72be","text":"\u003Cp\u003EThe \u003Ccode\u003Ecpc_sketch_build\u003C/code\u003E function is an aggregate — it works like \u003Ccode\u003Ecount()\u003C/code\u003E or \u003Ccode\u003Esum()\u003C/code\u003E, collapsing all the rows into a single sketch value. That sketch is an opaque binary blob, and you use \u003Ccode\u003Ecpc_sketch_get_estimate\u003C/code\u003E to extract the cardinality from it.\u003C/p\u003E\r\n\u003Ch3\u003ETheta sketches: set operations on distinct counts\u003C/h3\u003E\r\n\u003Cp\u003EHere's where things get interesting and where DataSketches really separates itself from plain HLL. Theta sketches let you do set operations — intersection, union, difference — on distinct count sketches.\u003C/p\u003E\r\n\u003Cp\u003EHere's a question you simply cannot answer with HLL or \u003Ccode\u003ECOUNT(DISTINCT)\u003C/code\u003E: \u003Cb\u003E&quot;How many users visited via \u003Ci\u003Eboth\u003C/i\u003E Google and Facebook?&quot;\u003C/b\u003E\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_1468592557":{"id":"code-snippet-e1abd96930","language":"sql","codeSnippet":"WITH channel_sketches AS (\r\n    SELECT\r\n        channel,\r\n        theta_sketch_build(user_id) AS sketch\r\n    FROM page_views\r\n    WHERE channel IN ('google', 'facebook')\r\n    GROUP BY channel\r\n)\r\nSELECT\r\n    theta_sketch_get_estimate(\r\n        (SELECT sketch FROM channel_sketches WHERE channel = 'google')\r\n    )::int AS google_users,\r\n    theta_sketch_get_estimate(\r\n        (SELECT sketch FROM channel_sketches WHERE channel = 'facebook')\r\n    )::int AS facebook_users,\r\n    theta_sketch_get_estimate(\r\n        theta_sketch_intersection(\r\n            (SELECT sketch FROM channel_sketches WHERE channel = 'google'),\r\n            (SELECT sketch FROM channel_sketches WHERE channel = 'facebook')\r\n        )\r\n    )::int AS users_on_both;\r\n-- google_users: 489,695 | facebook_users: 482,491 | users_on_both: 474,272\r\n-- Time: 382 ms\r\n-- Standard Postgres time: 2,499 ms (self-join on raw table)","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_1390155145":{"id":"blog-text-4344901550","text":"\u003Cp\u003EWith pre-aggregated \u003Ccode\u003ECOUNT(DISTINCT)\u003C/code\u003E rollups, answering overlap questions like this is impossible — you'd have to go back to the raw event table and do a self-join. With Theta sketches, you just intersect two pre-built sketches directly.\u003C/p\u003E\r\n\u003Cp\u003EThere's also \u003Ccode\u003Etheta_sketch_a_not_b\u003C/code\u003E for exclusive reach (&quot;Google users who did NOT visit via Facebook&quot;) and you can compute a full pairwise overlap matrix across all channels.\u003C/p\u003E\r\n\u003Ch3\u003EKLL sketches: quantiles without sorting\u003C/h3\u003E\r\n\u003Cp\u003E&quot;What's the p99 response time?&quot; is a question that normally requires Postgres to sort your entire dataset. KLL sketches (named after their inventors Karnin, Lang, and Liberty, who published the \u003Ca href=\"https://arxiv.org/abs/1603.05346\" target=\"_blank\" rel=\"noopener noreferrer\"\u003Ealgorithm in 2016\u003C/a\u003E) maintain a compact summary that can answer quantile, rank, and histogram questions without ever sorting.\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_1632883888":{"id":"code-snippet-ee94db7275","language":"sql","codeSnippet":"SELECT\r\n    kll_float_sketch_get_quantile(kll_float_sketch_build(response_ms::real), 0.50)::numeric(8,2) AS p50,\r\n    kll_float_sketch_get_quantile(kll_float_sketch_build(response_ms::real), 0.90)::numeric(8,2) AS p90,\r\n    kll_float_sketch_get_quantile(kll_float_sketch_build(response_ms::real), 0.99)::numeric(8,2) AS p99\r\nFROM page_views;\r\n-- p50: 121.80 | p90: 390.01 | p99: 1056.67\r\n-- Time: 625 ms\r\n-- Standard Postgres time: 2,617 ms","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_744703937":{"id":"blog-text-f7d69fa92a","text":"\u003Cp\u003ECompared to the exact values in this example (p50: 121.55, p90: 385.23, p99: 986.39), KLL is within about 7% on the tails. For monitoring and dashboards where you're looking at trends and alerting on big changes, this is more than accurate enough. And it ran four times faster.\u003C/p\u003E\r\n\u003Cp\u003EYou can also get full histograms with KLL using the probability mass function:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_157475885":{"id":"code-snippet-2c4416d8d5","language":"sql","codeSnippet":"SELECT\r\n    unnest(ARRAY['0-50ms','50-100ms','100-200ms','200-500ms','500-1000ms','1000ms+']) AS bucket,\r\n    round(unnest(\r\n        kll_float_sketch_get_pmf(\r\n            kll_float_sketch_build(response_ms::real),\r\n            ARRAY[50, 100, 200, 500, 1000]\r\n        )\r\n    )::numeric, 4) AS fraction\r\nFROM page_views;","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_1608570159":{"id":"blog-text-29a4f16054","text":"\u003Cp\u003ELike all the other sketch types, KLL is additive. Build one sketch per time bucket in your pipeline, then merge them at query time to get percentiles across any date range without resorting the original data.\u003C/p\u003E\r\n\u003Ch3\u003EFrequent Strings: most common values\u003C/h3\u003E\r\n\u003Cp\u003EThe last sketch type finds the most common values in a column:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_355572341":{"id":"code-snippet-07a66f8d6e","language":"sql","codeSnippet":"SELECT frequent_strings_sketch_result_no_false_negatives(\r\n    frequent_strings_sketch_build(7, page_path), 50000\r\n) FROM page_views;\r\n-- Returns the ~76 most-visited page paths with estimated counts\r\n-- Time: 560 ms\r\n-- Standard Postgres time: 529 ms (GROUP BY page_path ORDER BY count(*) DESC LIMIT 76)","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_390676450":{"id":"blog-text-ffd542adaf","text":"\u003Cp\u003EOn a single full-table scan, the sketch is actually slightly slower than a plain GROUP BY. The value shows up when you preaggregate — build one sketch per day, then merge across any date range without re-scanning millions of rows:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_553256594":{"id":"code-snippet-63085c8b6d","language":"sql","codeSnippet":"-- Pre-build daily sketches (do once per day in your pipeline)\r\nCREATE TABLE daily_freq_paths AS\r\nSELECT\r\n    date_trunc('day', event_time)::date AS event_date,\r\n    frequent_strings_sketch_build(7, page_path) AS paths_sketch\r\nFROM page_views\r\nGROUP BY 1;\r\n\r\n-- Top pages for the last 30 days — merge pre-built sketches\r\nSELECT frequent_strings_sketch_result_no_false_negatives(\r\n    frequent_strings_sketch_merge(7, paths_sketch), 50000\r\n) FROM daily_freq_paths\r\nWHERE event_date \u003E= current_date - 30;\r\n-- Time: 1 ms\r\n-- Standard Postgres time: 1,027 ms (GROUP BY on raw table, 30-day filter)","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_1368028614":{"id":"blog-text-08cd8e7289","text":"\u003Cp\u003EThat's 1,000x faster in our testing. The rollup table has 92 rows — one tiny sketch per day. The standard Postgres query has to scan 3+ million rows, hash them by page_path, sort, and return the top 76.\u003C/p\u003E\r\n\u003Ch2\u003EThe preaggregate-then-merge pattern\u003C/h2\u003E\r\n\u003Cp\u003EThis is the architectural pattern that ties everything together. The idea is:\u003C/p\u003E\r\n\u003Col\u003E\r\n\u003Cli\u003E\u003Cb\u003EBuild\u003C/b\u003E sketches once during your ETL or data pipeline, grouping by the dimensions you care about (date, channel, region, and so on)\u003C/li\u003E\r\n\u003Cli\u003E\u003Cb\u003EStore\u003C/b\u003E those sketches in a rollup table alongside regular aggregates like \u003Ccode\u003Ecount(*)\u003C/code\u003E and \u003Ccode\u003Esum(revenue)\u003C/code\u003E\u003C/li\u003E\r\n\u003Cli\u003E\u003Cb\u003EQuery\u003C/b\u003E the rollup table at dashboard time: Filter to the dimensions you need, merge the sketches, get instant answers\u003C/li\u003E\r\n\u003C/ol\u003E\r\n\u003Cp\u003EHere's what the rollup table looks like:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_416839381":{"id":"code-snippet-de96d7156e","language":"sql","codeSnippet":"CREATE TABLE analytics_rollup AS\r\nSELECT\r\n    date_trunc('day', event_time)::date AS event_date,\r\n    channel,\r\n    region,\r\n    cpc_sketch_build(user_id) AS users_sketch,\r\n    theta_sketch_build(user_id) AS theta_users,\r\n    kll_float_sketch_build(response_ms::real) AS latency_sketch,\r\n    count(*) AS total_events,\r\n    sum(cost_cents) AS total_cost_cents\r\nFROM page_views\r\nGROUP BY 1, 2, 3;","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_1039412481":{"id":"blog-text-f728661a5a","text":"\u003Cp\u003EThis produces 3,680 rows (90 days x 5 channels x 8 regions). The table is 88 MB because it stores the full binary sketches. Compare that to the 1.4 GB raw table.\u003C/p\u003E\r\n\u003Cp\u003ENow every analytics query hits the rollup instead of scanning the raw data:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_640135930":{"id":"code-snippet-e1e073a8e6","language":"sql","codeSnippet":"-- Unique users in the last 7 days\r\nSELECT cpc_sketch_get_estimate(cpc_sketch_union(users_sketch))::int AS unique_users_7d\r\nFROM analytics_rollup\r\nWHERE event_date \u003E= current_date - 7;\r\n-- Time: 4 ms\r\n-- Standard Postgres time: 329 ms (COUNT(DISTINCT) on raw table, 7 days)","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_1237104672":{"id":"blog-text-da35f87d60","text":"\u003Cp\u003EThat's an 80x speedup — from over 300 milliseconds down to 4 milliseconds. And it works for all the sketch types:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_246204236":{"id":"code-snippet-0b7e5784bf","language":"sql","codeSnippet":"-- p99 latency by channel (merged KLL sketches)\r\nSELECT\r\n    channel,\r\n    kll_float_sketch_get_quantile(kll_float_sketch_merge(latency_sketch), 0.99)::numeric(8,2) AS p99_ms\r\nFROM analytics_rollup\r\nGROUP BY channel;\r\n-- Time: 22 ms\r\n-- Standard Postgres time: 3,932 ms (percentile_cont by channel on raw table)","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_180620154":{"id":"blog-text-aa4e4e056c","text":"\u003Chr\u003E\r\n\r\n\u003Ch2\u003EDon't overlook the hardware payoff here\u003C/h2\u003E\r\n\u003Cp\u003EAnother thing worth pointing out is that sketches don't just make queries faster, they also make your hardware requirements smaller.\u003C/p\u003E\r\n\u003Cp\u003EThink about what exact queries are doing under the hood. \u003Ccode\u003ECOUNT(DISTINCT)\u003C/code\u003E builds a hash table in memory that grows with the number of unique values. \u003Ccode\u003Epercentile_cont()\u003C/code\u003E sorts the entire column in memory. When these structures exceed your \u003Ccode\u003Ework_mem\u003C/code\u003E setting, Postgres writes temp files to disk — and that's when things really slow down.\u003C/p\u003E\r\n\u003Cp\u003EHere's what we saw with \u003Ccode\u003Ework_mem = 4MB\u003C/code\u003E:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_1883468919":{"id":"blog-text-4aba5ffdbd","text":"\u003Ctable\u003E\r\n\u003Cthead\u003E\u003Ctr\u003E\u003Cth\u003EQuery\u003C/th\u003E\r\n\u003Cth\u003ETemp files?\u003C/th\u003E\r\n\u003Cth\u003ETime\u003C/th\u003E\r\n\u003C/tr\u003E\u003C/thead\u003E\u003Ctbody\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003Epercentile_cont(0.99)\u003C/code\u003E (exact)\u003C/td\u003E\r\n\u003Ctd\u003EYes — spills to disk\u003C/td\u003E\r\n\u003Ctd\u003E2,620 ms\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003ECOUNT(DISTINCT user_id)\u003C/code\u003E (exact)\u003C/td\u003E\r\n\u003Ctd\u003ENo (uses index-only scan)\u003C/td\u003E\r\n\u003Ctd\u003E946 ms\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003Ecpc_sketch_build(user_id)\u003C/code\u003E\u003C/td\u003E\r\n\u003Ctd\u003ENo — fixed ~2 KB regardless of input\u003C/td\u003E\r\n\u003Ctd\u003E273 ms\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003Ekll_float_sketch_build(response_ms)\u003C/code\u003E\u003C/td\u003E\r\n\u003Ctd\u003ENo — fixed ~4 KB regardless of input\u003C/td\u003E\r\n\u003Ctd\u003E566 ms\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003ERollup query (prebuilt sketches)\u003C/td\u003E\r\n\u003Ctd\u003ENo — reads 576 buffers (4.5 MB total)\u003C/td\u003E\r\n\u003Ctd\u003E5 ms\u003C/td\u003E\r\n\u003C/tr\u003E\u003C/tbody\u003E\u003C/table\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_2005723149":{"id":"blog-text-a1097f8aa4","text":"\u003Cp\u003EThe key insight: A CPC sketch uses 2-4 KB of memory whether you feed it 500 thousand or 500 million distinct values. A KLL sketch uses 4-8 KB whether you're computing percentiles over 10 million or 10 billion values. The memory footprint is bounded by the sketch's precision parameter, not by the size of your data.\u003C/p\u003E\r\n\u003Cp\u003EWhat this means in practice:\u003C/p\u003E\r\n\u003Cul\u003E\r\n\u003Cli\u003EYou don't need to tune \u003Ccode\u003Ework_mem\u003C/code\u003E up to 256MB to avoid disk spills\u003C/li\u003E\r\n\u003Cli\u003EYou don't need fast NVMe storage for temp file I/O\u003C/li\u003E\r\n\u003Cli\u003EYour analytics queries can run comfortably on a modest cloud instance\u003C/li\u003E\r\n\u003Cli\u003EDashboards stay responsive even on constrained hardware\u003C/li\u003E\r\n\u003C/ul\u003E\r\n\u003Cp\u003EThe rollup query reads 576 buffers — that's 4.5 MB of data total. You could serve that from basically anything.\u003C/p\u003E\r\n\u003Chr\u003E\r\n\r\n\u003Ch2\u003EKeeping rollups fresh\u003C/h2\u003E\r\n\u003Cp\u003EOnce you've got a rollup table, the next question is how to keep it up to date as new events arrive. There are a few good options depending on how much infrastructure you want.\u003C/p\u003E\r\n\u003Ch3\u003EMaterialized views\u003C/h3\u003E\r\n\u003Cp\u003EThe simplest approach is a materialized view — same SQL as the rollup table, but with a built-in refresh mechanism:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_87021746":{"id":"code-snippet-5995d8e072","language":"sql","codeSnippet":"CREATE MATERIALIZED VIEW analytics_rollup AS\r\nSELECT\r\n    date_trunc('day', event_time)::date AS event_date,\r\n    channel,\r\n    region,\r\n    cpc_sketch_build(user_id) AS users_sketch,\r\n    theta_sketch_build(user_id) AS theta_users,\r\n    kll_float_sketch_build(response_ms::real) AS latency_sketch,\r\n    count(*) AS total_events,\r\n    sum(cost_cents) AS total_cost_cents\r\nFROM page_views\r\nGROUP BY 1, 2, 3;\r\n\r\n-- Add a unique index so CONCURRENTLY works\r\nCREATE UNIQUE INDEX ON analytics_rollup (event_date, channel, region);","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_1563551537":{"id":"blog-text-ae30bd4f77","text":"\u003Cp\u003ENow \u003Ccode\u003EREFRESH MATERIALIZED VIEW CONCURRENTLY analytics_rollup;\u003C/code\u003E rebuilds it without blocking reads. You can put that in a cron job or call it from your pipeline after loading new data.\u003C/p\u003E\r\n\u003Cp\u003EThe tradeoff: Every refresh does a full recompute — it rescans all 10M rows and rebuilds every sketch from scratch. For our test table that takes about 10-15 seconds, which is fine for a nightly job. At hundreds of millions of rows it starts to hurt. For more on making matviews query-friendly, see \u003Ca href=\"https://www.crunchydata.com/blog/indexing-materialized-views-in-postgres\" target=\"_blank\" rel=\"noopener noreferrer\"\u003EIndexing Materialized Views in Postgres\u003C/a\u003E.\u003C/p\u003E\r\n\u003Ch3\u003Epg_incremental: process only new data\u003C/h3\u003E\r\n\u003Cp\u003EIf full-table rescans become too expensive, \u003Ca href=\"https://github.com/CrunchyData/pg_incremental\" target=\"_blank\" rel=\"noopener noreferrer\"\u003Epg_incremental\u003C/a\u003E gives you incremental processing. Instead of rebuilding the entire rollup, you define a pipeline that only processes rows that arrived since the last run:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_1682867120":{"id":"code-snippet-e0d06953f2","language":"sql","codeSnippet":"-- Create the rollup table\r\nCREATE TABLE analytics_rollup (\r\n    event_date date, channel text, region text,\r\n    users_sketch bytea, latency_sketch bytea,\r\n    total_events bigint, total_cost_cents bigint\r\n);\r\n\r\n-- Process each day's data exactly once, appending sketch rows to the rollup\r\nSELECT incremental.create_time_interval_pipeline(\r\n    pipeline_name := 'rollup_pipeline',\r\n    time_interval := '1 day',\r\n    source_table_name := 'page_views',\r\n    start_time := (SELECT min(event_time) FROM page_views),\r\n    command := $$\r\n        INSERT INTO analytics_rollup\r\n        SELECT\r\n            date_trunc('day', event_time)::date AS event_date,\r\n            channel,\r\n            region,\r\n            cpc_sketch_build(user_id) AS users_sketch,\r\n            kll_float_sketch_build(response_ms::real) AS latency_sketch,\r\n            count(*) AS total_events,\r\n            sum(cost_cents) AS total_cost_cents\r\n        FROM page_views\r\n        WHERE event_time \u003E= $1 AND event_time \u003C $2\r\n        GROUP BY 1, 2, 3\r\n    $$\r\n);","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_258456775":{"id":"blog-text-20e5e1ee26","text":"\u003Cp\u003EThis is a natural fit for sketches because they're additive — you build a sketch for today's data and union it with yesterday's at query time. You don't have to re-scan historical rows. See \u003Ca href=\"https://www.crunchydata.com/blog/pg_incremental-incremental-data-processing-in-postgres\" target=\"_blank\" rel=\"noopener noreferrer\"\u003Epg_incremental: Incremental Data Processing in Postgres\u003C/a\u003E for the full details.\u003C/p\u003E\r\n\u003Ch2\u003ESketches on remote data with pg_lake\u003C/h2\u003E\r\n\u003Cp\u003EYour event data doesn't have to live in a Postgres heap table for any of this to work. If your data already lives in object storage — synced from a warehouse, landed by a pipeline, or written directly by an application, \u003Ca href=\"https://docs.snowflake.com/en/user-guide/snowflake-postgres/postgres-pg_lake\" target=\"_blank\" rel=\"noopener noreferrer\"\u003Epg_lake\u003C/a\u003E lets you query it from Postgres as an Apache Iceberg™ table. Under the hood, pg_lake uses a DuckDB columnar engine for the scan, so analytical queries on remote data are often \u003Ci\u003Edramatically\u003C/i\u003E faster than on a local heap table:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_183700051":{"id":"code-snippet-dcc102aeb4","language":"sql","codeSnippet":"-- Iceberg table — data lives in object storage, queried via DuckDB columnar engine\r\nCREATE TABLE page_views_iceberg (...) USING iceberg;\r\n\r\n-- Exact distinct count on 10M rows\r\nSELECT count(DISTINCT user_id) FROM page_views_iceberg;\r\n-- Heap: 336 ms | Iceberg: 285 ms (similar — both hash-bound)\r\n\r\nSELECT avg(response_ms)::numeric(8,2) FROM page_views_iceberg;\r\n-- Heap: 221 ms | Iceberg: 16 ms (14x faster — columnar only reads one column)","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_205358017":{"id":"blog-text-a5a48a6a0e","text":"\u003Cp\u003EThe speedup depends on the query. For aggregates that scan a single column (avg, sum, filtered counts), Iceberg's columnar format is dramatically faster because it only reads the bytes it needs. For hash-heavy operations like COUNT(DISTINCT), both engines are doing similar work and performance converges.\u003C/p\u003E\r\n\u003Cp\u003EBut sketches may still matter — you can't query object storage in under a millisecond, no matter how fast the engine is. The rollup pattern we saw before can work on remote object stores. Scan Iceberg once, store sketches locally, and you'll have single-digit millisecond dashboard queries over data that lives remotely:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"code_snippet_780365281":{"id":"code-snippet-53c4b6ca68","language":"sql","codeSnippet":"-- Scan remote data once, store sketch rollup in local Postgres (8 seconds)\r\nCREATE TABLE analytics_rollup AS\r\nSELECT\r\n    date_trunc('day', event_time)::date AS event_date,\r\n    channel,\r\n    cpc_sketch_build(user_id) AS users_sketch,\r\n    kll_float_sketch_build(response_ms::real) AS latency_sketch,\r\n    count(*) AS total_events\r\nFROM page_views_iceberg  -- data lives in S3\r\nGROUP BY 1, 2;\r\n\r\n-- Query the local rollup: \u003C1 ms instead of scanning remote storage\r\nSELECT cpc_sketch_get_estimate(cpc_sketch_union(users_sketch))::int\r\nFROM analytics_rollup WHERE event_date \u003E= current_date - 7;\r\n-- Time: 4 ms\r\n-- Standard Postgres time: 88 ms (COUNT(DISTINCT) on Iceberg table, 7 days)","multiLine":true,":type":"snowflake-site/components/code-snippet"},"blog_text_2006340389":{"id":"blog-text-85921f4977","text":"\u003Cp\u003EThe source data stays in object storage (cheap, shared with other tools). The sketch rollup lives in local Postgres (fast, tiny — 2.3 MB for 90 days of rollups vs 1.4 GB of raw data in our example). You can get sub-millisecond dashboard queries without copying terabytes of raw events into your database.\u003C/p\u003E\r\n\u003Ch2\u003EWhen NOT to approximate\u003C/h2\u003E\r\n\u003Cp\u003ENot everything should be approximated. Before the pitchforks come out in the comments section I'll add this warning. Here are cases where you should stick with exact:\u003C/p\u003E\r\n\u003Cp\u003E\u003Cb\u003EFinancial reporting, billing, compliance:\u003C/b\u003E If a number goes on an invoice or in a regulatory filing, you need exact. No debate.\u003C/p\u003E\r\n\u003Cp\u003E\u003Cb\u003ESmall tables:\u003C/b\u003E If you have under about 1 million rows, exact aggregates are fast enough — they'll come back in under 200ms. Don't add complexity to a problem that doesn't exist.\u003C/p\u003E\r\n\u003Cp\u003E\u003Cb\u003EWhen you need individual rows:\u003C/b\u003E Sketches are aggregates. They can tell you &quot;about 500K unique users&quot; but they can't tell you \u003Ci\u003Ewhich\u003C/i\u003E users. If you need \u003Ccode\u003EWHERE user_id = X\u003C/code\u003E, you need the raw data.\u003C/p\u003E\r\n\u003Cp\u003E\u003Cb\u003EJoins on approximate results:\u003C/b\u003E You can't JOIN on a sketch. You can TABLESAMPLE a table and then join it to something else, but be aware that sampling happens before the join — a 10% sample of two joined tables gives you ~1% of the matching rows.\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_1492962835":{"id":"blog-text-b8cb098bfb","text":"\u003Cblockquote\u003E\u003Cp\u003E&quot;Close only counts in horseshoes and hand grenades.&quot;\u003C/p\u003E\r\n\u003Cfooter\u003E\r\n—\u003Ccite\u003EFrank Robinson\u003C/cite\u003E\u003C/footer\u003E\r\n\u003C/blockquote\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_820253149":{"id":"blog-text-1db832af22","text":"\u003Ch2\u003EBenchmark summary\u003C/h2\u003E\r\n\u003Cp\u003EAll numbers from our 10M-row \u003Ccode\u003Epage_views\u003C/code\u003E table on Postgres 18 in Docker (Apple M-series, 256MB shared_buffers, 64MB work_mem, \u003Ccode\u003Emax_parallel_workers_per_gather = 2\u003C/code\u003E unless noted). Parallel query is enabled for all benchmarks equally — both exact and sketch-based queries benefit from parallel table scans:\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_1704642617":{"id":"blog-text-263dae607f","text":"\u003Ctable\u003E\r\n\u003Cthead\u003E\u003Ctr\u003E\u003Cth\u003EMethod\u003C/th\u003E\r\n\u003Cth\u003ETime\u003C/th\u003E\r\n\u003Cth\u003EStandard Postgres time\u003C/th\u003E\r\n\u003Cth\u003EResult\u003C/th\u003E\r\n\u003Cth\u003EError\u003C/th\u003E\r\n\u003Cth\u003ENotes\u003C/th\u003E\r\n\u003C/tr\u003E\u003C/thead\u003E\u003Ctbody\u003E\u003Ctr\u003E\u003Ctd\u003EExact \u003Ccode\u003ECOUNT(DISTINCT)\u003C/code\u003E\u003C/td\u003E\r\n\u003Ctd\u003E671 ms\u003C/td\u003E\r\n\u003Ctd\u003E—\u003C/td\u003E\r\n\u003Ctd\u003E500,000\u003C/td\u003E\r\n\u003Ctd\u003E0%\u003C/td\u003E\r\n\u003Ctd\u003EIndex-only scan\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003EExact (30 days only)\u003C/td\u003E\r\n\u003Ctd\u003E1,025 ms\u003C/td\u003E\r\n\u003Ctd\u003E—\u003C/td\u003E\r\n\u003Ctd\u003E497,618\u003C/td\u003E\r\n\u003Ctd\u003E0%\u003C/td\u003E\r\n\u003Ctd\u003EIndex on event_time, spills to disk\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003ETABLESAMPLE SYSTEM(10)\u003C/code\u003E\u003C/td\u003E\r\n\u003Ctd\u003E217 ms\u003C/td\u003E\r\n\u003Ctd\u003E671 ms\u003C/td\u003E\r\n\u003Ctd\u003E~414K (x10=4.14M)\u003C/td\u003E\r\n\u003Ctd\u003E730%\u003C/td\u003E\r\n\u003Ctd\u003ETerrible for distinct counts\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003ETABLESAMPLE BERNOULLI(10)\u003C/code\u003E\u003C/td\u003E\r\n\u003Ctd\u003E404 ms\u003C/td\u003E\r\n\u003Ctd\u003E671 ms\u003C/td\u003E\r\n\u003Ctd\u003E~416K (x10=4.16M)\u003C/td\u003E\r\n\u003Ctd\u003E730%\u003C/td\u003E\r\n\u003Ctd\u003ESame problem, slower\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003EHLL (\u003Ccode\u003Ehll_add_agg\u003C/code\u003E)\u003C/td\u003E\r\n\u003Ctd\u003E320 ms\u003C/td\u003E\r\n\u003Ctd\u003E671 ms\u003C/td\u003E\r\n\u003Ctd\u003E497,209\u003C/td\u003E\r\n\u003Ctd\u003E0.6%\u003C/td\u003E\r\n\u003Ctd\u003EFixed memory, mergeable\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003ECPC sketch\u003C/td\u003E\r\n\u003Ctd\u003E389 ms\u003C/td\u003E\r\n\u003Ctd\u003E671 ms\u003C/td\u003E\r\n\u003Ctd\u003E494,394\u003C/td\u003E\r\n\u003Ctd\u003E1.1%\u003C/td\u003E\r\n\u003Ctd\u003ESlightly more compact storage\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003ETheta sketch\u003C/td\u003E\r\n\u003Ctd\u003E370 ms\u003C/td\u003E\r\n\u003Ctd\u003E671 ms\u003C/td\u003E\r\n\u003Ctd\u003E497,746\u003C/td\u003E\r\n\u003Ctd\u003E0.5%\u003C/td\u003E\r\n\u003Ctd\u003EAdds set operations\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003EKLL sketch (p99)\u003C/td\u003E\r\n\u003Ctd\u003E625 ms\u003C/td\u003E\r\n\u003Ctd\u003E2,617 ms\u003C/td\u003E\r\n\u003Ctd\u003E1056.67\u003C/td\u003E\r\n\u003Ctd\u003E~7%\u003C/td\u003E\r\n\u003Ctd\u003Evs exact p99=986.39; 4x faster\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003EExact \u003Ccode\u003Epercentile_cont(0.99)\u003C/code\u003E\u003C/td\u003E\r\n\u003Ctd\u003E2,617 ms\u003C/td\u003E\r\n\u003Ctd\u003E—\u003C/td\u003E\r\n\u003Ctd\u003E986.39\u003C/td\u003E\r\n\u003Ctd\u003E0%\u003C/td\u003E\r\n\u003Ctd\u003ESorts entire column\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003ETheta intersection\u003C/td\u003E\r\n\u003Ctd\u003E382 ms\u003C/td\u003E\r\n\u003Ctd\u003E2,499 ms\u003C/td\u003E\r\n\u003Ctd\u003E474,272\u003C/td\u003E\r\n\u003Ctd\u003E—\u003C/td\u003E\r\n\u003Ctd\u003Evs self-join on raw table\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Cb\u003ERollup query (7 days)\u003C/b\u003E\u003C/td\u003E\r\n\u003Ctd\u003E\u003Cb\u003E4 ms\u003C/b\u003E\u003C/td\u003E\r\n\u003Ctd\u003E\u003Cb\u003E329 ms\u003C/b\u003E\u003C/td\u003E\r\n\u003Ctd\u003E373,440\u003C/td\u003E\r\n\u003Ctd\u003E~3%\u003C/td\u003E\r\n\u003Ctd\u003E\u003Cb\u003E80x faster than raw scan\u003C/b\u003E\u003C/td\u003E\r\n\u003C/tr\u003E\u003C/tbody\u003E\u003C/table\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_155102234":{"id":"blog-text-b2c981243f","text":"\u003Chr\u003E\r\n\r\n\u003Ch2\u003EComparison at a glance\u003C/h2\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"},"blog_text_507496595":{"id":"blog-text-b6ffbc28d3","text":"\u003Ctable\u003E\r\n\u003Cthead\u003E\u003Ctr\u003E\u003Cth\u003E&nbsp;\u003C/th\u003E\r\n\u003Cth\u003ETABLESAMPLE\u003C/th\u003E\r\n\u003Cth\u003EHLL (postgresql-hll)\u003C/th\u003E\r\n\u003Cth\u003EDataSketches\u003C/th\u003E\r\n\u003C/tr\u003E\u003C/thead\u003E\u003Ctbody\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Cb\u003EWhat it does\u003C/b\u003E\u003C/td\u003E\r\n\u003Ctd\u003EReads a random subset of rows\u003C/td\u003E\r\n\u003Ctd\u003EDistinct counting\u003C/td\u003E\r\n\u003Ctd\u003EDistinct counting, set ops, quantiles, heavy hitters\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Cb\u003EExtension required\u003C/b\u003E\u003C/td\u003E\r\n\u003Ctd\u003ENo (built-in since PG 9.5)\u003C/td\u003E\r\n\u003Ctd\u003E\u003Ccode\u003Ehll\u003C/code\u003E\u003C/td\u003E\r\n\u003Ctd\u003E\u003Ccode\u003Edatasketches\u003C/code\u003E\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Cb\u003EPrecomputable\u003C/b\u003E\u003C/td\u003E\r\n\u003Ctd\u003ENo (scan-time only)\u003C/td\u003E\r\n\u003Ctd\u003EYes\u003C/td\u003E\r\n\u003Ctd\u003EYes\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Cb\u003EMergeable\u003C/b\u003E\u003C/td\u003E\r\n\u003Ctd\u003ENo\u003C/td\u003E\r\n\u003Ctd\u003EYes (\u003Ccode\u003Ehll_union_agg\u003C/code\u003E)\u003C/td\u003E\r\n\u003Ctd\u003EYes (all sketch types)\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Cb\u003EDistinct counts\u003C/b\u003E\u003C/td\u003E\r\n\u003Ctd\u003EPoor (overcounts when extrapolated)\u003C/td\u003E\r\n\u003Ctd\u003EYes\u003C/td\u003E\r\n\u003Ctd\u003EYes (CPC, Theta)\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Cb\u003ESet operations\u003C/b\u003E\u003C/td\u003E\r\n\u003Ctd\u003ENo\u003C/td\u003E\r\n\u003Ctd\u003ENo\u003C/td\u003E\r\n\u003Ctd\u003EYes (Theta)\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Cb\u003EQuantiles / percentiles\u003C/b\u003E\u003C/td\u003E\r\n\u003Ctd\u003ENo\u003C/td\u003E\r\n\u003Ctd\u003ENo\u003C/td\u003E\r\n\u003Ctd\u003EYes (KLL)\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Cb\u003EMost common value / top-N\u003C/b\u003E\u003C/td\u003E\r\n\u003Ctd\u003ENo\u003C/td\u003E\r\n\u003Ctd\u003ENo\u003C/td\u003E\r\n\u003Ctd\u003EYes (Frequent Strings)\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Cb\u003ETypical error\u003C/b\u003E\u003C/td\u003E\r\n\u003Ctd\u003EVaries with sample %\u003C/td\u003E\r\n\u003Ctd\u003E~2% at default\u003C/td\u003E\r\n\u003Ctd\u003E~1-3% at defaults\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Cb\u003EError guarantee\u003C/b\u003E\u003C/td\u003E\r\n\u003Ctd\u003ENone (statistical)\u003C/td\u003E\r\n\u003Ctd\u003EMathematically bounded\u003C/td\u003E\r\n\u003Ctd\u003EMathematically bounded\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Cb\u003EMemory per sketch\u003C/b\u003E\u003C/td\u003E\r\n\u003Ctd\u003EN/A\u003C/td\u003E\r\n\u003Ctd\u003E~1.3 KB\u003C/td\u003E\r\n\u003Ctd\u003E~1-4 KB\u003C/td\u003E\r\n\u003C/tr\u003E\u003Ctr\u003E\u003Ctd\u003E\u003Cb\u003EBest for\u003C/b\u003E\u003C/td\u003E\r\n\u003Ctd\u003EQuick exploration, smoke tests\u003C/td\u003E\r\n\u003Ctd\u003ESimple unique counts, rolling windows\u003C/td\u003E\r\n\u003Ctd\u003EMulti-dimensional analytics, set ops, quantiles\u003C/td\u003E\r\n\u003C/tr\u003E\u003C/tbody\u003E\u003C/table\u003E\r\n","richText":true,":type":"snowflake-site/components/blog/blog-text"}},":itemsOrder":["blog_text","code_snippet","blog_text_1452300736","code_snippet_304759495","blog_text_811621137","code_snippet_1757267569","blog_text_795054801","code_snippet_285538223","blog_text_1490876437","code_snippet_164400665","blog_text_1984430024","code_snippet_1652045645","blog_text_1140811835","code_snippet_1697379323","blog_text_1795912919","code_snippet_1299592688","blog_text_1014003239","code_snippet_1274423142","blog_text_1380109897","code_snippet_547472791","blog_text_824961338","code_snippet_312769523","blog_text_2138710847","blog_text_892720800","code_snippet_882258116","blog_text_1590427254","code_snippet_1314625764","blog_text_226390574","code_snippet_1468592557","blog_text_1390155145","code_snippet_1632883888","blog_text_744703937","code_snippet_157475885","blog_text_1608570159","code_snippet_355572341","blog_text_390676450","code_snippet_553256594","blog_text_1368028614","code_snippet_416839381","blog_text_1039412481","code_snippet_640135930","blog_text_1237104672","code_snippet_246204236","blog_text_180620154","blog_text_1883468919","blog_text_2005723149","code_snippet_87021746","blog_text_1563551537","code_snippet_1682867120","blog_text_258456775","code_snippet_183700051","blog_text_205358017","code_snippet_780365281","blog_text_2006340389","blog_text_1492962835","blog_text_820253149","blog_text_1704642617","blog_text_155102234","blog_text_507496595"],":type":"wcm/foundation/components/responsivegrid"},"responsivegrid_premium_content_banner":{"columnCount":12,"columnClassNames":{},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12","appliedCssClassNames":"snowflake-responsive-component-top-padding-medium",":items":{},":itemsOrder":[],":type":"wcm/foundation/components/responsivegrid"},"container_author_chip":{"layout":"RESPONSIVE_GRID","columnCount":12,"columnClassNames":{"author_chip":"aem-GridColumn aem-GridColumn--default--12"},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12","id":"container-336a3baba0",":type":"snowflake-site/components/container",":items":{"author_chip":{"id":"author-chip-9931d8882d","title":{"id":"title","type":"heading2","lines":["Learn more about the authors"],":type":"snowflake-site/components/title-v2"},"authors":[{"authorImage":{"id":"image-e7f862dfa8","height":"675","isLcpImage":false,"src":"https://www.snowflake.com/adobe/dynamicmedia/deliver/dm-aid--8e4d14e7-49fc-4d3a-9899-8848b9f2bb1d/echristensen.png?preferwebp=true&quality=85","lazyEnabled":true,"width":"675",":type":"snowflake-site/components/image"},"authorCta":{"id":"button-0f9f832469","showOutboundIcon":false,"buttonLink":{"valid":true,"url":"/en/blog/authors/elizabeth-garrett-christensen/"},"linkTargetContentType":"DOCUMENT_LEARN",":type":"snowflake-site/components/button","linkType":"SNOWFLAKE_INTERNAL","text":"Elizabeth Garrett Christensen"},"authorTitle":"Developer Advocate"}],":type":"snowflake-site/components/blog/author-chip"}},":itemsOrder":["author_chip"],"appliedCssClassNames":"snowflake-responsive-component-top-padding-medium"}},":itemsOrder":["container_hero","responsivegrid_content","responsivegrid_premium_content_banner","container_author_chip"]},"flexible_column_content_container_2":{"layout":"SIMPLE","id":"container-ca392144ef",":type":"snowflake-site/components/flexible-column-container/flexible-column-content-container",":items":{"blog_table_of_content":{"id":"blog-table-of-content-8b9db0fa94",":type":"snowflake-site/components/blog/blog-table-of-content","tableOfContents":[]}},":itemsOrder":["blog_table_of_content"]},":type":"snowflake-site/components/flexible-column-container","isBlogPage":true,"isActiveTOC":false},"related_content":{"id":"related-content-bd3e82d579","relatedContent":[],":type":"snowflake-site/components/blog/related-content","isBlogPage":true}},":itemsOrder":["flexible_column_container","related_content"],"appliedCssClassNames":"snowflake-container"}},":itemsOrder":["container_breadcrumb","container_main_content"],":type":"wcm/foundation/components/responsivegrid"},"container_47873732":{"additionalClasses":"section--blog-newsletter","layout":"RESPONSIVE_GRID","columnCount":12,"columnClassNames":{"flexible_column_cont":"aem-GridColumn aem-GridColumn--default--12"},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12","id":"container-3d056e6e06",":type":"snowflake-site/components/container",":items":{"flexible_column_cont":{"id":"flexible-column-container-d4b230e8ef","type":"1-column","alignColumns":"top","containerMaxWidth":"extra-large","topPadding":"small","bottomPadding":"none","spaceBetween":"small","reverseOnMobile":false,"carouselOnMobile":false,"propertiesCSSClasses":"section--blog-newsletter","backgroundImageOption":"none","flexible_column_content_container_1":{"layout":"SIMPLE","id":"container-c079ed78e5",":type":"snowflake-site/components/flexible-column-container/flexible-column-content-container",":items":{"marketo_v2":{"id":"marketo-v2-c100fc2f65","marketoForm":{"hidden":null,"formId":"3320","successUrl":null,"edit":false,"script":null,"values":null},"title":{"id":"title","type":"heading3","lines":["Subscribe to our blog newsletter","Get the best, coolest and latest delivered to your inbox each week"],":type":"snowflake-site/components/title-v2"},"munchkinId":"252-RFO-227","serverInstance":"252-RFO-227.mktoweb.com","marketoConfigured":true,"formConfigured":true,":type":"snowflake-site/components/form/marketo-v2"},"text":{"id":"text-27e96e61b5","additionalClasses":"newsletter-disclaimer","text":"\u003Cp\u003EBy submitting this form, I understand Snowflake will process my personal information in accordance with their Privacy Notice.\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/text"}},":itemsOrder":["marketo_v2","text"]},":type":"snowflake-site/components/flexible-column-container","isBlogPage":true,"isActiveTOC":false}},":itemsOrder":["flexible_column_cont"],"appliedCssClassNames":"snowflake-container"},"experiencefragment-pre-footer":{"id":"experiencefragment-28a9cf304d","localizedFragmentVariationPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/get-started-pre-footer/get-started-pre-footer/jcr:content","configured":true,":type":"snowflake-site/components/experiencefragment","xfModelPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/get-started-pre-footer/get-started-pre-footer.xfmodel.json"},"markup_editor":{"id":"markup-editor-f7c75ed7cd","title":"Page CSS","cssContent":"@media screen and (min-width:768px){.snowflake-blog-author-chip-wrapper{justify-content:flex-start}.snowflake-blog-related-content-on-blog-page{max-width:1408px;margin-left:auto;margin-right:auto}.snowflake-text{font-family:Lato,sans-serif;font-weight:400;font-size:16px;line-height:24px}}.section--blog-newsletter{max-width:none;width:100%;padding-left:0;padding-right:0;margin-left:0;margin-right:0;margin-bottom:0}.section--blog-newsletter .mktoField{background-color:transparent !important}.section--blog-newsletter\u003E.container{padding-left:0;padding-right:0}@media screen and (min-width:768px){.section--blog-newsletter\u003E.container{padding-left:0;padding-right:0}}.newsletter-disclaimer p{font-size:14px !important}.section--blog-newsletter .snowflake-marketo-form-container{margin-bottom:24px;background-color:#f6f9fa;gap:48px;box-shadow:none}.section--blog-newsletter .snowflake-title p.snowflake-title-line:first-child{font-family:Texta;font-size:24px;line-height:26px;font-weight:700;margin-bottom:4px}.section--blog-newsletter .snowflake-title p.snowflake-title-line{text-transform:none;font-family:\"Lato\",sans-serif;font-size:16px;line-height:24px;font-weight:normal}@media screen and (min-width:1024px){.section--blog-newsletter .snowflake-marketo-form-container{display:flex;justify-content:center}.section--blog-newsletter .snowflake-title .snowflake-title-line{text-align:left}.section--blog-newsletter .snowflake-marketo-form .mktoFormRow:has(\u003E input[type=\"hidden\"]){flex-grow:0}.section--blog-newsletter .snowflake-marketo-form{display:flex;width:50% !important}.section--blog-newsletter .snowflake-marketo-form .mktoButtonRow{flex-grow:0;width:auto !important;margin-left:0;margin-right:0}.section--blog-newsletter .snowflake-marketo-form .mktoFormRow{flex-grow:1}.section--blog-newsletter\u003E.container{padding-left:0;padding-right:0}.section--blog-newsletter .snowflake-marketo-form-title{width:50%;margin-bottom:0 !important}.section--blog-newsletter .center .snowflake-title{align-items:flex-start}}.snowflake-sub-navigation a.snowflake-sub-navigation-primary-link{width:auto !important}.snowflake-blog-hero{align-items:stretch !important}",":type":"snowflake-site/components/markup-editor","isGSAPEnabled":false},"markup_editor-table":{"id":"markup-editor-87cf73ae48","title":"Table Styling CSS","cssContent":"#snowflake-blog-template-main-container table{width:100%;background-color:var(--ui-background-01);border-collapse:collapse;border:2px solid var(--ui-background-09);font-family:'Lato',sans-serif;color:var(--ui-background-09)}#snowflake-blog-template-main-container table thead{background-color:var(--ui-01)}#snowflake-blog-template-main-container table th,#snowflake-blog-template-main-container table td{border:2px solid var(--ui-background-09);padding:var(--spacing-01)}",":type":"snowflake-site/components/markup-editor","isGSAPEnabled":false},"experiencefragment-footer":{"id":"experiencefragment-7a254778cc","localizedFragmentVariationPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/footer/master/jcr:content","configured":true,":type":"snowflake-site/components/experiencefragment","xfModelPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/footer/master.xfmodel.json"}},":itemsOrder":["experiencefragment-banner","experiencefragment-header","experiencefragment-sub-header","responsivegrid","container_47873732","experiencefragment-pre-footer","markup_editor","markup_editor-table","experiencefragment-footer"],":type":"wcm/foundation/components/responsivegrid"}},":itemsOrder":["root"],":hierarchyType":"page",":path":"/content/snowflake-site/global/en/blog/engineering/postgres-count-distinct-approximation","analyticsEnabled":true,"coveoConfig":{"pipeline":"snowflake.com","apiKey":"xx335921a6-2a0a-40f2-a167-e390b4766c3d","organizationId":"snowflakecomputingproduction8neljofn","searchHub":"snowflake.com"},"isPasswordProtected":false,"analyticsContentTags":["snowflake-site:taxonomy/blog/engineering-blog/open-source"],"locale":"en"}
  