Let's talk about joins
cghlewis.com
cghlewis.com
For left join this isn’t entirely true. If there are more matching cases on the right joined table then you’ll get additional rows for each match. That is unless you take steps to ensure only at most one row is matched per row on the left (eg using something like DISTINCT ON in Postgres)
> Until now we have discussed scenarios that are considered one-to-one merges. In these cases, we only expect one participant in a dataset to be joined to one instance of that same participant in the other dataset.
> However, there are scenarios where this will not be the case…
I wrote about this: https://minimalmodeling.substack.com/p/many-explanations-of-...
I also wrote about this ;-) https://pragmathics.nl/2023/10/24/putting-the-relational-bac...
Of course this requirement isn't ever enforced because the real world isn't kind enough to give strictly modelled data. It would simplify the query language a lot though.
"Vertical join" is normally called union.
"Right join" is just (bad) syntax sugar and should be avoided. Left join with the tables reversed is the usual convention.
The join condition for inner is optional- if it's always true then you get a "cross join". Can be useful to show all the possible combinations of two fields.
A “vertical join” (SQL UNION) is one of the “non-left joins”. How can you transform a “vertical join” to a “left join”?
WITH top_t AS (
SELECT
a
,b
,c
,'top' as nonexistent_col
FROM table_1
), bottom_t AS (
SELECT
a
,b
,c
,'bottom' as nonexistent_col
FROM table_2
)
SELECT
COALESCE(top_t.a, bottom_t.a) AS a
,COALESCE(top_t.b, bottom_t.b) AS b
,COALESCE(top_t.c, bottom_t.c) AS c
FROM top_t FULL OUTER JOIN bottom_t
ON top_t.a = bottom_t.a
AND top_t.b = bottom_t.b
AND top_t.c = bottom_t.c
AND top_t.nonexistent_col = bottom_t.nonexistent_col -- remove this for a normal UNIONAssuming a null we can use as sentinel:
SELECT COALESCE(a.col, b.col) AS col
FROM a
LEFT JOIN b ON (TRUE)
WHERE (a.col IS NULL) IS DISTINCT FROM (b.col IS NULL)
(Getting that sentinel might take effort, depending on eg whether there are useful window functions. Here is a way to do it with only left joins and the assumption the column has no duplicate values: WITH crossed AS (
SELECT * FROM UNNEST([1, 2]) AS sentinal
)
SELECT IF(sentinal = 1, col, NULL) AS col
FROM a
LEFT JOIN crossed ON TRUE
WHERE sentinal = 1 OR col = (SELECT * FROM a LIMIT 1)I agree that it is seldom good query-writing practice, but I think it makes sense in education because it rounds out the join variations to name them symmetrical, and thus easier to remember.
Now consider b LEFT JOIN a LEFT JOIN c. The result is not the same at all.
a LEFT JOIN b RIGHT JOIN c
is equivalent to (a LEFT JOIN B) RIGHT JOIN c.
Once you have that, you can flip the RIGHT JOIN by swapping the operands, giving you c LEFT JOIN (a LEFT JOIN b). c left join (
a left join b on a.a_id = b.a_id
) on c.a_id = a.a_id
The parenthesis syntax is also super useful when you want to left join to a group of tables that all need to inner join together: a left join (
b inner join c
on b.b_id = c.b_id
inner join d
on c.c_id = d.c_id
inner join e
on d.e_id = e.e_id
)
on a.b_id = b.b_id
that way, the b/c/d relation is all or nothing instead of the possibility of get values from b but not c and d.https://github.com/totalhack/zillion
Disclaimer: this project is currently a one man show, though I use it in production at my own company.
Part of this is the columnar layout that means it can avoid reading columns that are not involved in the query. However it’s also able to push query predicates into the table scan, using metadata (like bloom filters) that tell it what values are in each chunk of data.
But for joins, you typically end up needing to read all of the data and materialize it in memory.
For realtime joins the best option is to do it in a steaming fashion on ingestion, for example in a system like Flink or Arroyo [0], which I work on.
https://docs.estuary.dev/concepts/derivations/
Fully managed, UI and config driven. Write SQLite programs using lambdas that are driven by your source events, in order to join data, do streaming transaction processing, and no doubt lots of other things we haven't thought of.
One option in Flink is to load the entire fact table into the pipeline (using the filesystem source or a custom operator) and join against that. This provides good performance, but at the cost of managing additional long-running state in the pipeline (and potentially long startup times). This works pretty well for very small fact tables (stuff like currency conversions, B2B customer data, etc.).
The other option is to store the fact table in a database and query it dynamically. Flink SQL has explicit support for this (called "lookup joins") but this requires careful tuning to not overwhelm your database with high-volume streaming traffic (particularly when doing bootstrapping or recovery).
Doing these sorts of joins is a huge use case, and definitely something we're trying to improve in Arroyo.
[1] https://nightlies.apache.org/flink/flink-docs-master/docs/de...
Natural joins are naturally more implicit, and while SQL tends to be a little bit verbose, given the significance of data integrity and the dififculty of testing SQL, the trade-off goes very clearly towards being explicit.
Step 1: use natural join. Life is great. Step 2: someone adds a `comment` field on table A. Life is great. Step 3: someone adds a `comment` field on table B. Ruh roh.
I'll use them in short-lived personal projects, but not on something where I'm collaborating with other people on software that evolves over several years.
CTE_A AS (SELECT ... comment as comment_a from A...),
CTE_B AS (SELECT ... comment as comment_b from B...)This is a very positive spin on "you have to manage column names very rigorously for this strategy to be sustainable".
No, records with the same join-column value are multiplied.
Joins can be also categorized by the used join algorithm.
The simplest join is a nested loop join. In this case a larger dataset is iterated and another small dataset is combined using a simple key lookup.
Then there is a merge join. In this case two larger datasets are first sorted using merge sort and then aligned linearly.
Then there is a hash join. For the hash join a hash table is generated first based on the smaller dataset and the larger dataset is iterated and the join is made by making a hash lookup to the generated hash table.
The difference between nested loop join and hash join might be confusing.
In case of a nested loop join the second table is not loaded from the storage medium first, instead an index is used to lookup the location of the records. This has O(log n) complexity for each lookup. For hash join the table is loaded and hash table is generated. In this case each lookup has O(1) complexity but creation of hash table is expensive (it has O(n) complexity) and is only worth the cost when the dataset is relatively large.
A popular one with time series is the asof join.
There are also literal joins, which are generalizations of adjacent difference.
Is there some reason to use non-standard terminology for posts that are trying to be in-depth, authoritative tutorials ?
- Ability to specify whether your join should be one-to-one, many-to-one, etc. So that R will throw an error instead of quietly returning 100x as many rows as expected (which I've seen a lot in SQL pipelines).
- A direct anti_join function. Much cleaner than using LEFT JOIN... WHERE b IS NULL to replicate an anti join.
- Support for rolling joins. E.g. for each user, get the price of their last transaction. Super common but can be a pain in SQL since it requires nested subqueries or CTEs.
This should actually be "Let's try this again using dplyr, one of the most elegant pieces of software ever written".
Hadley Wickham is a treasure to humanity.
https://code.kx.com/q/ref/asof/
https://code.kx.com/q/learn/brief-introduction/#time-joins
https://clickhouse.com/docs/en/sql-reference/statements/sele...
https://duckdb.org/docs/guides/sql_features/asof_join.html
> Do you have time series data that you want to join, but the timestamps don’t quite match? Or do you want to look up a value that changes over time using the times in another table?
DuckDB blog post on temporal joins:
https://duckdb.org/2023/09/15/asof-joins-fuzzy-temporal-look...
note: this idea is very useful for timestamps but can also be used for other types of values
> Similar to horizontal joins, there are many use cases for joining data horizontally, also called appending data.
Shouldn't this read "joining data vertically"? This seems like a typo.
Disgusting
Anyway, I declare and coin that VLOOKUP is a LEFT JOIN