How to convert result of query into count aggregate of each row - sql

Here is the result of the query:
LNAME | LISTAGG
--------------+---------------
| ALEX
BAIRSTOW |
BROAD | STUART
BUTLER |
COOK | ALAISTER,ALEX
HALES | ALEX
JENNINGS |
--------------+---------------
(7 rows)
I would like to take result in 0, 1 or count of entries in the row like (ALAISTER,ALEX) are 2 and empty format.
So the output should be like:
LNAME | LFNAME | LNAME_COUNT | LFNAME_COUNT
----------+---------------+--------------+-------------
BROAD | STUART | 1 | 1
BAIRSTOW | | 1 | 0
COOK | ALAISTER,ALEX | 1 | 2
| ALEX | empty | 1
JENNINGS | | 1 | empty
HALES | ALEX | 1 | 1
BUTLER | | 1 | 0
----------+---------------+--------------+-------------
(7 rows)
I've used a case expression, but couldn't break out the (ALAISTER,ALEX) part.

In SQL Server, you can do this using LEN().
SELECT LNAME, LISTAGG AS LFNAME,
CASE WHEN LNAME IS NULL THEN CAST(0 AS VARCHAR (5))
WHEN LEN(LNAME) > 1 THEN CAST((LEN(LNAME) - LEN(REPLACE(LNAME, ',', ''))) + 1 AS VARCHAR (5))
ELSE 'empty' END AS LNAME_COUNT,
CASE WHEN LISTAGG IS NULL THEN CAST(0 AS VARCHAR (5))
WHEN LEN(LISTAGG) > 1 THEN CAST((LEN(LISTAGG) - LEN(REPLACE(LISTAGG, ',', ''))) + 1 AS VARCHAR (5))
ELSE 'empty' END AS LFNAME_COUNT
FROM TableName
Demo on db<>fiddle for SQL Server
If your DBMS is MySQL, simply change the LEN() to LENGTH()

The following query will do what you are looking for
Declare #table table ( lname varchar (20), listagg varchar (20));
insert into #table ( lname , listagg ) values
('' , 'alex'),
('bairstow', null),
('broad' , 'stuart'),
('butler' , ''),
('cook' , 'alaister,alex'),
('hales' , 'alex'),
('jennings', null);
select
lname,
listagg as LFName,
case when lname <>'' then len(lname)-len(replace(lname,',',''))+1 else 0 end as LName_count,
case when listagg <>'' then len(listagg)-len(replace(listagg,',',''))+1 else 0 end as LFName_count
from #table

Related

Calculating age from a varchar column with discrepancies

