Skip to content

Commit

Permalink
ui: introduce schema insights page on db-console
Browse files Browse the repository at this point in the history
This change introduces the schema insights page to the DB-Console.
Schema insights are fetched from `schemaInsightsApi` using the SQL-over
HTTP API and the corresponding component `schemaInsightsView` is
available from cluster-ui for future use in the CC console. The schema
insights page display a table of schema insights - currently different
types of index recommendations (i.e. drop/create/replace index
recommendations), with the intention to add different types of schema
insights in the future. Each schema insight row offers an actionable
button, offering the user the ability to execute the corresponding SQL
query that realizes their schema insight. Filters are available to
filter by database and the schema insight type, as well as search.

Release note (ui change): Added new Schema Insights page to DB Console.
The Schema Insights page displays a table of schema insights - currently
different types of index recommendations (i.e. drop/create/replace index
recommendations). Each schema insight row offers the user the ability to
execute the corresponding SQL query that realizes their schema insight
via a clickable button. Filters are available to filter the surfaced
schema insights by database and insight type, as well as search.

Release justification: low risk, high benefit changes to existing
functionality
  • Loading branch information
Thomas Hardy committed Aug 25, 2022
1 parent 9a7ec4b commit 2be29c6
Show file tree
Hide file tree
Showing 36 changed files with 1,440 additions and 77 deletions.
1 change: 1 addition & 0 deletions pkg/ui/workspaces/cluster-ui/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export * from "./nodesApi";
export * from "./clusterLocksApi";
export * from "./insightsApi";
export * from "./indexActionsApi";
export * from "./schemaInsightsApi";
195 changes: 195 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/api/schemaInsightsApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// Copyright 2022 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import { executeSql, SqlExecutionRequest, SqlTxnResult } from "./sqlApi";
import {
InsightRecommendation,
InsightType,
recommendDropUnusedIndex,
} from "../insights";
import { HexStringToInt64String } from "../util";

// Export for db-console import from clusterUiApi.
export type { InsightRecommendation } from "../insights";

export type ClusterIndexUsageStatistic = {
table_id: number;
index_id: number;
last_read?: string;
created_at?: string;
index_name: string;
table_name: string;
database_id: number;
database_name: string;
unused_threshold: string;
};

type CreateIndexRecommendationsResponse = {
fingerprint_id: string;
db: string;
query: string;
querysummary: string;
implicittxn: boolean;
index_recommendations: string[];
};

type SchemaInsightResponse =
| ClusterIndexUsageStatistic
| CreateIndexRecommendationsResponse;
type SchemaInsightQuery<RowType> = {
name: InsightType;
query: string;
toSchemaInsight: (response: SqlTxnResult<RowType>) => InsightRecommendation[];
};

function clusterIndexUsageStatsToSchemaInsight(
txn_result: SqlTxnResult<ClusterIndexUsageStatistic>,
): InsightRecommendation[] {
const results: Record<string, InsightRecommendation> = {};

txn_result.rows.forEach(row => {
const result = recommendDropUnusedIndex(row);
if (result.recommend) {
const key = row.table_id.toString() + row.index_id.toString();
if (!results[key]) {
results[key] = {
type: "DROP_INDEX",
database: row.database_name,
query: `DROP INDEX ${row.table_name}@${row.index_name};`,
indexDetails: {
table: row.table_name,
indexID: row.index_id,
indexName: row.index_name,
lastUsed: result.reason,
},
};
}
}
});

return Object.values(results);
}

function createIndexRecommendationsToSchemaInsight(
txn_result: SqlTxnResult<CreateIndexRecommendationsResponse>,
): InsightRecommendation[] {
const results: InsightRecommendation[] = [];

txn_result.rows.forEach(row => {
row.index_recommendations.forEach(rec => {
const recSplit = rec.split(" : ");
const recType = recSplit[0];
const recQuery = recSplit[1];
let idxType: InsightType;
switch (recType) {
case "creation":
idxType = "CREATE_INDEX";
break;
case "replacement":
idxType = "REPLACE_INDEX";
break;
case "drop":
idxType = "DROP_INDEX";
break;
}

results.push({
type: idxType,
database: row.db,
execution: {
statement: row.query,
summary: row.querysummary,
fingerprintID: HexStringToInt64String(row.fingerprint_id),
implicit: row.implicittxn,
},
query: recQuery,
});
});
});
return results;
}

const dropUnusedIndexQuery: SchemaInsightQuery<ClusterIndexUsageStatistic> = {
name: "DROP_INDEX",
query: `SELECT
us.table_id,
us.index_id,
us.last_read,
ti.created_at,
ti.index_name,
t.name as table_name,
t.parent_id as database_id,
t.database_name,
(SELECT value FROM crdb_internal.cluster_settings WHERE variable = 'sql.index_recommendation.drop_unused_duration') AS unused_threshold
FROM "".crdb_internal.index_usage_statistics AS us
JOIN "".crdb_internal.table_indexes as ti ON us.index_id = ti.index_id AND us.table_id = ti.descriptor_id
JOIN "".crdb_internal.tables as t ON t.table_id = ti.descriptor_id and t.name = ti.descriptor_name
WHERE t.database_name != 'system' AND ti.index_type != 'primary';`,
toSchemaInsight: clusterIndexUsageStatsToSchemaInsight,
};

