This page describes how to join data in Kafka with in Lenses SQL Processors.
Joins allow rows from different sources to be combined.
Lenses allows two sources of data to be combined based either on the equality of their _key facet or using a user-provided expression.
A query using joins looks like a regular query apart from the definition of its source and in some cases the need to specify a window expression:
projection: the projection of a join expression differs very little from a regular projection. The only important consideration is that since data is selected from two sources, some fields may be common to both. The syntax table.field
is recommended to avoid this type of problem.
sourceA/sourceB : the two sources of data to combine.
window: only used if two streams are joined. Specifies the interval of time to search matching results.
joinExpression: a boolean expression that specifies how the combination of the two sources is calculated.
filterExpression: a filter expression specifying which records should be filtered.
When two sources of data are combined it is possible to control which records to keep when a match is not found:
Disclaimer: The following examples do not take into consideration windowing and/or table materialization concerns.
Customers
Orders
This join type will only emit records where a match has occurred.
(Notice there’s no item with customer.id = 2
nor is there a customer with id = 3
so these two rows are not present in the result).
This join type selects all the records from the left side of the join regardless of a match:
(Notice all the rows from orders are present but since no customer.id = 3
no name can be set.)
A right join can be seen as a mirror of a LEFT JOIN. It selects all the records from the right side of the join regardless of a match:
An outer join can be seen as the union of left and right joins. It selects all records from the left and right side of the join regardless of a match happening:
By default, if no ON expression is provided, the join will be evaluated based on the equality of the _key facet. This means that the following queries are equivalent:
When an expression is provided, however, there are limitations regarding what kind of expressions can be evaluated.
Currently, the following expression types are supported:
Equality expressions using equality (=) with one table on each side:
customers.id = order.user_id
customers.id - 1 = order.user_id - 1
substr(customers.name, 5) = order.item
Any boolean expression which references only one table:
len(customers.name) > 10
substr(customer.name,1) = "J"
len(customer.name) > 10 OR customer_key > 1
Allowed expressions mixed together using an AND operator:
customers._key = order.user_id AND len(customers.name) > 10
len(customers.name) > 10 AND substr(customer.name,1) = "J"
substr(customers.name, 5) = order.item AND len(customer.name) > 10 OR customer_key > 1
Any expressions not following the rules above will be rejected:
More than one table is referenced on each side of the equality operator
concat(customer.name, item.name) = "John"
customer._key - order.customer_id = 0
a boolean expression not separated by an AND references more than one table:
customer._key = 1 OR customer._key = order.customer_id
When two streams are joined Lenses needs to know how far away in the past and in the future to look for a matching record.
This approach is called a “Sliding Window” and works like this:
Customers
Purchases
At t=10
, when both the Computer and the Keyboard records arrive, only one customer can be found within the given time window (the specified window is 5s thus the window will be [10-5,10+5]s ).
This means that the following would be the result of running the query:
Note: John will not match the Keyboard purchase since t=20s is not within the window interval [10-5,10+5]s.
When streaming data, records can be produced at different rates and even out of order. This means that often a match may not be found because a record hasn’t arrived yet.
The following example shows an example of a join between a stream and a table where the arrival of the purchase information is made available before the customers’ information is.
(Notice that the purchase of a “Keyboard” by customer_id = 2
is produced before the record with the customer details is.)
Customers
Purchases
Running the following query:
would result in the following:
If later, the record for customer_id = 2
is available:
a record would be emitted with the result now looking like the following:
Notice that “Keyboard” appears twice, once for the situation where the data is missing and another for when the data is made available.
This scenario will happen whenever a Stream is joined with a Table using a non-inner join.
The following table shows which combinations of table/stream joins are available:
In order to evaluate a join between two sources, the key facet for both sources has to share the same initial format.
If formats are not the same the join can’t be evaluated. To address this issue, an intermediate topic can be created with the correct format using a STORE AS statement. This newly created topic can then be created as the new source.
In addition to the constraint aforementioned, when joining, it’s required that the partition number of both sources be the same.
When a mismatch is found, an additional step will be added to the join evaluation in order to guarantee an equal number of partitions between the two sources. This step will write the data from the source topic with a smaller count of partitions into an intermediate one.
This newly created topic will match the partition count of the source with the highest partition count.
In the topology view, this step will show up as a Repartition Node.
Joining two topics is only possible if the two sources used in the join share the same key shape and decoder.
When an ON statement is specified, the original key facet will have to change so that it matches the expression provided in the ON statement. Lenses will do this calculation automatically. As a result, the key schema of the result will not be the same as either one of the sources. It will be a Lenses calculated object equivalent to the join expression specified in the query.
As discussed when addressing join types, some values may have null values when non-inner joins are used.
Due to this fact, fields that may have null values will be typed as the union of null and their original type.
Within the same query, joins may only be evaluated between two sources.
When a join between more than two sources is required, multiple queries can be combined using a WITH statement:
In order to group the results of a join, one just has to provide a GROUP BY
expressions after a join expression is specified.
Purchases
When a join between a table and a join is processed, lenses will, for each stream input (orders in the example above), look for a matching record on the specified table (customers).
Notice that the record with Frank’s purchase information is processed at t = 10s
at which point Frank’s Customer information hasn’t yet been processed. This means that no match will be found for this record.
At t=20s
however, the record with Frank’s customer information is processed; this will only trigger the emission of a new record if an Outer Join is used.
There are some cases where filter expressions can help optimize a query. A filter can be broken down into multiple steps so that some can be applied before the join node is evaluated. This type of optimization will reduce the number of records going into the join node and consequentially increase its speed.
For this reason, in some cases, filters will show up before the join in the topology node.
_key | id | name |
---|---|---|
_key | customer_id | item |
---|---|---|
name | item |
---|---|
name | item |
---|---|
name | item |
---|---|
name | item |
---|---|
arrival | id | name |
---|---|---|
arrival | item | customer_id |
---|---|---|
name | item |
---|---|
arrival | id | name |
---|---|---|
arrival | item | customer_id |
---|---|---|
name | item |
---|---|
arrival | id | name |
---|---|---|
name | item |
---|---|
Left | Right | Allowed types | Window | Result |
---|---|---|---|---|
emmited | processed | id | name |
---|---|---|---|
arrival | processed | item | customer_id |
---|---|---|---|
1
1
John
2
2
Frank
1
1
Computer
2
1
Mouse
3
3
Keyboard
John
Computer
John
Mouse
John
Computer
John
Mouse
null
Keyboard
John
Computer
John
Mouse
null
Keyboard
John
Computer
John
Mouse
null
Keyboard
Frank
null
t = 6s
1
Frank
t = 20s
2
John
t = 10s
Computer
1
t = 11s
Keyboard
2
Frank
Computer
null
Keyboard
t = 0s
1
Frank
t = 10s
1
John
t = 0s
Computer
1
t = 1s
Keyboard
2
Frank
Computer
null
Keyboard
t = 10s
2
John
Frank
Computer
null
Keyboard
John
Keyboard
Stream
Stream
All
Required
Stream
Table
Table
All
No
Table
Table
Stream
RIGHT JOIN
No
Stream
Stream
Table
INNER, LEFT JOIN
No
Stream
t = 0s
t = 20s
1
Frank
t = 0s
t = 10s
Computer
1