MySQL table joins, nested select statements, or create a view? - php

disclaimer: I posted this on another site first
I have a table (res_table) that is about 200 columns wide. One of these columns is named "feature_lk", and it consists of a string of numbers which are "|" delimited. The numbers stand for feature catagories which reside in another table named "features"
Thanks to this thread: http://www.kirupa.com/forum/showthread.php?t=224203 I figured out how to parse the features out!
Now my problem is how to look them up? I feel like I either need to join my two tables, but I'm not sure how, or I need to do a another select query for each of the features that I parse.. This is what I have to far (removed connection strings for posting purposes)
PHP Code:
<?php
$sql = ("SELECT * FROM res_table");
$result = mysql_query($sql);
while($row = mysql_fetch_array($result))
{
$feature_string = $row['features_lk'];
$features = explode( '|', $feature_string );
foreach( $features as $feature ) {
$feature = trim( $feature );
echo $feature.': ';
$sql2 = "SELECT * from features where features.feature_id like $feature";
$result2 = mysql_query($sql2);
while ($row2 = mysql_fetch_array($result2))
{
$feat_desc = $row2['feature_description']; //this is another column in the features table
echo $feat_desc . '<br>';
}
}
echo '<br>';
}
?>
SO that works OK because when I run it, i'll get about results that look like this:
13: None
62: Water Softener - Rented
71: Full
168: Barn
222: Storage Shed
226: Walkout
309: Detached
347: 2 Story
384: Attic Storage
439: Laundry Hook Up
466: Rural
476: Trees
512: School Bus
562: Mud Room
563: Pantry
2273: Septic Tank
643: Private Well
My question is: is there a better way to do this? There are about 10k rows in the main res_table with only a couple hundred hits, you can see that the number of select statements performed grows LARGE in no time at all.
I'm sure this is PHP + MySQL 101 stuff, but I'm just a beginner so any ideas? Thanks in advance.

When you are storing more than one piece of information in a column, your table is not normalized. Doing lookups on feature_lk will necessarily be slow and difficult. feature_lk should become its own table:
Table feature_lk:
res_table_id FK to res_table
feature_id FK to feature table
primary key(res_table_id,feature_id)
Then your query is:
SELECT f.* from features f
JOIN feature_lk lk ON (f.id=lk.feature_id)
JOIN res_table r ON (lk.res_table_id=r.id);
One query only. No loop. No parsing out the features.
ETA
stored procedure for splitting an arbitrary length string by an arbitrary character
DELIMITER $$
DROP PROCEDURE IF EXISTS `dorepeat` $$
CREATE PROCEDURE `dorepeat`(in ToBeSplit LONGTEXT , in Splitter CHAR)
Begin
DECLARE TotalLength INT;
DECLARE SplitterPosition INT;
DECLARE SubstringLength INT;
DECLARE SubstringStart INT;
DROP Table if exists Split_Values;
CREATE temporary TABLE Split_Values (split varchar(255));
SET TotalLength = LENGTH(ToBeSplit);
SET SplitterPosition = LOCATE(Splitter, ToBeSplit);
SET SubstringStart = 1;
ss: WHILE SplitterPosition < TotalLength DO
IF SplitterPosition!=0 THEN
SET SubstringLength = SplitterPosition - SubstringStart;
Insert into Split_Values VALUES (SUBSTRING(ToBeSplit,SubstringStart,SubstringLength));
SET SubstringStart = SplitterPosition+1;
SET SplitterPosition = LOCATE(Splitter, ToBeSplit, SplitterPosition+1);
ELSE
Insert into Split_Values VALUES (SUBSTRING(ToBeSplit,SubstringStart));
SET SplitterPosition=TotalLength;
END IF;
END WHILE ss;
End $$
DELIMITER ;
Using dorepeat in another procedure makes temp table with res_table_id and each feature:
DELIMITER $$
DROP PROCEDURE IF EXISTS `multido` $$
CREATE PROCEDURE `multido`()
Begin
DECLARE done INT default 0;
DECLARE rt_id INT (10);
DECLARE features LONGTEXT;
DECLARE mycur cursor for select distinct res_table_id, feature_lk from res_table WHERE feature_lk!='';
DECLARE continue handler for sqlstate '02000' set done=1;
drop table if exists tmpfeatures;
create temporary table tmpfeatures( res_table_id int(10), feature varchar(255));
open mycur;
repeat
fetch mycur into rt_id,features;
call dorepeat(features,'|');
insert into tmpfeatures select rt_id, trim(split) from Split_Values;
until done end repeat;
close mycur;
End $$
DELIMITER ;

