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 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.
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.
| Type | Size | Example literal |
|---|---|---|
int | signed 32-bit integer | 8 |
long | signed 64-bit integer | 5L |
float | signed 32-bit float | 5.5F |
double | 64-bit float | 10.5 |
chararray | string, UTF-8 | 'tutorial' |
bytearray | byte array / blob | — |
boolean | true / false | true |
datetime | date-time value | 1970-01-01T00:00Z |
biginteger | Java BigInteger | 60708090709 |
bigdecimal | Java BigDecimal | 185.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;| id | name | city |
|---|---|---|
| 1 | Rajiv | Hyderabad |
| 2 | Omer | NULL |
| 3 | Rajesh | Delhi |
Result.
| id | name | city |
|---|---|---|
| 1 | Rajiv | Hyderabad |
| 3 | Rajesh | Delhi |
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.
x = (a == 1) ? 20 : 30;
CASE f2 % 2
WHEN 0 THEN 'even'
WHEN 1 THEN 'odd'
ENDThe 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.
Bincond. (a == 1) ? 20 : 30 with a = 10.
CASE parity. f2 holds a count.
Comparison and type construction operatorsSearch on YouTube
| Operator | Meaning |
|---|---|
== | Equal to |
!= | Not equal to |
> | Greater than |
< | Less than |
>= | Greater than or equal to |
<= | Less than or equal to |
matches | Pattern 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:
| Constructor | Builds | Literal 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 | ||
|---|---|---|
| 1 | Rajiv | Hyderabad |
| 2 | Omer | Kolkata |
| 3 | Rajesh | Delhi |
| fees | |
|---|---|
| 1 | 500 |
| 3 | 700 |
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;| Form | Keeps | NULLs appear on |
|---|---|---|
LEFT OUTER | every tuple of the left relation (students) | the fees side, where no match exists |
RIGHT OUTER | every tuple of the right relation (fees) | the students side, where no match exists |
FULL OUTER | every tuple of both relations | whichever side lacks a match |
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.
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.
JOIN | COGROUP |
|---|---|
| Flattens each match into one wide tuple | Nests 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 relations | Use 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.
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:
| Command | Shows |
|---|---|
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:
| Shortcut | Command |
|---|---|
\d alias | DUMP |
\de alias | DESCRIBE |
\e alias | EXPLAIN |
\i alias | ILLUSTRATE |
\q | quit 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) |
| 101 | 1 | Hyderabad | 450.0 |
| 102 | 2 | Kolkata | 0.0 |
| 103 | 1 | Hyderabad | 220.5 |
| 104 | 3 | Delhi | 180.0 |
| customers.csv | ||
|---|---|---|
customer_id (int) | name (chararray) | signup_year (int) |
| 1 | Rajiv | 2021 |
| 2 | Omer | 2022 |
| 3 | Rajesh | 2020 |
| 4 | Divya | 2023 |
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.
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.
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.
, , , and . leaves no remainder.
3. CASE f2 % 2 WHEN 0 THEN 'even' WHEN 1 THEN 'odd' END: what do f2 = 7 and f2 = 8 give?
, so f2 = 7 gives 'odd'. , 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: . Delhi: . 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
LOADandSTORE. Every statement ends with;.DUMPorSTOREtriggers execution. - Keywords are case-insensitive. Relation, field and function names are case-sensitive.
--and/* */mark comments.REGISTERloads a JAR. - Multi-query execution shares work across the outputs of one script. Batch
.pigruns 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 beNULL. - Test nulls with
is null/is not null. Never use==or!=for them. - Arithmetic
+ - * / %, bincond?:andCASE ... WHEN ... THEN ... ENDbuild expressions.matchestests a regex. - Inner
JOINkeeps only keys in every relation.LEFT,RIGHTandFULL OUTERkeep one side or both, withNULLon missing sides. COGROUPreturns(key, bag1, bag2)and keeps unmatched keys with{}.STREAM ... THROUGHpipes tuples through an external program: stdin lines in, stdout lines out.DESCRIBEshows the schema,EXPLAINshows the plans,ILLUSTRATEtraces a sample. Shortcuts:\d \de \e \i \q.- The e-commerce pipeline pattern is
FILTER(drop bad rows),GROUP+SUM,ORDER DESC,LIMIT, thenJOINfor enrichment andLEFT OUTERfor inactive customers.
Pig Latin Data Model and Data Types
Apache Pig Latin