Hoping you had a restful Easter. Appreciate if you could advice/help me in the following. (Using Function/without function)
Below are my DataSet, Desired Output (With derived Age using the DOB specifications from the rules)
Your help is needed in (Please note that i looking for solution in MSSQL environment):-
1. Coming up with Age field. (I tried the following script but it didn't work as it is not dynamic enough to include all the DOB rules, i also attached an oracle script which worked as a reference for you guys)
SELECT
[ID],
[DOB],
'age' = DATEDIFF(HOUR,(CONVERT(date,(CASE WHEN ([DOB] like '99/%/%') THEN (REPLACE([DOB],'99','01'))
ELSE [DOB] END),103)),GETDATE())/8766
from [Sample]
Sample_Dataset
create table Sample (
Id Varchar (50),
DOB Varchar (50))
insert into Sample(Id, DOB)
Values
('38603', '24/02/1969'),
('38605', '22/09/1969'),
('36356', '17/03/1954'),
('36374', '17/05/1975'),
('36441', '17/08/1961'),
('1a', '10/05/9999'),
('1b', '10/99/9999'),
('1c', '99/99/9999'),
('2a', '--/--/1935'),
('2b', '00/00/1935'),
('2c', '88/88/1935'),
('2d', '99/99/1935'),
('3a', '10/--/1935'),
('3b', '10/00/1935'),
('3c', '10/88/1935'),
('3d', '10/99/1935'),
('4a', '--/09/1935'),
('4b', '00/09/1935'),
('4c', '88/09/1935'),
('4d', '99/09/1935')
Desired output
ID | DOB | Age (As of 05-03-2018; dd-mm-yyyy)
38603 | 24/02/1969 | 49 --Everything is known
38605 | 22/09/1969 | 48
36356 | 17/03/1954 | 63
36374 | 17/05/1975 | 42
36441 | 17/08/1961 | 56
1a | 10/05/9999 |null --unknown year
1b | 10/99/9999 |null
1c | 99/99/9999 |null
2a | --/--/1935 |82 --unknown day and month
2b | 00/00/1935 |82
2c | 88/88/1935 |82
2d | 99/99/1935 |82
3a | 10/--/1935 |82 --unknown month but known year
3b | 10/00/1935 |82
3c | 10/88/1935 |82
3d | 10/99/1935 |82
4a | --/09/1935 |82 --unknown day but known month
4b | 00/09/1935 |82
4c | 88/09/1935 |82
4d | 99/09/1935 |82
Rules:-
As you can see in the above 5 scenarios in the comments
Everything is known (use the stated DOB to calculate the age)
Unknown Year (put age as null as the year is known)
Unknown day and month (Use 01/07 for the unknown dd/mm and the stated yyyy)
Unknown month but known day (Use 07 for the unknown mm and the stated dd/07/yyyy)
Unknown day but known month (Use 15 for the unknown dd and the stated 15/mm/yyyy)
Solution in Oracle
Creating a function first (Tried replicating this logic in T-SQL but unsuccessful, hence i am here)
create or replace function check_dt(in_date in VARCHAR2, in_format in VARCHAR2 default 'DD/MM/YYYY')
RETURN NUMBER
IS
V_DATE DATE;
V_STATUS INTEGER;
BEGIN
SELECT TO_DATE(in_date,in_format)
INTO V_DATECASE
FROM DUAL;
V_STATUS := 0;
RETURN V_STATUS;
EXCEPTION WHEN OTHERS THEN
V_STATUS := SQLCODE;
RETURN V_STATUS;
END;
select check_dt('11/30/2017') from dual;
select TO_DATE('15/--/9999','DD/MM/YYYY') from dual;
select id, dob,
case when check_dt(dob) = -1843 --not valid month, default it to July (07)
THEN substr(dob,1,2)||'/07'||substr(dob,7,4)
when check_dt(dob) = -01847 -- day of month must between 1 and last day of month
THEN '1/07/'||substr(dob,7,4)
WHEN check_dt(dob) = 0 and to_date(dob,'dd/mm/yyyy') > sysdate
THEN NULL
WHEN check_dt(dob) = -0183 -- date not valid for month
THEN '15/'||substr(dob,4)
ELSE
THEN dob
END New_dob
from SAMPLE;
Any help would be much appreciated.
Thank you very Much.
SQL SERVER
SELECT id,
CASE WHEN YEAR(GETDATE())-REVERSE(LEFT(REVERSE(DOB), CHARINDEX('/', REVERSE(DOB)) - 1)) > = 0
THEN
YEAR(GETDATE())-REVERSE(LEFT(REVERSE(DOB), CHARINDEX('/', REVERSE(DOB)) - 1))
ELSE
NULL
END AS Age
FROM Sample
Solution For Your Question
WITH CTE AS
(
SELECT id,
CASE WHEN ISNUMERIC(REVERSE(LEFT(REVERSE(DOB), CHARINDEX('/', REVERSE(DOB)) - 1))) = 1 THEN
REVERSE(LEFT(REVERSE(DOB), CHARINDEX('/', REVERSE(DOB)) - 1))
ELSE
NULL
END
AS Year,
CASE WHEN ISNUMERIC(LEFT(DOB, CHARINDEX('/', DOB) - 1)) = 1 THEN
LEFT(DOB, CHARINDEX('/', DOB) - 1)
ELSE
NULL
END AS DAY,
CASE WHEN ISNUMERIC(SUBSTRING(DOB,CHARINDEX('/',DOB)+1, CHARINDEX('/',DOB,CHARINDEX('/',DOB)+1) -CHARINDEX('/',DOB)-1)) = 1 THEN
CASE WHEN SUBSTRING(DOB,CHARINDEX('/',DOB)+1, CHARINDEX('/',DOB,CHARINDEX('/',DOB)+1) -CHARINDEX('/',DOB)-1) >= 1 AND SUBSTRING(DOB,CHARINDEX('/',DOB)+1, CHARINDEX('/',DOB,CHARINDEX('/',DOB)+1) -CHARINDEX('/',DOB)-1) <= 12 THEN
SUBSTRING(DOB,CHARINDEX('/',DOB)+1, CHARINDEX('/',DOB,CHARINDEX('/',DOB)+1) -CHARINDEX('/',DOB)-1)
ELSE
NULL
END
ELSE
NULL
END AS MONTH
FROM Sample),CTE1 AS
(
SELECT id,
year,
month,
CASE WHEN DAY IS NOT NULL THEN
CASE WHEN DAY >= 1 AND DAY <= DAY(EOMONTH(year+'-'+month+'-01')) THEN
DAY
ELSE
NULL
END
ELSE NULL
END AS Day
FROM CTE
)
,CTE2 AS
(
SELECT id,
CASE WHEN YEAR IS NULL
THEN NULL
ELSE
CASE WHEN DAY IS NULL AND MONTH IS NULL THEN '01/07'
WHEN MONTH IS NULL AND DAY IS NOT NULL THEN CAST(day AS VARCHAR)+'/07'
WHEN MONTH IS NOT NULL AND DAY IS NULL THEN '15/'+CAST(MONTH AS VARCHAR)
ELSE CAST(day AS VARCHAR)+'/'+CAST(MONTH AS VARCHAR)
END
+ '/'+CAST(YEAR AS VARCHAR)
END
AS DOB
FROM CTE1
)
SELECT id,DOB,
CASE WHEN DOB IS NOT NULL
THEN
CASE WHEN DATEDIFF (day, CONVERT(DATE, DOB, 103),CONVERT(DATE,GETDATE(),103)) >=0
THEN FLOOR(DATEDIFF (day, CONVERT(DATE, DOB, 103), CONVERT(DATE,GETDATE(),103)) / 365.2425)
ELSE
NULL
END
ELSE
DOB
END AS Age
FROM CTE2
DEMO LIVE
http://dbfiddle.uk/?rdbms=sqlserver_2017&fiddle=3714d33cacb02c3fce4f0868c9d0990b
You can use the following. I used different CTE's to show you the progression towards obtaining the date of birth from your varchar DOB. I also changed the table to a temporary one.
IF OBJECT_ID('tempdb..#Sample') IS NOT NULL
DROP TABLE #Sample
create table #Sample (
Id Int,
DOB Varchar (50))
insert into #Sample(Id, DOB)
Values
(38603, '24/02/1969'),
(38605, '22/09/1969'),
(36356, '17/03/1954'),
(36374, '17/05/1975'),
(36441, '17/08/1961'),
(119, '10/05/9999'),
(114, '10/99/9999'),
(132, '99/99/9999'),
(25125, '--/--/1935'),
(2323, '00/00/1935'),
(2512, '88/88/1935'),
(2156, '99/99/1935'),
(368, '10/--/1935'),
(34135, '10/00/1935'),
(3435, '10/88/1935'),
(3241, '10/99/1935'),
(4512, '--/09/1935'),
(4161, '00/09/1935'),
(4312, '88/09/1935'),
(456, '99/09/1935')
;WITH ParsedBirth AS
(
SELECT
S.Id,
S.DOB,
Year = SUBSTRING(S.DOB, 7, 4),
Month = SUBSTRING(S.DOB, 4, 2),
Day = SUBSTRING(S.DOB, 1, 2)
FROM
#Sample AS S
),
ParsedBirthInteger AS
(
SELECT
P.Id,
P.DOB,
Year = CASE WHEN ISNUMERIC(P.Year) = 1 AND P.Year <> '9999' THEN CONVERT(INT, P.Year) END,
Month = CASE
WHEN ISNUMERIC(P.Month) = 1 AND CONVERT(INT, P.Month) BETWEEN 1 AND 12 THEN CONVERT(INT, P.Month)
ELSE 7 END,
Day = CASE
WHEN ISNUMERIC(P.Day) = 1 AND CONVERT(INT, P.Day) BETWEEN 1 AND 31 THEN CONVERT(INT, P.Day)
ELSE 15 END
FROM
ParsedBirth AS P
),
InferredBirth AS
(
SELECT
P.Id,
P.DOB,
InferredBirth = CONVERT(DATE, CONVERT(VARCHAR(100), P.Year * 10000 + P.Month * 100 + P.Day))
FROM
ParsedBirthInteger AS P
)
SELECT
T.Id,
T.DOB,
T.InferredBirth,
Age = (CONVERT(INT,CONVERT(char(8), GETDATE(),112))-CONVERT(char(8),T.InferredBirth,112))/10000
FROM
InferredBirth AS T
First of all:
It is a very bad idea to store a date in a culture dependant string format.
It is a very bad idea to use magic values (9999 meaning "no year").
Is is a most dangerous, bad idea to mix this!
The following code will transform your values to the format you should use to store this actually. You can build your age-logic from here, but I'd really recommend you, to use this approach to clean up this mess and store your data properly!
DECLARE #sample TABLE(
Id VARCHAR(10),
DOB VARCHAR (50))
INSERT INTO #sample(Id, DOB)
VALUES
('38603', '24/02/1969'),
('38605', '22/09/1969'),
('36356', '17/03/1954'),
('36374', '17/05/1975'),
('36441', '17/08/1961'),
('1a', '10/05/9999'),
('1b', '10/99/9999'),
('1c', '99/99/9999'),
('2a', '--/--/1935'),
('2b', '00/00/1935'),
('2c', '88/88/1935'),
('2d', '99/99/1935'),
('3a', '10/--/1935'),
('3b', '10/00/1935'),
('3c', '10/88/1935'),
('3d', '10/99/1935'),
('4a', '--/09/1935'),
('4b', '00/09/1935'),
('4c', '88/09/1935'),
('4d', '99/09/1935');
--The query will split your string on the / and try to cast the values to int:
WITH Splitted AS
(
SELECT Id
,DOB
,CAST('<x>' + REPLACE(DOB,'/','</x><x>') + '</x>' AS XML).value('/x[1]','varchar(10)') AS DOB_Day
,CAST('<x>' + REPLACE(DOB,'/','</x><x>') + '</x>' AS XML).value('/x[2]','varchar(10)') AS DOB_Month
,CAST('<x>' + REPLACE(DOB,'/','</x><x>') + '</x>' AS XML).value('/x[3]','varchar(10)') AS DOB_Year
FROM #sample
)
,Casted AS
(
SELECT Id
,DOB
--below SQL-Server 2012 you can use `CASE` with `ISNUMERIC` instead of TRY_CAST
,TRY_CAST(DOB_Day AS INT) AS CastedDay
,TRY_CAST(DOB_Month AS INT) AS CastedMonth
,TRY_CAST(DOB_Year AS INT) AS CastedYear
FROM Splitted
)
,Checked AS
(
SELECT Id
,DOB
--You can use further logic to get the month's days correctly (instead of the plain 31)
,CASE WHEN CastedDay BETWEEN 1 AND 31 THEN CastedDay ELSE NULL END AS TheDay
,CASE WHEN CastedMonth BETWEEN 1 AND 12 THEN CastedMonth ELSE NULL END AS TheMonth
,CASE WHEN CastedYear BETWEEN 1900 AND 2100 THEN CastedYear ELSE NULL END AS TheYear
FROM Casted
)
SELECT *
FROM Checked;
The result
+-------+------------+--------+----------+---------+
| Id | DOB | TheDay | TheMonth | TheYear |
+-------+------------+--------+----------+---------+
| 38603 | 24/02/1969 | 24 | 2 | 1969 |
+-------+------------+--------+----------+---------+
| 38605 | 22/09/1969 | 22 | 9 | 1969 |
+-------+------------+--------+----------+---------+
| 36356 | 17/03/1954 | 17 | 3 | 1954 |
+-------+------------+--------+----------+---------+
| 36374 | 17/05/1975 | 17 | 5 | 1975 |
+-------+------------+--------+----------+---------+
| 36441 | 17/08/1961 | 17 | 8 | 1961 |
+-------+------------+--------+----------+---------+
| 1a | 10/05/9999 | 10 | 5 | NULL |
+-------+------------+--------+----------+---------+
| 1b | 10/99/9999 | 10 | NULL | NULL |
+-------+------------+--------+----------+---------+
| 1c | 99/99/9999 | NULL | NULL | NULL |
+-------+------------+--------+----------+---------+
| 2a | --/--/1935 | NULL | NULL | 1935 |
+-------+------------+--------+----------+---------+
| 2b | 00/00/1935 | NULL | NULL | 1935 |
+-------+------------+--------+----------+---------+
| 2c | 88/88/1935 | NULL | NULL | 1935 |
+-------+------------+--------+----------+---------+
| 2d | 99/99/1935 | NULL | NULL | 1935 |
+-------+------------+--------+----------+---------+
| 3a | 10/--/1935 | 10 | NULL | 1935 |
+-------+------------+--------+----------+---------+
| 3b | 10/00/1935 | 10 | NULL | 1935 |
+-------+------------+--------+----------+---------+
| 3c | 10/88/1935 | 10 | NULL | 1935 |
+-------+------------+--------+----------+---------+
| 3d | 10/99/1935 | 10 | NULL | 1935 |
+-------+------------+--------+----------+---------+
| 4a | --/09/1935 | NULL | 9 | 1935 |
+-------+------------+--------+----------+---------+
| 4b | 00/09/1935 | NULL | 9 | 1935 |
+-------+------------+--------+----------+---------+
| 4c | 88/09/1935 | NULL | 9 | 1935 |
+-------+------------+--------+----------+---------+
| 4d | 99/09/1935 | NULL | 9 | 1935 |
+-------+------------+--------+----------+---------+