You're feeling the pain of poor database modeling here. If you have any control over the database schema, then you should fix it so that this is properly normalized. Anytime you see a pipe (or comma, or tab, or whatever) delinated list in a database, you should be very suspicious of it.
You should have a join table between your table and categories, generally named something like RES_CATEGORIES that contains the ID from RES and the ID from CATEGORIES. This is the standard way to model a many-to-many relationship in a relational database.
If you can't control the schema, than your best bet is to just parse that out in code and execute a separate query (or queries) to get the category info. You can at least specify multiple category IDs in the where clause, to make it slightly less painful.

from what i understand in your question, you need an intermediate table. for example, you have the table tbl_user and tbl_features where users can subscribe to a number of features and each feature can be subscribed by a number of users.
your database would be more manageable with an extra table tbl_userfeatures {userFeatureID, userID, featureID}, which links the other two tables and allows you to add different combinations.

One simple optimisation step would be to fetch the features in one step, instead of looping over them. Something like this:
$result = mysql_query('SELECT * FROM res_table');
while ($row = mysql_fetch_array($result)) {
$features = str_replace('|', ',', $features);
$result2 = mysql_query("SELECT * FROM features WHERE feature_id IN $features");
while ($row2 = mysql_fetch_array($result2) {
printf('%d: %s', $row2['feature_id'], $row2['feature_description']);
}
}
That's one query for each row in res_table instead of one for each feature.
But before you do this, first listen to the other responses. If you are able to change the database schema to something saner, do so!

Related

MySql Duplicate entries with auto updating context sensitive relations in PHP

Is there any possibility to duplicate specific entries by auto updating context sensitive relations?
Given a table 'table1' like:
My goal is to duplicate all entries with categoryId 42 while updating parentId if neccessary:
id is an auto incremented column and parentId is used to identify relations between the entries.
Currently I'm inserting them one by one, selecting the old data and managing the logic for the parentId in PHP.
//Thats simplified what I do ($conn is a Zend_Db_Adapter_Abstract)
$rows = $conn->fetchAll("SELECT * FROM table1 WHERE categoryId = 42 ORDER BY parentId ASC");
$newIds = [];
foreach($rows as $row){
if(array_key_exists($row['parentId'],$newIds))
$row['parentId'] = $newIds[$row['parentId']];
else
$row['parentId'] = null;
$conn->query("INSERT INTO table1 (parentId,info,categoryId) VALUES (?,?,?)",[$row['parentId'],$row['info'],$row['categoryId']]);
$newId = $conn->lastInsertId('table1');
$newIds[$row['id']] = $newId;
}
I'm stuck with this because I need the lastInsertedId of the new element to set the new parentId for the next one.
But I'm experiencing this to be pretty slow (in relation to one single query which contains the logic).
Is there any possibility to give a query some kind of incremental element sensitive logic? Or have you any suggestions on how to fasten this up?
Any help appreciated! :)
You are not showing any code. I guess you use mysqli_insert_id() to get the last inserted ID. Maybe you can do something with MAX(ID)+1.
Something like:
INSERT INTO table1
SELECT MAX(ID)+1,
info,
categoryId
FROM
table1
WHERE .............. -- the conditions to retreive the parent

using mysql stored-function with select query

