<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Artem Chebotko</title>
    <description>The latest articles on DEV Community by Artem Chebotko (@artemchebotko).</description>
    <link>https://dev.to/artemchebotko</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F748368%2F53c7d630-d074-4ee4-a6cf-78888e7e7178.png</url>
      <title>DEV Community: Artem Chebotko</title>
      <link>https://dev.to/artemchebotko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/artemchebotko"/>
    <language>en</language>
    <item>
      <title>Five Data Models for IoT: Managing the Latest IoT Events Based on a State in Apache Cassandra</title>
      <dc:creator>Artem Chebotko</dc:creator>
      <pubDate>Thu, 28 Jul 2022 17:36:42 +0000</pubDate>
      <link>https://dev.to/datastax/five-data-models-for-iot-managing-the-latest-iot-events-based-on-a-state-in-apache-cassandra-47n1</link>
      <guid>https://dev.to/datastax/five-data-models-for-iot-managing-the-latest-iot-events-based-on-a-state-in-apache-cassandra-47n1</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxou6ju5g6mdoxzteibdc.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxou6ju5g6mdoxzteibdc.jpg" alt="Image description" width="350" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;© Shutterstock / everything possible&lt;/p&gt;

&lt;p&gt;Apache Cassandra is a rock-solid choice for managing IoT and time series data at scale. The most popular use case of storing, querying and analyzing time series generated by IoT devices in Cassandra is well-understood and documented. In general, a time series is stored and queried based on its source IoT device. However, there exists another class of IoT applications that require quick access to the most recent data generated by a collection of IoT devices based on a known state. The question that such applications need to answer is: Which IoT devices or sensors are currently reporting a specific state? In this blog post, we focus on this question and provide five possible data modeling solutions to efficiently answer it in Cassandra.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Introduction&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The Internet of Things (IoT) is generating massive amounts of time series data that needs to be stored, queried, and analyzed. Apache Cassandra is an excellent choice for this task: not only because of its speed, reliability and scalability, but also because its internal data model has built-in support for time-ordered data.&lt;/p&gt;

&lt;p&gt;In Cassandra, time series are usually stored and retrieved by a source (e.g., an IoT device or sensor) or a subject (e.g., a parameter or metric). There are many good resources that cover this topic in great detail, including &lt;a href="https://www.datastax.com/resources/video/datastax-accelerate-2019-designing-time-series-applications-scale-datastax" rel="noopener noreferrer"&gt;this conference presentation video&lt;/a&gt;, and ready-to-use Cassandra data models for &lt;a href="https://www.datastax.com/learn/data-modeling-by-example/sensor-data-model" rel="noopener noreferrer"&gt;sensor data&lt;/a&gt; and &lt;a href="https://www.datastax.com/learn/data-modeling-by-example/time-series-model" rel="noopener noreferrer"&gt;time series&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this post, we look at a related class of IoT use cases that need to manage a snapshot of the latest data coming from many IoT devices. Moreover, such a snapshot needs to be queried or filtered based on a specific state reported by IoT devices. In other words, we should be able to quickly answer this question in Cassandra: Which IoT devices are currently reporting a specific state? For many real-life use cases, this question may sound more like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which lights are currently on (off) in a smart home?&lt;/li&gt;
&lt;li&gt;Which parking spots are currently occupied (unoccupied) in a parking structure?&lt;/li&gt;
&lt;li&gt;Which vehicles are currently available (unavailable) near a specific location?&lt;/li&gt;
&lt;li&gt;Which security alarms are currently triggered (activated, disabled) in an area?&lt;/li&gt;
&lt;li&gt;Which doors are currently opened (closed, locked, unlocked) in a building?&lt;/li&gt;
&lt;li&gt;Which fire detection sensors are currently reporting an abnormal (normal standby, error) state in a sensor network?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the following, we define the problem a bit more formally and propose five practical solutions with example CQL implementations.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The problem definition&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Given a collection of IoT devices or sensors that generate time-ordered sequences of events containing timestamps, data points and states, find the latest events with a known state reported by all IoT devices. The three key components of this problem are illustrated in the figure below and are described as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The input consists of time series generated by IoT devices. The time series are generally stored in one or more Cassandra tables.&lt;/li&gt;
&lt;li&gt;The intermediate view is a snapshot of only the latest events reported by the IoT devices. It is possible to either store the latest events separately and explicitly, or compute them dynamically from the input.&lt;/li&gt;
&lt;li&gt;The final result is all the latest events with a known state. The latest events with the same state should be either stored together or readily computable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxt1xiz92fkbqf2i2xoxa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxt1xiz92fkbqf2i2xoxa.png" alt="Image description" width="768" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Managing the Latest IoT Events Based on a State&lt;/p&gt;

&lt;p&gt;We identify several challenges to managing the latest IoT events based on a state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A snapshot of the latest events is constantly evolving. Additional effort may be required to incrementally capture any changes.&lt;/li&gt;
&lt;li&gt;A frequency of event occurrences is generally unpredictable. It may be difficult to partition and organize events based on only their timestamp components.&lt;/li&gt;
&lt;li&gt;A state can usually take only a few unique values. Partitioning and indexing data based on a low-cardinality column may result in large partitions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We use the following running example as a starting point. Table events_by_device is the input. This&lt;a href="https://www.datastax.com/learn/cassandra-fundamentals/tables-multi-row-partitions" rel="noopener noreferrer"&gt; table with multi-row partitions&lt;/a&gt; is designed to store time series, such that each partition corresponds to one device, and rows in a partition represent events with timestamps, states and values. Events within each partition are always sorted by their timestamps in the descending order. This table essentially stores one time series per partition. We insert five events into the table and retrieve the time series for one device. Moreover, in the second query, we demonstrate that it is possible to dynamically compute all the latest events for all devices. Unfortunately, we should not rely on this query to solve the problem: it can potentially become very expensive as it accesses every partition in the table.&lt;/p&gt;

&lt;p&gt;SEE ALSO: &lt;a href="https://jaxenter.com/oleary-interview-174607.html" rel="noopener noreferrer"&gt;“An automated testing program is easier to iterate on than starting from scratch”&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Schema&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- All events by device

CREATE TABLE events_by_device (

device_id  UUID,

timestamp  TIMESTAMP,

state   TEXT,

value   TEXT,

PRIMARY KEY((device_id), timestamp)

) WITH CLUSTERING ORDER BY (timestamp DESC);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Data&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Event 1-1
INSERT INTO events_by_device 
       (device_id, timestamp, state, value)
VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, 
        '2021-01-01 01:11:11', 'on', 'event 1-1');
-- Event 1-2
INSERT INTO events_by_device 
       (device_id, timestamp, state, value)
VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, 
        '2021-01-01 02:22:22', 'off', 'event 1-2');
-- Event 1-3
INSERT INTO events_by_device 
       (device_id, timestamp, state, value)
VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, 
        '2021-01-01 03:33:33', 'on', 'event 1-3');
-- Event 2-1
INSERT INTO events_by_device 
       (device_id, timestamp, state, value)
VALUES (22222222-aaaa-bbbb-cccc-12345678abcd, 
        '2021-02-02 01:11:11', 'off', 'event 2-1');
-- Event 3-1
INSERT INTO events_by_device 
       (device_id, timestamp, state, value)
VALUES (33333333-aaaa-bbbb-cccc-12345678abcd, 
        '2021-03-03 01:11:11', 'off', 'event 3-1');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Queries&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Find all events for a device
SELECT device_id, timestamp, state, value
FROM   events_by_device
WHERE  device_id = 11111111-aaaa-bbbb-cccc-12345678abcd;

 device_id                            | timestamp                       | state | value
--------------------------------------+---------------------------------+-------+-----------
 11111111-aaaa-bbbb-cccc-12345678abcd | 2021-01-01 03:33:33.000000+0000 |    on | event 1-3
 11111111-aaaa-bbbb-cccc-12345678abcd | 2021-01-01 02:22:22.000000+0000 |   off | event 1-2
 11111111-aaaa-bbbb-cccc-12345678abcd | 2021-01-01 01:11:11.000000+0000 |    on | event 1-1

-- Find the latest events for all devices
SELECT device_id, timestamp, state, value
FROM   events_by_device
PER PARTITION LIMIT 1;

 device_id                            | timestamp                       | state | value
--------------------------------------+---------------------------------+-------+-----------
 33333333-aaaa-bbbb-cccc-12345678abcd | 2021-03-03 01:11:11.000000+0000 |   off | event 3-1
 22222222-aaaa-bbbb-cccc-12345678abcd | 2021-02-02 01:11:11.000000+0000 |   off | event 2-1
 11111111-aaaa-bbbb-cccc-12345678abcd | 2021-01-01 03:33:33.000000+0000 |    on | event 1-3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that, we can assume that the number of events per device is not to exceed 100,000. Otherwise, we may have to further split partitions in table events_by_device by introducing another column into its partition key definition. Since this is not important for the problem we are solving in this post, let’s keep things simple.&lt;/p&gt;

