YugabyteDB Tips
  • Home
  • About
  • Featured Videos

Expanding Pure SQL Table DDL Export in YugabyteDB: Indexes, Constraints, Triggers, and Partitions

YugabyteDB Tips > API > YSQL > Expanding Pure SQL Table DDL Export in YugabyteDB: Indexes, Constraints, Triggers, and Partitions

Expanding Pure SQL Table DDL Export in YugabyteDB: Indexes, Constraints, Triggers, and Partitions

Jim KnicelyMay 7, 2026

In a previous YugabyteDB Tip, we built a pure SQL function to export a table’s CREATE TABLE DDL directly from YugabyteDB system catalogs:

  • πŸ‘‰ Export YugabyteDB CREATE TABLE DDL Using Pure SQL (Including SPLIT AT and SPLIT INTO)

That function was useful because it included several YugabyteDB-specific details that are easy to miss when manually reconstructing table DDL, including:

  • ● Inline primary keys
    ● SPLIT AT VALUES
    ● SPLIT INTO ... TABLETS
    ● TABLESPACE
    ● COLOCATION
    ● COLOCATION_ID
    ● Partitioned table parent definitions
    ● Column defaults
    ● Identity columns

But the original function focused mostly on the base table definition.

It did not attempt to export everything around the table, such as secondary indexes, non-primary-key constraints, triggers, or child partitions.

So in this tip, we’ll expand the function.

The goal is not to replace ysql_dump.

The goal is to create a handy SQL-only helper that gives you a more complete table-focused DDL script from inside ysqlsh.

Key Insight

In YugabyteDB, the primary key is not just a logical constraint. It helps define how table data is distributed across tablets. That is why this function keeps the primary key inline inside the CREATE TABLE statement instead of adding it later with ALTER TABLE.

Why Not Just Use ysql_dump?

For complete schema export, backup, restore, or migration workflows, you should still use ysql_dump.

For example:

				
					ysql_dump --include-yb-metadata --schema-only --table=public.orders yugabyte
				
			

That is the right tool when you need a complete and reliable schema dump.

But sometimes you want something lighter.

For example, you may want to inspect one table from inside ysqlsh, quickly document a table definition, compare DDL across environments, review tablet split clauses, or generate a table-focused script for a demo.

That is where a pure SQL helper function can be useful.

It lets you run:

				
					SELECT public.export_table_plus('public.orders');
				
			

And get back a readable, mostly executable table-focused DDL script.

Why the Primary Key Must Stay Inline

In PostgreSQL, it is common for dump tools to create a table first and then add indexes and constraints later.

That pattern can be fine for PostgreSQL.

But YugabyteDB is different.

In YugabyteDB, the primary key affects the physical layout of the table. It influences how rows are distributed across tablets.

So this pattern is not ideal:

				
					CREATE TABLE orders (
  order_id bigint,
  customer_id bigint,
  order_date date
);

ALTER TABLE orders
ADD PRIMARY KEY (order_id);
				
			

In YugabyteDB, that can be expensive because the table may need to be rewritten and redistributed around the new primary key.

Instead, the primary key should be part of the original table definition:

				
					CREATE TABLE orders (
  order_id bigint,
  customer_id bigint,
  order_date date,
  PRIMARY KEY (order_id HASH)
);
				
			

That is why this function keeps the primary key inline.

Secondary indexes, triggers, and other supporting objects can be emitted later, but the primary key belongs in the CREATE TABLE.

What This Expanded Function Includes

This version expands the original helper function to include more table-related objects.

Object / Metadata Included? Notes
Columns Yes Includes data types, defaults, identity columns, and NOT NULL.
Primary key Yes Emitted inline inside CREATE TABLE.
Non-PK constraints Yes Includes unique, foreign key, check, and exclusion constraints.
Secondary indexes Yes Uses pg_get_indexdef(). Primary key index is excluded.
Triggers Yes User-defined triggers only. Internal triggers are excluded.
Child partitions Yes Direct child partitions are emitted.
Tablespaces Yes Included for the base table and child partitions.
YugabyteDB split metadata Yes Preserves SPLIT AT and SPLIT INTO behavior from the original function.

