How to pivot without using PIVOT functions in SQL? - sql

I'd like to pivot a table without using any PIVOT function but using pure SQL only (let's say PostgreSQL syntax for example).
So having this data-set as an example:
Order | Element | Value |
---------------------------
1 | Item | Bread |
1 | Quantity | 3 |
1 | TotalCost | 3,30 |
2 | Item | Pizza |
2 | Quantity | 2 |
2 | TotalCost | 10 |
3 | Item | Pasta |
3 | Quantity | 5 |
3 | TotalCost | 2,50 |
I'd expect to pivot on the Order column and having somenthing like:
Order | Item | Quantity | TotalCost |
-------------------------------------
1 | Bread | 3 | 3,30 |
2 | Pizza | 2 | 10 |
3 | Pasta | 5 | 2,50 |
How would I achieve that, once again, without using any pivot function?

Solution A
using CASE WHEN statements:
SELECT Order,
MAX(CASE WHEN Element = 'Item' THEN Value END) AS Item,
MAX(CASE WHEN Element = 'Quantity' THEN Value END) AS Quantity,
MAX(CASE WHEN Element = 'TotalCost' THEN Value END) AS TotalCost
FROM MyTable
GROUP BY 1
Solution B
Using self left joins:
SELECT A.Order, A.Item, B.Quantity, C.TotalCost
FROM
(SELECT Order, Value as Item
FROM MyTable
WHERE Element = 'Item') as A
LEFT JOIN
(SELECT Order, Value as Quantity
FROM MyTable
WHERE Element = 'Quantity') as B ON A.Order = B.Order
LEFT JOIN
(SELECT Order, Value as TotalCost
FROM MyTable
WHERE Element = 'TotalCost') as C ON B.Order = C.Order
The latter might not be as much as efficient but there are use case in which might come useful.

In Postgres, you would use FILTER:
SELECT Order,
MAX(Value) FILTER (WHERE Element = 'Item') AS Item,
MAX(Value) FILTER (WHERE Element = 'Quantity') AS Quantity,
MAX(Value) FILTER (WHERE Element = 'TotalCost') AS TotalCost
FROM MyTable
GROUP BY 1;
Note that ORDER is a SQL Keyword so it is a bad choice for a column name.

Related

How to pivot a table of strings in sql? [duplicate]

I'd like to pivot a table without using any PIVOT function but using pure SQL only (let's say PostgreSQL syntax for example).
So having this data-set as an example:
Order | Element | Value |
---------------------------
1 | Item | Bread |
1 | Quantity | 3 |
1 | TotalCost | 3,30 |
2 | Item | Pizza |
2 | Quantity | 2 |
2 | TotalCost | 10 |
3 | Item | Pasta |
3 | Quantity | 5 |
3 | TotalCost | 2,50 |
I'd expect to pivot on the Order column and having somenthing like:
Order | Item | Quantity | TotalCost |
-------------------------------------
1 | Bread | 3 | 3,30 |
2 | Pizza | 2 | 10 |
3 | Pasta | 5 | 2,50 |
How would I achieve that, once again, without using any pivot function?
Solution A
using CASE WHEN statements:
SELECT Order,
MAX(CASE WHEN Element = 'Item' THEN Value END) AS Item,
MAX(CASE WHEN Element = 'Quantity' THEN Value END) AS Quantity,
MAX(CASE WHEN Element = 'TotalCost' THEN Value END) AS TotalCost
FROM MyTable
GROUP BY 1
Solution B
Using self left joins:
SELECT A.Order, A.Item, B.Quantity, C.TotalCost
FROM
(SELECT Order, Value as Item
FROM MyTable
WHERE Element = 'Item') as A
LEFT JOIN
(SELECT Order, Value as Quantity
FROM MyTable
WHERE Element = 'Quantity') as B ON A.Order = B.Order
LEFT JOIN
(SELECT Order, Value as TotalCost
FROM MyTable
WHERE Element = 'TotalCost') as C ON B.Order = C.Order
The latter might not be as much as efficient but there are use case in which might come useful.
In Postgres, you would use FILTER:
SELECT Order,
MAX(Value) FILTER (WHERE Element = 'Item') AS Item,
MAX(Value) FILTER (WHERE Element = 'Quantity') AS Quantity,
MAX(Value) FILTER (WHERE Element = 'TotalCost') AS TotalCost
FROM MyTable
GROUP BY 1;
Note that ORDER is a SQL Keyword so it is a bad choice for a column name.

