Custom ordering before regular ordering? - sql

I have 3 tables:
CREATE TABLE items (
id integer PRIMARY KEY,
title varchar (256) NOT NULL
);
INSERT INTO items (id, title) VALUES (1, 'qux');
INSERT INTO items (id, title) VALUES (2, 'quux');
INSERT INTO items (id, title) VALUES (3, 'quuz');
INSERT INTO items (id, title) VALUES (4, 'corge');
INSERT INTO items (id, title) VALUES (5, 'grault');
CREATE TABLE last_used (
item_id integer NOT NULL REFERENCES items (id),
date integer NOT NULL
);
INSERT INTO last_used (item_id, date) VALUES (2, 1000);
INSERT INTO last_used (item_id, date) VALUES (3, 2000);
INSERT INTO last_used (item_id, date) VALUES (2, 3000);
CREATE TABLE rating (
item_id integer NOT NULL REFERENCES items (id),
rating integer NOT NULL
);
INSERT INTO rating (item_id, rating) VALUES (1, 400);
INSERT INTO rating (item_id, rating) VALUES (2, 100);
INSERT INTO rating (item_id, rating) VALUES (3, 200);
INSERT INTO rating (item_id, rating) VALUES (4, 300);
INSERT INTO rating (item_id, rating) VALUES (5, 500);
I want to select rows in the following order:
Last used items matching the search string;
Most rated items matching the search string;
All other items matching the search string.
For the search i.title ~* '(?=.*u)', I get:
id | title | max(last_used.date) | rating.rating
3 | quuz | 2000 | 200
2 | quux | 3000 | 100
5 | grault | null | 500
1 | qux | null | 400
…with the following code:
WITH used AS (
SELECT lu.item_id
FROM last_used lu
JOIN (
SELECT item_id, max(date) AS date
FROM last_used
GROUP BY 1
) sub USING (date)
-- WHERE lu.user_id = 1
ORDER BY lu.date DESC
)
SELECT i.id, i.title, r.rating
FROM items i
LEFT JOIN rating r
ON r.item_id = i.id
WHERE
i.title ~* '(?=.*u)'
ORDER BY
i.id NOT IN (SELECT item_id FROM used),
r.rating DESC NULLS LAST
LIMIT 5 OFFSET 0
Is it possible to get the following results (latest used items first)?
id | title | max(last_used.date) | rating.rating
2 | quux | 3000 | 100
3 | quuz | 2000 | 200
5 | grault | null | 500
1 | qux | null | 400

You can use the following query to get the desired order
through the ORDER BY clause with l.date DESC NULLS LAST, r.rating DESC NULLS LAST:
SELECT i.id, i.title, l.date, r.rating
FROM items i
LEFT JOIN rating r
ON r.item_id = i.id
LEFT JOIN ( SELECT item_id, max(date) AS date FROM last_used GROUP BY 1 ) l
ON l.item_id = i.id
WHERE
i.title ~* '(?=.*u)'
ORDER BY l.date DESC NULLS LAST, r.rating DESC NULLS LAST
LIMIT 5 OFFSET 0;
Demo

Related

Grouping values in order to sort by minimum of a value, then other fields then by that value itself

