Skip to content
Closed
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
21 changes: 21 additions & 0 deletions CITATION.cff
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
cff-version: 1.2.0
title: "SQL Server Performance Studio"
message: "If you use this software, please cite it as below."
type: software
authors:
- given-names: Erik
family-names: Darling
affiliation: "Darling Data, LLC"
website: "https://erikdarling.com"
repository-code: "https://github.com/erikdarlingdata/PerformanceStudio"
license: MIT
version: "1.2.0"
date-released: "2026-03-18"
keywords:
- sql-server
- execution-plan
- query-plan-analyzer
- query-optimization
- mcp-server
- cross-platform
- dba-tools
21 changes: 21 additions & 0 deletions llms.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# SQL Server Performance Studio

> Free, open-source SQL Server execution plan analyzer by Erik Darling (Darling Data, LLC). Cross-platform desktop GUI (Avalonia) and CLI tool with 30 analysis rules that identify memory grants, row estimate mismatches, missing indexes, spills, parallel skew, parameter sniffing, implicit conversions, and more. Built-in MCP server for AI-assisted plan analysis. SSMS extension for one-click plan transfer. Runs on Windows, macOS, and Linux. MIT licensed.

- Analyzes .sqlplan XML files or captures plans live from SQL Server
- 30 rules covering memory, estimates, indexes, parallelism, joins, filters, functions, parameters, patterns, compilation, objects, and operators
- Query Store browser with search by identifier, multi-plan history charting, and in-line bar charts
- CLI supports batch processing of .sql file directories with JSON and text output
- Built-in MCP server with 13 tools for plan analysis and Query Store data
- SSMS 18-22 extension adds "Open in Performance Studio" context menu

## Documentation

