Audit the data warehouse

Audit the data warehouse

출처: HashiCorp Boundary docs Boundary's PostgreSQL database includes a built-in data warehouse (wh_* tables) that captures the complete lifecycle of sessions, connections, authentication tokens, users, hosts, and credentials. This dimensional model is populated automatically via database triggers as Boundary events occur, enabling real-time analytics without external ETL pipelines. Refer to Boundary data warehouse for more information about the data warehouse's architecture and tables.

Compliance use cases

The following queries are verified against Boundary v0.21.0 and address common compliance requirements including SOC 2, SOX, PCI-DSS, and HIPAA access audit mandates.

C-001: Complete access audit trail

This query generates a comprehensive audit trail showing who accessed what system, when, from where, with what credentials, and how much data moved. It satisfies the core requirement of every major compliance framework.

SELECT

    s.session_id,

    s.session_pending_time 
AS
 access_time,

    s.session_terminated_time 
AS
 end_time,

    CASE

        WHEN
 s.session_terminated_time 
=
 'infinity'
 THEN
 NULL

        ELSE
 ROUND
(EXTRACT(EPOCH 
FROM
 (s.session_terminated_time 
-
 s.session_pending_time))::
numeric
, 
1
)

    END
 AS
 duration_seconds,

    u.user_name,

    u.auth_account_email,

    u.auth_account_full_name,

    u.auth_method_type,

    u.user_organization_name,

    h.target_name,

    h.target_type,

    h.target_description,

    h.project_name,

    h.organization_name,

    h.host_name,

    h.host_type,

    c.client_tcp_address,

    c.endpoint_tcp_address,

    c.endpoint_tcp_port_number,

    COALESCE
(c.bytes_up, 
0
) 
/
 1024
.
0
 AS
 kb_uploaded,

    COALESCE
(c.bytes_down, 
0
) 
/
 1024
.
0
 AS
 kb_downloaded,

    s.total_connection_count

FROM
 wh_session_accumulating_fact s

JOIN
 wh_user_dimension u 
ON
 s.user_key 
=
 u.key 
AND
 u.current_row_indicator 
=
 'Current'

JOIN
 wh_host_dimension h 
ON
 s.host_key 
=
 h.key 
AND
 h.current_row_indicator 
=
 'Current'

LEFT JOIN
 wh_session_connection_accumulating_fact c 
ON
 s.session_id 
=
 c.session_id

WHERE
 s.session_pending_time 
BETWEEN
 :
start_date
 AND
 :end_date

ORDER BY
 s.session_pending_time 
DESC
;

Compliance frameworks addressed: SOC 2 (CC6.1, CC6.7), SOX (Section 404), PCI-DSS (Requirement 10), HIPAA (164.312(b)). Columns returned for each access event:

  • Who - user_name, auth_account_email, auth_account_full_name
  • How authenticated - auth_method_type
  • What system - target_name, target_type, host_name
  • When - access_time, end_time, duration_seconds
  • From where - client_tcp_address (source IP), endpoint_tcp_address (destination)
  • Data transferred - kb_uploaded, kb_downloaded
  • Organizational context - organization_name, project_name

C-002: User access review (inactive and orphaned account detection)

This query identifies users who have never accessed a target or whose last access exceeds a configurable threshold. It is a cornerstone of quarterly access certification reviews.

SELECT

    u.user_name,

    u.auth_account_email,

    u.auth_method_type,

    u.user_organization_name,

    COUNT
(
DISTINCT
 s.session_id) 
AS
 total_sessions,

    COUNT
(
DISTINCT
 h.target_id) 
AS
 unique_targets_accessed,

    MAX
(s.session_pending_time) 
AS
 last_activity,

    CASE

        WHEN
 MAX
(s.session_pending_time) 
IS
 NULL
 THEN
 'NEVER USED'

        WHEN
 MAX
(s.session_pending_time) 
<
 CURRENT_DATE 
-
 INTERVAL 
'90 days'
 THEN
 'INACTIVE >90d'

        WHEN
 MAX
(s.session_pending_time) 
<
 CURRENT_DATE 
-
 INTERVAL 
'30 days'
 THEN
 'INACTIVE >30d'

        ELSE
 'ACTIVE'

    END
 AS
 activity_status,

    array_agg(
DISTINCT
 h.target_name 
ORDER BY
 h.target_name) 