How This Compares to \d, \dt, and \d+

ysqlsh already gives you helpful table inspection commands

For example:

				
					\d public.orders
\d+ public.orders
\dt+ public.*
				
			

Those commands are great for humans.

They show table structure, indexes, constraints, triggers, ownership, access method, tablespace, and other metadata in a readable format.

But they do not generate a reusable CREATE TABLE script.

This function takes a different approach. It focuses on reconstructing executable table-focused DDL.

Tool Best For
\d Quick table structure inspection.
\d+ More detailed human-readable table metadata.
\dt+ Listing tables with extra metadata.
ysql_dump --include-yb-metadata Complete schema export, backup, restore, and migration workflows.
export_table_plus() SQL-only, table-focused DDL reconstruction from inside ysqlsh.

So no, this function does not include everything that \d+ shows.

It overlaps with \d+, but it is not equivalent to \d+.

For example, this function does not currently export grants, comments, row-level security policies, sequence definitions, trigger function bodies, publications, subscriptions, table sizes, or all recursive dependencies.

The Expanded Function

Here is the expanded function.

				
					CREATE OR REPLACE FUNCTION public.export_table_plus(
    p_rel regclass,
    include_if_not_exists boolean DEFAULT false,
    include_child_partitions boolean DEFAULT true,
    include_indexes boolean DEFAULT true,
    include_triggers boolean DEFAULT true
)
RETURNS text
LANGUAGE plpgsql
SET client_min_messages TO 'warning'
AS $function$
DECLARE
  v_schema             text;
  v_table              text;
  v_relkind            "char";
  v_persistence        "char";
  v_reloptions         text[];
  v_cols               text;
  v_constraints        text;
  v_pk_clause          text;
  v_part_clause        text;
  v_tablespace         text;
  v_stmt               text;
  v_split_clause       text;
  v_with_items         text[] := ARRAY[]::text[];

  v_num_tablets        int;
  v_num_hash_cols      int := 0;
  v_is_colocated       boolean;
  v_colocation_id      oid;

  v_prefix             text := 'CREATE TABLE';
  v_conindid           oid;
  v_indexdef           text;
  v_allow_split_into   boolean := false;

  v_child_partitions   text;
  v_indexes            text := '';
  v_triggers           text;

  v_idx_rec            record;
  v_idx_num_tablets    int;
  v_idx_num_hash_cols  int;
  v_idx_is_colocated   boolean;
  v_idx_split_clause   text;
  v_idx_ddl            text;
