Select case with multiple columns along with other columns in Oracle? - sql

Consider the following tables
ID_TYPE
ID
TYPE
1
3
2
3
3
1
4
2
5
2
ID_HISTORY
DEBIT_ID
DEBIT_LOCATION
AMOUNT
CREDIT_ID
CREDIT_LOCATION
MONTH
3
LOC1
100
1
LOC5
MAY
4
LOC2
200
3
LOC6
MAY
2
LOC3
300
5
LOC7
MAY
1
LOC4
400
3
LOC8
JUNE
3
LOC9
500
2
LOC10
JUNE
Now suppose I want to fetch all rows from ID_HISTORY in the MONTH of MAY, and result should contain only these columns:
Id, Location, Amount
Cases:
Result should contain rows only where either DEBIT_ID or CREDIT_ID is of TYPE=3 in ID_TYPE table
If the DEBIT_ID is of TYPE = 3 in the ID_TYPE table, then pick DEBIT_ID as "Id", else pick CREDIT_ID as "Id"
Similarly, If the DEBIT_ID is of TYPE 3 in the ID_TYPE table, then pick DEBIT_LOCATION as "Location", else pick CREDIT_LOCATION as "Location"
For example, above tables should result in the following:
Id
Location
Amount
1
LOC5
100
2
LOC3
300
I know that something like the following should work:
SELECT
(CASE
WHEN (Tab.DEBIT_ID IN (
SELECT ID
FROM ID_TYPE Typ
WHERE Typ.TYPE = 3)
) THEN Tab.DEBIT_ID
ELSE Tab.CREDIT_ID END
) "Id",
(CASE
WHEN (Tab.DEBIT_ID IN (
SELECT ID
FROM ID_TYPE Typ
WHERE Typ.TYPE = 3)
) THEN Tab.DEBIT_LOCATION
ELSE Tab.CREDIT_LOCATION END
) "Location",
Tab.AMOUNT "Amount"
FROM (
SELECT *
FROM ID_HISTORY Tab
WHERE Tab.MONTH = 'MAY'
--this block will be very complicated and contain complex multi-level queries to fetch data
)
But as you can see this will be inefficient as I have to basically duplicate the full case logic for each conditional columns. Also, this is no way "clean" in case there are a lot of similar columns.
Also, if the case logic is complex, it will be inefficient even further. It would be better if i could select multiple columns in THEN / ELSE cases. I tried doing that, but it just gives me "ORA-00913: too many values" error.
What would be the optimized version?

You could use a join to remove the sub-queries:
SELECT CASE
WHEN typ.id IS NOT NULL
THEN h.debit_id
ELSE h.credit_id
END AS id,
CASE
WHEN typ.id IS NOT NULL
THEN h.debit_location
ELSE h.credit_location
END AS location,
h.AMOUNT
FROM (
SELECT *
FROM ID_HISTORY
WHERE MONTH = 'MAY'
-- this block will be very complicated and contain complex multi-level queries to fetch data
) h
LEFT OUTER JOIN (
SELECT id
FROM id_type
WHERE type = 3
) typ
ON (h.debit_id = typ.id)

Related

Compare two columns in SQL

