The Hadoop ecosystem: HBase, Pig, Hive, Impala

After this lesson you will address an HBase cell and say why its empty cells cost nothing, count the MapReduce jobs a Pig script or a Hive query becomes, explain how Hive skips the partitions a query does not need, and say what Impala gains — and gives up — by not using MapReduce at all.

Page 900 asks a question every website asks itself: "find the top 5 most visited pages by users aged 18-25". Page 901 answers it in Java for MapReduce — three columns of code too small to read. Page 902 answers it in nine lines of Pig. The nine lines still run on MapReduce: Pig writes the jobs for you.

Then the deck turns to SQL. Hive asks the question in SQL and also runs MapReduce underneath. Impala asks it in the same SQL without MapReduce, and its slide promises: "Always faster then Hive at least 10 times" (p. 921). Hive is still on that slide, doing things Impala cannot. Why would anyone keep an engine that is ten times slower?

The answer is not about speed, and it comes in the last section. On the way you meet a table where an empty cell costs nothing, a script that becomes exactly three jobs, and a query that skips almost the whole table without opening it.

HBase: a sorted map that looks like a table

In lesson 31 HDFS stored huge files that MapReduce reads whole, in batch. The deck that follows stacks four projects on it, one per complaint. Page 891 names three — "Pig – High-level language for data analysis", "HBase – Table storage for semi-structured data", "Hive – SQL-like Query language and Metastore" — and Impala comes later (Table 1).

Table 1. The four projects of this lesson and the complaint each one answers.
ProjectWhat it isYou want…What runs
HBasetable storage on HDFS (pp. 893–897)one row, now, by its keyits own servers; no MapReduce
Piga data-flow language (pp. 899–904)no hand-written Javathe MapReduce jobs it generates
HiveSQL-like HiveQL and a Metastore (pp. 906–913)SQL over your filesthe MapReduce jobs it generates
Impalaan MPP SQL engine (pp. 915–922)SQL answers while you waitits own daemons, straight on HDFS

Start with HBase. Page 893 describes it in five lines: "Modeled on Google's Bigtable", "Key-valued row/column store", "Billions of rows/millions on columns", "Column-oriented - nulls are free", "Untyped - stores byte[]". The example of the next slide explains each line.

A zoo in one row

A zoo keeps one row per enclosure (p. 894). The row key is enclosure1. Its columns are grouped into two column families: animal:, with the columns animal:type and animal:size, and repairs:, with repairs:cost. At time t1 the enclosure held a big lion; at time t2 a zebra moved in. The slide draws one row with two timestamp lines — t1: lion, big; t2: zebra — plus a repair cost of 1000 EUR (see the red pen below).

Row key — the key of a row, enclosure1. Rows are kept sorted by it.

Column family — a group of columns declared in the table's schema, normally when the table is created (animal:, repairs:), and stored apart from the other families.

Column — written family:qualifier, as animal:type. The qualifier is never declared: any row can start using a new one at any time.

Timestamp — when a cell was written. A cell keeps several versions, one per timestamp, and a plain read returns the newest.

Cell — one value, addressed by row key, column and timestamp. The value is a byte[]: HBase has no types.

\[ (\text{row}, \text{column}, \text{timestamp}) \mapsto \text{value} \]
row
the row key, enclosure1
column
family:qualifier, animal:type
timestamp
t1, t2, … — one per version of the cell
value
uninterpreted bytes, byte[]

That is the whole data model: a map from cell addresses to bytes — in the Bigtable paper's words, "a sparse, distributed, persistent multidimensional sorted map" (background). The table of p. 894 is one way of drawing it.

What is written to disk

Page 895 shows the other way. Each column family is stored separately, as a list of entries, and each entry is a key (row, timestamp, column) with its value. Family animal: holds three entries — (enclosure1, t2, animal:type) zebra, (enclosure1, t1, animal:size) big, (enclosure1, t1, animal:type) lion. Family repairs: holds one, (enclosure1, t1, repairs:cost) 1000 EUR. Four entries in all.

The empty cells of the table have no entry. Nothing marks them and nothing reserves room for them: a missing entry simply is the null. That is "nulls are free". Space grows with the cells you write, not with rows times columns — why a store like this suits "semi-structured data" (p. 891), where every row fills a different handful of columns out of thousands. A relational table keeps a slot, or at least a NULL marker, for every declared column in every row (Fig. 1).

HBase stores one entry per written cell, (row, timestamp, column) → value, family by family; an empty cell is not stored at all.

key type size cost enclosure1 zebra big 1000 EUR enclosure2 giraffe NULL NULL enclosure3 NULL NULL 250 EUR 3 rows × 3 declared columns = 9 slots, 4 of them hold NULL a slot for every column

Before: a relational table

family animal: (enclosure1, t1, animal:size) big (enclosure1, t2, animal:type) zebra (enclosure1, t1, animal:type) lion (enclosure2, t3, animal:type) giraffe family repairs: (enclosure1, t1, repairs:cost) 1000 EUR (enclosure3, t4, repairs:cost) 250 EUR empty cells: nothing

After: HBase, one entry per written cell

Fig. 1. The same zoo twice. Before: a relational table keeps a slot in every row for every declared column, and 4 of its 9 slots hold NULL. After: HBase keeps one entry per written cell, family by family — the four entries of p. 895, a giraffe written into enclosure2 at t3 and a repair of enclosure3 at t4 — and an empty cell is simply absent. The lion is still there too, as an older version of animal:type.

Writing and reading by key

The code of p. 896 writes exactly this data, with the HBase API of the time:

// p. 896: lion and big at t1, then zebra at t2
HTable table = …
Text row = new Text("enclosure1");
Text col1 = new Text("animal:type");
Text col2 = new Text("animal:size");
BatchUpdate update = new BatchUpdate(row);
update.put(col1, "lion".getBytes("UTF-8"));
update.put(col2, "big".getBytes("UTF-8"));
table.commit(update);