BEGIN
  SELECT n.nspname,
         c.relname,
         c.relkind,
         c.relpersistence,
         c.reloptions
  INTO v_schema, v_table, v_relkind, v_persistence, v_reloptions
  FROM pg_class c
  JOIN pg_namespace n
    ON n.oid = c.relnamespace
  WHERE c.oid = p_rel;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'relation % not found', p_rel;
  END IF;

  IF v_persistence = 't' THEN
    v_prefix := 'CREATE TEMP TABLE';
  ELSIF v_persistence = 'u' THEN
    v_prefix := 'CREATE UNLOGGED TABLE';
  END IF;

  IF include_if_not_exists THEN
    v_prefix := v_prefix || ' IF NOT EXISTS';
  END IF;

  /*
   * YugabyteDB table properties.
   *
   * These are used to help reconstruct SPLIT INTO, colocation metadata,
   * and other YugabyteDB-specific physical layout details.
   */
  BEGIN
    SELECT num_tablets,
           num_hash_key_columns,
           is_colocated,
           colocation_id
    INTO v_num_tablets,
         v_num_hash_cols,
         v_is_colocated,
         v_colocation_id
    FROM yb_table_properties(p_rel);
  EXCEPTION
    WHEN OTHERS THEN
      v_num_tablets   := NULL;
      v_num_hash_cols := 0;
      v_is_colocated  := NULL;
      v_colocation_id := NULL;
  END;

  /*
   * Column definitions.
   */
  SELECT string_agg(def, E',\n  ' ORDER BY attnum)
  INTO v_cols
  FROM (
    SELECT
      a.attnum,
      format(
        '%I %s%s%s%s',
        a.attname,
        format_type(a.atttypid, a.atttypmod),
        CASE
          WHEN ad.adbin IS NOT NULL
          THEN ' DEFAULT ' || pg_get_expr(ad.adbin, ad.adrelid)
          ELSE ''
        END,
        CASE WHEN a.attnotnull THEN ' NOT NULL' ELSE '' END,
        CASE
          WHEN a.attidentity = 'a' THEN ' GENERATED ALWAYS AS IDENTITY'
          WHEN a.attidentity = 'd' THEN ' GENERATED BY DEFAULT AS IDENTITY'
          ELSE ''
        END
      ) AS def
    FROM pg_attribute a
    LEFT JOIN pg_attrdef ad
      ON ad.adrelid = a.attrelid
     AND ad.adnum   = a.attnum
    WHERE a.attrelid = p_rel
      AND a.attnum > 0
      AND NOT a.attisdropped
  ) s;

  /*
   * Reconstruct the primary key inline.
   *
   * This is important for YugabyteDB because the primary key affects
   * how data is distributed across tablets.
   */
  v_pk_clause := NULL;

  SELECT conindid
  INTO v_conindid
  FROM pg_constraint
  WHERE conrelid = p_rel
    AND contype = 'p';

  IF v_conindid IS NOT NULL THEN
    SELECT pg_get_indexdef(v_conindid)
    INTO v_indexdef;

    v_pk_clause :=
      regexp_replace(
        v_indexdef,
        '^.*USING\s+\S+\s+(\(.*\))$',
        'PRIMARY KEY \1'
      );

    IF v_pk_clause = v_indexdef THEN
      v_pk_clause := NULL;
    END IF;
  END IF;

  /*
   * Non-primary-key constraints.
   *
   * The primary key is intentionally excluded because it is emitted inline.
   */
  SELECT string_agg(
           'CONSTRAINT ' || quote_ident(conname) || ' ' || pg_get_constraintdef(oid, true),
           E',\n  '
           ORDER BY contype, conname
         )
  INTO v_constraints
  FROM pg_constraint
  WHERE conrelid = p_rel
    AND contype IN ('u','f','c','x');

  IF v_pk_clause IS NOT NULL THEN
    v_cols := v_cols || E',\n  ' || v_pk_clause;
  END IF;

  IF v_constraints IS NOT NULL THEN
    v_cols := v_cols || E',\n  ' || v_constraints;
  END IF;

  /*
   * PARTITION BY clause for partitioned table parents.
   */
  IF v_relkind = 'p' THEN
    SELECT 'PARTITION BY ' || pg_get_partkeydef(p_rel)
    INTO v_part_clause;
  END IF;

  /*
   * WITH (...) options.
   */
  IF v_reloptions IS NOT NULL THEN
    v_with_items := v_with_items || v_reloptions;
  END IF;

  IF v_is_colocated IS TRUE THEN
    v_with_items := array_append(v_with_items, 'COLOCATION = true');
  END IF;

  IF v_colocation_id IS NOT NULL THEN
    v_with_items := array_append(v_with_items, 'COLOCATION_ID = ' || v_colocation_id);
  END IF;

  /*
   * Base CREATE TABLE statement.
   */
  v_stmt :=
      v_prefix || ' '
   || quote_ident(v_schema) || '.' || quote_ident(v_table)
   || ' (' || E'\n  '
   || v_cols
   || E'\n)';

  IF array_length(v_with_items, 1) IS NOT NULL THEN
    v_stmt := v_stmt || E'\nWITH (' || array_to_string(v_with_items, ', ') || ')';
  END IF;

  IF v_part_clause IS NOT NULL THEN
    v_stmt := v_stmt || E'\n' || v_part_clause;
  END IF;

  /*
   * TABLESPACE.
   */
  SELECT ts.spcname
  INTO v_tablespace
  FROM pg_class c
  JOIN pg_tablespace ts
    ON ts.oid = c.reltablespace
  WHERE c.oid = p_rel
    AND c.reltablespace <> 0;

  IF v_tablespace IS NOT NULL THEN
    v_stmt := v_stmt || E'\nTABLESPACE ' || quote_ident(v_tablespace);
  END IF;

  /*
   * SPLIT clause for the base table.
   *
   * Range splits are reconstructed using yb_get_range_split_clause().
   * Hash splits fall back to SPLIT INTO when appropriate.
   */
  v_split_clause := NULL;

  BEGIN
    SELECT yb_get_range_split_clause(p_rel)
    INTO v_split_clause;
  EXCEPTION
    WHEN OTHERS THEN
      v_split_clause := NULL;
  END;

  IF v_split_clause IS NOT NULL THEN
    v_split_clause := btrim(v_split_clause);

    IF v_split_clause = '' THEN
      v_split_clause := NULL;
    END IF;
  END IF;

  v_allow_split_into :=
       v_pk_clause IS NULL
    OR position(' HASH' IN coalesce(v_pk_clause, '')) > 0
    OR COALESCE(v_num_hash_cols, 0) > 0;

  IF v_split_clause IS NOT NULL THEN
    v_stmt := v_stmt || E'\n' || v_split_clause;
  ELSIF v_allow_split_into
     AND COALESCE(v_num_tablets, 0) > 1
     AND COALESCE(v_is_colocated, false) = false
  THEN
    v_stmt := v_stmt || E'\nSPLIT INTO ' || v_num_tablets || ' TABLETS';
  END IF;

  v_stmt := v_stmt || ';';

  /*
   * Child partitions.
   *
   * This emits CREATE TABLE ... PARTITION OF statements for direct child
   * partitions of the requested parent table.
   */
  IF include_child_partitions THEN
    SELECT string_agg(child_ddl, E'\n' ORDER BY child_name)
    INTO v_child_partitions
    FROM (
      SELECT
        child.relname AS child_name,
        'CREATE TABLE '
        || quote_ident(child_ns.nspname) || '.' || quote_ident(child.relname)
        || ' PARTITION OF '
        || quote_ident(v_schema) || '.' || quote_ident(v_table)
        || ' '
        || pg_get_expr(child.relpartbound, child.oid)
        || CASE
             WHEN child_ts.spcname IS NOT NULL
             THEN E'\nTABLESPACE ' || quote_ident(child_ts.spcname)
             ELSE ''
           END
        || ';' AS child_ddl
      FROM pg_inherits i
      JOIN pg_class child
        ON child.oid = i.inhrelid
      JOIN pg_namespace child_ns
        ON child_ns.oid = child.relnamespace
      LEFT JOIN pg_tablespace child_ts
        ON child_ts.oid = child.reltablespace
      WHERE i.inhparent = p_rel
    ) s;

    IF v_child_partitions IS NOT NULL THEN
      v_stmt := v_stmt
             || E'\n\n-- Child Partitions\n'
             || v_child_partitions;
    END IF;
  END IF;

  /*
   * Secondary indexes.
   *
   * Primary key indexes are intentionally excluded because the primary key
   * is emitted inline inside CREATE TABLE.
   *
   * pg_get_indexdef() does not include YugabyteDB split metadata, so this
   * block separately checks each index with yb_table_properties() and
   * appends SPLIT AT / SPLIT INTO metadata when available.
   */
  IF include_indexes THEN
    FOR v_idx_rec IN
      SELECT i.indexrelid,
             ic.relname AS index_name,
             pg_get_indexdef(i.indexrelid) AS indexdef
      FROM pg_index i
      JOIN pg_class ic
        ON ic.oid = i.indexrelid
      WHERE i.indrelid = p_rel
        AND i.indisprimary = false
      ORDER BY ic.relname
    LOOP
      v_idx_num_tablets   := NULL;
      v_idx_num_hash_cols := 0;
      v_idx_is_colocated  := NULL;
      v_idx_split_clause  := NULL;

      BEGIN
        SELECT num_tablets,
               num_hash_key_columns,
               is_colocated
        INTO v_idx_num_tablets,
             v_idx_num_hash_cols,
             v_idx_is_colocated
        FROM yb_table_properties(v_idx_rec.indexrelid);
      EXCEPTION
        WHEN OTHERS THEN
          v_idx_num_tablets   := NULL;
          v_idx_num_hash_cols := 0;
          v_idx_is_colocated  := NULL;
      END;

      BEGIN
        SELECT yb_get_range_split_clause(v_idx_rec.indexrelid)
        INTO v_idx_split_clause;
      EXCEPTION
        WHEN OTHERS THEN
          v_idx_split_clause := NULL;
      END;

      IF v_idx_split_clause IS NOT NULL THEN
        v_idx_split_clause := btrim(v_idx_split_clause);

        IF v_idx_split_clause = '' THEN
          v_idx_split_clause := NULL;
        END IF;
      END IF;

      v_idx_ddl := v_idx_rec.indexdef;

      IF v_idx_split_clause IS NOT NULL THEN
        v_idx_ddl := v_idx_ddl || E'\n' || v_idx_split_clause;
      ELSIF COALESCE(v_idx_is_colocated, false) = false
         AND COALESCE(v_idx_num_tablets, 0) > 1
         AND (
              COALESCE(v_idx_num_hash_cols, 0) > 0
              OR position(' HASH' IN v_idx_rec.indexdef) > 0
             )
      THEN
        v_idx_ddl := v_idx_ddl || E'\nSPLIT INTO ' || v_idx_num_tablets || ' TABLETS';
      END IF;

      v_idx_ddl := v_idx_ddl || ';';

      IF v_indexes = '' THEN
        v_indexes := v_idx_ddl;
      ELSE
        v_indexes := v_indexes || E'\n' || v_idx_ddl;
      END IF;
    END LOOP;

    IF v_indexes <> '' THEN
      v_stmt := v_stmt
             || E'\n\n-- Indexes\n'
             || v_indexes;
    END IF;
  END IF;

  /*
   * User-defined triggers.
   *
   * Internal/system triggers are excluded.
   */
  IF include_triggers THEN
    SELECT string_agg(pg_get_triggerdef(t.oid) || ';', E'\n' ORDER BY t.tgname)
    INTO v_triggers
    FROM pg_trigger t
    WHERE t.tgrelid = p_rel
      AND t.tgisinternal = false;

    IF v_triggers IS NOT NULL THEN
      v_stmt := v_stmt
             || E'\n\n-- Triggers\n'
             || v_triggers;
    END IF;
  END IF;

  RETURN v_stmt;