let me explain my purpose first, i have an vehicle booking application where, visitor will add start date and end date of his journey, in the database there is list of drivers with there availability (available_from_date and available_to_date) which is kind of duration during which they are operating, there is an field for exclude_dates for some specific dates when they are not working.
the application needs to find a list of vehicles which are available during the journey dates entered by the user.
for example user enters he want to go from place A to B during 13th sept, 2014 to 17th sept, 2014
then database needs to return a list of taxi which are available during this period and must not have any exclude date within this period.
Now i have stored the exclude_dates in comma separated format in table (i could have created a separate table but then it would take much more time for a query to execute)
I was trying to create a mysql function which would be called within the actual search query and would return true if there is some there is some excluded date present within the duration and false if not.
these are the queries that i have written
SELECT id, exclude_dates
FROM `taxi_route`
WHERE status = 1
AND `to_city` = 'Surat'
AND `from_city` = 'Ahmedabad'
AND `trip_type` = 2
AND `available_from_date` <= '2014-09-13'
AND available_to_date >= '2014-09-17'
AND STR_TO_DATE((SELECT `split`(exclude_dates, ',', 1)),'%d-%m-%Y')
NOT BETWEEN STR_TO_DATE('13-09-2014','%d-%m-%Y')
AND STR_TO_DATE('17-09-2014','%d-%m-%Y')
Split is a function i have created in mysql to separate the dates present in comma format
DELIMITER $$
CREATE FUNCTION split( str VARCHAR(500), delchar VARCHAR(2), x INT )
RETURNS VARCHAR(500)
BEGIN
RETURN SUBSTR(SUBSTRING_INDEX(str, delchar, x),
LENGTH(SUBSTRING_INDEX(str, delchar, x-1))+IF(x > 1, 2, 1));
END$$
DELIMITER ;
this works fine as far as i pass 1 in split(exclude_dates, ',', 1) , but if the exclude_dates have more then one date then this will not work
can someone please suggest or guide, how this can be accomplished.
snapshot of database is here http://i.imgur.com/JaI8MSx.png
Your query is most likely going to take more time to execute than defining a separate table for exclusion dates. It's not a good practice using comma separated list inside a column for searching purposes, this is against normalization rules.
You should define your tables separately, (e.g. taxi, taxi_route, taxi_route_exclusion, route_exclusion) and later add necessary indexes to make your searches more efficient.
Example:
taxi
---------
id
country
***
***
***
taxi_route
-------------------
id
taxi_id
available_from_date
available_to_date
from_city
to_city
route_exclusion
---------------
id
taxi_id
exclusion_date
And also add a relation table between taxi_route and route_exclusion tables to represent many-to-many relationship. Later define foreign keys on taxi_route_route_exclusion table to point taxi_route and route_exclusion tables.
taxi_route_route_exclusion
--------------------------
taxi_route_id
route_eclusion_id
Define foreign keys like:
taxi_route.taxi_id -> taxi.id
taxi_route_route_exclusion.taxi_route_id -> taxi_route.id
taxi_route_route_exclusion.route_exclusion_id -> route_exclusion.id
Define indexes like:
taxi: IX1 (status, trip_type)
taxi_route: IX1(to_city, from_city, available_from_date, available_to_date)
Your final query should look like this:
SELECT tr.id, re.exclusion_date
FROM `taxi_route` tr JOIN `taxi_route_route_exclusion` trre
ON tr.id = trre.taxi_route_id
JOIN `route_exclusion` re
ON re.id = trre.route_exclusion_id
JOIN `taxi` t
ON t.id = tr.id
WHERE
t.status = 1
AND t.trip_type = 2
AND tr.to_city = 'Surat'
AND tr.from_city = 'Ahmedabad'
AND tr.available_from_date <= '2014-09-13'
AND tr.available_to_date >= '2014-09-17'

Delete one column from table, Update another with PHP MYSQL