update = new BatchUpdate(row);
update.put(col1, "zebra".getBytes("UTF-8"));
table.commit(update);

Read it aloud. A BatchUpdate collects the writes for one row; commit sends them together, so lion and big share a timestamp, t1. The second BatchUpdate writes only animal:type, at t2. Nothing is written to animal:size at t2: p. 894 leaves that cell empty, p. 895 has no entry for it, and a read of animal:size still returns big, its newest version. Every value goes in as bytes — "lion".getBytes("UTF-8") is "Untyped - stores byte[]" in code.

Page 897 lists the reads. There are three, and only three:

// p. 897: a cell (newest version: zebra), a row, a scan
Cell = table.getRow("enclosure1")
            .getColumn("animal:type").getValue();
RowResult = table.getRow( "enclosure1" );
Scanner s = table.getScanner( new String[] { "animal:type" } );

"Retrieve a cell", "Retrieve a row", "Scan through a range of rows": by key, by key, by a range of keys. Rows are kept sorted, so a range is one contiguous read. Nothing else: no SQL, no join, no secondary index — a condition on a value can only be checked while scanning (background, HBase guide). To find "all enclosures of the savannah", put the area into the row key so they sort together — the reasoning of Cassandra's partition key (lesson 21).

The slide's classes date from 2008; today they are Put, Get and Scan (background). Answer the exam with the slide's names. In today's HBase shell the zoo takes six lines:

# HBase shell today (background): the zoo of pp. 894–897
create 'zoo', {NAME => 'animal', VERSIONS => 3}, 'repairs'
put 'zoo', 'enclosure1', 'animal:type', 'lion'
put 'zoo', 'enclosure1', 'animal:size', 'big'
put 'zoo', 'enclosure1', 'animal:type', 'zebra'
get 'zoo', 'enclosure1', {COLUMN => 'animal:type', VERSIONS => 2}
scan 'zoo', {COLUMNS => 'animal:type'}

The get returns zebra, then lion. A family keeps only as many versions as it was created with — one by default since HBase 0.96 (background); the slide's example keeps two.

"Column-oriented, so HBase is the store for averaging one column over a billion rows." No. On p. 893 "Column-oriented" means that column families are stored apart; inside a family the columns of a row sit together under its key, row by row. A column store in the sense of lesson 20 keeps the values of each column together, in a piece of their own, as the Parquet files of section 5 do. Keep the slide's label and give its meaning: a sparse column-family store, built for access by key.

If the zoo reminds you of Cassandra (lesson 20), it should: both copy Bigtable's data model. Underneath, HBase keeps its files in HDFS and gives each range of rows to exactly one server, under a master, while Cassandra has no master (lesson 22 draws both). In the Hadoop stack HBase is the "online data access" of p. 835: random reads and writes by key, next to engines that read whole files.

Now write some cells yourself: every put adds exactly one entry to one family's list, and a get returns the newest version while the older ones stay stored.

Sparse table, free nulls

Above, the logical view of the table zoo: three row keys, two column families, five qualifiers. A filled cell shows its newest value, with one dot under it per stored version; a dotted cell is empty, and a greyed row key has no cell at all — for HBase that row does not exist yet. Below, the physical view: what HBase stores, one list per column family, in HBase's order (row key, column, newest version first; p. 895 lists the same entries in another order). Choose a row key, a column and a value, then put, get or scan. It starts from the four entries of p. 895.

  1. Before touching anything: the drawing has 3 rows and 5 columns, and each family could take a million more qualifiers. How many entries are stored? Predict, then read the lower half and the readout.
    What you should see

    Four — the entries of p. 895. Twelve of the fifteen drawn cells are empty and have no entry; enclosure2 and enclosure3 have no cell at all, so they do not exist as rows yet. A million more empty qualifiers would cost exactly the same: nothing.

  2. Keep enclosure1 and animal:type and press get the cell. Which value comes back, and is lion still stored?
    What you should see

    zebra, written at t2: the newest version. lion stays in the list as (enclosure1, t1, animal:type), tagged older. A put adds a version; it does not overwrite. That is how p. 894 can show lion and zebra in the same row.

  3. Choose enclosure2, animal:diet — a qualifier nobody has used — and hay, then predict: how many entries will a put add, to which list, and do enclosure1 and enclosure3 change? Press put.
    What you should see

    One entry, (enclosure2, t3, animal:diet) hay, in the list of animal:. Nothing else changes: no table change, no NULL in the other rows, and enclosure2 now exists. Only the families are fixed in advance; qualifiers are free.

  4. Put giraffe into enclosure3, animal:type. Now predict what scan the column returns for animal:type, and in which order. Then try putting Rossi into animal:size: does HBase object?
    What you should see

    enclosure1 zebra, then enclosure3 giraffe, in row-key order; enclosure2 has no animal:type and is skipped. That is p. 897's getScanner on animal:type. And HBase accepts Rossi as a size without a word: it stores bytes and checks no type.

Pig: nine lines, three MapReduce jobs

Page 899 introduces Pig: "Started at Yahoo! Research"; it "Expresses sequences of MapReduce jobs"; its "Data model: nested "bags" of items"; it "Provides relational (SQL) operators (JOIN, GROUP BY, etc.)"; and it is "Easy to plug in Java functions". The language is Pig Latin; Pig compiles a script into MapReduce jobs and runs them in order.

The question of p. 900: user data (name, age) in one file, website data (user, url) in another; find the top 5 most visited pages by users aged 18–25. The slide draws its plan as eight boxes: load users, filter by age, load pages, join on name, group on url, count clicks, order by clicks, take top 5.

Page 901 builds that plan in Java, every box by hand. To join, each mapper tags its records with the file they came from ("1" for pages, "2" for users), and a reducer splits them by the tag and forms every pair. To count, a mapper emits a 1 per visit and a reducer adds them up. To sort by clicks, a mapper swaps each pair so that the count becomes the key the framework sorts on. To keep the top, a single reducer stops after N records. Five jobs are chained by hand; each but the last writes its output to a temporary HDFS directory for the next one.

