SQL Server: How to calculate the percentage of a table through a join using multiple tables? - sql

I need to find the percentage of assets that have work order tickets that are open. The following shows the structure of the tables with some dummy data.
Assets Table
+---------------------+
| Asset_Number_PK |
+---------------------+
| 56412 |
| 56413 |
| 56414 |
| 56415 |
+---------------------+
Statuses Table
+-------------------++-------------------+
| Open_Number || Closed_Number |
+-------------------++-------------------+
| 5 || 8 |
| 2 || 7 |
| 9 || 11 |
+-------------------++-------------------+
Work_Orders Table
+-------------------++-------------------++-------------------+
| WO_Status_Number || WO_Description || Asset_number |
+-------------------++-------------------++-------------------+
| 5 || Fix air handler || 56415 |
| 5 || Replace chiller || 56412 |
| 5 || 2 fans 2nd fl || 56414 |
| 7 || 4 fans 2nd fl || 56414 |
| 7 || Fix Air duct || 56413 |
+-------------------++-------------------++-------------------+
I've tried the following two queries to calculate the percentage but my output is incorrect:
sql = "SELECT DISTINCT COUNT(Asset_Number_PK) AS tol_open_assets "
sql = sql & "FROM Assets WHERE Asset_Number_PK IN (SELECT a.Asset_Number "
sql = sql & "FROM Work_Orders a, Statuses b, Assets c "
sql = sql & "WHERE a.WO_Status_Number = b.Open_Number AND a.Asset_Number = c.Asset_Number_PK)"
myRs.Open sql,con,1,2
if myRs.eof then
tol_open_assets = 0
else
tol_open_assets = myRs("tol_open_assets")
end if
myRs.Close
sql = "SELECT COUNT(Asset_Number_PK) AS tol_assets FROM Assets"
myRs.Open sql,con,1,2
tol_assets = myRs("tol_assets")
myRs.Close
if tol_assets = 0 then
tol_asset_percentage = 0
else
tol_asset_percentage = round(100*tol_open_assets/tol_assets,0)
end if
response.write(tol_asset_percentage)

Your result (75 based on your data) appears correct.
You could determine the percentage of open assets in a single query as well:
SELECT
PercentOpen = SUM(COALESCE(openOrder.IsOpen, 0)) / COUNT(*) * 100
FROM Assets a
OUTER APPLY (
SELECT TOP 1 IsOpen = CAST(1 AS DECIMAL(10,2))
FROM Work_Orders wo
JOIN Statuses s ON wo.WO_Status_Number = s.Open_Number
WHERE wo.Asset_Number = a.Asset_Number_PK
) openOrder
SQL Fiddle

Related

SQL: SUM on Aggregate columns

I have this query:
SELECT
spend_codes.spend_code,
SUM(orders.shipping_cost + orders.sales_tax + orders.manual_total + orders.fees) as order_total,
SUM(ordered_items.fees + ordered_items.price * ordered_items.quantity) as item_total
FROM spend_codeables
LEFT JOIN spend_codes
ON spend_codeables.spend_code_id = spend_codes.id
LEFT JOIN orders
ON spend_codeables.spend_codeable_id = orders.id AND spend_codeables.spend_codeable_type = 'App\Order'
LEFT JOIN ordered_items
ON spend_codeables.spend_codeable_id = ordered_items.id AND spend_codeables.spend_codeable_type = 'App\OrderedItem'
GROUP BY
spend_codes.spend_code;
Which has this result:
+--------------+---------------+--------------+
| spend_code | order_total | item_total |
|--------------+---------------+--------------|
| 1230131391 | $362.00 | <null> |
| A12345 | <null> | <null> |
| B29393 | <null> | $374.28 |
+--------------+---------------+--------------+
However, I'd like to add order_total and item_total in order to get just total.
So I'd expect this:
+--------------+---------------+
| spend_code | total |
|--------------+---------------+
| 1230131391 | $362.00 |
| A12345 | <null> |
| B29393 | $374.28 |
+--------------+---------------+
Doing this did not work:
SUM(orders.shipping_cost + orders.sales_tax + orders.manual_total + orders.fees) +
SUM(ordered_items.fees + ordered_items.price * ordered_items.quantity) as total
Another monkeywrench is that the type of the numbers is money not integer.
Anyone would be able to help?
If any of the 2 sums returns NULL then the result of the sum of the sums will also be NULL because NULL + anything returns NULL.
Use SUM() only once:
SUM(
orders.shipping_cost +
orders.sales_tax +
orders.manual_total +
orders.fees +
ordered_items.fees +
ordered_items.price * ordered_items.quantity
) as total
If any of the columns involved may also be NULL use COALESCE() like COALESCE(orders.shipping_cost, 0::money)
you do it in next step using sub query and I used coalesce() for avoid null
with cte as ( SELECT
spend_codes.spend_code,
SUM(orders.shipping_cost + orders.sales_tax + orders.manual_total + orders.fees) as order_total,
SUM(ordered_items.fees + ordered_items.price * ordered_items.quantity) as item_total
FROM spend_codeables
LEFT JOIN spend_codes
ON spend_codeables.spend_code_id = spend_codes.id
LEFT JOIN orders
ON spend_codeables.spend_codeable_id = orders.id AND spend_codeables.spend_codeable_type = 'App\Order'
LEFT JOIN ordered_items
ON spend_codeables.spend_codeable_id = ordered_items.id AND spend_codeables.spend_codeable_type = 'App\OrderedItem'
GROUP BY
spend_codes.spend_code
) select spend_code,coalesce(order_total,0)+coalesce(item_total,0) as total from cte

