In a previous tip, we explored how yb_tablet_metadata makes it easy to see tablet leadership and replica placement across a YugabyteDB cluster.
That works beautifully for hash-sharded tables.
But what about range-sharded tables created with SPLIT AT VALUES?
When you presplit a range table, each split point produces a new tablet. However:
●
yb_tablet_metadatadoes not show human-readable key bounds.●
yb_local_tabletsstores the real range boundaries… but in DocDB-encoded form.
In this tip, we’ll:
- 1. Create an anonymized range-sharded table
- 2. Presplit it at known UUID boundaries
- 3. Decode the partition key boundaries
- 4. Show:
- ● first / middle / last tablet
- ● which split window it represents
- ● which split point produced it
- 5. Create a reusable helper function that works for any range table
The result is a clean “tablet provenance” view that explains exactly how your splits map to tablets.
Hash vs Range Presplitting Refresher
Hash sharding
CREATE TABLE t (
id INT,
PRIMARY KEY(id HASH)
) SPLIT INTO 8 TABLETS;
Hash space is divided evenly. Tablet metadata shows hash ranges.
Range sharding
CREATE TABLE t (
id INT PRIMARY KEY ASC
) SPLIT AT VALUES ((100), (200), (300));
Here, tablets are divided by actual key boundaries… not hash ranges.
That’s where things get interesting.
Step 1 – Create an Anonymized Range-Sharded Table
This mimics a real customer workload but uses neutral names.
Primary key:
PRIMARY KEY (acct_uuid ASC, event_uuid ASC)
Because the leading column is ASC (not HASH), this is a range-sharded table.
We split on deterministic UUID boundaries:
● acct_id = 250
● acct_id = 500
● acct_id = 750
DROP SCHEMA IF EXISTS demo CASCADE;
CREATE SCHEMA demo;
CREATE TABLE demo.events_r (
event_uuid uuid NOT NULL,
acct_uuid uuid NOT NULL,
ext_id bigint NULL,
occurred_at timestamp NOT NULL DEFAULT now(),
created_at timestamp NOT NULL DEFAULT now(),
updated_at timestamp NOT NULL DEFAULT now(),
is_archived boolean NOT NULL DEFAULT false,
is_active boolean NOT NULL DEFAULT true,
note text,
CONSTRAINT events_r_pkey
PRIMARY KEY (acct_uuid ASC, event_uuid ASC)
)
SPLIT AT VALUES
(
(('00000000-0000-0000-0000-' || lpad(to_hex(250), 12, '0'))::uuid),
(('00000000-0000-0000-0000-' || lpad(to_hex(500), 12, '0'))::uuid),
(('00000000-0000-0000-0000-' || lpad(to_hex(750), 12, '0'))::uuid)
);
This produces 4 tablets:
1.
(-infinity, 250)2.
[250, 500)3.
[500, 750)4.
[750, +infinity)
Step 2 – Why We Need a Decoder
If we query yb_local_tablets, we see something like:
\x53000100010001000100010001000100010001000100010001000100010001fa00000021
This is not raw UUID bytes.
It is:
●
0x53→ UUID type tag● UUID bytes encoded in a memcomparable format
●
00 01→ escaped zero byte●
00 00→ terminator●
...00000021→ DocDB suffix
So we must unescape the UUID bytes before formatting them.
For a deep dive on decoding other key types (int, timestamp, text, composite keys), see:
Step 3 – UUID Partition Key Decoder
This function handles:
● zero escaping (
00 01)● termination (
00 00)● formatting 16 bytes into UUID
CREATE OR REPLACE FUNCTION yb_decode_uuid_from_pk(pk BYTEA, start_at INT DEFAULT 0)
RETURNS UUID
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
i INT;
b INT;
b2 INT;
hex_out TEXT := '';
BEGIN
IF pk IS NULL OR pk = ''::bytea THEN
RETURN NULL;
END IF;
i := start_at + 1;
WHILE i < length(pk) LOOP
b := get_byte(pk, i);
IF b <> 0 THEN
hex_out := hex_out || lpad(to_hex(b), 2, '0');
i := i + 1;
ELSE
IF i + 1 >= length(pk) THEN
EXIT;
END IF;
b2 := get_byte(pk, i + 1);
IF b2 = 0 THEN
EXIT;
ELSIF b2 = 1 THEN
hex_out := hex_out || '00';
i := i + 2;
ELSE
RETURN NULL;
END IF;
END IF;
IF length(hex_out) >= 32 THEN
EXIT;
END IF;
END LOOP;
IF length(hex_out) < 32 THEN
RETURN NULL;
END IF;
hex_out := left(hex_out, 32);
RETURN (
substr(hex_out, 1, 8) || '-' ||
substr(hex_out, 9, 4) || '-' ||
substr(hex_out, 13, 4) || '-' ||
substr(hex_out, 17, 4) || '-' ||
substr(hex_out, 21, 12)
)::uuid;
END;
$$;
Step 4 – Build the Tablet Provenance View
This query:
● tablet placement
● first / middle / last
● split window
● decoded UUID bounds
● which split point produced the tablet
WITH local_ranges AS (
SELECT DISTINCT ON (tablet_id)
tablet_id,
partition_key_start,
partition_key_end
FROM yb_local_tablets
WHERE namespace_name = current_database()
AND table_name = 'events_r'
ORDER BY tablet_id
),
ordered AS (
SELECT
lr.*,
row_number() OVER (ORDER BY partition_key_start NULLS FIRST) AS rn,
count(*) OVER () AS n
FROM local_ranges lr
),
split_points AS (
SELECT *
FROM (VALUES
(1, (('00000000-0000-0000-0000-' || lpad(to_hex(250), 12, '0'))::uuid)),
(2, (('00000000-0000-0000-0000-' || lpad(to_hex(500), 12, '0'))::uuid)),
(3, (('00000000-0000-0000-0000-' || lpad(to_hex(750), 12, '0'))::uuid))
) v(split_no, split_acct_uuid)
),
decoded AS (
SELECT
o.*,
yb_decode_uuid_from_pk(o.partition_key_start, 0) AS start_uuid,
yb_decode_uuid_from_pk(o.partition_key_end, 0) AS end_uuid
FROM ordered o
)
SELECT
ytm.tablet_id,
ytm.leader,
CASE
WHEN d.rn = 1 THEN 'first tablet'
WHEN d.rn = d.n THEN 'last tablet'
ELSE 'middle tablet'
END AS tablet_position,
CASE
WHEN d.rn = 1 THEN 'before split #1'
WHEN d.rn = d.n THEN format('after split #%s', d.n - 1)
ELSE format('between split #%s and split #%s', d.rn - 1, d.rn)
END AS split_window,
format('[%s, %s)',
COALESCE(d.start_uuid::text, '(-infinity)'),
COALESCE(d.end_uuid::text, '(+infinity)')
) AS acct_uuid_range,
CASE
WHEN d.rn = 1 THEN 'implicit (-infinity)'
ELSE format('split #%s (%s)', sp.split_no, sp.split_acct_uuid)
END AS produced_by_split_point
FROM yb_tablet_metadata ytm
JOIN decoded d USING (tablet_id)
LEFT JOIN split_points sp
ON sp.split_acct_uuid = d.start_uuid
WHERE ytm.db_name = current_database()
AND ytm.relname = 'events_r'
ORDER BY d.rn;
Example output:
. tablet_id | leader | tablet_position | split_window | acct_uuid_range | produced_by_split_point
----------------------------------+----------------+-----------------+-------------------------------+------------------------------------------------------------------------------+-------------------------------------------------
72fd6f70a8c34c8391f94602e53a09e8 | 127.0.0.1:5433 | first tablet | before split #1 | [(-infinity), 00000000-0000-0000-0000-0000000000fa) | implicit (-infinity)
3ab5f6157cf542fd88bce3150a81c7ed | 127.0.0.1:5433 | middle tablet | between split #1 and split #2 | [00000000-0000-0000-0000-0000000000fa, 00000000-0000-0000-0000-0000000001f4) | split #1 (00000000-0000-0000-0000-0000000000fa)
1bf7a1d8c36c4181bcb3b28eeac56f21 | 127.0.0.1:5433 | middle tablet | between split #2 and split #3 | [00000000-0000-0000-0000-0000000001f4, 00000000-0000-0000-0000-0000000002ee) | split #2 (00000000-0000-0000-0000-0000000001f4)
3537d8b94ff5475c96d5187e6fbc00cb | 127.0.0.1:5433 | last tablet | after split #3 | [00000000-0000-0000-0000-0000000002ee, (+infinity)) | split #3 (00000000-0000-0000-0000-0000000002ee)
(4 rows)
Step 5 – Reusable Helper for Any Range Table
Views cannot take parameters in YSQL, so we create a reusable function.
CREATE OR REPLACE FUNCTION yb_range_tablet_map(p_relname TEXT)
RETURNS TABLE (
tablet_id TEXT,
leader TEXT,
replicas TEXT[],
tablet_position TEXT,
split_window TEXT,
range_start_hex TEXT,
range_end_hex TEXT,
partition_key_start BYTEA,
partition_key_end BYTEA
)
LANGUAGE sql
STABLE
AS $$
WITH local_ranges AS (
SELECT DISTINCT ON (tablet_id)
tablet_id,
partition_key_start,
partition_key_end
FROM yb_local_tablets
WHERE namespace_name = current_database()
AND table_name = p_relname
ORDER BY tablet_id
),
ordered AS (
SELECT
lr.*,
row_number() OVER (ORDER BY partition_key_start NULLS FIRST) AS rn,
count(*) OVER () AS n
FROM local_ranges lr
)
SELECT
ytm.tablet_id,
ytm.leader,
ytm.replicas,
CASE
WHEN o.rn = 1 THEN 'first tablet'
WHEN o.rn = o.n THEN 'last tablet'
ELSE 'middle tablet'
END AS tablet_position,
CASE
WHEN o.rn = 1 THEN 'before split #1'
WHEN o.rn = o.n THEN format('after split #%s', o.n - 1)
ELSE format('between split #%s and split #%s', o.rn - 1, o.rn)
END AS split_window,
COALESCE(encode(o.partition_key_start, 'hex'), '(empty)') AS range_start_hex,
COALESCE(encode(o.partition_key_end, 'hex'), '(empty)') AS range_end_hex,
o.partition_key_start,
o.partition_key_end
FROM yb_tablet_metadata ytm
JOIN ordered o USING (tablet_id)
WHERE ytm.db_name = current_database()
AND ytm.relname = p_relname
ORDER BY o.rn;
$$;
Use it like this:
SELECT
tablet_index,
tablet_id,
leader,
tablet_position,
split_window,
format('[%s, %s)',
COALESCE(yb_decode_uuid_from_pk(partition_key_start, 0)::text, '(-infinity)'),
COALESCE(yb_decode_uuid_from_pk(partition_key_end, 0)::text, '(+infinity)')
) AS acct_uuid_range
FROM yb_range_tablet_map('demo', 'events_r')
ORDER BY tablet_index;
Example output:
.tablet_index | tablet_id | leader | tablet_position | split_window | acct_uuid_range
--------------+----------------------------------+------------------+-----------------+-------------------------------+------------------------------------------------------------------------------
1 | 3e2d9d967d684ef3b58192730f6d6aa1 | 10.8.26.130:5433 | first tablet | before split #1 | [(-infinity), 00000000-0000-0000-0000-0000000000fa)
2 | 6467b4435cbf4c74971da35b7e454749 | 10.8.28.63:5433 | middle tablet | between split #1 and split #2 | [00000000-0000-0000-0000-0000000000fa, 00000000-0000-0000-0000-0000000001f4)
3 | 0754aee2b0b44b90a128d8008e7590ec | 10.8.24.179:5433 | middle tablet | between split #2 and split #3 | [00000000-0000-0000-0000-0000000001f4, 00000000-0000-0000-0000-0000000002ee)
4 | f3a2ce5506524585b785ea97e24720b0 | 10.8.24.179:5433 | last tablet | after split #3 | [00000000-0000-0000-0000-0000000002ee, (+infinity))
(4 rows)
To decode bounds for a specific table, layer a decoder on top.
Final Thoughts
For hash-sharded tables, yb_tablet_metadata is often enough.
For range-sharded tables, the real key boundaries live in yb_local_tablets.
When you combine:
●
yb_tablet_metadata(placement)●
yb_local_tablets(boundaries)● decoding logic (from the linked tip)
…you gain full visibility into:
● which split point created each tablet
● how key space is partitioned
● how tablet placement maps to your split strategy
This is incredibly useful when validating presplit strategies before production rollout, especially in multi-tenant or hot-range scenarios.
Have Fun!
