SQL Server 2008 - need help on a antithetical query - sql

I want to find out meter reading for given transaction day. In some cases there won’t be any meter reading and would like to see a meter reading for previous day.
Sample data set follows. I am using SQL Server 2008
declare #meter table (UnitID int, reading_Date date,reading int)
declare #Transactions table (Transactions_ID int,UnitID int,Transactions_date date)
insert into #meter (UnitID,reading_Date,reading ) values
(1,'1/1/2014',1000),
(1,'2/1/2014',1010),
(1,'3/1/2014',1020),
(2,'1/1/2014',1001),
(3,'1/1/2014',1002);
insert into #Transactions(Transactions_ID,UnitID,Transactions_date) values
(1,1,'1/1/2014'),
(2,1,'2/1/2014'),
(3,1,'3/1/2014'),
(4,1,'4/1/2014'),
(5,2,'1/1/2014'),
(6,2,'3/1/2014'),
(7,3,'4/1/2014');
select * from #meter;
select * from #Transactions;
I expect to get following output
Transactions
Transactions_ID UnitID Transactions_date reading
1 1 1/1/2014 1000
2 1 2/1/2014 1010
3 1 3/1/2014 1020
4 1 4/1/2014 1020
5 2 1/1/2014 1001
6 2 3/1/2014 1001
7 3 4/1/2014 1002

Your SQL Query to get your desired out put will as following:
SELECT Transactions_ID, T.UnitID, Transactions_date
, (CASE WHEN ISNULL(M.reading,'') = '' THEN
(
SELECT MAX(Reading) FROM #meter AS A
JOIN #Transactions AS B ON A.UnitID=B.UnitID AND A.UnitID=T.UnitID
)
ELSE M.reading END) AS Reading
FROM #meter AS M
RIGHT OUTER JOIN #Transactions AS T ON T.UnitID=M.UnitID
AND T.Transactions_date=M.reading_Date

I can think of two ways to approach this - neither of them are ideal.
The first (and slightly better) way would be to create a SQL Function that took the Transactions_date as a parameter and returned the reading for Max(Reading_date) where reading_date <= transactions_date. You could then use this function in a select statement against the Transactions table.
The other approach would be to use a cursor to iterate through the transactions table and use the same logic as above where you return the reading for Max(Reading_date) where reading_date <= transactions_date.

Try the below query:
Please find the result of the same in SQLFiddle
select a.Transactions_ID, a.UnitID, a.Transactions_date,
case when b.reading IS NULL then c.rd else b.reading end as reading
from
Transactions a
left outer join
meter b
on a.UnitID = b.UnitID
and a.Transactions_date = b.reading_Date
inner join
(
select UnitID,max(reading) as rd
from meter
group by UnitID
) as C
on a.UnitID = c.UnitID

Related

SQL Select to return one line multiple times based on a number within the dataset

