Skip to content

Advanced Features

This chapter covers the power-user (DBA / data engineer / backend dev) features. They live in right-click menus, the ⌘K command palette, or deeper toolbar drawers — you won't hit them in everyday SELECTs, but they save serious time when:

  • Trying to see whether an execution plan uses indexes, and which node is slowest
  • Wanting to infer what indexes to build from historical SQL
  • Wanting to see a table's column distribution / NULL ratio / whether types are oversized
  • Cleaning duplicates / back-filling defaults / restoring from soft-delete
  • Searching the whole database for a value's occurrences
  • Building queries by drag-pick instead of typing SQL
  • Managing Doris/StarRocks partitions / ClickHouse parts / MySQL binlog / PG extensions
  • Migrating a whole Oracle DB to DM

The sections follow "look → change → search → build → migrate".

1. EXPLAIN visualizer — PlanPanel

Anyone who writes SQL has read EXPLAIN, but raw text is hard. SkylerX attaches a Plan panel next to QueryPane that renders EXPLAIN as a tree + summary.

Trigger

EntryAction
QueryPane toolbar 📊 PlanEXPLAIN current SQL (doesn't actually run)
⌘⇧E / Ctrl+Shift+ESame
▶ Analyze next to 📊 PlanEXPLAIN ANALYZE (actually runs — careful with DML)

Backend goes through plan.ts → planQuery(dialect, sql, { analyze }):

DialectStatement
PostgreSQL / KingbaseEXPLAIN (FORMAT JSON) <sql> / EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) <sql>
MySQL / MariaDB / OceanBaseEXPLAIN FORMAT=TREE <sql> / EXPLAIN ANALYZE <sql> (MySQL 8.0.18+)
OtherFall back to tabular EXPLAIN (plain <pre>)

Node tree rendering

PG's JSON Plan is parsed with parsePgPlan into a PlanNode tree, then flattenPlan flattens it to {node, depth} for rendering. Each node shows:

  • Label: Seq Scan / Index Scan / Hash Join ...
  • Detail: on users / using users_pk / inner join
  • Cost bar: width = cost / maxCost * 60px, green-to-red gradient
  • Numbers: cost 1234.56 · est 1000 · act 1234 · 12.3ms (act / ms only with ANALYZE)

Slow-node coloring

PlanPanel marks the "expensive 1/3" nodes red:

ts
function isSlow(node) {
  return node.cost >= maxCost.value * 0.33 && maxCost.value > 0
}

Red background + red label — the eye spots what to optimize instantly without comparing cost numbers row by row.

Estimated vs actual skew

estimateSkew(node) computes max(est, act) / min(est, act). ≥ 10× signals stale optimizer stats (a typical tell); a yellow side bar appears on the node and a ⚠ 24× badge at the end. The summary bar also calls out the worst node:

ts
let skewWorst = null
for (const r of arr) {
  const sk = estimateSkew(r.node)
  if (sk == null) continue
  if (!skewWorst || sk > skewWorst.skew) skewWorst = { node: r.node, skew: sk }
}

When you see this badge, it's usually time to ANALYZE table or refresh pg_statistic.

Summary bar

Top of the panel:

FieldMeaning
Total CostCost of the heaviest node (root cumulative)
Actual msWith EXPLAIN ANALYZE: per-node actual time summed
HeaviestName of the highest-cost node
SkewWorst est-vs-actual node + multiplier

2. Index recommender — IndexRecommender

⌘K → Index recommender or right-click a DB node in NavTree 🔧 Recommend indexes.

Inputs and outputs

InputSource
Historical SQL patternsclient.connections.history(connId, 1000) (latest 1000)
Existing indexesMySQL information_schema.STATISTICS / PG pg_index + pg_class

Output: IndexHint[] — each with table name, columns, score, reasoning, and a ready-to-run CREATE INDEX DDL.

Inference algorithm (index-recommender.ts)

No SQL parser (expensive and dialect-divergent); uses regex heuristics to extract WHERE / JOIN / ORDER BY / GROUP BY:

  1. Aggregate history: identical SQL text rolls into one row with count + totalMs
  2. Filter: keep only SELECT / WITH, skip DML/DDL
  3. Parse aliases: parseTableAliases(sql) extracts tbl [AS] alias from FROM/JOIN into a Map
  4. Scan four clause types, each hit weighted:
