Pivot/Transpose telephone numbers stored in rows - sql

I have seen a lot of posts and topics regarding pivots and transposing data, I'm either not understanding them (likely) or I'm trying to over complicate what I need to do.
I'm running a simple select statement like so:
SELECT Customer, Telephone
FROM DetailsTable
WHERE Customer = 74270571
GROUP BY Customer, Telephone
This returns:
Customer | Telephone
74270571 | 01556589962
74270571 | 07756563729
And what I am trying to get is
Customer | Tel1 | Tel2
74270571 | 01556589962 | 07756563729
There can be a max of 5 tel numbers.

Since you have multiple Telephone values for each Customer, the easiest way to get the result would be to use a windowing function like row_number to create a unique value for each Telephone/Customer combination. Once you have this value, then you can PIVOT the result using an aggregate function and CASE expression or the PIVOT function.
Having up to five telephone numbers, makes this easy to write the query as a hard-coded or static version. Using an aggregate function with a CASE expression the code would be:
select
customer,
Tel1 = max(case when rn = 1 then telephone else null end),
Tel2 = max(case when rn = 2 then telephone else null end),
Tel3 = max(case when rn = 3 then telephone else null end),
Tel4 = max(case when rn = 4 then telephone else null end),
Tel5 = max(case when rn = 5 then telephone else null end)
from
(
select customer, telephone,
rn = row_number() over(partition by customer order by telephone)
from DetailsTable
) x
group by customer;
See SQL Fiddle with Demo
If you want to use the PIVOT function, then the code would be:
select customer,
Tel1,
Tel2,
Tel3,
Tel4,
Tel4
from
(
select customer, telephone,
col = 'Tel'+cast(row_number() over(partition by customer order by telephone)
as varchar(1))
from DetailsTable
) x
pivot
(
max(telephone)
for col in (Tel1, Tel2, Tel3, Tel4, Tel5)
) p
See SQL Fiddle with Demo. Both give a result:
| CUSTOMER | TEL1 | TEL2 | TEL3 | TEL4 |
|----------|------------|------------|--------|--------|
| 74270571 | 1556589962 | 7756563729 | (null) | (null) |

Related

Conditional grouping of records

I have a problem with grouping records in PostgreSQL. I have a structure containing 3 columns, non unique id, name, group (it's old system and I can't change this structure).
Sample records:
id | name | group
-----+----------+------
1 | product1 | 0
1 | product1 | test
2 | product2 | test
3 | product3 | test123
I want the groups unequal 0 to be concatenated (get the id, name of the first record from the group).
The expected result:
id | name | group
-----+----------+------
1 | product1 | 0
1 | product1 | test
3 | product3 | test123
Currently count records in the following way:
SELECT
COUNT(CASE WHEN group = '0' THEN group END) +
COUNT(DISTINCT CASE WHEN group <> '0' THEN group END) AS count
FROM
table
Is it correct way? How can I convert it to retrieve records?
You can use row_number():
select id, name, group
from (select t.*, row_number() over (partition by group order by id) as seqnum
from t
) t
where seqnum = 1 or group = '0';
Note: group is a really bad name for a column. It is a SQL keyword, so you should escape the name. I am leaving it as is because your query uses it.

Select N rows in aggregate functions SQL Server