I would like to sort my data by an aggregate value, then by other fields, then by the unaggregated value.
I have the following schema:
CREATE TABLE priority_t (
id NUMERIC(10),
priority NUMERIC(10),
CONSTRAINT priority_t_pk PRIMARY KEY (id)
);
CREATE TABLE value_t (
id NUMERIC(10),
value NUMERIC(10),
CONSTRAINT value_t_pk PRIMARY KEY (id)
);
CREATE TABLE file_t (
id NUMERIC(10),
CONSTRAINT file_t_pk PRIMARY KEY (id)
);
CREATE TABLE main_t (
id NUMERIC(10),
priority_id NUMERIC(10),
value_id NUMERIC(10),
file_id NUMERIC(10),
CONSTRAINT main_t_pk PRIMARY KEY (id),
CONSTRAINT priority_t_fk FOREIGN KEY (priority_id) REFERENCES priority_t(id),
CONSTRAINT value_t_fk FOREIGN KEY (value_id) REFERENCES value_t(id),
CONSTRAINT file_t_fk FOREIGN KEY (file_id) REFERENCES file_t(id)
);
Then I insert the following data:
INSERT INTO priority_t (id, priority) VALUES (1, 10);
INSERT INTO priority_t (id, priority) VALUES (2, 20);
INSERT INTO value_t (id, value) VALUES (1, 987);
INSERT INTO value_t (id, value) VALUES (2, 876);
INSERT INTO value_t (id, value) VALUES (3, 765);
INSERT INTO value_t (id, value) VALUES (4, 654);
INSERT INTO file_t (id) VALUES (111);
INSERT INTO file_t (id) VALUES (222);
INSERT INTO file_t (id) VALUES (333);
INSERT INTO file_t (id) VALUES (444);
INSERT INTO main_t (id, priority_id, value_id, file_id) VALUES (1, 1, 1, 111);
INSERT INTO main_t (id, priority_id, value_id, file_id) VALUES (2, 2, 1, 111);
INSERT INTO main_t (id, priority_id, value_id, file_id) VALUES (3, 2, 2, 222);
INSERT INTO main_t (id, priority_id, value_id, file_id) VALUES (4, 1, 2, 333);
INSERT INTO main_t (id, priority_id, value_id, file_id) VALUES (5, 2, 3, 111);
INSERT INTO main_t (id, priority_id, value_id, file_id) VALUES (6, 1, 4, 444);
INSERT INTO main_t (id, priority_id, value_id, file_id) VALUES (7, 2, 4, 444);
COMMIT;
And I want to get the following result:
min_priority | priority | value | value_id | file_id
(hidden) | | | (hidden) |
--------------+----------+-------+----------+---------
10 | 10 | 654 | 4 | 444
10 | 20 | 654 | 4 | 444
10 | 10 | 876 | 2 | 333
10 | 10 | 987 | 1 | 111
10 | 20 | 987 | 1 | 111
20 | 20 | 765 | 3 | 111
20 | 20 | 876 | 2 | 222
I know how to sort them:
ORDER BY min_value ASC, value ASC, value_id ASC, priority ASC
But my problem is that I don't know how to group the values themselves: I keep getting duplicates in my rows, and/or incorrect values.
My closest attempt is the following:
WITH listing AS (
SELECT m.id AS main_id,
p.id AS priority_id,
p.priority AS priority,
v.id AS value_id,
v.value AS value,
f.id AS file_id
FROM main_t m
INNER JOIN priority_t p ON m.priority_id = p.id
INNER JOIN value_t v ON m.value_id = v.id
INNER JOIN file_t f ON m.file_id = f.id
)
SELECT min_p.min_priority AS min_priority,
listing.priority AS priority,
listing.value AS value,
listing.file_id AS file_id
FROM listing,
(
SELECT min(min_p_value.min_priority) AS min_priority,
min_p_value.value_id AS min_value_id,
listing.file_id AS file_id
FROM listing,
(
SELECT min(listing.priority) AS min_priority,
listing.value AS value,
listing.value_id AS value_id
FROM listing
GROUP BY listing.value, listing.value_id
) min_p_value
WHERE listing.value = min_p_value.value
AND listing.value_id = min_p_value.value_id
AND min_p_value.min_priority = min_priority
GROUP BY min_p_value.value_id, listing.file_id
) min_p
WHERE min_p.min_value_id = listing.value_id
AND min_p.file_id = listing.file_id
ORDER BY min_p.min_priority ASC,
listing.value ASC,
listing.value_id ASC,
listing.priority;
And this returns the following incorrect result:
MIN_PRIORITY PRIORITY VALUE FILE_ID
------------- ---------- ---------- ----------
10 10 654 444
10 20 654 444
10 10 876 333
10 20 876 222 <-- incorrect, should have a min_priority of 20, and therefore be the last
10 10 987 111
10 20 987 111
20 20 765 111
How can I achieve what I expect?
this should work:
select (select min(priority)
from main_t mm
join priority_t tt
on tt.id = mm.priority_id
where mm.value_id = m.value_id) as min_priority,
p.priority as priority,
v.value as value,
m.value_id,
m.file_id
from main_t m
join priority_t p
on p.id = m.priority_id
join value_t v
on v.id = m.value_id
order by 1, 3, 4, 2;
It determines the minimum priority by value_id.
You can order the result by column number as shown.
I have written this code in MySQL (not Oracle) and was not sure about the CTE syntax, so I replaced the WITH clause with a temp table called min_priority, but you can of course replace the temp table with a CTE. When I ran this on MySQl, I got the same result as you wanted.
Also was not sure whether you need a left join or inner join, both would work in this example.
-- find min priority for each value
create temporary table if not exists min_priority as (
select m.value_id, min(p.priority) as min_pri
from main_t m
inner join priority_t p on m.priority_id = p.id
group by m.value_id
);
-- just join all the tables, including min_priority, and order the result
select mp.min_pri, p.priority, v.value, v.id, f.id
from main_t m
left join priority_t p on m.priority_id = p.id
left join value_t v on m.value_id = v.id
left join file_t f on m.file_id = f.id
left join min_priority mp on mp.value_id = v.id
order by mp.min_pri asc, v.value asc, v.id asc, p.priority asc;
Just as a side note, I wouldn't use multiple levels of inner queries, as your example in the question, as they would affect the performance.
When you need aggregated and non-aggregated values use analytic functions. I think, that would be something like this:
select *
from (select min(p.priority) over (partition by v.id, v.value, f.id) min_priority,
p.priority, v.value, v.id value_id, f.id file_id
from main_t m
join priority_t p on m.priority_id = p.id
join value_t v on m.value_id = v.id
join file_t f on m.file_id = f.id)
order by min_priority, value, value_id, priority
Result:
MIN_PRIORITY PRIORITY VALUE VALUE_ID FILE_ID
------------ ----------- ----------- ----------- -----------
10 10 654 4 444
10 20 654 4 444
10 10 876 2 333
10 10 987 1 111
10 20 987 1 111
20 20 765 3 111
20 20 876 2 222