&lt;p&gt;Given the problem definition and the running CQL example of IoT events, we are ready to describe the five solutions with different characteristics.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Solution 1: Materialized view&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The first solution requires a new table and a materialized view. Table latest_events_by_device is a table with &lt;a href="https://www.datastax.com/learn/cassandra-fundamentals/tables-single-row-partitions" rel="noopener noreferrer"&gt;single-row partitions&lt;/a&gt;, where each partition corresponds to a device and each row corresponds to the latest known event. The purpose of this table is to have a snapshot of only the latest events reported by the IoT devices. The table is also a base table for materialized view latest_events_by_state that enables querying the latest events using a state.&lt;/p&gt;

&lt;p&gt;Notice that exactly the same data is inserted into both tables events_by_device and latest_events_by_device. However, for the latter, inserts become &lt;a href="https://www.datastax.com/learn/cassandra-fundamentals/inserts-updates-deletes" rel="noopener noreferrer"&gt;upserts&lt;/a&gt; that update rows to the latest events.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Schema&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Latest known events by device
CREATE TABLE latest_events_by_device (
    device_id  UUID,
    timestamp  TIMESTAMP,
    state      TEXT,
    value      TEXT,
    PRIMARY KEY((device_id))
);

-- Latest events by state
CREATE MATERIALIZED VIEW latest_events_by_state AS 
  SELECT * FROM latest_events_by_device
  WHERE state IS NOT NULL AND device_id IS NOT NULL
PRIMARY KEY ((state), device_id);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Data&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Event 1-1
INSERT INTO latest_events_by_device 
       (device_id, timestamp, state, value)
VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, 
        '2021-01-01 01:11:11', 'on', 'event 1-1');
-- Event 1-2
INSERT INTO latest_events_by_device 
       (device_id, timestamp, state, value)
VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, 
        '2021-01-01 02:22:22', 'off', 'event 1-2');
-- Event 1-3
INSERT INTO latest_events_by_device 
       (device_id, timestamp, state, value)
VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, 
        '2021-01-01 03:33:33', 'on', 'event 1-3');
-- Event 2-1
INSERT INTO latest_events_by_device 
       (device_id, timestamp, state, value)
VALUES (22222222-aaaa-bbbb-cccc-12345678abcd, 
        '2021-02-02 01:11:11', 'off', 'event 2-1');
-- Event 3-1
INSERT INTO latest_events_by_device 
       (device_id, timestamp, state, value)
VALUES (33333333-aaaa-bbbb-cccc-12345678abcd, 
        '2021-03-03 01:11:11', 'off', 'event 3-1');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Queries&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Find all the latest events with state 'on'
SELECT state, device_id, timestamp, value
FROM   latest_events_by_state
WHERE  state = 'on';

 state | device_id                            | timestamp                       | value
-------+--------------------------------------+---------------------------------+-----------
    on | 11111111-aaaa-bbbb-cccc-12345678abcd | 2021-01-01 03:33:33.000000+0000 | event 1-3

-- Find all the latest events with state 'off'
SELECT state, device_id, timestamp, value
FROM   latest_events_by_state
WHERE  state = 'off';

 state | device_id                            | timestamp                       | value
-------+--------------------------------------+---------------------------------+-----------
   off | 22222222-aaaa-bbbb-cccc-12345678abcd | 2021-02-02 01:11:11.000000+0000 | event 2-1
   off | 33333333-aaaa-bbbb-cccc-12345678abcd | 2021-03-03 01:11:11.000000+0000 | event 3-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The materialized view solution has the following characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Applicability: state-based queries return 100K rows / 100MBs of data or less.&lt;/li&gt;
&lt;li&gt;Pros: the view is maintained automatically; excellent performance.&lt;/li&gt;
&lt;li&gt;Cons: materialized views have &lt;a href="https://www.datastax.com/learn/cassandra-fundamentals/materialized-views" rel="noopener noreferrer"&gt;a few limitations&lt;/a&gt;; data distribution may become skewed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To support multiple tenants, we can change the table primary key to PRIMARY KEY((tenant, device_id)) or PRIMARY KEY((tenant), device_id), and the materialized view primary key to PRIMARY KEY ((tenant, state), device_id). Multi-tenancy may also help improve data distribution.&lt;/p&gt;

&lt;p&gt;This data model can be a simple, effective and efficient choice for many applications, as long as you are aware of and willing to counteract the &lt;a href="https://www.datastax.com/learn/cassandra-fundamentals/materialized-views" rel="noopener noreferrer"&gt;materialized view limitations&lt;/a&gt;. Another less obvious advantage of this data model is how easy it would be to feed data from an event streaming platform like &lt;a href="https://www.datastax.com/blog/2021/01/four-reasons-why-apache-pulsar-essential-modern-data-stack" rel="noopener noreferrer"&gt;Apache Pulsar or Apache Kafka&lt;/a&gt;. All events can go to the base table and the materialized view takes care of the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Solution 2: Secondary index&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The second solution requires a new table and a secondary index. The table is the same as in the materialized view solution. Table latest_events_by_device is a table with &lt;a href="https://www.datastax.com/learn/cassandra-fundamentals/tables-single-row-partitions" rel="noopener noreferrer"&gt;single-row partitions&lt;/a&gt;, where each partition corresponds to a device and each row corresponds to the latest known event. The purpose of this table is to have a snapshot of only the latest events reported by the IoT devices. Secondary index latest_events_by_state_2i is created for this table to query the latest events based on a state.&lt;/p&gt;

&lt;p&gt;Once again, exactly the same data is inserted into both tables events_by_device and latest_events_by_device. However, for the latter, inserts become &lt;a href="https://www.datastax.com/learn/cassandra-fundamentals/inserts-updates-deletes" rel="noopener noreferrer"&gt;upserts&lt;/a&gt; that update rows to the latest events.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Schema&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Latest known events by device
CREATE TABLE latest_events_by_device (
    device_id  UUID,
    timestamp  TIMESTAMP,
    state      TEXT,
    value      TEXT,
    PRIMARY KEY((device_id))
);

-- Latest events by state
CREATE INDEX latest_events_by_state_2i 
ON latest_events_by_device (state);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Data&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Event 1-1
INSERT INTO latest_events_by_device
(device_id, timestamp, state, value)
VALUES (11111111-aaaa-bbbb-cccc-12345678abcd,
'2021-01-01 01:11:11', 'on', 'event 1-1');
-- Event 1-2
INSERT INTO latest_events_by_device
(device_id, timestamp, state, value)
VALUES (11111111-aaaa-bbbb-cccc-12345678abcd,
'2021-01-01 02:22:22', 'off', 'event 1-2');
-- Event 1-3
INSERT INTO latest_events_by_device
(device_id, timestamp, state, value)
VALUES (11111111-aaaa-bbbb-cccc-12345678abcd,
'2021-01-01 03:33:33', 'on', 'event 1-3');
-- Event 2-1
INSERT INTO latest_events_by_device
(device_id, timestamp, state, value)
VALUES (22222222-aaaa-bbbb-cccc-12345678abcd,
'2021-02-02 01:11:11', 'off', 'event 2-1');
-- Event 3-1
INSERT INTO latest_events_by_device
(device_id, timestamp, state, value)
VALUES (33333333-aaaa-bbbb-cccc-12345678abcd,
'2021-03-03 01:11:11', 'off', 'event 3-1');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Queries&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Find all the latest events with state 'on'
SELECT state, device_id, timestamp, value
FROM   latest_events_by_device
WHERE  state = 'on';

 state | device_id                            | timestamp                       | value
-------+--------------------------------------+---------------------------------+-----------
    on | 11111111-aaaa-bbbb-cccc-12345678abcd | 2021-01-01 03:33:33.000000+0000 | event 1-3

-- Find all the latest events with state 'off'
SELECT state, device_id, timestamp, value
FROM   latest_events_by_device
WHERE  state = 'off';

 state | device_id                            | timestamp                       | value
-------+--------------------------------------+---------------------------------+-----------
   off | 33333333-aaaa-bbbb-cccc-12345678abcd | 2021-03-03 01:11:11.000000+0000 | event 3-1
   off | 22222222-aaaa-bbbb-cccc-12345678abcd | 2021-02-02 01:11:11.000000+0000 | event 2-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The secondary index solution has the following characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Applicability: state-based queries return 100K rows / 100MBs of data or more; state-based queries are executed infrequently.&lt;/li&gt;
