I want to flip all rows to columns. So if have following 5 columns
with 3 data rows
ACCT_ID NAME PHONE MOBILE ALTERNATIVE_NAME
01 JOE BROWN 0456-9992-6666 07767828432 ZOE BROWN
02 GILL SHARP 0456-9992-6666 07763928432 BILL SHARP
03 ZAC LOWE 0236-9992-5644 07663925672 LUKE LOWE
I want the result set to look as follows. So have 3 columns and 5 rows.
COL headings (COL1 - COL3) are only added to make clearer, i don't need
columns headings
COL1 COL2 COL3
01 02 03
JOE BROWN GILL SHARP ZAC LOWE
0456-9992-6666 0456-9992-6666 0236-9992-5644
07767828432 07763928432 07663925672
ZOE BROWN BILL SHARP LUKE LOWE
The following SQL will work if know the acct_id's. But i wont know the acct_id, so need to write some dynamic sql to select the acct_id from table REPORTER.TEMP_PSR_REGION and then do the pivot/unpivot.
CREATE TABLE REPORTER.TEMP_PSR_REGION
(
ACCT_ID VARCHAR(50) NOT NULL,
NAME VARCHAR(50) NOT NULL,
PHONE VARCHAR(50) NOT NULL,
MOBILE VARCHAR(50) NOT NULL,
ALTERNATIVE_CONTACT VARCHAR(50) NOT NULL
)
INSERT INTO TEMP_PSR_REGION
(ACCT_ID,
NAME,
PHONE,
MOBILE,
ALTERNATIVE_CONTACT)
VALUES ('01',
'JOE BROWN',
'0456-9992-6666',
'07767828432',
'ZOE BROWN')
INSERT INTO TEMP_PSR_REGION
(ACCT_ID,
NAME,
PHONE,
MOBILE,
ALTERNATIVE_CONTACT)
VALUES ('02',
'GILL SHARP',
'0456-9992-6666',
'07763928432',
'BILL SHARP')
INSERT INTO TEMP_PSR_REGION
(ACCT_ID,
NAME,
PHONE,
MOBILE,
ALTERNATIVE_CONTACT)
VALUES ('03',
'ZAC LOWE',
'0236-9992-5644',
'07663925672',
'LUKE LOWE')
--- PIVOT/UNPIVOT Example
select col1, col2, col3
from (
select t.*, t.acct_id as col_id
from TEMP_PSR_REGION t
)
unpivot
(
value FOR heading in (acct_id, name, phone, mobile, ALTERNATIVE_CONTACT)
)
pivot
(
max(value) for col_id in ('01' as col1, '02' as col2, '03' as col3)
)
order by case heading
when 'ACCT_ID' then 1
when 'NAME' then 2
when 'PHONE' then 3
when 'MOBILE' then 4
when 'ALTERNATIVE_CONTACT' then 5
end
What would the dynamic sql look like to do this. I have not written dynamic sql before?
You can get the pivot's IN clause with a query:
select listagg('''' || acct_id || ''' as col' || rownum, ', ') within group (order by acct_id)
from TEMP_PSR_REGION;
LISTAGG(''''||ACCT_ID||'''ASCOL'||ROWNUM,',')WITHINGROUP(ORDERBYACCT_ID)
------------------------------------------------------------------------
'01' as col1, '02' as col2, '03' as col3
You can then use that as part of a query to generate the dynamic SQL statement, and then open that statement as a ref cursor. In this example I'm using a SQL*Plus (or SQL Developer or SQLcl) client refcursor variable to simplify how to declare and sisplay it:
var rc refcursor
declare
l_sql varchar2(32767);
begin
select
q'[
select *
from (
select t.*, t.acct_id as col_id
from TEMP_PSR_REGION t
)
unpivot
(
value FOR heading in (acct_id, name, phone, mobile, ALTERNATIVE_CONTACT)
)
pivot
(
max(value) for col_id in (]'
|| listagg('''' || acct_id || ''' as col' || rownum, ', ') within group (order by acct_id)
|| q'[)
)
order by case heading
when 'ACCT_ID' then 1
when 'NAME' then 2
when 'PHONE' then 3
when 'MOBILE' then 4
when 'ALTERNATIVE_CONTACT' then 5
end]'
into l_sql
from TEMP_PSR_REGION;
dbms_output.put_line(l_sql); -- for debugging only
open :rc for l_sql;
end;
/
Most of that is your original query. Essentially it puts your query into a string variable, but changes
select col1, col2, col3
from (
...
to
q'[
select *
from (
...]'
though you could generate that select-list with another listagg() if you prefer; and more importantly:
...
pivot
(
max(value) for col_id in ('01' as col1, '02' as col2, '03' as col3)
)
...
to:
q'[...
pivot
(
max(value) for col_id in (]'
|| listagg('''' || acct_id || ''' as col' || rownum, ', ') within group (order by acct_id)
|| q'[)
)
...]'
When that block is executed the dbms_output debug shows the generated statement, which is the same as your original once listagg() has been evaluated:
select *
from (
select t.*, t.acct_id as col_id
from TEMP_PSR_REGION t
)
unpivot
(
value FOR heading in (acct_id, name, phone, mobile, ALTERNATIVE_CONTACT)
)
pivot
(
max(value) for col_id in ('01' as col1, '02' as col2, '03' as col3)
)
order by case heading
when 'ACCT_ID' then 1
when 'NAME' then 2
when 'PHONE' then 3
when 'MOBILE' then 4
when 'ALTERNATIVE_CONTACT' then 5
end
PL/SQL procedure successfully completed.
And you can display the opened ref cursor:
print rc
COL1 COL2 COL3
-------------------------------------------------- -------------------------------------------------- --------------------------------------------------
01 02 03
JOE BROWN GILL SHARP ZAC LOWE
0456-9992-6666 0456-9992-6666 0236-9992-5644
07767828432 07763928432 07663925672
ZOE BROWN BILL SHARP LUKE LOWE
Using rownum is slightly dodgy as the headings might be out of order (as there is no ordering), but it seems you don't care about those anyway. If it does turn out to matter you can use a subquery to assign a numeric index to each row in the base table and then use that value instead of rownum.
You could potentially put this in a function that returns a ref cursor, but it depends how you're going to call it and use the results. The var and print are just a demonstration and probably not what you would really use.
You might also want to consider letting a reporting layer, if you have one, deal with the pivoting.
Related
I would like to know if, and if yes, how I could accomplsh the following:
Lets say I have two tables:
Table A has two Columns: id, name
Table B columns: owner, argument
Now I am trying to find in table A all rows with specific name (animal) and use their ids to find it's argument value in table b. Those argument values are different ids in table a. So as a result I would like to get two columns. first has the id of the items who has the specific name (animal) I am looking for and second column has the name of the item which has the id that is argument of the initial ids.
table a (example)
id || name
1 || animal
2 || animal
3 || animal
4 || animal
15 || cat
16 || dog
17 || horse
18 || bird
...
table b (example)
owner || argument
1 || 15
2 || 16
3 || 17
4 || 18
...
result (example)
id || name
1 || cat
2 || dog
3 || horse
4 || bird
Thanks in advance for any hints / help.
Andreas
You need a double join from tablea to tableb and again doublea:
select
a.name ownwename,
t.name name
from tablea a
inner join tableb b
on b.owner = a.id
inner join tablea t
on t.id = b.argument
where a.name = 'animal'
See the demo
I believe the following will do what you want
SELECT owner, name FROM tableb JOIN tablea ON argument = id;
However, as using a subquery you could use :-
SELECT owner, (SELECT name FROM tablea WHERE argument = id) AS name FROM tableb;
Working Example :-
DROP TABLE If EXISTS tablea;
CREATE TABLE IF NOT EXISTS tablea (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO tablea (name) VALUES ('animal'),('animal'),('animal'),('animal'),('cat'),('dog'),('horse'),('bird'),
('animal'),('cat'),('dog'),('horse'),('bird'),('animal'),
('cat'),('dog'),('horse'),('bird') -- id's 15-18 inclusive
;
DROP TABLE IF EXISTS tableb;
CREATE TABLE IF NOT EXISTS tableb (owner INTEGER PRIMARY KEY, argument INTEGER);
INSERT INTO tableb (argument) VALUES(15),(16),(17),(18);
SELECT owner, name FROM tableb JOIN tablea ON argument = id;
SELECT owner, (SELECT name FROM tablea WHERE argument = id) AS name FROM tableb;
Results :-
and the second
In DB2 is there a way to basically say:
case when sku (select * from table1 where tb1field = 'SMOMD') then 'True' end
Okay so this is my query so far, I've been going at this for at least a month now so any help would be great.
select tb4.customer, tb4.sku, tb4.qty, tb4.retqty, tb4.stipqty, tb4.lastdate, tb4.firstdate, tb4.stipdate
from(
--Table 4
select tb3.Customer as Customer, tb3.sku as SKU, tb3.qty as Qty, tb3.retqty as RetQty, tb3.stipqty as STIPQty,
case when tb3.lastdate is null then '00/0000' else substr(tb3.lastdate,5,2)||'/'||substr(tb3.lastdate,1,4) end as LastDate,
case when tb3.firstdate is null then '00/0000' else substr(tb3.firstdate,5,2)||'/'||substr(tb3.firstdate,1,4) end as FirstDate,
case when tb3.stipdate is null then '00/0000' else substr(tb3.stipdate,5,2)||'/'||substr(tb3.stipdate,1,4) end as STIPDate
from(
--Table 3
select tb2.Customer as Customer, tb2.SKU as SKU, tb2.Qty as Qty, tb2.RetQty as RetQty, tb2.STIPQty as STIPQty,
max(case when tb2.TranID in ('010','100') then tb2.datenum end) as LastDate,
min(case when tb2.TranID in ('010','100') then tb2.datenum end) as FirstDate,
case when tb2.RC = '4M' then tb2.datenum end as STIPDate
from(
--Table 2
select tb1.Customer as Customer, tb1.SKU as SKU,
sum(case when tb1.TranID in ('010','100') then abs(tb1.OrdNet) else '0' end) as Qty,
sum(case when tb1.TranID = '500' and tb1.rc != '4M' then abs(tb1.OrdNet) else '0' end) as RetQty,
count(case when tb1.rc = '4M' then tb1.sku end) as STIPQty,
tb1.datenum as datenum, tb1.TranID as tranid, tb1.RC as rc
from(
--Table 1
select distinct stkund as Customer, sthptg||space(1)||stmodl||space(1)||stvari||space(1)||stfarb||space(1)||stgroe as SKU,
stvorg as TranID, stggru as RC, stprg09 as PG9, stprg08 as PG8, stperi as datenum, ormne1 as OrdNet
from st_usus.s_stati_pv
join caspdtau.cospf440 on stadrn = jadr40
where trim(stvert) in ('111S','122S')
and sthptg != 'V'
and aktv40 = 'A'
and stprg01 in ('01','04')
and stprg02 = '01'
and stvorg in ('500','010','100')
and stperi >= '20160100'
) as tb1
group by tb1.Customer, tb1.SKU, tb1.datenum, tb1.tranid, tb1.rc
) as tb2
group by tb2.customer, tb2.sku, tb2.qty, tb2.retqty, tb2.stipqty, tb2.tranid, tb2.rc, tb2.datenum
) as tb3
group by tb3.customer, tb3.sku, tb3.qty, tb3.retqty, tb3.stipqty, tb3.lastdate, tb3.firstdate, tb3.stipdate
) as tb4
order by tb4.Customer, tb4.sku
I'm not going to try to decipher exactly what you're trying to do...
Some general advice, rather than using Nested Table Expressions (NTE)
select <..> from (select <...>from mytable)
Consider Common Table Expressions (CTE)
with
table1 as (select <...> from st_usus.s_stati_pv join caspdtau.cospf440 on stadrn = jadr40)
, table2 as (select <...> from table1)
, table3 as (select <...> from table2)
, table4 as (select <....> from table3)
select <...> from table4;
Each CTE (ie. tableX) can refer to a prior CTE or a physical table/view as needed. The final select can refer to one or more CTE's along with one or more physical tables or views.
Nice thing about building with CTE's, is that you can check your results after each step..
with
table1 as (select <...> from st_usus.s_stati_pv join caspdtau.cospf440 on stadrn = jadr40)
select * from table1;
I have a table call PAYMENT_DET with the fields PAYMENT_DATE DATE, PAYMENT_TIME VARCHAR2 and its data is as shown below where date format is MM/DD/YYYY
PAYMENT_DATE PAYMENT_TIME
2/13/2017 13:03:59
2/13/2017 14:03:59
2/14/2017 01:03:59
2/14/2017 04:03:00
My requirement is, I have two input dates like from date with time and To Date with time. Between the given inputs range i want get details from the above table.
Please suggest me query for this.
A very simple example to help you out. Hope this helps.
SELECT *
FROM
(SELECT 1 col1, '02/13/2017' col2, '13:03:59' col3 FROM dual
UNION
SELECT 2 col1, '02/13/2017' col2, '14:03:59' col3 FROM dual
UNION
SELECT 3 col1, '02/14/2017' col2, '01:03:59' col3 FROM dual
)a
WHERE to_date(a.col2
||' '
||col3,'mM/dd/yyyy hh24:mi:ss') BETWEEN TO_DATE('01/01/2017 00:00:00','mm/dd/yyyy hh24:mi:ss') AND TO_DATE('01/01/2018 00:00:00','mm/dd/yyyy hh24:mi:ss');
I'm using Teradata, I have a table like this
ID String
123 Jim
123 John
123 Jane
321 Jill
321 Janine
321 Johan
I want to query the table so I get
ID String
123 Jim, John, Jane
321 Jill, Janine, Johan
I tried partition but there can be many names.
How do I get this result. Even, to point me in the right direction would be great.
Unfortunately there's no PIVOT in Teradata (only a TD_UNPIVOT in 14.10).
If you got luck there's an aggregate UDF at your site to do a group concat (probably low possibility).
Otherwise there are two options: recursion or aggregation.
If the maximum number of rows per id is known aggregation is normally faster. It's a lot of code, but most of it is based on cut&paste.
SELECT
id,
MAX(CASE WHEN rn = 1 THEN string END)
|| MAX(CASE WHEN rn = 2 THEN ',' || string ELSE '' END)
|| MAX(CASE WHEN rn = 3 THEN ',' || string ELSE '' END)
|| MAX(CASE WHEN rn = 4 THEN ',' || string ELSE '' END)
|| ... -- repeat up to the known maximum
FROM
(
SELECT
id, string,
ROW_NUMBER()
OVER (PARTITION BY id
ORDER BY string) AS rn
FROM t
) AS dt
GROUP BY 1;
For large tables it's much more efficient when you materialize the result of the Derived Table in a Volatile Table first using the GROUP BY column as PI.
For recursion you should use a Volatile Table, too, as OLAP functions are not allowed in the recursive part. Using a view instead will repeatedly calculate the OLAP function and thus result in bad performance.
CREATE VOLATILE TABLE vt AS
(
SELECT
id
,string
,ROW_NUMBER()
OVER (PARTITION BY id
ORDER BY string DESC) AS rn -- reverse order!
,COUNT(*)
OVER (PARTITION BY id) AS cnt
FROM t
) WITH DATA
UNIQUE PRIMARY INDEX(id, rn)
ON COMMIT PRESERVE ROWS;
WITH RECURSIVE cte
(id, list, rn) AS
(
SELECT
id
,CAST(string AS VARCHAR(1000)) -- define maximum size based on maximum number of rows
,rn
FROM vt
WHERE rn = cnt
UNION ALL
SELECT
vt.id
,cte.list || ',' || vt.string
,vt.rn
FROM vt
JOIN cte
ON vt.id = cte.id
AND vt.rn = cte.rn - 1
)
SELECT id, list
FROM cte
WHERE rn = 1;
There's one problem with this approach, it might need a lot of spool which is easy to see when you omit theWHERE rn = 1.
SELECT ID,
TRIM(TRAILING ',' FROM (XMLAGG(TRIM(String)|| ',' ORDER BY String) (VARCHAR(10000)))) as Strings
FROM db.table
GROUP BY 1
SQL Server 2017+ and SQL Azure: STRING_AGG
Starting with the next version of SQL Server, we can finally concatenate across rows without having to resort to any variable or XML witchery.
STRING_AGG (Transact-SQL)
SELECT ID, STRING_AGG(String, ', ') AS Strings
FROM TableName
GROUP BY ID
I have a table in my database, user_answers that stores users answers to a series of questions, with rows; user_id, question_id, answer_id and text_entry. Question text and answer text (if any) are stored in lookup tables. There are three types of questions, single-answer questions, multiple-answer questions and text-entry answer questions. So a single user might have entries like the following in the user_answers table:
user_id question_id answer_id text_entry
------- ----------- --------- ----------
123 100 1010 (null)
123 200 2010 (null)
123 200 2030 (null)
123 300 3000 "code 789"
Lets say the questions_text table has:
question_id text
----------- -------------
100 "Gender"
200 "Interests"
300 "Your code"
and the answers_text table has:
answer_id text
--------- -----------
1010 "Female"
1020 "Male"
2010 "Sports"
2020 "Computers"
2030 "Movies"
3000 (null)
I want to extract the data into a csv with one line per user_id showing the answers, something like this:
User,Gender,Sports,Computers,Movies,Code
123,Female,1,0,1,code 789
I know how to generate the CSV file via SQLPlus (I only have access to the DB via SQLPlus for reasons beyond my control...) but I don't know how to generate the PL/SQL statement.
In PL/SQL I know I can generate a pivot of the Gender question by doing
SELECT
user_id || ',' ||
MIN(DECODE(question_id, '100', (SELECT text FROM answers_text where answer_id = answer_text.answer_id)))
FROM user_answers
GROUP BY user_id
ORDER BY user_id
;
(I'm not an SQL guy, so this is copied off the internets!)
This code is (at least as far as my testing is telling me) good for the single-answer questions but will not work on multiple-answer or text-entry type questions.
I saw some stuff online about using the case statement in PL/SQL like so:
MIN(CASE WHEN question_id = '200' AND answer_id = '2010' THEN '1' ELSE '0' END)
...but I can't figure out how to get the answers into columns. And all the SO questions I can find that might be related are sql-server specific.
Is there a way to generate my desired output from a single PL/SQL statement? Preferably written in a way that does not depend on the data in the tables as we have a number of databases that this might need to be run on.
To accomplish what you are looking for (and not be specific to this data) I believe you are going to need some extra fields in your tables. For example, you will need to know which questions are Single-Answer, Multi-Answer, and Text-Entry without having to look at the data. You will also need to know which answers are possible for your Multi-answer questions without having to link through the data. From there, you can loop through the meta information about each question / answer combination and build yourself a query to run that will return the data in your desired format. Something like:
/* Create Tables with Data - Note 2 new columns added to questions_text */
create table user_answers
as
select 123 user_id, 100 question_id, 1010 answer_id, null text_entry from dual
union all
select 123 user_id, 200 question_id, 2010 answer_id, null text_entry from dual
union all
select 123 user_id, 200 question_id, 2030 answer_id, null text_entry from dual
union all
select 123 user_id, 300 question_id, 3000 answer_id, 'code 789' text_entry from dual;
create table questions_text
as
select 100 question_id, 'Gender' question_text, 'S' question_type, 1000 answer_set_id from dual
union all
select 200 question_id, 'Interests' question_text, 'M' question_type, 2000 answer_set_id from dual
union all
select 300 question_id, 'Your code' question_text, 'T' question_type, 3000 answer_set_id from dual;
create table answers_text
as
select 1010 answer_id, 'Female' text, 1000 answer_set_id from dual
union all
select 1020 answer_id, 'Male' text, 1000 answer_set_id from dual
union all
select 2010 answer_id, 'Sports' text, 2000 answer_set_id from dual
union all
select 2020 answer_id, 'Computers' text, 2000 answer_set_id from dual
union all
select 2030 answer_id, 'Movies' text, 2000 answer_set_id from dual
union all
select 3000 answer_id, null text, 3000 answer_set_id from dual;
/* PL/SQL for creating SQL statement to return data in desired format */
declare
v_sql VARCHAR2(32767);
begin
v_sql := 'select ua.user_id "User",';
FOR question IN (
select question_id, question_text, question_type, answer_set_id
from questions_text
)
LOOP
IF question.question_type = 'M'
THEN
FOR answer IN (
select answer_id, text
from answers_text
where answer_set_id = question.answer_set_id
)
LOOP
v_sql := v_sql||chr(10)||'max(case when ua.question_id = '||question.question_id||' and ua.answer_id = '||answer.answer_id||' then 1 else 0 end) "'||answer.text||'",';
END LOOP;
ELSIF question.question_type = 'S'
THEN
v_sql := v_sql||chr(10)||'min(case when ua.question_id = '||question.question_id||' then at.text end) "'||question.question_text||'",';
ELSIF question.question_type = 'T'
THEN
v_sql := v_sql||chr(10)||'min(case when ua.question_id = '||question.question_id||' then ua.text_entry end) "'||question.question_text||'",';
END IF;
END LOOP;
v_sql := rtrim(v_sql,',');
v_sql := v_sql||' from
user_answers ua
inner join questions_text qt
on qt.question_id = ua.question_id
inner join answers_text at
on at.answer_id = ua.answer_id
group by
ua.user_id';
-- replace dbms_output with code to write file
dbms_output.put_line(v_sql);
END;
Queries with an unknown number of columns are problematic at best. Will you really not know what the data will look like? You might want to look at this Ask Tom response for a package which might help get you the results you need.