Check if Item has Data

I have a table that contains a whole lot of fields.
What I am trying to do is see if any items are a missing certain fields.
Example of data:
+--------+----------+-------+
| ITEMNO | OPTFIELD | VALUE |
+--------+----------+-------+
| 0 | x | 1 |
+--------+----------+-------+
| 0 | x | 1 |
+--------+----------+-------+
| 0 | x | 1 |
+--------+----------+-------+
| 0 | x | 1 |
+--------+----------+-------+
| 0 | x | 1 |
+--------+----------+-------+
There are 4 "OPTFIELD" which I want to see if all "ITEMNO" have.
So the logic I want to apply is something along the lines of:
Show all items that do not have the "OPTFIELD" - "LABEL","PG4","PLINE","BRAND"
Is this even possible?
Your data makes no sense. From the description of your question, it looks like you want itemno that do not have all 4 optfields. For this, one method uses aggregation:
select itemno
from mytable
where optfield in ('LABEL', 'PG4', 'PLINE', 'BRAND')
group by itemno
having count(*) < 4
On the other hand, if you want to exhibit all missing (itemno, optfield) tuples, then you can cross join the list of itemnos with a a derived table with of optfields, then use not exists:
select i.itemno, o.optfield
from (select distinct itemno from mytable) i
cross join (values ('LABEL'), ('PG4'), ('PLINE'), ('BRAND')) o(optfield)
where not exists (
select 1
from mytable t
where t.itemno = i.itemno and t.optfield = o.optfield
)

Get rows with multiple conditions

