Preserving Array Member Order in Postgres Query - sql

I would like to know how to preserve/utilize the order of array elements when issuing a select query in Postgres. (In case it's relevant, the array is multidimensional.)
For example, given the following data:
id | points
----+---------------------------------
1 | {{1,3},{7,11},{99,101},{0,1}}
2 | {{99,101},{7,11},{0,1},{77,22}}
I'd like to know how to write a query which finds rows whose points:
contain the subarray {{7, 11}, {99, 101}}
but not {{99, 101},{7, 11}}.
I've tried using various array operators (#>, &&), adding an index using the intarray module, etc. but have not found a workable solution.

to be able to "unnest array by 1 dimention" and use the result set for incomarison, use Pavel Stěhule suggested function:
t=# with c(i,p) as (values(1,'{{1,3},{7,11},{99,101},{0,1}}'::int[][]),(2,'{{99,101},{7,11},{0,1},{77,22}}'))
, p as (select *,a,case when e = '{7, 11}' and lead(e) over (partition by i order by o) = '{99, 101}' and o = lead(o) over (partition by i order by o) -1 then true end from c, reduce_dim(p) with ordinality as a (e,o))
select * from p;
i | p | e | o | a | case
---+---------------------------------+----------+---+----------------+------
1 | {{1,3},{7,11},{99,101},{0,1}} | {1,3} | 1 | ("{1,3}",1) |
1 | {{1,3},{7,11},{99,101},{0,1}} | {7,11} | 2 | ("{7,11}",2) | t
1 | {{1,3},{7,11},{99,101},{0,1}} | {99,101} | 3 | ("{99,101}",3) |
1 | {{1,3},{7,11},{99,101},{0,1}} | {0,1} | 4 | ("{0,1}",4) |
2 | {{99,101},{7,11},{0,1},{77,22}} | {99,101} | 1 | ("{99,101}",1) |
2 | {{99,101},{7,11},{0,1},{77,22}} | {7,11} | 2 | ("{7,11}",2) |
2 | {{99,101},{7,11},{0,1},{77,22}} | {0,1} | 3 | ("{0,1}",3) |
2 | {{99,101},{7,11},{0,1},{77,22}} | {77,22} | 4 | ("{77,22}",4) |
(8 rows)
now, that you see the logic, complete where:
t=# with c(i,p) as (values(1,'{{1,3},{7,11},{99,101},{0,1}}'::int[][]),(2,'{{99,101},{7,11},{0,1},{77,22}}'))
, p as (select *,a,case when e = '{7, 11}' and lead(e) over (partition by i order by o) = '{99, 101}' and o = lead(o) over (partition by i order by o) -1 then true end from c, reduce_dim(p) with ordinality as a (e,o))
select i,p from p where "case";
i | p
---+-------------------------------
1 | {{1,3},{7,11},{99,101},{0,1}}
(1 row)
not to mention that in case of sequential array pair, you can just cast it to text and use like operator:
t=# with c(i,p) as (values(1,'{{1,3},{7,11},{99,101},{0,1}}'::int[][]),(2,'{{99,101},{7,11},{0,1},{77,22}}'))
select * from c where p::text like '%{7,11},{99,101}%';
i | p
---+-------------------------------
1 | {{1,3},{7,11},{99,101},{0,1}}
(1 row)

Related

Can I have SQL incrementally count XML elements while parsing?

So this is my first foray into parsing XML, and I'm trying to figure out how to get this to work how I want.
Given the following XML format:
<Tiles>
<TileRow>
<TileValue>2</TileValue>
<TileValue>3</TileValue>
<TileValue>4</TileValue>
</TileRow>
<TileRow>
<TileValue>2</TileValue>
<TileValue>7</TileValue>
</TileRow>
</Tiles>
I want it to put it in a SQL table as the following:
| X | Y | Val |
|---|---|-----|
| 1 | 1 | 2 |
| 1 | 2 | 3 |
| 1 | 3 | 4 |
| 2 | 1 | 2 |
| 2 | 2 | 7 |
Basically, imagine a grid, and each "TileRow" starts a new Row in that grid. Each "TileValue" assigns the Column position in that grid, with the actual TileValue being what's in the 'cell' in that grid.
Is there a way to make SQL 'count' each time it passes over an element, or something to that effect?
Please try the following solution.
It is based on the Node Order Comparison operator in XQuery.
Node Order Comparison Operators
SQL
DECLARE #xml XML =
N'<Tiles>
<TileRow>
<TileValue>2</TileValue>
<TileValue>3</TileValue>
<TileValue>4</TileValue>
</TileRow>
<TileRow>
<TileValue>2</TileValue>
<TileValue>7</TileValue>
</TileRow>
</Tiles>';
SELECT c.value('for $i in . return count(/Tiles/TileRow[. << $i])', 'INT') AS [X]
, c.value('for $i in . return count(../*[. << $i]) + 1', 'INT') AS [Y]
, c.value('(./text())[1]', 'INT') as Value
FROM #xml.nodes('/Tiles/TileRow/TileValue') AS t(c);
Output
+---+---+-------+
| X | Y | Value |
+---+---+-------+
| 1 | 1 | 2 |
| 1 | 2 | 3 |
| 1 | 3 | 4 |
| 2 | 1 | 2 |
| 2 | 2 | 7 |
+---+---+-------+
Unfortunately, SQL Server does not support returning position() directly, it only allows it inside a predicate. If it did then you could simply query:
select
v.TileValue.value('position(parent::*)[1]','int'),
v.TileValue.value('./position()[1]','int'),
v.TileValue.value('text()[1]','int')
from #x.nodes('/Tiles/TileRow/TileValue') v(TileValue)
Instead, you can simulate it with ROW_NUMBER()
select
v1.rn,
row_number() over (partition by v1.rn order by (select 1)),
v2.TileValue.value('text()[1]','int')
from (
select v1.TileRow.query('.') TileRow,
row_number() over (order by (select 1)) rn
from #x.nodes('/Tiles/TileRow') v1(TileRow)
) v1
cross apply v1.TileRow.nodes('TileRow/TileValue') v2(TileValue)

How to select oldest date row from each product using SQL

I would like to get the oldest only one from every type product and sum of the prices listed in listofproduct table. Another thing is to search only between prodacts that has at least one peace on lager.
With the SQL I managed to get all the products has at least one on the stock. But the rest I am stack...
So the sum cold be done later, that was my plan, but if you have better idea feel free to write
Here is my data:
+-------------+----------------+---------------+----------+
| IDProizvoda | NazivProizvoda | DatumKupovine | NaLageru |
+-------------+----------------+---------------+----------+
| 77 | Cokolada | 25-Feb-20 | 2 |
| 44 | fgyhufrthr | 06-Aug-20 | 5 |
| 55 | Auto | 06-Aug-23 | 0 |
| 55 | Auto | 11-Aug-20 | 200 |
| 77 | Cokolada | 06-Aug-27 | 0 |
| 77 | Cokolada | 25-Feb-20 | 10 |
| 77 | Cokolada | 25-Jan-20 | 555 |
| 77 | Cokolada | 25-Mar-20 | 40 |
+-------------+----------------+---------------+----------+
Access.ExeQuery("SELECT * FROM Products " &
"WHERE IDProizvoda IN (SELECT value FROM STRING_SPLIT(#listofproduct, ',')) " &
"AND NaLageru > 0 ")
I tried to add GROUP BY and HAVING but it does not worked because i choose the whole table. But I need Product ID and Stock field for edit it later, to subtract one from the stock for those products.
I would like to get the result:
+-------------+----------------+---------------+----------+
| IDProizvoda | NazivProizvoda | DatumKupovine | NaLageru |
+-------------+----------------+---------------+----------+
| 44 | fgyhufrthr | 06-Aug-20 | 5 |
| 55 | Auto | 11-Aug-20 | 200 |
| 77 | Cokolada | 25-Jan-20 | 555 |
+-------------+----------------+---------------+----------+
Thank you for all the help.
You can do it with a Cross Apply, this would be your SQL query:
Select P.IDProizvoda,
P.NazivProizvoda,
N.DatumKupovine,
N.NaLageru,
N.IDKupovine,
N.CenaPoKomadu
From
products P
Cross Apply
(
Select top 1 DatumKupovine,
NaLageru,
IDKupovine,
CenaPoKomadu
From products P2
where P2.IDProizvoda = P.IDProizvoda
and P2.NaLageru > 0
order by DatumKupovine
) N
group by P.IDProizvoda, P.NazivProizvoda, N.DatumKupovine, N.NaLageru, N.IDKupovine, N.CenaPoKomadu
And this your ExeQuery:
Access.ExeQuery("Select P.IDProizvoda, P.NazivProizvoda, N.DatumKupovine, N.NaLageru, N.IDKupovine, N.CenaPoKomadu From products P " &
" Cross Apply( Select top 1 DatumKupovine, NaLageru, IDKupovine, CenaPoKomadu From products P2 where P2.IDProizvoda = P.IDProizvoda and P2.NaLageru > 0 order by DatumKupovine) N " &
" where P.IDProizvoda in (Select value From STRING_SPLIT(#listofproduct, ',')) " &
" group by P.IDProizvoda, P.NazivProizvoda, N.DatumKupovine, N.NaLageru, N.IDKupovine, N.CenaPoKomadu " )
I think this is just aggregation with a filter:
SELECT IDProizvoda, NazivProizvoda, MAX(DatumKupovine),
SUM(NaLegaru)
FROM Products p
WHERE NaLegaru > 0
GROUP BY IDProizvoda, NazivProizvoda;
This should do it:
with cte as (
SELECT *, row_number() over (
partition by NazivProizvoda
order by DatumKupovine
) as rn
FROM Products
WHERE IDProizvoda IN (
SELECT value
FROM STRING_SPLIT(#listofproduct, ',')
)
AND NaLageru > 0
)
select *
from cte
where rn = 1;
By way of explanation, I'm using a common table expression to select the superset of the data you want by criteria and adding a column that enumerates each row within a group (a group being defined here as having NazivProizvoda be the same) in order of the DatumKupovine). With that done, anything that admits the value of 1 for that enumeration will be the oldest in the group. If you data is such that more than one row can be the oldest, use rank() instead of row_number().

SELECT 1 ID and all belonging elements

I try to create a json select query which can give me back the result on next way.
1 row contains 1 main_message_id and belonging messages. (Like the bottom image.) The json format is not a requirement, if its work with other methods, it will be fine.
I store the data as like this:
+-----------------+---------+----------------+
| main_message_id | message | sub_message_id |
+-----------------+---------+----------------+
| 1 | test 1 | 1 |
| 1 | test 2 | 2 |
| 1 | test 3 | 3 |
| 2 | test 4 | 4 |
| 2 | test 5 | 5 |
| 3 | test 6 | 6 |
+-----------------+---------+----------------+
I would like to create a query, which give me back the data as like this:
+-----------------+-----------------------+--+
| main_message_id | message | |
+-----------------+-----------------------+--+
| 1 | {test1}{test2}{test3} | |
| 2 | {test4}{test5}{test6} | |
| 3 | {test7}{test8}{test9} | |
+-----------------+-----------------------+--+
You can use json_agg() for that:
select main_message_id, json_agg(message) as messages
from the_table
group by main_message_id;
Note that {test1}{test2}{test3} is invalid JSON, the above will return a valid JSON array e.g. ["test1", "test2", "test3"]
If you just want a comma separated list, use string_agg();
select main_message_id, string_ag(message, ', ') as messages
from the_table
group by main_message_id;

ORDER BY FIELD LIST - Subquery returns more than 1 row

What i want to do is quite simple:
Write an SQL that will return a bunch of record and order the records by some list of id from the FIELD LIST section of my SQL
TABLE SAMPLE
lessons
+----+----------------------+
| id | name |
+----+----------------------+
| 9 | Greedy algorithms |
| 5 | Maya civilization |
| 3 | eFront Beginner |
| 2 | eFront Intermediate |
+----+----------------------+
mod_comp_rule
+----+---------------------+
| id | lesson_id | comp_id |
+----+---------------------+
| 1 | 3 | 1 |
| 2 | 2 | 1 |
| 3 | 9 | 2 |
+----+---------------------+
WHAT I WANT TO GET FROM MY QUERY
SELECT * FROM lessons ORDER BY FIELD(id,'3','2','9') ASC;
MY SQL
SELECT ls.id, ls.name
FROM lessons ls
ORDER BY FIELD(ls.id,
(SELECT mcr.lesson_id FROM mod_comp_rule mcr
INNER JOIN lessons ls ON ls.id = mcr.lesson_id))
My SQL Query returned the following error
MySQL said: #1242 - Subquery returns more than 1 row
So how can i make my SQL return FIELD(id,'3','2','9') without flagging the more than 1 row error ?
I don't see why FIELD() is needed for this. A correlated query will do what you want:
SELECT ls.id, ls.name
FROM lessons ls
ORDER BY (SELECT mcr.id FROM mod_comp_rule mcr WHERE ls.id = mcr.lesson_id);

Pick a record based on a given value in postgres

I have a table in postgres like below,
alg_campaignid | alg_score | cp | sum
----------------+-----------+---------+----------
9829 | 30.44056 | 12.4000 | 12.4000
9880 | 29.59280 | 12.0600 | 24.4600
9882 | 29.59280 | 12.0600 | 36.5200
9827 | 29.27504 | 11.9300 | 48.4500
9821 | 29.14840 | 11.8800 | 60.3300
9881 | 29.14840 | 11.8800 | 72.2100
9883 | 29.14840 | 11.8800 | 84.0900
10026 | 28.79280 | 11.7300 | 95.8200
10680 | 10.31504 | 4.1800 | 100.0000
From which i have to select a record based on randomly generated number from 0 to 100.i.e first record should be returned if random number picked is between 0 and 12.4000,second if rendom is between 12.4000 and 24.4600,and likewise last if random no is between 95.8200 and 100.0000.
For Example
if the random number picked is 8 then the first record should be returned
or
if the random number picked is 48 then the fourth record should be returned
Is it possible to do this postgres if so kindly recommend a solution for this..
Yes, you can do this in Postgres. If you want to generate the number in the database:
with r as (
select random() * 100 as r
)
select t.*
from table t cross join r
where t.sum <= r.r
order by t.sum desc
limit 1;