advanced data modeling with apache cassandra
TRANSCRIPT
©2013 DataStax Confidential. Do not distribute without consent.
@PatrickMcFadin
Patrick McFadinChief Evangelist for Apache Cassandra
Advanced Data Modeling with Apache Cassandra
1
Advanced?• Two thoughts here
Formal Practical
Formal Data Modeling Methods
Data Modeling: Level Up• Understand the data • Conceptual data model
• Understand queries or access patterns • Query graph or workflow
• Apply a query-driven data modeling methodology • Logical data model
• Apply optimizations and implement the design using CQL • Physical data model
Conceptual Data Model• Shows understanding of data entities and relationships • Find errors • Technology independent • Graphical
Conceptual Data Model
User
DigitalArtifact
Venue
Article PresentationReview
User
DigitalArtifact
Venue
likes
n
m
features
1
n
IsA
Article Presentation
disjoint,covering
posts
1
n
Review
likes
features
n
m
n
1
Application Query Workflow• Cassandra is an application database • Show the how the application accesses data • Find queries
Application Query WorkflowSearch'for'
artifacts'by'a'venue,'
author,'title,'or'keyword
Display'information'for'a'venue
Display'a'rating'of'an'artifact
Display'reviews'for'an'artifact
Display'likes'for'an'artifact
Find'information'for'an'artifact'with'a'given'
id
Show'information'about'a'user
Show'likes'for'a'review
Show'reviews'by'a'user
Application Query Workflow
Q5
Q1,Q2,Q3,Q4
Q8Q6 Q7
Search1for1artifacts1by1a1
venue,1author,1title,1or1keyword
Display1information1for1a1venue
Display1a1rating1of1an1artifact
Display1reviews1for1an1artifact
Display1likes1for1an1artifact
Find1information1for1an1artifact1with1a1given1
id
Show1information1about1a1user
Show1likes1for1a1review
Show1reviews1by1a1user
Q9 Q10
Q12
Q11
ACCESS%PATTERNSQ1:$Find$artifacts$for$a$specified$venue$...Q2:$Find$artifacts$for$a$specified$author$...Q3:$Find$artifacts$with$a$specified$title$...Q4:$Find$artifacts$with$a$specified$keyword$...Q5:$Find$information$for$a$specified$venue.Q6:$Find$an$average$rating$for$a$specified$artifact.Q7:$Find$reviews$for$a$specified$artifact$......
Logical Data Model• Combines Conceptual and Query Workflow • Defines Mapping rules •Helps define schema • Chebotko Diagrams
Logical Data Model
ACCESS%PATTERNSQ1:$Find$artifacts$for$a$specified$venue;$order$by$year$(DESC).Q2:$Find$artifacts$for$a$specified$author;$order$by$year$(DESC).Q3:$Find$artifacts$with$a$specified$title;$order$by$year$(DESC).Q4:$Find$artifacts$with$a$specified$keyword;$order$by$year$(DESC).Q5:$Find$information$for$a$specified$venue.Q6:$Find$an$average$rating$for$a$specified$artifact.Q7:$Find$reviews$for$a$specified$artifact,$possibly$with$a$specified$rating.Q8:$Find$a$number$of$‘likes’$for$a$specified$artifact.Q9:$Find$reviews$for$a$specified$user;$order$by$review$timestamp$(DESC).Q10:$Find$a$user$with$a$specified$id.Q11:$Find$a$number$of$‘likes’$for$a$specified$review.Q12:$Find$information$for$a$specified$artifact....
Venues
name Kyear Kcountry IDXhomepage
Q5
Artifacts_by_venue
venue Kyear C�artifact C�typetitleauthors$(list)keywords$(set)
Artifacts_by_author
author Kyear C�artifact C�typetitleauthors$(list)keywords$(set)venue
Artifacts_by_title
title Kyear C�artifact C�typeauthors$(list)keywords$(set)venue
Artifacts_by_keyword
keyword Kyear C�artifact C�typetitleauthors$(list)keywords$(set)venue
Users
id Knameemail
Ratings_by_artifact
artifact Knum_ratings$(counter)sum_ratings$(counter)
Reviews_by_user
user Kreview$(timeuuid) C�ratingtitlebodyartifact_idartifact_titleartifact_authors$(list)user_name Suser_email S
Reviews_by_artifact
artifact Kreview$(timeuuid) C�rating IDXtitlebodyuser
Likes_by_artifact
artifact Knum_likes$(counter)
Likes_by_review
review Knum_likes$(counter)
Q1 Q2 Q3 Q4
Artifacts
artifact Ktypetitleauthors$(list)keywords$(set)venueyear
Q8Q6 Q7
Q11
Q10
Q9
Q12
Physical Data Model• Final blueprint • Ready to be CQL
Practical Modeling Methods
Top User Scores
Game API
Nightly Spark Jobs
Daily Top 10 Users handle | score-----------------+------- subsonic | 66.2 neo | 55.2 bennybaru | 49.2 tigger | 46.2 velvetfog | 45.2 flashberg | 43.6 jbellis | 43.4 cafruitbat | 43.2 groovemerchant | 41.2 rustyrazorblade | 39.2
User Score Table• After each game, score is stored • Partition is user + game • Record timestamp is reversed
(last score first)
CREATE TABLE userScores ( userId uuid, handle text static, gameId uuid, score_timestamp timestamp, score double, PRIMARY KEY ((userId, gameId), score_timestamp)) WITH CLUSTERING ORDER BY (score_timestamp DESC);
Top Ten User Scores•Written by Spark job • Default TTL = 3 days • Using Date Tiered Compaction Strategy
CREATE TABLE TopTen ( gameId uuid, process_timestamp timestamp, score double, userId uuid, handle text, PRIMARY KEY (gameId, process_timestamp, score)) WITH CLUSTERING ORDER BY (process_timestamp DESC, score DESC) AND default_time_to_live = '259200' AND COMPACTION = {'class': 'DateTieredCompactionStrategy', 'enabled': 'TRUE'};
DTCS• Built for time series • SSTable windows of time ranges • Compaction grouped by time • Best for same TTLed data(default TTL) • Entire SSTables can be dropped
Queries, Yo
gameid | process_timestamp | score | handle | userid--------------------------------------+--------------------------+-------+-----------------+-------------------------------------- 99051fe9-6a9c-46c2-b949-38ef78858dd0 | 2014-12-31 13:42:40-0800 | 66.2 | subsonic | 99051fe9-6a9c-46c2-b949-38ef78858d07 99051fe9-6a9c-46c2-b949-38ef78858dd0 | 2014-12-31 13:42:40-0800 | 55.2 | neo | 99051fe9-6a9c-46c2-b949-38ef78858d11 99051fe9-6a9c-46c2-b949-38ef78858dd0 | 2014-12-31 13:42:40-0800 | 49.2 | bennybaru | 99051fe9-6a9c-46c2-b949-38ef78858d06 99051fe9-6a9c-46c2-b949-38ef78858dd0 | 2014-12-31 13:42:40-0800 | 46.2 | tigger | 99051fe9-6a9c-46c2-b949-38ef78858d05 99051fe9-6a9c-46c2-b949-38ef78858dd0 | 2014-12-31 13:42:40-0800 | 45.2 | velvetfog | 99051fe9-6a9c-46c2-b949-38ef78858d04 99051fe9-6a9c-46c2-b949-38ef78858dd0 | 2014-12-31 13:42:40-0800 | 43.6 | flashberg | 99051fe9-6a9c-46c2-b949-38ef78858d10 99051fe9-6a9c-46c2-b949-38ef78858dd0 | 2014-12-31 13:42:40-0800 | 43.4 | jbellis | 99051fe9-6a9c-46c2-b949-38ef78858d09 99051fe9-6a9c-46c2-b949-38ef78858dd0 | 2014-12-31 13:42:40-0800 | 43.2 | cafruitbat | 99051fe9-6a9c-46c2-b949-38ef78858d02 99051fe9-6a9c-46c2-b949-38ef78858dd0 | 2014-12-31 13:42:40-0800 | 41.2 | groovemerchant | 99051fe9-6a9c-46c2-b949-38ef78858d03 99051fe9-6a9c-46c2-b949-38ef78858dd0 | 2014-12-31 13:42:40-0800 | 39.2 | rustyrazorblade | 99051fe9-6a9c-46c2-b949-38ef78858d01 99051fe9-6a9c-46c2-b949-38ef78858dd0 | 2014-12-31 13:42:40-0800 | 20.2 | driftx | 99051fe9-6a9c-46c2-b949-38ef78858d08
SELECT gameId, process_timestamp, score, handle, userIdFROM topten WHERE gameid = 99051fe9-6a9c-46c2-b949-38ef78858dd0 AND process_timestamp = '2014-12-31 13:42:40';
File Storage Use Case
Upload API
It’s all about the model
• Start with our queries • All data for a image • All images over time • Specific images over a range • Access times of each image
• Use case • User creates an account • User uploads image • Image is distributed worldwide • User can check access patterns
user Table• Our standard POJO • emails are dynamic
CREATE TABLE user ( username text, firstname text, lastname text, emails list<text>, PRIMARY KEY (username) );
INSERT INTO user (username, firstname, lastname, emails) VALUES (‘pmcfadin’, ‘Patrick’, ‘McFadin’, [‘[email protected]’, ‘[email protected]’] IF NOT EXISTS;
image Table• Basic POJO for an image • list of tags for potential search • username is from user table
CREATE TABLE image ( image_id uuid, //Proxy image ID username text, created_at timestamp, image_name text, image_description text, tags list<text>, // ? search in Solr ? images map<text, uuid> , // orig, thumbnail, medium PRIMARY KEY (image_id) );
images_timeseries Table• Time ordered list of images • Reversed - Last image first •Map stores versions
CREATE TABLE images_timeseries ( username text, bucket int, //yyyymm sequence timestamp, image_id uuid, image_name text, image_description text, images map<text, uuid>, // orig, thumbnail, medium PRIMARY KEY ((username, bucket), sequence) ) WITH CLUSTERING ORDER BY (sequence DESC); // reverse clustering on sequence
bucket_index Table• List of buckets for a user • Bucket order is reversed •High reads, no updates. Use LeveledCompaction
CREATE TABLE bucket_index ( username text, bucket int, PRIMARY KEY( username, bucket) ) WITH CLUSTERING ORDER BY (bucket DESC); //LCS + reverse clustering
blob Table•Main pointer to chunks • count and checksum for errors detection •META-DATA stored with as an optimization
CREATE TABLE blob ( object_id uuid, // unique identifier chunk_count int, // total number of chunks size int, // total byte size chunk_size int, // maximum size of the chunks. checksum text, // optional checksum, this could be stored // for each blob but only checked on a certain // percentage of reads attributes text, // optional text blob for additional json // encoded attributes PRIMARY KEY (object_id) );
blob_chunk Table•Main data storage table • Size of blob is up to the client • Return size for error detection • Run in parallel!
CREATE TABLE blob_chunk ( object_id uuid, // same as the object.object_name above chunk_id int, // order for this chunk in the blob chunk_size int, // size of this chunk, the last chunk // may be of a different size. data blob, // the data for this blob chunk PRIMARY KEY ((object_id, chunk_id)) );
access_log Table• Classic time series table • Inserts at CL.ONE • Read at CL.ONE
CREATE TABLE access_log ( object_id uuid, access_date text, // YYYYMMDD portion of access timestamp access_time timestamp, // Access time to the ms ip_address inet, // x.x.x.x inet address PRIMARY KEY ((object_id, access_date), access_time, ip_address) );
Light Weight Transactions
The race is onProcess 1 Process 2
SELECT firstName, lastNameFROM usersWHERE username = 'pmcfadin';
SELECT firstName, lastNameFROM usersWHERE username = 'pmcfadin';
(0 rows)
(0 rows)
INSERT INTO users (username, firstname, lastname, email, password, created_date)VALUES ('pmcfadin','Patrick','McFadin', ['[email protected]'], 'ba27e03fd95e507daf2937c937d499ab', '2011-06-20 13:50:00');
INSERT INTO users (username, firstname, lastname, email, password, created_date)VALUES ('pmcfadin','Paul','McFadin', ['[email protected]'], 'ea24e13ad95a209ded8912e937d499de', '2011-06-20 13:51:00');
T0
T1
T2
T3
Got nothing! Good to go!
This one wins
Solution LWTProcess 1
INSERT INTO users (username, firstname, lastname, email, password, created_date)VALUES ('pmcfadin','Patrick','McFadin', ['[email protected]'], 'ba27e03fd95e507daf2937c937d499ab', '2011-06-20 13:50:00')IF NOT EXISTS;
T0
T1 [applied]----------- True
•Check performed for record •Paxos ensures exclusive access •applied = true: Success
Solution LWTProcess 2
T2
T3
[applied] | username | created_date | firstname | lastname -----------+----------+--------------------------+-----------+---------- False | pmcfadin | 2011-06-20 13:50:00-0700 | Patrick | McFadin
INSERT INTO users (username, firstname, lastname, email, password, created_date)VALUES ('pmcfadin','Paul','McFadin', ['[email protected]'], 'ea24e13ad95a209ded8912e937d499de', '2011-06-20 13:51:00')IF NOT EXISTS;
•applied = false: Rejected •No record stomping!
LWT Fine Print•Light Weight Transactions solve edge conditions •They have latency cost.
•Be aware •Load test •Consider in your data model
•Now go shut down that ZooKeeper mess you have!
Use case: Form Versioning
Form Versioning Pt 1•From “Next top data model” •Great idea, but edge conditions
CREATE TABLE working_version (username varchar,form_id int,version_number int,locked_by varchar,form_attributes map<varchar,varchar> PRIMARY KEY ((username, form_id), version_number)
) WITH CLUSTERING ORDER BY (version_number DESC);
•Each user has a form •Each form needs versioning •Need an exclusive lock on the form
Form Versioning Pt 1
INSERT INTO working_version (username, form_id, version_number, locked_by, form_attributes)VALUES ('pmcfadin',1138,1,'',{'FirstName<text>':'First Name: ','LastName<text>':'Last Name: ','EmailAddress<text>':'Email Address: ','Newsletter<radio>':'Y,N'});
UPDATE working_version SET locked_by = 'pmcfadin'WHERE username = 'pmcfadin'AND form_id = 1138AND version_number = 1;
INSERT INTO working_version (username, form_id, version_number, locked_by, form_attributes)VALUES ('pmcfadin',1138,2,null,{'FirstName<text>':'First Name: ','LastName<text>':'Last Name: ','EmailAddress<text>':'Email Address: ','Newsletter<checkbox>':'Y'});
1. Insert first version
2. Lock for one user
3. Insert new version. Release lock
Danger Zone
Form Versioning Pt 2INSERT INTO working_version (username, form_id, version_number, locked_by, form_attributes)VALUES ('pmcfadin',1138,1,'pmcfadin',{'FirstName<text>':'First Name: ','LastName<text>':'Last Name: ','EmailAddress<text>':'Email Address: ','Newsletter<radio>':'Y,N'})IF NOT EXISTS;
UPDATE working_version SET form_attributes['EmailAddress<text>'] = 'Primary Email Address: 'WHERE username = 'pmcfadin'AND form_id = 1138AND version_number = 1IF locked_by = 'pmcfadin';
UPDATE working_version SET form_attributes['EmailAddress<text>'] = 'Email Adx: 'WHERE username = 'pmcfadin'AND form_id = 1138AND version_number = 1IF locked_by = 'dude';
1. Insert first version
Exclusive lock
Accepted
Rejected (sorry dude)
Form Versioning Pt 2•Old way: Edge cases with problems
•Use external locking? •Take your chances?
•New way: Managed expectations (LWT) •Exclusive by existence check •Continued with IF clause •Downside: More latency
Cassandra 2.1 Data Model Enhancements
User Defined Types
• Complex data in one place
• No multi-gets (multi-partitions)
• Nesting! CREATE TYPE address ( street text, city text, zip_code int, country text, cross_streets set<text> );
BeforeCREATE TABLE videos ( videoid uuid, userid uuid, name varchar, description varchar, location text, location_type int, preview_thumbnails map<text,text>, tags set<varchar>, added_date timestamp, PRIMARY KEY (videoid) );
CREATE TABLE video_metadata ( video_id uuid PRIMARY KEY, height int, width int, video_bit_rate set<text>, encoding text );
SELECT * FROM videos WHERE videoId = 2;
SELECT * FROM video_metadata WHERE videoId = 2;
Title: Introduction to Apache Cassandra
Description: A one hour talk on everything you need to know about a totally amazing database.
480 720
Playback rate:
In-applicationjoin
After
• Now video_metadata is embedded in videos
CREATE TYPE video_metadata ( height int, width int, video_bit_rate set<text>, encoding text );
CREATE TABLE videos ( videoid uuid, userid uuid, name varchar, description varchar, location text, location_type int, preview_thumbnails map<text,text>, tags set<varchar>, metadata set <frozen<video_metadata>>, added_date timestamp, PRIMARY KEY (videoid) );
Wait! Frozen??
• Staying out of technical debt
• 3.0 UDTs will not have to be frozen
• Applicable to User Defined Types and Tuples
Do you want to build a schema?Do you want to store some JSON?
Let’s store some JSON{ "productId": 2, "name": "Kitchen Table", "price": 249.99, "description" : "Rectangular table with oak finish", "dimensions": { "units": "inches", "length": 50.0, "width": 66.0, "height": 32 }, "categories": { { "category" : "Home Furnishings" { "catalogPage": 45, "url": "/home/furnishings" }, { "category" : "Kitchen Furnishings" { "catalogPage": 108, "url": "/kitchen/furnishings" } } }
Let’s store some JSON{ "productId": 2, "name": "Kitchen Table", "price": 249.99, "description" : "Rectangular table with oak finish", "dimensions": { "units": "inches", "length": 50.0, "width": 66.0, "height": 32 }, "categories": { { "category" : "Home Furnishings" { "catalogPage": 45, "url": "/home/furnishings" }, { "category" : "Kitchen Furnishings" { "catalogPage": 108, "url": "/kitchen/furnishings" } } }
CREATE TYPE dimensions ( units text, length float, width float, height float );
Let’s store some JSON{ "productId": 2, "name": "Kitchen Table", "price": 249.99, "description" : "Rectangular table with oak finish", "dimensions": { "units": "inches", "length": 50.0, "width": 66.0, "height": 32 }, "categories": { { "category" : "Home Furnishings" { "catalogPage": 45, "url": "/home/furnishings" }, { "category" : "Kitchen Furnishings" { "catalogPage": 108, "url": "/kitchen/furnishings" } } }
CREATE TYPE dimensions ( units text, length float, width float, height float );
CREATE TYPE category ( catalogPage int, url text );
Let’s store some JSON{ "productId": 2, "name": "Kitchen Table", "price": 249.99, "description" : "Rectangular table with oak finish", "dimensions": { "units": "inches", "length": 50.0, "width": 66.0, "height": 32 }, "categories": { { "category" : "Home Furnishings" { "catalogPage": 45, "url": "/home/furnishings" }, { "category" : "Kitchen Furnishings" { "catalogPage": 108, "url": "/kitchen/furnishings" } } }
CREATE TYPE dimensions ( units text, length float, width float, height float );
CREATE TYPE category ( catalogPage int, url text );
CREATE TABLE product ( productId int, name text, price float, description text, dimensions frozen <dimensions>, categories map <text, frozen <category>>, PRIMARY KEY (productId) );
Let’s store some JSONINSERT INTO product (productId, name, price, description, dimensions, categories) VALUES (2, 'Kitchen Table', 249.99, 'Rectangular table with oak finish', { units: 'inches', length: 50.0, width: 66.0, height: 32 }, { 'Home Furnishings': { catalogPage: 45, url: '/home/furnishings' }, 'Kitchen Furnishings': { catalogPage: 108, url: '/kitchen/furnishings' }
} );
dimensions frozen <dimensions>
categories map <text, frozen <category>>
Retrieving fields
Thank you!
Bring the questions
Follow me on twitter @PatrickMcFadin