Below is my Postgres table:
Table:
+------+-----------------+---------+
| sku | properties | value |
|------+-----------------+---------|
| 1 | Family_ID | 21 |
| 1 | Class_ID | 21 |
| 2 | Family_ID | 20 |
| 2 | Class_ID | 21 |
| 3 | Family_ID | 21 |
| 3 | Class_ID | 21 |
+------+-----------------+---------+
How to query if I want to fetch data where the Family_ID and Class_ID is 21.
The expected return value:
+------+-----------------+---------+
| sku | properties | value |
|------+-----------------+---------|
| 1 | Family_ID | 21 |
| 1 | Class_ID | 21 |
| 3 | Family_ID | 21 |
| 3 | Class_ID | 21 |
+------+-----------------+---------+
How to query if I want to fetch data where the Family_ID is 20 and Class_ID is 21.
The expected return value:
+------+-----------------+---------+
| sku | properties | value |
|------+-----------------+---------|
| 2 | Family_ID | 20 |
| 2 | Class_ID | 21 |
+------+-----------------+---------+
This query:
select sku
from tablename
group by sku
having
max(case when properties = 'Family_ID' then value end) = 21
and
max(case when properties = 'Class_ID' then value end) = 21
returns all the skus that satisfy your conditions and you can use it with the operator IN like this:
select * from tablename
where sku in (
select sku
from tablename
group by sku
having
max(case when properties = 'Family_ID' then value end) = 21
and
max(case when properties = 'Class_ID' then value end) = 21
)
You could also use MAX() window function:
select t.sku, t.properties, t.value
from (
select *,
max(case when properties = 'Family_ID' then value end) over (partition by sku) family_id,
max(case when properties = 'Class_ID' then value end) over (partition by sku) class_id
from tablename
) t
where t.family_id = 21 and t.class_id = 21
See the demo.
Results:
> sku | properties | value
> --: | :--------- | ----:
> 1 | Family_ID | 21
> 1 | Class_ID | 21
> 3 | Family_ID | 21
> 3 | Class_ID | 21
To operate across rows you need to group, but the easiest thing here (given that you seem like you will want ever more variations of this theme) is probably to pivot your data:
WITH x as(
SELECT f.sku, c.value as class_value, f.value as family_value
FROM
(select sku, value FROM table WHERE properties = 'family_id') f
INNER JOIN
(select sku, value FROM table WHERE properties = 'class_id') c
ON f.sku = c.sku
)
You can now use a WHERE clause like normal:
SELECT * FROM x WHERE family_value = 20 and class_value = 21
If you need the data back in a column format, you can unpivot it again:
SELECT
sku,
'family_id' as properties,
family_value as value
FROM
x
UNION ALL
SELECT
sku,
'class_id' as properties,
class_value as value
FROM
x
But it might be easier to just work with the data in its pivoted form.
I wouldn't necessarily do this myself, but its a lot easier to understand this form of pivoting if you're used to regular joining, union and other similar "typical" database operations, so I'm recommending this as you may well find it easier to maintain and extend. Conditional aggregation to do a pivot is potentially more efficient but more complex to maintain:
WITH X as (
SELECT
sku,
MAX(CASE WHEN properties = 'Family_ID' THEN value END) as family_value,
MAX(CASE WHEN properties = 'Class_ID' THEN value END) as class_value
FROM
table
GROUP BY sku
)
The CASE WHENs spread the values across columns according to the properties value. The group by/max then collapse the rows removing the nulls leaving you with a unique sku and the values in named columns according to what kind of value they are
--case 1
select *
from tbl t
where exists (select *
from tbl t1
where t1.sku=t.sku
and t1.properties='Family_ID'
and t1.value=21)
and exists (select *
from tbl t1
where t1.sku=t.sku
and t1.properties='Class_ID'
and t1.value=21)
--case 2
select *
from tbl t
where exists (select *
from tbl t1
where t1.sku=t.sku
and t1.properties='Family_ID'
and t1.value=20)
and exists (select *
from tbl t1
where t1.sku=t.sku
and t1.properties='Class_ID'
and t1.value=21)

SQL select distinct when one column in and another column greater than

Consider the following dataset:
+---------------------+
| ID | NAME | VALUE |
+---------------------+
| 1 | a | 0.2 |
| 1 | b | 8 |
| 1 | c | 3.5 |
| 1 | d | 2.2 |
| 2 | b | 4 |
| 2 | c | 0.5 |
| 2 | d | 6 |
| 3 | a | 2 |
| 3 | b | 4 |
| 3 | c | 3.6 |
| 3 | d | 0.2 |
+---------------------+
I'm tying to develop a sql select statement that returns the top or distinct ID where NAME 'a' and 'b' both exist and both of the corresponding VALUE's are >= '1'. Thus, the desired output would be:
+---------------------+
| ID | NAME | VALUE |
+---------------------+
| 3 | a | 2 |
+----+-------+--------+
Appreciate any assistance anyone can provide.
You can try to use MIN window function and some condition to make it.
SELECT * FROM (
SELECT *,
MIN(CASE WHEN NAME = 'a' THEN [value] end) OVER(PARTITION BY ID) aVal,
MIN(CASE WHEN NAME = 'b' THEN [value] end) OVER(PARTITION BY ID) bVal
FROM T
) t1
WHERE aVal >1 and bVal >1 and aVal = [Value]
sqlfiddle
This seems like a group by and having query:
select id
from t
where name in ('a', 'b')
having count(*) = 2 and
min(value) >= 1;
No subqueries or joins are necessary.
The where clause filters the data to only look at the "a" and "b" records. The count(*) = 2 checks that both exist. If you can have duplicates, then use count(distinct name) = 2.
Then, you want the minimum value to be 1, so that is the final condition.
I am not sure why your desired results have the "a" row, but if you really want it, you can change the select to:
select id, 'a' as name,
max(case when name = 'a' then value end) as value
you can use in and sub-query
select top 1 * from t
where t.id in
(
select id from t
where name in ('a','b')
group by id
having sum(case when value>1 then 1 else 0)>=2
)
order by id