FILTER
 (
WHERE
 h.target_name 
IS NOT NULL
) 
AS
 targets_accessed

FROM
 wh_user_dimension u

LEFT JOIN
 wh_session_accumulating_fact s 
ON
 u.key 
=
 s.user_key

LEFT JOIN
 wh_host_dimension h 
ON
 s.host_key 
=
 h.key 
AND
 h.current_row_indicator 
=
 'Current'

WHERE
 u.current_row_indicator 
=
 'Current'

GROUP BY
 u.user_name, u.auth_account_email, u.auth_method_type, u.user_organization_name

ORDER BY
 last_activity 
DESC
 NULLS
 LAST
;

Review actions:

  • NEVER USED - Candidate for account deprovisioning
  • INACTIVE >90d - Flag for manager confirmation before next review cycle
  • INACTIVE >30d - Monitor; include in next review

C-003: Privileged access report

This query surfaces all access to targets or credentials with administrative or production markings, supporting least-privilege audits and privileged access reviews.

SELECT

    s.session_id,

    s.session_pending_time,

    u.user_name,

    u.auth_method_type,

    u.user_organization_name,

    h.target_name,

    h.target_type,

    h.project_name,

    CASE

        WHEN
 h.target_name ILIKE 
'%admin%'
 OR
 h.target_name ILIKE 
'%root%'
 THEN
 'ADMINISTRATIVE'

        WHEN
 h.target_name ILIKE 
'%prod%'
 THEN
 'PRODUCTION'

        WHEN
 h.target_name ILIKE 
'%db%'
 OR
 h.target_name ILIKE 
'%database%'
 THEN
 'DATABASE'

        ELSE
 'STANDARD'

    END
 AS
 access_sensitivity,

    s.total_connection_count,

    CASE

        WHEN
 s.session_terminated_time 
=
 'infinity'
 THEN
 NULL

        ELSE
 ROUND
(EXTRACT(EPOCH 
FROM
 (s.session_terminated_time 
-
 s.session_pending_time))::
numeric
, 
1
)

    END
 AS
 duration_seconds

FROM
 wh_session_accumulating_fact s

JOIN
 wh_user_dimension u 
ON
 s.user_key 
=
 u.key 
AND
 u.current_row_indicator 
=
 'Current'

JOIN
 wh_host_dimension h 
ON
 s.host_key 
=
 h.key 
AND
 h.current_row_indicator 
=
 'Current'

WHERE
 s.session_pending_time 
>=
 CURRENT_DATE 
-
 INTERVAL 
'30 days'

  AND
 (h.target_name ILIKE 
'%admin%'

       OR
 h.target_name ILIKE 
'%prod%'

       OR
 h.target_name ILIKE 
'%db%'
)

ORDER BY
 s.session_pending_time 
DESC
;

Note Customize the ILIKE patterns in the WHERE clause and CASE expression to match your organization's target naming conventions.

C-004: Separation of duties verification

This query detects users who access both development and production systems, a common separation-of-duties violation. It groups targets by environment based on naming conventions and flags users spanning multiple environments.

WITH
 user_environment_access 
AS
 (

    SELECT

        u.user_name,

        u.user_organization_name,

        CASE

            WHEN
 h.target_name ILIKE 
'%prod%'
 THEN
 'PRODUCTION'

            WHEN
 h.target_name ILIKE 
'%dev%'
 THEN
 'DEVELOPMENT'

            WHEN
 h.target_name ILIKE 
'%test%'
 OR
 h.target_name ILIKE 
'%staging%'
 THEN
 'NON-PROD'

            ELSE
 'OTHER'

        END
 AS
 environment,

        COUNT
(
*
) 
AS
 access_count

    FROM
 wh_session_accumulating_fact s

    JOIN
 wh_user_dimension u 
ON
 s.user_key 
=
 u.key 
AND
 u.current_row_indicator 
=
 'Current'

    JOIN
 wh_host_dimension h 
ON
 s.host_key 
=
 h.key 
AND
 h.current_row_indicator 
=
 'Current'

    WHERE
 s.session_pending_time 
>=
 CURRENT_DATE 
-
 INTERVAL 
'90 days'

    GROUP BY
 u.user_name, u.user_organization_name, environment

)

SELECT

    user_name,

    user_organization_name,

    array_agg(
DISTINCT
 environment 
ORDER BY
 environment) 
