MySQL跟蹤SQL&慢查詢分析工具

来源:https://www.cnblogs.com/holdengong/archive/2018/12/13/10115450.html
-Advertisement-
Play Games

簡介 之前的工作一直使用的SQL SERVER, 用過的都知道,SQL SERVER有配套的SQL跟蹤工具SQL Profiler,開發或者定位BUG過程中,可以在操作頁面的時候,實時查看資料庫執行的SQL語句,十分方便。最近的項目使用MySQL,沒有類似的功能,感覺到十分的不爽,網上也沒有找到合適 ...


簡介

之前的工作一直使用的SQL SERVER, 用過的都知道,SQL SERVER有配套的SQL跟蹤工具SQL Profiler,開發或者定位BUG過程中,可以在操作頁面的時候,實時查看資料庫執行的SQL語句,十分方便。最近的項目使用MySQL,沒有類似的功能,感覺到十分的不爽,網上也沒有找到合適的免費工具,所以自己研究做了一個簡單工具。

功能
  • 實時查詢MySql執行的SQL語句
  • 查看性能異常的SQL(執行超過2秒)
技術方案
  • 前端vue,樣式bootstrap
  • 後臺dotnet core mvc

先看一下的效果:

實現原理

Mysql支持輸出日誌,通過以下命令查看當前狀態
  • show VARIABLES like '%general_log%' //是否開啟輸出所有日誌

  • show VARIABLES like '%slow_query_log%' //是否開啟慢SQL日誌
  • show VARIABLES like '%log_output%' //查看日誌輸出方式(預設file,還支持table)
  • show VARIABLES like '%long_query_time%' //查看多少秒定義為慢SQL

下麵我們將所有日誌、慢SQL日誌打開,日誌輸出修改為table,定義執行2秒以上的為慢SQL
  • set global log_output='table' //日誌輸出到table(預設file)
  • set global general_log=on; //打開輸出所有日誌
  • set global slow_query_log=on; //打開慢SQL日誌
  • set global long_query_time=2 //設置2秒以上為慢查詢
  • repair table mysql.general_log //修複日誌表(如果general_log表報錯的情況下執行)

註意:以上的設置,資料庫重啟後將失效,永久改變配置需要修改my.conf文件

現在日誌文件都存在資料庫表裡面了,剩下的工作就是取數並展示出來就行了。本項目後臺使用的MVC取數,然後VUE動態綁定,Bootstrap渲染樣式。
前端代碼
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <title>開發工具</title>
    <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
    <script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
    <script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
    <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
    <script src="https://cdn.staticfile.org/vue-resource/1.5.1/vue-resource.min.js"></script>
