MySQL selecting "previous" max(id) WHERE - php

I have a table that inserts data when a kid checks in into a summer camp area. If the kid is < than 12 years of age a parents barcode must be scanned to allow the kid entry into the young kids area (<12 years of age).
The table has the following columns.
kcID
uID
id
age
room
barcode
date
amber
The data will usually be presented like this.
kcID uID id age room barcode date amber
25 1 1 30 1000 0001 6/26/2014 1:27:40 AM 0
26 6 1 1 1000 0005 6/26/2014 1:27:40 AM 0
The problem I have is that I need to compare the dates/hours to know if the kid is entering or leaving the camp area and via php send an SMS to the parents so they know their kid is outside a particular area.
I know I can retrieve the max(kcID) WHERE barcode = XXXX and that will return the last inserted row, but, in order for me to retrieve said information the kid must be scanned, properly inserting a new row and rendering max(kcID) useless in this case.
What I need is to be able to select max(kcID) WHERE barcode = xxxx and then select the previous row record in which barcode = xxxx is found. That way I can compare dates and know if the kid is leaving or entering that particular area.
The easiest solution I can think of right now is to have 2 tables (1 for entry 1 for out) and have the camp counselors choose if the kid is entering or leaving but I'm wondering if I can use only 1 table.

Add a column with a status left and returned
ALTER TABLE `table` ADD `status` enum('left', 'returned') NOT NULL DEFAULT 'left';
now you can select a second last row by following query
select `kcID`, `barcode` from `table` where `status` = 'left' ORDER BY `barcode` DESC LIMIT 1,1

You could add a column indicating leave / return, and add this to your condition.
e.g.
ALTER TABLE `table`
ADD `status` enum('left', 'returned') NOT NULL DEFAULT 'left';
a query would be
SELECT `kcID`, `barcode`
FROM `table`
WHERE `status` = 'left'
ORDER BY `timefield` DESC
LIMIT 1

Related

Pagination Offset Issues - MySQL

I have an orders grid holding 1 million records. The page has pagination, sort and search options. So If the sort order is set by customer name with a search key and the page number is 1, it is working fine.
SELECT * FROM orders WHERE customer_name like '%Henry%' ORDER BY
customer_name desc limit 10 offset 0
It becomes a problem when the User clicks on the last page.
SELECT * FROM orders WHERE customer_name like '%Henry%' ORDER BY
customer_name desc limit 10 offset 100000
The above query takes forever to load. Index is set to the order id, customer name, date of order column.
I can use this solution https://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/ if I don't have a non-primary key sort option, but in my case sorting is user selected. It will change from Order id, customer name, date of order etc.
Any help would be appreciated. Thanks.
Problem 1:
LIKE "%..." -- The leading wildcard requires a full scan of the data, or at least until it finds the 100000+10 rows. Even
... WHERE ... LIKE '%qzx%' ... LIMIT 10
is problematic, since there probably not 10 such names. So, a full scan of your million names.
... WHERE name LIKE 'James%' ...
will at least start in the middle of the table-- if there is an index starting with name. But still, the LIMIT and OFFSET might conspire to require reading the rest of the table.
Problem 2: (before you edited your Question!)
If you leave out the WHERE, do you really expect the user to page through a million names looking for something?
This is a UI problem.
If you have a million rows, and the output is ordered by Customer_name, that makes it easy to see the Aarons and the Zywickis, but not anyone else. How would you get to me (James)? Either you have 100K links and I am somewhere near the middle, or the poor user would have to press [Next] 'forever'.
My point is that the database is not the place to introduce efficiency.
In some other situations, it is meaningful to go to the [Next] (or [Prev]) page. In these situations, "remember where you left off", then use that to efficiently reach into the table. OFFSET is not efficient. More on Pagination
I use a special concept for this. First I have a table called pager. It contains an primary pager_id, and some values to identify a user (user_id,session_id), so that the pager data can't be stolen.
Then I have a second table called pager_filter. I consist of 3 ids:
pager_id int unsigned not NULL # id of table pager
order_id int unsigned not NULL # store the order here
reference_id int unsigned not NULL # reference into the data table
primary key(pager_id,order_id);
As first operation I select all records matching the filter rules from and insert them into pager_filter
DELETE FROM pager_filter WHERE pager_id = $PAGER_ID;
INSERT INTO pager_filter (pager_id,order_id,reference_id)
SELECT $PAGER_ID pager_id, ROW_NUMBER() order_id, data_id reference_id
FROM data_table
WHERE $CONDITIONS
ORDER BY $ORDERING
After filling the filter table you can use an inner join for pagination:
SELECT d.*
FROM pager_filter f
INNER JOIN data_table d ON d.data_id = f.reference id
WHERE f.pager_id = $PAGER_ID && f.order_id between 100000 and 100099
ORDER BY f.order_id
or
SELECT d.*
FROM pager_filter f
INNER JOIN data_table d ON d.data_id = f.reference id
WHERE f.pager_id = $PAGER_ID
ORDER BY f.order_id
LIMIT 100 OFFSET 100000
Hint: All code above is not tested pseudo code

