I have an sql view which returns over 10,000 records with about 120 columns (ridiculous, I know).
The view data is gathered from the database in PHP using a Zend_Db_Table_Abstract object. We have a report controller which does a fetchAll (which obviously creates a massive array) and stores the array in a view object. Next we have a view script which iterates over the array and echoes a json formatted array.
This is slowwww. Like over 1 minute to render this report, what can I do to speed this up?
Bare in mind I am very new to this project and have no knowledge of Zend and little of PHP. Also I can't give away to much due to data protection etc.
If you could ask what you need to help you gather the picture that would be great. But basically this is what the script does:
Report controller:
Setup some basic json report data (for data tables)
$this->view->report_data = fetchALL($oSelect);
View script:
Output the basic json stuff, then:
"report_data": [
<?php
for($i = 0; $i < count($this->report_data); $i++){
echo "[";
foreach($this->columns as $k=>$col){
echo '"'. $this->escape($this->report_data[$i][$col]) .'"';
if($k < count($this->columns) -1 ){
echo ',';
}
}
echo "]";
if($i < count($this->report_data) -1){
echo ",";
}
echo"\n";
}
?> ]
So after doing some research I gathered that I could use fetch() instead of fetchAll() so to get 1 row at a time, to avoid the massive array. However I tried to use the fetch() and got this error:
Call to undefined method Zend_Db_Table_Abstract::fetch()
So now I'm bummed.
My idea was to get each row and output each row Json formatted, however I don't really know how I would do this in the report controller/ view scenario. Would I have to just get rid of the view script and just use the controller to output the data?
Anyway why does fetch() not work, I can't actually see it in the docs either, well there is some array function _fetch()
Any help would be appreciated, any if you need more info just ask.
Thanks
See the docs for fetch().
The following is not very elegant, but could help you:
Model:
...
function getResults()
{
return $db->query('SELECT * FROM bugs');
}
...
Controller:
...
$this->view->report_data = $model->getResults();
...
View:
...
while ($row = $stmt->fetch(PDO::FETCH_OBJ)) {
echo Zend_Json::encode($row);
}
...
I haven't tested the code, but you should get the idea.
You have to improvise use of framework here to gain that extra bit of performance
In controller
$this->view->result = mysql_query($sql);
In view
while ($row = mysql_fetch_assoc($this->result)) {
echo json_encode($row);
}
Note to setup mysql procedure interface read
http://php.net/manual/en/function.mysql-fetch-assoc.php
As you have realised from the error message Zend_Db_Table_Abstract does not have a fetch() method. It does, however, have a fetchRow() method that has this signature:-
/**
* Fetches one row in an object of type Zend_Db_Table_Row_Abstract,
* or returns null if no row matches the specified criteria.
*
* #param string|array|Zend_Db_Table_Select $where OPTIONAL An SQL WHERE clause or Zend_Db_Table_Select object.
* #param string|array $order OPTIONAL An SQL ORDER clause.
* #param int $offset OPTIONAL An SQL OFFSET value.
* #return Zend_Db_Table_Row_Abstract|null The row results per the
* Zend_Db_Adapter fetch mode, or null if no row found.
*/
public function fetchRow($where = null, $order = null, $offset = null)
So replacing fetch() with fetchRow() and looping while !null === fetchRow() should work for you.
It is always worth delving into the code of Zend Framework to see what is on offer.
Zend Paginator may also be an option for you as it just fetches the records required to render the page.
Related
I'm trying to return the result of 3 tables being joined together for a user to download as CSV, and this is throwing the error:
Allowed memory size of 734003200 bytes exhausted
This is the query being run:
SELECT *
FROM `tblProgram`
JOIN `tblPlots` ON `tblPlots`.`programID`=`tblProgram`.`pkProgramID`
JOIN `tblTrees` ON `tblTrees`.`treePlotID`=`tblPlots`.`id`
The line of code causing the error is this:
$resultsALL=$this->db->query($fullQry);
Where $fullQry is the query shown above. When I comment out that single line, everything runs without the error. So I'm certain its not an infinite loop somewhere I'm missing.
I'm wondering how do I break up the query so that I can get the results without erroring out? The tables only have a relatively small amount of data in them right now and will be even larger eventually, so I don't think raising the memory size is a good option.
I'm using CodeIgniter/php/mysql. I can provide more code if need be...
Thank you for any direction you can advise!
Based off of: MySQL : retrieve a large select by chunks
You may also try retrieving the data in chunks by using the LIMIT clause.
Since you're using CodeIgniter 3, here is how you can go about it.
You may need to pass a different $orderBy argument#6 to the getChunk(...) method if in case your joined tables have conflicting id column names.
I.e: $this->getChunk(..., ..., ..., 0, 2000, "tblProgram.id");
Solution:
<?php
class Csv_model extends CI_Model
{
public function __construct()
{
parent::__construct();
$this->load->database();
}
public function index()
{
$sql = <<< END
SELECT *
FROM `tblProgram`
JOIN `tblPlots` ON `tblPlots`.`programID`=`tblProgram`.`pkProgramID`
JOIN `tblTrees` ON `tblTrees`.`treePlotID`=`tblPlots`.`id`
END;
$this->getChunk(function (array $chunk) {
/*
* Do something with each chunk here;
* Do something with each chunk here;
* log_message('error', json_encode($chunk));
* */
}, $this->db, $sql);
}
/*
* Processes a raw SQL query result in chunks sending each chunk to the provided callback function.
* */
function getChunk(callable $callback, $DBContext, string $rawSQL = "SELECT 1", int $initialRowOffset = 0, int $maxRows = 2000, string $orderBy = "id")
{
$DBContext->query('DROP TEMPORARY TABLE IF EXISTS chunkable');
$DBContext->query("CREATE TEMPORARY TABLE chunkable AS ( $rawSQL ORDER BY `$orderBy` )");
do {
$constrainedSQL = sprintf("SELECT * FROM chunkable ORDER BY `$orderBy` LIMIT %d, %d", $initialRowOffset, $maxRows);
$queryBuilder = $DBContext->query($constrainedSQL);
$callback($queryBuilder->result_array());
$initialRowOffset = $initialRowOffset + $maxRows;
} while ($queryBuilder->num_rows() === $maxRows);
}
}
Use getUnbufferedRow() for processing large result sets.
getUnbufferedRow()
This method returns a single result row without prefetching the whole
result in memory as row() does. If your query has more than one row,
it returns the current row and moves the internal data pointer ahead.
$query = $db->query("YOUR QUERY");
while ($row = $query->getUnbufferedRow()) {
echo $row->title;
echo $row->name;
echo $row->body;
}
For use with MySQLi you may set MySQLi’s result mode to
MYSQLI_USE_RESULT for maximum memory savings. Use of this is not
generally recommended but it can be beneficial in some circumstances
such as writing large queries to csv. If you change the result mode be
aware of the tradeoffs associated with it.
$db->resultMode = MYSQLI_USE_RESULT; // for unbuffered results
$query = $db->query("YOUR QUERY");
$file = new \CodeIgniter\Files\File(WRITEPATH.'data.csv');
$csv = $file->openFile('w');
while ($row = $query->getUnbufferedRow('array'))
{
$csv->fputcsv($row);
}
$db->resultMode = MYSQLI_STORE_RESULT; // return to default mode
Note:
When using MYSQLI_USE_RESULT all subsequent calls on the same
connection will result in error until all records have been fetched or
a freeResult() call has been made. The getNumRows() method will
only return the number of rows based on the current position of the
data pointer. MyISAM tables will remain locked until all the records
have been fetched or a freeResult() call has been made.
You can optionally pass ‘object’ (default) or ‘array’ in order to
specify the returned value’s type:
$query->getUnbufferedRow(); // object
$query->getUnbufferedRow('object'); // object
$query->getUnbufferedRow('array'); // associative array
freeResult()
It frees the memory associated with the result and deletes the result
resource ID. Normally PHP frees its memory automatically at the end of
script execution. However, if you are running a lot of queries in a
particular script you might want to free the result after each query
result has been generated in order to cut down on memory consumption.
$query = $thisdb->query('SELECT title FROM my_table');
foreach ($query->getResult() as $row) {
echo $row->title;
}
$query->freeResult(); // The $query result object will no longer be available
$query2 = $db->query('SELECT name FROM some_table');
$row = $query2->getRow();
echo $row->name;
$query2->freeResult(); // The $query2 result object will no longer be available
I seriously need help with GDS and PHP-GDS Library.
I am doing a fetch from outside google appengine using the fantastic php-gds library. So far the library works fine.
My problem is that my data fetch from GDS returns inconsistent results and I have no idea what the issue might be.
Please see code below:
<?php
//...
//...
$offset = 0;
do{
$query = "SELECT * FROM `KIND` order by _URI ASC limit 300 offset ".$offset;
$tableList=[];
$tableList = $this->obj_store->fetchAll($query);
$offset += count($tableList);
$allTables[] = $tableList;
$totalRecords = $offset;
}while(!empty($tableList));
echo $totalRecords; // i expect total records to be equal to the number of entities in the KIND.
// but the results are inconsistent. In some cases, it is correct
// but in most cases it is far less than the total records.
// I could have a KIND with 750 entities and only 721 will be returned in total.
// I could have a KIND with 900 entities and all entities will be returned.
// I could have a KIND with 4000 entities and only 1200 will be returned.
?>
Please help. Also when I run the exact same query in the cloud console I get the right entity count. (Hope this helps someone)
UPDATE
I ended up using cursors. New code below:
<?php
$query = "SELECT * FROM `KIND`";
$tableList=[];
$queryInit = $this->obj_store->query($query);
do{
$page = $this->obj_store->fetchPage(300);// fetch page.
$tableList = am($tableList,$page); //merge with existing records.
$this->obj_store->setCursor($this->obj_store->getCursor());//set next cursor to previous last cursor
}while(!empty($page)); //as long as page result is not empty.
?>
Try using a cursor instead of an offset. See the discussion of cursors (including samples in PHP) here:
https://cloud.google.com/datastore/docs/concepts/queries#datastore-cursor-paging-php
In Zend app, I use Zend\Db\TableGateway and Zend\Db\Sql to retrieve data data from MySQL database as below.
Model -
public function getCandidateEduQualifications($id)
{
$id = (int) $id;
$rowset = $this->tableGateway->select(function (Sql\Select $select) use ($id)
{
$select->where
->AND->NEST->equalTo('candidate_id', $id)
->AND->equalTo('qualification_category', 'Educational');
});
return $rowset;
}
View -
I just iterate $rowset and echo in view. But it gives error when try to echo two or more times. Single iteration works.
This result is a forward only result set, calling rewind() after
moving forward is not supported
I can solve it by loading it to another array in view. But is it the best way ? Is there any other way to handle this ?
$records = array();
foreach ($edu_qualifications as $result) {
$records[] = $result;
}
EDIT -
$resultSet->buffer(); solved the problem.
You receive this Exception because this is expected behavior. Zend uses PDO to obtain its Zend\Db\ResultSet\Resultset which is returned by Zend\Db\TableGateway\TableGateway. PDO result sets use a forward-only cursor by default, meaning you can only loop through the set once.
For more information about cursors check Wikipedia and this article.
As the Zend\Db\ResultSet\Resultset implements the PHP Iterator you can extract an array of the set using the Zend\Db\ResultSet\Resultset:toArray() method or using the iterator_to_array() function. Do be careful though about using this function on potentially large datasets! One of the best things about cursors is precisely that they avoid bringing in everything in one go, in case the data set is too large, so there are times when you won't want to put it all into an array at once.
Sure, It looks like when we use Mysql and want to iterate $resultSet, this error will happen, b/c Mysqli only does
forward-moving result sets (Refer to this post: ZF2 DB Result position forwarded?)
I came across this problem too. But when add following line, it solved:
$resultSet->buffer();
but in this mentioned post, it suggest use following line. I just wonder why, and what's difference of them:
$resultSet->getDataSource()->buffer();
This worked for me.
public function fetchAll()
{
$select = $this->tableGateway->getSql()->select();
$resultSet = $this->tableGateway->selectWith($select);
$resultSet->buffer();
$resultSet->next();
return $resultSet;
}
$sql = new Zend\Db\Sql($your_adapter);
$select = $sql->select('your_table_name');
$statement = $sql->prepareStatementForSqlObject($select);
$results = $statement->execute();
$resultSet = new ResultSet();
$resultSet->initialize($results);
$result = $resultSet->toArray();
as the question states.
I have implemented a function wherein it fetches multiple rows from a database (*1) and then (*2) instantiate each row as an Object. After instantiation, the Objects are then stored into an array and then return the result to caller function and then iterates through the result array and displays each Object and add html formatting for each.
Here is the snippet of the code:
function find_all() {
//(*1) Fetch 30 comments from DB
$sql = 'SELECT * FROM comments';
$sql .= ' ORDER BY datetime DESC LIMIT 30';
return find_by_sql($sql);
}
function find_by_sql($sql='') {
global $database;
$result_set = $database->query($sql);
$object_array = array();
while($row = $database->fetch_array($result_set)) {
//(*2) Instantiate each row to a Comment object
// and then stores each comment to an object array
$object_array[] = Comment::instantiate($row);
}
return $object_array;
}
//(*3) Format and display each result.
$comments = find_all();
foreach ( $comments as $comment ) {
// Not sure if syntax is correct.. anyhow..
echo "<li>$comment->get_text()</li>";
}
However, I like the above approach since it's cleaner, easier to read, maintainable, and more OOP. But the problem is it takes a longer time to display than just simply iterating through each result than display each result once it's fetched, like so:
while ($row = mysql_fetch_array($sql)) {
echo "<li>$row['text']</li>";
}
I know the reason behind why it is slow. What I want to know is there a better way to solve the problem above?
While caching might be a good idea, it won't help because I need an updated list every time the list is fetched.
I think it can be a little faster if the script gets only the part you are interested in the result set, because fetch_array() returns 2 arrays with the same result set: associative, and numeric.
By adding MYSQLI_ASSOC (if you use mysqli): mysqli_fetch_array($result, MYSQLI_ASSOC), or try with mysql_fetch_assoc(), the script receives only the associative array.
You can test in pmpMyAdmin to see the diferences.
I'm using a concrete implementation of Zend_Db_Table_Abstract:
class DB_TestClass extends Zend_Db_Table_Abstract {
protected $_name = "test.TestData";
}
If I want select all rows in the table, I seem to have one option:
$t = new DB_TestClass;
$rowset = $t->fetchAll();
This returns an instance of Zend_Db_Table_Rowset which has an iterable interface you can loop though and access each row entry as a rowClass instance:
foreach($rowset as $row) {
var_dump($row);
}
HOWEVER, the rowset has loaded every row from the database into memory(!) On small tables this is OK, but on large tables - thousands of rows, for example - it quickly exhausts the memory available to PHP and the script dies.
How can I, using Zend_Db, iterate through the result set retrieving one row from a statement handle at a time, ala mysql_fetch_assoc()? This would allow efficient access to any number of rows (thousands, millions) without using excessive memory.
Thanks in advance for any suggestions.
Then fetchAll () is not capable of what you want.
You need to use Zend_Db_Select to get all the records and then do what you've intended through the loop
$select = new Zend_Db_Select ($this->getFrontController()->getParam('bootstrap')->getResource ('Db'));
$statement = $select->from ('verses')->query ();
$statement->execute ();
while ($row = $statement->fetch ())
{
// try something like that
$db_row = new Zend_Db_Table_Row (array ('table' => $table, 'data' => $row));
$db_row->text = $db_row->text . '_';
$db_row->save ();
}
Can you please specify the purpose for fetching all the rows together? Cause if you want to do some kind of processing on all the rows in the table then you any which ways have to get all the rows in to the memory. On the other hand if you want to do something like pagination, you can always use zend_pagination object which will just fetch the limited no. of rows at one time. Or better still you can set the offset and no. of rows to be fetched in the fetchAll function itself.