YugabyteDB Tips
  • Home
  • About
  • Featured Videos

Exploring Range Tablet Placement (and Which Split Point Created Each Tablet)

YugabyteDB Tips > Sharding > Range Sharding > Exploring Range Tablet Placement (and Which Split Point Created Each Tablet)

Exploring Range Tablet Placement (and Which Split Point Created Each Tablet)

Jim KnicelyFebruary 24, 2026

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_metadata does not show human-readable key bounds.

  • ● yb_local_tablets stores 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:

  • Decode yb_local_tablets() Split Ranges into Human-Readable Bounds
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_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 split_window;
				
			

Example output:

				
					.           tablet_id             |     leader     | tablet_position |         split_window          |                               acct_uuid_range
----------------------------------+----------------+-----------------+-------------------------------+------------------------------------------------------------------------------
 3537d8b94ff5475c96d5187e6fbc00cb | 127.0.0.1:5433 | last tablet     | after split #3                | [00000000-0000-0000-0000-0000000002ee, (+infinity))
 72fd6f70a8c34c8391f94602e53a09e8 | 127.0.0.1:5433 | first tablet    | before split #1               | [(-infinity), 00000000-0000-0000-0000-0000000000fa)
 3ab5f6157cf542fd88bce3150a81c7ed | 127.0.0.1:5433 | middle tablet   | between split #1 and split #2 | [00000000-0000-0000-0000-0000000000fa, 00000000-0000-0000-0000-0000000001f4)
 1bf7a1d8c36c4181bcb3b28eeac56f21 | 127.0.0.1:5433 | middle tablet   | between split #2 and split #3 | [00000000-0000-0000-0000-0000000001f4, 00000000-0000-0000-0000-0000000002ee)
(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!

Spotted: ✈️ A welcome pit stop at PIT ... Mineo's Pizza House is now at the airport! Proof that even when you’re flying out of Pittsburgh, you can still grab some of the best ‘za in the Burg 🍕😋 (and yes, that’s a mile-high recommendation).
Meta-Data, Range Sharding, Sharding, Tablets

Post navigation

Does DDL Block DML? (It Depends on Table-Level Locks)
Demonstrating Hot Shard Detection (and Why a Tablet Can Refuse to Split)



YugabyteDB Tips

Copyright 2025 - Knicely/Li