How to display output like this in SQL? - sql

I have a table like the one shown below:
+----------------+-------+----------+---------+
| Name | Model | system | ItemTag |
+----------------+-------+----------+---------+
| Alarm Id | T58 | ASC | |
+----------------+-------+----------+---------+
| Door Lock | F48 | ASC | |
+----------------+-------+----------+---------+
| AlarmSounder | T58 | ASC | |
+----------------+-------+----------+---------+
| Card Reader | K12 | ASC | |
+----------------+-------+----------+---------+
| Magnetic Lock | F48 | ASC | |
+----------------+-------+----------+---------+
| T2 Card Reader | K12 | ASC | |
+----------------+-------+----------+---------+
| Power Supply | Null | ASC | |
+----------------+-------+----------+---------+
| Battery | Null| ASC | |
+----------------+-------+----------+---------+
Now I want to display the data like this:
+-------------+-------+--------+--------+
| Name | Model | system | count |
+-------------+-------+--------+--------+
| Alarm | T58 | ASC | 2 |
+-------------+-------+--------+--------+
| Door Lock | F58 | ASC | 2 |
+-------------+-------+--------+--------+
| Card Reader | K12 | ASC | 2 |
+-------------+-------+--------+--------+
|Power supply | Null | ASC | 1 |
+-------------+-------+--------+--------+
| Battery | Null | ASC | 1 |
+-------------+-------+--------+--------+
How to do it in SQL?
Updated
I also included null column as my second update.

You could use windowed functions:
SELECT Name, Model, system, cnt AS count
FROM (SELECT *, COUNT(*) OVER(PARTITION BY Model) AS cnt,
ROW_NUMBER() OVER(PARTITION BY Model ORDER BY ...) AS rn
FROM your_tab) AS sub
WHERE rn = 1;
Rextester Demo
Keep in mind that you need a column to sort so (id/timestamp) should be used to get first value in a group.
EDIT:
As i have different Name relating to null column. how can i seperate it out
SELECT Name, Model, system, cnt AS count
FROM (SELECT *, COUNT(*) OVER(PARTITION BY Model) AS cnt,
ROW_NUMBER() OVER(PARTITION BY Model ORDER BY id) AS rn
FROM my_tab
WHERE Model IS NOT NULL) AS sub
WHERE rn = 1
UNION ALL
SELECT Name, Model, system, 1
FROM my_tab
WHERE Model IS NULL;
RextesterDemo 2

You can have a simple query as below
SELECT MIN(Name) Name,
Model,
system,
COUNT(*) [count]
FROM yourtable
GROUP BY Model, system
Result
Name Model system count
Door Lock F58 ASC 2
Card Reader K12 ASC 2
Alarm Id T58 ASC 2

lad2025's solution simplified, calculate both NULL and NOT NULL in a single step and add some logic for the NULL rows:
SELECT Name, Model, system,
CASE WHEN Model IS NULL THEN 1 ELSE cnt END AS count
FROM
(
SELECT *,
COUNT(*) OVER(PARTITION BY Model) AS cnt,
ROW_NUMBER() OVER(PARTITION BY Model ORDER BY Name) AS rn
FROM my_tab
) AS sub
WHERE rn = 1 -- one row per model
OR Model IS NULL; -- all rows for the NULL model

Related

How to compare the current row with all the others in PostgreSQL?

I have a table like this
| id | state | updatedate |
|:--------|:---------------|:------------|
| 1 | state_review | 1668603529 |
| 1 | state_review | 1668601821 |
| 1 | state_review_2 | 1668601821 |
| 2 | state_review | 1668601709 |
| 2 | state_review | 1668600822 |
| 2 | state_review_2 | 1668600747 |
| 3 | state_review | 1668559849 |
| 3 | state_review_2 | 1668539849 |
| 3 | state_review | 1668529849 |
| 3 | state_review_2 | 1661599849 |
| 3 | state_review | 1668599849 |
I'm trying to find how to count first occurance of changed state for all ids based on provided values, i have two incoming states from(state_review) to(state_review_2)
in this particular case there would be only three changed states that are going
from state_review -> state_review_2
resulting table would look like this
| amount |
|:--------|
| 3 |
I suspect window function might help with this but i'm not sure how to compare current state with all the others, states have to be ordered by id
Was trying to use this query, but that doesn't seem to work, instead of counting the latest unique transitions it counts all of them, if the first found transition doesn't match given states then skip the entire section for a certain id
SELECT
COUNT(DISTINCT (
CASE
WHEN
(
q.state = 'state_review'
AND 'state_review' != 'state_review_2'
)
THEN
ID
END
)) AS amount
FROM
(
SELECT
id,
state
FROM
states_table
WHERE
updatedate >= 1668603529
AND updatedate <= 1671599849
AND
(
state = 'state_review'
OR state = 'state_review_2'
)
ORDER BY
id, updatedate DESC
)
AS q
Transitions between 2 predefined states can be obtained with a LAG function.
Example with state_review and state_review_2
SELECT *
FROM(
SELECT ID, LAG(State) OVER (PARTITION BY ID ORDER BY updatedate) As FromState, State, updatedate
FROM States_table
) T
WHERE FromState = 'state_review' AND state = 'state_review2'
You can do variations of the above:
To avoid double-counting when an id transitioned from state S1 to state S2 several times, change the sub-query with DISTINCT and without updatedate like so: SELECT DISTINCT ID, LAG(State) OVER (PARTITION BY ID ORDER BY updatedate) As FromState, State
And of course, do SELECT COUNT(*) instead if all you want is the count.