- [README](https://github.com/erikdarlingdata/PerformanceStudio/blob/main/README.md): Complete documentation including quick start, CLI reference, analysis rules, MCP server setup, and platform support
- [Releases](https://github.com/erikdarlingdata/PerformanceStudio/releases): Download pre-built binaries for Windows, macOS, and Linux
- [Examples](https://github.com/erikdarlingdata/PerformanceStudio/tree/main/examples): Sample queries, plans, and analysis output

## Optional

- [License](https://github.com/erikdarlingdata/PerformanceStudio/blob/main/LICENSE): MIT License
- [Third-party notices](https://github.com/erikdarlingdata/PerformanceStudio/blob/main/THIRD_PARTY_NOTICES.md): License information for bundled components
1 change: 1 addition & 0 deletions src/PlanViewer.App/App.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://PlanViewer.App/Themes/DarkTheme.axaml"/>
<ResourceInclude Source="avares://PlanViewer.App/Themes/BarChartConfig.axaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
Expand Down
28 changes: 28 additions & 0 deletions src/PlanViewer.App/Controls/BarChartCell.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="PlanViewer.App.Controls.BarChartCell">
<Grid RowDefinitions="Auto,4" Margin="2,1">
<!-- Numeric value text -->
<TextBlock x:Name="ValueText"
Grid.Row="0"
FontSize="11"
VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}"/>

<!-- Bar row: background track + coloured fill -->
<Grid Grid.Row="1" ColumnDefinitions="*">
<Border x:Name="BarTrack"
Height="3"
CornerRadius="1"
Background="{DynamicResource BackgroundLightBrush}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"/>
<Border x:Name="BarFill"
Height="3"
CornerRadius="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
MinWidth="0"/>
</Grid>
</Grid>
</UserControl>
140 changes: 140 additions & 0 deletions src/PlanViewer.App/Controls/BarChartCell.axaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
using Avalonia;
using System;
using Avalonia.Controls;
using Avalonia.Media;

namespace PlanViewer.App.Controls;

/// <summary>
/// A DataGrid cell control that shows a text value above a proportional bar.
/// The bar colour is resolved from application resources using the priority:
/// BarChart.&lt;ColumnId&gt; → BarChart.Sorted (when isSorted) → BarChart.Default
/// </summary>
public partial class BarChartCell : UserControl
{
// ── Avalonia properties ────────────────────────────────────────────────

public static readonly StyledProperty<string> DisplayTextProperty =
AvaloniaProperty.Register<BarChartCell, string>(nameof(DisplayText), string.Empty);

/// <summary>Value in [0..1] that controls the bar width.</summary>
public static readonly StyledProperty<double> RatioProperty =
AvaloniaProperty.Register<BarChartCell, double>(nameof(Ratio), 0.0);

/// <summary>Column identifier used to look up per-column brush overrides.</summary>
public static readonly StyledProperty<string> ColumnIdProperty =
AvaloniaProperty.Register<BarChartCell, string>(nameof(ColumnId), string.Empty);

/// <summary>When true the "Sorted" brush is preferred over "Default".</summary>
public static readonly StyledProperty<bool> IsSortedColumnProperty =
AvaloniaProperty.Register<BarChartCell, bool>(nameof(IsSortedColumn), false);

// ── CLR accessors ──────────────────────────────────────────────────────

public string DisplayText
{
get => GetValue(DisplayTextProperty);
set => SetValue(DisplayTextProperty, value);
}

public double Ratio
{
get => GetValue(RatioProperty);
set => SetValue(RatioProperty, value);
}

public string ColumnId
{
get => GetValue(ColumnIdProperty);
set => SetValue(ColumnIdProperty, value);
}

public bool IsSortedColumn
{
get => GetValue(IsSortedColumnProperty);
set => SetValue(IsSortedColumnProperty, value);
}

// ── Constructor ────────────────────────────────────────────────────────

public BarChartCell()
{
InitializeComponent();
}

// ── Property-change overrides ──────────────────────────────────────────

protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);

if (change.Property == DisplayTextProperty)
UpdateText();
else if (change.Property == RatioProperty || change.Property == BoundsProperty)
UpdateBar();
else if (change.Property == ColumnIdProperty || change.Property == IsSortedColumnProperty)
UpdateBarColor();
}

protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
UpdateText();
UpdateBar();
UpdateBarColor();
}

// ── Helpers ────────────────────────────────────────────────────────────

private void UpdateText()
{
if (ValueText != null)
ValueText.Text = DisplayText;
}

private void UpdateBar()
{
if (BarTrack == null || BarFill == null) return;

var ratio = global::System.Math.Max(0.0, global::System.Math.Min(1.0, Ratio));
var trackWidth = BarTrack.Bounds.Width;

// Fall back to control width when layout hasn't run yet
if (trackWidth <= 0)
trackWidth = Bounds.Width - 4; // subtract Margin="2,1" × 2

BarFill.Width = trackWidth > 0 ? ratio * trackWidth : 0;
}

private void UpdateBarColor()
{
if (BarFill == null) return;

// 1. Per-column override
if (!string.IsNullOrEmpty(ColumnId) &&
Application.Current!.TryFindResource($"BarChart.{ColumnId}", out var colRes) &&
colRes is IBrush colBrush)
{
BarFill.Background = colBrush;
return;
}

// 2. Sorted vs Default
var key = IsSortedColumn ? "BarChart.Sorted" : "BarChart.Default";
if (Application.Current!.TryFindResource(key, out var res) && res is IBrush brush)
BarFill.Background = brush;
else
BarFill.Background = IsSortedColumn
? new SolidColorBrush(Color.FromRgb(0x2E, 0xAE, 0xF1))
: new SolidColorBrush(Color.FromRgb(0x3A, 0x6E, 0xA8));
}

// ── Layout override to update bar after measure ─────────────────────--

protected override Size ArrangeOverride(Size finalSize)
{
var result = base.ArrangeOverride(finalSize);
UpdateBar();
return result;
}
}
2 changes: 2 additions & 0 deletions src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,8 @@ private bool HasQueryStoreTab()
.Any(t => t.Content is QueryStoreGridControl);
}

public void TriggerQueryStore() => QueryStore_Click(null, new RoutedEventArgs());

private async void QueryStore_Click(object? sender, RoutedEventArgs e)
{
// If a QS tab already exists, always show connection dialog for a fresh tab
Expand Down
Loading
Loading