MySQL: Order by 2nd column--if not null--while maintaining 1st column order

Essentially I want to order by "title", while grouping movies with the same, non-NULL "series_id" together within that order. Titles where series_id IS NULL should order by title only, while non NULL series_ids should order by "series_order" and be listed in the results based on "series"."title" within the original columns sort. I would prefer to achieve this within the query rather then loading the entire database and sorting from there.
What I have tried (MySQL 5.7.17):
ORDER BY title, series_id
SELECT * FROM media
ORDER BY title, series_id, series_order
LIMIT 0, 30
Does not account for titles in a series that are not alphabetically the same (see sample below).
ORDER BY CASE using CONCAT to sort by 'series'.'title' + 'media'.'series_order'
SELECT * FROM media
ORDER BY CASE
WHEN series_id IS NULL THEN title
ELSE CONCAT((SELECT title FROM series WHERE id = media.series_id), series_order)
END
LIMIT 0, 30
Th results are correctly ordered in a SQL Fiddle, but not on the dev server. To be fair this is still not the desired result as the 'series'.'title' may differ from the original movies title.
LEFT JOIN to include the 'series' table for sorting using the same idea
SELECT media.*, series.title, series.id FROM media
LEFT JOIN series ON media.series_id = series.id
LIMIT 0, 30
This does not order the data correctly, either.
Sample Data:
title
series_id
series_order
88 Minutes
NULL
NULL
Live Free or Die Hard
100094
4
3rd Rock from the Sun
100000
2
2 Guns
NULL
NULL
Die Hard
100094
1
Evil Dead
NULL
NULL
A Good Day to Die Hard
100094
5
3rd Rock from the Sun
100000
1
Desired Result
Order
2 Guns
NULL
3rd Rock from the Sun
1
3rd Rock from the Sun
2
88 Minutes
NULL
Die Hard
1
Live Free or Die Hard
4
A Good Day to Die Hard
5
Evil Dead
NULL
Primary Table: media
Relevant Columns: title, series_id, series_order
Series Table: series
Relevant Columns: id, title
Fiddle: http://sqlfiddle.com/#!9/efca7c/3
On the fiddle, option 2 appears to be working. On the dev version it only partially orders things.
EDIT: As it turns out the PHP code I was using to store the data before converting it to JSON was inevitably re-ordering the results by the Primary ID.
Based on the required convolution to achieve your desired sorting, if this was my application, I'd probably create a new column which contains the "base title" for the series and populate that value during insertion, then you could sort on that without any voodoo or eye-strain.
In the absence of modifying your table structure, I managed to downgrade a solution that was using ROW_NUMBER() and PARTITION (MySQL8.0 Demo) into a couple of nested subqueries -- it's not what I consider beautiful.
SQL (Demo)
SELECT m2.title grouping_title, m1.title, COALESCE(m2.title, m1.title), m1.series_order
FROM media m1
LEFT JOIN (
SELECT series_id, title
FROM media m3
WHERE series_order = (SELECT MIN(series_order) FROM media WHERE series_id = m3.series_id)
) m2 ON m1.series_id = m2.series_id
ORDER BY COALESCE(m2.title, m1.title), m1.series_order
You can modify the outer SELECT as you wish, but I just wanted to show what the COALESCE() function was generating. Effectively, I'm joining media table onto itself so that I can obtain the lowest series_order value for a given series_id. The title in the THAT row represents the "base title" to be used in the first rule of the sorting algorithm -- unless it is NULL, in which case, we just use the title from the parent query.
For your application output, you will want to use the m1.title and the m1.series_order.
Use a CASE expression to check whether seris_order value is null, if it is null then take only titlw, otherwise concatenate title with the series_order.
Query
Select case when series_order is null then title else concat(title, ' : Season ', series_order) end as title
From table_name
Order by 1;

