views:

161

answers:

5

I want to get the latest MainNumber, Serial, BarType and Notes for a given MainNumber, if they exist. Note that BarType is stored in a lookup table and referenced with BarID.

Unreason came up with this:

SELECT @MainNumber, COALESCE(n.Notes, 'None')
FROM numbers 
     LEFT JOIN notes n ON numbers.MainNumber = n.MainNumber
     LEFT JOIN notes n2 ON n.MainNumber = n2.MainNumber AND n.Date < n2.Date
WHERE n2.Date IS NULL AND numbers.MainNumber = @MainNumber

This is fine whether Notes is NULL or not, but now I need the Serial and the BarType. A MainNumber may have been assigned to multiple Serials during its lifetime, but I only want the latest Serial. (I'll need to do this with about 15 other fields in other tables, so a performant answer would be appreciated where possible)

Tables

Numbers Table:

CREATE TABLE `numbers` (
  `ID` int(10) unsigned NOT NULL auto_increment,
  `MainNumber` varchar(11) NOT NULL,
  `Serial` varchar(20) NOT NULL,
  `Date` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
  PRIMARY KEY  (`ID`),
  UNIQUE KEY `Serial` (`Serial`)
) ENGINE=MyISAM AUTO_INCREMENT=460 DEFAULT CHARSET=latin1

Notes table:

CREATE TABLE `notes` (
  `ID` int(10) unsigned NOT NULL auto_increment,
  `MainNumber` varchar(11) NOT NULL,
  `Notes` longtext NOT NULL,
  `Date` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
  PRIMARY KEY  (`ID`),
  KEY `MainNumber` (`MainNumber`)
) ENGINE=MyISAM AUTO_INCREMENT=11 DEFAULT CHARSET=latin1 ROW_FORMAT=DYNAMIC

ref_bars table:

CREATE TABLE `ref_bars` (
  `BarID` varchar(6) NOT NULL,
  `BarType` varchar(30) NOT NULL,
  PRIMARY KEY  USING BTREE (`BarID`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1

bars table:

CREATE TABLE `bars` (
  `ID` int(10) unsigned NOT NULL auto_increment,
  `MainNumber` varchar(11) NOT NULL,
  `BarID` varchar(6) NOT NULL,
  `Date` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
  PRIMARY KEY  (`ID`),
  KEY `MainNumber` (`MainNumber`)
) ENGINE=MyISAM AUTO_INCREMENT=212 DEFAULT CHARSET=latin1 ROW_FORMAT=DYNAMIC

Sample Data

SELECT * FROM notes:

'ID','MainNumber','Notes','Date'
'1','1','repaired','2009-03-23 12:00:00'
'2','1','replaced','2009-08-15 19:20:05'

Note: two rows for MainNumber = 1, but no row for a MainNumber of 2. The IDs are just technical and are never used.

SELECT * FROM numbers:

'ID','MainNumber','Serial','Date'
'1','1','4642785154854','2008-08-15 12:30:00'
'2','1','4642315642316','2009-08-15 12:50:00'
'3','2','5412558456223','2010-08-15 11:30:00'

SELECT * FROM bars:

'ID','MainNumber','BarID','Date'
'1','1',1,'2008-08-15 12:30:00'
'2','1',2,'2009-08-15 12:50:00'
'3','2',2,'2010-08-15 11:30:00'

SELECT * FROM ref_bars:

'BarID','BarType'
'1','Banned'
'2','Payment required'

Expected Output

MainNumber = 1

MainNumber,Notes,Banned,Unpaid
'1','replaced','Yes','Yes'

MainNumber = 2

MainNumber,Notes,Banned,Unpaid
'2','None','No','Yes'

Edit: Fixed it and tested it, whilst making things clearer (hopefully). I was rushed off to do other things earlier today, sorry for wasting people's time with a badly-written, incomplete question.

Updated to clarify the more complex requirements

A: 

Maybe something like this?

LEFT JOIN (select notes n
JOIN (
    SELECT
        MAX(Date) AS Date,
        MainNumber
    FROM notes
    GROUP BY MainNumber
) AS nx
    ON n.MainNumber = nx.MainNumber AND n.Date = nx.Date) n
ON notes.MainNumber = main.MainNumber

I removed the where-clause since you don't describe why you have it. If you need it you can just insert it again.

EDIT: The query is updated, but it is still hard to understand exatcly what you want. When I wrote example data I meant 2-3 rows for each table together with expected output, and if you could simplify the example that would be even better. Hope it helps.

mastoj
+1  A: 

Choosing to ignore your initial queries since they do not reproduce your problem I am looking at you expected output I assume that you are trying to achieve the following:

Given MainNumber look for the record in tabel notes and return the row with max date, if there are no records in table notes for a given MainNumber then return constant 'None'

(this might not be what is requested, so please correct the expected output if it is not)

This can be easily achieved with, for example

SELECT @MainNumber, n.Notes
FROM notes n
     LEFT JOIN notes n2 ON n.MainNumber = n2.MainNumber AND n.Date < n2.Date
WHERE n2.Date IS NULL AND n.MainNumber = @MainNumber

Which will return the latest row from notes. Now from the application side if it does not return any rows just print @MainNumber, 'None' and that is it...

If you look for pure SQL (and assuming that you do need some other columns from the numbers table) then you can do:

SELECT @MainNumber, COALESCE(n.Notes, 'None')
FROM numbers 
     LEFT JOIN notes n ON numbers.MainNumber = n.MainNumber
     LEFT JOIN notes n2 ON n.MainNumber = n2.MainNumber AND n.Date < n2.Date
WHERE n2.Date IS NULL AND numbers.MainNumber = @MainNumber

EDIT: The first query is tested

mysql> SET @MainNumber = 1;                                                     Query OK, 0 rows affected (0.00 sec)

mysql> SELECT @MainNumber, n.Notes FROM notes n      LEFT JOIN notes n2 ON n.MainNumber = n2.MainNumber AND n.Date < n2.Date WHERE n2.Date IS NULL AND n.MainNumber = @MainNumber;
+-------------+----------+
| @MainNumber | Notes    |
+-------------+----------+
|           1 | replaced | 
+-------------+----------+
1 row in set (0.00 sec)

The second query initially returned multiple rows in case of multiple entries in the numbers table, DISTINCT fixes that

mysql> SET @MainNumber = 1;                                                     Query OK, 0 rows affected (0.00 sec)

mysql> SELECT DISTINCT @MainNumber, COALESCE(n.Notes, 'None')
    -> FROM numbers 
    ->      LEFT JOIN notes n ON numbers.MainNumber = n.MainNumber
    ->      LEFT JOIN notes n2 ON n.MainNumber = n2.MainNumber AND n.Date < n2.Date
    -> WHERE n2.Date IS NULL AND numbers.MainNumber = @MainNumber
    -> ;
+-------------+---------------------------+
| @MainNumber | COALESCE(n.Notes, 'None') |
+-------------+---------------------------+
|           1 | replaced                  | 
+-------------+---------------------------+
1 row in set (0.00 sec)

mysql> SET @MainNumber = 2;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT DISTINCT @MainNumber, COALESCE(n.Notes, 'None')
    -> FROM numbers 
    ->      LEFT JOIN notes n ON numbers.MainNumber = n.MainNumber
    ->      LEFT JOIN notes n2 ON n.MainNumber = n2.MainNumber AND n.Date < n2.Date
    -> WHERE n2.Date IS NULL AND numbers.MainNumber = @MainNumber
    -> ;
+-------------+---------------------------+
| @MainNumber | COALESCE(n.Notes, 'None') |
+-------------+---------------------------+
|           2 | None                      | 
+-------------+---------------------------+
1 row in set (0.00 sec)
Unreason
Thanks for persevering with me. You understood what I wanted, but I'm not sure how to use your queries. I looked at the MySQL documentation on variables, then tried preceding the query with `SET @MainNumber = 1;`, but then I don't get a resultset. I tried with `2`,`'1'` and `'2'` as well. Also I do need to get MainNumber from `numbers` as there won't be a row in `notes` when MainNumber = 2. I'll update the question again.
Alan
@Alan, will update the answer, have an error in the second one anyway
Unreason
Your second query has a catch: It will return two rows for MainNumber=1 (though they are identical). As far as I can see, it will always return as many rows as there are rows for the given MainNumber in "numbers". No problem if you run it as-is and just use the first result row, but using it e.g. in a subquery you might get a problem.
sleske
BTW: The trick of using a "LEFT self join" to get the maximum record in a set is a nice one. Though using "window functions" would be nicer; but few DBMS support them...
sleske
@sleske, yes I noticed my error. fixed it with DISTINCT (could have been LIMIT 1 as well if query will always use scalar parameter) @Alan, you will find the transcript of the commands as a proof that the above works - try to compare with what you are doing
Unreason
@sleske, not so few - postgres, oracle, db2 and ms sql all have capability to use windowsing contructs, but yes, this is mysql so no cigar for the moment
Unreason
Okay, even `SET @MainNumber = 1; SELECT @MainNumber;` in the Query Browser didn't give me a 1, but I'll be using named parameters in a prepared statement, so I think it should be fine.
Alan
I eventually got this working, but still not using the variable. I need another field from `numbers` anyway (but I worked that out). The main problem is that I'm doing this with about 15 tables, and I've found that I'm getting multiple rows with differing data (I have `DISTINCT` in there anyway) in the fields where I'm using a lookup/description table. You did answer correctly, so I'll mark this as accepted.
Alan
@Alan - ok, having multiple tables does complicate the situation, maybe EDIT the question and add the more complicated requirements (there is a solution, and it is normal that DISTINCT will return multiple values if there are columns that differ; of course there are ways around it; subqueries would help). There is still incentive for solving that since you did not assign the bounty :)
Unreason
I've edited the question, now it's dead again. Any ideas what the problem (with the question itself) is? I unaccepted your answer because I thought that it might have been the reason.
Alan
+1  A: 

You can do it with a JOIN, as suggested by Unreason.

Another way would be with a subquery:

select distinct s.MainNumber, 
COALESCE(
  (select n.notes from notes n where n.MainNumber=s.MainNumber order by n.Date desc limit 1), 
  'None') as LastNote
from numbers s
WHERE s.MainNumber=?

Note that the solution using JOIN may or may not perform better, you'll have to try it.

Also note that "LIMIT" is MySQL-specific (not ANSI SQL), so take care if you intend to migrate to another DBMS.

sleske
@Alan, this answer works, too (returns the requested data). I have tested it.
Unreason
A: 
SELECT MainNumber,
       COALESCE(Notes,"None") as Notes,
       if(BarID=1,"Yes","No") as Banned,
       if(BarID=2,"Yes","No") as Unpaid,
       COALESCE(notes.Date,CURRENT_TIMESTAMP) as Date
FROM numbers LEFT JOIN notes USING (MainNumber)
     LEFT JOIN bars USING (MainNumber)
GROUP BY MainNumber,Date
HAVING Date=COALESCE(MAX(notes.Date),CURRENT_TIMESTAMP)
tobyodavies
This doesn't work. I tried fixing it (changing the BarTypes, the bar_ref to ref_bars, adding a `LEFT JOIN` to bars and adding notes.Date to the `SELECT` clause) but then it comes up with no results. Could you work on this a bit?
Alan
tinkered and tesed
tobyodavies
A: 

I finally worked it out. It's actually a lot more simple than I thought. It's closer to the original query I was writing, too.

SELECT
    s.MainNumber,
    n.Notes
FROM numbers s
JOIN (
    SELECT
        MAX(Date) AS Date,
        MainNumber
    FROM numbers
    WHERE MainNumber = 2
) AS sx
    ON s.MainNumber = sx.MainNumber AND s.Date = sx.Date

LEFT JOIN notes n
    ON s.MainNumber = n.MainNumber
LEFT JOIN (
    SELECT
            MAX(Date) AS Date,
            COALESCE(MainNumber,0) AS MainNumber
    FROM notes
    WHERE MainNumber = 2
) AS nx
    ON n.MainNumber = nx.MainNumber AND n.Date = nx.Date
Alan