&lt;li&gt;Pros: may better distribute the query workload across nodes in a cluster when retrieving a large result set.&lt;/li&gt;
&lt;li&gt;Cons: secondary indexes have &lt;a href="https://www.datastax.com/learn/cassandra-fundamentals/secondary-indexes" rel="noopener noreferrer"&gt;a few limitations&lt;/a&gt;; performance may become unsatisfactory for real-time applications.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This data model can be a reasonable choice in some cases. In particular, when multi-tenancy is introduced by changing the table primary key to PRIMARY KEY((tenant), device_id), we can hit &lt;a href="https://www.datastax.com/learn/cassandra-fundamentals/secondary-indexes" rel="noopener noreferrer"&gt;the sweet spot&lt;/a&gt; of using secondary indexes for real-time transactional queries. That is when retrieving rows from a large multi-row partition based on both a partition key and an indexed column specified in a query predicate.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Solution 3: State-Partitioned Table&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The third solution relies on table latest_events_by_state to organize and query the latest events using a state. Every insert of an event with some state into this table must be accompanied by deletes of any outdated events with the other states for the same IoT device. In our example, we have one insert and one delete for each event, since we only have two unique states. If we were to have three possible states, each new event would result in one insert and two deletes.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Schema&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Latest events by state
CREATE TABLE latest_events_by_state (
    state      TEXT,
    device_id  UUID,
    timestamp  TIMESTAMP,
    value      TEXT,
    PRIMARY KEY((state), device_id)
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Data&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Event 1-1
INSERT INTO latest_events_by_state 
       (state, device_id, timestamp, value)
VALUES ('on', 11111111-aaaa-bbbb-cccc-12345678abcd, 
        '2021-01-01 01:11:11', 'event 1-1');
DELETE FROM latest_events_by_state 
WHERE state = 'off' AND
      device_id = 11111111-aaaa-bbbb-cccc-12345678abcd;
-- Event 1-2
INSERT INTO latest_events_by_state 
       (state, device_id, timestamp, value)
VALUES ('off', 11111111-aaaa-bbbb-cccc-12345678abcd, 
        '2021-01-01 02:22:22', 'event 1-2');
DELETE FROM latest_events_by_state 
WHERE state = 'on' AND
      device_id = 11111111-aaaa-bbbb-cccc-12345678abcd;
-- Event 1-3
INSERT INTO latest_events_by_state 
       (state, device_id, timestamp, value)
VALUES ('on', 11111111-aaaa-bbbb-cccc-12345678abcd, 
        '2021-01-01 03:33:33', 'event 1-3');
DELETE FROM latest_events_by_state 
WHERE state = 'off' AND
      device_id = 11111111-aaaa-bbbb-cccc-12345678abcd;
-- Event 2-1
INSERT INTO latest_events_by_state 
       (state, device_id, timestamp, value)
VALUES ('off', 22222222-aaaa-bbbb-cccc-12345678abcd, 
        '2021-02-02 01:11:11', 'event 2-1');
DELETE FROM latest_events_by_state 
WHERE state = 'on' AND
      device_id = 22222222-aaaa-bbbb-cccc-12345678abcd;
-- Event 3-1
INSERT INTO latest_events_by_state 
       (state, device_id, timestamp, value)
VALUES ('off', 33333333-aaaa-bbbb-cccc-12345678abcd, 
        '2021-03-03 01:11:11', 'event 3-1');
DELETE FROM latest_events_by_state 
WHERE state = 'on' AND
      device_id = 33333333-aaaa-bbbb-cccc-12345678abcd;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Queries&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Find all the latest events with state 'on'
SELECT state, device_id, timestamp, value
FROM   latest_events_by_state
WHERE  state = 'on';

 state | device_id                            | timestamp                       | value
-------+--------------------------------------+---------------------------------+-----------
    on | 11111111-aaaa-bbbb-cccc-12345678abcd | 2021-01-01 03:33:33.000000+0000 | event 1-3

-- Find all the latest events with state 'off'
SELECT state, device_id, timestamp, value
FROM   latest_events_by_state
WHERE  state = 'off';

 state | device_id                            | timestamp                       | value
-------+--------------------------------------+---------------------------------+-----------
   off | 22222222-aaaa-bbbb-cccc-12345678abcd | 2021-02-02 01:11:11.000000+0000 | event 2-1
   off | 33333333-aaaa-bbbb-cccc-12345678abcd | 2021-03-03 01:11:11.000000+0000 | event 3-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The state-partitioned table solution has the following characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Applicability: state-based queries return 100K rows / 100MBs of data or less.&lt;/li&gt;
&lt;li&gt;Pros: excellent performance.&lt;/li&gt;
&lt;li&gt;Cons: additional deletes are required to maintain the table; measures to prevent &lt;a href="https://medium.com/de-bijenkorf-techblog/experiences-with-tombstones-in-apache-cassandra-7302092e7423" rel="noopener noreferrer"&gt;tombstone-related problems&lt;/a&gt; may be necessary; data distribution may become skewed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither of the three cons should be considered a serious obstacle in most circumstances. Additional deletes are equivalent to additional writes, and Cassandra can readily scale to handle more writes. Given that inserts and deletes are applied to the same rows again and again, tombstones are likely to get &lt;a href="https://docs.datastax.com/en/cassandra-oss/3.0/cassandra/dml/dmlHowDataWritten.html" rel="noopener noreferrer"&gt;resolved in a MemTable rather than in SSTables&lt;/a&gt;, which can greatly reduce the overall number of tombstones. For example, for a given IoT device, even frequent status updates that all hit the same MemTable can only result in one tombstone. We still recommend &lt;a href="https://medium.com/de-bijenkorf-techblog/experiences-with-tombstones-in-apache-cassandra-7302092e7423" rel="noopener noreferrer"&gt;monitoring table metrics&lt;/a&gt; to be on top of any potential problems. Last but not least, data distribution depends on data and application characteristics. We take full control of data distribution in our last solution in this post.&lt;/p&gt;

&lt;p&gt;We can easily support multiple tenants by changing the table primary key to PRIMARY KEY((tenant, state), device_id). Multi-tenancy may also help improve data distribution. Overall, in terms of performance, this solution should be comparable to the materialized view solution.&lt;/p&gt;

&lt;p&gt;SEE ALSO: &lt;a href="https://jaxenter.com/security-dev-issues-174540.html" rel="noopener noreferrer"&gt;4 Common Software Security Development Issues &amp;amp; How to Fix Them&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Solution 4: Multiple tables&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The fourth solution features a separate table for each state. Every insert to table latest_on_events_by_device must be accompanied by a delete from table latest_off_events_by_device, and vice versa. This is to ensure that the latest event always cancels out any outdated event with a different state for the same device. The state-based queries over the tables can become very expensive as they have to scan all partitions in the tables.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Schema&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Latest 'on' events by device
CREATE TABLE latest_on_events_by_device (
    device_id  UUID,
    timestamp  TIMESTAMP,
    value      TEXT,
    PRIMARY KEY((device_id))
);

-- Latest 'off' events by device
CREATE TABLE latest_off_events_by_device (
    device_id  UUID,
    timestamp  TIMESTAMP,
    value      TEXT,
    PRIMARY KEY((device_id))
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Data&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Event 1-1
INSERT INTO latest_on_events_by_device 
       (device_id, timestamp, value)
VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, 
        '2021-01-01 01:11:11', 'event 1-1');
DELETE FROM latest_off_events_by_device 
WHERE device_id = 11111111-aaaa-bbbb-cccc-12345678abcd;
-- Event 1-2
INSERT INTO latest_off_events_by_device 
       (device_id, timestamp, value)
VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, 
        '2021-01-01 02:22:22', 'event 1-2');
DELETE FROM latest_on_events_by_device 
WHERE device_id = 11111111-aaaa-bbbb-cccc-12345678abcd;
-- Event 1-3
INSERT INTO latest_on_events_by_device 
       (device_id, timestamp, value)
VALUES (11111111-aaaa-bbbb-cccc-12345678abcd, 
        '2021-01-01 03:33:33', 'event 1-3');
DELETE FROM latest_off_events_by_device 
WHERE device_id = 11111111-aaaa-bbbb-cccc-12345678abcd;
-- Event 2-1
INSERT INTO latest_off_events_by_device 
       (device_id, timestamp, value)
VALUES (22222222-aaaa-bbbb-cccc-12345678abcd, 
        '2021-02-02 01:11:11', 'event 2-1');
DELETE FROM latest_on_events_by_device 
WHERE device_id = 22222222-aaaa-bbbb-cccc-12345678abcd;
-- Event 3-1
INSERT INTO latest_off_events_by_device 
       (device_id, timestamp, value)
VALUES (33333333-aaaa-bbbb-cccc-12345678abcd, 
        '2021-03-03 01:11:11', 'event 3-1');
DELETE FROM latest_on_events_by_device 
WHERE device_id = 33333333-aaaa-bbbb-cccc-12345678abcd;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Queries&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Find all the latest events with state 'on'
SELECT device_id, timestamp, value
FROM   latest_on_events_by_device;

 device_id                            | timestamp                       | value
--------------------------------------+---------------------------------+-----------
 11111111-aaaa-bbbb-cccc-12345678abcd | 2021-01-01 03:33:33.000000+0000 | event 1-3

-- Find all the latest events with state 'off'
SELECT device_id, timestamp, value
FROM   latest_off_events_by_device;

 device_id                            | timestamp                       | value
--------------------------------------+---------------------------------+-----------
 33333333-aaaa-bbbb-cccc-12345678abcd | 2021-03-03 01:11:11.000000+0000 | event 3-1
 22222222-aaaa-bbbb-cccc-12345678abcd | 2021-02-02 01:11:11.000000+0000 | event 2-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The multi-table solution has the following characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Applicability: state-based queries return 100K rows / 100MBs of data or more; state-based queries are executed infrequently.&lt;/li&gt;
