diff --git a/src/SQLInsight.Collectors/Concurrency/ActivityCollector.cs b/src/SQLInsight.Collectors/Concurrency/ActivityCollector.cs
index f2822b8..635b4d3 100644
--- a/src/SQLInsight.Collectors/Concurrency/ActivityCollector.cs
+++ b/src/SQLInsight.Collectors/Concurrency/ActivityCollector.cs
@@ -14,11 +14,12 @@ public sealed class ActivityCollector(int transactionMinutes = 10, int transacti
private const string NotAgent = "s.is_user_process = 1 AND s.session_id <> @@SPID AND (s.program_name IS NULL OR s.program_name NOT LIKE N'SQLInsight Pro%')";
///
- /// One row per open transaction of a user session. The statement is the running one, or else the session's last.
+ /// One row per open transaction of a user session, at most 100: those holding more log than the threshold first, then
+ /// the oldest. The statement is the running one (the first request of a MARS session), or else the session's last.
/// Times are converted to UTC on the server.
///
internal const string TransactionsSql = """
- SELECT t.session_id, t.transaction_id, t.name, DATEADD(minute, DATEDIFF(minute, GETDATE(), GETUTCDATE()), t.begin_time),
+ SELECT TOP 100 t.session_id, t.transaction_id, t.name, DATEADD(minute, DATEDIFF(minute, GETDATE(), GETUTCDATE()), t.begin_time),
t.used, t.reserved, t.db, t.dbs, s.login_name, s.host_name, s.program_name, s.status, r.wait_type,
CAST(CASE WHEN r.sql_handle IS NOT NULL
THEN SUBSTRING(q.text, r.statement_start_offset / 2 + 1,
@@ -32,14 +33,16 @@ public sealed class ActivityCollector(int transactionMinutes = 10, int transacti
LEFT JOIN sys.dm_tran_database_transactions dt ON dt.transaction_id = st.transaction_id AND dt.database_id <> 32767
GROUP BY st.session_id, at.transaction_id) t
JOIN sys.dm_exec_sessions s ON s.session_id = t.session_id
- LEFT JOIN sys.dm_exec_requests r ON r.session_id = t.session_id
+ OUTER APPLY (SELECT TOP 1 x.sql_handle, x.statement_start_offset, x.statement_end_offset, x.wait_type FROM sys.dm_exec_requests x
+ WHERE x.session_id = t.session_id ORDER BY x.request_id) r
OUTER APPLY (SELECT TOP 1 c.most_recent_sql_handle FROM sys.dm_exec_connections c WHERE c.session_id = t.session_id) c
OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, c.most_recent_sql_handle)) q
WHERE {notAgent} AND t.begin_time IS NOT NULL
+ ORDER BY CASE WHEN t.used + t.reserved >= {logBytes} THEN 0 ELSE 1 END, t.begin_time
""";
internal const string LongRequestsSql = """
- SELECT r.session_id, DATEADD(minute, DATEDIFF(minute, GETDATE(), GETUTCDATE()), r.start_time), r.total_elapsed_time, r.command, r.status,
+ SELECT TOP 50 r.session_id, DATEADD(minute, DATEDIFF(minute, GETDATE(), GETUTCDATE()), r.start_time), r.total_elapsed_time, r.command, r.status,
r.wait_type, r.wait_time, DB_NAME(r.database_id), s.login_name, s.host_name, s.program_name, r.blocking_session_id,
CAST(SUBSTRING(q.text, r.statement_start_offset / 2 + 1,
CASE WHEN r.statement_end_offset = -1 THEN 1000 ELSE (r.statement_end_offset - r.statement_start_offset) / 2 + 1 END) AS nvarchar(1000))
@@ -47,6 +50,7 @@ public sealed class ActivityCollector(int transactionMinutes = 10, int transacti
JOIN sys.dm_exec_sessions s ON s.session_id = r.session_id
OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) q
WHERE {notAgent} AND r.total_elapsed_time > {thresholdMs} AND r.percent_complete = 0
+ ORDER BY r.total_elapsed_time DESC
""";
public override string Name => "activity";
@@ -56,7 +60,10 @@ public sealed class ActivityCollector(int transactionMinutes = 10, int transacti
protected override async Task CollectAsync(CollectorContext context, ActivitySnapshot? previous)
{
var ct = context.CancellationToken;
- var transactions = await Db.QueryAsync(context.Connection, TransactionsSql.Replace("{notAgent}", NotAgent, StringComparison.Ordinal), r => new OpenTransaction(
+ var logBytes = (long)transactionLogMegabytes * 1024 * 1024;
+ var transactionsSql = TransactionsSql.Replace("{notAgent}", NotAgent, StringComparison.Ordinal)
+ .Replace("{logBytes}", logBytes.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal);
+ var transactions = await Db.QueryAsync(context.Connection, transactionsSql, r => new OpenTransaction(
r.Int(0), r.Long(1), r.NullableString(2), Utc(r.DateTime(3)), r.Long(4), r.Long(5), r.NullableString(6), r.Int(7),
r.NullableString(8), r.NullableString(9), r.NullableString(10), r.NullableString(11), r.NullableString(12), r.NullableString(13)), ct).ConfigureAwait(false);
var thresholdMs = (long)requestMinutes * 60_000;
diff --git a/src/SQLInsight.Repository/InsightRepository.Activity.cs b/src/SQLInsight.Repository/InsightRepository.Activity.cs
index 505cf80..f3f381a 100644
--- a/src/SQLInsight.Repository/InsightRepository.Activity.cs
+++ b/src/SQLInsight.Repository/InsightRepository.Activity.cs
@@ -17,7 +17,8 @@ public sealed partial class InsightRepository
{
private static async Task SaveActivityAsync(SqlConnection connection, int instanceId, ActivitySnapshot snapshot)
{
- var kept = snapshot.Transactions.Where(t => ActivityAnalysis.IsWorthKeeping(t, snapshot.CapturedAt)).ToList();
+ // One row per transaction: a duplicate in the source would make the MERGE insert the same key twice.
+ var kept = snapshot.Transactions.Where(t => ActivityAnalysis.IsWorthKeeping(t, snapshot.CapturedAt)).DistinctBy(t => (t.SessionId, t.TransactionId)).ToList();
if (kept.Count == 0)
{
return;