END;
$function$;
				
			

Demo: Comparing \d with export_table_plus()

To demonstrate the function, let’s use a table named public.orders.

This table includes several objects that are useful for testing DDL reconstruction:

  • ● An inline primary key
    ● A default value
    ● Check constraints
    ● A foreign key constraint
    ● Secondary indexes
    ● A trigger
    ● Different tablet split counts for the table and indexes

First, let’s inspect the table using the standard ysqlsh \d command:

				
					yugabyte=# \d public.orders
				
			

Output:

				
					.                            Table "public.orders"
    Column    |           Type           | Collation | Nullable |   Default
--------------+--------------------------+-----------+----------+-------------
 order_id     | bigint                   |           | not null |
 customer_id  | bigint                   |           | not null |
 order_status | text                     |           | not null | 'NEW'::text
 order_total  | numeric(12,2)            |           | not null |
 order_region | text                     |           | not null |
 created_at   | timestamp with time zone |           |          | now()
 updated_at   | timestamp with time zone |           |          | now()
Indexes:
    "orders_pkey" PRIMARY KEY, lsm (order_id HASH)
    "orders_customer_id_idx" lsm (customer_id HASH, created_at DESC)
    "orders_region_status_idx" lsm (order_region HASH, order_status ASC)
Check constraints:
    "orders_status_check" CHECK (order_status = ANY (ARRAY['NEW'::text, 'PAID'::text, 'SHIPPED'::text, 'CANCELLED'::text]))
    "orders_total_check" CHECK (order_total >= 0::numeric)
