Limit results to distinct system name when multiple records returned - sql-server

This is running on SQL Server 2000
Background: when a new server is brought onto the network, it will pass through a series of status (stages) such as (loading, testing, configuring, production, etc) were production is the final step (not every server in the report will be in production so this bit of information may be a moot point).
I inherited this so if there are any questions, I'll answer them as best as I can. I'm trying to run a query to get the latest status of all the servers during a specific time frame. My query is currently returning every status for the server and I only need the current status and that is where I need your help.
The query I'm working with is as follows:
SELECT SD.ProjectName, SD.SystemName, SD.Status, H.history_id
FROM dbo.SI_SystemDetail AS SD
INNER JOIN dbo.SI_Projects AS P ON SD.ProjectName = P.ProjectName
INNER JOIN dbo.SI_StatusHistory AS H ON SD.SystemName = H.SystemName
WHERE
(P.Cancelled = 'N')
AND (P.Platform LIKE '%ibm%')
AND ('20110101' <= CONVERT(varchar(8), H.EffectiveDate, 112))
AND (CONVERT(varchar(8), H.EffectiveDate, 112) <= '20111111')
ORDER BY
H.history_id DESC, SD.SystemName, SD.ContactSBCuid, SD.ActualLiveDAte DESC,
SD.ProjectName, SD.SystemType, H.EffectiveDate`
This will return several duplicates for systemname but I only need one and that should correspond with the highest history_id number. For example, lets say there is a server named:
Server Name Status History_ID
Server01 Loading 1001
Server01 Configuring 1081
Server01 Testing 1101
Server01 Production 1451
Server02 Loading 1002
Server02 Configuring 1083
Server02 Testing 1104
Server02 Failed 1455
I would just need the following results returned:
Server Name Status History_ID
Server01 Production 1451
Server02 Failed 1455
Thanks in advance for the assistance.

UNTESTED:
SELECT SD.ProjectName, SD.SystemName, SD.Status, H.history_id
FROM dbo.SI_SystemDetail AS SD
INNER JOIN dbo.SI_Projects AS P
ON SD.ProjectName = P.ProjectName
INNER JOIN dbo.SI_StatusHistory AS H
ON SD.SystemName = H.SystemName
WHERE (P.Cancelled = 'N') AND (P.Platform LIKE '%ibm%') AND ('20110101' <= CONVERT(varchar(8), H.EffectiveDate, 112)) AND (CONVERT(varchar(8), H.EffectiveDate, 112) <= '20111111')
AND history_ID = (
Select max(history_ID)
FROM SI_Status_History iSH
where iSH.SystemName = SD.SystemName)
ORDER BY H.history_id DESC, SD.SystemName, SD.ContactSBCuid, SD.ActualLiveDAte DESC, SD.ProjectName, SD.SystemType, H.EffectiveDate

Related

SymmetricDS Tables not syncing

I took over a project that uses SymmetricDs to sync tables between a target and a source. The tables from the source aren't syncing to the target at the moment.
I have searched online but found no help.
I have checked the sym_outgoing_batch and sym_incoming_batch tables but can't figure out the use of the information there.
I also queried the sync_trigger table. I have the result of the query as a link below.
If you have an idea on where I could look, please let me know. I can run queries and give you the result.sync_trigger result
Uncomment these lines "--where status != 'OK'" to see if anything is NOT in OK state if there is that is causing the SYNC to STOP
-- SQL QUERY
-- Symmetric DS : MONITOR : HEARTBEAT / INCOMING / OUTGOING / MONITOREVENTS
SELECT node_id, host_name, getdate() as dtNOW ,heartbeat_time FROM [tablename].[dbo].[sd_node_host] with (NOLOCK) where heartbeat_time > '2022-01-01'
--SELECT * FROM [tablename].[dbo].[sd_context] with (NOLOCK)
SELECT * FROM [tablename].[dbo].[sd_outgoing_batch] with (NOLOCK)
--where status != 'OK'
order by create_time desc
SELECT * FROM [tablename].[dbo].[sd_incoming_batch] with (NOLOCK)
--where status != 'OK'
order by create_time desc
-- Symmetric DS : MONITOR
SELECT * FROM [tablename].[dbo].[sd_monitor_event] with (NOLOCK) WHERE is_resolved != 1
SELECT * FROM [tablename].[dbo].[sd_monitor_event] with (NOLOCK)
EDIT
here is the link for the symmetricds user guide.
https://www.symmetricds.org/doc/3.13/html/user-guide.html#_outgoing_batch
basically NE means it is ready for replication. Did you ever have it set up or is this a new setup that you are trying to get started?
EDIT 2 ENGINE CONFIGS
MAIN
engine.name=<SDS_MAIN>
db.driver=net.sourceforge.jtds.jdbc.Driver
db.url=jdbc:jtds:sqlserver://<IP>:1433/<DB>;useCursors=true;bufferMaxMemory=10240;lobBuffer=5242880
db.user=***********
db.password=***********
registration.url=
sync.url=ttp://<IP>:<PORT>/sync/<SDS_MAIN>
group.id=<GID>
external.id=000
auto.registration=true
initial.load.create.first=true
sync.table.prefix=sym
#start.initial.load.extract.job=false
compression.level=-1
compression.strategy=0
CHILD
engine.name=<SDS_CHILD>
db.driver=net.sourceforge.jtds.jdbc.Driver
db.url=jdbc:jtds:sqlserver://<IP>:1433/<DB>;useCursors=true;bufferMaxMemory=10240;lobBuffer=5242880
db.user=***********
db.password=***********
registration.url=http://<IP>:<PORT>/sync/<SDS_MAIN>
sync.url=http://<IP>:<PORT>/sync/<SDS_CHILD>
group.id=<GID>
external.id=100
auto.registration=true
initial.load.create.first=true
sync.table.prefix=sym
start.initial.load.extract.job=false
compression.level=-1
compression.strategy=0

Joins and linked servers query causing delay

Hey guys the code below is taking a really long time. I've been looking at it for quite a while. Is there anything that stands out as the obvious cause of delay?
[SQLSRV-3-JB] is a linked server BTW
Select count(cast (Unique_ID as bigint)) as [Count],
T.[Region_Code],
T.[Region_Name],
U.[Region_Code],
Y.[Examination_Year],
case when [Subject] = 'MUSIC THEORY' THEN 'Theory'
else 'Practical'
end
from [SQLSRV-3-JB].[X].[dbo].[Exam_and_Candidate_Details] Y
left join [SQLSRV-3-JB].[X].[dbo].[UK_Exam_Centre_Info] T
on Y.Centre_Code = T.Centre_Code
left join [SQLSRV-3-JB].[X].[dbo].[UK_Exam_Centres] U
on Y.Centre_Code = U.Centre_Code
where Y.[Examination_Year] between 2010 and 2016
group by Y.[Examination_Year],
T.[Region_Code],
T.[Region_Name],
U.[Region_Code],
case when [Subject] = 'MUSIC THEORY' THEN 'Theory'
else 'Practical'
end
Yes, there is one very obvious problem - the remote server. When you use a linked server like this, SQL Server has a disturbing habit of pulling all of the data in the remote tables to the local server before performing JOINs or filtration.
The correct way to handle this is to make this query a view on the remote server, and query the view with your WHERE clause on your end. So, your remote code would look like this:
-- Remote Server
USE X
GO
CREATE VIEW dbo.ExamCenterInfo
AS
Select count(cast (Unique_ID as bigint)) as [Count],
T.[Region_Code],
T.[Region_Name],
U.[Region_Code],
Y.[Examination_Year],
case when [Subject] = 'MUSIC THEORY' THEN 'Theory'
else 'Practical'
end
from dbo.[Exam_and_Candidate_Details] Y
left join dbo.[UK_Exam_Centre_Info] T
on Y.Centre_Code = T.Centre_Code
left join dbo.[UK_Exam_Centres] U
on Y.Centre_Code = U.Centre_Code
group by Y.[Examination_Year],
T.[Region_Code],
T.[Region_Name],
U.[Region_Code],
case when [Subject] = 'MUSIC THEORY' THEN 'Theory'
else 'Practical'
end
Then your local code:
-- Local Server
SELECT *
FROM [SQLSRV-3-JB].[X].[dbo].ExamCenterInfo ECI
where ECI.[Examination_Year] between 2010 and 2016
NOTE: There is some possibility that this will pull all of the view output before filtering by year. If this is still a problem, you will have to create a more complex mechanism to execute the view with filtration on the remote server.

Automatically stop SQL Server Agent when no jobs are running [duplicate]

I have around 40 different sql server jobs in one instance. They all have different schedules. Some run once a day some every two mins some every five mins. If I have a need to stop sql server agent, how can I find the best time when no jobs are running so I won't interrupt any of my jobs?
how can I find the best time when no jobs are running so I won't interrupt any of my jobs?
You basically want to find a good window to perform some maintenance. #MaxVernon has blogged about it here with a handy script
/*
Shows gaps between agent jobs
-- http://www.sqlserver.science/tools/gaps-between-sql-server-agent-jobs/
-- requires SQL Server 2012+ since it uses the LAG aggregate.
Note: On SQL Server 2005, SQL Server 2008, and SQL Server 2008 R2, you could replace the LastEndDateTime column definition with:
LastEndDateTime = (SELECT TOP(1) s1a.EndDateTime FROM s1 s1a WHERE s1a.rn = s1.rn - 1)
*/
DECLARE #EarliestStartDate DATETIME;
DECLARE #LatestStopDate DATETIME;
SET #EarliestStartDate = DATEADD(DAY, -1, GETDATE());
SET #LatestStopDate = GETDATE();
;WITH s AS
(
SELECT StartDateTime = msdb.dbo.agent_datetime(sjh.run_date, sjh.run_time)
, MaxDuration = MAX(sjh.run_duration)
FROM msdb.dbo.sysjobs sj
INNER JOIN msdb.dbo.sysjobhistory sjh ON sj.job_id = sjh.job_id
WHERE sjh.step_id = 0
AND msdb.dbo.agent_datetime(sjh.run_date, sjh.run_time) >= #EarliestStartDate
AND msdb.dbo.agent_datetime(sjh.run_date, sjh.run_time) < = #LatestStopDate
GROUP BY msdb.dbo.agent_datetime(sjh.run_date, sjh.run_time)
UNION ALL
SELECT StartDate = DATEADD(SECOND, -1, #EarliestStartDate)
, MaxDuration = 1
UNION ALL
SELECT StartDate = #LatestStopDate
, MaxDuration = 1
)
, s1 AS
(
SELECT s.StartDateTime
, EndDateTime = DATEADD(SECOND, s.MaxDuration - ((s.MaxDuration / 100) * 100)
+ (((s.MaxDuration - ((s.MaxDuration / 10000) * 10000))
- (s.MaxDuration - ((s.MaxDuration / 100) * 100))) / 100) * 60
+ (((s.MaxDuration - ((s.MaxDuration / 1000000) * 1000000))
- (s.MaxDuration - ((s.MaxDuration / 10000) * 10000))) / 10000) * 3600, s.StartDateTime)
FROM s
)
, s2 AS
(
SELECT s1.StartDateTime
, s1.EndDateTime
, LastEndDateTime = LAG(s1.EndDateTime) OVER (ORDER BY s1.StartDateTime)
FROM s1
)
SELECT GapStart = CONVERT(DATETIME2(0), s2.LastEndDateTime)
, GapEnd = CONVERT(DATETIME2(0), s2.StartDateTime)
, GapLength = CONVERT(TIME(0), DATEADD(SECOND, DATEDIFF(SECOND, s2.LastEndDateTime, s2.StartDateTime), 0))
FROM s2
WHERE s2.StartDateTime > s2.LastEndDateTime
ORDER BY s2.StartDateTime;
The question title scared me a bit - I thought you wanted to programmatically shut the SQL Server agent down anytime there were no jobs running. My answer to that question would be "Why?" There is no need to.
But if you are just looking to do a planned restart or shut down and you don't have a third party tool like Sentry One's SQL Sentry Event Manager to have a visualization, I would just let the SQL Server Agent Job History and Job Activity Monitor help here. The Job Activity monitor can show you which jobs are running right now in the status column. You can also see the last execute and next execute dates and times.
In the object browser in SSMS, connect to your instance, then expand SQL Server Agent, then you'll see Jobs and under that you'll see "Job Activity Monitor" - this view should show you what you need.
Also - don't worry about shutting down before a job executes. If you do that, you will either have that job just missing its schedule and you can let it run when it is next due to (depending on the job and its purpose) or you can manually right click and execute the job.
For more on the activity monitor for jobs, see Monitor Job Activity in the product documentation.
I recommend creating a script that will disable your jobs. Disabled jobs still exist but will not be automatically launched by their schedules. Run this script (based on procedure sp_update_job in the msdb database) to disable jobs, wait for any currently running jobs to finish execution, then stop SQL agent. A similar script to re-enable disabled jobs would be useful. You might need to plan around jobs that are and should remain disabled.
A complete “SQL Agent shutdown” process could be fully scripted, but I question the wisdom of doing so. A bit of research implies that there is no 100% reliable way of programmatically telling if a given job is or is not running, and while there is an undocumented (where "undocumented" means "you really shouldn't be using this") system procedure for stopping and starting services, doing so from with SQL Server itself seems like a pretty bad idea.
You can query the system tables as shown by Dattatrey Sindol in the MSSQLTips.com article Querying SQL Server Agent Job Information:
SELECT
[sJOB].[job_id] AS [JobID]
, [sJOB].[name] AS [JobName]
, [sDBP].[name] AS [JobOwner]
, [sCAT].[name] AS [JobCategory]
, [sJOB].[description] AS [JobDescription]
, CASE [sJOB].[enabled]
WHEN 1 THEN 'Yes'
WHEN 0 THEN 'No'
END AS [IsEnabled]
, [sJOB].[date_created] AS [JobCreatedOn]
, [sJOB].[date_modified] AS [JobLastModifiedOn]
, [sSVR].[name] AS [OriginatingServerName]
, [sJSTP].[step_id] AS [JobStartStepNo]
, [sJSTP].[step_name] AS [JobStartStepName]
, CASE
WHEN [sSCH].[schedule_uid] IS NULL THEN 'No'
ELSE 'Yes'
END AS [IsScheduled]
, [sSCH].[schedule_uid] AS [JobScheduleID]
, [sSCH].[name] AS [JobScheduleName]
, CASE [sJOB].[delete_level]
WHEN 0 THEN 'Never'
WHEN 1 THEN 'On Success'
WHEN 2 THEN 'On Failure'
WHEN 3 THEN 'On Completion'
END AS [JobDeletionCriterion]
FROM
[msdb].[dbo].[sysjobs] AS [sJOB]
LEFT JOIN [msdb].[sys].[servers] AS [sSVR]
ON [sJOB].[originating_server_id] = [sSVR].[server_id]
LEFT JOIN [msdb].[dbo].[syscategories] AS [sCAT]
ON [sJOB].[category_id] = [sCAT].[category_id]
LEFT JOIN [msdb].[dbo].[sysjobsteps] AS [sJSTP]
ON [sJOB].[job_id] = [sJSTP].[job_id]
AND [sJOB].[start_step_id] = [sJSTP].[step_id]
LEFT JOIN [msdb].[sys].[database_principals] AS [sDBP]
ON [sJOB].[owner_sid] = [sDBP].[sid]
LEFT JOIN [msdb].[dbo].[sysjobschedules] AS [sJOBSCH]
ON [sJOB].[job_id] = [sJOBSCH].[job_id]
LEFT JOIN [msdb].[dbo].[sysschedules] AS [sSCH]
ON [sJOBSCH].[schedule_id] = [sSCH].[schedule_id]
ORDER BY [JobName]

How can I get an accurate count of computers logged in to my SQL Server database?

We license our software by number of workstations.
I have a query that I have used for years to get an accurate count of the workstations logged in to my SQL Server database. For simplicity, all users use the same login name/password. This is built in to the script that attaches to the DB. They have access only to that DB with the exception of
USE [Master] GRANT VIEW SERVER STATE to MyUser
The query that has been working is below:
SELECT COUNT(Users) AS UserCount FROM (SELECT COUNT(Master.dbo.sysprocesses.hostname) AS Users FROM Master.dbo.sysprocesses LEFT OUTER JOIN Master.dbo.sysdatabases ON Master.dbo.sysdatabases.dbid = Master.dbo.sysprocesses.dbid WHERE (Master.dbo.sysdatabases.name = 'MyDatabase') GROUP BY Master.dbo.sysprocesses.net_address) AS UserCount_1
Basically this relies on the mac address (Master.dbo.sysprocesses.net_address), since both Workstation names and ip addresses can be duplicated.
Recently this has stopped working at a number of customers. Suddenly individual workstations are showing multiple net addresses for the same workstation causing a substantial overcount of users. This may be related to SQL Server 2012 - not sure.
What I need is a very reliable way to get a count of workstations logged in to my database.
If anyone can tell me why I am suddenly getting multiple net_addresses for each workstation and how to prevent that that would be one possible solution.
Otherwise if anyone can give me a rock solid way to get a workstation count other than the above that would be great. Our largest customer is 50 users by the way.
HERE IS AN EXAMPLE:
SELECT Master.dbo.sysprocesses.hostname AS Users, Master.dbo.sysprocesses.net_address FROM Master.dbo.sysprocesses
LEFT OUTER JOIN Master.dbo.sysdatabases ON Master.dbo.sysdatabases.dbid = Master.dbo.sysprocesses.dbid
WHERE Master.dbo.sysdatabases.name = 'mydb' GROUP BY Master.dbo.sysprocesses.hostname, Master.dbo.sysprocesses.net_address
RETURNS:
DAVID-PC 001CC490239E
FLOOR1 001CC41D8012
FLOOR2 CB8FEE6C5856
FLOOR3 A50B18FF1516
KER-PC7 6C626DEA68CC
LIZ-PC A4E553460E35
LIZ-PC EFE3F0E20260
LIZ-PC FD32F7B30360
PAP 9D35A704C29C
PAP CFB724BA1183
PAP D1A58A8878E6
PAP E9B116CA34B8
PAP F38B335A7AE6
Thanks in advance for any help.
You can get an accurate count of the users logged in to SQL Server database by the following query:
SELECT
DB_NAME(dbid) as DB
COUNT(dbid) as Numb
loginame as LoginNa
FROM
sys.sysprocesses
WHERE
dbid > 0
GROUP BY
dbid, loginame
Also you can get full details of connected users by this:
sp_who2 'Active'
Here is a way that will give you the number of IPs or MAC addresses connected to a given database. I'd go with IP as you might have multiple NICs/MACs in a given high end workstation.
SELECT COUNT(DISTINCT c.client_net_address) as DistinctIPCount,
COUNT(DISTINCT p.net_address) as DistinctMACCount
FROM sys.dm_exec_connections c INNER JOIN sys.dm_exec_sessions s ON c.session_id = s.session_id
INNER JOIN sysprocesses p ON s.session_id = p.spid
WHERE s.is_user_process = 1
AND p.dbid = DB_ID('yourdbnamehere')

Multiple tables condensed to one row with various totals

We have Applications, which run on Hosts, which have Software on them.
An Application can run on many Hosts. A Host typically has many Software items loaded onto it.
We capture this information with an Application table, and two more tables to capture the relationships, Application-Host, Host-Software.
It is possible, however, that a Host does not have Software. (It might be genuinely empty or we might not have information yet, both need to be highlighted.)
For each Application, I need to count the Hosts having software on them. I cannot find a way to do it.
Assume an Application has 5 Hosts, four of which are connected to Software. One relationship table contains 5 Hosts, the other many instances of 4 Hosts, but the 4 are indirectly connected through the first relationship table.
How can I get the correct answer, 4? Whatever I do I get 5, or the total number of Software items on all hosts.
This is what I have so far, including debug code.
select distinct
AH.APPLICATION_X_COMPONENT_NAME,
count(case when T.TECH_ITEM_REL_ITCM_CAT = 'Operating System' then 1 end) over (partition by AH.APPLICATION_X_COMPONENT_NAME) as NoOfOpsys,
x.APPLICATION_X_COMPONENT_NAME,
x.HOST_COMPONENT_NAME,
x.NoHostsWithTIR
from dbo.CTO_TechnologyItemRelease as T
inner join dbo.CTOR_HOST_TECHITEMRELEASE as HT
on HT.TECH_ITEM_RELEASE_COMP_ID = T.TechnologyItemReleaseComponent
inner join dbo.CTOR_APPLICATIONX_HOST as AH
on AH.HOST_COMPONENT_ID = HT.HOST_COMPONENT_ID
inner join
(
select distinct
AH2.APPLICATION_X_COMPONENT_NAME,
AH2.HOST_COMPONENT_NAME,
count(case when HT2.HOST_COMPONENT_NAME is not null then 1 end) over (partition by AH2.APPLICATION_X_COMPONENT_NAME, HT2.HOST_COMPONENT_NAME) as NoHostsWithTIR
from dbo.CTOR_APPLICATIONX_HOST as AH2
inner join dbo.CTOR_HOST_TECHITEMRELEASE as HT2
on HT2.HOST_COMPONENT_NAME = AH2.HOST_COMPONENT_NAME
)
as x on x.APPLICATION_X_COMPONENT_NAME = AH.APPLICATION_X_COMPONENT_NAME
order by AH.APPLICATION_X_COMPONENT_NAME
Data:
App table
App_ID App_Name
A0001 Application_1
A0002 Application_2
A0003 Application_3
A0004 Application_4
App-Host table
App_ID App_Name Host_ID Host_Name
A0001 Application_1 H0001 Host_1
A0002 Application_2 H0001 Host_1
A0002 Application_2 H0002 Host_2
A0002 Application_2 H0003 Host_3
A0002 Application_2 H0004 Host_4
A0003 Application_3 H0005 Host_5
A0004 Application_4 H0002 Host_2
A0004 Application_4 H0006 Host_6
Host-TI table
Host_ID Host_Name TI_ID TI_Name
H0001 Host_1 T0001 MS SQL Server 2005 SP1
H0001 Host_1 T0002 MS Windows Server 2008
H0002 Host_2 T0002 Red Hat Enterprise Linux 3
H0003 Host_3 T0002 MS Windows Server 2008
H0003 Host_3 T0003 Oracle Database Server 9i 9.2
H0005 Host_5 T0001 MS SQL Server 2005 SP1
H0006 Host_6 T0004 Tivoli Storage Manager 5.2
TI table
TI_ID TI_Name TI_Type
T0001 MS SQL Server 2005 SP1 Software Product
T0002 MS Windows Server 2008 Operating System
T0003 Red Hat Enterprise Linux 3 Operating System
T0003 Oracle Database Server 9i 9.2 Software Product
T0004 Tivoli Storage Manager 5.2 Software Product
The required output
App Name Operating System count Hosts with Tech Items
Application_1 1 1
Application_2 3 3
Application_3 0 1
Application_4 1 2
The crucial line is Application_2 having 3 hosts with tech items. I can only get a 4 in this position and my Operating System count goes on the blink frequently.
The database design seems seriously fubar'd, so the data appear to be inconsistent:
These rows appear to contradict one another:
H0001 Host_1 T0002 MS Windows Server 2008
H0002 Host_2 T0002 Red Hat Enterprise Linux 3
as do these:
T0003 Red Hat Enterprise Linux 3 Operating System
T0003 Oracle Database Server 9i 9.2 Software Product
I'm gonna assume that the data is invalid, and that the correct data is along the lines in the SQL Fiddle.
Under that assumption this code will get you what you need:
select a.app_ID, a.app_name,
(select Count(*)
from apphost ah
inner join hostti ht on ah.host_id = ht.host_id
inner join ti on ht.ti_id=ti.ti_id
where ti_type = 'Operating System'
and ah.app_id = a.app_id) as OSCount,
(select Count(distinct ah.host_id)
from apphost ah
inner join hostti ht on ah.host_id = ht.host_id
where ah.app_id = a.app_id) as HostWithSoftwareCount
from App a
Something like this:
select a.name, COUNT(*)
from application a
join application_host ah on (a.id = ah.application)
join host h on (h.id = ah.host)
where exists (select top 1 1 from host_software hs where hs.host = ah.host)
group by a.name
http://sqlfiddle.com/#!2/c4bbb/2
I agree with #SWeko on the point of inconsistency in your data samples and here's also my take on the problem:
SELECT
ap.App_Name,
OperatingSystemCount = COUNT(ti.TI_ID),
HostsWithTechItems = COUNT(DISTINCT ah.Host_ID)
FROM
App AS ap
LEFT JOIN
AppHost AS ah
INNER JOIN HostTI AS ht ON ah.Host_ID = ht.Host_ID
LEFT JOIN TI AS ti ON ht.TI_ID = ti.TI_ID AND ti.TI_Type = 'Operating System'
ON ap.App_ID = ah.App_ID
GROUP BY
ap.App_Name
;
A SQL Fiddle demo for this query can be found here: http://sqlfiddle.com/#!3/677ae/3
Note that when counting operating systems, the query doesn't recognise the fact that some of them may be installed on the same host: in that case, each instance would be counted. If you actually meant to count hosts with operating systems instead, you would need to change the above query by replacing COUNT(ti.TI_ID) with something like
COUNT(DISTINCT CASE WHEN ti.TI_ID IS NOT NULL THEN ah.Host_ID END)

Resources