SQL Grouping by sequential occurrences of a value

I have the below table with 2 columns
ID | Dept
1 | A
2 | A
3 | B
4 | B
5 | B
6 | A
I want to do a count such that the output should look as the table below.
Dept | Count
A | 2
B | 3
A | 1
Thanks for your help in advance!
Slightly different to Michael's, same result:
with cte1 as (
select id,
dept,
row_number() over (partition by dept order by id) -
row_number() over (order by id) group_num
from test),
cte2 as (
select dept,
group_num,
count(*) c_star,
max(id) max_id
from cte1
group by dept,
group_num)
select dept,
c_star
from cte2
order by max_id;
http://sqlfiddle.com/#!4/ff747/1
From your example, it looks like you're wanting to count sequential records for each department.
You can do this by combining the row number and the ordering Id.
create table tblDept (
id int not null,
dept varchar(50)
);
insert into tblDept values (1, 'A');
insert into tblDept values (2, 'A');
insert into tblDept values (3, 'B');
insert into tblDept values (4, 'B');
insert into tblDept values (5, 'B');
insert into tblDept values (6, 'A');
with orderedDepts as (
select
dept,
id,
row_number() over (partition by dept order by id) -
row_number() over (order by id) as rn
from tblDept
)
select
dept,
count(*) as num
from orderedDepts
group by
dept,
rn
order by
max(id)
Gives the output:
+------+-----+
| DEPT | NUM |
+------+-----+
| A | 2 |
| B | 3 |
| A | 1 |
+------+-----+
SQL Fiddle
You cannot do this with SQL. Count counts distinct items, so count in your case would give you a count of A and a count of B.
You can only count/group by values in the table, not by the order of rows. Order is not guaranteed in SQL if you don't use an order by anyway.
run this query for this:
SELECT Dept, count(*) FROM table_name group By Dept

How to GROUP entries BY uninterrupted sequence?

CREATE TABLE entries (
id serial NOT NULL,
title character varying,
load_sequence integer
);
and data
INSERT INTO entries(title, load_sequence) VALUES ('A', 1);
INSERT INTO entries(title, load_sequence) VALUES ('A', 2);
INSERT INTO entries(title, load_sequence) VALUES ('A', 3);
INSERT INTO entries(title, load_sequence) VALUES ('A', 6);
INSERT INTO entries(title, load_sequence) VALUES ('B', 4);
INSERT INTO entries(title, load_sequence) VALUES ('B', 5);
INSERT INTO entries(title, load_sequence) VALUES ('B', 7);
INSERT INTO entries(title, load_sequence) VALUES ('B', 8);
Is there a way in PostgreSQL to write SQL that groups data by same title segments after ordering them by load_sequence.
I mean:
=# SELECT id, title, load_sequence FROM entries ORDER BY load_sequence;
id | title | load_sequence
----+-------+---------------
9 | A | 1
10 | A | 2
11 | A | 3
13 | B | 4
14 | B | 5
12 | A | 6
15 | B | 7
16 | B | 8
AND I want groups:
=# SELECT title, string_agg(id::text, ',' ORDER BY id) FROM entries ???????????;
so result would be:
title | string_agg
-------+-------------
A | 9,10,11
B | 13,14
A | 12
B | 15,16
You can use the following query:
SELECT title, string_agg(id::text, ',' ORDER BY id)
FROM (
SELECT id, title,
ROW_NUMBER() OVER (ORDER BY load_sequence) -
ROW_NUMBER() OVER (PARTITION BY title
ORDER BY load_sequence) AS grp
FROM entries ) AS t
GROUP BY title, grp
Calculated grp field serves to identify slices of title records having consecutive load_sequence values. Using this field in the GROUP BY clause we can achieve the required aggregation over id values.
Demo here
There's a trick you can use with sum as a window function running over a lagged window for this.
The idea is that when you hit an edge/discontinuity you return 1, otherwise you return 0. You detect the discontinuities using the lag window function.
SELECT title, string_agg(id::text, ', ') FROM (
SELECT
id, title, load_sequence,
sum(title_changed) OVER (ORDER BY load_sequence) AS partition_no
FROM (
SELECT
id, title, load_sequence,
CASE WHEN title = lag(title, 1) OVER (ORDER BY load_sequence) THEN 0 ELSE 1 END AS title_changed FROM entries
) x
) y
GROUP BY partition_no, title;