So I've tried many other attempts at answers around this topic from here and so far everything has either outright failed or not given me the result I'm after:
I have a select statement to use for a report that brings through delivery information. The result set is from a main table that only has one line per delivery number (the delivery header record) and within the dataset there is also a field called palletspaces which we use to indicate (you guessed it) how many pallets are needed for the delivery
What I now need to do is the following:
find that palletspaces number
return the single delivery record the same number of times as that palletspaces number
include a new column in the results that counts up to that palletspaces number
so for instance, my SQL will return every record from the deliveries table and would look something like this
id traderid toaddressid county postcode palletspaces
D-124597 2101 2 READING RG6 1AZ 3
D-124600 20060 12 MAGOR, GWENT NP26 3DF 1
D-124601 20060 13 RUGBY CV23 8YH 2
So now, I'd need to see that palletspaces number, then return the particular line that many times and then also have a new column that counts these instances:
id traderid toaddressid county postcode palletspaces LineCount
D-124597 2101 2 READING RG6 1AZ 3 1
D-124597 2101 2 READING RG6 1AZ 3 2
D-124597 2101 2 READING RG6 1AZ 3 3
D-124600 20060 12 MAGOR, GWENT NP26 3DF 1 1
D-124601 20060 13 RUGBY CV23 8YH 2 1
D-124601 20060 13 RUGBY CV23 8YH 2 2
The other thing to mention is that naturally I'll have hundreds of different delivery records (all returned as one line each) and all will have differing palletspaces numbers. And of course stating the obvious I need the line to only replicate and count based on it's own palletspaces number
The SQL in use is as below
select
deliveries.id,
deliveries.traderid,
customers.name,
deliveries.toaddressid,
deliveries.eutransportid,
deliveries.street,
deliveries.city,
deliveries.county,
deliveries.postcode,
delivery_custom.palletspaces,
ectransport.ectranspdesc
from deliveries
INNER JOIN customers ON
deliveries.traderid = customers.id
INNER JOIN delivery_custom ON
deliveries.id = delivery_custom.id
INNER JOIN ectransport ON
deliveries.eutransportid = ectransport.ectranspcode
Try like this:
select deliveries.id,
deliveries.traderid,
customers.name,
deliveries.toaddressid,
deliveries.eutransportid,
deliveries.street,
deliveries.city,
deliveries.county,
deliveries.postcode,
delivery_custom.palletspaces,
ectransport.ectranspdesc
INTO #MyTemp
from deliveries
INNER JOIN customers ON
deliveries.traderid = customers.id
INNER JOIN delivery_custom ON
deliveries.id = delivery_custom.id
INNER JOIN ectransport ON
deliveries.eutransportid = ectransport.ectranspcode
;WITH CTE AS(
SELECT id,traderid,toaddressid,county,postcode,palletspaces,1 AS LineCount
FROM #MyTemp
UNION ALL
SELECT id,traderid,toaddressid,county,postcode,palletspaces,LineCount+1
FROM CTE
WHERE LineCount<palletspaces
)
SELECT *
FROM CTE
ORDER BY id, LineCount;
DROP TABLE #MyTemp
Hope this time you get it.
Using Recursive CTE, we can achieve this:
DECLARE #TAB TABLE ([D Number] VARCHAR(20) ,customer INT, postcode VARCHAR(20), palletspaces INT)
INSERT INTO #TAB VALUES('D-123456' ,19114, 'DA12 1TF' , 4)
INSERT INTO #TAB VALUES('D-111111' ,19114, 'DDDD 1TF' , 3)
;WITH CTE AS(
SELECT [D Number],customer,postcode,palletspaces,1 AS A
FROM #TAB
UNION ALL
SELECT [D Number],customer,postcode,palletspaces,A+1
FROM CTE
WHERE A<palletspaces
)
SELECT *
FROM CTE
ORDER BY [D Number], LineCount;
Output:
D Number customer postcode palletspaces LineCount
D-123456 19114 DA12 1TF 4 1
D-123456 19114 DA12 1TF 4 2
D-123456 19114 DA12 1TF 4 3
D-123456 19114 DA12 1TF 4 4

Extract non-existent values based on previous months?