I have a table that looks like this:
+--------+----------+--------+------------+-------+
| ID | CHANNEL | VENDOR | num_PERIOD | SALES |
+--------+----------+--------+------------+-------+
| 000001 | Business | Shop | 1 | 40 |
| 000001 | Business | Shop | 2 | 60 |
| 000001 | Business | Shop | 3 | NULL |
+--------+----------+--------+------------+-------+
With many combinations of ID, CHANNEL and VENDOR, and sales records for each of them over time (num_PERIOD).
The idea is to obtain a new column which returns the number of NULLS in SALES column, but in the first 111 registers according to num_PERIOD column.
I have been trying something like this:
SELECT ID,
CHANNEL,
VENDOR,
sum(CASE
WHEN SALES IS NULL THEN 1
ELSE 0
END) OVER (PARTITION BY ID,
CHANNEL,
VENDOR
ORDER BY num_PERIOD ROWS BETWEEN UNBOUNDED PRECEDING AND 111 FOLLOWING) AS NULL_SALES_SET
FROM TABLE
GROUP BY ID,
CHANNEL,
VENDOR
But I'm not obtaining what I'm looking for.
So to obtain a table simillar to:
+--------+--------------+--------+----------------+
| ID | CHANNEL | VENDOR | NULL_SALES_SET |
+--------+--------------+--------+----------------+
| 000001 | Business | Shop | 1 |
| 000002 | Business | Market | 0 |
| 000002 | Non Business | Shop | 3 |
+--------+--------------+--------+----------------+
The difficulty comes when selecting these first 111 rows per ID, CHANNEL AND VENDOR ordered by num_PERIOD.
Use a CTE (Common Table Expression) with the ROW_NUMBER windowed function and you should be set:
;WITH MyCTE AS
(
SELECT
id,
channel,
vendor,
sales,
ROW_NUMBER() OVER (PARTITION BY id, channel, vendor ORDER BY num_period) AS row_num
FROM
MyTable
)
SELECT
id,
channel,
vendor,
SUM(CASE WHEN sales IS NULL THEN 1 ELSE 0 END) AS null_sales_set
FROM
MyCTE
WHERE
row_num <= 111
GROUP BY
id, channel, vendor
Do you have to use the windowing function?
SELECT ID
, CHANNEL
, VENDOR
, NULL_SALES_SET = SUM(CASE WHEN SALES IS NULL THEN 1 ELSE 0 END)
FROM Table
WHERE num_PERIOD <= 111
GROUP BY ID, CHANNEL, VENDOR
Or are you looking for the first 111 num_PERIOD values allowing for gaps in the num_PERIOD column?
SELECT t.ID
, t.CHANNEL
, t.VENDOR
, NULL_SALES_SET = SUM(CASE WHEN t.SALES IS NULL THEN 1 ELSE 0 END)
FROM Table t
INNER JOIN ( SELECT i.ID
, i.CHANNEL
, i.VENDOR
, i.num_PERIOD
, rowNum = ROW_NUMBER(PARTITION BY i.ID, i.CHANNEL, i.VENDOR ORDER BY i.num_PERIOD)
FROM Table i ) l
ON t.ID = l.ID
AND t.CHANNEL = l.CHANNEL
AND t.VENDOR = l.VENDOR
AND t.num_PERIOD = l.num_PERIOD
WHERE l.rowNum <= 111
GROUP BY ID, CHANNEL, VENDOR
Edit: Not sure how I overlooked it, but it is necessary to JOIN on the num_PERIOD column.
Edit: Add the number of distinct num_PERIOD per ID, Channel, Vendor without affecting the NULL_SALES_SET
SELECT t.ID
, t.CHANNEL
, t.VENDOR
-- Counts the NULL Sales when the num_PERIOD is in the
-- first 111 num_PERIODs
, NULL_SALES_SET = SUM(CASE WHEN l.rowNum IS NOT NULL AND t.SALES IS NULL
THEN 1
ELSE 0 END)
-- Counts the distinct num_PERIOD values
, PERIOD_COUNT = COUNT(DISTINCT t.num_PERIOD)
FROM Table t
LEFT OUTER JOIN ( SELECT i.ID
, i.CHANNEL
, i.VENDOR
, i.num_PERIOD
, rowNum = ROW_NUMBER(PARTITION BY i.ID,
i.CHANNEL,
i.VENDOR
ORDER BY i.num_PERIOD)
FROM Table i ) l
ON t.ID = l.ID
AND t.CHANNEL = l.CHANNEL
AND t.VENDOR = l.VENDOR
AND t.num_PERIOD = l.num_PERIOD
AND l.rowNum <= 111
GROUP BY ID, CHANNEL, VENDOR

Query to get multiple row values into multiple columns

