Big Data

Partitioning, bucketing and joins

This lecture organises Hive tables on disk so queries skip data (managed tables, partitioning, bucketing). It then covers the HiveQL joins that combine tables. After this page you can compute bucket numbers, count cross-join rows and list which partition directories a query reads.

The problem: full scans do not scaleSearch on YouTube

A 500 GB orders table grows every day. A query for the orders of 2026-08-11 still reads all 500 GB. It opens every file and every row, then throws away everything except one day.

The bigger the table gets, the worse this scales. Every query pays the full cost of the whole table, no matter how little of it the query needs.

The slides give two different fixes. Partitioning splits data into separate directories, so a query skips whole directories it does not need. It suits columns you filter on often. Bucketing splits data into a fixed number of files by a hash of a column value. It suits high-cardinality columns used in joins or sampling. Neither replaces the other. The two combine on the same table.

Think of a library with no sections. To find one day's newspaper you would open every bundle on every shelf. Partitioning adds labelled rooms. Bucketing sorts each room into a fixed set of boxes.

Managed and external tablesSearch on YouTube

Before partitioning or bucketing, every Hive table is one of two types. The choice is made at CREATE TABLE time. A managed (also called internal) table is fully owned by Hive. Its files live inside Hive's own warehouse directory. An external table keeps only metadata in Hive: a schema and a LOCATION pointer. The files live wherever that location points, and something else may own them.

Creating each typeSearch on YouTube

CREATE TABLE sales.orders (
  order_id INT, customer_id INT, amount DOUBLE, order_date STRING
)
STORED AS ORC;
-- data lands in the default warehouse path:
-- /user/hive/warehouse/sales.db/orders/

No LOCATION is given, so Hive picks the path inside its own warehouse directory.

CREATE EXTERNAL TABLE sales.orders_ext (
  order_id INT, customer_id INT, amount DOUBLE
)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY ','
LOCATION '/user/shared/orders_raw';

The EXTERNAL keyword plus an explicit LOCATION tells Hive to read data it does not own. A raw landing zone can be queried in place with no copy step. Engines such as Spark or Presto can read the same files.

What a DROP deletesSearch on YouTube

Managed table. Drop deletes everything.

DROP TABLE sales.orders;

Both the table definition and the data files Hive loaded are deleted. Nothing survives. Hive owned the data, so Hive removes it.

External table. Drop deletes only metadata.

DROP TABLE sales.orders_ext;
-- OK
-- metadata gone, but the file at
-- /user/shared/orders_raw is untouched

The data file still sits on HDFS. Check it with hdfs dfs -ls right after the drop.

When to use eachSearch on YouTube

Use a managed table when Hive should fully own the data lifecycle. It suits staging or intermediate tables that only Hive touches. It also suits the case where DROP TABLE should mean "delete everything" on purpose.

Use an external table when the data is shared with another team or tool. It suits a raw landing zone that you query in place. It suits files that Spark, Presto or another engine also reads. It also protects against an accidental DROP TABLE, since the files survive.

ManagedExternal
Created withCREATE TABLECREATE EXTERNAL TABLE ... LOCATION
Data locationHive's own warehouse directoryWherever LOCATION points
Who owns the dataHive, fullySomething else (or shared)
DROP TABLE effectDeletes metadata AND dataDeletes metadata ONLY
Best forTables Hive manages end to endShared or landing-zone data, multi-tool access

Partitioning on diskSearch on YouTube

Partitioning splits a table into subdirectories based on the value of one or more partition columns. On HDFS it is only a directory structure:

/warehouse/sales.db/orders_partitioned/
  order_date=2026-08-10/
    000000_0
  order_date=2026-08-11/
    000000_0
  order_date=2026-08-12/
    000000_0

Each directory holds only the rows for that one partition value. The partition column itself is NOT stored inside the data files. Hive infers its value from the directory name. That is why it is sometimes called a virtual column.