How can I find the leaderboard position of a user based on the amount of points they have using SQL?

In my database I have a profile table which includes the following columns:
Id, profilename,points,lastupdated
Users can be put into the profile table more than once, and for the leaderboard I use the value of the max(lastupdated) column.
Below I have SQL code to try and determine someones leaderboard position, and it works except for 1 small problem.
SELECT COUNT( DISTINCT `profilename` )
FROM `profiletable`
WHERE `points` >= integer
If two people have the same points, then they have the same leaderboard position, but this makes the count function work in a way that I didnt not intend.
For example:
position 1: profilename john points 500
position 2: profilename steve points 300
position 2: profilename alice points 300
position 4: profilename ben points 200
When I run the SQL query with Integer being set to john's points, I get a return value of 1 from the count function, but when I run the SQL query for steve or alice, instead of getting 2, I get 3.
I have also tried using the logic points > integer but this returns 1 for steve or alice.
How can I modify the SQL query so that the correct leaderboard position is returned by the count?
Use greater than in the where clause and add 1 to the count:
SELECT COUNT( DISTINCT `profilename` ) + 1
FROM `profiletable`
WHERE `points` > integer
Update:
If you need to take into the updated time field as well, then first you need to get the record associated with the last update time:
SELECT COUNT(p1.`profilename` ) + 1
FROM `profiletable` p1
LEFT JOIN `profiletable` p2 ON p1.profilename=p2.profilename
and p1.lastupdated<p2.lastupdated
WHERE p1.`points` > integer and p2.profilename is null

Group SQL rows by column value without flattening [duplicate]

Is it possible to sort in MySQL by "order by" using a predefined set of column values (ID) like order by (ID=1,5,4,3) so I would get records 1, 5, 4, 3 in that order out?
UPDATE: Why I need this...
I want my records to change sort randomly every 5 minutes. I have a cron task to update the table to put different, random sort order in it.
There is just one problem! PAGINATION.
I will have visitors who come to my page, and I will give them the first 20 results. They will wait 6 minutes, go to page 2 and have the wrong results as the sort order has already changed.
So I thought that if I put all the IDs into a session on page 2, we get the correct records even if the sorting had already changed.
Is there any other better way to do this?
You can use ORDER BY and FIELD function.
See http://lists.mysql.com/mysql/209784
SELECT * FROM table ORDER BY FIELD(ID,1,5,4,3)
It uses Field() function, Which "Returns the index (position) of str in the str1, str2, str3, ... list. Returns 0 if str is not found" according to the documentation. So actually you sort the result set by the return value of this function which is the index of the field value in the given set.
You should be able to use CASE for this:
ORDER BY CASE id
WHEN 1 THEN 1
WHEN 5 THEN 2
WHEN 4 THEN 3
WHEN 3 THEN 4
ELSE 5
END
On the official documentation for mysql about ORDER BY, someone has posted that you can use FIELD for this matter, like this:
SELECT * FROM table ORDER BY FIELD(id,1,5,4,3)
This is untested code that in theory should work.
SELECT * FROM table ORDER BY id='8' DESC, id='5' DESC, id='4' DESC, id='3' DESC
If I had 10 registries for example, this way the ID 1, 5, 4 and 3 will appears first, the others registries will appears next.
Normal exibition
1
2
3
4
5
6
7
8
9
10
With this way
8
5
4
3
1
2
6
7
9
10
There's another way to solve this. Add a separate table, something like this:
CREATE TABLE `new_order` (
`my_order` BIGINT(20) UNSIGNED NOT NULL,
`my_number` BIGINT(20) NOT NULL,
PRIMARY KEY (`my_order`),
UNIQUE KEY `my_number` (`my_number`)
) ENGINE=INNODB;
This table will now be used to define your own order mechanism.
Add your values in there:
my_order | my_number
---------+----------
1 | 1
2 | 5
3 | 4
4 | 3
...and then modify your SQL statement while joining this new table.
SELECT *
FROM your_table AS T1
INNER JOIN new_order AS T2 on T1.id = T2.my_number
WHERE ....whatever...
ORDER BY T2.my_order;
This solution is slightly more complex than other solutions, but using this you don't have to change your SELECT-statement whenever your order criteriums change - just change the data in the order table.
If you need to order a single id first in the result, use the id.
select id,name
from products
order by case when id=5 then -1 else id end
If you need to start with a sequence of multiple ids, specify a collection, similar to what you would use with an IN statement.
select id,name
from products
order by case when id in (30,20,10) then -1 else id end,id
If you want to order a single id last in the result, use the order by the case. (Eg: you want "other" option in last and all city list show in alphabetical order.)
select id,city
from city
order by case
when id = 2 then city else -1
end, city ASC
If i had 5 city for example, i want to show the city in alphabetical order with "other" option display last in the dropdown then we can use this query.
see example other are showing in my table at second id(id:2) so i am using "when id = 2" in above query.
record in DB table:
Bangalore - id:1
Other - id:2
Mumbai - id:3
Pune - id:4
Ambala - id:5
my output:
Ambala
Bangalore
Mumbai
Pune
Other
SELECT * FROM TABLE ORDER BY (columnname,1,2) ASC OR DESC

