I'm using car makes as an example, which fits my situation nicely.
Example query I have now, that gives a simple count per state:
SELECT
state as State,
count(distinct(idnumber)) as Total
FROM
database.table
WHERE
make IN('honda', 'toyota', 'subaru')
GROUP BY
state
ORDER BY
state
Note that this would give me the count for each of the car makes, excluding things like Ford, Chevy, etc. The list of makes would be every make.
Is there a way I can break that down to give me the count of each make by state without resorting to a sub-query? In my head it would be like having a where statement in the count(distinct(idnumber)) select, but I'm not sure that's possible.
Here's what is in my head:
SELECT
state as State,
count(distinct(idnumber)) as Total_State,
(count(distinct(idnumber)) WHERE make = 'honda') as Total_Honda
WHERE
make IN('honda', 'toyota', 'subaru')
GROUP BY
state
ORDER BY
state
You could add multiple columns to your group by:
GROUP BY
state, make
I may misunderstand your question, but you can group along two columns, so you will get the number of fords made in CA and hondas made in CA etc
To be explicit, your query would be this:
SELECT
state as State,
count(distinct(idnumber)) as Total,
make as Make
FROM
database.table
WHERE
make IN('honda', 'toyota', 'subaru')
GROUP BY
state, make
ORDER BY
state
Just as a fun test, I did this:
create table `cars` (
`id` int(11),
`make` varchar(255),
`state` varchar(255)
);
insert into cars(id, make, state) values
(1, 'honda', 'ca'), (2, 'honda', 'ca'), (3, 'toyota', 'ca'),
(4, 'toyota', 'az'), (5, 'toyota', 'az'), (6, 'honda', 'az');
SELECT state as State, count(id) as Total, make as Make
FROM cars
WHERE make IN('honda', 'toyota', 'subaru')
GROUP BY state, make
ORDER BY state
And got back:
+-------+-------+--------+
| State | Total | Make |
+-------+-------+--------+
| az | 1 | honda |
| az | 2 | toyota |
| ca | 2 | honda |
| ca | 1 | toyota |
+-------+-------+--------+
Which is what I expected. Is that what you were thinking?
Related
Okay, this one might be a little tricky, so let me start with a visual.
Here's what the data looks like:
Original Data From Source
I'm trying to simplify it so that it looks like this:
End Result that I'm working towards
The problem is that I have an employee who changed back to a previous manager, so when I try to partition and group the data, those two instances get combined together and I end up with data that looks like this:
Actual Results
In the image above we can see that Manager Tom has a Start and End Date that is within the Start and End date of Manager Bob which is an error. Any suggestions on how to isolate the grouping of a item that gets reintroduced at a later time? This would be determined by Start Date and Rank over Partition I believe, but I can't seem to get this to work.
Here's the query to build the sample data:
CREATE VOLATILE TABLE VT_AGENT
(
EmpID INT
,Manager VARCHAR(16)
,Director VARCHAR(16)
,Record_Start DATE
,Record_End DATE
) ON COMMIT PRESERVE ROWS;
INSERT INTO vt_agent VALUES(12345678, 'Jill M.', 'Mike B.', '2019-08-21', '2019-09-07');
INSERT INTO vt_agent VALUES(12345678, 'Jill M.', 'Mike B.', '2019-09-07', '2019-09-16');
INSERT INTO vt_agent VALUES(12345678, 'Bob S.', 'Mike B.', '2019-09-16', '2019-10-15');
INSERT INTO vt_agent VALUES(12345678, 'Bob S.', 'Mike B.', '2019-10-15', '2019-11-23');
INSERT INTO vt_agent VALUES(12345678, 'Tom A.', 'Mike B.', '2019-11-23', '2019-12-07');
INSERT INTO vt_agent VALUES(12345678, 'Tom A.', 'Mike B.', '2019-12-07', '2019-12-12');
INSERT INTO vt_agent VALUES(12345678, 'Bob S.', 'Mike B.', '2019-12-12', '2020-01-15');
INSERT INTO vt_agent VALUES(12345678, 'Bob S.', 'Mike B.', '2020-01-15', '9999-12-31');
Select * FROM VT_AGENT
Assuming your last insert had the typos mentioned in the comments, you can make use of Teradata's Period data type (and functions) to make this super simple:
SELECT NORMALIZE
empid,
manager,
directory,
PERIOD(record_start, record_end) as valid_period
FROM VT_AGENT;
What this is doing is constructing a PERIOD column type from the record_start and record_end dates. Then we use the NORMALIZE keyword to compress periods where all other non-period columns are equal across more than one record. The result is a single record with the expanded period. This works only when the periods in those matching records meet (the end of one stops at the start of the next) or overlap (the end of one is after the start of the next).
With the assumed typo corrected, this outputs:
+----------+---------+----------+--------------------------+
| EmpID | Manager | Director | valid_period |
+----------+---------+----------+--------------------------+
| 12345678 | Bob S. | Mike B. | (2019-09-16, 2019-11-23) |
| 12345678 | Bob S. | Mike B. | (2019-12-12, 9999-12-31) |
| 12345678 | Jill M. | Mike B. | (2019-08-21, 2019-09-16) |
| 12345678 | Tom A. | Mike B. | (2019-11-23, 2019-12-12) |
+----------+---------+----------+--------------------------+
I want to get the greatest (or lowest) value in a field for a specific value of a different field but I am a bit lost. I am already aware of answered questions on the topic, but I already have a join in my query and I can't apply the terrific answers I found on my specific problem.
I have two tables, namely register and records. Records has all (weather) stations listed once for each month (each stationid represented 12 times, if complete data exists, a stationid can thus not be presented more than 12 times), and register has all stations listed with some of their characteristics. For the sake of the example, the two tables look pretty much like this:
CREATE TABLE IF NOT EXISTS `records` (
`stationid` varchar(30),
`month` int(11),
`tmin` decimal(3,1),
`tmax` decimal(3,1),
`commentsmax` text,
`commentsmin` text,
UNIQUE KEY `webcode` (`stationid`,`month`)
);
INSERT INTO `records` (`stationid`, `month`, `tmin`, `tmax`, `commentsmin`, `commentsmax`) VALUES
('station1', 7, '10.0', '46.0', 'Extremely low temperature.', 'Very high temperature.'),
('station2', 7, '15.0', '48.0', 'Very low temperature.', 'Extremely low temperature.'),
('station1', 1, '-10', '15', 'Extremely low temperature.', 'Somewhat high temperature.');
CREATE TABLE IF NOT EXISTS `register` (
`stationid` varchar(30),
`stationname` varchar(40),
`stationowner` varchar(10),
`georegion` varchar(40),
`altitude` int(4),
KEY `stationid` (`stationid`)
);
INSERT INTO `register` (`stationid`, `stationname`, `stationowner`, `georegion`, `altitude`) VALUES
('station1', 'Halifax', 'Maria', 'the North', 16),
('station2', 'Leeds', 'Peter', 'the South', 240);
The desired output is:
+-------------+-------+-------+---------------+-----------+----------+-----------------------------+
| stationname | month | tmin | stationowner | georegion | altitude | commentsmin |
+-------------+-------+-------+---------------+-----------+----------+-----------------------------+
| Leeds | 7 | 15.0 | Peter | the South | 240 | Very low temperature |
| Halifax | 1 | -10.0 | Maria | the North | 16 | Extremely low temperature |
+-------------+-------+-------+---------------+-----------+----------+-----------------------------+
where each station appears only one with the lowest temperatures from table 'records', including some station properties from the table 'register'. I am using the following code:
SELECT register.stationname, records.month, min(records.tmin), register.stationowner, register.georegion, register.altitude, records.commentsmin FROM records INNER JOIN register ON records.stationid=register.stationid GROUP BY records.stationid ORDER BY min(tmin) ASC
but it doesn't give the correct bits of the records table corresponding to the lowest tmin values BY stationid when there are many records in the tables.
I have seen solutions like this one here: MySQL Greatest N Results with Join Tables, but I just can't get my head around applying it on my two tables. I would be grateful for any ideas!
SELECT stuff
FROM some_table x
JOIN some_other_table y
ON y.something = x.something
JOIN
( SELECT something
, MIN(something_other_thing) min_a
FROM that_other_table
GROUP
BY something
) z
ON z.something = y.something
AND z.min_a = y.another_thing;
I asked this question previously, then someone suggested that it was a duplicate of another previously answered question. However, I could not adapt that solution to what I need despite 3 hours of trying.
So, my new question is how to adapt that solution to my own needs.
A simplified version of my category/subcategory database schema looks like this:
tblAllCategories
record_id title level parent_cat_id parent_id keywords
-------------------------------------------------------------------------------------------
1 Antiques & Collectables 0 NULL NULL junk
2 Art 0 NULL NULL
25 Furniture 1 1 1
59 Office Furniture 2 1 25 retro,shabby chic
101 Chairs 3 1 59
Notes:
Level 0 = top-level category, level 1 = second level, etc
parent_cat_id is the top-level category (i.e. having level 0)
parent_id refers to the level immediately above the relevant level
I added the keyword column to assist keyword searches so that items in certain relevant categories would be returned if the user entered a keyword but did not select a category to drill down into.
So, at the front end, after the user enters keyword, e.g., "Retro", I need to return not only the category that has the term "retro" in its keyword column, but also all higher level categories. So, according to the schema above, a search on "retro" would return category 59 along with its super-categories - 25 and 1.
The query should be sorted by level, such that the front end search results would look something like this (after necessary coding):
The solution offered is from this question
And the query is as follows:
SELECT T2.id, T2.title,T2.controller,T2.method,T2.url
FROM (
SELECT
#r AS _id,
(SELECT #r := parent_id FROM menu WHERE id = _id) AS parent_id,
#l := #l + 1 AS lvl
FROM
(SELECT #r := 31, #l := 0) vars,
menu m
WHERE #r <> 0) T1
JOIN menu T2
ON T1._id = T2.id
ORDER BY T1.lvl DESC;
I need to adapt this query to work off a passed keyword, not an ID.
Edit the vars subquery to have #r equal to the record_id of the row with the keywork, something like
SELECT T2.record_id, T2.title,T2.level,T2.keywords
FROM (SELECT #r AS _id
, (SELECT #r := parent_id
FROM tblAllCategories
WHERE record_id = _id) AS parent_id
, #l := #l + 1 AS lvl
FROM (SELECT #r := record_id, #l := 0
FROM tblAllCategories
WHERE keywords like '%retro%') vars
, tblAllCategories m
WHERE #r <> 0) T1
JOIN tblAllCategories T2 ON T1._id = T2.record_id
ORDER BY T1.lvl DESC;
SQLFiddle demo
Having the keywork as a comma separated values is not the best, a many to many relationship between this table and a keyword table (with the compulsory junction table) will be better as it will avoid the use of LIKE. In this example if there were another category with the keyword 'retrobike' that category and all his hierarchy will also be in the result.
This is going to take a while so get some coffee.
There are a lot of good resources available for hierarchical development. Most of what you will see below comes from sites like this and it refers you to Celko which I hardily recommend.
The first thing you'll have to do is remove the keywords field. The extra effort in development, use and maintenance is nowhere near the benefit received. I'll show you how to implement it later.
In this design, think of a row as a node. Each node has two values, the left boundary and the right boundary. These form a range or span of influence. If a node has boundaries of 1:4 and another node has 2:3, the second node is a subnode of the first as its span is contained in the span of the first. Also, as the boundaries of the second node are consecutive, there can be no node below it, so it must be a leaf node. This may sound complicated at first, especially when considering many levels of nodes, but you will see how the SQL is relatively easy to write and the maintenance effort for the table is minimal.
The complete script is here.
CREATE TABLE categories (
id INT not null auto_increment PRIMARY KEY,
name VARCHAR( 50 ) NOT NULL,
lBound INT NOT NULL,
rBound INT NOT NULL,
-- MySQL does not implement check constraints. These are here for illustration.
-- The functionality will be implemented via trigger.
CONSTRAINT cat_ptr_incr_chk CHECK ( lBound < rBound ), -- basic integrity check
CONSTRAINT cat_ptr_root_chk CHECK ( lBound >= 0 ) -- eliminates negative values
);
create unique index ndx_cat_lBound on categories( lBound );
create unique index ndx_cat_rBound on categories( rBound );
Notice there is nothing here that say "I'm a leaf node", "I'm a root" or "My root node is such-and-such." This information is all encompassed by the lBound and rBound (left boundary, right boundary) values. Let's build a few nodes so we can see what this looks like.
INSERT INTO categories( name, lBound, rBound )
values( 'Categories', 0, 1 );
ID name lBound rBound
== ========== ====== ======
1 Categories 0 1
This we do before creating the triggers on the table. That's really so the insert trigger doesn't have to have special code that must recognize when the first row (the root node of the entire structure). That code would only be executed when the first row is inserted and never again. Now we don't have to worry about it.
So now me have the root of the structure. Notice that its bounds are 0 and 1. Nothing can fit between 0 and 1 so this is a leaf node. The tree root is also a leaf. That means the tree is empty.
So now we write the triggers and dml procedures. The code is in the script so I won't duplicate it here, just say that the insert and delete triggers will not allow just anyone to issue an Insert or Delete statement. Anyone may issue an Update, but only the name is allowed to be changed. The only way Inserts, Deletes and complete Updates may be performed is through the procedures. With that in mind, let's create the first node under the root.
call ins_category( 'Electronics', 1 );
This creates a node with the name 'Electronics' as a subnode of the node with ID=1 (the root).
ID name lBound rBound
== ========== ====== ======
1 Categories 0 3
2 Electronics 1 2
Notice how the trigger has expanded the right boundary of the root to allow for the new node. The next node will be yet another level.
call ins_category( 'Televisions', 2 );
Node 2 is Electronics so the new node will be its subnode.
ID name lBound rBound
== ========== ====== ======
1 Categories 0 5
2 Electronics 1 4
3 Televisions 2 3
Let's create a new upper level node -- still it must be under the root, but will be the start of a subtree beside Electronics.
call ins_category( 'Antiques & Collectibles', 1 );
ID name lBound rBound
== ========== ====== ======
1 Categories 0 7
2 Electronics 1 4
3 Televisions 2 3
4 Antiques & Collectibles 5 6
Notice the 5-6 does not fit between any boundary range except for the root. So it is a subnode directly under the root, just like Electronics, but is independent of the other subnodes.
The SQL to give a clearer picture of the structure is not complicated. After completing the tree with a lot more nodes, let's see what it looks like:
-- Examine the tree or subtree using pre-order traversal. We start at the node
-- specified in the where clause. The root of the entire tree has lBound = 0.
-- Any other ID will show just the subtree starting at that node.
SELECT n.ID, n.NAME, n.lBound, n.rBound
FROM categories p
join categories n
on n.lBound BETWEEN p.lBound AND p.rBound
where p.lBound = 0
ORDER BY n.lBound;
+----+----------------------------+--------+--------+
| id | name | lBound | rBound |
+----+----------------------------+--------+--------+
| 1 | >Categories | 0 | 31 |
| 2 | -->Electronics | 1 | 20 |
| 3 | ---->Televisions | 2 | 9 |
| 4 | ------>Tube | 3 | 4 |
| 5 | ------>LCD | 5 | 6 |
| 6 | ------>Plasma | 7 | 8 |
| 7 | ---->Portable Electronics | 10 | 19 |
| 8 | ------>MP3 Players | 11 | 14 |
| 9 | -------->Flash | 12 | 13 |
| 10 | ------>CD Players | 15 | 16 |
| 11 | ------>2-Way Radios | 17 | 18 |
| 12 | -->Antiques & Collectibles | 21 | 28 |
| 14 | ---->Furniture | 22 | 27 |
| 15 | ------>Office Furniture | 23 | 26 |
| 16 | -------->Chairs | 24 | 25 |
| 13 | -->Art | 29 | 30 |
+----+----------------------------+--------+--------+
The output above is actually from a view defined in the script, but it shows clearly the hierarchical structure. This may easily be converted to a set of nested menus or navigational nodes.
There are enhancements that may be made, but they needn't change this basic structure. You'll find it reasonably easy to maintain. I had started out thinking this would be a whole lot easier in a DBMS such as Oracle, SQL Server or PostGreSQL which allows triggers on views. Then access could be limited to only the views so triggers would take care of everything. That would eliminate the need for separate stored procedures. But this way isn't half bad. I could happily live with it. In fact, there is a simplicity and flexibility to using the stored procedures that wouldn't be available thru views alone (you can't pass parameters to views).
The keyword feature is also defined but I won't show that here. Look at the script. Execute it a little at a time to get a clear picture of what is taking place. If you have any questions, you know where to find me.
[Edit] Added a few enhancements, including working with the keywords.
You can use this simple, for Add a new column by HeirarchyID type for management Tree :
We can use Microsoft example CLICK HERE
THIS IS A SAMPLE TABLE
create table [EmployeeTB]
(
employee int identity primary key,
name nvarchar(50),
hourlyrate money,
managerid int -- parent in personnel tree
);
set identity_insert dbo.[EmployeeTB] on;
insert into [EmployeeTB] (employee, name, hourlyrate, managerid)
values
(1, 'Big Boss', 1000.00, 1),
(2, 'Joe', 10.00, 1),
(8, 'Mary', 20.00, 1),
(14, 'Jack', 15.00, 1),
(3, 'Jane', 10.00, 2),
(5, 'Max', 35.00, 2),
(9, 'Lynn', 15.00, 8),
(10, 'Miles', 60.00, 8),
(12, 'Sue', 15.00, 8),
(15, 'June', 50.00, 14),
(18, 'Jim', 55.00, 14),
(19, 'Bob', 40.00, 14),
(4, 'Jayne', 35.00, 3),
(6, 'Ann', 45.00, 5),
(7, 'Art', 10.00, 5),
(11, 'Al', 70.00, 10),
(13, 'Mike', 50.00, 12),
(16, 'Marty', 55.00, 15),
(17, 'Barb', 60.00, 15),
(20, 'Bart', 1000.00, 19);
set identity_insert dbo.[EmployeeTB] off;
select * from [EmployeeTB]
order by managerid
--Big Boss /
--Joe /1/
--Jane /1/1/
--Max /1/2/
--Ann /1/2/1/
--Art /1/2/2/
Now add NEW COLUMN BY HEIRARCHY
alter table [EmployeeTB]
add [Chain] hierarchyid;
-- fills all Chains
with sibs
as
(
select managerid,
employee,
cast(row_number() over (partition by managerid order by employee) as varchar) + '/' as sib
from [EmployeeTB]
where employee != managerid
)
--select * from sibs
,[noChain]
as
(
select managerid, employee, hierarchyid::GetRoot() as Chain from [EmployeeTB]
where employee = managerid
UNION ALL
select P.managerid, P.employee, cast([noChain].Chain.ToString() + sibs.sib as hierarchyid) as Chain
from [EmployeeTB] as P
join [noChain] on P.managerid = [noChain].employee
join sibs on
P.employee = sibs.employee
)
--select Chain.ToString(), * from [noChain]
update [EmployeeTB]
set Chain = [noChain].Chain
from [EmployeeTB] as P join [noChain]
on P.employee = [noChain].employee
select Chain.ToString(), * from [EmployeeTB]
order by managerid
we can find any model of view for this example.
I am currently in the process of developing a Database software for a company that I am working with. I based the tables off of Len Silverston's book, as I found it to be an excellent source for information based on data modeling.
Now, you do not need to be acquainted with his book to know the solution to my problem, but I could not think of any other way to word my title.
Suppose I have two tables, Persons and Person_Names:
CREATE TABLE Persons
(
party_id INT PRIMARY KEY,
birth_date DATE,
social VARCHAR(20)
);
CREATE TABLE Person_Names
(
party_id INT,
name_id INT,
person_name VARCHAR(20),
CONSTRAINT person_names_cpk
PRIMARY KEY (party_id, name_id)
);
The two tables can be joined by party_id. Also, under Person_Names, name_id = 1 correlates to the person's first name (which is stored in the field person_name) and name_id = 2 is the person's last name.
* EDIT *
Someone asked for some data, so I will add some data below:
INSERT INTO Persons VALUES
(1, '01-01-1981', '111-11-1111'),
(2, '02-02-1982', '222-22-2222'),
(3, '03-03-1983', '333-33-3333');
INSERT INTO Person_Names VALUES
(1, 1, 'Kobe'),
(1, 2, 'Bryant'),
(2, 1, 'LeBron'),
(2, 2, 'James'),
(3, 1, 'Kevin'),
(3, 2, 'Durant');
Now that I added those data, how would I query the following?
-----------------------------------------------------------------------
| Party Id | First Name | Last Name | Birthdate | Social No. |
-----------------------------------------------------------------------
| 1 | Kobe | Bryant | 01-01-1981 | 111-11-1111 |
| 2 | LeBron | James | 02-02-1982 | 222-22-2222 |
| 3 | Kevin | Durant | 03-03-1983 | 333-33-3333 |
-----------------------------------------------------------------------
Thanks for taking your time to read my question!
Quite easily. I don't know the book, but presumably it contains some material describing table joins and their application in queries such as this one:
SELECT Persons.party_id AS "Party Id",
firstname.person_name AS "First Name",
lastname.person_name AS "Last Name",
Persons.birth_date AS "Birthdate",
Persons.social AS "Social No."
FROM Persons
INNER JOIN Person_Names firstname
ON Persons.party_id = firstname.party_id
AND firstname.name_id = 1
INNER JOIN Person_Names lastname
ON Persons.party_id = lastname.party_id
AND lastname.name_id = 2
Be advised that this will return results only for those people who have both a first and a last name defined in your Person_Names table; if one or the other isn't present, the INNER JOINs' ON clause conditions will exclude those rows entirely.
I'm working on manipulating a product data feed, and am currently working on grouping the related products. I've almost got things where I want them, but, like a mediocre racing driver, I've run out of skill right when I need it the most.
To illustrate my problem I've created a simplified version. Here is the data structure:
CREATE TABLE `feed` (
`sku` VARCHAR(10),
`price` DECIMAL(6,2),
`groupkey` VARCHAR(10)
);
INSERT INTO `feed` (`sku`, `price`, `groupkey`) VALUES
('AAA', 10.00, NULL),
('BBB', 10.00, 'group1'),
('CCC', 12.00, 'group1'),
('DDD', 10.00, 'group2'),
('EEE', 12.00, 'group2'),
('FFF', 14.00, 'group2'),
('GGG', 20.00, NULL);
My current query is:
SELECT feed.groupkey
, group_concat(feed.sku) AS skus
, group_concat(feed.price) AS prices
, feed.price AS pprice
FROM
feed
WHERE
feed.groupkey IS NOT NULL
GROUP BY
feed.groupkey;
The query returns the following rows:
+----------+-------------+-------------------+--------+
| groupkey | skus | prices | pprice |
+----------+-------------+-------------------+--------+
| group1 | BBB,CCC | 10.00,12.00 | 10.00 |
| group2 | DDD,EEE,FFF | 10.00,12.00,14.00 | 10.00 |
+----------+-------------+-------------------+--------+
What I actually need to do is subtract pprice from each concatenated price, giving me the price difference between each sku, rather than their absolute prices. This would return the dream result:
+----------+-------------+-------------------+--------+
| groupkey | skus | prices | pprice |
+----------+-------------+-------------------+--------+
| group1 | BBB,CCC | 0.00,2.00 | 10.00 |
| group2 | DDD,EEE,FFF | 0.00,2.00,4.00 | 10.00 |
+----------+-------------+-------------------+--------+
I've spent a lot of time on this feed in general, and am really stuck on what is probably the last hurdle in the integration. I'd really appreciate some guidance to help me in the right direction.
edit: I'm using the results from this query as "virtual" product rows, to serve as parents for the products in the group.
You can just do the subtraction in the group_concat(), for something like:
SELECT feed.groupkey, group_concat(feed.sku) AS skus,
group_concat(feed.price - min(feed.price)) AS prices
min(feed.price) AS pprice
FROM feed
WHERE feed.groupkey IS NOT NULL
GROUP BY feed.groupkey
The problem is . . . which feed.price? The value returned in your original query is an arbitrary value from one of the rows in the group. Thinking that you might want the difference over the minimum, I used that value.
I think the best way to write the query is:
SELECT feed.groupkey, group_concat(feed.sku) AS skus,
group_concat(feed.price - fsum.minprice) AS prices
min(feed.price) AS pprice
FROM feed left outer join
(select groupkey, MIN(feed.price) as minprice
from feed
group by groupkey
) fsum
on feed.groupkey = fsum.groupkey
WHERE feed.groupkey IS NOT NULL
GROUP BY feed.groupkey
You CANNOT assume the ordering for hidden columns and group_concat(). The documentation is quite explicit on this point:
MySQL extends the use of GROUP BY so that the select list can refer to
nonaggregated columns not named in the GROUP BY clause. This means
that the preceding query is legal in MySQL. You can use this feature
to get better performance by avoiding unnecessary column sorting and
grouping. However, this is useful primarily when all values in each
nonaggregated column not named in the GROUP BY are the same for each
group. The server is free to choose any value from each group, so
unless they are the same, the values chosen are indeterminate.
Furthermore, the selection of values from each group cannot be
influenced by adding an ORDER BY clause. Sorting of the result set
occurs after values have been chosen, and ORDER BY does not affect
which values the server chooses.
If you want things in a particular order, then you need to be sure the structure is queried properly. That said, it often works in practice, but there is no guarantee.