An SQL query that combines aggregate and non-aggregate values in one row

The following query gives me the information that I need but I want it to take it just a step further. In the table at the bottom (only showing a subset of the fields), I want to group by cust_line in an unusual way (at least to me it's unusual).
Let's look at the items with a cust_line of 2 as an example. I would like these to be represented by one line not 5. For this line, I would like to select all the fields except for the price field where the cust_part = "GROUPINVC". For the total field I would like it to be 'sum(total) as new_total' and for the price, I would like it to be new_total / qty_invoiced, where qty_invoiced is the value on the line where cust_part = "GROUPINV".
Is what I am asking for completely ridiculous? Is it even possible? I'm not advanced at SQL so it may also be easy and I just don't know how to approach it. I thought of using 'partition by' but I couldn't imagine how I would get it to work as I figured it would still return 5 rows where I only want 1.
I've also looked at these questions with similar titles but not really what I am looking for:
SQL query that returns aggregate AND non aggregate results
Combined aggregated and non-aggregate query in SQL
SELECT L.CUST_LINE, I.LINE_NO, I.ORDER_NO, I.STAGE, I.ORDER_LINE_POS, I.CUST_PART,
I.LINE_ITEM_NO, I.QTY_INVOICED, I.CUST_DESC, I.DESCRIPTION, I.SALE_UNIT_PRICE, I.PRICE_TOTAL,
I.INVOICE_NO, I.CUSTOMER_PO_NO, I.ORDER_NO, I.CUSTOMER_NO, I.CATALOG_DESC, I.ORDER_LINE_NOTES
FROM
(SELECT CUST_LINE, ORDER_NO, LINE_NO
FROM CUSTOMER_ORDER_LINE
GROUP BY CUST_LINE, ORDER_NO, LINE_NO
) L
INNER JOIN CUSTOMER_ORDER_IVC_REP I
ON I.ORDER_NO = L.ORDER_NO
WHERE RESULT_KEY = 999999
AND I.LINE_NO = L.LINE_NO
ORDER BY L.CUST_LINE;
| cust_line | line_no | cust_part | qty_invoiced | cust_desc | price | total |
| 1 | 4 | ... | 1 | ... | 55 | 55 |
| 2 | 1 | GROUPINV | 1 | some part | 0 | 0 |
| 2 | 6 | ... | 3 | ... | 0 | 0 |
| 2 | 2 | ... | 1 | ... | 0 | 0 |
| 2 | 3 | ... | 1 | ... | 0 | 0 |
| 2 | 7 | ... | 2 | ... | 10 | 20 |
| 3 | 7 | ... | 1 | ... | 67 | 67 |
You can use an analytic function to calculate a total over multiple rows of a result set, then filter out the rows you don't want.
Leaving out all the extra columns for sake of brevity:
SELECT cust_line, qty_invoiced, order_total/qty_invoiced AS price
FROM (
SELECT l.cust_line, qty_invoiced,
SUM(total) OVER (PARTITION BY l.cust_line) AS order_total,
COUNT(cust_line) OVER (PARTITION BY l.cust_line) AS group_count
FROM
(SELECT CUST_LINE, ORDER_NO, LINE_NO
FROM CUSTOMER_ORDER_LINE
GROUP BY CUST_LINE, ORDER_NO, LINE_NO
) L
INNER JOIN CUSTOMER_ORDER_IVC_REP I
ON I.ORDER_NO = L.ORDER_NO
WHERE RESULT_KEY = 999999
AND I.LINE_NO = L.LINE_NO
)
WHERE ( cust_part = 'GROUPINV' OR group_count = 1 )
ORDER BY cust_line
I am guessing on what you want in the PARTITION BY clause; this is essentially a GROUP BY that applies only to the SUM function. Not sure if you might also want order_no in the partition.
The trick is to select all the rows in the inner query, applying SUM across them all; then filter out the rows you are not interested in in the outermost query.