How to optimize the SQL query with the subquery?

I want to improve the performance of a SQL query. I have the table 'tblEntries' with the column 'sTag':
+----+------+-------+------+---------+
| Id | sTag | sPath | lVer | bActive |
+====+======+=======+======+=========+
| 1 | NULL | t1 | 100 | + |
| 2 | NULL | t2 | 110 | + |
| 3 | x1 | t4 | 110 | + |
| 4 | x1 | t3 | 120 | + |
| 5 | x2 | t7 | 100 | + |
+----+------+-------+------+---------+
A client queries for the path with the specified tag and the query should return a specified entry with the next condition:
If there is an entry with the specified tag it should returns the
entry with the maximum lVer value and bActive should be TRUE.
If
there is no entry with the specified tag it should returns the entry
with the NULL sTag value and with the maximum lVer value and bActive
should be TRUE.
The "tagged" entry has the more priority over "non-tagged" one.
The current SQL query is:
SELECT lVer, sPath
FROM tblEntries
INNER JOIN
(SELECT MAX(lVer) AS V, sTag AS T
FROM tblEntries
WHERE bActive = TRUE
GROUP BY sTag)
ON lVer = V
WHERE T IS NULL OR T = 'user_tag'
ORDER BY T DESC
Then i can select the first entry which satisfies the conditions.
Can i avoid the subquery?
Thanks!
Depending on your data and database, this might have sufficient performance:
select top (1) e.*
from tblEntries e
where e.bActive = TRUE and
(e.t IS NULL OR e.t = 'user_tag')
order by (e.t desc), -- null values last
t.lver desc;
If speed is really an issue and you have an index on (t, active, lver desc), this might be a bit faster:
(select top (1) e.*
from tblEntries e
where e.t = 'user_tag' and e.bActive = TRUE
order by e.lver desc
) union all
(select top (1) e.*
from tblEntries e
where e.t is null and e.bActive = TRUE and
not exists (select 1 from tblEntries e2 where e2.t = 'user_tag' and e2.bActive = TRUE )
order by e.lver desc
);

SQL records with common ID - update all with single user defined function call