Page 902 says the same in nine lines of Pig Latin:

-- p. 902: top 5 pages of users aged 18-25
Users = load 'users' as (name, age);
Filtered = filter Users by age >= 18 and age <= 25;
Pages = load 'pages' as (user, url);
Joined = join Filtered by name, Pages by user;
Grouped = group Joined by url;
Summed = foreach Grouped generate group, COUNT(Joined) as clicks;
Sorted = order Summed by clicks desc;
Top5 = limit Sorted 5;
store Top5 into 'top5sites';

Read it line by line. Each line but the last names a new relation — a bag of tuples — built from earlier ones: load reads a file with a declared schema, filter keeps the users aged 18 to 25, join pairs each kept user with every visit whose user equals their name. Then the line that shows Pig's data model: group Joined by url counts nothing. It produces one tuple per url, (group, Joined), whose second field is the bag of all joined tuples with that url — the "nested bag" of p. 899. foreach … generate outputs, per group, the url and COUNT(Joined), the size of its bag: the clicks. Nothing runs until the store (background): Pig plans the whole flow at once.

In SQL the same question is a single statement (section 4 writes it in HiveQL); Pig spreads it over one step per line. "Easy to plug in Java functions" means user-defined functions, Java classes called by name inside foreach or filter (background).

Ease of translation: where the jobs come from

Page 903 lays the script next to the plan: each box is one statement, in order. Page 904, the final build of the same slide, frames the boxes into three MapReduce jobs: Job 1 holds load users, filter by age, load pages and join on name; Job 2 holds group on url and count clicks; Job 3 holds order by clicks and take top 5. Why these three?

  1. A MapReduce job has at most one shuffle: map, then shuffle and sort, then reduce (lesson 30). Data is regrouped by a new key across the network once per job — or never, in a map-only job.

  2. A join on name needs every kept user and every visit with the same name on one machine: a shuffle keyed by name. The two loads and the filter look at one tuple at a time, so they run in the map phase before it. Join on the reduce side: that is Job 1.

  3. Grouping by url needs all joined tuples with the same url together: a shuffle keyed by url. It is a different key, so it needs a second job. Its map side reads Job 1's output from HDFS; counting each bag is its reduce side: Job 2.

  4. Ordering by clicks needs the counts sorted across all urls: a shuffle keyed by clicks, a third job. Its reduce side reads them in order and keeps the first 5: Job 3.

  5. So the number of jobs is the number of operators that need a shuffle — JOIN, GROUP, ORDER: three. Every other statement rides along in a map or a reduce phase of one of them.

Pig cuts a script into one MapReduce job per operator that needs a shuffle — JOIN, GROUP, ORDER — and folds every other statement into those jobs.

The Java of p. 901 used five jobs: its author wrote the two loads as separate map-only jobs. Pig needs three and writes fewer temporary files, but every job it keeps still costs a start-up and an output written to HDFS for the next job. Pig saves mostly your time, not the cluster's.

The widget runs the script on a toy dataset, one box at a time, and shows the jobs cut exactly at the shuffles. Its engine menu also runs the question in HiveQL and in Impala — come back to it after sections 4 and 5.

Nine lines, three jobs

The plan of p. 900, framed into the jobs of p. 904. Step through it with Next, or click a box: the readouts show the statement, where it runs and what the shuffle moves; under the drawing, the tuples after that step, computed on a toy dataset of 8 users and 16 visits. Blue arrows carry data across the network; a dashed arrow means "written to HDFS, read by the next job". The Hive and Impala plans are our simplified sketches; only the Pig frames are printed on the slide.

  1. Step to Join on name. Before you look at the panel: which tuples does the shuffle move, and do the users outside 18–25 travel?
    What you should see

    21 tuples keyed by name: the 5 users aged 18–25 and all 16 visits. bob (34), eli (17) and gus (41) were dropped by the filter on the map side and never travel; their 4 visits do travel, and find no partner in the join. The result, Joined, has 12 tuples.

  2. Step to Group on url and predict the shape of one tuple of Grouped. Then step to Count clicks: where does the number 3 for news come from?
    What you should see

    Each tuple is a url and a bag: (news, {(ann, 19, ann, news), (cai, 22, cai, news), (dev, 25, dev, news)}) — a nested bag. COUNT(Joined) counts that bag: 3. GROUP collects, the foreach counts.

  3. Switch on stop after the count. Predict the number of jobs before you read it.
    What you should see

    2. Without ORDER there is no third shuffle, so no third job: the counts are stored at the end of Job 2. LIMIT alone would not need a job of its own either — in the slide's plan it runs on the reduce side of the ORDER.

  4. After reading the Hive and Impala sections, switch the engine. Predict for each: how many MapReduce jobs, and what is written to HDFS between the steps?
    What you should see

    HiveQL: three jobs again — the same three shuffles, generated from SQL — and the outputs of jobs 1 and 2 go through HDFS. Impala: no job at all; the same three moves of data become exchanges that stream rows in memory, and nothing is written until the result. The toy results are identical: the engines differ in how they run the plan, not in what they compute.

Hive: SQL and a Metastore over plain files

Page 906: Hive was "Developed at Facebook" and is "Used for majority of Facebook jobs". It is a "Relational database" built on Hadoop — with the quotation marks on the slide. It "Maintains list of table schemas", offers an "SQL-like query language (HiveQL)", "Can call Hadoop Streaming scripts from HiveQL" and "Supports table partitioning, clustering, complex data types, some optimizations".

The quotation marks are the point. Hive looks relational — tables, typed columns, SELECT, JOIN, GROUP BY — but has no storage or execution engine of its own: a query becomes MapReduce jobs over files in HDFS and takes as long as they take. In the Hive tutorial's words, "Hive is not designed for online transaction processing. It is best used for traditional data warehousing tasks" (background).