const createIndexRecommendationsQuery: SchemaInsightQuery<CreateIndexRecommendationsResponse> =
{
name: "CREATE_INDEX",
query: `SELECT
encode(fingerprint_id, 'hex') AS fingerprint_id,
metadata ->> 'db' AS db,
metadata ->> 'query' AS query,
metadata ->> 'querySummary' as querySummary,
metadata ->> 'implicitTxn' AS implicitTxn,
index_recommendations
FROM (
SELECT
fingerprint_id,
statistics -> 'statistics' ->> 'lastExecAt' as lastExecAt,
metadata,
index_recommendations,
row_number() over(
PARTITION BY
fingerprint_id
ORDER BY statistics -> 'statistics' ->> 'lastExecAt' DESC
) AS rank
FROM crdb_internal.statement_statistics WHERE aggregated_ts >= now() - INTERVAL '1 week')
WHERE rank=1 AND array_length(index_recommendations,1) > 0;`,
toSchemaInsight: createIndexRecommendationsToSchemaInsight,
};

const schemaInsightQueries: SchemaInsightQuery<SchemaInsightResponse>[] = [
dropUnusedIndexQuery,
createIndexRecommendationsQuery,
];

// getSchemaInsights makes requests over the SQL API and transforms the corresponding
// SQL responses into schema insights.
export function getSchemaInsights(): Promise<InsightRecommendation[]> {
const request: SqlExecutionRequest = {
statements: schemaInsightQueries.map(insightQuery => ({
sql: insightQuery.query,
})),
execute: true,
};
return executeSql<SchemaInsightResponse>(request).then(result => {
const results: InsightRecommendation[] = [];
if (result.execution.txn_results.length === 0) {
// No data.
return results;
}

result.execution.txn_results.map(txn_result => {
// Note: txn_result.statement values begin at 1, not 0.
const insightQuery: SchemaInsightQuery<SchemaInsightResponse> =
schemaInsightQueries[txn_result.statement - 1];
if (txn_result.rows) {
results.push(...insightQuery.toSchemaInsight(txn_result));
}
});
return results;
});
}
2 changes: 2 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/insights/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@

export * from "./workloadInsights";
export * from "./workloadInsightDetails";
export * from "./schemaInsights";
export * from "./utils";
export * from "./types";
export * from "./insightsErrorComponent";
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
import React, { useCallback, useState } from "react";
import { Modal } from "../modal";
import { Text, TextTypes } from "../text";
import { InsightType } from "../insightsTable/insightsTable";
import { Button } from "../button";
import { executeIndexRecAction, IndexActionResponse } from "../api";
import { createIndex, dropIndex, onlineSchemaChanges } from "../util";
import { Anchor } from "../anchor";
import { InlineAlert } from "@cockroachlabs/ui-components";
import classNames from "classnames/bind";
import styles from "./indexActionBtn.module.scss";
import { InsightType } from "./types";

const cx = classNames.bind(styles);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@

import React from "react";
import classNames from "classnames/bind";
import styles from "./workloadInsights.module.scss";
import styles from "./workloadInsights/util/workloadInsights.module.scss";

const cx = classNames.bind(styles);

type SQLInsightsErrorProps = {
execType: string;
};

export const WorkloadInsightsError = (
export const InsightsError = (
props: SQLInsightsErrorProps,
): React.ReactElement => {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2022 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import React from "react";
import { EmptyTable, EmptyTableProps } from "src/empty";
import magnifyingGlassImg from "src/assets/emptyState/magnifying-glass.svg";
import emptyTableResultsImg from "src/assets/emptyState/empty-table-results.svg";

const emptySearchResults = {
title: "No schema insight match your search.",
icon: magnifyingGlassImg,
};

export const EmptySchemaInsightsTablePlaceholder: React.FC<{
isEmptySearchResults: boolean;
}> = props => {
const emptyPlaceholderProps: EmptyTableProps = props.isEmptySearchResults
? emptySearchResults
: {
title: "No schema insight since this page was last refreshed.",
icon: emptyTableResultsImg,
};

return <EmptyTable {...emptyPlaceholderProps} />;
};
13 changes: 13 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/insights/schemaInsights/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright 2022 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

export * from "./indexUsageStatsRec";
export * from "./schemaInsightsView";
export * from "./emptySchemaInsightsTablePlaceholder";
Loading

0 comments on commit 2be29c6

Please sign in to comment.