How to make a single line query include multiple lines in Oracle - sql

I would like to take a set of data and expand it by adding date rows based an existing field. For instance, If I have the following table (TABLE1):
ID NAME YEAR
1 John 2001
2 Jim 2012
3 Sally 2005
I want to take this data and put it into another table but expand it to include a set of months (and from there I can add monthly information). If I just look at the first record (John) my result would be:
ID NAME YEAR MONTH
1 John 2001 01-JAN-2001
1 John 2001 01-FEB-2001
1 John 2001 01-MAR-2001
...
1 John 2001 01-DEC-2001
I have the mechanism to derive my monthly dates but how do I extract the data from TABLE1 to make TABLE2. Here is just a quick query but, of course, I get the ORA-01427 single-row subquery returns more than one row as expect. Just not sure how to organize the query to put these two pieces together:
select id,
name,
year,
book_cd,
(SELECT ADD_MONTHS('01-JAN-'|| year, LEVEL - 1)
FROM DUAL CONNECT BY LEVEL <= 12) month
from table1 ;
I realize I cant do this but I'm not sure how to put the two pieces together. I plan to bulk process records so it wont be one ID at a time Thanks for the help.

You can use a cross join:
select t.id,
t.name,
t.year,
t.book_cd,
ADD_MONTHS(to_date(t.year || '-01-01', 'YYYY-MM-DD'), m.rn) as mnth
from table1 t
cross join (select rownum - 1 as rn
from dual
connect by rownum <= 12) m

Related

How to consecutively count everything greater than or equal to itself in SQL?

Let's say if I have a table that contains Equipment IDs of equipments for each Equipment Type and Equipment Age, how can I do a Count Distinct of Equipment IDs that have at least that Equipment Age.
For example, let's say this is all the data we have:
equipment_type
equipment_id
equipment_age
Screwdriver
A123
1
Screwdriver
A234
2
Screwdriver
A345
2
Screwdriver
A456
2
Screwdriver
A567
3
I would like the output to be:
equipment_type
equipment_age
count_of_equipment_at_least_this_age
Screwdriver
1
5
Screwdriver
2
4
Screwdriver
3
1
Reason is there are 5 screwdrivers that are at least 1 day old, 4 screwdrivers at least 2 days old and only 1 screwdriver at least 3 days old.
So far I was only able to do count of equipments that falls within each equipment_age (like this query shown below), but not "at least that equipment_age".
SELECT
equipment_type,
equipment_age,
COUNT(DISTINCT equipment_id) as count_of_equipments
FROM equipment_table
GROUP BY 1, 2
Consider below join-less solution
select distinct
equipment_type,
equipment_age,
count(*) over equipment_at_least_this_age as count_of_equipment_at_least_this_age
from equipment_table
window equipment_at_least_this_age as (
partition by equipment_type
order by equipment_age
range between current row and unbounded following
)
if applied to sample data in your question - output is
Use a self join approach:
SELECT
e1.equipment_type,
e1.equipment_age,
COUNT(*) AS count_of_equipments
FROM equipment_table e1
INNER JOIN equipment_table e2
ON e2.equipment_type = e1.equipment_type AND
e2.equipment_age >= e1.equipment_age
GROUP BY 1, 2
ORDER BY 1, 2;
GROUP BY restricts the scope of COUNT to the rows in the group, i.e. it will not let you reach other rows (rows with equipment_age greater than that of the current group). So you need a subquery or windowing functions to get those. One way:
SELECT
equipment_type,
equipment_age,
(Select COUNT(*)
from equipment_table cnt
where cnt.equipment_type = a.equipment_type
AND cnt.equipment_age >= a.equipment_age
) as count_of_equipments
FROM equipment_table a
GROUP BY 1, 2, 3
I am not sure if your environment supports this syntax, though. If not, let us know we will find another way.

SQL - Select lowest values with group by and order by?