AS
 environments_accessed,

    SUM
(access_count) 
AS
 total_accesses

FROM
 user_environment_access

GROUP BY
 user_name, user_organization_name

HAVING
 COUNT
(
DISTINCT
 environment) 
>
 1

   AND
 'PRODUCTION'
 =
 ANY(array_agg(environment))

   AND
 (
'DEVELOPMENT'
 =
 ANY(array_agg(environment)) 
OR
 'NON-PROD'
 =
 ANY(array_agg(environment)))

ORDER BY
 user_name;

Policy violation: Any row returned by this query indicates a user with access spanning production and non-production environments — review whether this is authorized.

SecOps use cases

The following queries are verified against Boundary v0.21.0 and support threat hunting, incident response, and security monitoring workflows.

S-001: Sessions without connections (authorization failures / abandoned access)

This query surfaces sessions that were authorized, but never established a connection. These represent either abandoned access attempts, potential authorization failures, or reconnaissance activity.

SELECT

    s.session_id,

    u.user_name,

    h.target_name,

    h.target_type,

    s.session_pending_time,

    CASE

        WHEN
 s.session_terminated_time 
=
 'infinity'
 THEN
 'STILL PENDING'

        ELSE
 'TERMINATED'

    END
 AS
 session_status,

    ROUND
(EXTRACT(EPOCH 
FROM
 (

        COALESCE
(

            NULLIF
(s.session_terminated_time, 
'infinity'
),

            NOW
()

        ) 
-
 s.session_pending_time

    )))::
int
 AS
 seconds_since_creation

FROM
 wh_session_accumulating_fact s

JOIN
 wh_user_dimension u 
ON
 s.user_key 
=
 u.key 
AND
 u.current_row_indicator 
=
 'Current'

JOIN
 wh_host_dimension h 
ON
 s.host_key 
=
 h.key 
AND
 h.current_row_indicator 
=
 'Current'

LEFT JOIN
 wh_session_connection_accumulating_fact c 
ON
 s.session_id 
=
 c.session_id

WHERE
 s.session_pending_time 
>=
 CURRENT_DATE 
-
 INTERVAL 
'7 days'

  AND
 c.connection_id 
IS
 NULL

GROUP BY
 s.session_id, u.user_name, h.target_name, h.target_type,

         s.session_pending_time, s.session_terminated_time

ORDER BY
 s.session_pending_time 
DESC
;

Triage guidance:

  • Multiple pending sessions from one user against different targets → Potential reconnaissance or lateral movement attempt
  • Long-pending sessions with no connection → Abandoned access; verify user intent
  • Pattern of admin targeting sensitive targets without connecting → Escalate to incident response

S-002: Multi-target access velocity (lateral movement indicator)

This query detects users accessing an unusually high number of distinct targets in a short time window. This pattern is a strong indicator of lateral movement during an active compromise.

WITH
 rapid_access 
AS
 (

    SELECT

        u.user_name,

        u.user_id,

        COUNT
(
DISTINCT
 h.target_id) 
AS
 unique_targets,

        MIN
(s.session_pending_time) 
AS
 first_access,

        MAX
(s.session_pending_time) 
AS
 last_access,

        ROUND
(EXTRACT(EPOCH 
FROM
 (
MAX
(s.session_pending_time) 
-
 MIN
(s.session_pending_time))) 
/
 60
)::
int
 AS
 window_minutes,

        array_agg(
DISTINCT
 h.target_name 
ORDER BY
 h.target_name) 
AS
 targets_accessed

    FROM
 wh_session_accumulating_fact s

    JOIN
 wh_user_dimension u 
ON
 s.user_key 
=
 u.key 
AND
 u.current_row_indicator 
=
 'Current'

    JOIN
 wh_host_dimension h 
ON
 s.host_key 
=
 h.key 
AND
 h.current_row_indicator 
=
 'Current'

    WHERE
 s.session_pending_time 
>=
 CURRENT_DATE 
-
 INTERVAL 
'24 hours'

    GROUP BY
 u.user_name, u.user_id

)

SELECT
 *

FROM
 rapid_access

WHERE
 unique_targets 
>=
 3

   OR
 window_minutes 
<=
 30

ORDER BY
 unique_targets 
DESC
, window_minutes 
ASC
;

