Using PostgreSQL Arrays the Right Way
blog.heapanalytics.com
blog.heapanalytics.com
If data mining is necessary, it's right there in the event log and can be manipulated by much simpler (and faster, possibly by orders of magnitude) SQL. Additionally, it's not bloating regular access of the user records.
I've been seeing these designs more and more, recently. Is it a natural extension of non-DBAs taking DDL design roles? Because this data manipulation style doesn't really mix well with any RDBMS I'm familiar with.
I own a lot of stuff, but I don't drag my house everywhere in case I might need something. :p
That should not be construed as an endorsement. I would think the array append might get expensive.
> Well... generally this is solved via indices. An event log indexed by user ID would have the same effect. You get all the user info, and if you really need to grab historical or event data, it's there when necessary for parts of the app that actually need it.
Indeed! We're investigating a more traditional schema, as the topics of our more recent blog posts might suggest (http://blog.heapanalytics.com/speeding-up-postgresql-queries...).
I do have a question about the SQL style. I work in business intelligence, so I write a pretty good amount of SQL, and get to see a lot of other people's efforts, ranging from newbie analysts' first fumblings to experienced DBAs' queries and procedures. It seems that nested subqueries in the style of this article are used much more often than CTEs, and I'm curious why this is.
* I find CTEs much more readable than subqueries * For naive uses, they are optimized the same * For complex uses (e.g. recursion to traverse a parent-child hierarchy), CTEs are the only option
The only good reason I see for subqueries is if you are using an old version of your RDBMS that does not support CTEs.
I'm just wondering if anyone else can weigh in on the topic.
This causes a number of challenges for the planner, because:
* some of the places may benefit from quickly producing the first row (e.g. in EXISTS) while other places may need all the rows (e.g. when you pass the rows to GROUP BY or something)
* the places may use additional clauses, but these can't be pushed down to the CTEs because that'd influence the other place
There may be databases with planner that handle this in a different way, but in PostgreSQL the CTEs act as optimization fences - each CTE is optimized on it's own, without considering the rest of the query (additional clauses etc.)
People who use CTEs as named subqueries are often surprised by the impact this may have on the plans performance-wise.
Consider for example this:
CREATE TABLE foo (a INT UNIQUE, b FLOAT);
INSERT INTO foo SELECT i, random() FROM generate_series(1,1000000) s(i);
CREATE TABLE bar (a INT);
INSERT INTO bar SELECT i FROM generate_series(1,1000000) s(i);
WITH foo_filtered AS (SELECT * FROM foo WHERE b < 0.5)
SELECT * FROM bar JOIN foo_filtered USING (a) LIMIT 10;
QUERY PLAN
----------------------------------------------------------------------------------------
Limit (cost=36986.19..38236.83 rows=10 width=12)
CTE foo_filtered
-> Seq Scan on foo (cost=0.00..17906.00 rows=510375 width=12)
Filter: (b < '0.5'::double precision)
-> Hash Join (cost=19080.19..63848915.94 rows=510375 width=12)
Hash Cond: (bar.a = foo_filtered.a)
-> Seq Scan on bar (cost=0.00..14425.00 rows=1000000 width=4)
-> Hash (cost=10207.50..10207.50 rows=510375 width=12)
-> CTE Scan on foo_filtered (cost=0.00..10207.50 rows=510375 width=12)
(9 rows)
and without the CTE: SELECT * FROM bar JOIN foo USING (a) WHERE b < 0.5 LIMIT 10;
QUERY PLAN
----------------------------------------------------------------------------------
Limit (cost=0.42..10.26 rows=10 width=12)
-> Nested Loop (cost=0.42..502029.00 rows=510375 width=12)
-> Seq Scan on bar (cost=0.00..14425.00 rows=1000000 width=4)
-> Index Scan using foo_a_key on foo (cost=0.42..0.48 rows=1 width=12)
Index Cond: (a = bar.a)
Filter: (b < '0.5'::double precision)
(6 rows)
All because the planner was unable to consider the join condition and the LIMIT 10, when planning the CTE.This is good to know. Most of my work is in Microsoft SQL Server, and its query planner does allow other clauses to be pushed into the evaluation of the CTE.
In MSSQLServer, there is no expectation that a CTE is evaluated only once, just that is defined once - it is just an in-line view.
I'll play with this later today and see if it performs any better. In any case, it makes the query easier to read, so we'll probably use this unless it somehow makes the function slower.
Thanks again!
Interesting, and very surprising! I'm going to dig around and see if I can't figure out why this is the case.
I would have guessed there would be a minor performance improvement, because the first_value approach effectively allows PostgreSQL to discard the data sooner, instead of materializing it in an additional subquery.
I implemented several helper functions around this. Basically I have a row based function that makes sets unique then I have aggregation functions that give me the cardinality of these sets. I use intset when I can, otherwise I use the "key" part of hstore in substitute of the missing set structure
I've been pushing hard to use: https://github.com/aggregateknowledge/postgresql-hll
Being able to switch algorithms based on size IMO makes that one of the most badass thing about postgres right now.
HSTORE vs. JSON: Lower storage overhead, faster operations on stored data.
HSTORE vs. JSONB: Significantly lower storage overhead but more limited
That changed with JSONB, which got introduced in 9.4 - that's essentially HSTORE2, with additional improvements.