I'm new to SQL and have very basic queries in GCP.
Let's consider this table below:
Name
B
C
Arun
1234-5678
1234
Tara
6789 - 7654
6789
Arun
4567
4324
Here, I want to compare column B and C and if they match then give 1 else 0 in column same and else different (which we have to create).
So here the catch:
if column B has 1234-5678 and column C has 1234, then the column should match considering only the number before the "-" in the value.
The output should be :
Name
B
C
same
different
Arun
1234-5678
1234
1
0
Tara
6789 - 7654
6789
1
0
Arun
4567
4324
0
1
Also, I want to count the values of 1 for each values in Name for same and different columns.
So far I've tried this:
SELECT
name,
b,
c ,
if(b = c, 1, 0) as same,
if (b!=c,1,0) as different,
count(same),
count(different)
From Table
using "MySQL" (will work almost same with SQL server as well) here's the possible solution.
Step 1) Setup table
CREATE TABLE Users (
Name varchar(50),
B varchar(50),
C varchar(50)
);
INSERT INTO Users
VALUES
('Arun', '1234-5678', '1234'),
('Tara', '6789-7654', '6789'),
('Arun', '4567', '4324');
Step 2) same & different columns
SELECT
Name, B, C,
CASE WHEN SUBSTRING_INDEX(B, "-", 1) = C THEN 1 ELSE 0 END as same,
CASE WHEN SUBSTRING_INDEX(B, "-", 1) <> C THEN 1 ELSE 0 END as different
FROM
Users
Step 3) Join both results to get total_same & total_different for each user
SELECT
Name,
SUM(CASE WHEN SUBSTRING_INDEX(B, "-", 1) = C THEN 1 ELSE 0 END) as total_same,
SUM(CASE WHEN SUBSTRING_INDEX(B, "-", 1) <> C THEN 1 ELSE 0 END) as total_different
FROM
Users
GROUP BY Name
Reference: SQL Fiddle
For the first step, you will need to SUBSTR the column b.
We start at position 1 and we want 4 characters (only works if there's only 4 characters before the '-').
With table2 as (
select name, b,c, same, different from (select name, b, c, case when (SUBSTR(b,1,4) = c)
then '1' else '0' end as same, case when(SUBSTR(b,1,4)!= c) then '1' else '0' end as different
from Table1
group by name, b,c))
The WITH clause can be used when you have complex query, and if you want to create a temporary table in order to use it after.
The Table2 give you this :
After the WITH clause, you will have the second step, the count of same / different per name :
Select table1.name,count(table2.same+table2.different) as total from table1
join table2 on (table2.name = table1.name and table2.b = table1.b)
group by table1.name;
The output give you the total per name (the name are group by, so in your example you will only have 2 rows, one for Arun with a total of 2 (same + different) and the other one with a total of 1)
So here's the entire code :
with table2 as (
select name, b,c, same, different from (select name, b, c, case when (SUBSTR(b,1,4) = c) then '1' else '0' end as same, case when(SUBSTR(b,1,4)!= c) then '1' else '0' end as different
From Table1
group by name, b,c))
select table1.name, table1.b, table1.c, count(table2.same+table2.different) as total from table1
join table2 on (table2.name = table1.name and table2.b = table1.b)
group by table1.name;

Selecting a group with or without certain conditions across many rows in SQL

I have data like this:
ID SomeVar
123 0
123 1
123 2
234 1
234 2
234 3
456 3
567 0
567 1
I'm trying to group by my ID to to return all of the IDs that do not have a record with the value 0. That is, my selection would look like this:
ID
234
456
Is there an easy way to do this without creating a subset table with all records not containing 0 then joining it back to the full data set where the tables don't match?
I generally try to avoid subqueries, but you could use one for this case. Do the same group by, and check that the id isn't in a subquery of ids that have 0 for SomeVar. In this case, distinct will do the same and more efficiently, so I'll do that first:
SELECT DISTINCT ID
FROM [table_name]
WHERE ID NOT IN (
SELECT ID FROM [table_name] WHERE SomeVar = 0
);
And if you want to get other information by using a GROUP BY:
SELECT ID, max(SomeVar), count(*), sum(SomeVar)
FROM [table_name]
WHERE ID NOT IN (
SELECT ID FROM [table_name] WHERE SomeVar = 0
)
GROUP BY ID;
You can use aggregation and having:
select id
from t
group by id
having min(somevar) > 0;
This assumes that somevar is never negative. If that is a possibility, then you can use the slightly more verbose:
select id
from t
group by id
having sum(case when somevar = 0 then 1 else 0 end) = 0;
Use case statement with count or sum aggregation, filter by count using having:
select ID
from
(
select ID, count(case when SomeVar=0 then 1 end) cnt
from mytable
group by ID having count(case when SomeVar=0 then 1 end) = 0
) s
;

Create a query that counts instances in a related table

I have re-written this query about 20 times today and I keep getting close but no dice... I'm sure this is easy-peasy for y'all, but my SQL (Oracle) is pretty rusty.
Here's what I need:
PersonID Count1 Count2 Count3 Count4
1 0 0 2 1
2 1 1 1 0
3 1 1 1 2
Data is coming from several sources. I have a table People, and a table Values. People can have any number of values in that table.
PersonID Item Value
1 Check1 3
1 Check2 3
1 Check3 4
2 Check4 2
2 Check5 3
2 Check6 1
.. etc
So the query would, for each PersonID, count how many times the particular Value appears. The values are always 1, 2, 3, or 4. I tried to do 4 subqueries, but it wouldn't read the PersonID from the main query and just returned the count of all instances of value=1.
I was then thinking do a Group_By ... I don't know. Any help is appreciated!
ETA: I've deleted & re-written the query many times in many ways and unfortunately did not save any intermediate attempts. I didn't include it originally because I was in the middle of rearranging it again, and it's not runnable as-is. But here it is as it stands now:
/*sources are the tested requirements
values are the scores people received on the tested sources
people are those who were tested on the requirements */
WITH sub_query4 (
SELECT values.personid,
count (values.ID) as count4 --how many 4s
FROM values
INNER JOIN sources ON values.valueid = sources.sourceid
INNER JOIN people ON people.personid = values.personid
WHERE values.yearid = 2017
AND values.quarter = 'Q1'
AND instr (sources.identifier, 'TESTBANK.01', 1 ,1) > 0
AND values.value = '4'
GROUP_BY people.personid
)
SELECT p.first_name,
p.last_name,
p.position,
p.email,
p.locationid,
sub_query4.count4 as count4 --eventually this would repeat for 1, 2, & 3
FROM people p
WHERE p.locationid=406
AND p.position in (9,10);
values is a bad name for a table because it is a SQL keyword.
In any case, conditional aggregation should work:
select personid,
sum(case when value = 1 then 1 else 0 end) as cnt_1,
sum(case when value = 2 then 1 else 0 end) as cnt_2,
sum(case when value = 3 then 1 else 0 end) as cnt_3,
sum(case when value = 4 then 1 else 0 end) as cnt_4
from values
group by personid;
I prefer to use PIVOT for this. Here is Example SQL Fiddle
SELECT "PersonID", val1,val2,val3,val4 FROM
(
SELECT "PersonID", "Value" from VALS
)
PIVOT
(
count("Value")
FOR "Value" IN (1 as val1, 2 as val2, 3 as val3, 4 as val4)
);

how can I add a new column where there may be matches on an id field

I currently have a table that contains an id, and a count of a criteria for that id field. For example my table looks like this:
ID Banana_count
1 13
2 23
3 56
The original counts came from a join and a query from other tables.
create FRUIT_TABLE as
select id, count (fruit)
from my_table a
where exists (select null from DATE_FED b
where a.id = b.id
and date = (2/11/17)
and fruit_type = 'banana')
group by id;
My question is, how can i add other attributes to this particular table so that it looks like:
ID Banana_count Apple_count Orange_count
1 13 35 22
2 23 44
3 56
4 33 55
5 11
I will have to add more ids to FRUIT_TABLE that may not already be in the current table, but for fruits that are currently associated with an id, i'd like to add them in the same row.
This is a classic use case for merge:
merge into fruit_table
using apple_table
on (fruit_table.id = apple_table.id)
when matched then update set
fruit_table.apples = apple_table.apples
when not matched then insert (id,apples)
values(
apple_table.id,
apple_table.apples
);
I have simplified the problem slightly so that you are inserting from a table that simply has ids and a count of apples, so that the structure of the merge is clearer. But you can insert a subquery instead into the using... section of the statement to meet your actual requirements.
I would look into something like the following [you didn't provide your table definitions, or other application or requirements constraints so an exact answer is not possible]:
create FRUIT_TABLE as
select id
, sum(case when fruit_type = 'banana' then 1 else 0 end ) Banana_count
, sum(case when fruit_type = 'apple' then 1 else 0 end ) apple_count
, sum(case when fruit_type = 'orange' then 1 else 0 end ) orange_count
from my_table a
group by id;

How do I modify this query without increasing the number of rows returned?

I've got a sub-select in a query that looks something like this:
left outer join
(select distinct ID from OTHER_TABLE) as MYJOIN
on BASE_OBJECT.ID = MYJOIN.ID
It's pretty straightforward. Checks to see if a certain relation exists between the main object being queried for and the object represented by OTHER_TABLE by whether or not MYJOIN.ID is null on the row in question.
But now the requirements have changed a little. There's another row in OTHER_TABLE that can have a value of 1 or 0, and the query needs to know whether a relation exists between the primary for a 1-value, and also if it exists for a 0 value. The obvious solutions is to put:
left outer join
(select distinct ID, TYPE_VALUE from OTHER_TABLE) as MYJOIN
on BASE_OBJECT.ID = MYJOIN.ID
But that would be wrong because if 0-type and 1-type objects both exist for the same ID, it will increase the number of rows returned by the query, which isn't acceptable. So what I need is some sort of subselect that will return 1 row for each distinct ID, with a "1-type exists" column and a "0-type exists" column. And I have no idea how to code that in SQL.
For example, for the following table,
ID | TYPE_VALUE
_________________
1 | 1
3 | 0
3 | 1
4 | 0
I'd like to see a result set like this:
ID | HAS_TYPE_0 | HAS_TYPE_1
______________________________
1 | 0 | 1
3 | 1 | 1
4 | 1 | 0
Anyone know how I could set up a query to do this? Hopefully with a minimum of ugly hacks?
In the general case, you would use EXISTS:
SELECT DISTINCT ID,
CASE WHEN EXISTS (
SELECT * FROM Table1 y
WHERE y.TYPE_VALUE = 0 AND ID = x.ID)
THEN 1
ELSE 0 END AS HAS_TYPE_0,
CASE WHEN EXISTS (
SELECT * FROM Table1 y
WHERE y.TYPE_VALUE = 1 AND ID = x.ID)
THEN 1
ELSE 0 END AS HAS_TYPE_1
FROM Table1 x;
If you have a very large number of elements in the table, this won't perform so great - those nested subselects are often a kiss of death when it comes to performance.
For your specific case, you could also use GROUP BY and MAX() and MIN() to speed things up:
SELECT
ID,
CASE WHEN MIN(TYPE_VALUE) = 0 THEN '1' ELSE 0 END AS HAS_TYPE_0,
CASE WHEN MAX(TYPE_VALUE) = 1 THEN '1' ELSE 0 END AS HAS_TYPE_1
FROM Table1
GROUP BY ID;
Instead of select distinct ID, TYPE_VALUE from OTHER_TABLE
use
select ID,
MAX(CASE WHEN TYPE_VALUE =0 THEN 1 END) as has_type_0,
MAX(CASE WHEN TYPE_VALUE =1 THEN 1 END) as has_type_1
from OTHER_TABLE
GROUP BY ID;
You can do the same using PIVOT opearator...