Tip Adjust the thresholds (unique_targets >= 3, window_minutes <= 30) based on your environment's baseline. For high-churn environments, increase the target count threshold.

S-003: Auth token anomaly detection (multi-IP usage)

This query identifies authentication tokens used from multiple distinct client IP addresses — a potential indicator of token theft, session hijacking, or credential sharing.

SELECT

    u.user_name,

    a.auth_token_id,

    a.auth_token_issued_time,

    a.auth_token_approximate_last_access_time,

    COUNT
(
DISTINCT
 s.session_id) 
AS
 sessions_created,

    COUNT
(
DISTINCT
 c.client_tcp_address) 
AS
 unique_client_ips,

    array_agg(
DISTINCT
 c.client_tcp_address::
text
) 
FILTER
 (
WHERE
 c.client_tcp_address 
IS NOT NULL
) 
AS
 client_ips,

    array_agg(
DISTINCT
 h.target_name 
ORDER BY
 h.target_name) 
AS
 targets_accessed

FROM
 wh_auth_token_accumulating_fact a

JOIN
 wh_user_dimension u 
ON
 a.user_key 
=
 u.key 
AND
 u.current_row_indicator 
=
 'Current'

LEFT JOIN
 wh_session_accumulating_fact s 
ON
 a.auth_token_id 
=
 s.auth_token_id

LEFT JOIN
 wh_session_connection_accumulating_fact c 
ON
 s.session_id 
=
 c.session_id

LEFT JOIN
 wh_host_dimension h 
ON
 s.host_key 
=
 h.key 
AND
 h.current_row_indicator 
=
 'Current'

WHERE
 a.auth_token_issued_time 
>=
 CURRENT_DATE 
-
 INTERVAL 
'7 days'

GROUP BY
 u.user_name, a.auth_token_id, a.auth_token_issued_time, a.auth_token_approximate_last_access_time

HAVING
 COUNT
(
DISTINCT
 c.client_tcp_address) 
>
 1

ORDER BY
 unique_client_ips 
DESC
;

Response actions:

  • unique_client_ips > 3 → Critical: Investigate immediately. Token may be compromised.
  • unique_client_ips = 2 and IPs are geographically distant → Escalate for impossible travel analysis.
  • unique_client_ips = 2 and IPs are in same subnet → May be legitimate (VPN reconnection, roaming).

S-004: Data exfiltration indicators (volume anomaly detection)

This query surfaces connections with byte volumes significantly above baseline, a potential indicator of data exfiltration. It uses a self-baselining approach comparing each user's recent connections against their own 30-day average.

WITH
 user_baseline 
AS
 (

    SELECT

        user_key,

        AVG
(bytes_down) 
AS
 avg_bytes_down,

        STDDEV(bytes_down) 
AS
 stddev_bytes_down,

        AVG
(bytes_up) 
AS
 avg_bytes_up,

        STDDEV(bytes_up) 
AS
 stddev_bytes_up

    FROM
 wh_session_connection_accumulating_fact

    WHERE
 connection_authorized_time 
>=
 CURRENT_DATE 
-
 INTERVAL 
'30 days'

      AND
 bytes_down 
IS NOT NULL

    GROUP BY
 user_key

),

recent_connections 
AS
 (

    SELECT

        c.connection_id,

        c.session_id,

        c.user_key,

        u.user_name,

        h.target_name,

        h.target_type,

        c.bytes_up,

        c.bytes_down,

        c.connection_authorized_time

    FROM
 wh_session_connection_accumulating_fact c

    JOIN
 wh_user_dimension u 
ON
 c.user_key 
=
 u.key 
AND
 u.current_row_indicator 
=
 'Current'

    JOIN
 wh_host_dimension h 
ON
 c.host_key 
=
 h.key 
AND
 h.current_row_indicator 
=
 'Current'

    WHERE
 c.connection_authorized_time 
>=
 CURRENT_DATE 
-
 INTERVAL 
'24 hours'

      AND
 c.bytes_down 
IS NOT NULL

)

SELECT

    r.user_name,

    r.target_name,

    r.target_type,

    ROUND
(r.bytes_down 
/
 1024
.
0
, 
1
) 
AS
 kb_downloaded,

    ROUND
(b.avg_bytes_down 
/
 1024
.
0
, 
1
) 
AS
 user_avg_kb,

    ROUND
