Imposing LIMIT on UNION - mysql

We are working with some legacy code that is using a sub-optimally normalized DB. The problem that we are trying to solve is best described with an illustrative example.
hi_priority and lo_priority are two tables that have the same schema, as defined below:
CREATE TABLE hi_priority
(
id INT UNSIGNED AUTO_INCREMENT,
name VARCHAR(16),
col1 VARCHAR(16),
col2 INT,
PRIMARY KEY(id)
);
CREATE TABLE lo_priority
(
id INT UNSIGNED AUTO_INCREMENT,
name VARCHAR(16),
col1 VARCHAR(16),
col2 INT,
PRIMARY KEY(id)
);
In each table there is a 1:1 mapping between the name and col2.
The idea is to potentially SELECT name, col1, col2 from both tables but limit the total number of distinct names (let's say 7 for our illustrative example), subject to the following constraints:
Preference is to be given to entries from hi_priority table over entries from lo_priority tables.
For example, if there are 15 distinct names in hi_priority table,
we will select all rows with 7 names from hi_priority table,
ordered by col2.
If there are 5 distinct names in hi_priority
table, we select all rows with those 5 names and on top of that we
select all rows with 2 distinct names from lo_priority table,
ordered by col2
If there are some entries in both the
hi_priority table and the lo_priority table, we select entries
only from the hi_priority table
Let us assume that the tables are populated as follows:
INSERT INTO hi_priority(name, col1, col2) VALUES('john', 'kl7y5tis4yh4', 1), ('john', 'tiid6iywq02k', 1),
('john', 'detawgsxz615', 1), ('amy', 'i6u4hhc1trjk', 2), ('amy', 'wdpt0t5vtmbt', 2),
('amy', '87z5wgdfztwl', 2), ('amy', 'oj98jjdpb5yv', 2), ('steve', 'hllaazonflf0', 3),
('steve', '0h22y66kq3ow', 3), ('steve', 'o04ksti7di60', 3), ('steve', '3qlylbzqfr51', 3),
('steve', 'wmisshywtb12', 3), ('bob', 'ku8wpf7d6ta9', 3), ('bob', '6t7mn60g0g18', 3),
('bob', 'qv0s7ho3jku6', 3), ('bob', '5qgz7dznzwvn', 3), ('bob', 'mm76nhimm6fu', 3),
('bob', 'k7nlpksc55t2', 3), ('oliver', 'gvf9kjewpj7h', 4), ('oliver', '24w5s30w5te6', 4);
INSERT INTO lo_priority(name, col1, col2) VALUES('doug', 'j205tzrsfmax', 11), ('doug', '0w7rcazbh6es', 11),
('doug', '6xswf8frsjaw', 11), ('john', 'iw7d14vtysz2', 2), ('john', '6lg667dygaz1', 2),
('john', '83uk5dcobpu5', 2), ('john', 'tl8cpzatv0n9', 2), ('mike', '2dsarwozpmci', 3),
('mike', 'hinn6w03wdib', 3), ('mike', '4sxbgyacmjob', 3), ('mike', 'm4q13ln9gctj', 3),
('mike', 'pnip9c8cejo9', 3), ('steve', 'faff9p9v96x4', 4), ('steve', 'd5mxqpd3k8zi', 4),
('martha', 'bxggn5t6d2xn', 8), ('martha', 't05mi47i4n6l', 8), ('martha', 'p30wmw2o6nty', 8),
('martha', 'wip6efajt9yv', 8);
The expected output is:
+--------+------------+------+
| name | col1 | col2 |
+--------+------------+------+
| john | kl7y5tis4y | 1 |
| john | tiid6iywq0 | 1 |
| john | detawgsxz6 | 1 |
| amy | oj98jjdpb5 | 2 |
| amy | 87z5wgdfzt | 2 |
| amy | wdpt0t5vtm | 2 |
| amy | i6u4hhc1tr | 2 |
| bob | k7nlpksc55 | 3 |
| bob | mm76nhimm6 | 3 |
| bob | 5qgz7dznzw | 3 |
| bob | qv0s7ho3jk | 3 |
| bob | 6t7mn60g0g | 3 |
| bob | ku8wpf7d6t | 3 |
| steve | wmisshywtb | 3 |
| steve | 3qlylbzqfr | 3 |
| steve | o04ksti7di | 3 |
| steve | 0h22y66kq3 | 3 |
| steve | hllaazonfl | 3 |
| mike | 4sxbgyacmj | 3 |
| mike | pnip9c8cej | 3 |
| mike | m4q13ln9gc | 3 |
| mike | hinn6w03wd | 3 |
| mike | 2dsarwozpm | 3 |
| oliver | gvf9kjewpj | 4 |
| oliver | 24w5s30w5t | 4 |
| martha | bxggn5t6d2 | 8 |
| martha | t05mi47i4n | 8 |
| martha | p30wmw2o6n | 8 |
| martha | wip6efajt9 | 8 |
+--------+------------+------+
Amy, Bob and Oliver are only present in hi_priority table and will be selected.
Mike and Martha are only present in lo_priority table and will be selected.
John and Steve are present in both hi_priority table and lo_priority table but will be selected only from hi_priority table

Assuming you are using a version of MariaDB later than 10.2 (so it supports CTEs), you can use this query. It first gets a list of all the distinct names from hi_priority and lo_priority (in the names CTE), then finds the top 7 names by ordering those by priority and col2 (the top7 CTE) and finally joins that list back to hi_priority and lo_priority, giving preference to values from hi_priority over those from lo_priority:
WITH names AS (
SELECT DISTINCT name, col2, 1 AS priority
FROM hi_priority hi
UNION ALL
SELECT DISTINCT name, col2, 2 AS priority
FROM lo_priority lo
WHERE NOT EXISTS (
SELECT *
FROM hi_priority
WHERE name = lo.name
)
),
top7 AS (
SELECT name, priority
FROM names
ORDER BY priority, col2
LIMIT 7
)
SELECT DISTINCT
t.name,
COALESCE(hi.col1, lo.col1) AS col1,
COALESCE(hi.col2, lo.col2) AS col2
FROM top7 t
LEFT JOIN hi_priority hi ON hi.name = t.name
LEFT JOIN lo_priority lo ON lo.name = t.name
ORDER BY col2, priority, name, COALESCE(hi.id, lo.id)
Output (for your sample data):
name col1 col2
john kl7y5tis4yh4 1
john tiid6iywq02k 1
john detawgsxz615 1
amy i6u4hhc1trjk 2
amy wdpt0t5vtmbt 2
amy 87z5wgdfztwl 2
amy oj98jjdpb5yv 2
bob ku8wpf7d6ta9 3
bob 6t7mn60g0g18 3
bob qv0s7ho3jku6 3
bob 5qgz7dznzwvn 3
bob mm76nhimm6fu 3
bob k7nlpksc55t2 3
steve hllaazonflf0 3
steve 0h22y66kq3ow 3
steve o04ksti7di60 3
steve 3qlylbzqfr51 3
steve wmisshywtb12 3
mike 2dsarwozpmci 3
mike hinn6w03wdib 3
mike 4sxbgyacmjob 3
mike m4q13ln9gctj 3
mike pnip9c8cejo9 3
oliver gvf9kjewpj7h 4
oliver 24w5s30w5te6 4
martha bxggn5t6d2xn 8
martha t05mi47i4n6l 8
martha p30wmw2o6nty 8
martha wip6efajt9yv 8
Demo on dbfiddle
If you're running an earlier version, you can write the same query using subqueries:
SELECT DISTINCT
t.name,
COALESCE(hi.col1, lo.col1) AS col1,
COALESCE(hi.col2, lo.col2) AS col2
FROM (
SELECT name, priority
FROM (
SELECT DISTINCT name, col2, 1 AS priority
FROM hi_priority hi
UNION ALL
SELECT DISTINCT name, col2, 2 AS priority
FROM lo_priority lo
WHERE NOT EXISTS (
SELECT *
FROM hi_priority
WHERE name = lo.name
)
) n
ORDER BY priority, col2
LIMIT 7
) t
LEFT JOIN hi_priority hi ON hi.name = t.name
LEFT JOIN lo_priority lo ON lo.name = t.name
ORDER BY col2, priority, name, COALESCE(hi.id, lo.id)
Output is the same. Demo on dbfiddle

Related

SELECT WHERE in varchar with wildcard

Lets say i have a table like this
CREATE TABLE Parts (ID int, part_number varchar(100), isActive TINYINT);
and these sample records
|ID | part_number | isActive|
===============================
1 | 1N3.805.327 | 1
2 | 1N3.805.327.B | 1
3 | 1N3.804.108.B | 1
4 | 1N3.804.108.C | 1
5 | 1N3.804.107.B | 1
6 | 1N3.804.107.C | 1
7 | 1N3.804.106.A | 1
8 | 1N3.804.105.A | 1
Problem
I would like to combine a where in clause with the wildcard % operator
In my dbfiddle sample i tried the string function find_in_set and the comparison operator in(). Both do not work:
-- without wildcard the query works
SELECT * FROM Parts WHERE part_number in ('1N3.804.108.B', '1N3.804.106.A'); -- 2
-- with wildcard no records are returned
SELECT * FROM Parts WHERE part_number in ('1N3.804.108%', '1N3.804.106%'); -- 0
SELECT * FROM Parts WHERE FIND_IN_SET(part_number, '1N3.804.108%,1N3.804.106%'); -- 0
Questions
I assume i could use WHERE LEFT(part_number, 11) in ('1N3.804.108', '1N3.804.106') But i do not know if this has any disadvantages.
Is there a way to use a wildcard operator with in()?
Sample records
INSERT INTO
Parts(ID, part_number, isActive)
VALUES
(1, '1N3.805.327',1),
(2, '1N3.805.327.B',1),
(3, '1N3.804.108.B',1),
(4, '1N3.804.108.C',1),
(5, '1N3.804.107.B',1),
(6, '1N3.804.107.C',1),
(7, '1N3.804.106.A',1),
(8, '1N3.804.105.A',1);
Use REGEXP for that, when you want to use OR
CREATE TABLE Parts (ID int, part_number varchar(100), isActive TINYINT);
INSERT INTO
Parts(ID, part_number, isActive)
VALUES
(1, '1N3.805.327',1),
(2, '1N3.805.327.B',1),
(3, '1N3.804.108.B',1),
(4, '1N3.804.108.C',1),
(5, '1N3.804.107.B',1),
(6, '1N3.804.107.C',1),
(7, '1N3.804.106.A',1),
(8, '1N3.804.105.A',1);
✓
✓
SELECT * FROm Parts WHeRE part_number REGEXP '^(1N3.804.108|1N3.804.106)'
ID | part_number | isActive
-: | :------------ | -------:
3 | 1N3.804.108.B | 1
4 | 1N3.804.108.C | 1
7 | 1N3.804.106.A | 1
MySQL Can only UNION a certain number of tables. i think it is about 53.
With an index on partnumber, this will be the fastest.
SELECT * FROm Parts WHeRE part_number REGEXP '^1N3.804.108'
UNION all
SELECT * FROm Parts WHeRE part_number REGEXP '^1N3.804.106'
ID | part_number | isActive
-: | :------------ | -------:
3 | 1N3.804.108.B | 1
4 | 1N3.804.108.C | 1
7 | 1N3.804.106.A | 1
SELECT * FROm Parts WHeRE part_number LIKE '1N3.804.108%'
UNION all
SELECT * FROm Parts WHeRE part_number LIKE '1N3.804.106%'
ID | part_number | isActive
-: | :------------ | -------:
3 | 1N3.804.108.B | 1
4 | 1N3.804.108.C | 1
7 | 1N3.804.106.A | 1
db<>fiddle here
you can also try to do
select *,
REGEXP_LIKE(part_number,[pattern]) as pattern_test from Parts
where pattern_test is TRUE
here first you will create the pattern that you are interested in and apply it on the column you wish to apply it to. The pattern_test will return true if it matches with your pattern and then you can filter on that [where clause]

Get last mysql record only from a column

This is my existing table
id name version
| 1 | a | 1.1 |
| 2 | b | 2.1 |
| 3 | c | 3.1 |
| 4 | d | 1.2 |
| 5 | e | 4.1 |
how can I write a query to generate results where i will return all records but only the last record in the column version is selected like below?
id name version
| 4 | d | 1.2 |
| 2 | b | 2.1 |
| 3 | c | 3.1 |
| 5 | e | 4.1 |
If you prefer a slightly less laborious solution...
SELECT x.*
FROM t x
JOIN
( SELECT MAX(grade) grade
FROM t
GROUP
BY FLOOR(grade)
) y
ON y.grade = x.grade
http://sqlfiddle.com/#!9/f17db1/16
This is a bit laborious but it can be done
SELECT
SUBSTRING_INDEX(GROUP_CONCAT(id ORDER BY REPLACE(grade,'.','')*1 DESC),',',1) as id,
SUBSTRING_INDEX(GROUP_CONCAT(letter ORDER BY REPLACE(grade,'.','')*1 DESC),',',1) as letter,
MAX(grade) as grade
FROM
t
GROUP BY SUBSTRING_INDEX(grade,'.',1)
ORDER BY REPLACE(grade,'.','')*1
Assuming the last column is float you can use ORDER BY lastcol directly
FIDDLE
CREATE TABLE t
(`id` int, `letter` varchar(7), `grade` varchar(55))
;
INSERT INTO t
VALUES
(1, 'a', '1.1'),
(2, 'b', '2.1'),
(3, 'c', '3.1'),
(4, 'd', '1.2'),
(5, 'e', '4.1')

SQL ordering (MySQL) [duplicate]

This question already has answers here:
MySQL order by two values
(6 answers)
Closed 4 years ago.
This seemingly simple order requirement does not seem to have simple solution.
I would like a table be ordered by the item with the highest max value followed by the rest of the same item in descending order. Then the item with the next highest max value followed by the rest of the same item in descending order.. etc
For example I need to order a table similar like this:
item value
AAA 2
AAA 4
AAA 2
CCC 8
BBB 1
BBB 2
BBB 6
CCC 4
To be ordered like this..
item value
CCC 8
CCC 4
BBB 6
BBB 2
BBB 1
AAA 4
AAA 2
AAA 2
Does anyone know how to do this?
This is what you seem to be looking for:
select item, value
from tablename
order by item desc, value desc
Here is a working example: http://sqlfiddle.com/#!9/e56d7/1
select item, value from yourtable order by item desc, value desc;
MySQL order by two values
Here is a way: SQL Fiddle
MySQL 5.6 Schema Setup:
CREATE TABLE Table1
(`item` varchar(3), `value` int)
;
INSERT INTO Table1
(`item`, `value`)
VALUES
('AAA', 2),
('AAA', 4),
('AAA', 2),
('CCC', 8),
('BBB', 1),
('BBB', 2),
('BBB', 6),
('CCC', 4)
;
Query 1:
select
t1.*
from (
select item, max(value) mval from table1 group by item
) m1
inner join table1 t1 on m1.item = t1.item
order by
m1.mval DESC
, t1.item
, t1.value DESC
Results:
| item | value |
|------|-------|
| CCC | 8 |
| CCC | 4 |
| BBB | 6 |
| BBB | 2 |
| BBB | 1 |
| AAA | 4 |
| AAA | 2 |
| AAA | 2 |
In response to worthwhile comment below:
Here's another option that this approach permits (nb: case 'DDD' has been added)
SQL Fiddle
MySQL 5.6 Schema Setup:
CREATE TABLE Table1
(`item` varchar(3), `value` int)
;
INSERT INTO Table1
(`item`, `value`)
VALUES
('DDD', 8),
('DDD', 8),
('AAA', 2),
('AAA', 4),
('AAA', 2),
('CCC', 8),
('BBB', 1),
('BBB', 2),
('BBB', 6),
('CCC', 4)
;
Query 1:
select
t1.*
from (
select item, max(value) mval , sum(value) sval
from table1 group by item
) m1
inner join table1 t1 on m1.item = t1.item
order by
m1.mval DESC
, m1.sval DESC
, t1.item
, t1.value DESC
Results:
| item | value |
|------|-------|
| DDD | 8 |
| DDD | 8 |
| CCC | 8 |
| CCC | 4 |
| BBB | 6 |
| BBB | 2 |
| BBB | 1 |
| AAA | 4 |
| AAA | 2 |
| AAA | 2 |

select random rows in mysql

I have one table named Mydata as follows
id name type
--------------------------------------------
1 vinu 1
2 rinu 2
3 dilu 1
4 raju 2
5 manu 3
6 saju 3
7 ragu 3
8 sonu 1
9 sam 1
10 rag 1
--------------------------------------------
I want to print records with alternating type, for example:
First row with type =1
Second row with type =2
Third row with type =3
4th row type=1
5th row type=2 and so on
Required result as follows
id name type
-----------------------------------------
1 vinu 1
2 rinu 2
5 manu 3
3 dilu 1
4 raju 2
6 saju 3
8 sonu 1
7 ragu 3
9 sam 1
10 rag 1
----------------------------------------------
Sample data:
CREATE TABLE t
(`id` int, `name` varchar(4), `type` int)
;
INSERT INTO t
(`id`, `name`, `type`)
VALUES
(1, 'vinu', 1),
(2, 'rinu', 2),
(3, 'dilu', 1),
(4, 'raju', 2),
(5, 'manu', 3),
(6, 'saju', 3),
(7, 'ragu', 3),
(8, 'sonu', 1),
(9, 'sam', 1),
(10, 'rag', 1)
;
Query:
SELECT id, name, type FROM (
SELECT
t.*,
#rn := IF(#prev_type = type, #rn + 1, 1) AS rownumber,
#prev_type := type
FROM
t
, (SELECT #rn := 0, #prev_type := NULL) var_init_subquery
ORDER BY type
) sq
ORDER BY rownumber, type
Result:
| id | name | type |
|----|------|------|
| 1 | vinu | 1 |
| 4 | raju | 2 |
| 5 | manu | 3 |
| 9 | sam | 1 |
| 2 | rinu | 2 |
| 7 | ragu | 3 |
| 8 | sonu | 1 |
| 6 | saju | 3 |
| 10 | rag | 1 |
| 3 | dilu | 1 |
see it working live in an sqlfiddle
Caveat:
Don't expect this to be performant when you have lots of data. It's doing a full table scan.
Here's a manual entry to read when you're interested about how this variables work.
This cannot be done via a raw SQL query. Extract the rows you need to display, and then sort them via your application.
Alternatively... you could write a stored procedure, but I don't recommend this. You will need a temporary table and a cursor (that transparently creates another temporary table). Too much for a query that should be executed often.

MySQL only get overall ROLLUP

Performing a WITH ROLLUP when grouping by multiple fields, MySQL returns a rollup row for each group, as well as the overall summary:
CREATE TABLE test (name VARCHAR(50), number TINYINT);
INSERT INTO test VALUES
('foo', 1), ('foo', 1), ('foo', 2), ('foo', 3), ('foo', 3),
('bar', 1), ('bar', 2), ('bar', 2), ('bar', 2), ('bar', 3),
('baz', 1), ('baz', 2), ('bar', 2);
SELECT name, number, COUNT(1) FROM test GROUP BY name, number WITH ROLLUP;
+------+--------+----------+
| name | number | count(1) |
+------+--------+----------+
| bar | 1 | 1 |
| bar | 2 | 3 |
| bar | 3 | 1 |
| bar | NULL | 5 |
| baz | 1 | 1 |
| baz | 2 | 2 |
| baz | NULL | 3 |
| foo | 1 | 2 |
| foo | 2 | 1 |
| foo | 3 | 2 |
| foo | NULL | 5 |
| NULL | NULL | 13 |
+------+--------+----------+
I'm not interested in the rollups for foo/bar/baz, only the overall summary. What's the most efficient way to achieve this?
I know I can't use HAVING due to the rollup rows being added afterwards. Is the best solution to use a nested query for this, selecting where name and number are either both NOT NULL or both NULL?
HAVING can do the trick with no subquery:
SELECT `name`, number, COUNT(1) FROM test GROUP BY `name`, number WITH ROLLUP
HAVING number IS NOT NULL OR `name` IS NULL;
This filters out the post-rollup rows except for the grand total:
name number COUNT(1)
------ ------ --------
bar 1 1
bar 2 4
bar 3 1
baz 1 1
baz 2 1
foo 1 2
foo 2 1
foo 3 2
(NULL) (NULL) 13
Try to use a subquery, e.g. -
SELECT * FROM (
SELECT name, number, COUNT(1) FROM test GROUP BY name, number WITH ROLLUP) t
WHERE name IS NULL OR number IS NULL
You also may want to change NULL values with appropriate texts.
SELECT COALESCE(name, 'TOTAL') as Name, number, COUNT(1) FROM test GROUP BY name, number WITH ROLLUP;
Below Name column it would Display as Total. If you have issue with number as null same can be done for that too.