Big Data

Pig statements, types and joins

This lecture covers Pig Latin statements, data types, expression operators, JOIN, COGROUP and STREAM. It also covers the DESCRIBE, EXPLAIN and ILLUSTRATE toolkit. After this page you can trace FILTER, GROUP and JOIN pipelines on small relations and use the diagnostic commands.

Pig Latin statementsSearch on YouTube

A Pig Latin statement is an operator that takes a relation as input and produces a relation as output. LOAD and STORE are the exception. They read from the file system and write to it instead of mapping relation to relation.

Statements can span several lines. Every statement must end with a semicolon (;). A script has a fixed shape: LOAD, then a series of transformations, then DUMP or STORE.

LOAD  →  transformations  →  DUMP or STOREevery statement ends with ;\text{LOAD} \;\to\; \text{transformations} \;\to\; \text{DUMP or STORE}\qquad\text{every statement ends with } \mathbf{;}

LOAD reads data in. Transformations (FILTER, FOREACH, GROUP, JOIN) map relation to relation. DUMP prints to the screen. STORE writes to files. A missing ; is a syntax error.

DUMP or STORE is required to get output. Without one, Pig checks the script but never runs it. This split matters for debugging: a script with no output action only gets validated.

Worked example. Two scripts differ by one last line.

A = LOAD 'student' USING PigStorage()
    AS (name:chararray, age:int, gpa:float);
B = FOREACH A GENERATE name;

Without output: validated but never run.

Pig parses both statements and checks names and types. It starts no MapReduce job. Nothing prints. The slide shows no output for this form.

With output: validated and executed.

A = LOAD 'student' USING PigStorage()
    AS (name:chararray, age:int, gpa:float);
B = FOREACH A GENERATE name;
DUMP B;

Output:

(John)
(Mary)
(Bill)
(Joe)

DUMP B forces execution of the whole chain. Each tuple holds one name field.

LOAD transforms DUMP output No DUMP or STORE means validation with no execution
Script flow. LOAD feeds transformations, DUMP or STORE triggers the run.

Comments, case and REGISTERSearch on YouTube

Line comments start with --. Block comments sit between /* and */. Keywords such as LOAD and FOREACH are case-insensitive. Relation names, field names and function names are case-sensitive, so Students and students are different relations.

REGISTER makes a JAR file available to a script. The usual use is custom UDFs (Part 3 covers them). After registering, the script calls the UDF by its fully qualified class name inside FOREACH ... GENERATE.

-- single-line comment
/* multi-line
   comment block */
REGISTER ./tutorial.jar;

REGISTER takes the JAR path and ends with ;. Keywords accept any case. Relation, field and function names keep their exact case.

Multi-query executionSearch on YouTube

By default Pig runs all statements of a script together. It looks for ways to share work across the several DUMP and STORE outputs of one script. Batch runs of a .pig script are thus more efficient than typing the same statements one by one in Grunt.

Data typesSearch on YouTube

Every field has a type from the table below. LOAD ... AS sets types explicitly. Otherwise Pig infers them. The complex types (tuple, bag, map) repeat the Part 1 model. A field (also called atom) builds a tuple. Tuples build a bag. Bags build a relation. A map holds key-value pairs.

TypeSizeExample literal
intsigned 32-bit integer8
longsigned 64-bit integer5L
floatsigned 32-bit float5.5F
double64-bit float10.5
chararraystring, UTF-8'tutorial'
bytearraybyte array / blob—
booleantrue / falsetrue
datetimedate-time value1970-01-01T00:00Z
bigintegerJava BigInteger60708090709
bigdecimalJava BigDecimal185.9837