"Hadoop Streaming scripts, so Hive does stream processing." No. Hadoop Streaming is MapReduce's way of running a mapper or a reducer written in any language — a program that reads records on standard input and writes key-value pairs on standard output (background). Processing data that never stops arriving is something else: Storm, in lesson 33.

The architecture: metadata up here, data down there

Follow a query through p. 907 (Fig. 2). It arrives through the command line interface, the web interface, or the Thrift server, where programs connect with JDBC or ODBC as to any SQL database. The Driver compiles, optimizes and executes it: the compiler turns HiveQL into a plan of MapReduce jobs, looking the tables up in the Metastore — columns and types, partitions, and where in HDFS each table lives — and the executor submits the jobs to Hadoop, whose JobTracker runs their tasks next to the blocks.

Hive JDBC ODBC CLI Web UI Thrift server Driver compiler · optimizer · executor Metastore schemas, partitions jobs results Hadoop: MapReduce + HDFS JobTracker NameNode DataNode + TaskTracker the files live here
Fig. 2. Hive's architecture (p. 907), redrawn. A query enters through the CLI, the web interface or the Thrift server (JDBC, ODBC); the Driver compiles, optimizes and executes it with the table definitions of the Metastore. What leaves Hive are MapReduce jobs (orange) for the JobTracker; they run where the HDFS blocks are, and results come back (blue). Dashed ochre outlines: the two masters of Hadoop 1.

Hive keeps the metadata — schemas, partitions, locations — in its Metastore; the data stays in HDFS as plain files, and its queries run as MapReduce jobs.

Tables, partitions, buckets

Page 908 lists the data model. Tables have "Typed columns (int, float, string, date, boolean)" and "Also, array/map/struct for JSON-like data": one column can hold a list, a dictionary or a record, which a flat SQL table cannot. Partitions split a table by the values of one or more columns — "e.g., to range-partition tables by date". Buckets are "Hash partitions within ranges (useful for sampling, join optimization)": inside a partition, the rows are spread over a fixed number of files by hashing one column — the "clustering" of p. 906.

\[ \text{bucket}(r) = \operatorname{hash}(r.\text{col}) \bmod N \]
\(r\)
a row of the partition
col
the bucketing column, for example userid
\(N\)
the number of bucket files, fixed when the table is created

Both uses follow from the hash. One bucket out of N is a sample of about one row in N, spread evenly over the values. And two tables bucketed on the same join key can be joined bucket by bucket, since equal keys always land in matching buckets (background: in HiveQL, CLUSTERED BY (userid) INTO 32 BUCKETS).

"Hive range-partitions a table by date, one partition per range of dates." Not quite: a Hive partition holds one value of the partition columns — one directory per (dt, country) pair on p. 910. A date range in a query reads every single-day partition inside the range (p. 911). The slide's "range-partition" says what you can do with date partitions, not how they are cut; the exam may quote the printed line.

Storage: directories and flat files

Page 909 says where everything lives: a "Warehouse directory in HDFS – e.g., /user/hive/warehouse"; "Table row data stored in subdirectories of warehouse"; "Partitions form subdirectories of table directories"; "Actual data stored in flat files – Control char-delimited text, or SequenceFiles – With custom SerDe, can use arbitrary format".

So a table is a directory, a partition is a directory inside it, and the rows are ordinary files: text with control-character delimiters, or SequenceFiles, Hadoop's binary key-value format. A SerDe (serializer/deserializer) turns the bytes of a file into typed rows when a query reads them, and back when it writes. Hive checks nothing when files arrive: the Metastore's schema is applied at read time — the schema on read of lesson 4. And any other tool that reads HDFS can use the same files; Impala will. A table created with CREATE EXTERNAL TABLE … LOCATION '…' goes one step further: Hive registers files that stay where they are, and dropping the table deletes only its definition (background).

Page 910 creates the table that the rest of the deck queries:

-- p. 910: page views, one directory per (dt, country) pair
CREATE TABLE page_views(viewTime INT, userid BIGINT,
                        page_url STRING, referrer_url STRING,
                        ip STRING COMMENT 'User IP address')
COMMENT 'This is the page view table'
PARTITIONED BY(dt STRING, country STRING)
STORED AS SEQUENCEFILE;

Five typed columns, a comment on ip and one on the table. PARTITIONED BY declares dt and country, which are not in the column list: their values live in directory names, not in the files. STORED AS SEQUENCEFILE picks the file format. In the slide's words, "Partitioning breaks table into separate files for each (dt, country) pair".

The slide's example paths, "/hive/page_view/dt=2008-06-08,country=USA" and "…,country=CA", are a shorthand, with page_view for page_views. In HDFS each partition column is one directory level, under the warehouse of p. 909: /user/hive/warehouse/page_views/dt=2008-06-08/country=USA/, and next to it …/country=CA/ (Fig. 3 below draws the tree).

The Hive CLI of p. 913 lists and describes tables:

hive> show tables;
hive> describe page_views;
hive> describe extended page_views;

"List tables", "Describe a table", "More information": all three answer from the Metastore, without a job (background). describe prints the columns and their types, partition columns included; describe extended adds the HDFS location, the file format and the SerDe.

Asking Hive: partition pruning, joins and groups

Page 911 asks: "Find all page views coming from xyz.com on March 31st". As printed:

-- p. 911 as printed (see the red pen)
SELECT page_views.*
FROM page_views
WHERE page_views.date >= '2008-03-01'
AND page_views.date <= '2008-03-31'
AND page_views.referrer_url like '%xyz.com';

And the point of the slide: "Hive only reads partition 2008-03-01,* instead of scanning entire table". The mechanism has a name.

Partition pruning — before any job starts, the compiler matches the conditions on partition columns against the list of partitions in the Metastore, and only the matching directories become the input of the MapReduce jobs. The other directories are never opened.