I have two tables. One table is the matches table (e2wedstrijden) and another table is my scoring table with the points earned etc. (e2teams).
Now I have that I can delete a match from the e2wedstrijden table. And this is working fine.
But I want that if I delete a match from that table. It also add or decrease points to the table ("e2teams"). I tried to compare the tables but this is not working.
So I want for example:
If($row['thuisscore'] == $row['uitscore']) what are to row names in my e2wedstrijden table. So if these two are the same (like 0-0 or 1-1 or something) Than it needs to decrease 1 point from the table e2teams. But only by the teams that are the same as the rows "Thuisteam" and "Uitteam" in my e2wedstrijden table. So the Row Thuisteam (in "e2wedstrijden") Needs to find the same result in ("e2teams") row Team. And this needs to be done the same with the Row Uitteam (in "e2wedstrijden") Needs to find the same result in ("e2teams")
Thuisteam and Uitteam = Dutch for hometeam and awayteam. I think my fault is that the system can't link the 'Thuisteam' from e2wedstrijden to the Team in e2teams but don't know how to solve it
This is my deletematches.php, It deletes the match but doesn't decrease or adds points:
<?php
if(!isset($_COOKIE['E2ingelogd'])) {
header("location:../../index.php");
}
include "../../connect.php";
$dbhandle = mysql_connect($hostname, $username, $password) or die("Could not connect to database");
$selected = mysql_select_db("login", $dbhandle);
$result = mysql_query("SELECT * FROM e2wedstrijden WHERE ID = ".$_GET['del']."");
while($row = mysql_fetch_assoc($result)){
if( $row['thuisscore'] == $row['uitscore']){
echo $row['thuisscore'];
mysql_query("UPDATE e2teams SET Punten = Punten-1 WHERE Team ='".$row['Thuisteam']."'");
mysql_query("UPDATE e2teams SET Gespeeld = Gespeeld-1 WHERE Team = ('".$row['Thuisteam']."'");
mysql_query("UPDATE e2teams SET Verloren = Gelijk-1 WHERE Team ='".$row['Uitteam']."'");
echo "Team is deleted";
}else{
echo 'Update Error!';
}
}
$table_1_delete = mysql_query("DELETE FROM e2wedstrijden WHERE ID = ".$_GET['del']."");
?>
This is my e2teams table:
And this is my E2wedstrijden table:
So i need something like:
UPDATE e2teams SET Punten = Punten-1 WHERE Team = Look in table ("e2wedstrijden) deleted Thuisteam and deleted Uitteam
Hope you can help
You've placed an extra parentheses in the 2nd query for "gespeeld" right after the equal sign:
mysql_query("UPDATE e2teams SET Gespeeld = Gespeeld-1
WHERE Team = ('".$row['Thuisteam']."'");
Is this what isn't updating?
Without being 100% sure on how your data model works, it might make sense at refactoring what you have. Something that might be useful would be to create a view of the summary table and just update the data from the child/master table.... aggregating in the view layer. Views in mysql can be seen here.
If you are stuck with the data model you have (legacy application, etc.) you can possibly look at triggers if you have to modify data in two tables you might want to consider stored procedures or triggers, discussed here and here.
The third thing that comes to mind, is around correlated sub-queries and how you could reference the another table in a sort of update-from. However, you're ID's aren't surrogate keys in this situation.
Also, have a look at sql injection; I haven't looked at PHP in a while but those sql statements kind of look like they are created with sting composition
Good luck,

Create and optimize mysql query, php

Having trouble to create quality and optimize mysql query.
Lets create a table with values.
CREATE TABLE flow
(
US1 varchar(20),
US2 varchar(30)
);
INSERT INTO flow
(US1, US2)
VALUES
('rasim', 'tere1'),
('rasim', 'tere2'),
('rasim', 'tere3'),
('rasim', 'tere4'),
('tere1', 'tere5'),
('tere1', 'tere6'),
('tere2', 'tere7'),
('tere3', 'tere8'),
('tere3', 'tere9'),
('tere4', 'tere10'),
('tere5', 'tere11'),
('tere5', 'tere12'),
('tere9', 'tere13'),
('tere9', 'tere14');
What i am trying to achieve:
$firstUs = DB::query("SELECT US2 FROM `flow` WHERE US1='rasim'");
while($first = DB::fetch($firstUs)):
$second =(int) // select and count(US2) foreach value where US1 = $first['US2'];
$third =(int) //select and count(US2) foreach value where US1 = $second['US2'];
$four = (int) //select and count(US2) foreach value where US1 = $third['US2'];
endwhile;
$firstUs = returns 4 values ( tere1, tere2, tere3, tere4 ). I whant the script to count for each of these values, the number of entries from US1. $second = returns 2 values ( tere5, tere6 ). Count value would be (2) on first php while loop.
How to create a good mysql script so that this will work and if there would be a lot of users accsesing this page, the server wont crash and speed of the query would be much less.
Thank you
I am traying to achieve a pyramid scheme. Where 'rasim' is the primary user name. Where $second , $third and $four is a int number of users per level. $second will have caount of all users in the second level of the pyramid.
Example image how it will look like. http://silverstream.ee/img/stack.PNG
I think the best option here would be to break the data out into separate tables so that you don't have the Primary Key (US1) be relational to the "Foreign Key" (US2) within the same table. Then you could accomplish what you are trying to do with a query as opposed to numerous loops.
Unfortunately I still have no idea what exactly you are trying to accomplish so it's impossible for me to create an example scenario with what you've given.
I think it's something like:
select f2.us1, count(f2.us2)
from flow f1
inner join flow f2
f1.us2=f2.us1
where f1.us1='rasim'
group by f2.us1
I hope this is what you want.

Summing a field from all tables in a database

I have a MySQL database called "bookfeather." It contains 56 tables. Each table has the following structure:
id site votes_up votes_down
The value for "site" is a book title. The value for "votes_up" is an integer. Sometimes a unique value for "site" appears in more than one table.
For each unique value "site" in the entire database, I would like to sum "votes_up" from all 56 tables. Then I would like to print the top 25 values for "site" ranked by total "votes_up".
How can I do this in PHP?
Thanks in advance,
John
You can do something like this (warning: Extremely poor SQL ahead)
select site, sum(votes_up) votes_up
from (
select site, votes_up from table_1
UNION
select site, votes_up from table_2
UNION
...
UNION
select site, votes_up from table_56
) group by site order by sum(votes_up) desc limit 25
But, as Dav asked, does your data have to be like this? There are much more efficient ways of storing this kind of data.
Edit: You just mentioned in a comment that you expect there to be more than 56 tables in the future -- I would look into MySQL limits on how many tables you can UNION before going forward with this kind of SQL.
Here's a PHP code snip that should get it done.
I have not tested it so it might have some typos and stuff, make sure you replace DB_NAME
$result = mysql_query("SHOW TABLES");
$tables = array();
while ($row = mysql_fetch_assoc($result)) {
$tables[] = '`'.$row["Tables_in_DB_NAME"].'`';
}
$subQuery = "SELECT site, votes_up FROM ".implode(" UNION ALL SELECT site, votes_up FROM ",$tables);
// Create one query that gets the data you need
$sqlStr = "SELECT site, sum(votes_up) sumVotesUp
FROM (
".$subQuery." ) subQuery
GROUP BY site ORDER BY sum(votes_up) DESC LIMIT 25";
$result = mysql_query($sqlStr);
$arr = array();
while ($row = mysql_fetch_assoc($result)) {
$arr[] = $row["site"]." - ".$row["sumVotesUp"];
}
print_r($arr)
The UNION part of Ian Clelland answer can be generated using a statement like the following. The table INFORMATION_SCHEMA.COLUMNS has a column TABLE_NAME to get all tables.
select * from information_schema.columns
where table_schema not like 'informat%'
and column_name like 'VOTES_UP'
Join all inner SELECT with UNION ALL instead of UNION. UNION is doing an implicit DISTINCT (on oracle).
The basic idea would be to iterate over all your tables (using a SQL SHOW TABLES statement or similar) in PHP, then for every table, iterate over the rows (SELECT site,votes_up FROM $table). Then, for every row, check the site against an array that you're building with sites as keys and votes up as values. If the site is already in the array, increment its votes appropriately; otherwise, add it.
Vaguely PHP-like pseudocode:
// Build an empty array for use later
$votes_array = empty_array();
// Get all the tables and iterate over them
$tables = query("SHOW TABLES");
for($table in $tables) {
$rows = query("SELECT site,votes_up FROM $table");
// Iterate over the rows in each table
for($row in $rows) {
$site = $row['site'];
$votes = $row['votes_up'];
// If the site is already in the array, increment votes; otherwise, add it
if(exists_in_array($site, $votes_array)) {
$votes_array[$site] += $votes;
} else {
insert_into_array($site => $votes);
}
}
}
// Get the sites and votes as lists, and print out the top 25
$sorted_sites = array_keys($votes_array);
$sorted_votes = array_values($votes_array);
for($i = 0; $i < 25; $i++) {
print "Site " . $sorted_sites[$i] . " has " . $sorted_votes[$i] . " votes";
}
"I allow users to add tables to the database." - I hope all your users are benevolent and trustworthy and capable. Do you worry about people dropping or truncating tables, creating incorrect new tables that break your code, or other things like that? What kind of security do you have when users can log right into your database and change the schema?
Here's a tutorial on relational database normalization. Maybe it'll help.
Just in case someone else that comes after you wants to find what this could have looked like, here's a single table that could do what you want:
create database bookfeather;
create user bookfeather identified by 'bookfeather';
grant all on bookfeather.* to 'bookfeather'#'%';
use bookfeather;
create table if not exists book
(
id int not null auto_increment,
title varchar(255) not null default '',
upvotes integer not null default 0,
downvotes integer not null default 0,
primary key(id),
unique(title)
);
You'd vote a title up or down with an UPDATE:
update book set upvotes = upvotes + 1 where id = ?
Adding a new book is as easy as adding another row:
insert into book(title) values('grails in action')
I'd strongly urge that you reconsider.

Categories