((r.bytes_down 
-
 b.avg_bytes_down) 
/
 NULLIF
(b.stddev_bytes_down, 
0
), 
1
) 
AS
 std_deviations_from_mean,

    r.connection_authorized_time

FROM
 recent_connections r

JOIN
 user_baseline b 
ON
 r.user_key 
=
 b.user_key

WHERE
 r.bytes_down 
>
 (b.avg_bytes_down 
+
 (
3
 *
 b.stddev_bytes_down))

   OR
 r.bytes_up 
>
 (b.avg_bytes_up 
+
 (
3
 *
 b.stddev_bytes_up))

ORDER BY
 std_deviations_from_mean 
DESC
;

Threshold interpretation:

  • 3+ standard deviations above mean → Unusual; investigate the specific target and data type
  • 5+ standard deviations → Critical anomaly; escalate immediately
  • Sustained high-volume connections over multiple sessions → Potential staging activity

S-005: Incident timeline reconstruction

This query reconstructs a complete chronological timeline of all access events for a specific user during an incident window. Use this to understand the full scope of a compromise.

-- First event type: session authorizations

SELECT

    s.session_pending_time 
AS
 event_time,

    'SESSION_AUTHORIZED'
 AS
 event_type,

    u.user_name,

    h.target_name,

    h.target_type,

    h.project_name,

    NULL
 AS
 client_ip,

    NULL
 AS
 kb_transferred

FROM
 wh_session_accumulating_fact s

JOIN
 wh_user_dimension u 
ON
 s.user_key 
=
 u.key 
AND
 u.current_row_indicator 
=
 'Current'

JOIN
 wh_host_dimension h 
ON
 s.host_key 
=
 h.key 
AND
 h.current_row_indicator 
=
 'Current'

WHERE
 u.user_name 
=
 :incident_user

  AND
 s.session_pending_time 
BETWEEN
 :incident_start 
AND
 :incident_end

UNION ALL

-- Second event type: connections established

SELECT

    c.connection_authorized_time,

    'CONNECTION_ESTABLISHED'
,

    u.user_name,

    h.target_name,

    h.target_type,

    h.project_name,

    c.client_tcp_address::
text
,

    (
COALESCE
(c.bytes_up, 
0
) 
+
 COALESCE
(c.bytes_down, 
0
)) 
/
 1024
.
0

FROM
 wh_session_connection_accumulating_fact c

JOIN
 wh_user_dimension u 
ON
 c.user_key 
=
 u.key 
AND
 u.current_row_indicator 
=
 'Current'

JOIN
 wh_host_dimension h 
ON
 c.host_key 
=
 h.key 
AND
 h.current_row_indicator 
=
 'Current'

WHERE
 u.user_name 
=
 :incident_user

  AND
 c.connection_authorized_time 
BETWEEN
 :incident_start 
AND
 :incident_end

UNION ALL

-- Third event type: session terminations

SELECT

    s.session_terminated_time,

    'SESSION_TERMINATED'
,

    u.user_name,

    h.target_name,

    h.target_type,

    h.project_name,

    NULL
,

    COALESCE
(s.total_bytes_down, 
0
) 
/
 1024
.
0

FROM
 wh_session_accumulating_fact s

JOIN
 wh_user_dimension u 
ON
 s.user_key 
=
 u.key 
AND
 u.current_row_indicator 
=
 'Current'

JOIN
 wh_host_dimension h 
ON
 s.host_key 
=
 h.key 
AND
 h.current_row_indicator 
=
 'Current'

WHERE
 u.user_name 
=
 :incident_user

  AND
 s.session_terminated_time 
<>
 'infinity'

  AND
 s.session_terminated_time 
BETWEEN
 :incident_start 
AND
 :incident_end

ORDER BY
 event_time;

Usage: Replace :incident_user with the user's name and :incident_start/:incident_end with the investigation window. The output is a chronologically ordered table of every access event — authorizations, connections, and terminations — that can be exported directly into an incident report.

Setting up a read-only BI user

Use the following command to create read-only user role for business intelligence tools.

-- Create a read-only role for BI tools

CREATE
 ROLE
 boundary_bi_readonly 
WITH
 LOGIN
 PASSWORD
 '<secure_password>'
;

GRANT
 CONNECT
 ON
 DATABASE
 boundary 
TO
 boundary_bi_readonly;

GRANT
 USAGE 
ON
 SCHEMA
 public 
