Split Query Result into Virtual Columns based on Value - sql

I have an existing database that has column values abstracted out to a separate 'values' table (for localization reasons, but not necessarily important in the context of this question). In my case, say I have two tables:
***ITEM Table***
id cat
----|-----|
1 | A |
2 | A |
3 | B |
***VALUES Table***
seq id code value type
----|----|-------|-------|-------|
10 | 1 | NAME | Name1 | Type1 |
11 | 1 | DESC | Desc1 | Type1 |
12 | 1 | NAME | Name1 | Type2 |
13 | 1 | DESC | Desc1 | Type2 |
14 | 2 | NAME | Name2 | Type1 |
15 | 2 | DESC | Desc2 | Type1 |
Currently, I can retrieve names and descriptions like this:
***Current Result Set***
id code value
----|-------|-------|
1 | NAME | Name1 |
1 | DESC | Desc1 |
2 | NAME | Name2 |
2 | DESC | Desc2 |
However, I would like to retrieve names/descriptions like this:
***Target Result Set***
id NAME DESC
----|-------|-------|
1 | Name1 | Desc1 |
2 | Name2 | Desc2 |
I thought a CTE/Window function may be appropriate in this case, but I'm not sure how to tackle this.
In essence, how can I create column aliases and associated values, based on a value in a column (in this case, if the 'code' column contains 'NAME', a virtual 'NAME' column would be created, with the value from the associated 'value' column)?
I considered a CASE statement too, but couldn't use it to create a dynamic alias like this.
If this is impossible, as in (Dynamic column alias based on column value), would it be possible to do this if I knew of the "CODE" values ahead of time (i.e. I know that only "NAME" and "DESC" are valid codes.

Actually you need the data to be pivoted , you can use CASE for this with MAX aggregate function.
if you know the values in advance, it can be coded like this
if the values are dynamic then dynamic sql is preferred.
SELECT I.ID,
MAX ( case when code = 'NAME' THEN V.value end ) as 'NAME',
MAX ( case when code = 'DESC' THEN V.value end ) as 'DESC'
FROM ITEM I
INNER JOIN VALUE V
ON I.id = V.id
GROUP BY I.id

Related

Binding together tables using GROUP BY with columns about category

I have a problem with handle aggregate columns in SQL Server 2014 version which does not support GROUP_CONCAT function. My task is to create query which bind together a few tables by its common columns, so suppose there will be two example tables.
Table A (Category 1)
| name | size |
+------+------+
| aaa | 2 |
| bbb | 3 |
Table B (Category 2)
| name | size |
+------+------+
| aaa | 2 |
| ccc | 7 |
Please notice that first records on both tables are the same.
I want to get following results:
| name | size | category_id | secondary_category_id |
+------+------+-------------+-----------------------+
| aaa | 2 | 1 | 2 |
| bbb | 3 | 1 | NULL |
| ccc | 7 | 2 | NULL |
The category_id column is always filled by ID which is hardcoded for each table, for example:
SELECT name, size, '1' AS category_id
FROM Table_A
GROUP BY name, size
UNION ALL
SELECT name, size, '2' AS category_id
FROM Table_B
GROUP BY name, size
But some entries in tables may be duplicated and for those row I want fill secondary_column_id with value identifying table (in this case 2)
This looks like a full join:
select coalesce(c1.name, c2.name) as name,
coalesce(c1.size, c2.size) as size,
(case when c1.name is not null then 1 else 2 end) as category_id,
(case when c1.name is not null and c2.name is not null then 2 end) as secondary_category_id
from category1 c1 full join
category2 c2
on c1.name = c2.name and c1.size = c2.size

getting concatenated desc from a table of 3 different columns of another table

I am trying to get a single description column from a reference table in PostgreSQL using 3 id columns as a concatenated value.
I have a id Table as below:
+-----+-----+-----+
| id1 | id2 | id3 |
+-----+-----+-----+
| 1 | 2 | 3 |
| 4 | 6 | 5 |
+-----+-----+-----+
and Reference Table;
+----+----------+
| id | desc |
+----+----------+
| 1 | apple |
| 2 | boy |
| 3 | cat |
| 4 | dog |
| 5 | elephant |
| 6 | Flight |
+----+----------+
The Desired expected output is as below
I just have to concat a "/M" in the end additionally.
I don't have to add /M if id2 and id3 both are null
+-----------------------+
| desc |
+-----------------------+
| apple+boy+cat/M |
| dog+Flight+Elephant/M |
+-----------------------+
You can use string_agg() to concatenate all rows with a single expression. Something like:
select (select string_agg(r.descr, '+' order by r.id)||
case when count(r.descr) > 1 then '/M' else '' end
from ref r
where id in (i.id1, i.id2, id3)) as descr
from id_table i;
Online example: https://rextester.com/KVCGLD44632
The above sorts the descriptions by the ID value. If you need to preserve the order of the columns in the "id table", you could use something like this:
select (select string_agg(r.descr, '+' order by t.idx)||
case when count(r.descr) > 1 then '/M' else '' end
from ref r
join (values (i.id1, 1), (i.id2, 2), (i.id3, 3)) as t(id, idx)
on t.id = r.id
) as descr
from id_table i;
Note that desc is a reserved keyword, you should not use it as a column name. That's why I used descr in my example.

Loop through table, use conditionals, then store value in another table

The procedure is to fill the "City" column in Table B based on the "Letter" column from Table A.
TABLE A
+----------+-------+
| Number | Letter|
+----------+-------+
| 1 | A |
| 1 | |
| 1 | |
| 2 | |
| 2 | |
| 3 | |
| 3 | B |
| 3 | |
| 3 | C |
+----------+-------+
TABLE B
+----------+-------+
| AC | City |
+----------+-------+
| 1 | A |
| 1 | A |
| 1 | A |
| 1 | A |
| 2 | |
| 2 | |
| 2 | |
| 2 | |
| 3 | B |
| 3 | B |
| 3 | B |
+----------+-------+
If AC=1, refer to Number=1, and loop through the "Letter" values from top to bottom to get the top-most value.
For Number=1, the topmost value is A, so for AC=1, fill in all "City" column as A.
For AC=2, Number=2, and there are no values in Table A, so fill in all "City" for each AC=2 as blank.
For AC=3, Number=3, and the top-most value is B, so fill in all "City" for each AC=3 as B.
How do you code this in standard SQL?
I am using the Caspio software and will be inserting the SQL into the "City" column itself, but that shouldn't interfere too much with the code.
This is what I have so far:
SELECT Letter
FROM TableA
WHERE TableA.Number = TableB.AC
AND TableA.Number != ""
LIMIT 1
But it doesn't seem to be working, and I think it's necessary to loop through Table A to find the City value for each AC=Number.
Thanks for any help.
EDIT:
I have figured out the solution:
SELECT TOP 1 Letter
FROM TableA
WHERE Letter !='' AND Number=AC
Thanks.
It doesnt work because you are not including tableB in your FROM clause, or joining it. You can try this one:
SELECT Letter FROM TableA WHERE Number IN
(SELECT AC FROM TableB WHERE City!='' AND City IS NOT NULL)
AND Letter!='' AND LETTER IS NOT NULL
First things first, don't think of "looping" in SQL, it means that you're thinking about the problem wrong. You can to use set-based thinking.
So think about what you want to do, not how you want to do it.
You want to update the TableB.City based on the value of TableA.Letter
UPDATE TableB
SET City = Letter
FROM
(
SELECT Number, Letter,ROW_NUMBER () OVER ( PARTITION BY Number order by number ) AS SortOrder
FROM TableA
WHERE Letter IS NOT NULL AND Letter != ''
) AS A
WHERE A.SortOrder = 1 AND TableB.AC = A.number
I have included the Row_Number sorting, this is to ensure you get the first letter. Please note that you should order by your PK, assuming you have one and assuming that it's an IDENTITY and an int
See the sqlFiddle
EDIT
Sure, you can just do a select.
SELECT TableB.AC, A.Letter
FROM
(
SELECT Number, Letter,ROW_NUMBER () OVER ( PARTITION BY Number order by number ) AS SortOrder
FROM TableA
WHERE Letter IS NOT NULL AND Letter != ''
) AS A
LEFT OUTER JOIN TableB.AC = A.number
WHERE A.SortOrder = 1

Selecting column from one table and count from another

t1
id | name | include
-------------------
1 | foo | true
2 | bar | true
3 | bum | false
t2
id | some | table_1_id
-------------------------
1 | 42 | 1
2 | 43 | 1
3 | 42 | 2
4 | 44 | 1
5 | 44 | 3
Desired output:
name | count(some)
------------------
foo | 3
bar | 1
What I have currently from looking through other solutions here:
SELECT a.name,
COUNT(r.some)
FROM t1 a
JOIN t2 r on a.id=r.table_1_id
WHERE a.include = 'true'
GROUP BY a.id,
r.some;
but that seems to get me
name | count(r.some)
--------------------
foo | 1
foo | 1
bar | 1
foo | 1
I'm no sql expert (I can do simple queries) so I'm googling around as well but finding most of the solutions I find give me this result. I'm probably missing something really easy.
Just remove the second column from the group by clause
SELECT a.name,
COUNT(r.some)
FROM t1 a
JOIN t2 r on a.id=r.table_1_id
WHERE a.include = 'true'
GROUP BY a.name
Columns you want to use in an aggregate function like sum() or count() must be left out of the group by clause. Only put the columns in there you want to be unique outputted.
This is because multiple column group requires the all column values to be same.
See this link for more info., Using group by on multiple columns
Actually in you case., if some are equal, table_1_id is not equal (And Vice versa). so grouping cannot occur. So all are displayed individually.
If the entries are like,
id | some | table_1_id
-------------------------
1 | 42 | 1
2 | 43 | 1
3 | 42 | 2
4 | 42 | 1
Then the output would have been.,
name | count
------------------
foo | 2 (for 42)
foo | 1 (for 43)
bar | 1 (for 42)
Actually, if you want to group on 1 column as Juergen said, you could remove r.some; from groupby clause.

Select rows appearing after a row with a given ID when sorted by criteria unrelated to the ID

Given the data in the table "people":
+----+-------+
| id | name |
+----+-------+
| 1 | Jane |
| 2 | Joe |
| 4 | John |
| 5 | Alice |
| 6 | Bob |
+----+-------+
And the order:
SELECT * FROM people ORDER BY name
... which would return:
+----+-------+
| id | name |
+----+-------+
| 5 | Alice |
| 6 | Bob |
| 1 | Jane |
| 2 | Joe |
| 4 | John |
+----+-------+
How could one write a query--including the order above--which would return only rows after the one with a given id, e.g., if given an id of 1, it would return:
+----+-------+
| id | name |
+----+-------+
| 2 | Joe |
| 4 | John |
+----+-------+
To be clear, the id is variable and not known before hand.
An approach using commonly supported SQL would be great, but I'm using PostgreSQL 9.2 and ActiveRecord 3.2 if they have anything additional of use, e.g., OVER() and ROW_NUMBER().
[Edit] I'd previously showed the wrong desired result set, including the row with the given id. But, the result set, as described in the question, should only include rows after the given ID.
select *
from people
where
name >= (
select name
from people
where id = 1
)
and id != 1
order by name
So far the simplest approach I've found for a situation where precision is needed, e.g., no missing or duplicate results across multiple calls with varying values for ID is to combine window functions and CTEs, as in:
WITH ordered_people AS (
SELECT *, ROW_NUMBER() OVER (ORDER BY name) AS n
FROM people
ORDER BY name
)
SELECT *
FROM ordered_people
WHERE n > (SELECT n FROM ordered_people WHERE id = 1)
ORDER BY name
;