ClauseBase scoreNotes
WHERE col = ? / LIKE / IN / IS NULL / BETWEEN5Strong signal
JOIN ON a.col = b.col3Both sides scored
ORDER BY col2Sorted index helps
GROUP BY col2Same
  1. Time weighting: per-SQL count × min(perMs/avgMs, MAX_TIME_MULTIPLIER=5) — prevents one or two slow SQLs from swamping the table
  2. Multi-table SQL requires aliases for bare columns; single-table SQL accepts bare columns
  3. Filter existing indexes: isCovered(table, cols, known) — if any existing index's prefix fully covers the suggested column list, skip
  4. Compound suggestions: for each table, pair the top 3 columns to suggest two-column indexes

DDL generation

ts
function buildDdl(table, columns, dialect) {
  const idxName = `idx_${sanitize(table)}_${cols.map(sanitize).join('_')}`.slice(0, 60)
  return `CREATE INDEX ${quoteIdent(idxName)} ON ${quoteIdent(table)}(${cols.map(quoteIdent).join(', ')});`
}

MySQL uses backticks; PG uses double quotes.

UI flow

Open the dialog and run() fires: scan → candidate list (sorted by scoreEstimate desc). Per row:

  • [Adopt]emit('runSql', h.ddl) pushes DDL to QueryPane (you review then run)
  • [Copy all] copies every candidate DDL to clipboard
  • [Rescan] reruns the flow

Supported on MySQL family / PG family; others show "not supported".


3. Data inspector — DataInspector