&lt;li&gt;Pros: may better distribute the query workload across nodes in a cluster when retrieving a large result set.&lt;/li&gt;
&lt;li&gt;Cons: performance may become unsatisfactory for real-time applications; additional deletes are required to maintain the tables; measures to prevent tombstone-related problems may be necessary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This solution is on par with the secondary index solution in terms of query performance. Multiple tenants can be supported by changing the table primary keys to PRIMARY KEY((tenant, device_id)) or PRIMARY KEY((tenant), device_id). While we do not recommend this solution in practice, what is really interesting about this data model is how it prepares the stage for customizable partitioning discussed next.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Solution 5: Customizable partitioning&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Our final solution builds on the idea of using a separate table for each state. However, this time, we partition tables using artificial buckets. A bucket value is readily computable using user-defined function hash from a device UUID identifier. In this example, the function extracts the first three digits from a UUID literal, converts the resulting hexadecimal number to a decimal one, and returns the remainder of division of the decimal number by 3. Therefore, there can be at most three buckets or partitions per table with values 0, 1 or 2. It is just a coincidence that all our device identifiers map to bucket 0 in this example. Since &lt;a href="https://www.datastax.com/learn/cassandra-fundamentals/advanced-data-types" rel="noopener noreferrer"&gt;Version 4 UUIDs&lt;/a&gt; are randomly generated, for a large number of events, data should be more or less uniformly distributed among the three buckets.&lt;/p&gt;

&lt;p&gt;Similarly to the previous data model, every insert to table latest_on_events_by_bucket must be accompanied by a delete from table latest_off_events_by_bucket, and vice versa. Performance of the state-based queries depends on partitioning, and partitioning is customizable.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Schema&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Custom hash function
CREATE FUNCTION hash(id UUID) 
RETURNS NULL ON NULL INPUT 
RETURNS INT 
LANGUAGE Java AS 
'return Integer.parseInt(id.toString().substring(0,3),16) % 3;';

-- Latest 'on' events by device
CREATE TABLE latest_on_events_by_bucket (
    bucket     INT,
    device_id  UUID,
    timestamp  TIMESTAMP,
    value      TEXT,
    PRIMARY KEY((bucket), device_id)
);

