Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ui: introduce schema insights page on db-console #86317

Merged
merged 1 commit into from
Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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} />;
};
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