Need support with figuring out a query for a cron job

I have a voting system for articles. Articles are stored in 'stories' table and all votes are stored in 'votes' table. id in 'stories' table is equal to item_name in 'votes' table (therefore each vote is related to article with item_name).
I want to make it so when sum of votes gets to 10 it updates 'showing' field in 'stories' table to value of "1".
I was thinking about setting up a cron job that runs every hour to check all posts that have a showing = 0. If showing = 0 than it will sum up votes related to that article and set showing = 1 if sum of votes >= 10. I'm not sure if it is efficient as it might take up a lot of server resources, not sure.
So could anyone suggest a cron job that could do the task?
Here is my database structure:
Stories table
Votes table
Edit:
For example this row from 'stories' table:
id| 12
st_auth | author name
st_date | story date
st_title| story title
st_category| story category
st_body| story body
showing| 0 for unaproved and 1 for approved
This row is related to this one from 'votes' table
id| 83
item_name| 12 (id of article)
vote_value| 1 for upvote -1 for downvote
...
Couple of things:
Why did you name the column item_name in the votes table, when it is actually the id of the article table? I would recommend making this a match on the article table in that it is an int(11) vs a var_char(255). Also, you should add a foreign key constraint to the votes table, so if an article is ever deleted, you don't orphan a row in the votes table.
Why is the vote_value column an int(11)? If it can only be two states (1, or -1) you can do a tinyint(1) signed (for the -1).
The ip column in the votes table is a bit concerning. If you are regulating 'unique' votes by ip, did you account for proxy ips? Something like this should be handled at the account level, so several users from the same proxy IP can issue individual votes.
I wouldn't do a cronjob for determining whether the showing column should be flagged 0 or 1. Rather, I would issue a count every time a vote was cast against the article. So if someone up-voted or down-voted, calculate the new value of the story, and store it in cache for future reads.
Using this query, you get a list of all articles plus a column containing the count of associated votes.
SELECT s.*, SUM(v.vote_value) AS votes_total
FROM stories AS s INNER JOIN votes AS v
ON v.item_name = s.id
GROUP BY v.vote
This way, you can create a view from which you can filter on votes_total > 10, without need of the cron job.
Or you can use it as a normal query, something like this:
SELECT * FROM (
SELECT s.*, SUM(v.vote_value) AS votes_total
FROM stories AS s INNER JOIN votes AS v
ON v.item_name = s.id
GROUP BY v.vote
) WHERE votes_total > 10;
I would use a trigger (insert trigger) and handle your logic there (in the database itself)?
This would remove the poll code altogether (cron job).
I would also keep your foreign key (in VOTES) the same (at least the type) as the primary key (in STORIES)?
Using a trigger instead of polling will be much cleaner in the long run.
You don't specify your database, but in TSQL (for SQL Server) it could be close to this
CREATE TRIGGER myTrigger
ON VOTES
FOR INSERT
AS
DECLARE #I INT --HOLDS COUNT OF VOTES
DECLARE #IN VARCHAR(255) --HOLDS FK ID FOR LOOKUP INTO STORIES IF UPDATE REQUIRED
SELECT #IN = ITEM_NAME FROM INSERTED
SELECT #I = COUNT(*) FROM VOTES WHERE ITEM_NAME = #IN
IF (#I >= 10)
BEGIN
UPDATE STORIES SET SHOWING = 1 WHERE ID = #IN --This is why your PK/FK should be refactored
END

Categories