Foreign-key constraints:
    "orders_customer_fk" FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
Triggers:
    orders_audit_trigger BEFORE UPDATE ON orders FOR EACH ROW EXECUTE FUNCTION orders_audit_trigger_fn()
				
			

This output is great for interactive inspection.

It shows the columns, indexes, constraints, foreign key, and trigger in a readable format.

But it does not give us a reusable CREATE TABLE script. It also does not show the YugabyteDB tablet split details for the table or indexes.

That is where export_table_plus() helps.

Before running the function, I like to switch ysqlsh into unaligned, tuples-only output mode. This makes the generated DDL easier to copy and paste.

				
					yugabyte=# \a
Output format is unaligned.

yugabyte=# \t
Tuples only is on.
				
			

Now run the export function.

				
					yugabyte=# SELECT public.export_table_plus('public.orders');
				
			

Output:

				
					CREATE TABLE public.orders (
  order_id bigint NOT NULL,
  customer_id bigint NOT NULL,
  order_status text DEFAULT 'NEW'::text NOT NULL,
  order_total numeric(12,2) NOT NULL,
  order_region text NOT NULL,
  created_at timestamp with time zone DEFAULT now(),
  updated_at timestamp with time zone DEFAULT now(),
  PRIMARY KEY (order_id HASH),
  CONSTRAINT orders_status_check CHECK (order_status = ANY (ARRAY['NEW'::text, 'PAID'::text, 'SHIPPED'::text, 'CANCELLED'::text])),
  CONSTRAINT orders_total_check CHECK (order_total >= 0::numeric),
  CONSTRAINT orders_customer_fk FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
)
SPLIT INTO 8 TABLETS;