TO
 boundary_bi_readonly;

GRANT
 SELECT
 ON
 ALL TABLES 
IN
 SCHEMA
 public 
TO
 boundary_bi_readonly;

ALTER
 DEFAULT
 PRIVILEGES 
IN
 SCHEMA
 public 
GRANT
 SELECT
 ON
 TABLES 
TO
 boundary_bi_readonly;

ALTER
 ROLE
 boundary_bi_readonly 
SET
 default_transaction_read_only 
=
 ON
;

-- Prevent runaway queries from impacting the production database

ALTER
 ROLE
 boundary_bi_readonly 
SET
 statement_timeout 
=
 '60s'
;

Warning Never grant write access to BI users. The Boundary database is a production system. Use a read replica when possible.

Key analytical queries

Refer to the following sections for key analytical queries.

Session activity by target

SELECT

    wh.target_name,

    wh.project_name,

    COUNT
(
*
) 
AS
 session_count,

    AVG
(EXTRACT(EPOCH 
FROM
 (wsf.session_terminated_time 
-
 wsf.session_active_time))) 
AS
 avg_duration_seconds,

    SUM
(
COALESCE
(wsf.total_bytes_down, 
0
)) 
/
 (
1024
*
1024
) 
AS
 total_mb_downloaded

FROM
 wh_session_accumulating_fact wsf

JOIN
 wh_host_dimension wh 
ON
 wh.key 
=
 wsf.host_key

    AND
 wh.current_row_indicator 
=
 'Current'

WHERE
 wsf.session_pending_time 
>=
 CURRENT_DATE 
-
 INTERVAL 
'30 days'

GROUP BY
 wh.target_name, wh.project_name

ORDER BY
 session_count 
DESC
;

User access roster (for access reviews)

SELECT

    u.user_name,

    u.auth_account_email,

    u.auth_method_type,

    u.user_organization_name,

    COUNT
(
DISTINCT
 s.session_id) 
AS
 sessions_30d,

    MAX
(s.session_pending_time) 
AS
 last_activity

FROM
 wh_user_dimension u

LEFT JOIN
 wh_session_accumulating_fact s 
ON
 u.key 
=
 s.user_key

WHERE
 u.current_row_indicator 
=
 'Current'

GROUP BY
 u.user_name, u.auth_account_email, u.auth_method_type, u.user_organization_name

ORDER BY
 last_activity 
DESC
 NULLS
 LAST
;

Connection-level audit trail

SELECT

    u.user_name,

    h.target_name,

    c.connection_authorized_time,

    c.client_tcp_address,

    c.endpoint_tcp_address,

    c.bytes_up 
/
 1024
 AS
 kb_up,

    c.bytes_down 
/
 1024
 AS
 kb_down

FROM
 wh_session_connection_accumulating_fact c

JOIN
 wh_user_dimension u 
ON
 c.user_key 
=
 u.key 
AND
 u.current_row_indicator 
=
 'Current'

JOIN
 wh_host_dimension h 
ON
 c.host_key 
=
 h.key 
AND
 h.current_row_indicator 
=
 'Current'

WHERE
 c.connection_authorized_time 
>=
 CURRENT_DATE 
-
 INTERVAL 
'7 days'

ORDER BY
 c.connection_authorized_time 
DESC
;

Data retention and performance

Note the following data retention best practices for optimal performance:

  • The warehouse has no automatic purging. Implement your own retention policy.
  • Fact tables are populated only as Boundary events occur — a fresh install starts with zero rows in fact tables and only default records in dimensions.
  • The wh_date_dimension and wh_time_of_day_dimension values are pre-populated reference tables.
  • For high-volume deployments, consider periodic aggregation into materialized views for dashboard performance.

Version compatibility

This reference is validated against Boundary v0.21.0. The warehouse schema has evolved across versions. To verify your version's schema:

-- List all warehouse tables

SELECT
 tablename 
FROM
 pg_tables 
WHERE
 tablename 
LIKE
 'wh_%'
 ORDER BY
 tablename;

-- List all warehouse functions

SELECT
 proname 
FROM
 pg_proc 
WHERE
 proname 
LIKE
 'wh_%'
 ORDER BY
 proname;

-- Inspect a specific table's structure

\d wh_session_accumulating_fact

More information

Refer to Boundary data warehouse for more information about the data warehouse's architecture and tables.