PHP - Create XML from multiple MySQL queries and sort by date - php

I have 10-20 log-tables in a MySQL database. Each table contains 50-100.000 rows. I need to export these to XML and sort them by creation date.
Union is a good option as the tables doesn't contain the same columns (one table might contain 3 column, and another 30 columns).
This is how I create the XML:
// Events
$stmt = $db->query("
SELECT id, columnX, created
FROM table1
");
$row_count = $stmt->rowCount();
if ($row_count != '0') {
while($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$event = $xml->createElement("event");
$events->appendChild($event);
$event->appendChild($xml->createElement("ID", "XXXX"));
$event->appendChild($xml->createElement("columnX", $row['columnX']));
$event->appendChild($xml->createElement("created", $row['created']));
}
}
// Other events
$stmt = $db->query("
SELECT id, columnY1, columnY2, columnY3, created
FROM table2
");
$row_count = $stmt->rowCount();
if ($row_count != '0') {
while($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$event = $xml->createElement("event");
$events->appendChild($event);
$event->appendChild($xml->createElement("ID", "XXXX"));
$event->appendChild($xml->createElement("columnY1", $row['columnY1']));
$event->appendChild($xml->createElement("columnY2", $row['columnY2']));
$event->appendChild($xml->createElement("columnY3", $row['columnY3']));
$event->appendChild($xml->createElement("created", $row['created']));
}
}
Anyone got an idea of how to solve this?

I suggest using an INSERT INTO ... SELECT ... UNION ... SELECT construct to fetch all the data into a (temporary) table. INSERT INTO ... SELECT allows you to directly insert the result of an select into a table. UNION allows you to concat SELECT results. Because it is a database statement it all happens in the DBMS.
After that use a select to fetch the data ordered by date field and use XMLWriter to create the XML.

If there is possibility to sort all queries, you are able to sort final XML by getting all queries from database and then printing out them like in code bellow.
Be aware, that this code WILL probably consume as much memory as data returned by all queries in one time, because you cannot use unbuffered query in this case. I don't know, how big are datasets, you are talking about.
If memory would be your concern, you can use same algorithm to combine any data source. So you can prepare three XML files (per query) and combine these instead of combining SQL. It would be (in combination with mysql unbuffered queries) probably better variant for memory usage, but slower as you will need generate and parse XML.
// convert queries to generator
function processQuery(mysqli $db, $sql) {
$q = $db -> query($sql);
while ($row = $q -> fetch_assoc()) {
// just yield
yield $row;
}
}
// prepare all queries
$queries = [
processQuery($db, "SELECT id, columnX, created FROM table1 ORDER BY created"),
processQuery($db, "SELECT id, columnY1, columnY2, columnY3, created FROM table2 ORDER BY created"),
processQuery($db, "SELECT id, created FROM table3 ORDER BY created"),
];
// run all queries and fetch first row
foreach ($queries as $query) {
$query -> next(); // see \Generator
}
// now, we will run while any query has rows (generator returns key)
while (array_filter(array_map(function(Generator $query) { return $query -> key(); }, $queries))) {
// now we have to find query, which next row has minimal date
$minTimestamp = NULL;
$queryWithMin = NULL;
foreach ($queries as $queryId => $query) {
$current = $query -> current();
if ($current !== FALSE) {
if ($minTimestamp === NULL || $minTimestamp > $current['created']) {
// this query has row with lower date than previous queries
$minTimestamp = $current['created'];
$queryWithMin = $queryId;
}
}
}
// we now know, which query returns row with minimal date
PRINT_TO_XML($queries[$queryWithMin] -> current());
// move cursor of this query to next row
$queries[$queryWithMin] -> next();
}
Another aproach could be MySQL UNION only for getting ids (already sorted) and then process them in batches.
$q = $db -> query("SELECT 'table1' AS tableName, id, created FROM table1
UNION ALL SELECT 'table2' AS tableName, id, created FROM table2
UNION ALL SELECT 'table3' AS tableName, id, created FROM table3
ORDER BY created");
$sorter = [];
while ($row = $q -> fetch_assoc()) {
$sorter []= [$row['tableName'], $row['id']];
}
foreach (array_chunk($sorter, 5000) as $dataChunk) {
// get ids from each table
$table1Ids = array_map(function($rowInfo) { return $rowInfo[1]; }, array_filter($dataChunk, function($rowInfo) { return $rowInfo[0] === 'table1'; }));
$table2Ids = array_map(function($rowInfo) { return $rowInfo[1]; }, array_filter($dataChunk, function($rowInfo) { return $rowInfo[0] === 'table2'; }));
$table3Ids = array_map(function($rowInfo) { return $rowInfo[1]; }, array_filter($dataChunk, function($rowInfo) { return $rowInfo[0] === 'table3'; }));
// load full data from each table
$dataTable1 = [];
$q = $db -> query("SELECT * FROM table1 WHERE id IN (".implode(",", $table1Ids).")");
while ($row = $q -> fetch_assoc()) {
$dataTable1[$row['id']] = CREATE_XML($row);
}
// ... same with table2
// ... same with table3
// store
foreach ($dataChunk as $row) {
if ($row[0] === 'table1') {
echo $dataTable1[$row[1]];
}
if ($row[1] === 'table1') {
echo $dataTable2[$row[1]];
}
if ($row[2] === 'table1') {
echo $dataTable3[$row[1]];
}
}
}
This approach is less memory consuming, but in this exact code, you will need to load all IDs to memory first. It's possible to simple rewrite to generate XML in first loop (if count($sorter) > 5000 { printXmlForIds($sorter); $sorter = []; }) and algorithm would not exceed memory limt.

Related

PHP: While loop only running once, should run many times

First time tackling a project where i'm needing to pull data from one table (lets say 200 records/results), and based on the results of a certain column within that result set, i need to query one of my 5 other tables (which table i need to query isnt defined until i've made the first query)
I'm currently under the impression there is no way for me to use a JOIN of some kind to do this as i cannot know which table i need to join before the first set of results have come back.
so the solution i came up with was as follows (example code for simplicity sake)
$FirstTableVals = array();
$sql = ("SELECT * FROM TABLE_A");
$run = $con->query($sql);
if($run->num_rows > 0)
{
while($row = $run->fetch_assoc())
{
foreach($row as $key => $value)
{
$FirstTableVals[$key] = $value;
}
$valueToSwitch = $FirstTableVals["VAL_TO_SWITCH"];
//$SecondTable can be 1 of 5 different table names
$SecondTable = $FirstTableVals["SECOND_TABLE_TO_QUERY"];
switch ($valueToSwitch)
{
case"1":
$sql = ("SELECT * FROM $SecondTable WHERE SOME_COLUMN = SOME_VALUE");
$run = $con->query($sql);
if($run->num_rows > 0)
{
while($row = $run->fetch_assoc())
{
//save some values from the second table
}
}
//echo the results of TABLE_A and second table
break;
case"2":
$sql = ("SELECT * FROM $SecondTable WHERE SOME_OTHER_COLUMN = SOME_OTHER_VALUE");
$run = $con->query($sql);
if($run->num_rows > 0)
{
while($row = $run->fetch_assoc())
{
//save some values from the second table
}
}
//echo the results of TABLE_A and second table
break;
default:
break;
}
}
}
Now, the problem i'm running into is that once one of the "Second" sql queries is executed, after performing everything within the "Second" While loop, it will break out of the while loop its in and echo my values but stops there without breaking out of the switch statement and then running again, due to the "First" sql queries loop.
Essentially, this only seems to run for the first record inside of "TABLE_A" as opposed to looping again and executing the switch statement with "Second" sql queries for each record inside of "TABLE_A".
If any of this doesn't make any sense, please let me know and i'll do my best to elaborate on anything that may be confusing.
Really stumped with this one as it seems to me that should run as i've intended.
You are overridding the run variable, thats why it breaks the loop. Please change your code like this:
$FirstTableVals = array();
$sql = ("SELECT * FROM TABLE_A");
$run1 = $con->query($sql);
if($run1->num_rows > 0)
{
while($row = $run1->fetch_assoc())
{
foreach($row as $key => $value)
{
$FirstTableVals[$key] = $value;
}
$valueToSwitch = $FirstTableVals["VAL_TO_SWITCH"];
//$SecondTable can be 1 of 5 different table names
$SecondTable = $FirstTableVals["SECOND_TABLE_TO_QUERY"];
switch ($valueToSwitch)
{
case"1":
$sql = ("SELECT * FROM $SecondTable WHERE SOME_COLUMN = SOME_VALUE");
$run2 = $con->query($sql);
if($run2->num_rows > 0)
{
while($row = $run2->fetch_assoc())
{
//save some values from the second table
}
}
//echo the results of TABLE_A and second table
break;
case"2":
$sql = ("SELECT * FROM $SecondTable WHERE SOME_OTHER_COLUMN = SOME_OTHER_VALUE");
$run3 = $con->query($sql);
if($run3->num_rows > 0)
{
while($row = $run3->fetch_assoc())
{
//save some values from the second table
}
}
//echo the results of TABLE_A and second table
break;
default:
break;
}
}
}

How do I structure an array in this desired format that is run inside a foreach() loop?

In the following scenario, $communityPlayerIds is an array of the Id's of people in a community, and $noPlayers is the count of that array.
e.g
$communityPlayerIds = [2,5,6]
$noPlayers = 3
The following function should do the following:
Run an sql query for the number of times represented by $noPlayers, each time retrieving the desired data of a different $communityPlayerId.
At the moment this is creating one new array, players of 24 items, 8 for each player.
public function getCommunityForm($communityId, $noPlayers, $communityPlayersIds){
$returnValue = array();
$i = 0;
foreach ($communityPlayersIds as $cPI){
$sql = " SELECT player1_result, player1_name, date , results_id FROM `results` WHERE player1_id = '".$cPI."' AND community_id = '".$communityId."' UNION ALL SELECT player2_result, player2_name,date, results_id FROM `results` WHERE player2_id = '".$cPI."' AND community_id = '".$communityId."' ORDER BY date DESC Limit 8";
$result = $this->conn->query($sql);
if (mysqli_num_rows($result) === 0) {
$returnValue[] = ['status' => "nil"];
}
if($result != null && (mysqli_num_rows($result) >= 1)){
while($row = $result -> fetch_array(MYSQLI_ASSOC)){
if(!empty($row)){
$returnValue['players'][$i] = $row;
$i++;
}
}
}
}
return $returnValue;
}
What I want is to return a single array, that has within it 3 separate arrays, 1 for each query run.
How do I do this?
Use two separate counters. Use the $i counter for the queries, and another counter for the rows of each query.
In our code, move the increment of $i to the end of the foreach loop, so it gets incremented only one time each pass through that outer loop.
$i = 0
foreach ($communityPlayersIds as $cPI){
$sql = "...";
// process each query
$i++;
}
Within the body of the foreach loop, when you process the rows returned by a query, use another counter for the rows. Initialize before the loop, and increment as the last step in the loop.
And add another dimension to your result array
$rn = 0;
while($row = $result->fetch_array(MYSQLI_ASSOC)){
//
$returnValue['players'][$i][$rn] = ... ;
rn++;
}
EDIT
As Paul Spiegel notes, the $rn counter isn't strictly necessary. An assignment to an array using empty square brackets will add a new element to an array.
while($row = $result->fetch_array(MYSQLI_ASSOC)){
//
$returnValue['players'][$i][] = ... ;
}
First thing I did was simplify your database query (well, I at least made it more efficient) by getting rid of the UNION with the second query.
Next, I set up a prepared statement. This reduces all the overhead of repeated queries to the database. It's been a few years since I worked with mysqli but I believe it should be working, as long as your ID columns are all numbers. I would hope so, but your original code had quotes around them. If they're strings, change iiiii to sssss, and seriously reconsider your database schema (more on that below.)
You don't need a counter since you already have the player ID, just use that as the array index.
public function getCommunityForm($communityId, $noPlayers, $communityPlayersIds){
$sql = " SELECT IF(player1_id=?, player1_result, player2_result) AS result, IF(player1_id=?, player1_name, player2_name) AS name, date, results_id FROM `results` WHERE (player1_id=? OR player2_id=?) AND community_id=? ORDER BY date DESC Limit 8";
$stmt = $this->conn->prepare($sql);
foreach ($communityPlayersIds as $cPI) {
$stmt->bind_param("iiiii", $cPI, $cPI, $cPI, $cPI, $communityId);
$stmt->execute();
if ($result = $stmt->get_result()) {
while($row = $result->fetch_array(MYSQLI_ASSOC)){
$returnValue['players'][$cPI][] = $row;
}
}
}
}
return $returnValue;
}
And for free, here's the PDO version. I'd strongly recommending looking into PDO. It's more modern and less verbose than mysqli. You'll notice no binding of parameters, we get to used named parameters, and getting an array out of it is much easier.
public function getCommunityForm($communityId, $noPlayers, $communityPlayersIds){
$sql = " SELECT IF(player1_id=:pid, player1_result, player2_result) AS result, IF(player1_id=:pid, player1_name, player2_name) AS name, date, results_id FROM `results` WHERE (player1_id=:pid OR player2_id=:pid) AND community_id=:cid ORDER BY date DESC Limit 8";
$stmt = $this->conn->prepare($sql);
foreach ($communityPlayersIds as $cPI) {
if ($stmt->execute([":pid"=>$cPI, ":cid"=>$communityID])) {
$returnValue['players'][$cPI] = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
return $returnValue;
}
As for your database schema, you should not have a column for player names in your result table. How many times are names repeated in that table? What if a user wanted to change their name? You should instead have a player table, and then use a join to pull in their details.

Create single PHP Function for similar queries but with different WHERE clauses

I'm not sure if this is doable or not, and I'm not entirely sure how to search for this. I have several dynamic web pages that all link to the same MySQL database table, but pull different results. So for example, a dynamic web page with ID = 5 will run a query like:
SELECT * FROM myTable WHERE category1 = 1
The web page where ID = 7 will run:
SELECT * FROM myTable WHERE category2 = 1
And so on. The queries are all grabbing the data from the same table, but the WHERE clause is different for each query - its not looking at the same column. The page with ID 7 should ONLY be returning results where category2 = 1, and ignoring the results that would be returned for the page with id = 5. My website has about 20 different pages/queries like this which is why I'm looking to see if it can be done in a function instead.
Is there a way I can put that into a function, and if so, how would I set up the parameters correctly? Or is this an instance where I will have to just write out all the queries separately on each page?
function find_results(what to put here?) {
global $connection;
$query = "SELECT * FROM myTable WHERE (how to code this part?)";
$result = mysqli_query($connection, $query);
confirm_query ($result);
return $result;
}
You would add the necessary parameters to your functions argument list, then provide the values at runtime.
function find_results($column, $value)
{
global $connection;
$query = "SELECT * FROM myTable WHERE {$column} = $value";
$result = mysqli_query($connection, $query);
confirm_query ($result);
return $result;
}
//Usage:
$result = find_results("category2", 1)
If the value you are returning records by ever ends up being a string make sure your wrap $value in single quotes.
if its a constant relation between pageId and categoryId, you can just create an array to hold it indexed by pageId like:
$pageIdToCategoryMapping = [
1 => 'cateogory1',
2 => 'category5',
...
]
and then just use it to pass data to your function like
find_results($pageIdToCategoryMapping[$pageId])
function find_results($category) {
(...)
$query = "SELECT * FROM myTable WHERE ({$category} = 1)";
(...)
}
I have been using class and object methods for mysql operations. source code available in github
I would recommend you to pass array as an argument and can return query or result as array in format you required. And this function will work any number or condition
<?php
$arg['db']="database";
$arg['tabe']="table";
$arg['search']['id1']="id1";
$arg['search']['id2']="id2";
//
function searchAndReturnResultAsArray($arg)
{
$return = NULL;
$query="SELECT * FROM ".$arg['table'];
$flag=false;
foreach($arg['search'] as $key=>$value)
{
if($flag)
$query.=" AND ";
else
$flag=true;
$query.= $key." = '".$value."' ";
}
$row = mysqli_num_rows($query);
$field = mysqli_fetch_object($query);
if($row >= 1)
{
while($data = mysqli_fetch_array())
{
$return[] = $data;
}
}
return $return;
}
?>
Or alternatively you can just return query once it is ready.

PHP check if ID already is in use

I am currently busy with creating my own 6 player game based on PHP/MySQL. I want to check if the ID from players_online is already in use, if yes then it need to keep searching until it found one (till max 6) else it need to return 0;
$con is the connection to the database and you can find the settings on the .mysql_config.php
This is what I got:
function find_local_id()
{
include'./mysql_config.php';
$i=1;
$select_id=mysqli_fetch_assoc(mysqli_query($con, "SELECT `ID` FROM `players_online` WHERE ID = ".$i.""));
if ($select_id["ID"] == $i) {
$i++;
$select_id=mysqli_fetch_assoc(mysqli_query($con, "SELECT `ID` FROM `players_online` WHERE ID = ".$i.""));
if ($select_id["ID"] == $i) {
$i++;
$select_id=mysqli_fetch_assoc(mysqli_query($con, "SELECT `ID` FROM `players_online` WHERE ID = ".$i.""));
if ($select_id["ID"] == $i) {
$i++;
$select_id=mysqli_fetch_assoc(mysqli_query($con, "SELECT `ID` FROM `players_online` WHERE ID = ".$i.""));
if ($select_id["ID"] == $i) {
$i++;
$select_id=mysqli_fetch_assoc(mysqli_query($con, "SELECT `ID` FROM `players_online` WHERE ID = ".$i.""));
if ($select_id["ID"] == $i) {
$i++;
$select_id=mysqli_fetch_assoc(mysqli_query($con, "SELECT `ID` FROM `players_online` WHERE ID = ".$i.""));
if ($select_id["ID"] == $i) {
return 0;
} else {
return 6;
}
} else {
return 5;
}
} else {
return 4;
}
} else {
return 3;
}
} else {
return 2;
}
} else {
return 1;
}
Is there a way to have this smaller/compact? Because I think this is pretty big for just a small function. :)
I think this would work:
function find_local_id()
{
include'./mysql_config.php';
$result = mysqli_query($con, "SELECT `ID` FROM `players_online`");
while($row = mysqli_fetch_assoc($result)) {
$ids[] = $row['ID'];
}
if($available = array_diff(range(0, 6), $ids)) {
return min($available);
}
return 0;
}
If you have mysqli_fetch_all use that.
Get all of the IDs in the table
See which ones (1-6) are not in the results
If any are available return the lowest one
If not return 0
First of all, you could use a loop to do this (but you don't need one).
You're hitting the DB many times unnecessarily.
Create a function or query (like the one below) that accepts one integer id and returns the count of rows.
<?php
$db = new PDO('mysql:host=localhost;dbname=yourdb', 'user', 'pass');
$id = ''; //input, whether it be a param from a function, session superglobal, GET or POST.
$exists = $db->prepare('SELECT id FROM players_online WHERE id = ?');
$exists->execute(array($id));
echo $exists->rowCount(); // if greater than 1 or equal, it exists.
You'd be far better off doing something like
$sql = "SELECT * FROM players_online WHERE id BETWEEN {$i} AND {$i}+6";
$stmt = mysqli_query($con, $sql) or die(mysqli_error($con));
$in_use = mysqli_num_rows($stmt);
If all 6 IDs are in the database, then you'll get 6 rows in $in_use. If 5 show up, then you'll get 5 rows, etc...
You seem to have a really flawed design in that you are never going to scale for more than one game at at time with this approach, are going to need to delete rows from your table to make it work when generating a new game, and are going to have problems with race conditions in cases where concurrency is high. But assuming you have a means for managing inserting player record id's in order (1,2,3, etc.). Your query can simply be:
SELECT MAX(id) FROM players_online
This will tell you the current highest id in the table. You would simply insert the next player with the id value incremented by 1 above the current max value.

Group the output of PDO query

I have a PDO query in which I am doing inner join on two tables and extracting the columns required by me.
Now these two columns are - status, script.
Status can be - passed, failed, incomplete and
script - scripts/testSuite/layer3Features/ManualStaticRouting/manualStaticRoutes.tcl , scripts/testSuite/hostAgentFeatures/dhcp/DHCP IPv6/RelayBBasicFunc/ipv6DhcpRelayEnableDhcpv6RelayGlobally.tcl
the output of --
while($row = $getFamily->fetch(PDO::FETCH_ASSOC))
{
foreach($row as $key)
{
print_r($row);
}
is -
http://pastebin.com/WqcibEyp
the full query is -
$testType = 'TCL';
$getFamily = $conn->prepare('SELECT testcases2.script, results3.status FROM testcases2 INNER JOIN results3 ON testcases2.testcaseID = results3.testcaseID
WHERE testType = :testType');
$getFamily->execute(array(':testType' => $testType));
while($row = $getFamily->fetch(PDO::FETCH_ASSOC))
{
foreach($row as $key)
{
print_r($row);
$family[] = $key;
}
}
Now here what I want to do is, read the second location of each script (considering scripts at 0) and group all the values which have same stuff in the third location, with status together.
How this can be done.
Please guide.
You need to group your script and status like this,
while($row = $getFamily->fetch(PDO::FETCH_ASSOC))
{
$status[] = $row['status'];
$scripts[] = $row['script'];
}
print_r($scripts);
print_r($status);

Categories