2025-11-02
Gather the Usage and Size of all Indexes across every database
2025-10-03
Assessing the Latency of Availability Group Database Synchronization
The SQL script provided below can be utilized to assess the real-time latency in seconds of SQL Server AlwaysOn synchronization.
;WITH
AG_Stats AS
(
SELECT
AR.replica_server_name,
AG.name AS AGName,
HARS.role_desc,
DB_NAME(DRS.database_id) AS DBName,
DRS.last_commit_time
FROM sys.dm_hadr_database_replica_states DRS
INNER JOIN sys.availability_replicas AR ON DRS.replica_id = AR.replica_id
INNER JOIN sys.dm_hadr_availability_replica_states HARS
ON AR.group_id = HARS.group_id AND AR.replica_id = HARS.replica_id
INNER JOIN sys.availability_groups AG ON AG.group_id = AR.group_id
),
Pri_CommitTime AS
(
SELECT
replica_server_name,
AGName,
DBName,
last_commit_time
FROM AG_Stats
WHERE role_desc = 'PRIMARY'
),
Sec_CommitTime AS
(
SELECT
replica_server_name,
AGName,
DBName,
last_commit_time
FROM AG_Stats
WHERE role_desc = 'SECONDARY'
)
SELECT
p.replica_server_name AS PrimaryReplica,
p.AGName,
p.DBName AS DatabaseName,
s.replica_server_name AS SecondaryReplica,
DATEDIFF(SECOND, s.last_commit_time, p.last_commit_time) AS Sync_Latency_Secs
FROM Pri_CommitTime p
LEFT JOIN Sec_CommitTime s
ON s.DBName = p.DBName AND s.AGName = p.AGName;
Example output as shown below:
2025-09-14
Snapshot Replication Agent Profile's Parameter BcpBatchSize
This week, a customer of mine shared the image below, mentioning that their Snapshot Replication is experiencing bad performance.
The customer informed me that the snapshot replication was operating correctly. I then inquired whether the tables being replicated had increased in size, to which they responded affirmatively. Consequently, I recommended that they utilize a smaller BcpBatchSize value in the Snapshot Agent Profile. I recommended that they reduce the BcpBatchSize setting from its default value of 100000 to 50000 rows. This can be done by creating a new agent profile derived from the default profile, adjusting the BcpBatchSize value to 50000, and subsequently assigning this new profile to the Snapshot Publication.
2025-08-04
Streamline the MSSQL High Availability to improve business continuity
This short document proposes an exploration into the possibility of refining the SQL Server high availability architecture to improve business continuity.
The issue we aim to address
Since March 2025,
the Production SQL Server has faced outages 2 times. DBA team determined that
the cause was the Windows Server Failover Cluster (WSFC) losing its quorum,
which resulted in both the primary SQL availability replica in the US and the
secondary SQL availability replica in the DK site being in a
"Resolving" state, rendering them inaccessible for both reading and
writing. The DBA team cannot fix this issue alone and needs help from the IT
team to recover the WSFC quorum.
Current
architecture for production SQL Server High Availability
Displayed below is
the current WSFC architecture of the production SQL Server.
To mitigate the performance effects of network latency
between the US and DK sites, the primary SQL replica and the secondary replica
are configured to synchronize data in Asynchronous-Commit mode. This
configuration limits the SQL availability group to Manual Failover only,
where Automatic Failover is not supported, a fact that should already be known
and accepted by us.
The table below has been taken from Microsoft
Online regarding WSFC Quorum:
Based on the information provided by Microsoft, we can
derive the following disaster recovery scenarios for our existing
configuration:
|
Primary Server |
Cloud Witness |
Secondary Server |
Database accessibility within the availability group |
|
Up |
Up |
Up |
The primary database server allows
read and write access. |
|
Up |
Up |
Down |
The primary database server allows
read and write access. |
|
Up |
Down |
Up |
The primary database server allows
read and write access. |
|
Up |
Down |
Down |
According to dynamic quorum behavior of WSFC, if the
secondary server and the witness are taken down one at a time, the quorum
continues to exist, and the primary database server is still accessible.
However, if both the secondary server and the witness go down simultaneously
from the primary server's perspective—imagine the internet connection to both
is severed—the databases in the primary server's availability group will
become inaccessible, and immediate IT support is needed to recover the quorum.
If the secondary server is connected to the witness, we might be able to reinstate
the database services on the secondary server through a forced manual
failover. |
|
Down |
Up |
Up |
DBA must conduct a manual failover
to the secondary server to resume read-write access to the databases in availability
group. |
|
Down |
Down |
Up |
Quorum is lost; the databases are
inaccessible, and immediate IT support is needed to recover the quorum. |
The new architecture we present aims to tackle the highlighted
scenarios.
The proposed architecture – Clusterless Availability
Group
SQL Server 2017 introduces read-scale
availability groups without a cluster.
In the same way as a conventional clustered availability group, a Read-Scale
availability group (often referred to as Clusterless; the term Read-Scale is
merely a marketing label by Microsoft) allows for data synchronization between
primary and secondary replicas to be configured in either Synchronous-Commit
mode or Asynchronous-Commit mode at any time without causing service
interruptions. A clusterless availability group only allows for manual
failover, and does not support automatic failover. Using a clusterless
availability group offers the benefit of removing the necessity for WSFC quorum
and the cloud witness, thereby simplifying maintenance. Moreover, the disaster
recovery scenarios for our SQL Servers will be less complex as described below:
|
Primary Server |
Secondary Server |
Database accessibility within the
availability group |
|
Up |
Up |
Primary database server is
Read-Write accessible. |
|
Up |
Down |
Primary database server is
Read-Write accessible. |
|
Down |
Up |
DBA must conduct a Manual Failover
to the Secondary Server to resume Read-Write access to the databases in
Availability Group. |
Following Actions
DBA team is looking to obtain a new set of SQL Server VMs,
one in the US and the other in DK, to set up a lab environment for assessing
the clusterless availability group, focusing on disaster recovery and data
integrity. After we secure satisfactory testing results, we can develop a
migration plan for the clusterless architecture or include it in the
forthcoming SQL Server upgrade initiative (upgrading from version 2019 to
2022/2025).
2025-07-05
Out of Sync status for the AlwaysOn Availability Group Replica
Last week, a customer mentioned to me that their MSSQL server is running slowly. I verified that the CPU usage of the SQL Server was low, and the memory showed a high Page Life Expectancy value. This application is a vendor package software, with a low workload. Before informing the customer about the health of their SQL Server, I executed Paul Randal's Wait Statistics script. This script indicates that the primary wait type is HADR_SYNC_COMMIT, which signifies that the server is waiting for transaction commit processing for the synchronized secondary databases to harden the log. The screenshot below indicates that 75% of the wait time is attributed to HADR_SYNC_COMMIT.
2025-06-05
Obtain all permissions and access rights for every user in the database
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRAN
/*
Security Audit Report
1) List all access provisioned to a sql user or windows user/group directly
2) List all access provisioned to a sql user or windows user/group through a database or application role
3) List all access provisioned to the public role
Columns Returned:
UserName : SQL or Windows/Active Directory user account. This could also be an Active Directory group.
UserType : Value will be either 'SQL User' or 'Windows User'. This reflects the type of user defined for the
SQL Server user account.
DatabaseUserName: Name of the associated user as defined in the database user account. The database user may not be the
same as the server user.
Role : The role name. This will be null if the associated permissions to the object are defined at directly
on the user account, otherwise this will be the name of the role that the user is a member of.
PermissionType : Type of permissions the user/role has on an object. Examples could include CONNECT, EXECUTE, SELECT
DELETE, INSERT, ALTER, CONTROL, TAKE OWNERSHIP, VIEW DEFINITION, etc.
This value may not be populated for all roles. Some built in roles have implicit permission
definitions.
PermissionState : Reflects the state of the permission type, examples could include GRANT, DENY, etc.
This value may not be populated for all roles. Some built in roles have implicit permission
definitions.
ObjectType : Type of object the user/role is assigned permissions on. Examples could include USER_TABLE,
SQL_SCALAR_FUNCTION, SQL_INLINE_TABLE_VALUED_FUNCTION, SQL_STORED_PROCEDURE, VIEW, etc.
This value may not be populated for all roles. Some built in roles have implicit permission
definitions.
ObjectName : Name of the object that the user/role is assigned permissions on.
This value may not be populated for all roles. Some built in roles have implicit permission
definitions.
ColumnName : Name of the column of the object that the user/role is assigned permissions on. This value
is only populated if the object is a table, view or a table value function.
*/
--List all access provisioned to a sql user or windows user/group directly
SELECT DB_NAME() AS DB,
[UserName] = ulogin.[name],
[UserType] =ulogin.type_desc,
[DatabaseUserName] = princ.[name],
[Role] = null,
[PermissionType] = perm.[permission_name],
[PermissionState] = perm.[state_desc],
[ObjectType] = obj.type_desc,--perm.[class_desc],
[ObjectName] = OBJECT_NAME(perm.major_id),
[ColumnName] = col.[name]
FROM
--database user
sys.database_principals princ
LEFT JOIN
--Login accounts
sys.server_principals ulogin on princ.[sid] = ulogin.[sid]
LEFT JOIN
--Permissions
sys.database_permissions perm ON perm.[grantee_principal_id] = princ.[principal_id]
LEFT JOIN
--Table columns
sys.columns col ON col.[object_id] = perm.major_id
AND col.[column_id] = perm.[minor_id]
LEFT JOIN
sys.objects obj ON perm.[major_id] = obj.[object_id]
WHERE
princ.[type] in ('S','U')
UNION
--List all access provisioned to a sql user or windows user/group through a database or application role
SELECT DB_NAME() AS DB,
[UserName] = ulogin.[name],
[UserType] =ulogin.type_desc,
[DatabaseUserName] = memberprinc.[name],
[Role] = roleprinc.[name],
[PermissionType] = perm.[permission_name],
[PermissionState] = perm.[state_desc],
[ObjectType] = obj.type_desc,--perm.[class_desc],
[ObjectName] = OBJECT_NAME(perm.major_id),
[ColumnName] = col.[name]
FROM
--Role/member associations
sys.database_role_members members
JOIN
--Roles
sys.database_principals roleprinc ON roleprinc.[principal_id] = members.[role_principal_id]
JOIN
--Role members (database users)
sys.database_principals memberprinc ON memberprinc.[principal_id] = members.[member_principal_id]
LEFT JOIN
--Login accounts
sys.server_principals ulogin on memberprinc.[sid] = ulogin.[sid]
LEFT JOIN
--Permissions
sys.database_permissions perm ON perm.[grantee_principal_id] = roleprinc.[principal_id]
LEFT JOIN
--Table columns
sys.columns col on col.[object_id] = perm.major_id
AND col.[column_id] = perm.[minor_id]
LEFT JOIN
sys.objects obj ON perm.[major_id] = obj.[object_id]
UNION
--List all access provisioned to the public role, which everyone gets by default
SELECT DB_NAME() AS DB,
[UserName] = '{All Users}',
[UserType] = '{All Users}',
[DatabaseUserName] = '{All Users}',
[Role] = roleprinc.[name],
[PermissionType] = perm.[permission_name],
[PermissionState] = perm.[state_desc],
[ObjectType] = obj.type_desc,--perm.[class_desc],
[ObjectName] = OBJECT_NAME(perm.major_id),
[ColumnName] = col.[name]
FROM
--Roles
sys.database_principals roleprinc
LEFT JOIN
--Role permissions
sys.database_permissions perm ON perm.[grantee_principal_id] = roleprinc.[principal_id]
LEFT JOIN
--Table columns
sys.columns col on col.[object_id] = perm.major_id
AND col.[column_id] = perm.[minor_id]
JOIN
--All objects
sys.objects obj ON obj.[object_id] = perm.[major_id]
WHERE
--Only roles
roleprinc.[type] = 'R' AND
--Only public role
roleprinc.[name] = 'public' AND
--Only objects of ours, not the MS objects
obj.is_ms_shipped = 0
ORDER BY
princ.[Name],
OBJECT_NAME(perm.major_id),
col.[name],
perm.[permission_name],
perm.[state_desc],
obj.type_desc--perm.[class_desc]
ROLLBACK
2025-05-07
Comparing Agent Jobs among SQL Server instances utilizing PowerShell
Utilizing dbatools along with the Compare-Object PowerShell cmdlet allows for the comparison of SQL Server Agent jobs across two SQL Server instances.
Set-ExecutionPolicy Unrestricted
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
Import-Module dbatools
Set-DbatoolsConfig -FullName sql.connection.trustcert -Value $true -Register
Set-DbatoolsConfig -FullName sql.connection.encrypt -Value $false -Register
$RefServer = Get-DbaAgentJob -SqlInstance REFERENCE_SQL -SqlCredential sa
$DiffServer = Get-DbaAgentJob -SqlInstance DIFFERENCE_SQL -SqlCredential sa
Compare-Object $RefServer $DiffServer
The result of the comparison indicates whether a property value appeared only in the reference object (<=) or only in the difference object (=>).