Complex shapes: tuple (raja, 30), bag {(raju,30),(md,45)}, map [name#Raju, age#30].

NULL valuesSearch on YouTube

Every type, scalar or complex, can hold NULL. A NULL is an unknown or missing value. It serves as a placeholder for optional data. Nulls come from the source data itself. They also come from operations: the unmatched side of an outer JOIN, or a field that a UDF could not compute. Pig treats nulls much like SQL does.

clean = FILTER students BY city is not null;

is null and is not null are the tests for null-ness. == and != do not match NULL in a reliable way. Use the forms above inside FILTER.

Worked example. Keep rows whose city is known.

clean = FILTER students BY city is not null;
idnamecity
1RajivHyderabad
2OmerNULL
3RajeshDelhi

Result.

idnamecity
1RajivHyderabad
3RajeshDelhi

Row 2 fails the test because its city is NULL. Two of three rows survive.

Expression operatorsSearch on YouTube

Arithmetic and conditional operatorsSearch on YouTube

Suppose a = 10 and b = 20. Pig provides +, -, *, / and % (remainder). The bincond ?: picks one of two values from a boolean test. CASE ... WHEN ... THEN ... END chains several branches and reads better than nested binconds.

a+b→30a−b→−10a×b→200b/a→2b mod a→0\begin{aligned} a + b &\to 30 \qquad a - b \to -10 \qquad a \times b \to 200\\ b / a &\to 2 \qquad b \bmod a \to 0 \end{aligned}
x = (a == 1) ? 20 : 30;
CASE f2 % 2
  WHEN 0 THEN 'even'
  WHEN 1 THEN 'odd'
END

The bincond tests first, then returns the value before : or after it. CASE matches the expression against each WHEN in order.

Worked example. Evaluate each operator with a = 10, b = 20.

a+b=10+20=30a−b=10−20=−10a×b=10×20=200b/a=20/10=2b mod a=20 mod 10=0\begin{aligned} a + b &= 10 + 20 = \mathbf{30}\\ a - b &= 10 - 20 = \mathbf{-10}\\ a \times b &= 10 \times 20 = \mathbf{200}\\ b / a &= 20 / 10 = \mathbf{2}\\ b \bmod a &= 20 \bmod 10 = \mathbf{0} \end{aligned}

Bincond. (a == 1) ? 20 : 30 with a = 10.

a==1→falsex=30 (the value after the colon)\begin{aligned} a == 1 &\to \text{false}\\ x &= \mathbf{30}\ \text{(the value after the colon)} \end{aligned}

CASE parity. f2 holds a count.

f2=7: 7 mod 2=1→’odd’f2=8: 8 mod 2=0→’even’\begin{aligned} f2 = 7&:\ 7 \bmod 2 = 1 \to \mathbf{\text{'odd'}}\\ f2 = 8&:\ 8 \bmod 2 = 0 \to \mathbf{\text{'even'}} \end{aligned}

Comparison and type construction operatorsSearch on YouTube

OperatorMeaning
==Equal to
!=Not equal to
>Greater than
<Less than
>=Greater than or equal to
<=Less than or equal to
matchesPattern match against a regex

f1 matches '.*tutorial.*' is true when f1 holds tutorial anywhere. The dots match any characters and the stars repeat them.

The three bracket pairs build complex values inline:

ConstructorBuildsLiteral shape
( )tuple(Raju, 30)
{ }bag{(Raju,30),(Md,45)}
[ ]map[name#Raja, age#30]

These literals have the same shapes as the data types above. Use them to write tuple, bag and map values direct in a script.

Inner JOINSearch on YouTube

JOIN combines tuples from two or more relations with matching values on a key. By default it is an inner join: only keys present in every relation appear in the result. A tuple with no match on the other side drops out entire. That drop is what makes it "inner".

students = LOAD 'students.txt' USING PigStorage(',')
    AS (id:int, name:chararray, city:chararray);
fees = LOAD 'fees.txt' USING PigStorage(',')
    AS (id:int, amount:int);
paid = JOIN students BY id, fees BY id;
students
1RajivHyderabad
2OmerKolkata
3RajeshDelhi
fees
1500
3700

Worked example. Trace paid = JOIN students BY id, fees BY id.

Key 1. Students has (1,Rajiv,Hyderabad). Fees has (1,500). Both sides match, so the pair merges into one wide tuple.

Key 2. Students has (2,Omer,Kolkata). Fees has no id = 2. No match exists, so key 2 drops out.

Key 3. Students has (3,Rajesh,Delhi). Fees has (3,700). Both sides match, so they merge.

Result: 2 tuples.

(1,Rajiv,Hyderabad,1,500)
(3,Rajesh,Delhi,3,700)

id = 2 (Omer) never appears in an inner-join result. Each result row joins one students tuple with one fees tuple.

A relation can join with itself (self join). Load it twice under two aliases. Then JOIN the aliases by the shared key.

Write JOIN ... BY on both sides with the key after each relation. Declare the same type for join keys on both sides with AS.

Outer JOINsSearch on YouTube

Outer joins keep the tuples that an inner join would drop. The missing side fills with NULL.

left_j = JOIN students BY id LEFT OUTER, fees BY id;
right_j = JOIN students BY id RIGHT OUTER, fees BY id;
full_j = JOIN students BY id FULL OUTER, fees BY id;
FormKeepsNULLs appear on
LEFT OUTERevery tuple of the left relation (students)the fees side, where no match exists
RIGHT OUTERevery tuple of the right relation (fees)the students side, where no match exists
FULL OUTERevery tuple of both relationswhichever side lacks a match
LEFT keeps 1,2,3 RIGHT keeps 1,3 FULL keeps 1,2,3 key 2 carries NULL fees fields solid box = kept key, dashed = dropped key
Kept keys for each outer form on the students and fees data.

Worked example. Outer joins on the same students and fees data.

LEFT OUTER: 3 tuples. Every students row survives.

(1,Rajiv,Hyderabad,1,500)
(2,Omer,Kolkata,,)
(3,Rajesh,Delhi,3,700)

Omer has no fees row, so the fees fields come back empty (NULL).

RIGHT OUTER: 2 tuples. Every fees row survives.

(1,Rajiv,Hyderabad,1,500)
(3,Rajesh,Delhi,3,700)

Both fees rows (id 1 and 3) match a student. No NULL appears here. The result equals the inner join for this data.

FULL OUTER: 3 tuples. Every row of both sides survives.

(1,Rajiv,Hyderabad,1,500)
(2,Omer,Kolkata,,)
(3,Rajesh,Delhi,3,700)

Here FULL equals LEFT because only the students side has an unmatched key. With an unmatched fee (say id = 9), FULL would add a row with NULL student fields.

LEFT OUTER keeps all left keys but drops unmatched right tuples. RIGHT OUTER keeps all right keys but drops unmatched left tuples. Only FULL OUTER keeps every key from both sides. Each form adds NULL fields on the missing side. Filter or test those fields with is not null before arithmetic, or the NULL spreads through sums and counts.

COGROUPSearch on YouTube

GROUP groups one relation by a key. COGROUP groups two or more relations by a shared key at once. Each key gives one result tuple of the shape below.

(key, {matching tuples of relation 1}, {matching tuples of relation 2})\text{(key, \{matching tuples of relation 1\}, \{matching tuples of relation 2\})}
cogrouped = COGROUP students BY id, fees BY id;

Each relation keeps its own bag of matches. Unmatched keys stay in the result with an empty bag {}.

(1, {(1,Rajiv,Hyderabad)}, {(1,500)})
(2, {(2,Omer,Kolkata)}, {})
(3, {(3,Rajesh,Delhi)}, {(3,700)})

Key 2 still appears, with an empty bag on the fees side. COGROUP never drops a key in silence. That is the core difference from an inner JOIN.

JOINCOGROUP
Flattens each match into one wide tupleNests each relation's matches into its own bag
Drops keys with no match (inner form)Keeps every key, with {} where a side has no rows
Use to attach columns from two relationsUse to compute per-key summaries before flattening

The slides give the orders/customers case: joining orders with customers by customer_id merges each order and its customer into one flat row. COGROUP instead gives one bag of orders and one bag of customer info per customer_id. That shape suits a per-customer total computed before any flattening.

JOIN: flat wide rows COGROUP: key plus two bags dashed bag = empty, key kept anyway
JOIN output rows are flat. COGROUP output rows hold nested bags.

Worked example. Read the three COGROUP result rows.

Key 1. Students bag holds (1,Rajiv,Hyderabad). Fees bag holds (1,500). Both bags are full.

Key 2. Students bag holds (2,Omer,Kolkata). Fees bag is {}. The key survives with one empty bag.

Key 3. Students bag holds (3,Rajesh,Delhi). Fees bag holds (3,700). Both bags are full.

Three keys in, three rows out. An inner JOIN on the same data gives two rows because key 2 drops out.

STREAMSearch on YouTube

STREAM sends a relation through an external program in any language. Each tuple goes to the program's stdin, one line at a time. Each line of stdout becomes a tuple of the output relation.

cleaned = STREAM raw_logs THROUGH `normalize.py`;

Backticks mark the command. Pig serialises tuples to text lines, runs the program, and reads lines back as tuples. Field structure of the output depends on the script.

Reasons to use it:

  • Reuse an existing Perl, Python or shell script instead of rewriting logic as a UDF.
  • Plug in text-processing or cleansing tools already in use outside Pig.
  • Prototype a transform fast before writing it as a formal Java UDF.

The slides reuse a Python cleaner this way. It applies a messy-city-name script direct, with no Java rewrite.

cleaned = STREAM orders THROUGH `clean_city.py`;

DESCRIBE, EXPLAIN and ILLUSTRATESearch on YouTube

These three commands show what a script does without full runs:

CommandShows
DESCRIBE relation;Schema of a relation: field names and types, so a LOAD or transform can be sanity-checked
EXPLAIN relation;Logical, physical and MapReduce execution plans: how the script becomes jobs
ILLUSTRATE relation;Small sample traced step by step through the pipeline, so each statement's effect shows without the full job

Grunt shortcuts for the same commands:

ShortcutCommand
\d aliasDUMP
\de aliasDESCRIBE
\e aliasEXPLAIN
\i aliasILLUSTRATE
\qquit Grunt

D-E-I: d, de, e, i

DUMP is \d · DESCRIBE is \de · EXPLAIN is \e · ILLUSTRATE is \i. \q quits. Schema, plan, sample: that is the debug order.

Worked example. DESCRIBE on the enriched orders relation (Part 2 of the pipeline below).

DESCRIBE enriched;

Schema. Each JOIN side keeps its qualified names.

enriched: {valid::order_id: int, valid::customer_id: int,
  valid::city: chararray, valid::amount: double,
  customers::customer_id: int, customers::name: chararray,
  customers::signup_year: int}

Seven fields: four from valid, three from customers. The :: prefix tells which input each field came from. EXPLAIN revenue would next show whether the FILTER moved before the GROUP. ILLUSTRATE enriched would push one real order through every step.

Practice pipeline: e-commerce ordersSearch on YouTube

The slides end with a bookstore pipeline over two small CSV files. It answers business questions step by step.

orders.csv
order_id (int)customer_id (int)city (chararray)amount (double)
1011Hyderabad450.0
1022Kolkata0.0
1031Hyderabad220.5
1043Delhi180.0
customers.csv
customer_id (int)name (chararray)signup_year (int)
1Rajiv2021
2Omer2022
3Rajesh2020
4Divya2023

Pipeline part 1: revenue by citySearch on YouTube

orders = LOAD 'orders.csv' USING PigStorage(',')
    AS (order_id:int, customer_id:int, city:chararray, amount:double);
valid = FILTER orders BY amount > 0 AND city is not null;
by_city = GROUP valid BY city;
revenue = FOREACH by_city GENERATE
    group AS city, SUM(valid.amount) AS total;
ranked = ORDER revenue BY total DESC;
top5 = LIMIT ranked 5;

Worked example. Run the pipeline on the four orders.

FILTER. Order 102 has amount = 0.0, so the test amount > 0 drops it. All cities are known. Three rows survive: 101, 103, 104.

GROUP BY city. Two groups form.

Hyderabad: {(101,1,Hyderabad,450.0), (103,1,Hyderabad,220.5)}
Delhi:     {(104,3,Delhi,180.0)}

SUM per group.

Hyderabad=450.0+220.5=670.5Delhi=180.0\begin{aligned} \text{Hyderabad} &= 450.0 + 220.5 = \mathbf{670.5}\\ \text{Delhi} &= \mathbf{180.0} \end{aligned}

ORDER DESC and LIMIT 5. Hyderabad (670.5) ranks above Delhi (180.0). Only two cities exist, so LIMIT 5 keeps both.

(Hyderabad,670.5)
(Delhi,180.0)

Hyderabad brings the most revenue. Kolkata never appears because its one order was a zero-amount cancelled row.

Pipeline part 2: customers with and without ordersSearch on YouTube

customers = LOAD 'customers.csv' USING PigStorage(',')
    AS (customer_id:int, name:chararray, signup_year:int);
enriched = JOIN valid BY customer_id, customers BY customer_id;
inactive = JOIN customers BY customer_id LEFT OUTER, valid BY customer_id;
DESCRIBE enriched;
EXPLAIN revenue;
ILLUSTRATE enriched;

Worked example. Attach orders to customers, then find the inactive ones.

Inner JOIN (enriched): 3 tuples. Valid orders carry customer_id 1, 1 and 3. All three match a customer row.

(101,1,Hyderabad,450.0,1,Rajiv,2021)
(103,1,Hyderabad,220.5,1,Rajiv,2021)
(104,3,Delhi,180.0,3,Rajesh,2020)

LEFT OUTER (inactive): 4 customer rows kept. Customers 2 (Omer) and 4 (Divya) have no valid order, so their order fields are NULL.

(1,Rajiv,2021,101,1,Hyderabad,450.0)
(2,Omer,2022,,,,)
(3,Rajesh,2020,104,3,Delhi,180.0)
(4,Divya,2023,,,,)

Omer signed up in 2022 and Divya in 2023, but neither placed a kept order. (Omer's one order was the zero-amount row that FILTER removed.) A further FILTER inactive BY valid::order_id is null would keep exactly these two rows.

Sanity checks. DESCRIBE enriched confirms the seven-field schema after the JOIN. EXPLAIN revenue shows the job plan and early FILTER. ILLUSTRATE enriched traces one order through each step. Run all three before scheduling the nightly job.

orders FILTER GROUP+SUM ORDER JOIN inactive
Orders split into revenue ranking (ORDER) and customer matching (JOIN). FILTER feeds both.

Before scheduling a nightly job, run the three checks in order. DESCRIBE confirms the schema after each JOIN. EXPLAIN shows whether the FILTER moved early. ILLUSTRATE traces one real row through every step.

Quick self-testSearch on YouTube

1. A script has LOAD, FILTER and FOREACH but no DUMP or STORE. Does it run?

No. Pig validates the script but starts no job. Add DUMP or STORE to force execution.

2. With a = 10 and b = 20, compute a + b, a - b, a * b, b / a and b % a.

3030, −10-10, 200200, 22 and 00. 20 mod 1020 \bmod 10 leaves no remainder.

3. CASE f2 % 2 WHEN 0 THEN 'even' WHEN 1 THEN 'odd' END: what do f2 = 7 and f2 = 8 give?

7 mod 2=17 \bmod 2 = 1, so f2 = 7 gives 'odd'. 8 mod 2=08 \bmod 2 = 0, so f2 = 8 gives 'even'.

4. LEFT OUTER JOIN students BY id, fees BY id on the lecture data: how many tuples, and which row holds NULLs?

Three tuples. (2,Omer,Kolkata) keeps its student fields and gets NULL fees fields. Keys 1 and 3 match on both sides.

5. Revenue pipeline: what is Hyderabad's total, and which city ranks first?

Hyderabad: 450.0+220.5=670.5450.0 + 220.5 = 670.5. Delhi: 180.0180.0. Hyderabad ranks first under ORDER ... DESC.

6. Differentiate JOIN and COGROUP.

JOIN flattens each match into one wide tuple and drops unmatched keys (inner form). COGROUP nests each relation's matches into its own bag and keeps every key with {} where a side has no rows.

7. Which of DESCRIBE, EXPLAIN, ILLUSTRATE shows the schema, the job plan and the sample trace? What is the Grunt shortcut for the schema?

DESCRIBE shows the schema. EXPLAIN shows the logical, physical and MapReduce plans. ILLUSTRATE traces a small sample through the pipeline. The schema shortcut is \de revenue.

8. Why does COGROUP keep id 2 with an empty bag while an inner JOIN drops it?

COGROUP groups each relation apart, so a missing side becomes {} and the key stays. An inner JOIN needs a match in every relation, so key 2 has no pair to flatten.

9. When do you reach for STREAM instead of writing a UDF?

When a script in Python, Perl or shell already does the transform. STREAM ... THROUGH reuses it direct instead of a Java rewrite. It also suits fast prototypes before a formal UDF.

10. Are LOAD and load the same keyword? Are students and Students the same relation?

Keywords are case-insensitive, so LOAD and load match. Relation names are case-sensitive, so students and Students are two different relations.

  • A Pig Latin statement maps a relation to a relation, except LOAD and STORE. Every statement ends with ;. DUMP or STORE triggers execution.
  • Keywords are case-insensitive. Relation, field and function names are case-sensitive. -- and /* */ mark comments. REGISTER loads a JAR.
  • Multi-query execution shares work across the outputs of one script. Batch .pig runs beat one-by-one Grunt typing.
  • Types: int, long, float, double, chararray, bytearray, boolean, datetime, biginteger, bigdecimal, plus tuple ( ), bag { } and map [ ]. Any field can be NULL.
  • Test nulls with is null / is not null. Never use == or != for them.
  • Arithmetic + - * / %, bincond ?: and CASE ... WHEN ... THEN ... END build expressions. matches tests a regex.
  • Inner JOIN keeps only keys in every relation. LEFT, RIGHT and FULL OUTER keep one side or both, with NULL on missing sides.
  • COGROUP returns (key, bag1, bag2) and keeps unmatched keys with {}.
  • STREAM ... THROUGH pipes tuples through an external program: stdin lines in, stdout lines out.
  • DESCRIBE shows the schema, EXPLAIN shows the plans, ILLUSTRATE traces a sample. Shortcuts: \d \de \e \i \q.
  • The e-commerce pipeline pattern is FILTER (drop bad rows), GROUP + SUM, ORDER DESC, LIMIT, then JOIN for enrichment and LEFT OUTER for inactive customers.