I have a CSV file with 2 columns .
Empid | SID
:-----|-----:
12312 | S-1-5-21-3751615294
12312 | S-1-5-21-3751615298
12312 | S-1-5-21-3751615292
12313 | S-1-5-21-3751615294-5078
13546 | S-1-5-21-3751615294-50725
12312 | S-1-5-21-3751615291
14151 | S-1-5-21-3751615294-50722
For an Empid there are multiple SIDs available .I need help writing a sql SELECT query that can map(and store) these SIDs(sorted) into multiple columns.
Desired SQL Select output is below :-
+--------+---------------------------+---------------------+--------------------+--------------------+
| Empid | SID1 | SID2 | SID3 | SID4 |
+--------+---------------------------+---------------------+--------------------+--------------------+
| 12312 | S-1-5-21-3751-65291 | S-1-5-21-375165292 | S-1-5-21-375165294 | S-1-5-21-375165298 |
| 12313 | S-1-5-21-3751615294-5078 | NULL | NULL | NULL |
| 13546 | S-1-5-21-3751615294-50725 | NULL | NULL | NULL |
+--------+---------------------------+---------------------+--------------------+--------------------+
I am collecting an employee record in my application collector(using sql select queries) from a CSV file and need to collect these SIDs in his record .Maximum 4 SIDs can be possible so I created 4 attributes for SIDs .
Thanks in advance .
Presumably, you know the number of columns. If so, you can do it using conditional aggregation and row_number():
select empid,
max(case when seqnum = 1 then sid end) as sid_1,
max(case when seqnum = 2 then sid end) as sid_2,
max(case when seqnum = 3 then sid end) as sid_3,
max(case when seqnum = 4 then sid end) as sid_4
from (select t.*, row_number() over (partition by empid order by empid) as seqnum
from t
) t
group by empid;
If you don't know the number, then perhaps a comma-delimited list will do:
select empid, listagg(sid, ',') within group (order by sid) as sids
from t
group by empid;
A SQL query has a fixed number of columns, so a result set that has a flexible number of columns would require dynamic SQL.
You can use window function row_number() to assign row number to sid within each empid and then use conditional aggregation to get the final results.
select
empid,
min(case when rn = 1 then sid end) sid1,
min(case when rn = 2 then sid end) sid2,
min(case when rn = 3 then sid end) sid3,
min(case when rn = 4 then sid end) sid4
from (select
t.*,
row_number() over (partition by empid order by sid) rn
from table t) group by empid;

Return rows if data exists based on a given priority in SQL

I am trying to form a PostgreSQL statement that returns a customer email based on the email type with a given priority. Below I have a table with customers 1 and two. Customer 1 has both personal and company emails whereas customer 2 has on the company.
The problem I am trying to solve is returned the customers personal email if it exists first and if not return the company. So, the personal email is given priority over the company. Is this even possible in PostgreSQL?
customers
+------------+
| cusomterID |
+------------+
| 1 |
| 2 |
+------------+
customer_email
+------------+-------------+
| cusomterID | email_type |
+------------+-------------+
| 1 | personal | -- 0
| 2 | company | -- 1
| 1 | company | -- 1
+------------+-------------+
What I am trying now is not really working. It returns all of the rows and does not filter
SELECT *
FROM customers cs
JOIN cumstomer_email cm ON cm.customerId = cs.customreId
WHERE COALESCE(cm.email_type,0) IN (0,1)
One option would be to use conditional aggregation:
select customerId, max(case when email_type = 'personal' then email_type
else email_type
end) email_type
from customer_email
group by customerId
SQL Fiddle Demo
And here's another option using row_number():
select customerId, email_type
from (select *,
row_number() over (partition by customerId
order by email_type = 'personal' desc) rn
from customer_email) t
where rn = 1
More Fiddle
You could do this with a common table expression (CTE):
with emailPriority as (
select customerId,
max(email_type) emailType
from customer_email
group by customer_id)
select cs.*, cm.email_address
from customers cs join emailPriority ep on cs.customerId = ep.customerId
join customer_email cm on cm.email_type = ep.email_type

Using the same table twice in a FROM clause

I have a table that looks like this:
1 | 'Frank' | 'A'
2 | 'Frank' | 'B'
3 | 'Tom' | 'A'
4 | 'Tom' | 'B'
And I want an output that looks like this:
Frank | A | B
Tom | A | B
I've come up with this:
SELECT N.Name, N.Surname, M.Surname
FROM NAMES N, NAMES M
WHERE N.Surname = 'B' AND M.Surname = 'A'
GROUP BY N.Name;
This seems to work, but I don't know if it is a good practise to use the same table twice in the FROM clause, or if the performance will be affected by this on large tables.
Is there a simpler solution?
Having more than one table behind from is old-fashioned: you can make your join more readable with the standard on syntax:
from Names n
join Names m
on n.name = m.name
A self-join with a restrictive condition isn't too expensive. And Name is a pretty restrictive condition: only 2 records will share the name.
You could write it a little bit more efficient in a database that supports row_number(). That allows you to run the query without a join:
select Name
, max(case when rn = 1 then Surname end) as Surname1
, max(case when rn = 2 then Surname end) as Surname2
, max(case when rn = 3 then Surname end) as Surname3
from (
select row_number() over(
partition by Name
order by Surname) as rn
, Name
, Surname
from YourTable
) SubQueryAlias
group by
Name