The two conditions of the query play opposite roles. The condition on the date is about a partition column — dt on p. 910, see the red pen — and its values are directory names, so the compiler can decide on it without reading a byte. The condition on referrer_url is about a column inside the files: it can only be checked row by row, by the mappers, on whatever survived the pruning. It reduces the output, never the input (Fig. 3).

Corrected for March 31st:

-- from xyz.com on March 31st: reads only dt=2008-03-31
SELECT page_views.*
FROM page_views
WHERE page_views.dt = '2008-03-31'
AND page_views.referrer_url LIKE '%xyz.com';

For the whole month, keep the slide's range on dt: 31 days of every country. The range works on a STRING column because dates written yyyy-mm-dd sort as strings in date order.

WHERE referrer_url LIKE '%xyz.com' (nothing on dt or country) page_views/ country= dt=2008-03-01/ USA CA dt=2008-03-02/ USA CA … 28 more days, all read dt=2008-03-31/ USA CA read: every directory, the whole table full scan

Before: no condition on a partition column

WHERE dt = '2008-03-31' AND referrer_url LIKE '%xyz.com' page_views/ country= dt=2008-03-01/ USA CA dt=2008-03-02/ USA CA … 28 more days, skipped dt=2008-03-31/ USA CA read: 2 directories, chosen before the job pruned

After: a condition on dt

Fig. 3. Partition pruning on the table of p. 910, March 2008, two countries (orange: read by the query). Before: a condition on referrer_url alone cannot skip anything — that column lives inside the files — so every directory is read. After: the condition on dt is matched against the partitions in the Metastore before the job starts, and only the two directories of March 31st become its input; the LIKE is then checked inside them, row by row.

Partition pruning: a condition on a partition column chooses directories before any job runs; a condition on any other column filters rows inside the files that are read.

"A partition works like an index." No: there is no row-level index at all. Pruning skips whole directories by the value in their name; inside the surviving directories every row is read. A query on referrer_url alone is a full scan, however selective it is.

Joins and aggregation

Page 912: "Count users who visited each page by gender". As printed:

-- p. 912 as printed (see the red pen)
SELECT pv.page_url, u.gender, COUNT(DISTINCT u.id)
FROM page_views pv JOIN user u ON (pv.userid = u.id)
GROUP BY pv.page_url, u.gender
WHERE pv.date = '2008-03-03';
-- p. 912 corrected: distinct users per page and gender
SELECT pv.page_url, u.gender, COUNT(DISTINCT u.id)
FROM page_views pv JOIN user u ON (pv.userid = u.id)
WHERE pv.dt = '2008-03-03'
GROUP BY pv.page_url, u.gender;
Table 2. The sample output of p. 912 (the header says count(userid); the query counts u.id, the same value through the join).
page_urlgendercount(userid)
home.phpMALE12,141,412
home.phpFEMALE15,431,579
photo.phpMALE23,941,451
photo.phpFEMALE21,231,314

COUNT(DISTINCT u.id) counts users, not views: a user who opened home.php ten times counts once — and there are tens of millions of them per page in one day. The plan has Pig's shape: a shuffle for the join on pv.userid = u.id, another for the grouping on (page_url, gender), and the jobs are cut at those shuffles. Pruning comes first: only the files of March 3rd enter the join.

The question of p. 900, asked in HiveQL over two tables users(name, age) and pages(username, url), is one statement — switch the engine of the widget above to HiveQL and step through it:

-- p. 900 in HiveQL: top 5 pages of users aged 18-25
SELECT p.url, COUNT(*) AS clicks
FROM users u JOIN pages p ON (u.name = p.username)
WHERE u.age >= 18 AND u.age <= 25
GROUP BY p.url
ORDER BY clicks DESC
LIMIT 5;

The same three shuffles — join, group, order — so the same three jobs, which Hive writes for you. Hive, like Pig, saves your time; the cluster still runs MapReduce.

Impala: SQL beside MapReduce, not on top of it

Page 915: Impala is a "Massive parallel processing (MPP) database engine, developed by Cloudera", "Integrated into Hadoop stack on the same level as MapReduce, and not above it (as Hive and Pig)". The figure of that slide is the thesis of the whole section (Fig. 4).

an Impala query a Hive query Pig Hive Impala MapReduce HDFS read once, rows stay in memory each job writes to HDFS
Fig. 4. The layering of p. 915. Pig and Hive sit on top of MapReduce: a Hive query becomes MapReduce jobs, and each job writes its output to HDFS for the next one to read (teal: down, up, down). Impala sits beside MapReduce, directly on HDFS: its query reads the files and keeps its rows in memory to the end (magenta).

Why build it? Page 916 argues in six lines: "Data has a gravity"; "Today a lot of data live in HDFS"; "It is not practical to move big data"; "It is practical to bring engine to the data"; "In the same time – MapReduce is not must"; "Impala process data in Hadoop cluster without using MapReduce". Big data is too heavy to copy into a separate warehouse, so the engine moves onto the Hadoop machines — MapReduce's own "move the code, not the data" (lesson 30). But nothing forces that engine to be MapReduce, which starts every job from scratch and writes every output to disk. Nor is Impala alone: "Several other modern Database engines also realized the opportunity to bypass MapReduce but work right with HDFS" (p. 917, with Greenplum and JethroData).

MPP, massively parallel processing — the slide prints "Massive". Every node runs a piece of every query at the same time, and the pieces stream rows to each other over the network, without writing intermediate results to disk. It is the architecture of parallel analytic databases.

The architecture: every node plans, coordinates and executes

Follow a query through p. 918 (Fig. 5). A client — any "SQL App", through ODBC, with "Common Hive SQL and interface" — sends it to one Impala node. That node's query planner reads the "Unified metadata": the Hive Metastore for the tables, the HDFS NameNode for where each block lives, the State Store for which nodes are alive. Its query coordinator cuts the plan into fragments and sends them to every node, "Fully MPP Distributed". Each node's query exec engine reads the blocks of its own DataNode — "Local Direct Reads" — or its HBase, and streams its rows back. No job is started, and nothing is written between the steps.