</head>
<body>
    <div id="app">

        <ul id="myTab" class="nav nav-tabs">
            <li class="active">
                <a href="#trace" data-toggle="tab">
                    SQL跟蹤
                </a>
            </li>
            <li>
                <a href="#slow" data-toggle="tab">
                    性能異常SQL
                </a>
            </li>
        </ul>

        <hr />
        <div id="myTabContent" class="tab-content">
            <div id="trace" class="tab-pane fade in active">
                <div>
                    &nbsp;&nbsp;&nbsp;&nbsp;<input id="btnStart" class="btn btn-primary" type="button" value="開始" v-show="startShow" v-on:click="start" />
                    &nbsp;&nbsp;&nbsp;&nbsp;<input id="btnPause" class="btn btn-primary" type="button" value="暫停" v-show="pauseShow" v-on:click="pause" />
                    &nbsp;&nbsp;&nbsp;&nbsp;<input id="btnClear" class="btn btn-primary" type="button" value="清空" v-show="clearShow" v-on:click="clear" />
                </div>
                <hr />
                <div class="table-responsive">
                    <table class="table table-striped table-bordered">
                        <thead>
                            <tr>
                                <th>時間</th>
                                <th>執行語句</th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr v-for="log in logs">
                                <td>
                                    {{log.time}}
                                </td>
                                <td>
                                    @*<input class="btn btn-danger" type="button" value="複製" name="copy" />*@
                                    {{log.sql}}
                                </td>
                            </tr>
                        </tbody>
                    </table>
                </div>
            </div>

            <div id="slow" class="tab-pane fade">
                <div class="table-responsive">
                    <table class="table table-striped table-bordered">
                        <thead>
                            <tr>
                                <th>執行時長(時:分:秒,毫秒)</th>
                                <th>鎖定時長(時:分:秒,毫秒)</th>
                                <th>開始時間</th>
                                <th>資料庫</th>
                                <th>操作者</th>
                                <th>執行語句</th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr v-for="query in slowQuerys">
                                <td>
                                    {{query.queryTime}}
                                </td>
                                <td>
                                    @*<input class="btn btn-danger" type="button" value="複製" name="copy" />*@
                                    {{query.lockTime }}
                                </td>
                                <td>
                                    {{query.startTime }}
                                </td>
                                <td>
                                    {{query.db }}
                                </td>
                                <td>
                                    {{query.userHost}}
                                </td>
                                <td>
                                    {{query.sql}}
                                </td>
                            </tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>

        <script>

            new Vue({
                el: '#app',
                data: {
                    startShow: true,
                    pauseShow: false,
                    clearShow: true,
                    logs: [],
                    slowQuerys: []
                },
                methods: {
                    start: function () {
                        this.timer = setInterval(this.trace, 5000);
                        this.pauseShow = true;
                        this.startShow = false;
                    },
                    pause: function () {
                        clearInterval(this.timer);
                        this.pauseShow = false;
                        this.startShow = true;
                    },
                    clear: function () {
                        this.logs = null;
                    },
                    trace: function () {
                        //發送 post 請求
                        this.$http.post('/home/start', {}, { emulateJSON: true }).then(function (res) {
                            this.logs = res.body;
                        }, function (res) {
                            console.log(logs);
                        });
                    }
                },
                created: function () {

                },
                mounted: function () {
                    this.$http.post('/home/slow', {}, { emulateJSON: true }).then(function (res) {
                        this.slowQuerys = res.body;
                    }, function (res) {
                        console.log(this.slowQuerys);
                    });
                },
                destroyed: function () {
                    clearInterval(this.time)
                }
            });

        </script>
</body>
</html>
後端代碼
using Ade.Tools.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using MySql.Data.MySqlClient;
using Microsoft.Extensions.Caching.Memory;

namespace Ade.Tools.Controllers
{
    public class HomeController : Controller
    {
        public IConfiguration Configuration { get; set; }

        public HomeController(IConfiguration configuration)
        {
            this.Configuration = configuration;

            this.ConnStr = Configuration["Sql:DefaultConnection"];
        }

        public static DateTime StartTime { get; set; } = DateTime.MinValue;
        public static List<string> TableNames { get; set; }
        public string ConnStr { get; set; }

        public JsonResult Start()
        {
            if (StartTime == DateTime.MinValue)
            {
                StartTime = DateTime.Now;
            }

            int size = int.Parse(Configuration["Size"]);

            string[] blackList = Configuration["Blacklist"].Split(",");

            List<string> tableNames = GetTableNames();

            List<LogItem> logItems = new List<LogItem>();
            List<LogItemDTO> logItemDTOs = new List<LogItemDTO>();

            using (MySqlConnection mySqlConnection = new MySqlConnection(this.ConnStr))
            {
                //string sqlStart = "set global log_output='table';set global general_log=on; repair table mysql.general_log;";
                //Dapper.SqlMapper.Execute(mySqlConnection, sqlStart);

                logItemDTOs = Dapper.SqlMapper.Query<LogItemDTO>(mySqlConnection, $" select * from mysql.general_log " +
                     $"where event_time>'{StartTime.ToString("yyyy-MM-dd HH:mm:ss")}' " +
                     $"order by event_time desc ")
                     //+ $"limit {size} "
                     .ToList();
            }

            logItemDTOs.ForEach(e =>
            {
                LogItem logItem = new LogItem()
                {
                    Time = e.event_time.ToString("yyyy-MM-dd HH:mm:ss.fff"),
                    CommondType = e.command_type,
                    ServerId = e.server_id,
                    ThreadId = e.thread_id,
                    UserHost = e.user_host,
                    Sql = System.Text.Encoding.Default.GetString(e.argument)
                };

                if (tableNames.Any(a => logItem.Sql.Contains(a))
                && !blackList.Any(b => logItem.Sql.Contains(b))
                )
                {
                    logItems.Add(logItem);
                }
            });


            return new JsonResult(logItems);
        }