Get some values from the table by selecting

I have a table:
| id | Number |Address
| -----| ------------|-----------
| 1 | 0 | NULL
| 1 | 1 | NULL
| 1 | 2 | 50
| 1 | 3 | NULL
| 2 | 0 | 10
| 3 | 1 | 30
| 3 | 2 | 20
| 3 | 3 | 20
| 4 | 0 | 75
| 4 | 1 | 22
| 4 | 2 | 30
| 5 | 0 | NULL
I need to get: the NUMBER of the last ADDRESS change for each ID.
I wrote this select:
select dh.id, dh.number from table dh where dh =
(select max(min(t.history)) from table t where t.id = dh.id group by t.address)
But this select not correctly handling the case when the address first changed, and then changed to the previous value. For example id=1: group by return:
| Number |
| -------- |
| NULL |
| 50 |
I have been thinking about this select for several days, and I will be happy to receive any help.
You can do this using row_number() -- twice:
select t.id, min(number)
from (select t.*,
row_number() over (partition by id order by number desc) as seqnum1,
row_number() over (partition by id, address order by number desc) as seqnum2
from t
) t
where seqnum1 = seqnum2
group by id;
What this does is enumerate the rows by number in descending order:
Once per id.
Once per id and address.
These values are the same only when the value is 1, which is the most recent address in the data. Then aggregation pulls back the earliest row in this group.
I answered my question myself, if anyone needs it, my solution:
select * from table dh1 where dh1.number = (
select max(x.number)
from (
select
dh2.id, dh2.number, dh2.address, lag(dh2.address) over(order by dh2.number asc) as prev
from table dh2 where dh1.id=dh2.id
) x
where NVL(x.address, 0) <> NVL(x.prev, 0)
);

SQL group by but order is important

is there any option to group by items but order of grouping is important?
Let's assume I have table with hardware and it's assigned to some users. And this hardware has some states like broken, ok, service. I want to group this table to have information, how long user had this item, but state is not important.
What I have:
+----+-------+--------+------------+------------+
| id | owner | state | from | to |
+----+-------+--------+------------+------------+
| 1 | ow1 | ok | 01.02.2019 | 04.06.2019 |
| 2 | ow1 | broken | 04.06.2019 | 12.06.2019 |
| 3 | srvc | fixing | 12.06.2019 | 17.06.2019 |
| 4 | ow1 | ok | 17.06.2019 | null | -- null - still has
+----+-------+--------+------------+------------+
But I want to have:
+-------+------------+------------+
| owner | from | to |
+-------+------------+------------+
| ow1 | 01.02.2019 | 12.06.2019 | -- here we have min and max dates before state changed
| srvc | 12.06.2019 | 17.06.2019 |
| ow1 | 17.06.2019 | null | -- null - still has
+-------+------------+------------+
How to write query to achieve this result?
This looks like a gaps and islands problem. One solution is follows:
Mark rows where owner changes (different from previous row) with a value 1
Group all 1s and subsequent 0s together
I usually do this:
WITH cte1 AS (
SELECT *
, CASE WHEN owner = LAG(owner) OVER (PARTITION BY hardware_id ORDER BY [from]) THEN 0 ELSE 1 END AS chg
FROM t
), cte2 AS (
SELECT *
, SUM(chg) OVER (PARTITION BY hardware_id ORDER BY [from]) AS grp
FROM cte1
)
SELECT owner
, hardware_id
, grp
, MIN([from])
, MAX([to])
FROM cte2
GROUP BY owner, hardware_id, grp
I have assumed that you want separate results per every piece of hardware, remove the hardware column if that is not the case.
Demo on db<>fiddle
Try this below option with union all.
SELECT owner,from,to
FROM your_table
WHERE to IS NULL
UNION ALL
SELECT owner,MIN(from),MAX(to)
FROM your_table
WHERE to IS NOT NULL
GROUP BY owner