The older deck shows the same idea with 50 thousand student or employee rows. Fetching the students of one course the traditional way scans everything, so performance degrades as data grows. Partitioning divides the data by a required column such as date, course, city or country. The query then reads only one partition, so the response time drops.

Static partitioningSearch on YouTube

In static (manual) partitioning you pass the partition values by hand while loading. The data files do not contain the partition columns. First declare the partition column separately:

CREATE TABLE sales.orders_partitioned (
  order_id INT, customer_id INT, amount DOUBLE
)
PARTITIONED BY (order_date STRING)
STORED AS ORC;

order_date stays out of the main column list. It appears only in PARTITIONED BY. Then load one partition at a time:

INSERT OVERWRITE TABLE sales.orders_partitioned
  PARTITION (order_date='2026-08-10')
SELECT order_id, customer_id, amount
FROM sales.orders
WHERE order_date='2026-08-10';

You name the exact partition value. Repeat once per day, customer, region or whatever the column is. The older deck uses LOAD DATA for the same step:

LOAD DATA LOCAL INPATH '/home/codegyani/hive/student_details1'
INTO TABLE student PARTITION (course='java');
LOAD DATA LOCAL INPATH '/home/codegyani/hive/student_details2'
INTO TABLE student PARTITION (course='hadoop');

Each file lands in its own course=... directory. Later queries read only the directory they need instead of examining the entire data.

Dynamic partitioningSearch on YouTube

Dynamic partitioning lets Hive do the splitting. The partition values already exist inside the data, so you never name them by hand. The deck works through six steps on a student table.

Steps 1 and 2. Pick the database and enable the feature.

USE show;
SET hive.exec.dynamic.partition = true;
SET hive.exec.dynamic.partition.mode = nonstrict;

Steps 3 and 4. Build and fill a dummy staging table.

CREATE TABLE stud_demo (id INT, name STRING, age INT,
  institute STRING, course STRING)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY ',';
LOAD DATA LOCAL INPATH '/home/codegyani/hive/student_details'
INTO TABLE stud_demo;

Steps 5 and 6. Create the partition table and insert.

CREATE TABLE student_part (id INT, name STRING, age INT, institute STRING)
PARTITIONED BY (course STRING)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY ',';
INSERT INTO student_part
  PARTITION (course)
SELECT id, name, age, institute, course FROM stud_demo;

The partition column course is now the LAST column in the SELECT. Hive reads its value per row and creates whichever partition directories are needed automatically.

The slide screenshot shows the result: one directory per value found in the data.

/user/hive/warehouse/show.db/student_part/
  course=__HIVE_DEFAULT_PARTITION__/
  course=hadoop/
  course=java/

Hive creates one directory per value found in the data. Rows whose partition value is missing land in course=__HIVE_DEFAULT_PARTITION__. A plain SELECT * FROM student_part returns all rows. A filtered query such as SELECT * FROM student_part WHERE course='java' reads only the matching directory.

Partition pruning: the actual payoffSearch on YouTube

SELECT * FROM sales.orders_partitioned
WHERE order_date = '2026-08-11';

On the partitioned table only the order_date=2026-08-11 directory is opened. The 08-10 and 08-12 directories are never read. They are not filtered after reading. They are never touched at all.

On an unpartitioned table every file must be opened and every row inspected, to discard everything that is not 08-11. Same result, far more I/O. This skipping of whole directories is called partition pruning.

order_date 2026-08-10 order_date 2026-08-11 order_date 2026-08-12 skipped read skipped WHERE order_date = '2026-08-11'
Partition pruning. The filter names one directory, so Hive opens only that directory.

Worked example. Three day directories hold one day each.

A query with WHERE order_date = '2026-08-11' reads 1 of the 3 directories. It skips 2 of 3. With one directory per day, a single-day query touches a fixed small share no matter how many days the table holds.

Choosing partition columnsSearch on YouTube