        public JsonResult Slow()
        {
            List<SlowQuery> slowQueries = new List<SlowQuery>();
            using (MySqlConnection mySqlConnection = new MySqlConnection(this.ConnStr))
            {
                string sql = "select * from mysql.slow_log order by query_time desc";
                List<SlowQueryDTO> slowDtos = Dapper.SqlMapper.Query<SlowQueryDTO>(mySqlConnection, sql).ToList();

                slowDtos.ForEach(e => {
                    slowQueries.Add(new SlowQuery()
                    {
                        DB = e.db,
                        LockTime = DateTime.Parse(e.lock_time.ToString()).ToString("HH:mm:ss.fffff"),
                        QueryTime = DateTime.Parse(e.query_time.ToString()).ToString("HH:mm:ss.fffff"),
                        RowsExamined = e.rows_examined,
                        RowsSent = e.rows_sent,
                        Sql = System.Text.Encoding.Default.GetString( (byte[])e.sql_text),
                        StartTime = e.start_time.ToString("yyyy-MM-dd HH:mm:ss"),
                        UserHost = e.user_host
                    });
                });

            }

            return new JsonResult(slowQueries);
        }

        public string On()
        {
            using (MySqlConnection mySqlConnection = new MySqlConnection(this.ConnStr))
            {
                string sql = "set global log_output='table';set global general_log=on; repair table mysql.general_log;";
                Dapper.SqlMapper.Execute(mySqlConnection, sql);
            }

            return "ok";
        }

        public string Off()
        {
            using (MySqlConnection mySqlConnection = new MySqlConnection(this.ConnStr))
            {
                string sql = "set global general_log=off;";
                Dapper.SqlMapper.Execute(mySqlConnection, sql);
            }

            return "ok";
        }

        public string Clear()
        {
            using (MySqlConnection mySqlConnection = new MySqlConnection(this.ConnStr))
            {
                string sql = $@"
                                SET GLOBAL general_log = 'OFF';
                                RENAME TABLE general_log TO general_log_temp;
                                DELETE FROM `general_log_temp`;
                                RENAME TABLE general_log_temp TO general_log;
                                SET GLOBAL general_log = 'ON';
                                ";

                Dapper.SqlMapper.Execute(mySqlConnection, sql);
            }

            return "ok";
        }

        public IActionResult Index()
        {
            return View();
        }

        private List<string> GetTableNames()
        {
            MemoryCache memoryCache = new MemoryCache(new MemoryCacheOptions());
            var cacheKey = "MySqlProfile_TableNames";

            List<string> tableNames = memoryCache.Get <List<string>>(cacheKey);

            if (tableNames != null)
            {
                return tableNames;
            }

            string[] traceDbs = Configuration["TraceDatabaseNames"].Split(",");
            string sqlTables = "SELECT distinct TABLE_NAME FROM information_schema.columns";

            foreach (var db in traceDbs)
            {
                if (!sqlTables.Contains("WHERE"))
                {
                    sqlTables += " WHERE table_schema='" + db + "'";
                }
                else
                {
                    sqlTables += " OR table_schema='" + db + "'";
                }
            }

            using (MySqlConnection mySqlConnection = new MySqlConnection(this.ConnStr))
            {
                //  WHERE table_schema='mice'
                tableNames = Dapper.SqlMapper.Query<string>(mySqlConnection, sqlTables).ToList();
            }

            memoryCache.Set(cacheKey, tableNames, TimeSpan.FromMinutes(30));

            return tableNames;
        }
    }
}