-- Indexes
CREATE INDEX orders_customer_id_idx ON public.orders USING lsm (customer_id HASH, created_at DESC)
SPLIT INTO 6 TABLETS;
CREATE INDEX orders_region_status_idx ON public.orders USING lsm (order_region HASH, order_status ASC)
SPLIT INTO 3 TABLETS;

-- Triggers
CREATE TRIGGER orders_audit_trigger BEFORE UPDATE ON public.orders FOR EACH ROW EXECUTE FUNCTION orders_audit_trigger_fn();
				
			

Now we have something much closer to executable DDL.

The function reconstructs the base table definition, keeps the primary key inline, includes the check and foreign key constraints, and appends the secondary indexes and trigger.

The important YugabyteDB-specific detail is that the generated DDL also includes tablet split metadata.

In this example:

  • ● The base table is split into 8 tablets
    ● orders_customer_id_idx is split into 6 tablets
    ● orders_region_status_idx is split into 3 tablets

That matters because YugabyteDB secondary indexes are distributed objects too. They can have their own physical tablet layout, separate from the base table.

Index Split Detail

Notice that the base table is split into 8 tablets, while the secondary indexes use their own split counts. In YugabyteDB, indexes are distributed objects too, so their physical split metadata must be exported separately from the table definition.

There is one more important caveat.

The function exports the trigger definition, but it does not export the trigger function body.

In the output above, the trigger references this function:

				
					orders_audit_trigger_fn()
				
			

That function must already exist before the trigger can be recreated successfully.

Demo Note

The export function includes the trigger definition, but it does not export the trigger function body. If you are recreating this table elsewhere, make sure the referenced trigger function exists before creating the trigger.

