CTEs as lookup tables
misfra.me
misfra.me
with source as (
select * from wherever
),
transformed as (
...
),
joined as (
...
),
final as (
...
)
select * from final
You can switch `final` to `transformed` to see what the query is doing internally. Almost like having good control flow. Almost.source = ...
transformed = ...
joined = ...
final = ...
generally can't just swap transformed in final because final depends on the output of transformed?
This works as long as your CTE’s are correct SQL and only the logic is wrong or suspect.
From a debug point of view that feels more convenient to _me_.
Edit: also I don’t like to leave a mess behind and CTE’s don’t require cleaning up afterwards.
I appreciate that tools like dbt allow materialization options as both CTEs and views/tables/etc because being able to pivot between them is super nice.
I've seen cases where a few GBs of product data gets pulled into a CTE this way. Queries take 10s to complete from processing GBs of unindexed data
(I’d also love one that lets Postgres use a covering index for primary storage. This can reduce data size and load less data off disk due to locality. MySQL has this by default and SQLite has it via WITHOUT ROWID tables.)
Just tried the original query, but with explicitly not materialized CTEs and the runtime was basically as fast as the manually pre-filtered solution.
Now you know why I spam all the “CTEs are amazing” threads about materialization (there have been 5 or so in the last few months)
This was for a nightly data importer and it was filled with queries like that -- I only fixed the one giving us issues -- but I bet I could have reduced the total runtime by 90%.
In SQL you're describing the result that you want -- you aren't (and shouldn't be) saying how to achieve that result. The engine goes off and (hopefully) finds the optimal way to create the result you are describing. When you break queries down too far, the engine has more trouble finding the optimal path.
SQL Server can "see through" views and CTEs optimize them combined with a query but depending on the calculations it might have to generate and process intermediate data that it doesn't need. In my case above, the engine was reprocessing the same data set over and over, generating duplicate rows, and combining everything removing the duplicates.
My query was significantly simpler but also, in a way, harder to follow. It wasn't as clear what the intent was. Where they had nice named CTE subqueries, I had to use comments.
The result of the query is say declaration X. In this case, imagine you can construct a query to formulate X with out a CTE. The database goes off and computes X from the data.
But if you start adding CTE's for say readability or because you're building up the query piece by piece and testing it in stages. You might be adding additional declarations A, B, and C and combining them to ultimately get X. In theory, the database engine should be able to see that you're still trying to get X and perform the query that same way as if it was a single query without a CTE. And, in some cases, it can actually do that.
But if A, B, and C are complicated enough then it's just unable to determine that it all equals X in the end. This is a fairly understandable limitation of the engine -- SQL can get pretty complicated. It's also making the assumption that all these CTE's are actually needed to compute the result. It took me a whole day to convert a CTE based query into a non-CTE based query and get all the same results in the same way.
Unless you have realistic data in the test database the performance behavior of a query can be unpredictable.
SQL is very leaky abstraction.
I wrote some very elegant, readable SQL to perform a complex query. It was dog slow.
I handed it to a DBA, they ripped out the CTEs and replaced them all with temp tables. The query was an unreadable mess at the end, but boy was it orders of magnitude faster.
…on real or at least representative data.
Yes, materialization is fine for small hardcoded values in the post, but for most other lookup tables, it's definitely not fine.
For example, you read this article and think great, I'll make a lookup table to map various ids to be linked across tables.
with ids as (
select u.user_id, u.token, s.customer_id
from users u
join stripe_customer s on s.user_id = u.user_id
),
user_impressions as (
select i.*, ids.id
from impressions i join ids using token
),
payments as (
select p.*, ids.id from payments p
join ids using customer_id
),
select sum(i.views),
sum(i.clicks),
sum(p.amount)
from payments p
join user_impressions i using user_id
where user_id = $1
group by date;
You think, great, this gets some stats for one user since its filtered at the end, and with indexes on stripe_customer(user_id), user_impressions(token), and payments(customer_id), this will be really fast and efficient!Nope.. since ids is referenced more than once in this query, in Postgres this causes the ids CTE to be materialized onto disk with no indexes. So not only does it take up lots of extra space on disk to store all the users in your system, to join user_impressions and payments you have to O(N) search across the ids dataset. No indexes on a materialized table.
It would be a lot faster to join to users or stripe_customer or both in a loop with indexes than O(N) search through all the users in your system.
This is particularly dangerous because if you remove the payments part, ids only has one reference, so Postgres doesn't materialize the table, and everything is fast.
This can be fixed by adding WITH foobar AS NOT MATERIALIZED to the CTE syntax. IMO it should be a syntax error to not specify AS MATERIALIZED or AS NOT MATERIALIZED.. The default has too many potential performance problems and folks should have to think about whether they want materialization or not.
next step: include cte-to-subquery translation step as a part of your build pipeline. never needed tools like that but i guess they must exist since it's been such a common issue. using gpt for this is like nuking a mosquito from orbit.
In what world is this
WITH countries (code, name) AS (
...> SELECT \* FROM (VALUES
...> ('us', 'United States'), ('fr', 'France'), ('in', 'India')
...> ) AS codes
...> )
...> SELECT data.code, name FROM data LEFT JOIN countries ON countries.code = data.code;
easier to read than this SELECT code,
...> CASE code
...> WHEN 'us' THEN 'United States'
...> WHEN 'fr' THEN 'France'
...> WHEN 'in' THEN 'India'
...> END AS country
...> FROM data;It is perhaps not surprising that we end up with this sort of stuff in production code, because the original author only needed one simple CASE statement and then it organically grew from there.
If you were to run the first query on a fresh db, it’d return data. Running the second would fail.
Is there any reason syntactically the SELECT needs to be required? If you use a VALUES table literal as a subselect, you have to give it column names with an AS clause (see https://www.postgresql.org/docs/current/sql-values.html). I can imagine a simpler syntax where you'd do WITH VALUES ... AS table_name (column_1_name, ...).
Is there any reason to alias the lookup table as `codes`?
There are apparently lots of other clever uses of the WITH clause, such as https://www.postgresql.org/docs/current/queries-with.html#QU...
Basically, i could put with: values after the join.
CREATE TABLE vals (x, y);
INSERT INTO vals VALUES (1, 1), (2, 0.5), (3, 0.4), (4, 0.1), (5, 0);
WITH
m(v) AS (
SELECT ((COUNT(*) * (SUM(x * y))) - (SUM(x) * SUM(y))) / ((COUNT(*) * SUM(POW(x, 2))) - (POW(SUM(x), 2)))
FROM vals
),
b(v) AS (
SELECT (SUM(y) - (m.v * SUM(x))) / COUNT(*)
FROM vals JOIN m
)
SELECT
x AS real_x,
y AS real_y,
m.v AS m,
b.v AS b,
x * m.v + b.v AS interp_y
FROM vals JOIN m JOIN b;
real_x real_y m b interp_y
------ ------ ----- ---- -------------------
1 1 -0.24 1.12 0.88
2 0.5 -0.24 1.12 0.64
3 0.4 -0.24 1.12 0.4
4 0.1 -0.24 1.12 0.16
5 0 -0.24 1.12 -0.0800000000000001
If all you want is "m" and "b" the final query can just be "FROM m JOIN b" and the result will be 1 row.This seems like a case where it would be nice to be able to pass in a parameter `vals (x, y)`.
CREATE TABLE view_confs (key, value);
INSERT INTO view_confs VALUE ('start', 2), ('end', 4);
Now prefix it with another one: vals_range(x, y) AS (
SELECT x, y
FROM vals
WHERE x >= (SELECT value FROM view_confs WHERE key = 'start')
AND x < (SELECT value FROM view_confs WHERE key = 'end')
),
And swap the rest of the original query to use "vals_range" instead of "vals". Just gotta remember to update view_confs with the new range whenever you want to change the view. I think it should work inside a transaction to avoid multiple threads interfering with each other, but also to be clear: This is a workaround for sqlite not having stored procedures, and not wanting to implement it in code (so I can JOIN to the view in other queries). Better to use stored procedures in other databases than this workaround.I'm not sure I agree re performance btw. I've fixed a lot of slow mssql queries by changing them in the other direction (EDIT: or just adding a join). I do see how temp tables might shorten the time you hold locks---but then you might be subverting your transaction isolation level.
My hypothesis is that while memory use my be the same either way, you don't have the same transactional/locking requirements/overhead/consistency with multi statements as you do with a single big set of CTEs. And for analytics you rarely need that transactional isolation required by a single big CTE.
I can't speak for others, but most of my CTEs tend to be small enough where an index wouldn't help much. Of course toggling MATERIALIZED is an easy way to see whether the equivalent of a temp table would work best or if the planner can successfully include the body of CTE in optimization. A lot harder syntactically to swap out a subquery for a temp table, which is what the MATERIALIZED keyword essentially does under the hood (at least in Postgres).
WRT subverting the transaction isolation level, using temp tables is a tacit decision to do so and best for non-vital read queries in my own experience. That is, I used it for lookups of tables that did not _need_ to be completely up to date, or when I knew the data _would_ be up to date.
Less known is that you can get the same effect using a TOP N statement where N is a number larger than the rows you might get back. This presumably avoids the additional i/o penalty of the temp table creation, but I've never validated that. This trick likely is portable across db engines, too.
See https://bertwagner.com/posts/does-the-join-order-of-my-table... - Adam Machanic has some good videos diving deeper into this.
WITH countries (code, name) AS (
SELECT * FROM (VALUES
ROW('us', 'United States'),
ROW('fr', 'France'),
ROW('in', 'India')
) AS codes
)
SELECT data.code, name FROM data LEFT JOIN countries.code = data.code; SELECT transform(id, [1, 2, 11], ['ClickHouse', 'Postgres', 'MongoDB'], '')
It creates a lookup table. When you are using the CASE operator, in simple cases it will use the `transform` function under the hood.There are more advanced capabilities:
- The `Join` table engine. It is a pre-warmed state for joining, a hash table kept in memory.
- Dictionaries. Pre-warmed, automatically updated lookup data structures from various sources. For example, you can connect a dictionary of company names from your operational database while keeping only the ids in ClickHouse.
And, just in case, the same CTE works as in SQLite:
WITH countries AS (
SELECT c1 AS code, c2 AS name FROM VALUES(
('us', 'United States'), ('fr', 'France'), ('in', 'India')))
SELECT data.code, name FROM data LEFT JOIN countries ON countries.code = data.code;
Example: milovidov-desktop :) WITH countries AS (
SELECT c1 AS code, c2 AS name FROM VALUES(
('us', 'United States'), ('fr', 'France'), ('in', 'India')))
SELECT data.code, name FROM (SELECT 'us' AS code) AS data LEFT JOIN countries ON countries.code = data.code;
┌─code─┬─name──────────┐
│ us │ United States │
└──────┴───────────────┘
Disclaimer: I'm working on ClickHouse.Also not-mat CTEs can perform a lot worse: https://stackoverflow.com/questions/64016236/postgres-12-mat...
But so can MAT CTEs: https://dba.stackexchange.com/questions/257014/are-there-sid...
So the limitations are that it’s very much ymmv.
Not sure you’re completely on target here regarding CTE performance. I don’t have deep insight into Postgresql (but “create temporary view", yeah!). MSSQL does a pretty good job in Sql2019. If the predicate is sargeable in some way pushdown is reliable. If performance is a concern, examining the residuals can lead to significant insights e.g. applying index filtering which solves obvious problems. Recursion is another story.
CTE is more likely used by data analyst queries because it is an abstraction of composition. It’s not a great abstraction but it’s better than nothing, which is mostly what you get with SQL.
For this specific use case though, my comment was made because CASE statements tend to just be bad in comparison to CTEs. Probably dependent on engine though, I don't know them all by heart.
‘’’ WITH CTE AS ( SELECT ‘NY’ AS code, ‘New York’ AS state UNION SELECT ‘NJ’, ‘New Jersey’ ) SELECT * FROM CTE ‘’’
… likewise you can also do that in a subquery that you can immediately join:
‘’’ SELECT * FROM ( SELECT ‘NY’ AS code, ‘New York’ AS state UNION SELECT ‘NJ’, ‘New Jersey’ ) SUB INNER JOIN AnotherTable AT ON SUB.code = AT.code ‘’’
WITH my_cte AS (
SELECT \* FROM VALUES
(1, 'column 2 value', 3.0),
(2, 'column 2 value', 3.0),
(3, 'column 2 value', 3.0),
(4, 'column 2 value', 3.0)
)
you can often alias the VALUES values like: WITH my_cte AS (
SELECT \* FROM VALUES
(1, 'column 2 value', 3.0),
(2, 'column 2 value', 3.0),
(3, 'column 2 value', 3.0),
(4, 'column 2 value', 3.0)
as t(col1_name, col2_name, col3_name)
)
and some DB's allow you to alias via the cte name: WITH my_cte(col1_name, col2_name, col3_name) AS (
SELECT \* FROM VALUES
(1, 'column 2 value', 3.0),
(2, 'column 2 value', 3.0),
(3, 'column 2 value', 3.0),
(4, 'column 2 value', 3.0)
)WITH CTE1 AS ( SELECT.... ) CTE2 AS ( INSERT....RETURNING insertid ) INSERT INTO TABLE2 insertid as FK, otherdata...
The CTEs are executed in define-order as a single complete transaction, no need to use multiple calls to insert relational data
Worth noting that "Row values were added to SQLite version 3.15.0 (2016-10-14)" - my experience is that older versions of SQLite are a lot more common than I would hope they would be. https://www.sqlite.org/rowvalue.html
It's better when you need a lookup to multiple columns but will not be needed in any other query/ETL job. Might be steps in a funnel or values in a bespoke customer discount query.
Some DBs do allow struct/array fields but joining against a constant CTE is portable and has low overhead.
If, eventually, a few CTE distillations become common enough, then yeah, that's grounds for a request to basically shove that CTE into a view so other people can use it.
I use ctes for everything and do my joins at the end.
This seems far more readable.