ClientInfo Table
------------------------------------------------------------
||ClientInfoID | ClientID | FName | MName | LName ||
||1 | 1 | Don | A | Smith||
||2 | 1 | Dan | A | Smith||
||3 | 1 | Dan | G | Smith||
||4 | 2 | John | D | Doe ||
------------------------------------------------------------
Trying to get an sql call right in SQL Server. I've written a user defined function that generates a random first/middle/last names which is working fine. My challenge is that I want ALL records with the same ClientID to get updated with the result of a single call to my rename function (actually 3 calls = 1 for first, middle, and last name).
My attempt below is chaning EVERY record in ClientInfo DIFFERENT names instead of giving all ClientID = 1 records the SAME f/m/last names, ClientID = 2 the SAME f/m/last names, etc.
DESIRED RESULT:
------------------------------------------------------------
||ClientInfoID | ClientID | FName | MName | LName ||
||1 | 1 | Bill | X | Brown ||
||2 | 1 | Bill | X | Brown ||
||3 | 1 | Bill | X | Brown ||
||4 | 2 | Kate | Q | Ramirez ||
------------------------------------------------------------
ACTUAL RESULT:
----------------------------------------------------------------
|| ClientInfoID |ClientID | FName | MName | LName ||
|| 1 | 1 | Bill | X | Brown ||
|| 2 | 1 | Sue | R | Henderson||
|| 3 | 1 | Phil | S | Anders ||
|| 4 | 2 | Kate | Q | Ramirez ||
----------------------------------------------------------------
My SQL call
UPDATE ClientInfo
SET FirstName = X.NewFirstName
,MiddleName = X.NewMiddleName
,LastName = X.NewLastName
FROM (
SELECT UniqueClientID
,LastClientInfoID
,NewFirstName
,NewMiddleName
,NewLastName
FROM (
(
SELECT ClientID AS UniqueClientID
,MAX(ClientInfoID) AS LastClientInfoID
FROM ClientInfo
GROUP BY ClientID
) A INNER JOIN (
SELECT ClientInfoID
,NewFirstName = dbo.fnSampleFnameMnameLname(0, #MaxName, '')
,NewMiddleName = dbo.fnSampleFnameMnameLname(1, #MaxName, MiddleName)
,NewLastName = dbo.fnSampleFnameMnameLname(2, #MaxName, '')
FROM ClientInfo
) B ON A.LastClientInfoID = B.ClientInfoID
)
) X
WHERE ClientID = X.UniqueClientID
Solved it. Moved the creation of names attached to each clientid into a temp table first. Then just joined on clientinfo on that temp table to pull in the new sample names.
SELECT ClientID, NewFirstName, NewMiddleName, NewLastName
INTO #TempSampleNames
FROM (
(
SELECT ClientID,
MAX(ClientInfoID) MaxClientInfoID
FROM ClientInfo
GROUP BY ClientID
) A
INNER JOIN (
SELECT ClientInfoID
,NewFirstName = dbo.fnSampleFnameMnameLname(0, #MaxName, '')
,NewMiddleName = dbo.fnSampleFnameMnameLname(1, #MaxName, MiddleName)
,NewLastName = dbo.fnSampleFnameMnameLname(2, #MaxName, '')
FROM ClientInfo
) B ON A.MaxClientInfoID = B.ClientInfoID
)
UPDATE ClientInfo
SET
FirstName = B.NewFirstName
,MiddleName = B.NewMiddleName
,LastName = B.NewLastName
FROM ClientInfo A
INNER JOIN #TempSampleNames B ON A.ClientID = B.ClientID
DROP TABLE #TempSampleNames
Assuming that you have a Client table, and ClientID is a primary or unique key in that table, and ClientInfo has a one-to-many relationship with it on ClientID, then you could simply do this:
UPDATE A
SET
FirstName = B.NewFirstName
,MiddleName = B.NewMiddleName
,LastName = B.NewLastName
FROM ClientInfo A
INNER JOIN (
SELECT ClientID
,NewFirstName = dbo.fnSampleFnameMnameLname(0, #MaxName, '')
,NewMiddleName = dbo.fnSampleFnameMnameLname(1, #MaxName, MiddleName)
,NewLastName = dbo.fnSampleFnameMnameLname(2, #MaxName, '')
FROM Client
) B ON A.ClientID = B.ClientID
I would change the function from scalar to table valued
That's just a proof of concept. I fill the existing FirstName into the function to proof that it's always the same with one client...
ATTENTION: I used your data as given in your question. One 'Don' should rather be a 'Dan' I assume...
EDIT: expand example...
CREATE FUNCTION dbo.fnSampleFNameMnameLname
(
#Inp VARCHAR(30) --don't quite understand the #Maxname...
)
RETURNS TABLE
AS
--Create your Names in one go (this is inline syntax, maybe you'd do it with multi statement syntax...)
RETURN SELECT #Inp+'FName1' AS FName,#Inp+'MName1' AS MName,#Inp+'LName1' AS LName;
GO
DECLARE #ClientInfo TABLE(ClientInfoID INT,ClientID INT, FName VARCHAR(30),MName VARCHAR(30),LName VARCHAR(30));
INSERT INTO #ClientInfo VALUES
(1,1,'Don','A','Smith')
,(1,1,'Dan','A','Smith')
,(1,1,'Dan','A','Smith')
,(1,1,'John','D','Doe');
SELECT * FROM #ClientInfo AS ci
CROSS APPLY dbo.fnSampleFNameMnameLname(ci.FName) AS NewNames

Trigger after update not working

I'm struggling to get my 'after update' trigger to work properly.
As seen from my simple query, the sum of the production_work matches the sum of the order elements.
# select ident,ud,dp,swrv,sh,jmsw,sw,prrv,mhsw,bmsw,mp,pr,st,completed from orders;
ident | ud | dp | swrv | sh | jmsw | sw | prrv | mhsw | bmsw | mp | pr | st | completed
-------+----+----+------+----+------+----+------+------+------+----+----+----+-----------
2 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | f
(1 row)
# select * from production_work;
ident | order_id | producer_id | day | ud | dp | swrv | sh | jmsw | sw | prrv | mhsw | bmsw | mp | pr | st
-------+----------+-------------+------------+----+----+------+----+------+----+------+------+------+----+----+----
5 | 2 | 1 | 2013-08-09 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0
6 | 2 | 2 | 2013-08-09 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0
(2 rows)
I'm trying to set the 'completed' to true if the sum of the work elements match the order element by using this trigger:
CREATE OR REPLACE FUNCTION update_order_completion_status() RETURNS trigger AS
$BODY$
BEGIN
WITH w AS (
SELECT SUM(ud) AS ud, SUM(dp) AS dp, SUM(swrv) AS swrv, SUM(sh) AS sh, SUM(jmsw) AS jmsw, SUM(sw) AS sw, SUM(prrv) AS prrv,
SUM(mhsw) AS mhsw, SUM(bmsw) AS bmsw, SUM(mp) AS mp, SUM(pr) AS pr, SUM(st) AS st
FROM production_work
WHERE order_id = OLD.order_id
), o AS (
SELECT ud, dp, swrv, sh, jmsw, sw, prrv, mhsw, bmsw, mp, pr, st
FROM orders
WHERE ident = OLD.order_id
)
UPDATE orders
SET completed = (w.ud = o.ud AND w.dp = o.dp AND w.swrv = o.swrv AND w.sh = o.sh AND w.jmsw = o.jmsw AND w.sw = o.sw AND
w.prrv = o.prrv AND w.mhsw = o.mhsw AND w.bmsw = o.bmsw AND w.mp = o.mp AND w.pr = o.pr AND w.st = o.st)
WHERE ident = OLD.order_id;
END;
$BODY$ LANGUAGE plpgsql ;
CREATE TRIGGER update_order_completion_status_trigger
AFTER UPDATE OF ud, dp, swrv, sh, jmsw, sw, prrv, mhsw, bmsw, mp, pr, st
ON production_work
FOR EACH ROW EXECUTE PROCEDURE update_order_completion_status();
I'm not getting any error messages when I do the update to the production_work table, but as you can see the completed column is not being set to true.
The SQL query in your trigger func is needlessly complicated and also incorrect (missing FROM clause in UPDATE). A simplified, correct version would be:
UPDATE orders o
SET completed = (w.ud = o.ud AND w.dp = o.dp AND ...)
FROM (
SELECT SUM(ud) AS ud, SUM(dp) AS dp, ...
FROM production_work
WHERE order_id = NEW.order_id
) w
WHERE o.ident = NEW.order_id
AND o.completed <> (w.ud = o.ud AND w.dp = o.dp AND ...);
Assuming completed NOT NULL.
Note, I am using NEW, not OLD, since you want to use the new values.
But if production_work.order_id can change, you'd have to update orders for OLD and NEW.
The last line is to prevent empty updates.
Alternative: VIEW
If you don't need the redundant column completed to optimize performance, I would consider a VIEW instead:
CREATE VIEW order_plus AS
SELECT o.*, (SUM(w.ud) = o.ud AND SUM(w.dp) = o.dp AND ...) AS completed
FROM orders o
JOIN production_work w ON o.ident = w.ident
GROUP BY o.ident;
Assuming ident PRIMARY KEY.

Opposite of SELECT TOP

Transact-SQL has a handy SELECT TOP 4 [whatever] FROM.........
I want to make a SELECT query returning the last "n" entries from a table instead of the first ones.
This is the query I would use to return the first four items entered at the table, using SELECT TOP:
sql = "SELECT TOP 4 [news_title], [news_date_added], [news_short_description],
[news_ID] FROM [Web_Xtr_News] WHERE ([news_type] = 2 OR [news_type] = 3) AND
[news_language] = '" + Language + "' ORDER BY [news_ID] ASC"
I need to return the last four.
Change the order of the table from ASC to DESC.
It's exactly this: http://www.sqlfiddle.com/#!3/6c813/1
with bottom as(
select top 4 *
from tbl
order by n desc
)
select *
from bottom
order by n
Data source:
| N |
|----|
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
| 6 |
| 7 |
| 8 |
| 9 |
| 10 |
Output:
| N |
|----|
| 7 |
| 8 |
| 9 |
| 10 |
Continue to use TOP, and reverse the order:
SELECT TOP 4 [news_title],
[news_date_added],
[news_short_description],
[news_ID]
FROM [Web_Xtr_News]
WHERE ([news_type] = 2
OR [news_type] = 3)
AND [news_language] = #Language
ORDER BY [news_ID] DESC
(It was rewritten to use parameters of course. Your original is vulnerable to SQL injection.)
You can reverse the ordering by using DESC instead of ASC at the end of your query.
A quick way to select batches from a list is to create a unique identifier like a (ROW ID) as (1, 2, 3, etc.) and create a nested query.
Select *
from
(select
row_number() as id
,column1
,column2
from
table1
)as D
where D.id (<>=! between)