In my rankings database I have a table named times. I also have another table with authors. The authors have author id's (named ath_id inside the times table).
Records saved in times table:
id ath_id brand_id time date
------------- ------------ -------------- -------------- --------------
65125537 5384729 3 44741 May 8 2014
72073658 4298584 1 1104 Jun 28 2015
86139060 4298584 2 2376 Nov 20 2016
92237079 4298584 1 1115 Jun 24 2017
92237082 4298584 1 1104 Jun 24 2017
93436362 5384729 12 376492 Dec 31 2012
What I want to achieve
I'd like to retrieve an ordered list of the times that belong to the author (by the author id). I'd like to order them by brand_id, and I only want the records with the lowest time value.
Also, when there are multiple records with the same brand_id and the same time value, I'd like the list to be ordered by date. So the record with the latest date will be last.
What I have
I currently use this query: SELECT * FROM times WHERE ath_id = 4298584 GROUP BY brand_id ASC.
It works great, but it limits records with the same brand_id to 1, and thereby it limits records with the same time, even when multiple records have the lowest time value.
To sum it up
So in the case of the example above. When I select all the records with ath_id = 4298584, I'd like to retrieve the following ordered list:
id ath_id brand_id time date
------------- ------------ -------------- -------------- --------------
72073658 4298584 1 1104 Jun 28 2015
92237082 4298584 1 1104 Jun 24 2017
86139060 4298584 2 2376 Nov 20 2016
This is my first time doing a bit more advanced SQL queries. I'm working with Laravel, so giving both a raw SQL solution and a Laravel solution using the Laravel Query Builder wouldn't do any harm.
You could try using a derived table to get the min time for an ath_id and brand_id. Then join it back to your original table to get the rest of the data.
SELECT t.*
FROM times t
JOIN (SELECT ath_id, brand_id, MIN(time) AS time FROM dbo.times GROUP BY ath_id, brand_id) b
ON t.ath_id = b.ath_id AND t.brand_id = b.brand_id AND t.time = b.time
WHERE t.ath_id = 4298584
ORDER BY t.brand_id ASC, t.date DESC
This is another way you can do it. Although the output would be similar to SQLChao's answer, but the difference is that the inner query is creating and assigning ranks to the combination of ath_id,brand_id and date followed ordered by time. Then in outer query, you can use a filter to separate the rank 1. So basically you are replicating row_number() function.
You can use rnk=1 to rnk <= n in case you want first n records for your combination. But in you case, SQLChao's answer would be faster.
select t3.id,t3.ath_id,t3.brand_id,t3.time,t3.date
from times1 t3
inner join
(
select t1.ath_id,t1.brand_id,t1.date,t1.time,count(*) as rnk
from times1 t1
inner join times1 t2
on t1.ath_id=t2.ath_id
and t1.brand_id=t2.brand_id
and t1.date=t2.date
and t1.time >= t2.time
where t1.ath_id=4298584
group by t1.ath_id,t1.brand_id,t1.date,t1.time
) t4
on t3.ath_id=t4.ath_id
and t3.brand_id=t4.brand_id
and t3.date=t4.date
and t3.time = t4.time
and t4.rnk=1
;

Selecting multiple unique matches from one column that match another column

I have a list of codes (101, 102, 103, 104) and I want to pick out the people in the following table that have two or more different codes from the list occurring within a year of each other.
Name Code1 Code1date
John 101 01/01/2016
John 102 01/02/2013
Chris 101 01/01/2015
Chris 101 01/05/2014
Chris 102 01/10/2015
Mark 101 01/11/2011
Mark 101 01/01/2011
Mark 107 01/07/2012
So in this sample only Chris would be selected because he has a 101 code and a 102 code within a year of each other.
Thanks!
Try with the below query if you are using SQL Server.
SELECT name
FROM yourtable
WHERE code1 in (101, 102, 103, 104)
GROUP BY name, year(code1date)
HAVING COUNT(distinct code1) > 1
Version 1: Same year: happened in the same year
You need to make a group for each name and year and only show those distinct names that have more than 1 unique code:
select distinct name
from sample_table
where code1 between 101 and 104
group by name, extract(year from code1date)
having count(distinct code1) > 1
This will result in Chris only being presented in output.
EXTRACT function is ANSI-SQL compliant, but it will work assuming that code1date is of date type
In case it is of text data type, you could get 4 characters from the right, so for example right(code1date, 4)
Version 2: Same year: scan back- and onwards for one year difference
If by one year you mean not the same year, but scanning backwards and onwards from a date for 1 year difference, then here's the solution for Postgres:
SELECT
a.name
FROM sample_table a
JOIN sample_table b ON
a.name = b.name
AND a.code1 <> b.code1
AND b.code1date BETWEEN a.code1date - interval '1 year' AND a.code1date + interval '1 year'
WHERE a.code1 BETWEEN 101 AND 104
GROUP BY a.name
HAVING COUNT(DISTINCT a.code1) > 1
Above also assumes that your code1date is of date type. If that's not the case then you should think about converting it to a proper format. If that's beyond your reach, then you could always get the last character from your column, cast to Integer, increment it and append it back to the substring without the last char thus replacing the value of year :-)
SELECT
t1.Name
FROM
TableName t1
INNER JOIN TableName t2
ON t1.Name = t2.Name
AND t2.Code1Date BETWEEN DATEADD(year,-1,t1.Code1Date) AND DATEADD(year,1,t1.Code1Date)
AND t1.Code1 <> t2.Code1
AND t2.Code1 IN (101,102,103,104)
WHERE
t1.Code1 IN (101,102,103,104)
GROUP BY
t1.Name
HAVING
COUNT(DISTINCT t1.Code1) > 1
So this is for sql-server but just change up the BETWEEN statement to reflect the date functions of what ever rdbms you are working with.