-- Latest 'off' events by device
CREATE TABLE latest_off_events_by_bucket (
    bucket     INT,
    device_id  UUID,
    timestamp  TIMESTAMP,
    value      TEXT,
    PRIMARY KEY((bucket), device_id)
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Data&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Event 1-1
INSERT INTO latest_on_events_by_bucket 
       (bucket, device_id, timestamp, value)
VALUES (hash(11111111-aaaa-bbbb-cccc-12345678abcd), 
        11111111-aaaa-bbbb-cccc-12345678abcd,
        '2021-01-01 01:11:11', 'event 1-1');
DELETE FROM latest_off_events_by_bucket 
WHERE bucket = hash(11111111-aaaa-bbbb-cccc-12345678abcd) AND 
      device_id = 11111111-aaaa-bbbb-cccc-12345678abcd;
-- Event 1-2
INSERT INTO latest_off_events_by_bucket 
       (bucket, device_id, timestamp, value)
VALUES (hash(11111111-aaaa-bbbb-cccc-12345678abcd),
        11111111-aaaa-bbbb-cccc-12345678abcd, 
        '2021-01-01 02:22:22', 'event 1-2');
DELETE FROM latest_on_events_by_bucket 
WHERE bucket = hash(11111111-aaaa-bbbb-cccc-12345678abcd) AND 
      device_id = 11111111-aaaa-bbbb-cccc-12345678abcd;
-- Event 1-3
INSERT INTO latest_on_events_by_bucket 
       (bucket, device_id, timestamp, value)
VALUES (hash(11111111-aaaa-bbbb-cccc-12345678abcd),
        11111111-aaaa-bbbb-cccc-12345678abcd, 
        '2021-01-01 03:33:33', 'event 1-3');
DELETE FROM latest_off_events_by_bucket 
WHERE bucket = hash(11111111-aaaa-bbbb-cccc-12345678abcd) AND 
      device_id = 11111111-aaaa-bbbb-cccc-12345678abcd;
-- Event 2-1
INSERT INTO latest_off_events_by_bucket 
       (bucket, device_id, timestamp, value)
VALUES (hash(22222222-aaaa-bbbb-cccc-12345678abcd),
        22222222-aaaa-bbbb-cccc-12345678abcd, 
        '2021-02-02 01:11:11', 'event 2-1');
DELETE FROM latest_on_events_by_bucket 
WHERE bucket = hash(22222222-aaaa-bbbb-cccc-12345678abcd) AND 
      device_id = 22222222-aaaa-bbbb-cccc-12345678abcd;
-- Event 3-1
INSERT INTO latest_off_events_by_bucket 
       (bucket, device_id, timestamp, value)
VALUES (hash(33333333-aaaa-bbbb-cccc-12345678abcd),
        33333333-aaaa-bbbb-cccc-12345678abcd, 
        '2021-03-03 01:11:11', 'event 3-1');
DELETE FROM latest_on_events_by_bucket 
WHERE bucket = hash(33333333-aaaa-bbbb-cccc-12345678abcd) AND 
      device_id = 33333333-aaaa-bbbb-cccc-12345678abcd;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Queries&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Find all the latest events with state 'on'
SELECT bucket, device_id, timestamp, value
FROM   latest_on_events_by_bucket
WHERE  bucket IN (0,1,2);

 bucket | device_id                            | timestamp                       | value
--------+--------------------------------------+---------------------------------+-----------
      0 | 11111111-aaaa-bbbb-cccc-12345678abcd | 2021-01-01 03:33:33.000000+0000 | event 1-3

-- Find all the latest events with state 'off'
SELECT bucket, device_id, timestamp, value
FROM   latest_off_events_by_bucket
WHERE  bucket IN (0,1,2);

 bucket | device_id                            | timestamp                       | value
--------+--------------------------------------+---------------------------------+-----------
      0 | 22222222-aaaa-bbbb-cccc-12345678abcd | 2021-02-02 01:11:11.000000+0000 | event 2-1
      0 | 33333333-aaaa-bbbb-cccc-12345678abcd | 2021-03-03 01:11:11.000000+0000 | event 3-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The customizable partitioning solution has the following characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Applicability: can meet different requirements when customized.&lt;/li&gt;
&lt;li&gt;Pros: flexibility; performance can be optimized by customizing partitioning.&lt;/li&gt;
&lt;li&gt;Cons: a good partitioning function must be supplied; additional deletes are required to maintain the tables; measures to &lt;a href="https://medium.com/de-bijenkorf-techblog/experiences-with-tombstones-in-apache-cassandra-7302092e7423" rel="noopener noreferrer"&gt;prevent tombstone-related problems&lt;/a&gt; may be necessary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choosing a good partitioning function is a good problem to have. While this may add a bit more complexity, the solution gives full control over data partitioning and query performance. Finding a good partitioning function would depend on specific data and application requirements, and may require some experience and experimentation. For example, retrieving 100 rows from 1 partition is generally faster than retrieving 100 rows from 10 partitions, but retrieving 1,000,000 rows from 1 partition is usually slower than retrieving 1,000,000 rows from 10 partitions. Next, additional deletes are equivalent to additional writes, and Cassandra can readily scale to handle more writes.&lt;/p&gt;

&lt;p&gt;Given that inserts and deletes are applied to the same rows again and again, tombstones are likely to get &lt;a href="https://docs.datastax.com/en/cassandra-oss/3.0/cassandra/dml/dmlHowDataWritten.html" rel="noopener noreferrer"&gt;resolved in a MemTable rather than in SSTables&lt;/a&gt;, which can greatly reduce the overall number of tombstones. For example, for a given IoT device, even frequent status updates that all hit the same MemTable can only result in one tombstone. We still recommend &lt;a href="https://medium.com/de-bijenkorf-techblog/experiences-with-tombstones-in-apache-cassandra-7302092e7423" rel="noopener noreferrer"&gt;monitoring table metrics&lt;/a&gt; to be on top of any potential problems. Last but not least, data distribution depends on data and application characteristics. We take full control of data distribution in our last solution in this post.&lt;/p&gt;

&lt;p&gt;This data model provides ultimate flexibility. Multi-tenancy is achievable by changing the primary key of each table to PRIMARY KEY((tenant, bucket), device_id). More importantly, a partitioning function can be changed to increase or decrease the number of partitions. A query that retrieves a smaller result set should access a smaller number of partitions for better performance. A query that retrieves a larger result set should access a larger number of partitions for better workload distribution. It is possible to use different functions for different states and tenants to achieve optimal performance. Better partitioning should result in better performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;We defined the problem of managing the latest IoT events based on a state, identified its challenges, and described how it can be solved in Apache Cassandra using five different data models. We stated the applicability, pros and cons for each data model. Our final recommendation is to focus on the materialized view, state-partitioned table, and customizable partitioning data models. Choose the first two for their simplicity and ease of use. Consider customizable partitioning for ultimate flexibility when other options are exhausted. Finally, be open to exploring new possible solutions that may push some computation to an application or rely on specialized search indexes and other technologies.&lt;/p&gt;

&lt;p&gt;It is worth mentioning that this blog post was motivated by questions from Apache Cassandra community members on Discord. Join the &lt;a href="https://discord.com/invite/pPjPcZN" rel="noopener noreferrer"&gt;Fellowship of the (Cassandra) Rings&lt;/a&gt; today and connect to fellow community members and experts!&lt;/p&gt;

&lt;p&gt;Learn more about Cassandra, data modeling and IoT:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cassandra-as-a-Service &lt;a href="https://astra.dev/3PJ6GQQ" rel="noopener noreferrer"&gt;DataStax Astra&lt;/a&gt; – experience serverless Cassandra for free&lt;/li&gt;
&lt;li&gt;Short course &lt;a href="https://www.datastax.com/learn/cassandra-fundamentals" rel="noopener noreferrer"&gt;Cassandra Fundamentals&lt;/a&gt; – learn Cassandra Query Language (CQL)&lt;/li&gt;
&lt;li&gt;Learning series &lt;a href="https://www.datastax.com/learn/data-modeling-by-example" rel="noopener noreferrer"&gt;Data Modeling by Example&lt;/a&gt; – study Cassandra data modeling in depth&lt;/li&gt;
&lt;li&gt;Workshops &lt;a href="https://www.datastax.com/workshops" rel="noopener noreferrer"&gt;Developer Workshops&lt;/a&gt; – attend free data modeling workshops and more&lt;/li&gt;
&lt;li&gt;Whitepaper &lt;a href="https://www.datastax.com/resources/whitepaper/data-modeling-apache-cassandra" rel="noopener noreferrer"&gt;Data Modeling in Apache Cassandra&lt;/a&gt; – get an overview of the best data modeling practices&lt;/li&gt;
&lt;li&gt;More on IoT and time series &lt;a href="https://www.datastax.com/resources/video/datastax-accelerate-2019-designing-time-series-applications-scale-datastax" rel="noopener noreferrer"&gt;Fundamentals&lt;/a&gt; |&lt;a href="https://www.datastax.com/learn/data-modeling-by-example/sensor-data-model" rel="noopener noreferrer"&gt; IoT data model&lt;/a&gt; | &lt;a href="https://www.datastax.com/learn/data-modeling-by-example/time-series-model" rel="noopener noreferrer"&gt;Time series data model&lt;/a&gt; | &lt;a href="https://www.youtube.com/watch?v=nF502PmFi_w" rel="noopener noreferrer"&gt;Streaming&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Data Modeling in Cassandra and Astra DB</title>
      <dc:creator>Artem Chebotko</dc:creator>
      <pubDate>Thu, 16 Jun 2022 19:21:29 +0000</pubDate>
      <link>https://dev.to/datastax/data-modeling-in-cassandra-and-astra-db-18do</link>
      <guid>https://dev.to/datastax/data-modeling-in-cassandra-and-astra-db-18do</guid>
      <description>&lt;p&gt;What does it take to build an efficient and sound data model for &lt;a href="https://cassandra.apache.org/" rel="noopener noreferrer"&gt;Apache Cassandra&lt;/a&gt;® and &lt;a href="https://astra.dev/3zmzedJ" rel="noopener noreferrer"&gt;DataStax Astra DB&lt;/a&gt;? Where would one start? Are there any data modeling rules to follow? Can it be done consistently time and time again? The answers to these and many other questions can be found in the Cassandra data modeling methodology.&lt;/p&gt;

&lt;p&gt;In this post, we present a high-level overview of the data modeling methodology for Cassandra and Astra DB, and share over half a dozen complete data modeling examples from various real-life domains. We apply the methodology to create Cassandra and Astra DB data models for IoT, messaging data, digital library, investment portfolio, time series, shopping cart, and order management. We even provide our datasets and queries for you to try.&lt;/p&gt;

&lt;p&gt;As a side note, if you are new to Cassandra or if the terms &lt;a href="https://www.datastax.com/learn/cassandra-fundamentals/tables-single-row-partitions" rel="noopener noreferrer"&gt;single-row partitions&lt;/a&gt; and &lt;a href="https://www.datastax.com/learn/cassandra-fundamentals/tables-multi-row-partitions" rel="noopener noreferrer"&gt;multi-row partitions&lt;/a&gt; sound unfamiliar, we recommend taking a closer look at &lt;a href="https://www.datastax.com/learn/cassandra-fundamentals" rel="noopener noreferrer"&gt;Cassandra Fundamentals&lt;/a&gt; before deep diving into data modeling.&lt;/p&gt;

&lt;h1&gt;
  
  
  Data modeling and the methodology
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Data modeling&lt;/em&gt; is a process that involves many activities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collecting and analyzing data requirements&lt;/li&gt;
&lt;li&gt;Understanding domain entities and relationships&lt;/li&gt;
&lt;li&gt;Identifying data access patterns&lt;/li&gt;
&lt;li&gt;Organizing and structuring data in a particular way&lt;/li&gt;
&lt;li&gt;Designing and specifying a database schema&lt;/li&gt;
&lt;li&gt;Optimizing schema and data indexing techniques&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Data modeling can have a profound effect on data quality and data access. For data quality, think about data completeness, consistency, and accuracy. With respect to data access, think about queryability, efficiency, and scalability. An efficient and sound data model is crucial for both data and applications.&lt;/p&gt;

&lt;p&gt;Our &lt;em&gt;methodology&lt;/em&gt; defines how the data modeling process can be carried out in a well-organized and repeatable fashion. In particular, the Cassandra data modeling methodology is based on four objectives, four models, and two transitions; along with specific modeling, visualization, mapping, and optimization techniques and methods.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3iotz66rpqtnl88os1x3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3iotz66rpqtnl88os1x3.png" alt="Image description" width="700" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Figure 1: Cassandra data modeling methodology.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four objectives
&lt;/h2&gt;

&lt;p&gt;The Cassandra data modeling process, when discussed at a high level, can be distilled into these four key objectives:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Understand the data:&lt;/strong&gt; Whether starting from scratch or dealing with an existing dataset, do you understand data that needs to be managed? Things like entities, relationships, and key constraints come to mind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identify data access patterns:&lt;/strong&gt; Do you have a good idea of what a data-driven application should be able to do? Think of tasks (or microservices) and their required data access patterns, execution sequences and workflows, and how data retrieved in one task is used by the next one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply the query-first approach:&lt;/strong&gt; Do you know how to design Cassandra tables to support specific queries? It is called a query-first or query-driven approach because designing table schemas depends on both data and queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimize and implement:&lt;/strong&gt; How do you verify that both database tables and application queries are efficient and scalable? For example, large partitions and queries that access many partitions may require additional optimizations.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Four models
&lt;/h2&gt;

&lt;p&gt;The four models directly correspond to the four objectives and are meant to make the process more concrete, manageable, repeatable, documentable, collaborative, and shareable. They are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Conceptual data model:&lt;/strong&gt; A technology-independent, high-level view of data. Its purpose is to understand the data in a particular domain. While there are a number of conceptual data modeling techniques, we use the &lt;em&gt;Entity-Relationship Model&lt;/em&gt; and &lt;em&gt;Entity-Relationship Diagrams&lt;/em&gt; in &lt;em&gt;Chen’s Notation&lt;/em&gt; to document entity types, relationship types, attribute types, and cardinality and key constraints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application workflow model:&lt;/strong&gt; A technology-independent, high-level view of a data-driven application, consisting of application tasks, execution dependencies, and access patterns. Its purpose is to identify data access patterns and how they may be executed in sequences. These include queries, inserts, updates, and deletes required by different data-driven tasks. We use simple graph-like diagrams to represent application workflows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logical data model:&lt;/strong&gt; A Cassandra-specific data model featuring tables, materialized views, secondary indexes, user-defined types, and other database schema constructs. It is derived from a conceptual data model by organizing data into Cassandra-specific data structures based on data access patterns identified by an application workflow. This is where the query-first approach is applied. Logical data models can be conveniently captured and visualized using &lt;em&gt;Chebotko Diagrams&lt;/em&gt; that can feature tables, materialized views, indexes, and so forth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Physical data model:&lt;/strong&gt; A Cassandra-specific data model that is directly derived from a logical data model by analyzing and optimizing for performance. Physical data models can be conveniently captured and visualized using &lt;em&gt;Chebotko Diagrams&lt;/em&gt; and implemented in Cassandra using CQL.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Two transitions
&lt;/h2&gt;

&lt;p&gt;To complete the picture, the methodology must define the transitions between the models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mapping a conceptual data model and an application workflow model to a logical data model&lt;/li&gt;
&lt;li&gt;Optimizing a logical data model to produce a physical data model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In many aspects, the transitions are the most interesting and profound components of the methodology. To carry out the first transition, the methodology defines &lt;em&gt;mapping rules&lt;/em&gt; and &lt;em&gt;mapping patterns&lt;/em&gt;. For the second transition, some common &lt;em&gt;optimization techniques&lt;/em&gt; include splitting and merging partitions, data indexing, data aggregation, and concurrent data access optimizations.&lt;/p&gt;

&lt;p&gt;You can find more information about the Cassandra data modeling methodology in the &lt;a href="https://www.dropbox.com/s/4bu0dy0ayrqygei/cassandra-data-modeling-methodology-paper.pdf" rel="noopener noreferrer"&gt;original paper&lt;/a&gt;, &lt;a href="https://www.dropbox.com/s/3cul3hqzr84bark/cassandra-data-modeling-methodology-presentation.pdf" rel="noopener noreferrer"&gt;conference presentation&lt;/a&gt;, or &lt;a href="https://auth.cloud.datastax.com/auth/realms/CloudUsers/protocol/saml/clients/absorb" rel="noopener noreferrer"&gt;DataStax Academy video course DS220&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Data modeling in action
&lt;/h1&gt;

&lt;p&gt;One of the best ways to become skilled in data modeling is to explore concrete examples. We maintain &lt;a href="https://www.datastax.com/learn/data-modeling-by-example" rel="noopener noreferrer"&gt;this growing collection of data modeling examples&lt;/a&gt; from various domains to help you get started with Cassandra and Astra DB data modeling. Each example applies the Cassandra data modeling methodology to produce and visualize four important artifacts: conceptual data model, application workflow model, logical data model, and physical data model.&lt;/p&gt;

&lt;p&gt;Moreover, each example has a hands-on portion with practice questions and solutions. The hands-on scenarios make it straightforward to implement a data model in Cassandra, express data access patterns as CQL queries and run the queries against our sample datasets.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5dq2zb9dgcf5o7qwayia.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5dq2zb9dgcf5o7qwayia.png" alt="Image description" width="700" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Figure 2: Example hands-on scenario with schema, data, and queries.&lt;/p&gt;

&lt;p&gt;Go ahead and explore these data models, and execute real queries against them in your browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.datastax.com/learn/data-modeling-by-example/sensor-data-model" rel="noopener noreferrer"&gt;Sensor data model&lt;/a&gt;: Modeling sensor networks, sensors, and temperature measurements. The resulting database schema has four tables supporting four data access patterns.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.datastax.com/learn/data-modeling-by-example/messaging-data-model" rel="noopener noreferrer"&gt;Messaging data model&lt;/a&gt;: Modeling users, email folders, emails, and email attachments. The resulting database schema has five tables supporting four data access patterns.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.datastax.com/learn/data-modeling-by-example/digital-library-data-model" rel="noopener noreferrer"&gt;Digital library data model&lt;/a&gt;: Modeling performers, albums, album tracks, and users. The resulting database schema has eight tables supporting nine data access patterns.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.datastax.com/learn/data-modeling-by-example/investment-data-model" rel="noopener noreferrer"&gt;Investment portfolio data model&lt;/a&gt;: Modeling users, investment accounts, trades, and trading instruments. The resulting database schema has six tables supporting seven data access patterns.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.datastax.com/learn/data-modeling-by-example/time-series-model" rel="noopener noreferrer"&gt;Time series data model&lt;/a&gt;: Modeling IoT data sources, groups of related sources, metrics, data points, and time series with higher or lower resolution. The resulting database schema has seven tables supporting seven data access patterns.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.datastax.com/learn/data-modeling-by-example/shopping-cart" rel="noopener noreferrer"&gt;Shopping cart data model&lt;/a&gt;: Modeling users, items, and shopping carts. The resulting database schema has three tables and one materialized view supporting seven data access patterns, including updates that use batches and lightweight transactions.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.datastax.com/learn/data-modeling-by-example/order-management" rel="noopener noreferrer"&gt;Order management data model&lt;/a&gt;: Modeling users, payment methods, addresses, items, shopping carts, orders, delivery options, and order statuses. The resulting database schema has four tables supporting five data access patterns, including multi-step updates that use lightweight transactions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Data modeling and Astra DB
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://astra.dev/3zmzedJ" rel="noopener noreferrer"&gt;Astra DB&lt;/a&gt; is a cloud database service built on Apache Cassandra. It is a serverless and multi-region service that works in AWS, Azure and GCP. If you haven’t already, you should take advantage of &lt;a href="https://astra.dev/3zmzedJ" rel="noopener noreferrer"&gt;Astra DB’s free tier&lt;/a&gt; to create your own fully managed Cassandra database in the cloud. After all, how many multi-cloud, multi-region, serverless databases built on open-source technologies do you know? Astra DB is the first one.&lt;/p&gt;

&lt;p&gt;Astra DB databases are Cassandra databases. The same data modeling methodology applies and the above example data models can be instantiated in Astra DB. However, there are a couple of minor differences that you may want to be aware of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Astra DB does not support materialized views. &lt;a href="https://www.datastax.com/learn/cassandra-fundamentals/materialized-views" rel="noopener noreferrer"&gt;Materialized views&lt;/a&gt; are experimental in Cassandra and the use of regular tables is usually recommended instead.&lt;/li&gt;
&lt;li&gt;Astra DB does not support user-defined functions. Strictly speaking, user-defined functions are not data modeling constructs. They usually can be readily replaced with computation outside of a database.&lt;/li&gt;
&lt;li&gt;Astra DB supports &lt;em&gt;Storage-Attached Indexing&lt;/em&gt; or SAI. &lt;a href="https://www.datastax.com/dev/cassandra-indexing" rel="noopener noreferrer"&gt;Storage-attached indexes&lt;/a&gt; in Astra DB are secondary indexes with better performance, space efficiency, and more capabilities than &lt;a href="https://www.datastax.com/learn/cassandra-fundamentals/secondary-indexes" rel="noopener noreferrer"&gt;regular secondary indexes or experimental SASI&lt;/a&gt; in Cassandra. With that said, it is important to understand that SAI and other secondary indexes still have the same &lt;a href="https://www.datastax.com/learn/cassandra-fundamentals/secondary-indexes" rel="noopener noreferrer"&gt;use cases and limitations&lt;/a&gt;, and should be used with caution.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Astra DB and Cassandra differences with respect to materialized views, user-defined functions, and secondary indexes should not have any profound effect on data modeling.&lt;/p&gt;

&lt;h1&gt;
  
  
  Data modeling and K8ssandra
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://k8ssandra.io/" rel="noopener noreferrer"&gt;K8ssandra&lt;/a&gt; is a cloud-native distribution of Cassandra that runs on Kubernetes. Besides Cassandra, the distribution also includes several integrated components that enable richer data APIs, and provide better automation for observability, metrics monitoring, backup and restore, and data anti-entropy services.&lt;/p&gt;

&lt;p&gt;K8ssandra is open-source, free to use, and data modeling in K8ssandra is identical to data modeling in Cassandra.&lt;/p&gt;

&lt;h1&gt;
  
  
  Data modeling and Stargate
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://stargate.io/" rel="noopener noreferrer"&gt;Stargate&lt;/a&gt; is an open-source data gateway deployed between applications and a database. It supports different API options for an application to interact with Cassandra, Astra DB, and K8ssandra. Stargate’s API extensions include CQL, REST, GraphQL, and Document APIs.&lt;/p&gt;

&lt;p&gt;The use of CQL, REST, and GraphQL APIs has no effect on data modeling: the same data modeling methodology applies.&lt;/p&gt;

&lt;p&gt;The use of Document API has a significant impact on data modeling. With Document API, the focus shifts from organizing data as rows, columns, and partitions to structuring data as JSON documents. Stargate then uses the &lt;a href="https://stargate.io/2020/10/19/the-stargate-cassandra-documents-api.html" rel="noopener noreferrer"&gt;predefined mapping&lt;/a&gt; to shred JSON documents and store them as rows in Cassandra tables. The topic of data modeling for document databases is beyond the scope of this article.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Data modeling in Cassandra and Astra DB is a very important topic and we just scratched the surface in this post. We presented a high-level overview of the Cassandra data modeling methodology and urged you to sharpen your skills by exploring the &lt;a href="https://www.datastax.com/learn/data-modeling-by-example" rel="noopener noreferrer"&gt;data modeling examples&lt;/a&gt;. We also established that data modeling in Cassandra, Astra DB, and K8ssandra are practically identical; with &lt;a href="https://astra.dev/3zmzedJ" rel="noopener noreferrer"&gt;Astra DB&lt;/a&gt; having a significant advantage of being serverless and fully managed. Finally, we briefly discussed how Stargate APIs — namely CQL, REST, GraphQL, and Document APIs — can affect data modeling.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Explore &lt;a href="https://auth.cloud.datastax.com/auth/realms/CloudUsers/protocol/saml/clients/absorb" rel="noopener noreferrer"&gt;DataStax Academy&lt;/a&gt; to get certified in Apache Cassandra with hands-on courses. You can also &lt;a href="https://docs.google.com/forms/d/e/1FAIpQLSfEtzzVauuFpFJWUiepYndqchBpNsaOwm6raPJDsMt9nTvMbw/viewform" rel="noopener noreferrer"&gt;subscribe to our event alert&lt;/a&gt; to get notified about our latest developer workshops. Lastly, follow &lt;a href="https://datastax.medium.com/" rel="noopener noreferrer"&gt;DataStax on Medium&lt;/a&gt; for exclusive posts on all things Cassandra, streaming, Kubernetes, and more.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Resources
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://cassandra.apache.org/" rel="noopener noreferrer"&gt;Apache Cassandra&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://astra.dev/3zmzedJ" rel="noopener noreferrer"&gt;Astra DB&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://k8ssandra.io/" rel="noopener noreferrer"&gt;K8ssandra&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stargate.io/" rel="noopener noreferrer"&gt;Stargate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.datastax.com/learn/cassandra-fundamentals" rel="noopener noreferrer"&gt;Cassandra Fundamentals&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.datastax.com/learn/data-modeling-by-example" rel="noopener noreferrer"&gt;Data Modeling by Example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dropbox.com/s/4bu0dy0ayrqygei/cassandra-data-modeling-methodology-paper.pdf" rel="noopener noreferrer"&gt;A Big Data Modeling Methodology for Apache Cassandra&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://auth.cloud.datastax.com/auth/realms/CloudUsers/protocol/saml/clients/absorb" rel="noopener noreferrer"&gt;DataStax Academy DS220: Data Modeling with Apache Cassandra&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dropbox.com/s/3cul3hqzr84bark/cassandra-data-modeling-methodology-presentation.pdf" rel="noopener noreferrer"&gt;Using the Chebotko Method to Design Sound and Scalable Data Models for Apache Cassandra&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
    </item>
    <item>
      <title>Optimizing Distributed Joins: The Case of Google Cloud Spanner and DataStax Astra DB</title>
      <dc:creator>Artem Chebotko</dc:creator>
      <pubDate>Tue, 31 May 2022 16:25:22 +0000</pubDate>
      <link>https://dev.to/datastax/optimizing-distributed-joins-the-case-of-google-cloud-spanner-and-datastax-astra-db-2980</link>
      <guid>https://dev.to/datastax/optimizing-distributed-joins-the-case-of-google-cloud-spanner-and-datastax-astra-db-2980</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fal54av28a2rmj1e8a4pd.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fal54av28a2rmj1e8a4pd.jpeg" alt="Image description" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In this post, learn how relational and NoSQL databases, Google Cloud Spanner and DataStax Astra DB, optimize distributed joins for real-time applications.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Distributed joins are commonly considered to be too expensive to use for real-time transaction processing. That is because, besides joining data, they also frequently require moving or shuffling data between nodes in a cluster, which can significantly affect query response times and database throughput. However, there are certain optimizations that can completely eliminate the need to move data to enable faster joins. In this article, we first review the four types of distributed joins, including shuffle join, broadcast join, co-located join, and pre-computed join. We then demonstrate how leading fully managed Relational and NoSQL databases, namely &lt;a href="https://cloud.google.com/spanner" rel="noopener noreferrer"&gt;Google Cloud Spanner&lt;/a&gt;, support optimized joins that are suitable for real-time applications.&lt;/p&gt;

&lt;h1&gt;
  
  
  Four types of distributed joins
&lt;/h1&gt;

&lt;p&gt;Joins are used in databases to combine related data from one or more tables or datasets. Data is usually combined based on some condition that relates columns from participating tables. Hereafter, we refer to columns used in a join condition as &lt;em&gt;join keys&lt;/em&gt; and assume that they are always related by &lt;em&gt;equality&lt;/em&gt; operators.&lt;/p&gt;

&lt;p&gt;Distributed joins are joins in distributed databases, where data from each table is partitioned into smaller chunks — usually called &lt;em&gt;partitions&lt;/em&gt; — that are stored on different nodes in a cluster. While distributing data helps with managing large datasets, it also makes joins harder to perform and scale because table rows that can be joined may reside in different partitions on different nodes.&lt;/p&gt;

&lt;p&gt;A distributed join can be represented as a three-step process. The first step is to move data between nodes in the cluster, such that rows that can potentially be combined based on a join condition end up on the same nodes. Data movement is usually achieved by shuffling or broadcasting data. The second step is to compute a join result locally on each node. This usually involves one of the fundamental join algorithms, such as a nested-loop, sort-merge, or hash join algorithm. The last step is to merge or union local join results and return the final result. In many cases, it is possible to optimize a distributed join by eliminating one or even two steps from this process.&lt;/p&gt;

&lt;p&gt;Consider the four types of distributed joins illustrated in Figure 1:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;em&gt;shuffle join&lt;/em&gt; re-distributes rows from both tables among nodes based on join key values, such that all rows with the same join key value are moved to the same node. Depending on a particular algorithm used to compute joins, a shuffle join can be a shuffle hash join, shuffle sort-merge join, and so forth.&lt;/li&gt;
&lt;li&gt;A &lt;em&gt;broadcast join&lt;/em&gt; moves data stored in only one table, such that all rows from the smallest table are available on every node. Depending on a particular algorithm used to compute joins, a broadcast join can be a broadcast hash join, broadcast nested-loop join, and so forth.&lt;/li&gt;
&lt;li&gt;A &lt;em&gt;co-located join&lt;/em&gt; does not need to move data at all because data is already stored such that all rows with the same join key value reside on the same node. Data still needs to be joined using a nested-loop, sort-merge, or hash join algorithm.&lt;/li&gt;
&lt;li&gt;A &lt;em&gt;pre-computed join&lt;/em&gt; does not need to move data or compute joins locally on each node because data is already stored in a joined form. This type of join skips data movement and join computation and goes directly to merging and returning results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvfs4sw15yl70w3q9z621.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvfs4sw15yl70w3q9z621.png" alt="Image description" width="700" height="581"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Figure 1: Four Types of Distributed Joins&lt;/p&gt;

&lt;p&gt;Shuffle and broadcast joins are more suitable for batch or near real-time analytics. For example, they are used in &lt;a href="https://spark.apache.org/" rel="noopener noreferrer"&gt;Apache Spark&lt;/a&gt; as the main join strategies. Co-located and pre-computed joins are faster and can be used for online transaction processing with real-time applications. They frequently rely on organizing data based on unique storage schemes supported by a database.&lt;/p&gt;

&lt;p&gt;In the rest of this article, our focus is on co-located and pre-computed joins, and how they can be used in representative cloud-native Relational and NoSQL databases. For co-located joins, we choose &lt;a href="https://cloud.google.com/spanner" rel="noopener noreferrer"&gt;Google Cloud Spanner&lt;/a&gt;, which is a fully-managed relational database service. For pre-computed joins, we use &lt;a href="https://astra.dev/3LYTy7t" rel="noopener noreferrer"&gt;DataStax Astra DB&lt;/a&gt;, which is a serverless NoSQL database service. Both database services can be tried for free if you prefer to follow our examples.&lt;/p&gt;

&lt;h1&gt;
  
  
  Running example: Managing users, accounts, and transactions
&lt;/h1&gt;

&lt;p&gt;Let’s define a running example that we can implement in both Google Cloud Spanner and &lt;a href="https://astra.dev/3LYTy7t" rel="noopener noreferrer"&gt;DataStax Astra DB&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8jxtkopdvv8bv1pqlduo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8jxtkopdvv8bv1pqlduo.png" alt="Image description" width="700" height="545"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Figure 2: Example of relational database schema and SQL queries&lt;/p&gt;

&lt;p&gt;Figure 2 depicts the relational database schema with three tables and two SQL queries. We have users identified by user ids, bank accounts identified by account ids, and account transactions identified by transaction ids. A user can have one or more accounts, while each account must belong to exactly one user. An account can have zero or more transactions, while each transaction must be associated with exactly one account. These key and cardinality constraints are captured via &lt;em&gt;primary key&lt;/em&gt; (PK) and &lt;em&gt;foreign key&lt;/em&gt; (FK) constraints on the diagram.&lt;/p&gt;

&lt;p&gt;The first query retrieves all accounts for a specified user by joining tables &lt;code&gt;users&lt;/code&gt; and &lt;code&gt;accounts&lt;/code&gt;. The second query finds all transactions for a given account by joining tables &lt;code&gt;accounts&lt;/code&gt; and &lt;code&gt;transactions&lt;/code&gt;; transactions are also ordered by transaction dates in the result.&lt;/p&gt;

&lt;p&gt;This data model and queries can be readily instantiated in any relational database, including Google Cloud Spanner (see &lt;a href="https://github.com/ArtemChebotko/distributed-joins/blob/main/spanner-foreign-keys.sql" rel="noopener noreferrer"&gt;this SQL script&lt;/a&gt; as an example), but that would not result in the join optimizations we are looking to implement. We show how to do much better in the next two sections.&lt;/p&gt;

&lt;h1&gt;
  
  
  Co-located joins and interleaved tables in Google Cloud Spanner
&lt;/h1&gt;

&lt;p&gt;Co-located joins can perform significantly faster than shuffle and broadcast joins because they avoid moving data between nodes in a cluster. To use co-located joins, a distributed database needs to have a mechanism to specify which related data entities must be stored together on the same node. In &lt;a href="https://cloud.google.com/spanner" rel="noopener noreferrer"&gt;Google Cloud Spanner&lt;/a&gt;, this mechanism is called &lt;em&gt;table&lt;/em&gt; &lt;em&gt;interleaving&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Logically independent tables can be organized into &lt;em&gt;parent-child hierarchies&lt;/em&gt; by interleaving tables. This results in a &lt;em&gt;data locality relationship&lt;/em&gt; between parent and child tables, such that one or more rows from a child table are physically stored together with one row from a parent table. For two tables to be interleaved, the parent table primary key must also be included as the prefix of the child table primary key. In other words, the child table primary key must consist of the parent table primary key followed by additional columns.&lt;/p&gt;

&lt;p&gt;Figure 3 shows how to take advantage of table interleaving and co-located joins in Google Cloud Spanner to improve the performance of queries in our example of users, accounts, and transactions. The three tables are organized into a hierarchy, where table &lt;code&gt;users&lt;/code&gt; is the parent of table &lt;code&gt;accounts&lt;/code&gt;, and table &lt;code&gt;accounts&lt;/code&gt; is the parent of table &lt;code&gt;transactions&lt;/code&gt;. Column &lt;code&gt;user_id&lt;/code&gt; is the primary key of table &lt;code&gt;users&lt;/code&gt; and prefix of the primary key of table &lt;code&gt;accounts&lt;/code&gt;. Columns &lt;code&gt;user_id&lt;/code&gt; and &lt;code&gt;account_id&lt;/code&gt; constitute the primary key of table &lt;code&gt;accounts&lt;/code&gt; and prefix of the primary key of table &lt;code&gt;transactions&lt;/code&gt;. Finally, columns &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;account_id&lt;/code&gt; and &lt;code&gt;transaction_id&lt;/code&gt; constitute the primary key of table &lt;code&gt;transactions&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The SQL queries are unchanged when compared to our original running example. They use the same joins as before, but these joins can now be executed faster as co-located joins.&lt;/p&gt;

&lt;p&gt;To try this example in &lt;a href="https://cloud.google.com/spanner" rel="noopener noreferrer"&gt;Google Cloud Spanner&lt;/a&gt;, we share &lt;a href="https://github.com/ArtemChebotko/distributed-joins/blob/main/spanner-interleaved-tables.sql" rel="noopener noreferrer"&gt;our SQL script&lt;/a&gt; for Co-Located Joins.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fklq0i1v3vrs21jhluo80.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fklq0i1v3vrs21jhluo80.png" alt="Image description" width="700" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Figure 3: Co-located joins in Google Cloud Spanner&lt;/p&gt;

&lt;h1&gt;
  
  
  Pre-computed joins and tables with multi-row partitions in DataStax Astra DB
&lt;/h1&gt;

&lt;p&gt;Pre-computed joins are the fastest joins in our toolbox. They are significantly faster than shuffle and broadcast joins because they avoid moving data between nodes in a cluster. They are also faster than co-located joins because they do not need to compute joins dynamically. To store and serve pre-computed join results effectively, a distributed database needs to have a mechanism to nest related data entities together. In &lt;a href="https://astra.dev/3LYTy7t" rel="noopener noreferrer"&gt;DataStax Astra DB&lt;/a&gt;, this mechanism is called &lt;em&gt;tables with multi-row partitions&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Tables in Astra DB are defined and queried using CQL, an SQL-like language. They are similar to tables in relational databases as they have columns, rows, and primary keys. The important difference is that a table primary key consists of a mandatory &lt;em&gt;partition key&lt;/em&gt; and an optional &lt;em&gt;clustering key&lt;/em&gt;. A partition key uniquely identifies a partition in a table, and a clustering key uniquely identifies a row in a partition. When both partition and clustering keys are defined, a table can store multiple rows in each partition. Tables with multi-row partitions are used to store and retrieve related entities together very efficiently. In our case, we can store pre-joined entities in such tables.&lt;/p&gt;

&lt;p&gt;Figure 4 shows how to take advantage of tables with multi-row partitions and pre-computed joins in &lt;a href="https://astra.dev/3LYTy7t" rel="noopener noreferrer"&gt;DataStax Astra DB&lt;/a&gt; to make queries from our running example exceptionally fast. The two tables are specifically designed to support the two queries. To retrieve all accounts for a specified user, table &lt;code&gt;accounts_by_user&lt;/code&gt; defines &lt;code&gt;user_id&lt;/code&gt; as a partition key and &lt;code&gt;account_id&lt;/code&gt; as a clustering key. Each user in this table has a distinct partition that stores all user accounts as rows in this partition. In addition, each user partition also has information about the name and email of the user stored in static columns whose values are shared by all rows in the partition. To find all transactions for a given account, table &lt;code&gt;transactions_by_account&lt;/code&gt; defines &lt;code&gt;account_id&lt;/code&gt; as a partition key and &lt;code&gt;transaction_id&lt;/code&gt; as a clustering key. Each account in this table has a distinct partition that stores all account transactions as rows in this partition. Furthermore, transactions within each account partition are ordered based on timestamp components of their respective &lt;code&gt;timeuuid&lt;/code&gt; identifiers as defined by the clustering order.&lt;/p&gt;

&lt;p&gt;The CQL queries are much simplified when compared to their SQL counterparts. They are very efficient queries that retrieve one partition at a time based on a partition key value. There are no joins or ordering required because data is already organized in pre-joined and ordered form.&lt;/p&gt;

&lt;p&gt;To try this example in &lt;a href="https://astra.dev/3LYTy7t" rel="noopener noreferrer"&gt;DataStax Astra DB&lt;/a&gt;, we share &lt;a href="https://github.com/ArtemChebotko/distributed-joins/blob/main/astra-db.cql" rel="noopener noreferrer"&gt;our CQL script&lt;/a&gt;. If you are new to CQL, it stands for Cassandra Query Language and is used in both &lt;a href="https://astra.dev/3LYTy7t" rel="noopener noreferrer"&gt;DataStax Astra DB&lt;/a&gt; and &lt;a href="https://cassandra.apache.org/" rel="noopener noreferrer"&gt;Apache Cassandra&lt;/a&gt;. Astra DB is a serverless and multi-region database service that is based on Apache Cassandra, an open-source NoSQL database. To learn more about CQL and tables with multi-row partitions, the hands-on &lt;a href="https://www.datastax.com/learn/cassandra-fundamentals" rel="noopener noreferrer"&gt;Cassandra Fundamentals&lt;/a&gt; learning series is highly recommended. For more advanced data modeling, there is also the collection of &lt;a href="https://www.datastax.com/learn/data-modeling-by-example" rel="noopener noreferrer"&gt;data modeling example&lt;/a&gt;s from various domains.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyywyvi62kmnumsfzp252.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyywyvi62kmnumsfzp252.png" alt="Image description" width="700" height="578"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Figure 4: Pre-computed joins in DataStax Astra DB&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Having fast distributed joins is an important consideration when it comes to selecting a scalable database that can support real-time, high-throughput, data-driven applications. In this article, we discussed how shuffle, broadcast, co-located, and pre-computed joins work. We explained that shuffle and broadcast joins are more suitable for batch or near real-time analytics because they may require moving data among nodes in a cluster, which is expensive. Co-located and pre-computed joins are faster and can do well with real-time applications. Using &lt;a href="https://cloud.google.com/spanner" rel="noopener noreferrer"&gt;Google Cloud Spanner&lt;/a&gt;, we demonstrated how a fully managed, cloud-native relational database can take advantage of fast co-located joins. Using &lt;a href="https://astra.dev/3LYTy7t" rel="noopener noreferrer"&gt;DataStax Astra DB&lt;/a&gt;, we demonstrated how a serverless, cloud-native NoSQL database can take advantage of even faster pre-computed joins.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow the &lt;a href="https://datastax.medium.com/" rel="noopener noreferrer"&gt;DataStax Tech Blog&lt;/a&gt; for more developer stories. Check out our &lt;a href="https://www.youtube.com/channel/UCqA6zOSMpQ55vvguq4Y0jAg" rel="noopener noreferrer"&gt;YouTube channel&lt;/a&gt; for tutorials and here for &lt;a href="https://twitter.com/DataStaxDevs" rel="noopener noreferrer"&gt;DataStax Developers on Twitter&lt;/a&gt; for the latest news about our developer community.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  References
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://astra.dev/3LYTy7t" rel="noopener noreferrer"&gt;DataStax Astra DB&lt;/a&gt; — a serverless, cloud-native NoSQL database&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cassandra.apache.org/" rel="noopener noreferrer"&gt;Apache Cassandra&lt;/a&gt; — open source NoSQL database&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.datastax.com/learn/cassandra-fundamentals" rel="noopener noreferrer"&gt;Astra DB / Cassandra Fundamentals&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.datastax.com/learn/data-modeling-by-example" rel="noopener noreferrer"&gt;Astra DB / Cassandra Data Modeling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/spanner" rel="noopener noreferrer"&gt;Google Cloud Spanner&lt;/a&gt; — a fully managed, cloud-native relational database&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/spanner/docs/whitepapers/optimizing-schema-design" rel="noopener noreferrer"&gt;Optimizing Schema Design for Cloud Spanner&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
    </item>
  </channel>
</rss>