Common Hive SQL and interface SQL app ODBC Unified metadata Hive Metastore HDFS NameNode State Store query planner coordinator exec engine HDFS DN HBase planner coordinator exec engine HDFS DN HBase planner coordinator exec engine HDFS DN HBase fully MPP distributed local direct reads
Fig. 5. Impala's architecture (p. 918), redrawn; every node runs a Query Planner, a Query Coordinator and a Query Exec Engine. The query reaches one node (dashed ochre: the coordinator of this query), whose planner reads the Hive Metastore, the HDFS NameNode and the State Store. Its coordinator sends plan fragments to every node (ink arrows); each exec engine reads the blocks of its own DataNode (short blue arrows) and streams its rows back (blue).

Same tables, faster reads

Page 919: "Impala supports multiple formats. It is kind of schema on read"; "Impala shares metastore with Hive, which enables very simple adoption"; "Internally Impala have well defined way to add new formats". The table page_views that Hive created is the table Impala queries: same definition in the Metastore, same directories, same files. You adopt Impala by pointing it at the warehouse you already have — data gravity again, nothing moves.

Page 920 lists what makes it fast. Impala's "format adapters", called scanners, "have predicate pushdown capability": the WHERE condition is handed to the code that reads the file, so a row that fails it is dropped during the read instead of being materialised and filtered later. It is an "Open source MPP engine" that "Runs hundreds of CPU cores in one query efficiently without expensive license" — and "Hive give us the same but not efficiently". There are scanners for the formats of p. 922 (Table 3).

Table 3. The scanners of p. 922 and how each format lays out a table (the layout column is background).
Format (p. 922)LayoutNote
RCFilecolumnarHive's record columnar file
Parquet (native dremel format)columnaran open format, not Dremel's own: it encodes nested data as Google's Dremel paper does; lesson 40
CSVrow by row, textthe delimited text of p. 909
AVROrow by row, binaryeach file carries its schema
Sequence Filerow by row, binaryHadoop's key-value file of p. 910

With a columnar file, a scanner reads only the columns a query names — the column store of lesson 20, and the precise sense in which HBase is not column-oriented. Partition pruning skips directories, the columnar layout skips columns, predicate pushdown drops rows while they are read: together they make an analytic query read and process a small part of the data.

The price: one failure, one lost query

Page 921 is the honest slide: "Hive is doing things Impala can not do yet, like joins between several big tables"; "Hive has convenient java UDF, while impala has not"; "Impala does not have inter-query fault tolerance"; "In the same time – MapReduce is not good framework for the database engine"; "Always faster then Hive at least 10 times"; "Impala in the cloud is not elastic".

The other points follow from the same design. MPP joins build their tables in memory, so in the slide's version two big tables could not be joined, while Hive's reduce-side join spills to disk and handles any size, slowly. Java UDFs existed only in Hive, and Impala's daemons live on the DataNodes, so it cannot grow and shrink with demand like a cloud service. These describe the early Impala of the slides, and "at least 10 times" is the slide's own claim against Hive on MapReduce: learn them as printed, as facts about that version.

Back to the puzzle. Why keep an engine that is ten times slower? Because a long job must survive failures. A four-hour Hive job on hundreds of machines is likely to see some of them die, and MapReduce simply re-runs their tasks. An Impala query bets on being short enough to restart from scratch — a fine bet for a query of seconds, a ruinous one for a night of ETL. Hence the division of labour that follows from p. 921: Hive for long, heavy batch jobs and joins of big tables, Impala for interactive queries on the same tables, through the same Metastore. Speed was never the question; surviving failures, and tables too big for memory, was.

Impala is faster because it skips MapReduce — no job start-up, rows streamed in memory — and for the same reason a node failure kills the whole query: Hive for long fault-tolerant jobs, Impala for interactive ones.

In January 2008 David DeWitt and Michael Stonebraker called MapReduce "A giant step backward in the programming paradigm for large-scale data intensive applications", "Missing most of the features that are routinely included in current DBMS". Two years later Stonebraker and six co-authors published "MapReduce and Parallel DBMSs: Friends or Foes?" and concluded that the two complement each other: MapReduce is closer to an ETL system than to a database. Hive and Impala, side by side on p. 921, are that truce.

Exam-style questions

The Hadoop ecosystem belongs to the theory part: open answers, multiple choice and true or false, where wrong answers may carry a penalty announced just before the exam. The last question is an exercise of ours on the ER diagram of a past paper.

Describe the data model of HBase, using the zoo example of the slides. How is a cell identified, how is a table stored, and what does "nulls are free" mean?

Show model answer

HBase is modelled on Google's Bigtable: a key-valued row/column store on HDFS for billions of rows and millions of columns. A table is a sparse map in which each cell is addressed by its row key, its column — written family:qualifier — and a timestamp, and holds an untyped byte array. Column families are declared with the table (in the zoo example, animal: and repairs:), while qualifiers such as animal:type and animal:size can be added by any row. Every write creates a new version: enclosure1 has animal:type = lion at t1 and zebra at t2, and a read returns the newest version, zebra. Each column family is stored separately, as a list of (row, timestamp, column) → value entries: three in animal: and one in repairs: for the example. A cell that was never written has no entry at all, so it takes no space — that is "nulls are free"; space grows with the cells written, not with rows times columns. Access is only by key: get a cell or a row, or scan a range of row keys; there is no SQL and no join.

A complete answer covers four things: the cell address (row key, family:qualifier, timestamp) with an untyped value; versions, newest returned; storage per family as (row, timestamp, column) → value entries; the absent entry as the meaning of "nulls are free". Use the zoo: it is the slides' own example. If you mention "column-oriented", add that it means column families, not a column store.

The Pig Latin script below finds the top 5 most visited pages by users aged 18–25. Into how many MapReduce jobs is it compiled, which statements belong to each job, and why that number?