SQL Server find the missing number

I have a table like below
id name year
--------------
1 A 2000
2 B 2000
2 B 2000
2 B 2000
5 C 2000
1 D 2001
3 E 2001
as well as you see in the year 2000 we missed id '3' and id '4' and in the year 2001 we missed id '2'. I want to generate my second table which includes missed items.
2nd table :
From-id to-id name year
--------------------------------
3 4 null 2000
2 null null 2001
Which method in a SQL query can solve my problem?
Gaps and Islands in Sequences is the name of this problem. you read this article
Here's something to get you started:
WITH cte AS
(
SELECT *
FROM
(VALUES
(1),(2),(3),(4),(5)
) Tally(number)
), cte2 as
(
SELECT DISTINCT [year]
FROM
(VALUES
(2000),(2000),(2001)
)tbl([year])
), cte3 as
(
SELECT *
FROM cte
CROSS JOIN cte2
)
SELECT *
FROM cte3
LEFT OUTER JOIN YourTable ON cte3.number = YourTable.id AND cte3.[year] = YourTable[year)
A few notes: please avoid using reserved keywords as column names (such as year).
Furthermore, since I didn't know how you'd handle multiple missing ranges I did not format the output to reflect a range. For example: What would be your expected output if only one row with id=3 would be in your table?
I'd probably use ROW_NUMBER for this
This query gives you what the correct ID should be (if I interpreted your question right):
SELECT
ROW_NUMBER() OVER (PARTITION BY yr ORDER BY name, yr) as "Correct ID", *
FROM misorder
It assigns a row number (so a number starting from 1 increasing by 1 every time the year is the same).
And to let you know which ones are missing I think this should be a working solution:
WITH missing AS
(
SELECT
ROW_NUMBER() OVER (PARTITION BY yr ORDER BY name, yr) as "Correct ID", *
FROM misorder
)
SELECT * FROM missing
WHERE "Correct ID" != "id"
It takes the first query as a base to select only those records where the assumed correct ID is not equal to the currently assigned ID. You can turn this into a query to include the ranges you mentioned, but not sure if that is really necessary.
Hope this helps.

Sum values in one column and add to another table

My Table(BOB) is look like this:
Year Month Value
2010 1 100
2010 2 100
2010 3 100
2010 4 100
2010 5 100
I would like to add YTD values to another table (BOB2)
more exactly I want to see BOB 2 table like
Year Month Value
2010 1 100
2010 2 200
2010 3 300
2010 4 400
2010 5 500
See the answer below. I have simplified the query.
select
concat(cast(t1.year as char), cast(t1.month as char)) period_current,
sum(t1.amount) amount
from bob t1
left join bob t2 on
(t2.year + t2.month) <= (t1.year + t1.month)
group by
(t1.year + t1.month);
What the query is doing is using t1 as the base table and joining on the period (year + month) then you want to sum the amounts prior to that including the current amount. I haven't added in all the edge cases, but this gives you something to start from. If you are restricting your query to a single year, this should be enough.
Well, I think I understand what you are trying to do.. but if not, please re-phrase your question... You can accomplish what you have asked by using the following SQL.
--INSERT INTO BOB2 (Year, ID, Value)
SELECT a.Year, a.ID, (SELECT SUM(b.Value)
FROM BOB b
WHERE b.ID <= a.ID) as RunningTotalValue
FROM BOB a
ORDER BY a.Value;
Here is a SQLFiddle for you to look at.
EDIT: Change the ID column to "Month" after seeing the edit to your post.