Use SQL subqueries to count distinct 50x faster
periscope.io
periscope.io
I think MySQL basically gives up optimizing the moment you ask it to do a correlated subquery.
I use MSSQL at work and given it some really dumb schemas and really dumb queries and it still manages to pick good plans. It wouldn't surprise me if MSSQL generated the "optimal" plan from the first query.
It doesn't choose good plans if you break 3NF by a long shot or avoid the most obvious indexes. Otherwise the only thing to watch out for is stale stats and parameter sniffing.
Oracle 12c has adaptive plans which would be a great addition to MSSQL in the fight against parameter sniffing. Basically it figures out that it's probably got the wrong plan ("this nested loop join stopped making sense about 10,000 rows ago") and adjusts it accordingly.
Again, wouldn't be surprised if Oracle got it "right" from the first query.
Comparing across different RDBMS's is a great idea. Maybe we can squeeze it into a future blog post. :)
[1] https://news.ycombinator.com/item?id=7119976 or http://ta.speot.is/2014/01/25/use-subqueries-to-count-distin...
Here are the same queries tested on MySQL, MSSQL and Oracle as well: https://periscope.io/blog/count-distinct-in-mysql-postgres-s...
tl;dr: Oracle and MSSQL do very well, and even better on the more specific queries. Postgres's naive query is much worse than all the others.
My biggest problem with these benchmarks and your queries I cover in this [1] post.
If databases.name is unique then you'd have a unique index/constraint on it. Doing this in the environment I set up causes SQL Server to generate identical execution plans for all three queries.
If databases.name is not unique then your query's group by name doesn't make sense. I'll just quote what I wrote verbatim:
> No online retailer is looking at their sales reports and thinking that John Smith is their best customer and Amorette McFredson is their worst.
> ... I don’t know whether the query that was chosen for optimization because it’s good for demonstrating the technique on PgSQL or because it represents a query that is actually in use. I don’t like group by name without knowing whether dashboard names are unique.
> With that said, if it’s not unique then the query is arguably “wrong” because it aggregates results for distinct dashboard names rather than distinct dashboards.
> Removing the unique index on dashboards.name does change the execution plan of the first query – it’s slightly worse than the other two (which remain identical). “Fixing” the first query to group by dashboards.id and dashboards.name causes the better plan to be generated again.
[1] http://ta.speot.is/2014/01/25/use-subqueries-to-count-distin...
The next step is usually to jump into a column oriented data store, document store, or some other technology they understand even less than Sql.
Even with simple CRUD I've had to unwind faulty implementations where a smallish (50GB) database was being eager fetched into memory because of poor ORM config and overuse of FK relationships - when a user did something as simple as logging into the app.
And I'm not talking junior developers - some of these guys were 10+ years at Amazon.
A broken promise of declarative languages I guess.
SQL is a tiny kernel of beautiful relational mathematics wrapped up in a 3-foot ball of ugly implementation hacks.
NoSQL is what happens when you throw out the beautiful kernel and just keep the hacks, so it's not really better I guess.
In practice, every time I've seen a slow DB query, I've always been able to get huge improvements on it, even though the DB in theory had all the knowledge necessary to realize that I don't care about the intermediate results.
I can see why this would make the query plans look better (in that index scans make more sense to humans) but are you sure you're actually getting better performance? If you are getting better performance, why is the default so wrong? Computers are really fast at sequential scans, less so at random-access stuff.
This is true if you're simple talking about raw IO throughput, but in terms of algorithmic searching the truth is almost exactly the opposite. For example, to find a record in a 100 million record table with a unique key using sequential search would require on average a scan of 50 million records. To access the same record when that key is indexed requires accessing/testing a maximum 23 or 24 entries in the index (2^23 is roughly 10M) and then direct access of the exact record needed in the table. Indexes are used because they speed things up exponentially, even though it may be true that random access IO is much slower than sequential IO.
People, I suspect, also have much higher throughput when scanning sequentially compared to random access of items. But imagine trying to find the single person in a copy of the Manhattan white pages (is there still such a thing?) with the name 'Horace Greeley'. Would you prefer to scan sequentially from the beginning (no use of an index) or make use of the fact that white page phone books are indexed by lastname, firstname?
Of course, because sequential scanning is so much faster than random access it will always be faster on tiny tables. It will depend on the system and record size, but I'm guessing indexing will take over as faster method as table size grows beyond 5k records or so, give or take an order of magnitude. Not sure of exact numbers but you get the idea. As number of records goes up from there the speed advantage of indexing over scanning grows at exponential rate.
At least in my time with MSSQL, seeing `SELECT DISTINCT ...` was a red flag. It nearly always meant something needed to be refactored using GROUP BY.
It still leaves room to determine how and when to achieve the DISTINCT property, perhaps even doing no work at all.
* It can do a sort+unique or a hash, depending on cardinality, availability of memory, and a few other factors.
* It can push down a distinct or pull up a distinct, resulting in a faster plan.
* If there is another part of the plan that already implies distinct, like a GROUP BY, then it doesn't need to do any work at all. (Or similarly, if you're doing a distinct on a primary key).
is equivalent to
select distinct userid, name from users
It's easy to add another column to the "distinct" based query and change the behavior accidentally. With the "group by" query, you'd get an error saying that the new column needs to be either aggregated or added to the group by.
DISTINCT itself is handled well, e.g. "SELECT DISTINCT x, y ...".
But the distinct aggregates are not really seen by the planner, and they are always implemented with a sort. So this is a peculiar case for postgres.
One reason is because distinct aggregates can be more complicated to plan when you have a case like: "SELECT COUNT(distinct x), COUNT(distinct y) ..." because you actually need two different distinct sets as input.
Some databases have solutions for that problem, but postgres does not.
https://commitfest.postgresql.org/action/commitfest_view?id=...
Review is a good way to learn about new features, by the way. That's the main reason I'm able to actually write recursive queries, for instance.
Executor changes are much less intimidating and lots of people make executor changes. In my opinion, the executor is the most approachable/easiest major component.
I would encourage people to not get intimidated though. For every project where I was determined enough, I succeeded. Tom Lane does especially amazing things, of course, but I think reasoning like "I'm not Tom Lane, therefore the planner is too hard" is the wrong lesson to take away.
It's large and slightly complicated, but damn it's clean and understandable. Or at least it was back in 2005...
I detailed this at http://dennisforbes.ca/index.php/2014/01/24/your-query-plann...
Also, any idea how many times each variation was executed? 10M rows is not that big. At 1K per row (which is probably an overestimate) that comes out to 100 MB. That easily fits in memory. After the first full scan all of it would come from the cache. It's not a fair comparison unless you flush the cache between tests.
First query:
QUERY PLAN
-----------------------------------------------------------------------------------------------------
Sort (cost=3475858.60..3475859.56 rows=385 width=22)
Sort Key: (count(DISTINCT time_on_site_logs.user_id))
-> GroupAggregate (cost=3370756.94..3475842.06 rows=385 width=22)
-> Sort (cost=3370756.94..3405784.03 rows=14010837 width=22)
Sort Key: dashboards.name
-> Hash Join (cost=44.90..558337.80 rows=14010837 width=22)
Hash Cond: (time_on_site_logs.dashboard_id = dashboards.id)
-> Seq Scan on time_on_site_logs (cost=0.00..365621.11 rows=14016911 width=8)
-> Hash (cost=29.40..29.40 rows=1240 width=22)
-> Seq Scan on dashboards (cost=0.00..29.40 rows=1240 width=22)
(10 rows)Second Query:
QUERY PLAN
-----------------------------------------------------------------------------------------------------
Sort (cost=2709630.36..2709631.09 rows=291 width=26)
Sort Key: (count(DISTINCT time_on_site_logs.user_id))
-> Merge Join (cost=2604392.06..2709618.45 rows=291 width=26)
Merge Cond: (time_on_site_logs.dashboard_id = dashboards.id)
-> GroupAggregate (cost=2604392.06..2709521.80 rows=291 width=8)
-> Sort (cost=2604392.06..2639434.34 rows=14016911 width=8)
Sort Key: time_on_site_logs.dashboard_id
-> Seq Scan on time_on_site_logs (cost=0.00..365621.11 rows=14016911 width=8)
-> Index Scan using dashboards_pkey on dashboards (cost=0.00..87.00 rows=1240 width=22)
Third Query: QUERY PLAN
-----------------------------------------------------------------------------------------------------------------
Sort (cost=436381.86..436382.36 rows=200 width=26)
Sort Key: log_counts.ct
-> Hash Join (cost=436308.72..436374.22 rows=200 width=26)
Hash Cond: (dashboards.id = log_counts.dashboard_id)
-> Seq Scan on dashboards (cost=0.00..29.40 rows=1240 width=22)
-> Hash (cost=436306.22..436306.22 rows=200 width=12)
-> Subquery Scan on log_counts (cost=436302.22..436306.22 rows=200 width=12)
-> HashAggregate (cost=436302.22..436304.22 rows=200 width=4)
-> HashAggregate (cost=435705.66..435944.28 rows=23862 width=8)
-> Seq Scan on time_on_site_logs (cost=0.00..365621.11 rows=14016911 width=8)Since release 9.3 index-only scans have been implemented and performance of count distinct queries improved significantly - no more full table scans.
Wiki page: https://wiki.postgresql.org/wiki/Slow_Counting
It would be fun to have access to the same dataset or a more detailed description of the tables involved for further tinkering ....
My second thought was that 20 seconds is a long time for a query and there's probably a better solution using a materialized view.
It's good to learn the name.
http://technet.microsoft.com/en-us/library/ms190766(v=sql.10...
In SQL Server, aside from recursion, CTEs are purely syntactic sugar and thus a tool for organizing code. Whereas in Postgres, due to the optimization boundary, they are a tool for query tuning. If you rewrite a query in Postgres using CTEs just to clean it up, you are doing it wrong, since the CTEs will also affect the planner.
I think of it as each section builds a temp table.
This can be a good or bad thing, but it's sometimes important to know that it's happening.
A simple analysis over a very rotten table, doing a count(distinct) over a very varied field resulted in
Table Scan -> Temp Distinct Hash Table -> Hash Scan -> Final Results (330.022 ms) for a table with no keys and slightly over three million records.
I would love to do the same exact statements he is but need the table layout to replicate. I am curious if our analyzer would see a difference, it is really hard to "trick it" or beat it.
even a small landing page with some expected features or more information about the product-to-be would be a good idea.
(I saw the one-liner at the bottom of the post but would suggest a bit more to pique the interest of more people. If I missed it, my apologies.)
"ParAccel's DBMS engine is built for analytics, initially based on PostgreSQL" -> http://en.wikipedia.org/wiki/ParAccel
But heck, sometimes db language driver (like pymongo, python-mysql - just giving out example what I mean by language driver) are not optimized either. :) so a careful post-profiling analysis is required.
A good ORM helps you to generate the exact SQL you need.
> A good ORM helps you to generate the exact SQL you need.
Right, but sometimes it's the driver that behave badly. (One of those days when I claim to have free time I should dig deeper, do a good performance analysis.. one day...)
You would only write the first query if you didn't know what you were doing. So... The advice here seems to be, know what you're doing.
This logic seems quite obvious...
As a general recommendation I advise database developers to avoid exactly this sort of "outsmart the planner" behavior because it paints you into a corner -- data cardinality changes, schemas change, and you have large, clever queries that turn into a significant net detriment. They can absolutely be necessary in some cases, but this does not look like one of them.
The general theme should be, "how do I make my database do less work?" followed by a list of techniques, including "pre-aggregate to reduce volume". In this case, the pre-aggregates are in-line. Whether in-line pre-aggregates are appropriate will depend on context, just as you say.
Probably wise in general. This particular case is peculiar because postgres doesn't optimize distinct aggregates at all (see my other comment: https://news.ycombinator.com/item?id=7115707 ), so it's possible that the rewritten query is actually more optimizable than the original.
The original query won't adapt to changing data sizes or distributions as well as the rewritten one.
This is not fundamental, nor universal. It's a specific limitation of postgres, so I still agree with your general advice. And postgres may fix this, so your advice may prove to be true even in this situation in the long run.
select dashboards.name, count(distinct time_on_site_logs.user_id) from time_on_site_logs join dashboards on time_on_site_logs.dashboard_id = dashboards.id group by time_on_site_logs.dashboard_id, dashboards.name order by count desc
First query: 996s
First query with your variation: 593s
Second query: 67s
Third query: 21sMySQL? SQL Server? Postgre? Oracle DB? SQL Lite?
NB: These techniques are universal, but for syntax we chose Postgres. Thanks to the inimitable pgAdminIII for the Explain graphics.
This is not correct. The query will still be run through an optimizer, and the optimizer will still pick its own path of execution based on indexes and data uniqueness (as identified from the table metadata or its own dives into the data).
There are ways to force an optimizer to take a certain path, but those are dependent on which DB you're using.
Unfortunately, the original dataset doesn't exist online, so we can't verify their claims that it really is database agnostic.
Or more often one won't because the legacy of hundreds of slow but not terrible queries accumulate. It becomes a death by a thousand cuts, which is the end result of many heavily used databases.
My example in SQL Server sees the simple addition of a single distinct index to give that same metadata to the query planner, yielding the identical plan for the first and last. And this same benefit will flow to every query that follows the same path.
See wikipedia: https://en.wikipedia.org/wiki/SQL