Special order by on SQL query - sql

I need to display a list of records from a database table ordered by some numeric column. The table looks like this:
CREATE TABLE items (
position int NOT NULL,
name varchar(100) NOT NULL,
);
INSERT INTO items (position, name) VALUE
(1, 'first'),
(5, 'second'),
(8, 'third'),
(9, 'fourth'),
(15, 'fifth'),
(20, 'sixth');
Now, the order of the list should change according to a parameter provided by the user. This parameter specifies which record comes first like this:
position = 0
order should be = 1, 5, 8, 9, 15, 20
position = 1
order should be = 20, 1, 5, 8, 9, 15
position = 2
order should be = 15, 20, 1, 5, 8, 9
In other words the last record becomes the first and so on. Can you think of a way to do this in SQL?
I'm using MySQL but an example in any SQL database will do.
Thanks

See how this works for you. Uses generic SQL so it should be valid for MySql (untested) as well.
DECLARE #user_sort INTEGER
SET #user_sort = 0
SELECT position, name FROM
(
SELECT I1.position, I1.name, COUNT(*) AS rownumber, (SELECT COUNT(*) FROM items) AS maxrows
FROM items I1, items I2
WHERE I2.position <= I1.position
GROUP BY I1.position, I1.name
) Q1
ORDER BY
CASE WHEN maxrows - rownumber < (#user_sort % maxrows) THEN 1 ELSE 2 END, position
Note:
* If the user provided sort index is greater than the row count, the value will wrap to within the valid range. To remove this functionality, remove the "% maxrows" from the ORDER BY.
Results:
SET #user_sort = 0
position name
1 first
5 second
8 third
9 fourth
15 fifth
20 sixth
SET #user_sort = 1
position name
20 sixth
1 first
5 second
8 third
9 fourth
15 fifth
SET #user_sort = 2
position name
15 fifth
20 sixth
1 first
5 second
8 third
9 fourth
SET #user_sort = 9
9 fourth
15 fifth
20 sixth
1 first
5 second
8 third

Are you sure you want to do this in SQL?
To me, this sounds like you should load the results in a dataset of some sort, and then either re-order them as you want, or position the starting point at the correct position.
Possibly using a linked list.

ORDER BY (FIELD(position, 1, 5, 8, 9, 15, 20) + parameter) % 7
Edit: To make the peanut gallery happy, the general solution is:
ORDER BY (SELECT ix + parameter - 1 FROM (SELECT i.position, #ix := #ix + 1 AS ix FROM (SELECT #ix := 0) AS n, items AS i ORDER BY position) AS s WHERE s.position = items.position) % (SELECT COUNT(*) FROM items)

I'm riffing on beach's solution here, but eliminating the self-join and only selecting from the items table twice (and using Oracle syntax):
select
i.position
, i.name
from(
select
items.*
, ( SELECT COUNT(*) FROM items ) AS maxrows
from items
order by position
) i
order by
case
when rownum > maxrows - 2 -- NOTE: change "2" to your "position" variable
then 1 - 1 / rownum -- pseudo-rownum < 1, still ascending
else
rownum
end
;

If it's a set list that you know the number of items you could do something like:
SELECT *
FROM Items
ORDER BY CASE WHEN Position >= Position THEN POSITION ELSE Position+1000 END
But its really ugly.

This really is not an ideal thing to be doing in SQL.
I have solution, but with large tables it will be slow.
DECLARE #iOrder INT
SET #iOrder = 4
SELECT abc.position,abc.name FROM
(
SELECT position,name,ROW_NUMBER() OVER (ORDER BY position) AS rownum
FROM items
) abc
WHERE abc.rownum >= #iOrder
UNION ALL
SELECT def.position, def.name FROM
(
SELECT position,name,ROW_NUMBER() OVER (ORDER BY position) AS rownum
FROM items
) def
WHERE def.rownum < #iOrder
Note that the use of UNION (Without the all) will reorder the results as it'll be looking for duplicates

As per John's comment, but with LIMIT syntax instead (ROW_NUMBER/OVER doesn't work in MySQL and besides LIMIT is much easier to read):
(
SELECT position, name FROM items
ORDER BY position
LIMIT #offset, #bignum
) UNION ALL (
SELECT position, name FROM items
ORDER BY position
LIMIT #offset
)
Where #bignum is an arbitrary number higher than any number of results you might have.
I'm still not wholly convinced this will in practice be faster than rearranging the list on the web server... would depend exactly how you were dealing with the result set and how big it was, I suppose. But at least it avoids the self-cross-join involved in beach's clever approach.

Related

Split column value into separate columns based on length

I have multiple comma-separated values in one column with a size up to 20000 characters, and I want to split that column into one column but its based on character values 2000 (like into one new column it will take 2000 character and if length is grater than 2000 then its will be in second column like this).
When it's comma-separated value goes into first new column, then it should be meaningful like it should be based on , and up to 2000 characters only like this.
I have done from row level value to column level only but its should be 2000 character and based on comma
Could you please help me with this ?
siddesh, although this question lacks of everything I want to point some things out and help you (as you are an unexperienced SO-user):
First I set up a minimal reproducible exampel. This is on you the next time.
I'll start with a declared table with some rows inserted.
We on SO can copy'n'paste this into our environment which makes it easy to answer.
DECLARE #tbl TABLE(ID INT IDENTITY, YourCSVString VARCHAR(MAX));
INSERT INTO #tbl VALUES('1 this is long text, 2 some second fragment, 3 third fragment, 4 adfjksdahfljsadhfjhadlfhasdjks alsdjfsadhf k, 5 halksjfh asdkf ')
,('1 this is other long text, 2 some second fragment to show that this works with tabular data, 3 again a third fragment, 4 adfjksdahfljsadhfjhadlfhasdjks alsdjfsadhf k, 5 halksjfh asdkf ');
--This is, what you actually need:
SELECT fkID = t.ID
,B.fragmentPosition
,B.fragmentContent
,C.framgentLength
FROM #tbl t
CROSS APPLY OPENJSON(CONCAT(N'["',REPLACE(t.YourCSVString,N',','","'),'"]')) A
CROSS APPLY(VALUES(A.[key],TRIM(A.[value]))) B(fragmentPosition,fragmentContent)
CROSS APPLY(VALUES(LEN(B.fragmentContent))) C(framgentLength);
The result should be stored within a physical table, where the fkID points to the ID of the original row and the fragmentPosition stores the order. fkID and fragmentPosition should be a combined unique key.
If you really want to do, what you are suggesting in your question (not recommended!) you can try something along this:
DECLARE #maxPerColumn INT=75; --You can set the portion's max size, in your case 2000.
WITH cte AS
(
SELECT fkID = t.ID
,B.fragmentPosition
,B.fragmentContent
,C.framgentLength
FROM #tbl t
CROSS APPLY OPENJSON(CONCAT(N'["',REPLACE(t.YourCSVString,N',','","'),'"]')) A
CROSS APPLY(VALUES(A.[key],TRIM(A.[value]))) B(fragmentPosition,fragmentContent)
CROSS APPLY(VALUES(LEN(B.fragmentContent))) C(framgentLength)
)
,recCTE AS
(
SELECT *
,countPerColumn = 1
,columnCounter = 1
,sumLength = LEN(fragmentContent)
,growingString = CAST(fragmentContent AS NVARCHAR(MAX))
FROM cte WHERE fragmentPosition=0
UNION ALL
SELECT r.fkID
,cte.fragmentPosition
,cte.fragmentContent
,cte.framgentLength
,CASE WHEN A.newSumLength>#maxPerColumn THEN 1 ELSE r.countPerColumn + 1 END
,r.columnCounter + CASE WHEN A.newSumLength>#maxPerColumn THEN 1 ELSE 0 END
,CASE WHEN A.newSumLength>#maxPerColumn THEN LEN(cte.fragmentContent) ELSE newSumLength END
,CASE WHEN A.newSumLength>#maxPerColumn THEN cte.fragmentContent ELSE CONCAT(r.growingString,N', ',cte.fragmentContent) END
FROM cte
INNER JOIN recCTE r ON r.fkID=cte.fkID AND r.fragmentPosition+1=cte.fragmentPosition
CROSS APPLY(VALUES(r.sumLength+LEN(cte.fragmentContent))) A(newSumLength)
)
SELECT TOP 1 WITH TIES
fkID
,growingString
,LEN(growingString)
FROM recCTE
ORDER BY ROW_NUMBER() OVER(PARTITION BY fkID,columnCounter ORDER BY countPerColumn DESC );
The result
fkID pos Content
1 2 1 this is long text, 2 some second fragment, 3 third fragment
1 4 4 adfjksdahfljsadhfjhadlfhasdjks alsdjfsadhf k, 5 halksjfh asdkf
2 0 1 this is other long text
2 1 2 some second fragment to show that this works with tabular data
2 3 3 again a third fragment, 4 adfjksdahfljsadhfjhadlfhasdjks alsdjfsadhf k
2 4 5 halksjfh asdkf
The idea in short:
The first cte does the splitting (as above)
The recursive cte will iterate down the string and do the magic.
The final SELECT uses a hack with TOP 1 WITH TIES together with an ORDER BY ROW_NUMBER() OVER(...). This will return the highest intermediate result only.
Hint: Don't do this...
UPDATE
Just for fun:
You can replace the final SELECT with this
,getPortions AS
(
SELECT TOP 1 WITH TIES
fkID
,fragmentPosition
,growingString
,LEN(growingString) portionLength
FROM recCTE
ORDER BY ROW_NUMBER() OVER(PARTITION BY fkID,columnCounter ORDER BY countPerColumn DESC )
)
SELECT p.*
FROM
(
SELECT fkID
,CONCAT(N'col',ROW_NUMBER() OVER(PARTITION BY fkID ORDER BY fragmentPosition)) AS ColumnName
,growingString
FROM getPortions
) t
PIVOT(MAX(growingString) FOR ColumnName IN(col1,col2,col3,col4,col5)) p;
This will return exactly what you are asking for.
But - as said before - this is against all rules of best practice...

SQL query to select equal or less than or greater than

Assuming that there is a table t with the following columns: Code int, Name nvarchar(50).
I'd would like to query the table for the most matching row for a given Code c. The 'most matching' criteria (in order of importance):
1) select a row whose Code matches c
2) select a row whose Code is greater than c (but the very first one). For example, if c = 4 and t
contains 1, 2, 3, 5, 6, and 7, I'd like to select 5.
3) select a row whose
Code is less than c. For example, if c = 4 and t contains 3, 2, and
1, I'd like to select 3.
The code is going to be in a stored procedure.
Could someone please suggest how to accomplish the above.
Thanks.
Sample data and expected results:
1, "Name1"
2, "Name2"
4, "Name4"
5, "Name5"
If c=2, result: 2,"Name2"
If c=3, result: 4,"Name4"
if c=6, result: 5,"Name5"
I'd order the rows by two criteria - the absolute distance from the target number and whether it's greater or lesser than it, and just pick the top row. E.g., assuming the target code is 4:
SELECT TOP 1 *
FROM t
ORDER BY ABS(code - 4) ASC, CASE WHEN code > 4 THEN 1 ELSE 0 END DESC
That is a top 1 query; you want the one best matching record. So select TOP 1 along the desired order in ORDER BY.
select top 1 *
from mytable
order by
case when code = #code then 1
when code > #code then 2
else 3
end,
abs(code - #code);

Add sequence record numbers

I have a table with with two fields: ID field (FID) as primary key and Qty both are integers. I need to query this table and add two additional fields as sequence serial using the QTY field so I can get results like this:
table1
-------------------------------------
ID Qty range_begin range_end
50 2 1 2
53 1 3 3
65 3 4 6
67 2 7 8
range_begin field start at 1, next record should be range_end + 1.
And range_end = last range_end + Qty
I tried to write it using context variables Like this
I set all variables to null first
rdb$set_context('USER_TRANSACTION', 'range_end', null);
rdb$set_context('USER_TRANSACTION', 'range_begin', null);
Than I start the query:
Select
rdb$get_context('USER_TRANSACTION', 'range_begin'),
rdb$set_context('USER_TRANSACTION', 'range_begin', COALESCE(CAST(rdb$get_context('USER_TRANSACTION', 'range_end') AS INTEGER), 0) + 1)),
rdb$get_context('USER_TRANSACTION', 'range_end'),
rdb$set_context('USER_TRANSACTION', 'range_end', COALESCE(CAST(rdb$get_context('USER_TRANSACTION', 'range_end') AS INTEGER), 0) + Qty)),
Qty
from table1 where ...
But I did not able to get the correct sequence. I also tried to add more variables and other things but none worked for me so how this should be done so I can use single select query ?
I am using FireBird 2.5 and it is not necessary for the answer to use context variables.
Basically, what you want is
SELECT t.ID, t.QTY,
((select coalesce(sum(qty),0) from table1 a where a.ID < t.ID) + 1) as RANGE_BEGIN,
((select coalesce(sum(qty),0) from table1 a where a.ID < t.ID) + t.qty) as RANGE_END
FROM table1 t
For a big table this might get slow, so instead of calculated field you might want to use permanent fields and calculate the range values in trigger... it depend how the data changes etc.