I HAVE tb1
code Name sal_month
==== ===== ========
101 john 02/2017
102 mathe 02/2017
103 yara 02/2017
104 sara 02/2017
101 john 03/2017
102 mathe 03/2017
103 yara 03/2017
104 sara 03/2017
101 john 04/2017
103 yara 04/2017
In February all of them received salaries as well as March
How do I extract non-existent values based on previous months?
the result should be come
code sal_month
==== =======
102 04/2017
104 04/2017
Thank in advance
First I created this table:
create table #T(code int, sal_month varchar(10))
insert into #T values(101,'2/2017'),(102,'2/2017'),(103,'2/2017'),(104,'2/2017'),
(101,'3/2017'),(102,'3/2017'),(104,'3/2017'),(101,'4/2017'),(103,'4/2017')
Second, I executed this query:
SELECT code, Max(sal_Month)
From #T
Where code not in (select code from #T where sal_Month = (select Max(sal_Month) from #T))
Group by code
Then I got the following results:
Note: I am using SQL SERVER 2012
I think you can count salary_month grouped by id, something like this, and select the rows that shows less than 3 times.
select code, count (sal_month) from tb1
group by code
having count (sal_month) < 3
After that you join with initial table (just to filter the full rows which you need) on code.
So the final query will look like his:
select code, sal_month
from tb1 a
join (select code, count (sal_month) from tb1
group by code
having code < 3) X on a.code = X.code
Something like this:
DECLARE #DataSource TABLE
(
[code] INT
,[sal_month] VARCHAR(12)
);
INSERT #DataSource ([code], [sal_month])
VALUES (101, '2/2017')
,(102, '2/2017')
,(103, '2/2017')
,(104, '2/2017')
,(101, '3/2017')
,(102, '3/2017')
,(104, '3/2017')
,(101, '4/2017')
,(103, '4/2017');
WITH DataSource AS
(
SELECT *
,DENSE_RANK() OVER (ORDER BY [sal_month]) AS [MonthID]
,MAX([sal_month]) OVER () AS [MaxMonth]
FROM #DataSource DS1
)
SELECT DS1.[code]
,DS1.[sal_month]
FROM DataSource DS1
LEFT JOIN DataSource DS2
ON DS1.[code] = DS2.[code]
AND DS1.[MonthID] = DS2.[MonthID] - 1
LEFT JOIN DataSource DS3
ON DS1.[code] = DS3.[code]
AND DS1.[MonthID] = DS3.[MonthID] + 1
WHERE DS2.[code] IS NULL
AND DS3.[code] IS NOT NULL
AND DS1.[sal_month] <> DS1.[MaxMonth];
Some notes:
we need a way to sort the months and it is not easy as you are storing them in very unpractical way; you are not using a date/datetime column and your string is not a valid date; also, the string you are using is not good at all, because if you have [sal_month] from different years, we will not be able to sort them; you should think about this - one alternative is to use this format:
201512
201701
201702
201711
In this way we can sort by string.
in the core I am using ROW_NUMBER and sorting months as strings;
the idea is to look for all records, that have not exists in the next month, but have a record in the previous; at the same time, excluded records which are from the last month, as it's not possible for them to have record in the next month;
Try this:
select tb2.code, tb2.sal_month from tb
right join (
select code, sal_month, datepart(month, sal_month) + 1 as next_sal_month from tb) as tb2
on (tb.code = tb2.code and datepart(month, tb.sal_month) = tb2.next_sal_month)
where tb2.next_sal_month < 5 and tb.sal_month is null
In the result set there's one additional record: code 103 didn't receive salary in March, but did so in February, so it is included as well.
Here's SQL fiddle, to try :)
In the absence of more facts about your tables, create a cartesian product of the 2 axes of month & code, then left join the stored data. Then it is easy to identify missing items when no stored data exists when compared to every possible combination.
You might already have master tables of sal_month and/or code to use, if you do use those, but if not you can dynamically create them using select distinct as seen below.
create table tbl1 (code int, sal_month varchar(10))
insert into tbl1 values(101,'2/2017'),(102,'2/2017'),(103,'2/2017'),(104,'2/2017'),
(101,'3/2017'),(102,'3/2017'),(104,'3/2017'),(101,'4/2017'),(103,'4/2017')
select c.code, m.sal_month
from ( select distinct sal_month from tbl1 ) m
cross join ( select distinct code from tbl1 ) c
Left join tbl1 t on m.sal_month = t.sal_month and c.code = t.code
Where t.sal_month IS NULL
code | sal_month
---: | :--------
103 | 3/2017
102 | 4/2017
104 | 4/2017
dbfiddle here

SQL Server date diff for rows with certain conditions

I have a table named Call and another named CallLog. CallLog links to a CallLogType
CallId AgentId
---------------------
1 123
2 765
3 134
4 134
5 134
CallLogId CallId Time LogType
---------------------------------------------------------------
1 1 2017-04-05 17:38:00 1 (InProgress)
2 1 2017-04-05 17:40:00 2 (OnHold)
3 1 2017-04-05 17:45:00 1 (InProgress)
4 1 2017-04-05 17:48:00 3 (Completed)
CallLogTypeId Description
--------------------------------
1 InProgress
2 OnHold
3 Completed
I need to write a query that, given an AgentId, finds their actual talk time for the day.
One part I'm having trouble with is the fact the query needs to ignore the 'OnHold' state in the above example.
I should be able to pass in UserId 123 (they're on Call #1), and have it return a total time of 5 minutes for the sample data
(2 minutes in progress, ignore the 5 minutes on hold and then 3 minutes more in progress).
I'm struggling to find the best approach to solve this. Any help would be appreciated!
I would use the LAG function to get the date of the next record to be able to calculate the amount of time consumed in each status. Afterwards I would filter the OnHold status. Note that the query will give all the calls for the user with the ID stated in the parameter :theFilterValue with the time consumed.
select c.CallId,
sum(datediff(minute, cl.time, lag(cl.time) over (order by cl.time))) as totalMinutes
from CallLog cl
inner join Call c on cl.CallId = c.CallId
where cl.LogTypeId <> 2 -- OnHold
and c.AgentId = :theFilterValue
and lag(cl.time) over (order by cl.time) is not null
group by
c.AgentId,
c.CallId
Forgot about this one for a while, but came back to it and thought I should post an answer of some sort!
I've come up with a (likely less than ideal) query that seems to do the trick and returns the right results for the schema in the original question:
DECLARE #InProgress INT = 1
DECLARE #OnHold INT = 2
DECLARE #Completed INT = 3
DECLARE #AgentId INT = 123
DECLARE #Date DATE = '2017-07-11'
SELECT SUM(DATEDIFF(minute, LogJoin.Time, LogJoin.NextLogTime))
FROM Call C
INNER JOIN
(
SELECT
CL.CallId,
CL.LogType,
CL.Time,
LEAD(CL.LogType, 1, NULL) OVER (PARTITION BY CL.CallId ORDER BY CL.CallLogId ASC) AS NextLogType,
LEAD(CL.Time, 1, NULL) OVER (PARTITION BY CL.CallId ORDER BY CL.CallLogId ASC) AS NextLogTime
FROM CallLog CL
INNER JOIN Call C ON C.CallId = CL.CallId
WHERE C.AgentId = #AgentId AND #Date = CAST(CL.Time AS DATE)
) LogJoin ON LogJoin.CallId = C.CallId AND LogJoin.LogType <> #OnHold

Show data from table even if there is no data!! Oracle

I have a query which shows count of messages received based on dates.
For Eg:
1 | 1-May-2012
3 | 3-May-2012
4 | 6-May-2012
7 | 7-May-2012
9 | 9-May-2012
5 | 10-May-2012
1 | 12-May-2012
As you can see on some dates there are no messages received. What I want is it should show all the dates and if there are no messages received it should show 0 like this
1 | 1-May-2012
0 | 2-May-2012
3 | 3-May-2012
0 | 4-May-2012
0 | 5-May-2012
4 | 6-May-2012
7 | 7-May-2012
0 | 8-May-2012
9 | 9-May-2012
5 | 10-May-2012
0 | 11-May-2012
1 | 12-May-2012
How can I achieve this when there are no rows in the table?
First, it sounds like your application would benefit from a calendar table. A calendar table is a list of dates and information about the dates.
Second, you can do this without using temporary tables. Here is the approach:
with constants as (select min(thedate>) as firstdate from <table>)
dates as (select( <firstdate> + rownum - 1) as thedate
from (select rownum
from <table> cross join constants
where rownum < sysdate - <firstdate> + 1
) seq
)
select dates.thedate, count(t.date)
from dates left outer join
<table> t
on t.date = dates.thedate
group by dates.thedate
Here is the idea. The alias constants records the earliest date in your table. The alias dates then creates a sequence of dates. The inner subquery calculates a sequence of integers, using rownum, and then adds these to the first date. Note this assumes that you have on average at least one transaction per date. If not, you can use a bigger table.
The final part is the join that is used to bring back information about the dates. Note the use of count(t.date) instead of count(*). This counts the number of records in your table, which should be 0 for dates with no data.
You don't need a separate table for this, you can create what you need in the query. This works for May:
WITH month_may AS (
select to_date('2012-05-01', 'yyyy-mm-dd') + level - 1 AS the_date
from dual
connect by level < 31
)
SELECT *
FROM month_may mm
LEFT JOIN mytable t ON t.some_date = mm.the_date
The date range will depend on how exactly you want to do this and what your range is.
You could achieve this with a left outer join IF you had another table to join to that contains all possible dates.
One option might be to generate the dates in a temp table and join that to your query.
Something like this might do the trick.
CREATE TABLE #TempA (Col1 DateTime)
DECLARE #start DATETIME = convert(datetime, convert(nvarchar(10), getdate(), 121))
SELECT #start
DECLARE #counter INT = 0
WHILE #counter < 50
BEGIN
INSERT INTO #TempA (Col1) VALUES (#start)
SET #start = DATEADD(DAY, 1, #start)
SET #counter = #counter+1
END
That will create a TempTable to hold the dates... I've just generated 50 of them starting from today.
SELECT
a.Col1,
COUNT(b.MessageID)
FROM
TempA a
LEFT OUTER JOIN YOUR_MESSAGE_TABLE b
ON a.Col1 = b.DateColumn
GROUP BY
a.Col1
Then you can left join your message counts to that.

How to calculate the sum of values in a tree using SQL

I need to sum points on each level earned by a tree of users. Level 1 is the sum of users' points of the users 1 level below the user. Level 2 is the Level 1 points of the users 2 levels below the user, etc...
The calculation happens once a month on a non production server, no worries about performance.
What would the SQL look like to do it?
If you're confused, don't worry, I am as well!
User table:
ID ParentID Points
1 0 230
2 1 150
3 0 80
4 1 110
5 4 54
6 4 342
Tree:
0
|---\
1 3
| \
2 4---
\ \
5 6
Output should be:
ID Points Level1 Level2
1 230 150+110 150+110+54+342
2 150
3 80
4 110 54+342
5 54
6 342
SQL Server Syntax and functions preferably...
If you were using Oracle DBMS that would be pretty straightforward since Oracle supports tree queries with the CONNECT BY/STARTS WITH syntax. For SQL Server I think you might find Common Table Expressions useful
Trees don't work well with SQL. If you have very (very very) few write accesses, you could change the tree implementation to use nested sets, that would make this query incredibly easy.
Example (if I'm not mistaken):
SELECT SUM(points)
FROM users
where left > x and right < y
However, any changes on the tree require touching a massive amount of rows. It's probably better to just do the recursion in you client.
I would say: create a stored procedure, probably has the best performance.
Or if you have a maximum number of levels, you could create subqueries, but they will have a very poort performance.
(Or you could get MS SQL Server 2008 and get the new hierarchy functions... ;) )
SQL in general, like others said, does not handle well such relations. Typically, a surrogate 'relations' table is needed (id, parent_id, unique key on (id, parent_id)), where:
every time you add a record in 'table', you:
INSERT INTO relations (id, parent_id) VALUES ([current_id], [current_id]);
INSERT INTO relations (id, parent_id) VALUES ([current_id], [current_parent_id]);
INSERT INTO relations (id, parent_id)
SELECT [current_id], parent_id
FROM relations
WHERE id = [current_parent_id];
have logic to avoid cycles
make sure that updates, deletions on 'relations' are handled with stored procedures
Given that table, you want:
SELECT rel.parent_id, SUM(tbl.points)
FROM table tbl INNER JOIN relations rel ON tbl.id=rel.id
WHERE rel.parent_id <> 0
GROUP BY rel.parent_id;
Ok, this gives you the results you are looking for, but there are no guarantees that I didn't miss something. Consider it a starting point. I used SQL 2005 to do this, SQL 2000 does not support CTE's
WITH Parent (id, GrandParentId, parentId, Points, Level1Points, Level2Points)
AS
(
-- Find root
SELECT id,
0 AS GrandParentId,
ParentId,
Points,
0 AS Level1Points,
0 AS Level2Points
FROM tblPoints ptr
WHERE ptr.ParentId = 0
UNION ALL (
-- Level2 Points
SELECT pa.GrandParentId AS Id,
NULL AS GrandParentId,
NULL AS ParentId,
0 AS Points,
0 AS Level1Points,
pa.Points AS Level2Points
FROM tblPoints pt
JOIN Parent pa ON pa.GrandParentId = pt.Id
UNION ALL
-- Level1 Points
SELECT pt.ParentId AS Id,
NULL AS GrandParentId,
NULL AS ParentId,
0 AS Points,
pt.Points AS Level1Points,
0 AS Level2Points
FROM tblPoints pt
JOIN Parent pa ON pa.Id = pt.ParentId AND pa.ParentId IS NOT NULL
UNION ALL
-- Points
SELECT pt.id,
pa.ParentId AS GrandParentId,
pt.ParentId,
pt.Points,
0 AS Level1Points,
0 AS Level2Points
FROM tblPoints pt
JOIN Parent pa ON pa.Id = pt.ParentId AND pa.ParentId IS NOT NULL )
)
SELECT id,
SUM(Points) AS Points,
SUM(Level1Points) AS Level1Points,
CASE WHEN SUM(Level2Points) > 0 THEN SUM(Level1Points) + SUM(Level2Points) ELSE 0 END AS Level2Points
FROM Parent
GROUP BY id
ORDER by id
If you are working with trees stored in a relational database, I'd suggest looking at "nested set" or "modified preorder tree traversal". The SQL will be as simple as that:
SELECT id,
SUM(value) AS value
FROM table
WHERE left>left\_value\_of\_your\_node
AND right<$right\_value\_of\_your\_node;
... and do this for every node you are interested in.
Maybe this will help you:
http://www.dbazine.com/oracle/or-articles/tropashko4 or use google.
You have a couple of options:
Use a cursor and a recursive user-defined function call (it's quite slow)
Create a cache table, update it on INSERT using a trigger (it's the fastest solution but could be problematic if you have lots of updates to the main table)
Do a client-side recursive calculation (preferable if you don't have too many records)
You can write a simple recursive function to do the job. My MSSQL is a little bit rusty, but it would look like this:
CREATE FUNCTION CALC
(
#node integer,
)
returns
(
#total integer
)
as
begin
select #total = (select node_value from yourtable where node_id = #node);
declare #children table (value integer);
insert into #children
select calc(node_id) from yourtable where parent_id = #node;
#current = #current + select sum(value) from #children;
return
end
The following table:
Id ParentId
1 NULL
11 1
12 1
110 11
111 11
112 11
120 12
121 12
122 12
123 12
124 12
And the following Amount table:
Id Val
110 500
111 50
112 5
120 3000
121 30000
122 300000
Only the leaves (last level) Id's have a value defined.
The SQL query to get the data looks like:
;WITH Data (Id, Val) AS
(
select t.Id, SUM(v.val) as Val from dbo.TestTable t
join dbo.Amount v on t.Id = v.Id
group by t.Id
)
select cd.Id, ISNULL(SUM(cd.Val), 0) as Amount FROM
(
-- level 3
select t.Id, d.val from TestTable t
left join Data d on d.id = t.Id
UNION
-- level 2
select t.parentId as Id, sum(y.Val) from TestTable t
left join Data y on y.id = t.Id
where t.parentId is not null
group by t.parentId
UNION
-- level 1
select t.parentId as Id, sum(y.Val) from TestTable t
join TestTable c on c.parentId = t.Id
left join Data y on y.id = c.Id
where t.parentId is not null
group by t.parentId
) AS cd
group by id
this results in the output:
Id Amount
1 333555
11 555
12 333000
110 500
111 50
112 5
120 3000
121 30000
122 300000
123 0
124 0
I hope this helps.