Good candidates have low-to-moderate cardinality and appear often in WHERE clauses. Examples are order_date, region, country and status. Poor candidates have high cardinality or are rarely filtered directly. Examples are customer_id, order_id and exact timestamps.

Why it matters: every partition is a directory. Every directory is an entry the Metastore must track and the query planner must consider. Partition on customer_id and a table with a million customers gets a million directories. That is the "small files problem": strained NameNode memory and slow planning, for a column that pruning barely helps anyway.

Do not partition on customer_id or exact timestamps. One directory per value strains the Metastore and slows planning. Use bucketing for high-cardinality columns instead.

Bucketing and the hash ruleSearch on YouTube

Bucketing splits a table into a FIXED number of files using a hash of a column value. It suits the case where partitioning turns hard: too many distinct values for directories. Buckets can also subdivide partitions further.

bucket_number=hash(column value) mod num_buckets\text{bucket\_number} = \text{hash}(\text{column value}) \bmod \text{num\_buckets}

The older deck writes the same rule as F(x) mod 3F(x) \bmod 3 for 3 buckets. For an INT column the hash is the value itself, so the bucket is Id mod 3. Every row with the same key always lands in the same bucket, no matter how many rows exist or which insert wrote them.

Three properties follow. The mapping is deterministic: the same key always picks the same file. The count is fixed: it is set once at CREATE TABLE time and never grows with the data, unlike partitions. It is made for high cardinality: one partition per value would create far too many directories. A fixed number of bucket files stays efficient instead.

Creating a bucketed tableSearch on YouTube

CREATE TABLE sales.orders_bucketed (
  order_id INT, customer_id INT,
  amount DOUBLE, order_date STRING
)
CLUSTERED BY (customer_id) INTO 4 BUCKETS
STORED AS ORC;

CLUSTERED BY (customer_id) INTO 4 BUCKETS names the column and the fixed count. Add SORTED BY (col) to also sort rows inside each bucket file, which helps later queries and joins that like pre-sorted data. Then fill it:

INSERT OVERWRITE TABLE sales.orders_bucketed
SELECT * FROM sales.orders;

The older deck builds the same thing in seven steps on an employee table. It selects a database, creates a dummy table emp_demo, loads the file emp_details, and checks the rows. Then it enables bucketing, creates the bucketed table and copies the data over:

SET hive.exec.dynamic.partition = true;

is replaced here by the bucketing switch:

SET hive.enforce.bucketing = true;
CREATE TABLE emp_bucket (Id INT, Name STRING, Salary FLOAT)
CLUSTERED BY (Id) INTO 3 BUCKETS
ROW FORMAT DELIMITED
FIELDS TERMINATED BY ',';
INSERT OVERWRITE TABLE emp_bucket SELECT * FROM emp_demo;

The screenshot shows the physical result: exactly three files, one per bucket.

/user/hive/warehouse/showbucket.db/emp_bucket/
  000000_0   (bucket 0)
  000001_0   (bucket 1)
  000002_0   (bucket 2)

Faster joins with bucketsSearch on YouTube

Bucketing pays off when two tables share a join key. If both tables share the bucketed join column, Hive can join matching buckets directly. One bucket count must be a multiple of the other. Hive then knows which files can hold matching keys. No full shuffle crosses the network.

SET hive.optimize.bucketmapjoin = true;
SELECT o.order_id, c.name
FROM sales.orders_bucketed o
JOIN sales.customers_bucketed c
  ON o.customer_id = c.customer_id;

Worked example. One 8-bucket table joins one 4-bucket table on the same key.

4 divides 8, so the counts satisfy the multiple rule. Each of the 4 small-table buckets pairs with exactly 2 of the 8 large-table buckets (8/4=28 / 4 = 2). Hive reads those pairs directly. With unbucketed tables every row would travel the network during the shuffle.

Fast sampling with TABLESAMPLESearch on YouTube

TABLESAMPLE reads one (or a few) bucket files instead of scanning the whole table. That gives a fast, honest sample for exploratory queries or tests, without paying for a full scan:

SELECT * FROM sales.orders_bucketed
  TABLESAMPLE(BUCKET 1 OUT OF 4 ON customer_id);

Only rows whose hash(customer_id) mod 4 selects bucket 1 are returned. One file out of four means about one quarter of the rows. The clause needs the same column the table is bucketed on.

Worked example: eight employees into three bucketsSearch on YouTube

The staging table emp_demo holds eight rows:

IdNameSalary
1Gaurav30000
2Aryan20000
3Vishal40000
4John10000
5Henry25000
6William9000
7Lisa25000
8Ronit20000

The table is bucketed with CLUSTERED BY (Id) INTO 3 BUCKETS. Since Id is an INT, the hash is the value itself:

Divide each Id by 3 and keep the remainder.

6 mod 3=0,3 mod 3=07 mod 3=1,4 mod 3=1,1 mod 3=18 mod 3=2,5 mod 3=2,2 mod 3=2\begin{aligned} 6 \bmod 3 &= \mathbf{0}, \quad 3 \bmod 3 = \mathbf{0} \\ 7 \bmod 3 &= \mathbf{1}, \quad 4 \bmod 3 = \mathbf{1}, \quad 1 \bmod 3 = \mathbf{1} \\ 8 \bmod 3 &= \mathbf{2}, \quad 5 \bmod 3 = \mathbf{2}, \quad 2 \bmod 3 = \mathbf{2} \end{aligned}

Assign each row to its bucket file.

Bucket 0 holds Id 3 (Vishal, 40000) and Id 6 (William, 9000). Bucket 1 holds Id 1 (Gaurav, 30000), Id 4 (John, 10000) and Id 7 (Lisa, 25000). Bucket 2 holds Id 2 (Aryan, 20000), Id 5 (Henry, 25000) and Id 8 (Ronit, 20000).

Check against the warehouse screenshot.

HDFS shows exactly three files: 000000_0, 000001_0, 000002_0. The count never grows with the data. Reading bucket 1 later returns the same three rows every time, because the hash is deterministic.

bucket 0 Id 3, 6 3 mod 3 = 0 6 mod 3 = 0 Vishal William bucket 1 Id 1, 4, 7 1, 4, 7 mod 3 = 1 Gaurav John Lisa bucket 2 Id 2, 5, 8 2, 5, 8 mod 3 = 2 Aryan Henry Ronit
Hash routing. Id mod 3 fixes each employee to one of three files.

Partitioning vs bucketingSearch on YouTube

PartitioningBucketing
Storage unitA directoryA fixed-count file
Best column typeLow or moderate cardinalityHigh cardinality
Count decided byNumber of distinct values (grows)Fixed at CREATE TABLE (never grows)
Performance winSkips whole directories (pruning)Efficient joins and sampling
Risk if misusedToo many tiny partitionsMismatched counts silently lose the join benefit

Filter, Join, Neither: F-J-N

A column you Filter on often gets a Partition. A key you Join on often gets a Bucket. A column with Neither job stays flat.

Combining both on one tableSearch on YouTube

One table can be partitioned AND bucketed. Within every partition directory, data splits further into a fixed number of bucket files:

CREATE TABLE sales.orders_full (
  order_id INT, customer_id INT,
  amount DOUBLE
)
PARTITIONED BY (order_date STRING)
CLUSTERED BY (customer_id)
  INTO 4 BUCKETS
STORED AS ORC;

The layout has directories from partitioning, each holding four files from bucketing:

order_date=2026-08-10/  { 000000_0, 000001_0, 000002_0, 000003_0 }
order_date=2026-08-11/  { 000000_0, 000001_0, 000002_0, 000003_0 }

A query filtering by order_date prunes whole partitions. A join or sample on customer_id inside each partition gets the bucketing benefit too. Both wins apply on the same table, since each technique handles the column type it suits.