Table right-click 🔬 Data inspector. One dialog, 5 tabs — covers the "see data health + one-click maintenance" DBA core actions. Designed to NOT run SQL in parallel (don't want to overwhelm prod): only the active tab fetches data.

Tab 1: column sampling (A3)

Pick a column; one SQL computes all stats:

sql
SELECT
  COUNT(*) AS total,
  COUNT(col) AS non_null,
  COUNT(DISTINCT col) AS distinct_cnt,
  MIN(col) AS min_val,
  MAX(col) AS max_val
FROM <table>

Then a top-10:

sql
SELECT col AS value, COUNT(*) AS cnt
FROM <table> GROUP BY col ORDER BY cnt DESC LIMIT 10

Cards show stats + top-N. High NULL rate / tiny distinct (probably a status code) / outliers stand out at a glance.

Tab 2: whole-table profile (B6)

One big SELECT computes COUNT(col) + COUNT(DISTINCT col) for every column:

sql
SELECT COUNT(*) AS total,
       COUNT(`a`) AS nn_a, COUNT(DISTINCT `a`) AS dc_a,
       COUNT(`b`) AS nn_b, COUNT(DISTINCT `b`) AS dc_b,
       ...
FROM <table>

Output: column | type | NULL% | DISTINCT/total. NULL% > 50 turns yellow — "this column may not be in use".

Tab 3: constraint scan (B5)

Lists columns declared IS_NULLABLE = 'NO', then runs SELECT COUNT(*) WHERE col IS NULL on each. > 0 = constraint violation (usually NOT NULL was added later but dirty data wasn't cleaned).

Tab 4: type optimization suggestions (B9)

Per-column type-shrink suggestions:

Current typeCheckSuggestion
VARCHAR(255)MAX(CHAR_LENGTH(col)) actual maxVARCHAR(max(32, ceil(maxlen*1.5))), if declared > maxlen*4 and gap > 50
BIGINTMAX(ABS(col))If < 2³¹-1 → INT
INTSameIf < 32767 → SMALLINT

Each suggestion includes a reason (actual max is 20, declared 255 — wasting 235 bytes).

Tab 5: table maintenance (B10)

Per-dialect one-click buttons:

DialectButtons
MySQL familyANALYZE TABLE / OPTIMIZE TABLE / CHECK TABLE
PG familyANALYZE / VACUUM FULL / VACUUM / REINDEX TABLE

Each requires confirmation (VACUUM FULL locks the table).


4. Data fixup — DataFixup

Table right-click 🩹 Data fixup. Three tabs sharing the same "input → generate SQL → review → execute" skeleton. Does not auto-commit — pushes the generated SQL to QueryPane as a draft for you to inspect.

Tab 1: duplicate row detection (B3)

Pick a few columns as business key (email + tenant_id); first GROUP BY to see duplicates:

sql
SELECT col1, col2, COUNT(*) AS cnt
FROM <table>
GROUP BY col1, col2 HAVING COUNT(*) > 1
ORDER BY cnt DESC LIMIT 100

If confirmed, click Generate cleanup SQL for a ROW_NUMBER() window-delete (PG variant), with a MySQL self-join alternative in the comments:

sql
-- Keep ROW_NUMBER() = 1 per group, delete the rest
DELETE FROM <table>
WHERE (col1, col2, ctid) IN (
  SELECT col1, col2, ctid FROM (
    SELECT col1, col2, ctid,
           ROW_NUMBER() OVER (PARTITION BY col1, col2 ORDER BY ctid) AS rn
    FROM <table>
  ) sub WHERE sub.rn > 1
);

Tab 2: NULL back-fill (B4)

Pick a column + strategy:

StrategyGenerated SET expression
literal'<user-typed value>'
avg(SELECT AVG(col) FROM <table>)
min / max(SELECT MIN/MAX(col) FROM <table>)
most_common(SELECT col GROUP BY col ORDER BY COUNT(*) DESC LIMIT 1)

Final SQL: UPDATE <table> SET col = <expr> WHERE col IS NULL; — with a comment "do a SELECT COUNT first to see the impact".

Tab 3: soft-delete restore (B8)

Heuristic search for soft-delete columns (deleted_at / is_deleted / deleted). Generates the right "revive" statement per type:

Column nameGenerates
is_deleted / *_flagUPDATE ... SET col = FALSE WHERE col = TRUE
deleted_at / other timestampUPDATE ... SET col = NULL WHERE col IS NOT NULL

Optional "extra WHERE" (AND user_id = 42) limits scope — avoids reviving every soft-deleted row at once.


5. Cross-table value search — SearchValueDialog

⌘K → Cross-table search or right-click a result cell 🔎 Find this value elsewhere (the latter prefills the value).

Workflow

  1. Fetch "searchable" text columns (information_schema.columns):
    • MySQL: varchar / char / text / tinytext / mediumtext / longtext / json
    • PG: character varying / character / text / json / jsonb
  2. Group by table: each table gets a SELECT * FROM t WHERE col1 LIKE :v OR col2 LIKE :v ... LIMIT 50
  3. Parallel run (max 6 concurrent to protect the pool)
  4. Progress bar + hit list

Performance edge

Large DBs can have thousands of columns. Use table_prefix to narrow the scope (user_*). matchMode can be contains / exact:

  • containsLIKE '%v%' (slow but complete)
  • exact= 'v' (fast, good for ID lookup)

maxPerTable caps hits per table at 50 — prevents a wide table from blowing memory.

Example

Investigating "why is user alice@x.com getting these pushes":

  1. ⌘K → Cross-table search
  2. Value alice@x.com, mode exact
  3. Scan everything; sees users(email) + subscription(email) + mail_logs(to_addr) all contain it → locked the data flow

6. Row history — RowHistoryDialog

Right-click a row in the result grid ⏱ View history.

Heuristic shadow-table discovery

Given a PK ({id: 42}), auto-scan information_schema.tables for candidates:

sql
SELECT table_name FROM information_schema.tables
WHERE table_name LIKE '<base>_%'
   OR table_name = 'audit_<base>'
   OR table_name = '<base>_history'

Pick from the <datalist> dropdown or type your own.

Fetch history

Filter by PK, sort by changed_at / updated_at / created_at / version / revision desc:

sql
SELECT * FROM <shadowTable>
WHERE id = 42
ORDER BY changed_at, updated_at, created_at, version, revision DESC
LIMIT 200

Each row is a version; column headers are the shadow table's columns; string fields truncated to 80 chars.


7. Visual query builder — VisualQueryDialog

⌘K → Visual query or right-click DB node 🎨 Visual build.

MVP doesn't do a drag canvas — sticks with a sturdier "list + cards" layout that's actually usable, not just a demo.

Layout

AreaContent
LeftAll tables in the DB + search + checkboxes
MiddleChecked tables expand into cards; each column has a checkbox (checked → SELECT; unchecked → shown only)
TopWHERE / ORDER BY inputs + LIMIT
BottomLive-generated SQL + Open as new query tab

Auto JOIN

When two tables are checked, auto-detect "FK-looking" columns and emit INNER JOIN:

ts
// inferConventionalFks
const m = /^(.+?)_id$|^(.+?)Id$/.exec(col.name)
// user_id → users.id  /  category_id → categories.id

Target candidates: <base> as-is + simple plural (user → users, category → categories). If no FK path → CROSS JOIN (with a visual hint about efficiency).

SQL generation

sql
SELECT users.id AS users_id, users.name AS users_name,
       orders.id AS orders_id, orders.amount AS orders_amount
FROM users
  INNER JOIN orders ON users.id = orders.user_id
WHERE amount > 100
ORDER BY users.id DESC
LIMIT 200

Columns aliased as <table>_<col> to avoid name collisions across tables.


8. MPP partition management — MppPartitionDialog

For Doris / StarRocks (MySQL-wire). DB node right-click 🗂 Partitions.

Fields

Runs SHOW PARTITIONS FROM <db>.<tbl>; shows:

FieldMeaning
PartitionId / PartitionNamePartition meta
StateNORMAL etc.
PartitionKey / RangePartition column + range
DistributionKey / BucketsBucket key + count
ReplicationNumReplicas
StorageMediumHDD / SSD
CooldownTimeCooldown (HDD demotion)
DataSizePartition size (auto KB/MB/GB)

Actions

ButtonAction
+ Add partitionDialog for the ADD PARTITION ... clause; auto-prefixes ALTER TABLE <db>.<tbl>
Per-row DROPConfirm → ALTER TABLE <db>.<tbl> DROP PARTITION <name>
🔄 RefreshRe-runs SHOW PARTITIONS

9. Dialect-specific advanced panels

9.1 MysqlAdvancedDialog

For MySQL / MariaDB / OceanBase / TiDB / Doris / StarRocks. 3 tabs:

TabSQLs used
BinlogSHOW MASTER STATUS + SHOW BINARY LOGS + (after selecting a file) SHOW BINLOG EVENTS IN '<file>' LIMIT N
Replication statusSHOW REPLICA STATUS (8.0+) preferred; fall back to SHOW SLAVE STATUS (MariaDB / older)
Variables / StatusSHOW GLOBAL VARIABLES / SHOW GLOBAL STATUS, filterable; Variables also support SET GLOBAL k = v

9.2 PgAdvancedDialog

For PostgreSQL / Kingbase / openGauss / Greenplum / CockroachDB / Redshift. 3 tabs:

TabSource
Extensionspg_available_extensions; one-click CREATE EXTENSION IF NOT EXISTS "<name>" WITH SCHEMA "<schema>" / DROP EXTENSION
Publications / Subscriptionspg_publication + pg_publication_tables + pg_subscription (logical replication management)
Slotspg_replication_slots (slot_name / plugin / slot_type / active / restart_lsn / confirmed_flush_lsn / wal_status); supports DROP_REPLICATION_SLOT

9.3 ClickHouseAdvancedDialog

4 tabs, all reading system.*, mostly read-only:

TabSourceUse
Partitionssystem.parts (active only)View rows / bytes_on_disk / data_compressed_bytes / marks / min_date / max_date / level; supports DROP / DETACH / ATTACH PARTITION
Mutationsystem.mutationsView is_done / command / parts_to_do / latest_failed_part / latest_fail_reason
Replicassystem.replicasView is_leader / queue_size / inserts_in_queue / merges_in_queue / total_replicas / active_replicas / zookeeper_path
Table metadatasystem.tablesView engine / total_rows / total_bytes / partition_key / sorting_key / primary_key / sampling_key / storage_policy

Each tab has a database / table filter at the top — essential for large clusters.


10. Oracle → DM migration wizard

A common Chinese-compliance contractor scenario: move a customer's Oracle DB to DM. ⌘K → Oracle → DM migration opens the wizard.

5 steps

StepAction
1. Pick connectionsFilter saved connections by dialect == Oracle / dialect == DM; pick source + target
2. Pick objectsPull source tables / views / sequences / procedures; all selected by default, group / individual toggles
3. PreviewPer object: DBMS_METADATA.GET_DDL for source DDL → translateDdl() → show warnings + allow edits
4. ExecutePer object: client.connections.execute(dstConnId, ddl); errors collected, doesn't stop
5. ReportMarkdown summary of success / failure / warnings; copyable / saveable

Translation rules (oracleToDm.ts)

Type mapping (TYPE_MAP):

OracleDMNotes
VARCHAR2VARCHAR
NVARCHAR2NVARCHAR
NUMBERNUMERICDM accepts NUMBER too, but NUMERIC is more standard
CLOB / NCLOB / BLOBKept
DATEDATE⚠ Oracle has hⓂ️s; DM doesn't
TIMESTAMPTIMESTAMP
RAW / LONG RAWVARBINARY
LONGCLOBOracle deprecated
BINARY_FLOAT / BINARY_DOUBLEFLOAT / DOUBLE
ROWID / UROWIDVARCHAR(18) / VARCHAR(4000)DM has no equivalent — downgrade
XMLTYPEXMLXPath/XQuery expressions may need rewriting

Type-replacement implementation: sort by "longer key first" (LONG RAW before LONG so the shorter doesn't pre-empt); bare NUMBER without length isn't padded; NUMBER(p,s) carries numbers through; matched cases append warnings from TYPE_NOTES.

Function / keyword mapping (FN_MAP):

OracleDMNotes
SYSDATE / SYSTIMESTAMPCURRENT_TIMESTAMPDM accepts SYSDATE; standard form is sturdier
NVL(a, b)COALESCE(a, b)DM accepts NVL; COALESCE more portable
NVL2(...)KeptIf unsupported: CASE WHEN expr IS NOT NULL THEN a ELSE b END
MINUSEXCEPTDM accepts MINUS; EXCEPT more standard
DUAL / ROWNUMKeptDM supports

Complex-syntax warnings (HARD_WARNINGS — SQL is unchanged, just [review] warnings):

PatternWarning
DECODE(...)Still usable, but consider CASE WHEN for readability
CONNECT BYMostly compatible; advanced (NOCYCLE / SYS_CONNECT_BY_PATH) needs case-by-case review
MERGE INTOComplex branches (DELETE WHERE / multi-source UPDATE) may behave differently
INSTEAD OF (INSERT/UPDATE/DELETE) TRIGGERDM trigger semantics differ — bodies need manual migration
SDO_GEOMETRY / MDSYS.*Oracle Spatial has no equivalent — use DMGeo or third-party
DBMS_*Only partial simulation (DBMS_OUTPUT/DBMS_LOB); business packages need rewriting
UTL_* (UTL_HTTP/UTL_FILE etc.)Usually unsupported — needs external scripts
INTERVAL YEAR/DAY TO ...Some versions only support simplified forms — verify against your version
PIVOT(...) / UNPIVOT(...)Partial since DM 8.x; older versions need CASE WHEN aggregation rewrite
BULK COLLECT / FORALLPL/SQL bulk ops — DMSQL syntax differs slightly

Things deliberately not done

  • No PL/SQL block semantic translation — procedures get the CREATE shell only; the body is on you
  • No trigger body translation — same
  • No constraint dependency ordering — alphabetical; failures retry on you
  • No transactional atomicity — each object stands alone; failures get red marks

Data migration (experimental)

Step 4 "Include data migration (first 100 rows per table)":

sql
-- Source
SELECT * FROM "<table>"  -- limit 100 rows

-- Target
INSERT INTO "<table>" (col1, col2, ...) VALUES (v1, v2, ...)  -- per row

This is a skeleton only — full migrations need pagination + type conversion + batch insert; the wizard leaves that to later releases. For production data migrations, use professional tools like DTS / expdp + impdp.

Report

After execution, Step 5 shows a Markdown summary like:

markdown
# Oracle → DM migration report

- Source connection: `prod-oracle`
- Target connection: `dm-test`
- Time: 2026-05-30 10:23:11
- Total objects: 142, success 138, failure 4

## Succeeded
- [tables] ORDERS (124ms)
- [tables] USERS (89ms)
...

## Failed
- [procedures] CALC_BONUS
  - error: ORA-00942 table or view does not exist

## Translation warnings (human review)
- (ORDERS) [type] DATE: Oracle DATE includes h:m:s; DM DATE does not — switch to TIMESTAMP if you depend on time
- (ORDERS_REPORT) [review] PIVOT/UNPIVOT: partial since DM 8.x; older versions need CASE WHEN aggregation

Copy to clipboard or Save to a .md for the archive.


11. When to use what

A "right tool for the job" table to finish up:

I want…Use…
See where a slow SQL is stuckPlanPanel + ANALYZE
Don't know what indexes to buildIndexRecommender
Health-check a table I just inheritedDataInspector whole-table profile + type optimization
Clean dirty data / dedup rowsDataFixup
Find where a value appearsSearchValueDialog
See a row's change historyRowHistoryDialog
Demo query-building to non-technical folksVisualQueryDialog
Manage Doris partitionsMppPartitionDialog
Check MySQL binlog / replication lagMysqlAdvancedDialog
Install PG extensions / configure logical replicationPgAdvancedDialog
Check CH parts / mutation statusClickHouseAdvancedDialog
Migrate Oracle to DMOracleToDmWizard

These compose nicely with AI assistant — when PlanPanel shows a slow node, hit Ask AI; when IndexRecommender candidates are unclear, let AI explain; when DataInspector type suggestions worry you, let AI weigh the risk.

Apache License 2.0 · Wuhan Skyler Network Technology Co., Ltd.