Users = load 'users' as (name, age);
Filtered = filter Users by age >= 18 and age <= 25;
Pages = load 'pages' as (user, url);
Joined = join Filtered by name, Pages by user;
Grouped = group Joined by url;
Summed = foreach Grouped generate group, COUNT(Joined) as clicks;
Sorted = order Summed by clicks desc;
Top5 = limit Sorted 5;
store Top5 into 'top5sites';
Show model answer

Three MapReduce jobs. A MapReduce job has at most one shuffle, so Pig starts a new job for every operator that needs tuples with equal keys brought together across the cluster: the join on name, the group on url and the order by clicks. Job 1 loads users and pages and filters the users by age in the map phase, shuffles both inputs on the name and joins them in the reduce phase. Job 2 reads the joined tuples, shuffles them on url, and counts each group in the reduce phase (COUNT can also run as a combiner). Job 3 shuffles the (url, clicks) pairs on clicks, so they arrive sorted; the reduce side keeps the first five and stores them. LOAD, FILTER, FOREACH … GENERATE and LIMIT need no shuffle of their own and ride along in the map or reduce phase of these jobs; the outputs of jobs 1 and 2 are written to HDFS and read by the next job.

The number alone does not answer the "why": give the rule — one job per shuffle — and the statements assigned to jobs as on p. 904. The tempting wrong answers are nine (one per line) and one ("it is one script"). If you know that real Pig adds a small sampling job before the ORDER, say so after the three jobs of p. 904, never instead of them.

A Hive table page_views is PARTITIONED BY (dt STRING, country STRING) and holds 365 days of data for 4 countries, one partition per (dt, country) pair. How many partition directories does Hive read to answer the query below?

SELECT * FROM page_views
WHERE dt = '2008-03-31' AND referrer_url LIKE '%xyz.com';
  • 1 That would need a condition on country too. With dt alone, every country of that day is read.
  • 4 dt = '2008-03-31' selects one day; country is unconstrained, so the 4 directories of that day are read, and the LIKE filters rows inside them.
  • 124 31 days × 4 countries: the whole of March — what the printed query of p. 911 selects, not this one.
  • 1460 The whole table: that happens only without the dt condition. The LIKE on referrer_url prunes nothing.
Show model answer

4 directories out of 365 × 4 = 1460. Partition pruning matches the condition on the partition column dt against the partitions listed in the Metastore before the job starts, so only the directories dt=2008-03-31/country=… become input. The condition on referrer_url concerns a column inside the files: it is evaluated row by row in the mappers and does not reduce the data read.

Two ideas in one question: a partition is a directory chosen by value before the job runs, and a condition on an ordinary column prunes nothing.

Impala and Hive can query the same tables. Explain why Impala answers interactive queries much faster than Hive, what it gives up in exchange, and when Hive remains the better choice.

Show model answer

Hive compiles a HiveQL query into MapReduce jobs: every job pays a start-up cost, and the output of every job is written to HDFS before the next one reads it. Impala is a massively parallel processing (MPP) engine integrated into the Hadoop stack on the same level as MapReduce, not above it: long-running daemons on the DataNodes plan the query, split it into fragments that run in parallel on every node, read the HDFS blocks locally and stream intermediate rows to each other in memory, and its scanners push predicates down into the read. It shares Hive's Metastore, so it uses the same tables and files without moving any data — the engine is brought to the data. The slides claim it is always at least 10 times faster than Hive. In exchange it has no fault tolerance within a query (the slide says "inter-query"): if a node fails, the whole query is aborted and must be restarted, whereas MapReduce re-runs only the failed tasks because their inputs and outputs are on disk. In the slides' version Impala also could not join several big tables, had no Java UDFs and was not elastic in the cloud. Hive therefore remains the choice for long batch jobs over very large tables, where a restart would be expensive, and Impala for short interactive queries.

The core of the answer is the trade-off, not the speed: the mechanism (MPP daemons, no MapReduce, in-memory streaming, local reads, predicate pushdown), the price (a node failure kills the query — within a query, whatever the printed "inter-query" says) and the consequence (long jobs on Hive, interactive ones on Impala). The other points of p. 921 are dated: say "in the slides" when you use them.

Which of the following statements are true? Select all that apply.

  • Pig and Hive run their queries as MapReduce jobs, whereas Impala runs SQL without MapReduce. True: Pig and Hive sit on top of MapReduce; Impala sits on the same level and processes data "without using MapReduce" (pp. 915–916).
  • HBase stores a NULL marker for every unwritten cell of a row. False: an unwritten cell has no entry at all — "nulls are free" (pp. 893, 895).
  • Hive keeps the schemas of its tables in the Metastore and their data as files in HDFS directories. True: "Maintains list of table schemas" (p. 906); warehouse directory, subdirectories and flat files (p. 909).
  • Tables created in Hive must be redefined in Impala before Impala can query them. False: "Impala shares metastore with Hive, which enables very simple adoption" (p. 919).
  • A Hive bucket spreads the rows of a partition over a fixed number of files by hashing a column. True: "Hash partitions within ranges (useful for sampling, join optimization)" (p. 908).
  • "Can call Hadoop Streaming scripts from HiveQL" means that Hive processes real-time data streams. False: Hadoop Streaming runs mappers and reducers written in any language over standard input and output; stream processing is Storm.
Show model answer

True: the first, the third and the fifth. HBase does not store anything for an unwritten cell; Impala shares the Hive Metastore, so Hive's tables are visible to it at once; Hadoop Streaming is a way of writing MapReduce code in any language, not stream processing.

With a possible penalty, tick only what a slide supports. Each false statement swaps a word: NULL marker for no entry, redefine for share, streaming scripts for stream processing.

An exercise of ours, on the ER diagram of the Exam of 12 Sep 2025 (tracking farmers and their sales at local markets): People (Personal ID, Name, Surname, Age, Address, State) buy Products (Product ID, Name, Quantity, Expiration Date) through the relationship Buy (Buy Date, Amount); Products are sold at Markets (Name, Location, Opening Time, Closing Time, Size) through the relationship Sell.