SQL random number that doesn't repeat within a group

Suppose I have a table:
HH SLOT RN
--------------
1 1 null
1 2 null
1 3 null
--------------
2 1 null
2 2 null
2 3 null
I want to set RN to be a random number between 1 and 10. It's ok for the number to repeat across the entire table, but it's bad to repeat the number within any given HH. E.g.,:
HH SLOT RN_GOOD RN_BAD
--------------------------
1 1 9 3
1 2 4 8
1 3 7 3 <--!!!
--------------------------
2 1 2 1
2 2 4 6
2 3 9 4
This is on Netezza if it makes any difference. This one's being a real headscratcher for me. Thanks in advance!
To get a random number between 1 and the number of rows in the hh, you can use:
select hh, slot, row_number() over (partition by hh order by random()) as rn
from t;
The larger range of values is a bit more challenging. The following calculates a table (called randoms) with numbers and a random position in the same range. It then uses slot to index into the position and pull the random number from the randoms table:
with nums as (
select 1 as n union all select 2 union all select 3 union all select 4 union all select 5 union all
select 6 union all select 7 union all select 8 union all select 9
),
randoms as (
select n, row_number() over (order by random()) as pos
from nums
)
select t.hh, t.slot, hnum.n
from (select hh, randoms.n, randoms.pos
from (select distinct hh
from t
) t cross join
randoms
) hnum join
t
on t.hh = hnum.hh and
t.slot = hnum.pos;
Here is a SQLFiddle that demonstrates this in Postgres, which I assume is close enough to Netezza to have matching syntax.
I am not an expert on SQL, but probably do something like this:
Initialize a counter CNT=1
Create a table such that you sample 1 row randomly from each group and a count of null RN, say C_NULL_RN.
With probability C_NULL_RN/(10-CNT+1) for each row, assign CNT as RN
Increment CNT and go to step 2
Well, I couldn't get a slick solution, so I did a hack:
Created a new integer field called rand_inst.
Assign a random number to each empty slot.
Update rand_inst to be the instance number of that random number within this household. E.g., if I get two 3's, then the second 3 will have rand_inst set to 2.
Update the table to assign a different random number anywhere that rand_inst>1.
Repeat assignment and update until we converge on a solution.
Here's what it looks like. Too lazy to anonymise it, so the names are a little different from my original post:
/* Iterative hack to fill 6 slots with a random number between 1 and 13.
A random number *must not* repeat within a household_id.
*/
update c3_lalfinal a
set a.rand_inst = b.rnum
from (
select household_id
,slot_nbr
,row_number() over (partition by household_id,rnd order by null) as rnum
from c3_lalfinal
) b
where a.household_id = b.household_id
and a.slot_nbr = b.slot_nbr
;
update c3_lalfinal
set rnd = CAST(0.5 + random() * (13-1+1) as INT)
where rand_inst>1
;
/* Repeat until this query returns 0: */
select count(*) from (
select household_id from c3_lalfinal group by 1 having count(distinct(rnd)) <> 6
) x
;