order_date=2026-08-10 order_date=2026-08-11 4 bucket files 4 bucket files by customer_id by customer_id PARTITIONED BY decides the directory, CLUSTERED BY decides the file
Combined layout. Partitioning picks the directory, bucketing picks the file inside it.

Joins in HiveQLSearch on YouTube

Inner joinsSearch on YouTube

An inner join returns the intersection of the two tables: only rows with a match on both sides. The slide joins a students table to a grades table on the grade point:

SELECT students.*, grades.*
FROM students JOIN grades
ON (students.grade = grades.grade);
students.namestudents.gradegrades.gradegrades.letter
Steve2.82.8B
Raman3.23.2B+
Mary3.93.9A

Each student matches exactly one grade row, so 3 input rows give 3 output rows.

The slide's larger example joins a 7-row CUSTOMER table to a 4-row ORDER table. CUSTOMER_ID values 2, 3 and 4 match. Customer 3 has two orders, so the join output has 4 rows. They are order 102 with kaushik (3000), order 100 with kaushik (1500), order 101 with Khilan (1560) and order 103 with Chaitali (2060). Customers 1, 5, 6 and 7 have no orders and drop out of an inner join.

Outer joinsSearch on YouTube

An outer join keeps non-matching rows too, filling the missing side with NULL. Three types exist. LEFT OUTER JOIN returns a row for every row in the first (left) table. RIGHT OUTER JOIN returns a row for every row in the second (right) table. FULL OUTER JOIN returns a row for every row from both tables.

Count each join on the slide tables (7 customers, 4 orders).

Inner join keeps only matches: 4 rows. CUSTOMER LEFT OUTER JOIN ORDER keeps all 7 customers. The 3 matched customers give 4 rows, since customer 3 has two orders. The 4 unmatched customers give 4 NULL-filled rows. Total: 8 rows. RIGHT OUTER JOIN keeps all 4 orders: 4 rows. FULL OUTER JOIN keeps the 4 matched rows plus the 4 unmatched customers: 8 rows.

Cross joinsSearch on YouTube

A cross join, also called a Cartesian product, pairs every row of one table with every row of the other table. No ON condition is used:

SELECT * FROM orders CROSS JOIN customers;
-- also written with plain JOIN and no ON clause
cross-join rows=(left rows)×(right rows)\text{cross-join rows} = (\text{left rows}) \times (\text{right rows})

The slide rule: 10 left rows and 13 right rows give 10×13=13010 \times 13 = \mathbf{130} rows. The slide tables give 7×4=287 \times 4 = \mathbf{28} rows.

left both right customers orders inner = both left = left + both right = both + right full = all three cross = every pair
Join coverage. Inner keeps the overlap, outer joins add one or both sides, cross pairs everything.

Pitfalls and best practicesSearch on YouTube

  1. Partitioning on high cardinality. Columns such as customer_id or exact timestamps create one directory per value. Use bucketing for these instead.
  2. Over-partitioning. Too many small partitions strain the Metastore and slow query planning. Hive enforces hive.exec.max.dynamic.partitions as a safety limit. Do not rely on hitting it as your only check.
  3. Mismatched bucket counts. A bucket map join needs matching (or multiple) bucket counts on both sides. Get it wrong and the join still runs. It silently skips the optimization, with no error.
  4. Skipping table statistics. Run ANALYZE TABLE ... COMPUTE STATISTICS after large loads. The optimizer bases its partition-pruning and join decisions on real data, not guesses.

Mismatched bucket counts and over-partitioning both still run, only slower, with no error. Check the counts and the partition plan before the query, not after.

High, Over, Mismatch, Stats: check the four HOMS

High-cardinality partition columns. Over-partitioning past the safety limit. Mismatched bucket counts across the join. Stats never computed after the load.

Quick self-testSearch on YouTube

1. A table is bucketed with CLUSTERED BY (Id) INTO 3 BUCKETS. Which bucket holds Id = 5?

Bucket 2, since 5 mod 3=25 \bmod 3 = 2. For INT columns the hash is the value itself, so divide by 3 and keep the remainder.