This is why export_table_plus() is useful for quick inspection, documentation, and table-focused DDL reconstruction, but ysql_dump --include-yb-metadata is still the better choice for complete schema export.

Important Caveats

This function is useful, but it is not a complete schema export tool.

It is best thought of as a table-focused DDL helper for quick inspection, troubleshooting, demos, and documentation.

It does not currently export:

  • ● Grants
    ● Comments
    ● Row-level security policies
    ● Sequence definitions
    ● Sequence ownership
    ● Trigger function bodies
    ● Views
    ● Materialized views
    ● Foreign tables
    ● Rules
    ● Publications
    ● Subscriptions
    ● Table size metadata
    ● Full recursive partition hierarchies
    ● Every dependent object outside the requested table

For example, the demo output includes this trigger:

				
					CREATE TRIGGER orders_audit_trigger
BEFORE UPDATE ON public.orders
FOR EACH ROW
EXECUTE FUNCTION orders_audit_trigger_fn();
				
			

That is helpful, but it does not include the definition of orders_audit_trigger_fn().

So if you were recreating the table elsewhere, the trigger function would need to exist before the trigger could be created successfully.

The same idea applies to other external dependencies. If a column default references a sequence, the default expression may appear in the exported table DDL, but the sequence definition and ownership are not exported by this function.

Don’t Forget

Use this function for quick table-level DDL reconstruction. For full schema export, backup, restore, or migration workflows, continue to use ysql_dump --include-yb-metadata.

Why This Matters

YugabyteDB is PostgreSQL-compatible, but table layout has distributed systems implications.

A CREATE TABLE statement in YugabyteDB does more than define columns.

It can also define how data is distributed, how many tablets are created, whether the table is colocated, and where the table should be placed when tablespaces are used.

The primary key is especially important because it affects the table’s physical distribution. That is why this function keeps the primary key inline inside the CREATE TABLE statement instead of adding it later with ALTER TABLE.

Indexes matter too.

In YugabyteDB, secondary indexes are distributed objects. They can have their own tablet layout, separate from the base table.

That is why the demo output showed:

				
					CREATE TABLE public.orders (...)
SPLIT INTO 8 TABLETS;
				
			

But the indexes had their own split counts:

				
					CREATE INDEX orders_customer_id_idx ...
SPLIT INTO 6 TABLETS;

CREATE INDEX orders_region_status_idx ...
SPLIT INTO 3 TABLETS;
				
			

A table inspection command like \d public.orders is great for seeing the logical structure of the table. But it does not show every YugabyteDB physical layout detail needed to recreate the table and index definitions.

That is the gap this function helps fill.

Key Insight

In YugabyteDB, the base table and its secondary indexes can have different physical tablet layouts. A useful DDL export helper needs to capture split metadata for both.

Final Takeaway

export_table_plus() is not a replacement for \d, \d+, or ysql_dump.

Those tools still have their place.

Use \d and \d+ when you want quick, human-readable inspection.

Use ysql_dump --include-yb-metadata when you need a complete and reliable schema export.

Use export_table_plus() when you want a lightweight, SQL-only way to reconstruct table-focused DDL directly from ysqlsh.

The most important YugabyteDB-specific detail is that the primary key stays inline, and the split metadata is captured for both the base table and its secondary indexes.

Final Takeaway

A pure SQL export helper is perfect for quick table-level DDL reconstruction, but it should remain YugabyteDB-aware. Keep the primary key inline, export table split metadata, and remember that secondary indexes can have their own split configuration too.

Have Fun!

Every spring, I fall in love with this tree all over again. Seeing all that bright new growth on my favorite pine in the backyard is one of those little signs that the season has officially arrived. πŸŒ²πŸ’š
DDL, Meta-Data, Stored Procedures, YSQL

Post navigation

Override the Prometheus URL Used by the YBA UI
Exact Per-Tablet Row Counts in YugabyteDB – Hash and Range Sharding in One SQL Function



YugabyteDB Tips

Copyright 2025 - Knicely/Li