源代碼

修改完appsettings.json文件裡面的連接字元串以及其他配置(詳情自己看註釋,懶得寫了),就可以使用了。
https://github.com/holdengong/MysqlProfiler

最後一點

開啟日誌會產生大量的文件,需要註意定時清理

  • SET GLOBAL general_log = 'OFF'; // 關閉日誌
  • RENAME TABLE general_log TO general_log_temp; //表重命名
  • DELETE FROM general_log_temp; //刪除所有數據
  • RENAME TABLE general_log_temp TO general_log; //重命名回來
  • SET GLOBAL general_log = 'ON'; //開啟日誌

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • C# winfrom 寫的一個搜索助手,可以按照標題和內容搜索,指定目錄後,遍歷搜索文件和子目,現在只寫了支持.DOC.DOCX.XLS.XLSX.PPT.PPTX.PDF.HTML.HTM.TXT等格式的文件搜索,讀取execl 內容使用的是NPOI組件,doc,html,txt 格式的使用Str ...
  • 我利用網路上的windows 10 純凈版來進行安裝windows 10 鏡像的時候,發現很多的windows 的服務都是不能用的。比如啟動/刪除 windows 功能就是不能用的,會出現如下信息: 發現出現了:“參照的程式集沒有安裝在系統上” 的錯誤,這應該是,你不小心安裝上精簡版系統造成的,某些 ...
  • 前幾節里,通過幾個案例,分析了各種常見的 CPU 性能問題。通過這些,相信對 CPU 的性能分析已經不再陌生和恐懼,起碼有了基本的思路,也瞭解了不少 CPU 性能的分析工 具。 不過,我猜你可能也碰到了一個我曾有過的困惑: CPU 的性能指標那麼多,CPU 性能分析工具 也是一抓一大把,如果離開專欄 ...
  • 控制台命令 運行redis # ./bin/redis-server ./redis.conf 搜索redis是否運行 # ps -ef | grep -i redis 關閉redis # ./bin/redis-cli shutdown 進入redis # ./bin/redis-cli 127. ...
  • 多表聯合查詢簡介 定義:連接查詢是關係型資料庫最主要的查詢,通過連接運算符可以實現多個表連接數據查詢。 分類:內連接,外連接,全外連接。 內連接 定義 內聯接使用比較運算符根據每個表的通用列中的值匹配兩個表中的行。 語法 select 表1.*,表2.* from 表1 as a [wtih(鎖)] ...
  • 問題描述:主庫備庫之前正常連接,但是昨天磁碟空間滿了之後,由於不知什麼原因將備庫重做日誌刪了,今天早上發現DG不同步的報警。 當時思路如下:1、通過select thread#,low_sequence#,high_sequence# from v$archive_gap; 查看是否有歸檔沒有傳到備 ...
  • 一. Hbase 的 region 我們先簡單介紹下 Hbase 的 架構和 region : 從物理集群的角度看,Hbase 集群中,由一個 Hmaster 管理多個 HRegionServer,其中每個 HRegionServer 都對應一臺物理機器,一臺 HRegionServer 伺服器上又 ...
  • 1 小時 SQL 極速入門 前面兩篇我們從 SQL 的最基礎語法講起,到表聯結多表查詢。 大家可以點擊鏈接查看 "1 小時 SQL 極速入門(一)" "1 小時 SQL 極速入門(二)" 今天我們講一些在做報表和複雜計算時非常實用的分析函數。由於各個資料庫函數的實現不太一樣,本文基於 Oracle ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...