Using string_split to create rows from multiple columns

I have data that looks something like this example (on an unfortunately much larger scale):
+----+-------+--------------------+-----------------------------------------------+
| ID | Data | Cost | Comments |
+----+-------+--------------------+-----------------------------------------------+
| 1 | 1|2|3 | $0.00|$3.17|$42.42 | test test||previous thing has a blank comment |
+----+-------+--------------------+-----------------------------------------------+
| 2 | 1 | $420.69 | test |
+----+-------+--------------------+-----------------------------------------------+
| 3 | 1|2 | $3.50|$4.20 | |test |
+----+-------+--------------------+-----------------------------------------------+
Some of the columns in the table I have are pipeline delimited, but they are consistent by each row. So each delimited value corresponds to the same index in the other columns of the same row.
So I can do something like this which is what I want for a single column:
SELECT ID, s.value AS datavalue
FROM MyTable t CROSS APPLY STRING_SPLIT(t.Data, '|') s
and that would give me this:
+----+-----------+
| ID | datavalue |
+----+-----------+
| 1 | 1 |
+----+-----------+
| 1 | 2 |
+----+-----------+
| 1 | 3 |
+----+-----------+
| 2 | 1 |
+----+-----------+
| 3 | 1 |
+----+-----------+
| 3 | 2 |
+----+-----------+
but I also want to get the other columns as well (cost and comments in this example) so that the corresponding items are all in the same row like this:
+----+-----------+-----------+------------------------------------+
| ID | datavalue | costvalue | commentvalue |
+----+-----------+-----------+------------------------------------+
| 1 | 1 | $0.00 | test test |
+----+-----------+-----------+------------------------------------+
| 1 | 2 | $3.17 | |
+----+-----------+-----------+------------------------------------+
| 1 | 3 | $42.42 | previous thing has a blank comment |
+----+-----------+-----------+------------------------------------+
| 2 | 1 | $420.69 | test |
+----+-----------+-----------+------------------------------------+
| 3 | 1 | $3.50 | |
+----+-----------+-----------+------------------------------------+
| 3 | 2 | $4.20 | test |
+----+-----------+-----------+------------------------------------+
I'm not sure what the best or most simple way to achieve this would be
This isn't going to be achievable with STRING_SPLIT as Microsoft refuse to supply the ordinal position as part of the result set. As a result, you'll need to use a different function which does. Personally, I recommend Jeff Moden's DelimitedSplit8k.
Then, you can do this:
CREATE TABLE #Sample (ID int,
[Data] varchar(200),
Cost varchar(200),
Comments varchar(8000));
GO
INSERT INTO #Sample
VALUES (1,'1|2|3','$0.00|$3.17|$42.42','test test||previous thing has a blank comment'),
(2,'1','$420.69','test'),
(3,'1|2','$3.50|$4.20','|test');
GO
SELECT S.ID,
DSd.Item AS DataValue,
DSc.Item AS CostValue,
DSct.Item AS CommentValue
FROM #Sample S
CROSS APPLY dbo.DelimitedSplit8K(S.[Data],'|') DSd
CROSS APPLY (SELECT *
FROM DelimitedSplit8K(S.Cost,'|') SS
WHERE SS.ItemNumber = DSd.ItemNumber) DSc
CROSS APPLY (SELECT *
FROM DelimitedSplit8K(S.Comments,'|') SS
WHERE SS.ItemNumber = DSd.ItemNumber) DSct;
GO
DROP TABLE #Sample;
GO
There is, however, only one true answer to this question: Don't store delimited values in SQL Server. Store them in a normalised manner, and you won't have this problem.
Here is a solution approach using a recursive CTE instead of a User Defined Funtion (UDF) which is useful for those without permission to create functions.
CREATE TABLE mytable(
ID INTEGER NOT NULL PRIMARY KEY
,Data VARCHAR(7) NOT NULL
,Cost VARCHAR(20) NOT NULL
,Comments VARCHAR(47) NOT NULL
);
INSERT INTO mytable(ID,Data,Cost,Comments) VALUES (1,'1|2|3','$0.00|$3.17|$42.42','test test||previous thing has a blank comment');
INSERT INTO mytable(ID,Data,Cost,Comments) VALUES (2,'1','$420.69','test');
INSERT INTO mytable(ID,Data,Cost,Comments) VALUES (3,'1|2','$3.50|$4.20','|test');
This query allows choice of delimiter by using a variable, then using a common table expression it parses each delimited string to produce a rows for each portion of those strings, and retains the ordinal position of each.
declare #delimiter as varchar(1)
set #delimiter = '|'
;with cte as (
select id
, convert(varchar(max), null) as datavalue
, convert(varchar(max), null) as costvalue
, convert(varchar(max), null) as commentvalue
, convert(varchar(max), data + #delimiter) as data
, convert(varchar(max), cost + #delimiter) as cost
, convert(varchar(max), comments + #delimiter) as comments
from mytable as t
union all
select id
, convert(varchar(max), left(data, charindex(#delimiter, data) - 1))
, convert(varchar(max), left(cost, charindex(#delimiter, cost) - 1))
, convert(varchar(max), left(comments, charindex(#delimiter, comments) - 1))
, convert(varchar(max), stuff(data, 1, charindex(#delimiter, data), ''))
, convert(varchar(max), stuff(cost, 1, charindex(#delimiter, cost), ''))
, convert(varchar(max), stuff(comments, 1, charindex(#delimiter, comments), ''))
from cte
where (data like ('%' + #delimiter + '%') and cost like ('%' + #delimiter + '%')) or comments like ('%' + #delimiter + '%')
)
select id, datavalue, costvalue, commentvalue
from cte
where datavalue IS NOT NULL
order by id, datavalue
As the recursion adds new rows, it places the first portion of the delimited strings into the wanted output columns using left(), then also, using stuff(), removes the last used delimiter from the source strings so that the next row will start at the next delimiter. Note that to initiate the extractions, the delimiter is added to the end of the source delimited strings which is to ensure the where clause does not exclude any of the wanted strings.
the result:
id datavalue costvalue commentvalue
---- ----------- ----------- ------------------------------------
1 1 $0.00 test test
1 2 $3.17
1 3 $42.42 previous thing has a blank comment
2 1 $420.69 test
3 1 $3.50
3 2 $4.20 test
demonstrated here at dbfiddle.uk

Convert one row into multiple rows with fewer columns

I'd like to convert single rows into multiple rows in PostgreSQL, where some of the columns are removed. Here's an example of the current output:
name | st | ot | dt |
-----|----|----|----|
Fred | 8 | 2 | 3 |
Jane | 8 | 1 | 0 |
Samm | 8 | 0 | 6 |
Alex | 8 | 0 | 0 |
Using the following query:
SELECT
name, st, ot, dt
FROM
times;
And here's what I want:
name | t | val |
-----|----|-----|
Fred | st | 8 |
Fred | ot | 2 |
Fred | dt | 3 |
Jane | st | 8 |
Jane | ot | 1 |
Samm | st | 8 |
Samm | dt | 6 |
Alex | st | 8 |
How can I modify the query to get the above desired output?
SELECT
times.name, x.t, x.val
FROM
times cross join lateral (values('st',st),('ot',ot),('dt',dt)) as x(t,val)
WHERE
x.val <> 0;
The core problem is the reverse of a pivot / crosstab operation. Sometimes called "unpivot".
Basically, Abelisto's query is the way to go in Postgres 9.3 or later. Related:
SELECT DISTINCT on multiple columns
You may want to use LEFT JOIN LATERAL ... ON u.val <> 0 to include names without valid values in the result (and shorten the syntax a bit).
What is the difference between LATERAL JOIN and a subquery in PostgreSQL?
If you have more than a few value columns (or varying lists of columns) you may want to use a function to build and execute the query automatically:
CREATE OR REPLACE FUNCTION f_unpivot_columns(VARIADIC _cols text[])
RETURNS TABLE(name text, t text, val int)
LANGUAGE plpgsql AS
$func$
BEGIN
RETURN QUERY EXECUTE (
SELECT
'SELECT t.name, u.t, u.val
FROM times t
LEFT JOIN LATERAL (VALUES '
|| string_agg(format('(%L, t.%I)', c, c), ', ')
|| ') u(t, val) ON (u.val <> 0)'
FROM unnest(_cols) c
);
END
$func$;
Call:
SELECT * FROM f_unpivot_times_columns(VARIADIC '{st, ot, dt}');
Or:
SELECT * FROM f_unpivot_columns('ot', 'dt');
Columns names are provided as string literals and must be in correct (case-sensitive!) spelling with no extra double-quotes. See:
Are PostgreSQL column names case-sensitive?
db<>fiddle here
Related with more examples and explanation:
How to unpivot a table in PostgreSQL
One way:
with times(name , st , ot , dt) as(
select 'Fred',8 , 2 , 3 union all
select 'Jane',8 , 1 , 0 union all
select 'Samm',8 , 0 , 6 union all
select 'Alex',8 , 0 , 0
)
select name, key as t, value::int from
(
select name, json_build_object('st' ,st , 'ot',ot, 'dt',dt) as j
from times
) t
join lateral json_each_text(j)
on true
where value <> '0'
-- order by name, case when key = 'st' then 0 when key = 'ot' then 1 when key = 'dt' then 2 end

Extract Distinct Special Characters in a Column using SQL Select

I'm having a Table Company. I need to know what are all the Special Characters are in the Company Names.
Consider the Sample Table
S.No. CompanyName
__________________________________
1. 24/7 Customers
2. Rose & Co.
3. Rose Inc. Corp.
4. Rose Pvt. Ltd.,
From the above table I need the Select only the Distinct Special Characters to know what are all the special characters are involved in the CompanyName
The Output of the above table should be /&.,
There is one place where you need to define your "regular" characters
select '%[^a-zA-Z0-9 ]%'
with prm (regular_char)
as
(
select '%[^a-zA-Z0-9 ]%'
)
,cte (special_char,string_suffix)
as
(
select '' as special_char
,CompanyName as string_suffix
from t
union all
select substring (string_suffix,special_char_ind,1) as special_char
,substring (string_suffix,special_char_ind+1,len(string_suffix)) as string_suffix
from (select string_suffix
,nullif(patindex(prm.regular_char,string_suffix),0) as special_char_ind
from cte,prm
where string_suffix <> ''
) t
where special_char_ind is not null
)
select special_char
,ascii(special_char) as ascii_special_char
,count(*) as cnt
from cte
where special_char <> ''
group by special_char
option (maxrecursion 0)
+--------------+--------------------+-----+
| special_char | ascii_special_char | cnt |
+--------------+--------------------+-----+
| | 9 | 1 |
+--------------+--------------------+-----+
| & | 38 | 1 |
+--------------+--------------------+-----+
| , | 44 | 1 |
+--------------+--------------------+-----+
| . | 46 | 5 |
+--------------+--------------------+-----+
| / | 47 | 1 |
+--------------+--------------------+-----+
Try this,
declare #t table(SNo int,CompanyName varchar(30))
insert into #t VALUES
(1,'24/7 Customers')
,(2,'Rose & Co.')
,(3,'Rose Inc. Corp.')
,(4,'Rose Pvt. Ltd.,')
;With CTE as
(
select sno
,stuff(CompanyName,PATINDEX('%[,-#\.&/]%',CompanyName),1,'') CompanyName
,SUBSTRING(CompanyName,PATINDEX('%[,-#\.&/]%',CompanyName),1) spcol from #t
union ALL
select sno,
replace(CompanyName,SUBSTRING(CompanyName,PATINDEX('%[,-#\.&/]%',CompanyName),1),'')
,SUBSTRING(CompanyName,PATINDEX('%[,-#\.&/]%',CompanyName),1)
from cte
where PATINDEX('%[,-#\.&/]%',CompanyName)>0
)
select distinct spcol
from cte

Can we use two pivot in a single query?

Declare #tbl table(SessionId varchar(max),ItemID_FK int,Roles varchar(max))
insert into #tbl
select distinct SessionID,ItemID_FK,Roles from tbl_Answers where ID_FK=#ID
SELECT ItemID_PK,ItemName,case when [Role1] IS NULL then 0 else [Role1] end as [Role1],
case when [Role2] IS NULL then 0 else [Role2] end as [Role2],
case when [Role3] IS NULL then 0 else [Role3] end as [Role3],
case when [Role4] IS NULL then 0 else [Role4] end as [Role4],
case when [Role5] IS NULL then 0 else [Role5] end as [Role5],
case when [Role6] IS NULL then 0 else [Role6] end as [Role6],
case when [Role7] IS NULL then 0 else [Role7] end as [Role7]
FROM
(
select items.ItemID_PK ,items.ItemName,count(ans.Roles) as cntRoles,ans.Roles from tbl_Items items Full join #tbl ans
on items.ItemID_PK=ans.ItemID_FK where items.ID_FK= #ID group by Roles,ItemName , items.ItemID_PK
) d PIVOT
(
max(cntRoles)
FOR Roles IN ([Role1],[Role2],[Role3],[Role4],[Role5],[Role6],[Role7])
) AS pvt order by ItemID_PK
I used the above stored procedure and got the output as
+----------+----------+-----+-----+-----+-----+-----+-----+-----+
|ItemID_PK |ItemName |Role1|Role2|Role3|Role4|Role5|Role6|Role7|
+----------+----------+-----+-----+-----+-----+-----+-----+-----+
| 111 | aaaaa | 6 | 5 | 0 | 5 | 1 | 4 | 2 |
| 222 | bbbbb | 1 | 1 | 7 | 2 | 0 | 3 | 1 |
+----------+----------+-----+-----+-----+-----+-----+-----+-----+
I have another query and got the following output.
Select Category,Answer,Roles
from tbl_Answers where ID_FK=1 and Category='OtherText'
+---------+--------+-----+
|Category |Answer |Roles|
+---------+--------+-----+
|OtherText| xxx |Role1|
|OtherText| yyy |Role1|
|OtherText| zzz |Role2|
|OtherText| xzx |Role3|
+---------+--------+-----+
I need to merge the above two outputs to generate the result as
+----------+----------+-----+-----+-----+-----+-----+-----+-----+
|ItemID_PK |ItemName |Role1|Role2|Role3|Role4|Role5|Role6|Role7|
+----------+----------+-----+-----+-----+-----+-----+-----+-----+
| 111 | aaaaa | 6 | 5 | 0 | 5 | 1 | 4 | 2 |
| 222 | bbbbb | 1 | 1 | 7 | 2 | 0 | 3 | 1 |
| Null | Othertext| xxx | zzz | xzx | saa | | xxx | |
| Null | Othertext| yyy | | | zxz | | | |
+----------+----------+-----+-----+-----+-----+-----+-----+-----+
How to combine the second query to the first pivot query to get the result mentioned above?
Thanks in advance.
You could just use UNION ALL to combine the two results, you would need to convert the roles from the top query from int to VARCHAR though:
DECLARE #ID INT = 1;
WITH Ans AS
( SELECT DISTINCT SessionID,ItemID_FK,Roles
FROM tbl_Answers
WHERE ID_FK = #ID
), PivotData AS
( SELECT items.ItemID_PK,
items.ItemName,
cntRoles = COUNT(ans.Roles),
ans.Roles
FROM tbl_Items items
FULL JOIN Ans
ON items.ItemID_PK = ans.ItemID_FK
WHERE items.ID_FK = #ID
GROUP BY Roles,ItemName, items.ItemID_PK
)
SELECT ItemID_PK,
ItemName,
[Role1] = CAST(ISNULL([Role1], 0) AS VARCHAR(255)),
[Role2] = CAST(ISNULL([Role2], 0) AS VARCHAR(255)),
[Role3] = CAST(ISNULL([Role3], 0) AS VARCHAR(255)),
[Role4] = CAST(ISNULL([Role4], 0) AS VARCHAR(255)),
[Role5] = CAST(ISNULL([Role5], 0) AS VARCHAR(255)),
[Role6] = CAST(ISNULL([Role6], 0) AS VARCHAR(255)),
[Role7] = CAST(ISNULL([Role7], 0) AS VARCHAR(255))
FROM PivotData
PIVOT
( MAX(cntRoles)
FOR Roles IN ([Role1],[Role2],[Role3],[Role4],[Role5],[Role6],[Role7])
) AS pvt
UNION ALL
SELECT ItemID_PK = NULL,
ItemName = Category,
[Role1] = ISNULL([Role1], ''),
[Role2] = ISNULL([Role2], ''),
[Role3] = ISNULL([Role3], ''),
[Role4] = ISNULL([Role4], ''),
[Role5] = ISNULL([Role5], ''),
[Role6] = ISNULL([Role6], ''),
[Role7] = ISNULL([Role7], '')
FROM ( SELECT Category,Answer,Roles
FROM tbl_Answers
WHERE ID_FK = 1
AND Category = 'OtherText'
) pd
PIVOT
( MAX(Answer)
FOR Roles IN ([Role1],[Role2],[Role3],[Role4],[Role5],[Role6],[Role7])
) AS pvt
ORDER BY ItemID_PK;
Note, I have changed this expression:
case when [Role2] IS NULL then 0 else [Role2] end
to
ISNULL([Role2], 0)
as the effect is the same, but it is much shorter. I have also removed the table variable, and just placed the same query within a Common Table Expression, as it seems redundant to fill a table variable then only refer to it once. You are removing the use of indexes and statistics on the actual table and gaining no benefit for it.
Use a UNION to combine the two pivots.