How to update X% of rows to A, Y% of rows to B, Z% of rows to C

I have a table like this:
Products
(
ID int not null primary key,
Type int not null,
Route varchar(20) null
)
I have a list on the client in this format:
Type=1, Percent=0.4, Route=A
Type=1, Percent=0.4, Route=B
Type=1, Percent=0.2, Route=C
Type=2, Percent=0.5, Route=A
Type=2, Percent=0.5, Route=B
Type=3, Percent=1.0, Route=C
...etc
When done, I'd like to assign 40% of type 1 products to Route A, 40% to Route B and 20% to Route C. Then 50% of type 2 products to Route A and 50% of type 2 products to Route B, etc.
Is there some way to do this in a single update statement?
If not in one giant statement, can it be done in one statement per type or one statement per route? As currently we're doing one per type+route any of the above would be an improvement.
Here's an Oracle statement that I prepared before you posted that you were using SQL-Server, but it might give you some ideas, though you will have to roll your own ratio_to_report analytic function using CTE and self-joins. We calculate the cumulative proportion of each type in the products and client route tables and do a non equi-join on the matching proportion bands. The sample data I have used has some round-offs but these will reduce for larger data sets.
Here's the setup:
create table products (id int not null primary key, "type" int not null, route varchar (20) null);
create table clienttable ( "type" int not null, percent number (10, 2) not null, route varchar (20) not null);
insert into clienttable ("type", percent, route) values (1, 0.4, 'A');
insert into clienttable ("type", percent, route) values (1, 0.4, 'B');
insert into clienttable ("type", percent, route) values (1, 0.2, 'C');
insert into clienttable ("type", percent, route) values (2, 0.5, 'A');
insert into clienttable ("type", percent, route) values (2, 0.5, 'B');
insert into clienttable ("type", percent, route) values (3, 1.0, 'C');
insert into products (id, "type", route) values (1, 1, null);
insert into products (id, "type", route) values (2, 1, null);
insert into products (id, "type", route) values (3, 1, null);
insert into products (id, "type", route) values (4, 1, null);
insert into products (id, "type", route) values (5, 1, null);
insert into products (id, "type", route) values (6, 1, null);
insert into products (id, "type", route) values (7, 1, null);
-- 7 rows for product type 1 so we will expect 3 of route A, 3 of route B, 1 of route C (rounded)
insert into products (id, "type", route) values (8, 2, null);
insert into products (id, "type", route) values (9, 2, null);
insert into products (id, "type", route) values (10, 2, null);
insert into products (id, "type", route) values (11, 2, null);
insert into products (id, "type", route) values (12, 2, null);
-- 5 rows for product type 2 so we will expect 3 of route A and 2 of route B (rounded)
insert into products (id, "type", route) values (13, 3, null);
insert into products (id, "type", route) values (14, 3, null);
-- 2 rows for product type 3 so we will expect 2 of route C
and here's the statement
select prods.id, prods."type", client.route cr from
(
select
p.id,
p."type",
row_number () over (partition by p."type" order by p.id) / count (*) over (partition by p."type") cum_ratio
from
products p
) prods
inner join
(
select "type", route, nvl (lag (cum_ratio, 1) over (partition by "type" order by route), 0) ratio_start, cum_ratio ratio_end from
(select "type", route, sum (rr) over (partition by "type" order by route) cum_ratio
from (select c."type", c.route, ratio_to_report (c.percent) over (partition by "type") rr from clienttable c))) client
on prods."type" = client."type"
and prods.cum_ratio >= client.ratio_start and prods.cum_ratio < client.ratio_end
This gives the following result:-
+----+------+----+
| ID | type | CR |
+----+------+----+
| 1 | 1 | A |
| 2 | 1 | A |
| 3 | 1 | B |
| 4 | 1 | B |
| 5 | 1 | B |
| 6 | 1 | C |
| 8 | 2 | A |
| 9 | 2 | A |
| 10 | 2 | B |
| 11 | 2 | B |
| 13 | 3 | C |
+----+------+----+
How about something like
--For updating type 1, set every route for type 1 as null.
UPDATE MyTable
SET [Route] = null
WHERE [Type] = '1'
--Update Route A(40%)
DECLARE #myVal int;
SET #myVal =CAST(0.4*(SELECT COUNT(*) FROM myTable WHERE [Type]='1') AS INT);
WITH tab AS
(
SELECT TOP (#myVal) *
FROM myTable
)
UPDATE tab
SET [Route] = 'A'
WHERE [Route] is null
--Update Route B (40%)
DECLARE #myVal int;
SET #myVal =CAST(0.4*(SELECT COUNT(*) FROM myTable WHERE [Type]='1') AS INT);
WITH tab AS
(
SELECT TOP (#myVal) *
FROM myTable
)
UPDATE tab
SET [Route] = 'B'
WHERE [Route] is null
--Update Route C (20%)
DECLARE #myVal int;
SET #myVal =CAST(0.2*(SELECT COUNT(*) FROM myTable WHERE [Type]='1') AS INT);
WITH tab AS
(
SELECT TOP (#myVal) *
FROM myTable
)
UPDATE tab
SET [Route] = 'C'
WHERE [Route] is null
I do not know if similar functionality exist in SQL Server. In Oracle there is SAMPLE clause.
Below query selects 10% of rows from a table:
SELECT empno
FROM scott.emp
SAMPLE (10)
/
Then your update would be easy... Maybe smth similar exists in SQL Server. You can also count rows or data then calc percent then update...
WITH po AS
( SELECT
ID,
Type,
ROW_NUMBER() OVER ( PARTITION BY Type
ORDER BY ID
) AS Rn,
COUNT(*) OVER (PARTITION BY Type) AS CntType
FROM
Products
)
, ro AS
( SELECT
Type,
Route,
( SELECT SUM(rr.Percent)
FROM Route AS rr
WHERE rr.Type = r.Type
AND rr.Route <= r.Route
) AS SumPercent
FROM
Routes AS r
)
UPDATE p
SET p.Route =
( SELECT MIN(ro.Route)
FROM ro
WHERE ro.Type = po.Type
AND ro.SumPercent >= po.Rn / po.CntType
)
FROM Products AS p
JOIN
po ON po.ID = p.ID ;

double sorted selection from a single table

I have a table with an id as the primary key, and a description as another field.
I want to first select the records that have the id<=4, sorted by description, then I want all the other records (id>4), sorted by description. Can't get there!
select id, descr
from t
order by
case when id <= 4 then 0 else 1 end,
descr
select *, id<=4 as low from table order by low, description
You may want to use an id <= 4 expression in your ORDER BY clause:
SELECT * FROM your_table ORDER BY id <= 4 DESC, description;
Test case (using MySQL):
CREATE TABLE your_table (id int, description varchar(50));
INSERT INTO your_table VALUES (1, 'c');
INSERT INTO your_table VALUES (2, 'a');
INSERT INTO your_table VALUES (3, 'z');
INSERT INTO your_table VALUES (4, 'b');
INSERT INTO your_table VALUES (5, 'g');
INSERT INTO your_table VALUES (6, 'o');
INSERT INTO your_table VALUES (7, 'c');
INSERT INTO your_table VALUES (8, 'p');
Result:
+------+-------------+
| id | description |
+------+-------------+
| 2 | a |
| 4 | b |
| 1 | c |
| 3 | z |
| 7 | c |
| 5 | g |
| 6 | o |
| 8 | p |
+------+-------------+
8 rows in set (0.00 sec)
Related post:
Using MySql, can I sort a column but have 0 come last?
select id, description
from MyTable
order by case when id <= 4 then 0 else 1 end, description
You can use UNION
SELECT * FROM (SELECT * FROM table1 WHERE id <=4 ORDER by description)aaa
UNION
SELECT * FROM (SELECT * FROM table1 WHERE id >4 ORDER by description)bbb
OR
SELECT * FROM table1
ORDER BY
CASE WHEN id <=4 THEN 0
ELSE 1
END, description