How can a group by be converted to a self-join

for a table such as:
employeeID | groupCode
1 red111
2 red111
3 blu123
4 blu456
5 red553
6 blu423
7 blu341
how can I count the number of employeeIDs that are in parent groups (such as red or blu, but there are many more groups in the real table) that have a total number of group members greater than 2 (so all those with blu in this particular example) excluding themselves.
To expand: groupCode consists of a parent group (three letters), followed by some numbers for the subgroup.
using a self-join, or at least without using a group by statement.
So far I have:
SELECT T1.employeeID
FROM TABLE T1, TABLE T2
WHERE T1.groupCode <> T2.groupCode
AND SUBSTR(T1.groupCode, 1, 3) = SUBSTR(T2.gorupCode, 1, 3);
but that doesn't do much for me...
Add an index on the first 3 characters of EMPLOYEE.
Then try this one:
SELECT ed.e3
, COUNT(*)
FROM EMPLOYEE e
JOIN
( SELECT DISTINCT
SUBSTR(groupCode, 1, 3) AS e3
FROM EMPLOYEE
) ed
ON e.groupCode LIKE CONCAT(ed.e3, '%')
GROUP BY ed.e3
HAVING COUNT(*) >= 3 --- or whatever is wanted
What about
SELECT substring(empshirtno, 1, 3),
Count(SELECT 1 from myTable as myTable2
WHERE substring(mytable.empshirtno, 1, 3) = substring(mytable2.empshirtno, 1, 3))
FROM MyTable
GROUP BY substring(mytable2.empshirtno, 1, 3)
maybe counting from a subquery is speedier with an index