How to Number Each Group in PostgreSQL

How would I use a window function or similar, to number each group or partition of rows, based on certain shared characteristics?
For example:
I have a list of names ordered alphabetically that I wish to group and identify using IDs that describe the group that they belong to and position within each group.
-------------------------------------------
| outer_id | inner_id | src_id | name |
|----------|----------|--------|----------|
| 1 | 1 | 88129 | albert |
| 1 | 2 | 88130 | albrecht |
| 1 | 3 | 88131 | allan |
| 2 | 1 | 88132 | barnaby |
| 2 | 2 | 88133 | barry |
| 2 | 3 | 88134 | bart |
-------------------------------------------
I can achieve inner_id, src_id and name using a query similar to the following:
WITH cte (src_id, name) AS (
VALUES
(88129, 'albert'),
(88130, 'albrecht'),
(88131, 'allan'),
(88132, 'barnaby'),
(88133, 'barry'),
(88134, 'bart')
)
SELECT row_number() OVER (partition by left(name, 1) ORDER BY name DESC) AS inner_id, src_id, name
FROM cte;
How would I go about adding an outer_id column as shown, to represent each window (or group)?
You can use dense_rank():
select dense_rank() over (order by left(name, 1)) as outer_id,
row_number() over (partition by left(name, 1) order by name desc) as inner_id,
src_id, name
from cte;

SELECT only latest record of an ID from given rows

I have this table shown below...How do I select only the latest data of the id based on changeno?
+----+--------------+------------+--------+
| id | data | changeno | |
+----+--------------+------------+--------+
| 1 | Yes | 1 | |
| 2 | Yes | 2 | |
| 2 | Maybe | 3 | |
| 3 | Yes | 4 | |
| 3 | Yes | 5 | |
| 3 | No | 6 | |
| 4 | No | 7 | |
| 5 | Maybe | 8 | |
| 5 | Yes | 9 | |
+----+---------+------------+-------------+
I would want this result...
+----+--------------+------------+--------+
| id | data | changeno | |
+----+--------------+------------+--------+
| 1 | Yes | 1 | |
| 2 | Maybe | 3 | |
| 3 | No | 6 | |
| 4 | No | 7 | |
| 5 | Yes | 9 | |
+----+---------+------------+-------------+
I currently have this SQL statement...
SELECT id, data, MAX(changeno) as changeno FROM Table1 GROUP BY id;
and clearly it doesn't return what I want. This should return an error because of the aggrerate function. If I added fields under the GROUP BY clause it works but it doesn't return what I want. The SQL statement is by far the closest I could think of. I'd appreciate it if anybody could help me on this. Thank you in advance :)
This is typically referred to as the "greatest-n-per-group" problem. One way to solve this in SQL Server 2005 and higher is to use a CTE with a calculated ROW_NUMBER() based on the grouping of the id column, and sorting those by largest changeno first:
;WITH cte AS
(
SELECT id, data, changeno,
rn = ROW_NUMBER() OVER (PARTITION BY id ORDER BY changeno DESC)
FROM dbo.Table1
)
SELECT id, data, changeno
FROM cte
WHERE rn = 1
ORDER BY id;
You want to use row_number() for this:
select id, data, changeno
from (SELECT t.*,
row_number() over (partition by id order by changeno desc) as seqnum
FROM Table1 t
) t
where seqnum = 1;
Not a well formed or performance optimized query but for small tasks it works fine.
SELECT * FROM TEST
WHERE changeno IN (SELECT MAX(changeno)
FROM TEST
GROUP BY id)
for other alternatives :
DECLARE #Table1 TABLE
(
id INT, data VARCHAR(5), changeno INT
);
INSERT INTO #Table1
SELECT 1,'Yes',1
UNION ALL
SELECT 2,'Yes',2
UNION ALL
SELECT 2,'Maybe',3
UNION ALL
SELECT 3,'Yes',4
UNION ALL
SELECT 3,'Yes',5
UNION ALL
SELECT 3,'No',6
UNION ALL
SELECT 4,'No',7
UNION ALL
SELECT 5,'Maybe',8
UNION ALL
SELECT 5,'Yes',9
SELECT Y.id, Y.data, Y.changeno
FROM #Table1 Y
INNER JOIN (
SELECT id, changeno = MAX(changeno)
FROM #Table1
GROUP BY id
) X ON X.id = Y.id
WHERE X.changeno = Y.changeno
ORDER BY Y.id