2. A table has one partition directory per day for 2026-08-10, 2026-08-11 and 2026-08-12. A query filters WHERE order_date = '2026-08-11'. How many directories does Hive open?

One. Pruning opens only the order_date=2026-08-11 directory. The other two are never touched.

3. A CROSS JOIN combines a 10-row table with a 13-row table. How many output rows result? How many rows do the slide tables (7 customers, 4 orders) give?

10×13=13010 \times 13 = 130 rows. The slide tables give 7×4=287 \times 4 = 28 rows. Every left row pairs with every right row.

4. The slide ORDER table has 4 rows (customers 3, 3, 2, 4) and CUSTOMER has 7 rows. How many rows does an INNER JOIN on customer id return?

4 rows. All 4 orders match a customer. The 4 customers with no orders drop out.

5. What is the difference between a managed table and an external table when you run DROP TABLE?

Dropping a managed table deletes the metadata and the data files. Dropping an external table deletes only the metadata. The files at LOCATION survive.

6. Differentiate static and dynamic partitioning in one line each.

Static: you name each partition value by hand in every load (PARTITION (course='java')). Dynamic: Hive reads the value per row from the last SELECT column and creates the directories itself.

7. Why is customer_id a poor partition column but a good bucket column?

It has high cardinality, so partitioning makes one directory per customer and strains the Metastore. Bucketing fixes the file count and routes each key by hash, which suits joins and sampling on that key.

8. State the two conditions for a bucket map join, plus the setting that enables it.

Both tables are bucketed on the same join column, and one bucket count is a multiple of the other. Enable it with SET hive.optimize.bucketmapjoin = true;.

9. What does TABLESAMPLE(BUCKET 1 OUT OF 4 ON customer_id) read, and why is it fast?

It reads only the one bucket file whose rows satisfy the hash condition, about one quarter of the rows. It skips the other three files entirely instead of scanning the table.

10. Name the four silent or costly pitfalls of partitioning and bucketing.

High-cardinality partition columns, over-partitioning, mismatched bucket counts and missing statistics. Only mismatched bucket counts fail silently: the join still runs, only slower, with no error. An insert that exceeds hive.exec.max.dynamic.partitions stops with an error.

  • A managed table is owned by Hive: DROP TABLE deletes metadata and data. An external table keeps only metadata plus a LOCATION pointer: DROP TABLE deletes metadata only.
  • Partitioning splits a table into directories by column value. The partition column is not stored in the files (virtual column).
  • Static partitioning names each value per load. Dynamic partitioning sets two SET lines and puts the partition column last in the SELECT. Missing values land in __HIVE_DEFAULT_PARTITION__.
  • Partition pruning opens only the directories named in the WHERE clause. Good partition columns have low-to-moderate cardinality (date, region, status).
  • Bucket rule: bucket_number=hash(column value) mod num_buckets\text{bucket\_number} = \text{hash}(\text{column value}) \bmod \text{num\_buckets}. For INT, the hash is the value: Id mod 3.
  • CLUSTERED BY (col) INTO n BUCKETS fixes the file count at CREATE TABLE time. The slide example routes 8 employees into 3 files (000000_0 and two siblings).
  • Bucket map joins need the same join column on both sides and a multiple bucket count, plus SET hive.optimize.bucketmapjoin = true;.
  • TABLESAMPLE(BUCKET k OUT OF n ON col) samples one file, about 1/n1/n of the rows.
  • Partitioning AND bucketing combine: PARTITIONED BY picks the directory, CLUSTERED BY picks the file inside it.
  • Joins: inner keeps matches (4 rows on the slide tables). Left, right and full joins add NULL-filled non-matches (8, 4 and 8 rows).
  • Cross joins give m×nm \times n: 130 for 10 by 13, and 28 for the slide tables.
  • Four pitfalls: high-cardinality partitions, over-partitioning, mismatched bucket counts, stale statistics. Only mismatched bucket counts fail silently.