Suppose you record a large, continuously increasing number of purchases of products at the markets as files in HDFS, and analyse them with Hive, mostly one day at a time. (a) Write the Hive table for the purchases, with the attributes of the ER model you need, partitioned so that a query on one day reads only that day's data. (b) Write a HiveQL query that returns, for each market location, the number of distinct people who bought a product on 20/03/2025 and the total amount. (c) How many partition directories does your query read, and why?

Show model answer
CREATE TABLE purchases(product_id STRING, product_name STRING,
                       market_name STRING, market_location STRING,
                       personal_id STRING, amount DOUBLE)
COMMENT 'One row per purchase of a product at a market'
PARTITIONED BY(buy_date STRING)
STORED AS SEQUENCEFILE;
SELECT market_location, COUNT(DISTINCT personal_id), SUM(amount)
FROM purchases
WHERE buy_date = '2025-03-20'
GROUP BY market_location;

(a) The partition column buy_date is declared in PARTITIONED BY and not in the column list: each day becomes a directory such as /user/hive/warehouse/purchases/buy_date=2025-03-20/. Dates are stored as yyyy-mm-dd strings, which sort in date order. (b) WHERE comes before GROUP BY; COUNT(DISTINCT personal_id) counts people, not purchases. (c) One directory: buy_date is a partition column, so the compiler matches the WHERE against the partitions listed in the Metastore before the job starts and reads only buy_date=2025-03-20; the GROUP BY then needs one shuffle, on market_location.

The exercise is ours: Hive is not among the topics of the practical part (Neo4j, MongoDB, Elasticsearch, Redis, SparkQL), but the paper's own first exercise asks for a columnar design of the same sales. It checks what the theory part can ask about Hive: partition columns declared apart and turned into directories, the WHERE before the GROUP BY, and COUNT(DISTINCT …) counting people. Only attributes of the ER model are used, as the MongoDB exercise of the same paper demands ("Do not add variables besides the ones included in the ER model").

Check yourself

In the zoo table, enclosure3 has only animal:type written. A put writes animal:diet = hay into enclosure3. What changes in storage?

  • The put is refused until the table is altered to declare animal:diet Only column families are declared; qualifiers are free, as the widget's diet column showed.
  • A column animal:diet is added to all three rows, holding NULL where nothing was written Columns are not slots: nothing is written for the other rows, and their animal:diet simply has no entry.
  • One entry, (enclosure3, t, animal:diet) → hay, is added to the list of the family animal: One written cell, one entry, in its own family's list.
  • A new version of every cell of enclosure3 is written, with the same timestamp A put writes only the cells it names; the other cells keep their versions.

Any row can add any qualifier. Why does HBase keep column families at all?

  • They are the unit of storage: each family is kept apart, so reading repairs: never touches the entries of animal: P. 895 stores the two families as separate lists; columns read together belong in one family.
  • They give each column a type HBase is untyped: every value is a byte[] (p. 893).
  • Without families, empty cells would not be free Empty cells are free because absent entries are not stored, in any family.
  • They replace the row key in lookups Lookups go by row key (p. 897); the family only says which list to read.

A Pig script loads a log of visits, filters the visits of 2024, groups them by country, counts each group with foreach … generate and stores the result. By the one-job-per-shuffle rule of p. 904, into how many MapReduce jobs is it compiled?

  • 5, one per statement Statements are not jobs: load, filter, foreach and store run inside the map or reduce phase of a job.
  • 1: only the group needs a shuffle Load and filter on the map side, the shuffle on country, the count and the store on the reduce side.
  • 2: one for the filter, one for the group A filter looks at one tuple at a time: no shuffle, no job of its own.
  • 0: Pig does not use MapReduce Pig "Expresses sequences of MapReduce jobs" (p. 899).

page_views is partitioned by (dt, country), with 365 days and two countries, USA and CA. A query has WHERE country = 'CA' and nothing on dt. Which directories does Hive read?

  • Only the directory country=CA There is no such directory at the top: the country=… directories sit inside each dt=… directory.
  • None can be skipped: country is not the first partition column Any partition column can prune; the Metastore lists every (dt, country) pair.
  • All 730, because dt is not constrained Pruning applies to each partition column: the 365 USA directories are skipped.
  • 365: the country=CA directory inside every dt directory Every day has its CA subdirectory; half of the table is skipped.

Which are reasons why Impala answers a short query faster than Hive? Select all that apply.

  • No MapReduce job has to start: its daemons already run on every node Impala "process data in Hadoop cluster without using MapReduce" (p. 916).
  • It stores the tables in its own, faster storage engine No: it reads the same HDFS files as Hive, through the shared Metastore (p. 919).
  • Intermediate rows stream between nodes in memory instead of being written to HDFS between jobs "Fully MPP Distributed" (p. 918); MapReduce writes every job's output to HDFS.
  • Its scanners push the WHERE conditions down into the reading of the files "predicate pushdown capability" (p. 920): failing rows are dropped while reading.
  • It re-runs only the failed fragment when a node dies That is MapReduce's strength; Impala has no fault tolerance within a query (p. 921).

All three true reasons are one design: long-running processes that keep the data moving in memory. The same design is why a failure kills the query.

A nightly job joins three tables of several terabytes each and runs for four hours on a large cluster, where a machine fails every few hours. By the slides, Hive or Impala?

  • Impala, because it is always at least 10 times faster A node failure would abort the whole query, and p. 921 lists joins between several big tables as something Impala could not do yet.
  • Either: they share the Metastore, so they behave the same They share tables and files, not the way the query runs.
  • Hive: MapReduce re-runs failed tasks from data on disk, and it handles joins of big tables Long, fault-tolerant batch work is Hive's niche: "Hive is doing things Impala can not do yet, like joins between several big tables" (p. 921).
  • Neither: only Pig can join three tables Hive joins with SQL syntax (p. 912), any number of tables.

Key take-aways

What to watch and read