PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 1.3.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v1.3.1
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / DataAccess / LogAggregator.php
matomo / app / core / DataAccess Last commit date
LogQueryBuilder 6 years ago Actions.php 6 years ago ArchiveSelector.php 6 years ago ArchiveTableCreator.php 6 years ago ArchiveTableDao.php 6 years ago ArchiveWriter.php 6 years ago ArchivingDbAdapter.php 6 years ago LogAggregator.php 5 years ago LogQueryBuilder.php 6 years ago LogTableTemporary.php 6 years ago Model.php 6 years ago RawLogDao.php 6 years ago TableMetadata.php 6 years ago
LogAggregator.php
1209 lines
1 <?php
2 /**
3 * Piwik - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
8 */
9 namespace Piwik\DataAccess;
10
11 use Piwik\ArchiveProcessor\ArchivingStatus;
12 use Piwik\ArchiveProcessor\Parameters;
13 use Piwik\Common;
14 use Piwik\Concurrency\Lock;
15 use Piwik\Config;
16 use Piwik\Container\StaticContainer;
17 use Piwik\DataArray;
18 use Piwik\Date;
19 use Piwik\Db;
20 use Piwik\DbHelper;
21 use Piwik\Metrics;
22 use Piwik\Period;
23 use Piwik\Piwik;
24 use Piwik\Plugin\LogTablesProvider;
25 use Piwik\Segment;
26 use Piwik\Segment\SegmentExpression;
27 use Piwik\Tracker\GoalManager;
28 use Psr\Log\LoggerInterface;
29
30 /**
31 * Contains methods that calculate metrics by aggregating log data (visits, actions, conversions,
32 * ecommerce items).
33 *
34 * You can use the methods in this class within {@link Piwik\Plugin\Archiver Archiver} descendants
35 * to aggregate log data without having to write SQL queries.
36 *
37 * ### Aggregation Dimension
38 *
39 * All aggregation methods accept a **dimension** parameter. These parameters are important as
40 * they control how rows in a table are aggregated together.
41 *
42 * A **_dimension_** is just a table column. Rows that have the same values for these columns are
43 * aggregated together. The result of these aggregations is a set of metrics for every recorded value
44 * of a **dimension**.
45 *
46 * _Note: A dimension is essentially the same as a **GROUP BY** field._
47 *
48 * ### Examples
49 *
50 * **Aggregating visit data**
51 *
52 * $archiveProcessor = // ...
53 * $logAggregator = $archiveProcessor->getLogAggregator();
54 *
55 * // get metrics for every used browser language of all visits by returning visitors
56 * $query = $logAggregator->queryVisitsByDimension(
57 * $dimensions = array('log_visit.location_browser_lang'),
58 * $where = 'log_visit.visitor_returning = 1',
59 *
60 * // also count visits for each browser language that are not located in the US
61 * $additionalSelects = array('sum(case when log_visit.location_country <> 'us' then 1 else 0 end) as nonus'),
62 *
63 * // we're only interested in visits, unique visitors & actions, so don't waste time calculating anything else
64 * $metrics = array(Metrics::INDEX_NB_UNIQ_VISITORS, Metrics::INDEX_NB_VISITS, Metrics::INDEX_NB_ACTIONS),
65 * );
66 * if ($query === false) {
67 * return;
68 * }
69 *
70 * while ($row = $query->fetch()) {
71 * $uniqueVisitors = $row[Metrics::INDEX_NB_UNIQ_VISITORS];
72 * $visits = $row[Metrics::INDEX_NB_VISITS];
73 * $actions = $row[Metrics::INDEX_NB_ACTIONS];
74 *
75 * // ... do something w/ calculated metrics ...
76 * }
77 *
78 * **Aggregating conversion data**
79 *
80 * $archiveProcessor = // ...
81 * $logAggregator = $archiveProcessor->getLogAggregator();
82 *
83 * // get metrics for ecommerce conversions for each country
84 * $query = $logAggregator->queryConversionsByDimension(
85 * $dimensions = array('log_conversion.location_country'),
86 * $where = 'log_conversion.idgoal = 0', // 0 is the special ecommerceOrder idGoal value in the table
87 *
88 * // also calculate average tax and max shipping per country
89 * $additionalSelects = array(
90 * 'AVG(log_conversion.revenue_tax) as avg_tax',
91 * 'MAX(log_conversion.revenue_shipping) as max_shipping'
92 * )
93 * );
94 * if ($query === false) {
95 * return;
96 * }
97 *
98 * while ($row = $query->fetch()) {
99 * $country = $row['location_country'];
100 * $numEcommerceSales = $row[Metrics::INDEX_GOAL_NB_CONVERSIONS];
101 * $numVisitsWithEcommerceSales = $row[Metrics::INDEX_GOAL_NB_VISITS_CONVERTED];
102 * $avgTaxForCountry = $row['avg_tax'];
103 * $maxShippingForCountry = $row['max_shipping'];
104 *
105 * // ... do something with aggregated data ...
106 * }
107 */
108 class LogAggregator
109 {
110 const LOG_VISIT_TABLE = 'log_visit';
111
112 const LOG_ACTIONS_TABLE = 'log_link_visit_action';
113
114 const LOG_CONVERSION_TABLE = "log_conversion";
115
116 const REVENUE_SUBTOTAL_FIELD = 'revenue_subtotal';
117
118 const REVENUE_TAX_FIELD = 'revenue_tax';
119
120 const REVENUE_SHIPPING_FIELD = 'revenue_shipping';
121
122 const REVENUE_DISCOUNT_FIELD = 'revenue_discount';
123
124 const TOTAL_REVENUE_FIELD = 'revenue';
125
126 const ITEMS_COUNT_FIELD = "items";
127
128 const CONVERSION_DATETIME_FIELD = "server_time";
129
130 const ACTION_DATETIME_FIELD = "server_time";
131
132 const VISIT_DATETIME_FIELD = 'visit_last_action_time';
133
134 const IDGOAL_FIELD = 'idgoal';
135
136 const FIELDS_SEPARATOR = ", \n\t\t\t";
137
138 const LOG_TABLE_SEGMENT_TEMPORARY_PREFIX = 'logtmpsegment';
139
140 /** @var \Piwik\Date */
141 protected $dateStart;
142
143 /** @var \Piwik\Date */
144 protected $dateEnd;
145
146 /** @var int[] */
147 protected $sites;
148
149 /** @var \Piwik\Segment */
150 protected $segment;
151
152 /**
153 * @var string
154 */
155 private $queryOriginHint = '';
156
157 /**
158 * @var LoggerInterface
159 */
160 private $logger;
161
162 /**
163 * @var bool
164 */
165 private $isRootArchiveRequest;
166
167 /**
168 * @var bool
169 */
170 private $allowUsageSegmentCache = false;
171
172 /**
173 * Constructor.
174 *
175 * @param \Piwik\ArchiveProcessor\Parameters $params
176 */
177 public function __construct(Parameters $params, LoggerInterface $logger = null)
178 {
179 $this->dateStart = $params->getDateTimeStart();
180 $this->dateEnd = $params->getDateTimeEnd();
181 $this->segment = $params->getSegment();
182 $this->sites = $params->getIdSites();
183 $this->isRootArchiveRequest = $params->isRootArchiveRequest();
184 $this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface');
185 }
186
187 public function setSites($sites)
188 {
189 $this->sites = array_map('intval', $sites);
190 }
191
192 public function getSites()
193 {
194 return $this->sites;
195 }
196
197 public function getSegment()
198 {
199 return $this->segment;
200 }
201
202 public function setQueryOriginHint($nameOfOrigiin)
203 {
204 $this->queryOriginHint = $nameOfOrigiin;
205 }
206
207 public function getSegmentTmpTableName()
208 {
209 $bind = $this->getGeneralQueryBindParams();
210 $tableName = self::LOG_TABLE_SEGMENT_TEMPORARY_PREFIX . md5(json_encode($bind) . $this->segment->getString());
211
212 $lengthPrefix = Common::mb_strlen(Common::prefixTable(''));
213 $maxLength = Db\Schema\Mysql::MAX_TABLE_NAME_LENGTH - $lengthPrefix;
214
215 return Common::mb_substr($tableName, 0, $maxLength);
216 }
217
218 public function cleanup()
219 {
220 if (!$this->segment->isEmpty() && $this->isSegmentCacheEnabled()) {
221 $segmentTable = $this->getSegmentTmpTableName();
222 $segmentTable = Common::prefixTable($segmentTable);
223
224 if ($this->doesSegmentTableExist($segmentTable)) {
225 // safety in case an older MySQL version is used that does not drop table at the end of the connection
226 // automatically. also helps us release disk space/memory earlier when multiple segments are archived
227 $this->getDb()->query('DROP TEMPORARY TABLE IF EXISTS ' . $segmentTable);
228 }
229
230 $logTablesProvider = $this->getLogTableProvider();
231 if ($logTablesProvider->getLogTable($segmentTable)) {
232 $logTablesProvider->setTempTable(null); // no longer available
233 }
234 }
235 }
236
237 private function doesSegmentTableExist($segmentTablePrefixed)
238 {
239 try {
240 // using DROP TABLE IF EXISTS would not work on a DB reader if the table doesn't exist...
241 $this->getDb()->fetchOne('SELECT 1 FROM ' . $segmentTablePrefixed . ' LIMIT 1');
242 $tableExists = true;
243 } catch (\Exception $e) {
244 $tableExists = false;
245 }
246
247 return $tableExists;
248 }
249
250 private function isSegmentCacheEnabled()
251 {
252 if (!$this->allowUsageSegmentCache) {
253 return false;
254 }
255
256 $config = Config::getInstance();
257 $general = $config->General;
258 return !empty($general['enable_segments_cache']);
259 }
260
261 public function allowUsageSegmentCache()
262 {
263 $this->allowUsageSegmentCache = true;
264 }
265
266 private function getLogTableProvider()
267 {
268 return StaticContainer::get(LogTablesProvider::class);
269 }
270
271 private function createTemporaryTable($unprefixedSegmentTableName, $segmentSelectSql, $segmentSelectBind)
272 {
273 $table = Common::prefixTable($unprefixedSegmentTableName);
274
275 if ($this->doesSegmentTableExist($table)) {
276 return; // no need to create the table, it was already created... better to have a select vs unneeded create table
277 }
278
279 $engine = '';
280 if (defined('PIWIK_TEST_MODE') && PIWIK_TEST_MODE) {
281 $engine = 'ENGINE=MEMORY';
282 }
283
284 $tempTableIdVisitColumn = 'idvisit BIGINT(10) UNSIGNED NOT NULL';
285 $createTableSql = 'CREATE TEMPORARY TABLE ' . $table . ' (' . $tempTableIdVisitColumn . ') ' . $engine;
286 // we do not insert the data right away using create temporary table ... select ...
287 // to avoid metadata lock see eg https://www.percona.com/blog/2018/01/10/why-avoid-create-table-as-select-statement/
288
289 $readerDb = Db::getReader();
290 try {
291 $readerDb->query($createTableSql);
292 } catch (\Exception $e) {
293 if ($readerDb->isErrNo($e, \Piwik\Updater\Migration\Db::ERROR_CODE_TABLE_EXISTS)) {
294 return;
295 } elseif ($readerDb->isErrNo($e, 1173)
296 || $readerDb->isErrNo($e, 3750)
297 || stripos($e->getMessage(), 'requires a primary key') !== false
298 || stripos($e->getMessage(), 'table without a primary key') !== false) {
299 $createTableSql = str_replace($tempTableIdVisitColumn, $tempTableIdVisitColumn . ', PRIMARY KEY (`idvisit`)', $createTableSql);
300
301 try {
302 $readerDb->query($createTableSql);
303 } catch (\Exception $e) {
304 if ( $readerDb->isErrNo( $e, \Piwik\Updater\Migration\Db::ERROR_CODE_TABLE_EXISTS ) ) {
305 return;
306 } else {
307 throw $e;
308 }
309 }
310 } else {
311 throw $e;
312 }
313 }
314
315 $transactionLevel = new Db\TransactionLevel($readerDb);
316 $canSetTransactionLevel = $transactionLevel->canLikelySetTransactionLevel();
317
318 if ($canSetTransactionLevel) {
319 // i know this could be shortened to one if or one line but I want to make sure this line where we
320 // set uncomitted is easily noticable in the code as it could be missed quite easily otherwise
321 // we set uncommitted so we don't make the INSERT INTO... SELECT... locking ... we do not want to lock
322 // eg the visits table
323 if (!$transactionLevel->setUncommitted()) {
324 $canSetTransactionLevel = false;
325 }
326 }
327
328 if (!$canSetTransactionLevel) {
329 // transaction level doesn't work... we're instead executing the select individually and then insert the data
330 // this uses more memory but at least is not locking
331 $all = $readerDb->fetchAll($segmentSelectSql, $segmentSelectBind);
332 if (!empty($all)) {
333 // we're not using batchinsert since this would not support the reader DB.
334 $readerDb->query('INSERT INTO ' . $table . ' VALUES ('.implode('),(', array_column($all, 'idvisit')).')');
335 }
336 return;
337 }
338
339 $insertIntoStatement = 'INSERT INTO ' . $table . ' (idvisit) ' . $segmentSelectSql;
340 $readerDb->query($insertIntoStatement, $segmentSelectBind);
341
342 $transactionLevel->restorePreviousStatus();
343 }
344
345 public function generateQuery($select, $from, $where, $groupBy, $orderBy, $limit = 0, $offset = 0)
346 {
347 $segment = $this->segment;
348 $bind = $this->getGeneralQueryBindParams();
349
350 if (!$this->segment->isEmpty() && $this->isSegmentCacheEnabled()) {
351 // here we create the TMP table and apply the segment including the datetime and the requested idsite
352 // at the end we generated query will no longer need to apply the datetime/idsite and segment
353 $segment = new Segment('', $this->sites);
354
355 $segmentTable = $this->getSegmentTmpTableName();
356
357 $segmentWhere = $this->getWhereStatement('log_visit', 'visit_last_action_time');
358 $segmentBind = $this->getGeneralQueryBindParams();
359
360 $logQueryBuilder = StaticContainer::get('Piwik\DataAccess\LogQueryBuilder');
361 $forceGroupByBackup = $logQueryBuilder->getForcedInnerGroupBySubselect();
362 $logQueryBuilder->forceInnerGroupBySubselect(LogQueryBuilder::FORCE_INNER_GROUP_BY_NO_SUBSELECT);
363 $segmentSql = $this->segment->getSelectQuery('distinct log_visit.idvisit as idvisit', 'log_visit', $segmentWhere, $segmentBind, 'log_visit.idvisit ASC');
364 $logQueryBuilder->forceInnerGroupBySubselect($forceGroupByBackup);
365
366 $this->createTemporaryTable($segmentTable, $segmentSql['sql'], $segmentSql['bind']);
367
368 if (!is_array($from)) {
369 $from = array($segmentTable, $from);
370 } else {
371 array_unshift($from, $segmentTable);
372 }
373
374 $logTablesProvider = $this->getLogTableProvider();
375 $logTablesProvider->setTempTable(new LogTableTemporary($segmentTable));
376
377 foreach ($logTablesProvider->getAllLogTables() as $logTable) {
378 if ($logTable->getDateTimeColumn()) {
379 $whereTest = $this->getWhereStatement($logTable->getName(), $logTable->getDateTimeColumn());
380 if (strpos($where, $whereTest) === 0) {
381 // we don't need to apply the where statement again as it would have been applied already
382 // in the temporary table... instead it should join the tables through the idvisit index
383 $where = ltrim(str_replace($whereTest, '', $where));
384 if (stripos($where, 'and ') === 0) {
385 $where = substr($where, strlen('and '));
386 }
387 $bind = array();
388 break;
389 }
390 }
391
392 }
393
394 }
395
396 $query = $segment->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy, $limit, $offset);
397
398 $select = 'SELECT';
399 if ($this->queryOriginHint && is_array($query) && 0 === strpos(trim($query['sql']), $select)) {
400 $query['sql'] = trim($query['sql']);
401 $query['sql'] = 'SELECT /* ' . $this->queryOriginHint . ' */' . substr($query['sql'], strlen($select));
402 }
403
404 if (!$this->getSegment()->isEmpty() && is_array($query) && 0 === strpos(trim($query['sql']), $select)) {
405 $query['sql'] = trim($query['sql']);
406 $query['sql'] = 'SELECT /* ' . $this->dateStart->toString() . ',' . $this->dateEnd->toString() . ' sites ' . implode(',', array_map('intval', $this->sites)) . ' segmenthash ' . $this->getSegment()->getHash(). ' */' . substr($query['sql'], strlen($select));
407 }
408
409 return $query;
410 }
411
412 protected function getVisitsMetricFields()
413 {
414 return array(
415 Metrics::INDEX_NB_UNIQ_VISITORS => "count(distinct " . self::LOG_VISIT_TABLE . ".idvisitor)",
416 Metrics::INDEX_NB_UNIQ_FINGERPRINTS => "count(distinct " . self::LOG_VISIT_TABLE . ".config_id)",
417 Metrics::INDEX_NB_VISITS => "count(*)",
418 Metrics::INDEX_NB_ACTIONS => "sum(" . self::LOG_VISIT_TABLE . ".visit_total_actions)",
419 Metrics::INDEX_MAX_ACTIONS => "max(" . self::LOG_VISIT_TABLE . ".visit_total_actions)",
420 Metrics::INDEX_SUM_VISIT_LENGTH => "sum(" . self::LOG_VISIT_TABLE . ".visit_total_time)",
421 Metrics::INDEX_BOUNCE_COUNT => "sum(case " . self::LOG_VISIT_TABLE . ".visit_total_actions when 1 then 1 when 0 then 1 else 0 end)",
422 Metrics::INDEX_NB_VISITS_CONVERTED => "sum(case " . self::LOG_VISIT_TABLE . ".visit_goal_converted when 1 then 1 else 0 end)",
423 Metrics::INDEX_NB_USERS => "count(distinct " . self::LOG_VISIT_TABLE . ".user_id)",
424 );
425 }
426
427 public static function getConversionsMetricFields()
428 {
429 return array(
430 Metrics::INDEX_GOAL_NB_CONVERSIONS => "count(*)",
431 Metrics::INDEX_GOAL_NB_VISITS_CONVERTED => "count(distinct " . self::LOG_CONVERSION_TABLE . ".idvisit)",
432 Metrics::INDEX_GOAL_REVENUE => self::getSqlConversionRevenueSum(self::TOTAL_REVENUE_FIELD),
433 Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL => self::getSqlConversionRevenueSum(self::REVENUE_SUBTOTAL_FIELD),
434 Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_TAX => self::getSqlConversionRevenueSum(self::REVENUE_TAX_FIELD),
435 Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING => self::getSqlConversionRevenueSum(self::REVENUE_SHIPPING_FIELD),
436 Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT => self::getSqlConversionRevenueSum(self::REVENUE_DISCOUNT_FIELD),
437 Metrics::INDEX_GOAL_ECOMMERCE_ITEMS => "SUM(" . self::LOG_CONVERSION_TABLE . "." . self::ITEMS_COUNT_FIELD . ")",
438 );
439 }
440
441 private static function getSqlConversionRevenueSum($field)
442 {
443 return self::getSqlRevenue('SUM(' . self::LOG_CONVERSION_TABLE . '.' . $field . ')');
444 }
445
446 public static function getSqlRevenue($field)
447 {
448 return "ROUND(" . $field . "," . GoalManager::REVENUE_PRECISION . ")";
449 }
450
451 /**
452 * Helper function that returns an array with common metrics for a given log_visit field distinct values.
453 *
454 * The statistics returned are:
455 * - number of unique visitors
456 * - number of visits
457 * - number of actions
458 * - maximum number of action for a visit
459 * - sum of the visits' length in sec
460 * - count of bouncing visits (visits with one page view)
461 *
462 * For example if $dimension = 'config_os' it will return the statistics for every distinct Operating systems
463 * The returned array will have a row per distinct operating systems,
464 * and a column per stat (nb of visits, max actions, etc)
465 *
466 * 'label' Metrics::INDEX_NB_UNIQ_VISITORS Metrics::INDEX_NB_VISITS etc.
467 * Linux 27 66 ...
468 * Windows XP 12 ...
469 * Mac OS 15 36 ...
470 *
471 * @param string $dimension Table log_visit field name to be use to compute common stats
472 * @return DataArray
473 */
474 public function getMetricsFromVisitByDimension($dimension)
475 {
476 if (!is_array($dimension)) {
477 $dimension = array($dimension);
478 }
479 if (count($dimension) == 1) {
480 $dimension = array("label" => reset($dimension));
481 }
482 $query = $this->queryVisitsByDimension($dimension);
483 $metrics = new DataArray();
484 while ($row = $query->fetch()) {
485 $metrics->sumMetricsVisits($row["label"], $row);
486 }
487 return $metrics;
488 }
489
490 /**
491 * Executes and returns a query aggregating visit logs, optionally grouping by some dimension. Returns
492 * a DB statement that can be used to iterate over the result
493 *
494 * **Result Set**
495 *
496 * The following columns are in each row of the result set:
497 *
498 * - **{@link \Piwik\Metrics::INDEX_NB_UNIQ_VISITORS}**: The total number of unique visitors in this group
499 * of aggregated visits.
500 * - **{@link \Piwik\Metrics::INDEX_NB_VISITS}**: The total number of visits aggregated.
501 * - **{@link \Piwik\Metrics::INDEX_NB_ACTIONS}**: The total number of actions performed in this group of
502 * aggregated visits.
503 * - **{@link \Piwik\Metrics::INDEX_MAX_ACTIONS}**: The maximum actions perfomred in one visit for this group of
504 * visits.
505 * - **{@link \Piwik\Metrics::INDEX_SUM_VISIT_LENGTH}**: The total amount of time spent on the site for this
506 * group of visits.
507 * - **{@link \Piwik\Metrics::INDEX_BOUNCE_COUNT}**: The total number of bounced visits in this group of
508 * visits.
509 * - **{@link \Piwik\Metrics::INDEX_NB_VISITS_CONVERTED}**: The total number of visits for which at least one
510 * conversion occurred, for this group of visits.
511 *
512 * Additional data can be selected by setting the `$additionalSelects` parameter.
513 *
514 * _Note: The metrics returned by this query can be customized by the `$metrics` parameter._
515 *
516 * @param array|string $dimensions `SELECT` fields (or just one field) that will be grouped by,
517 * eg, `'referrer_name'` or `array('referrer_name', 'referrer_keyword')`.
518 * The metrics retrieved from the query will be specific to combinations
519 * of these fields. So if `array('referrer_name', 'referrer_keyword')`
520 * is supplied, the query will aggregate visits for each referrer/keyword
521 * combination.
522 * @param bool|string $where Additional condition for the `WHERE` clause. Can be used to filter
523 * the set of visits that are considered for aggregation.
524 * @param array $additionalSelects Additional `SELECT` fields that are not included in the group by
525 * clause. These can be aggregate expressions, eg, `SUM(somecol)`.
526 * @param bool|array $metrics The set of metrics to calculate and return. If false, the query will select
527 * all of them. The following values can be used:
528 *
529 * - {@link \Piwik\Metrics::INDEX_NB_UNIQ_VISITORS}
530 * - {@link \Piwik\Metrics::INDEX_NB_VISITS}
531 * - {@link \Piwik\Metrics::INDEX_NB_ACTIONS}
532 * - {@link \Piwik\Metrics::INDEX_MAX_ACTIONS}
533 * - {@link \Piwik\Metrics::INDEX_SUM_VISIT_LENGTH}
534 * - {@link \Piwik\Metrics::INDEX_BOUNCE_COUNT}
535 * - {@link \Piwik\Metrics::INDEX_NB_VISITS_CONVERTED}
536 * @param bool|\Piwik\RankingQuery $rankingQuery
537 * A pre-configured ranking query instance that will be used to limit the result.
538 * If set, the return value is the array returned by {@link \Piwik\RankingQuery::execute()}.
539 * @param bool|string $orderBy Order By clause to add (e.g. user_id ASC)
540 * @param int $timeLimitInMs Adds a MAX_EXECUTION_TIME query hint to the query if $timeLimitInMs > 0
541 *
542 * @return mixed A Zend_Db_Statement if `$rankingQuery` isn't supplied, otherwise the result of
543 * {@link \Piwik\RankingQuery::execute()}. Read {@link queryVisitsByDimension() this}
544 * to see what aggregate data is calculated by the query.
545 * @api
546 */
547 public function queryVisitsByDimension(array $dimensions = array(), $where = false, array $additionalSelects = array(),
548 $metrics = false, $rankingQuery = false, $orderBy = false, $timeLimitInMs = -1)
549 {
550 $tableName = self::LOG_VISIT_TABLE;
551 $availableMetrics = $this->getVisitsMetricFields();
552
553 $select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics, $metrics);
554 $from = array($tableName);
555 $where = $this->getWhereStatement($tableName, self::VISIT_DATETIME_FIELD, $where);
556 $groupBy = $this->getGroupByStatement($dimensions, $tableName);
557 $orderBys = $orderBy ? [$orderBy] : [];
558
559 if ($rankingQuery) {
560 $orderBys[] = '`' . Metrics::INDEX_NB_VISITS . '` DESC';
561 }
562
563 $query = $this->generateQuery($select, $from, $where, $groupBy, implode(', ', $orderBys));
564
565 if ($rankingQuery) {
566 unset($availableMetrics[Metrics::INDEX_MAX_ACTIONS]);
567
568 // INDEX_NB_UNIQ_FINGERPRINTS is only processed if specifically asked for
569 if (!$this->isMetricRequested(Metrics::INDEX_NB_UNIQ_FINGERPRINTS, $metrics)) {
570 unset($availableMetrics[Metrics::INDEX_NB_UNIQ_FINGERPRINTS]);
571 }
572
573 $sumColumns = array_keys($availableMetrics);
574
575 if ($metrics) {
576 $sumColumns = array_intersect($sumColumns, $metrics);
577 }
578
579 $rankingQuery->addColumn($sumColumns, 'sum');
580 if ($this->isMetricRequested(Metrics::INDEX_MAX_ACTIONS, $metrics)) {
581 $rankingQuery->addColumn(Metrics::INDEX_MAX_ACTIONS, 'max');
582 }
583
584 return $rankingQuery->execute($query['sql'], $query['bind'], $timeLimitInMs);
585 }
586
587 $query['sql'] = DbHelper::addMaxExecutionTimeHintToQuery($query['sql'], $timeLimitInMs);
588
589 return $this->getDb()->query($query['sql'], $query['bind']);
590 }
591
592 protected function getSelectsMetrics($metricsAvailable, $metricsRequested = false)
593 {
594 $selects = array();
595
596 foreach ($metricsAvailable as $metricId => $statement) {
597 if ($this->isMetricRequested($metricId, $metricsRequested)) {
598 $aliasAs = $this->getSelectAliasAs($metricId);
599 $selects[] = $statement . $aliasAs;
600 }
601 }
602
603 return $selects;
604 }
605
606 protected function getSelectStatement($dimensions, $tableName, $additionalSelects, array $availableMetrics, $requestedMetrics = false)
607 {
608 $dimensionsToSelect = $this->getDimensionsToSelect($dimensions, $additionalSelects);
609
610 $selects = array_merge(
611 $this->getSelectDimensions($dimensionsToSelect, $tableName),
612 $this->getSelectsMetrics($availableMetrics, $requestedMetrics),
613 !empty($additionalSelects) ? $additionalSelects : array()
614 );
615
616 $select = implode(self::FIELDS_SEPARATOR, $selects);
617 return $select;
618 }
619
620 /**
621 * Will return the subset of $dimensions that are not found in $additionalSelects
622 *
623 * @param $dimensions
624 * @param array $additionalSelects
625 * @return array
626 */
627 protected function getDimensionsToSelect($dimensions, $additionalSelects)
628 {
629 if (empty($additionalSelects)) {
630 return $dimensions;
631 }
632
633 $dimensionsToSelect = array();
634 foreach ($dimensions as $selectAs => $dimension) {
635 $asAlias = $this->getSelectAliasAs($dimension);
636 foreach ($additionalSelects as $additionalSelect) {
637 if (strpos($additionalSelect, $asAlias) === false) {
638 $dimensionsToSelect[$selectAs] = $dimension;
639 }
640 }
641 }
642
643 $dimensionsToSelect = array_unique($dimensionsToSelect);
644 return $dimensionsToSelect;
645 }
646
647 /**
648 * Returns the dimensions array, where
649 * (1) the table name is prepended to the field
650 * (2) the "AS `label` " is appended to the field
651 *
652 * @param $dimensions
653 * @param $tableName
654 * @param bool $appendSelectAs
655 * @param bool $parseSelectAs
656 * @return mixed
657 */
658 protected function getSelectDimensions($dimensions, $tableName, $appendSelectAs = true)
659 {
660 foreach ($dimensions as $selectAs => &$field) {
661 $selectAsString = $field;
662
663 if (!is_numeric($selectAs)) {
664 $selectAsString = $selectAs;
665 } else if ($this->isFieldFunctionOrComplexExpression($field)) {
666 // if complex expression has a select as, use it
667 if (!$appendSelectAs && preg_match('/\s+AS\s+(.*?)\s*$/', $field, $matches)) {
668 $field = $matches[1];
669 continue;
670 }
671
672 // if function w/o select as, do not alias or prefix
673 $selectAsString = $appendSelectAs = false;
674 }
675
676 $isKnownField = !in_array($field, array('referrer_data'));
677
678 if ($selectAsString == $field && $isKnownField) {
679 $field = $this->prefixColumn($field, $tableName);
680 }
681
682 if ($appendSelectAs && $selectAsString) {
683 $field = $this->prefixColumn($field, $tableName) . $this->getSelectAliasAs($selectAsString);
684 }
685 }
686
687 return $dimensions;
688 }
689
690 /**
691 * Prefixes a column name with a table name if not already done.
692 *
693 * @param string $column eg, 'location_provider'
694 * @param string $tableName eg, 'log_visit'
695 * @return string eg, 'log_visit.location_provider'
696 */
697 private function prefixColumn($column, $tableName)
698 {
699 if (strpos($column, '.') === false) {
700 return $tableName . '.' . $column;
701 } else {
702 return $column;
703 }
704 }
705
706 protected function isFieldFunctionOrComplexExpression($field)
707 {
708 return strpos($field, "(") !== false
709 || strpos($field, "CASE") !== false;
710 }
711
712 protected function getSelectAliasAs($metricId)
713 {
714 return " AS `" . $metricId . "`";
715 }
716
717 protected function isMetricRequested($metricId, $metricsRequested)
718 {
719 // do not process INDEX_NB_UNIQ_FINGERPRINTS unless specifically asked for
720 if ($metricsRequested === false) {
721 if ($metricId == Metrics::INDEX_NB_UNIQ_FINGERPRINTS) {
722 return false;
723 }
724 return true;
725 }
726 return in_array($metricId, $metricsRequested);
727 }
728
729 public function getWhereStatement($tableName, $datetimeField, $extraWhere = false)
730 {
731 $where = "$tableName.$datetimeField >= ?
732 AND $tableName.$datetimeField <= ?
733 AND $tableName.idsite IN (". Common::getSqlStringFieldsArray($this->sites) . ")";
734
735 if (!empty($extraWhere)) {
736 $extraWhere = sprintf($extraWhere, $tableName, $tableName);
737 $where .= ' AND ' . $extraWhere;
738 }
739
740 return $where;
741 }
742
743 protected function getGroupByStatement($dimensions, $tableName)
744 {
745 $dimensions = $this->getSelectDimensions($dimensions, $tableName, $appendSelectAs = false);
746 $groupBy = implode(", ", $dimensions);
747
748 return $groupBy;
749 }
750
751 /**
752 * Returns general bind parameters for all log aggregation queries. This includes the datetime
753 * start of entities, datetime end of entities and IDs of all sites.
754 *
755 * @return array
756 */
757 public function getGeneralQueryBindParams()
758 {
759 $bind = array($this->dateStart->toString(Date::DATE_TIME_FORMAT), $this->dateEnd->toString(Date::DATE_TIME_FORMAT));
760 $bind = array_merge($bind, $this->sites);
761
762 return $bind;
763 }
764
765 /**
766 * Executes and returns a query aggregating ecommerce item data (everything stored in the
767 * **log\_conversion\_item** table) and returns a DB statement that can be used to iterate over the result
768 *
769 * <a name="queryEcommerceItems-result-set"></a>
770 * **Result Set**
771 *
772 * Each row of the result set represents an aggregated group of ecommerce items. The following
773 * columns are in each row of the result set:
774 *
775 * - **{@link Piwik\Metrics::INDEX_ECOMMERCE_ITEM_REVENUE}**: The total revenue for the group of items.
776 * - **{@link Piwik\Metrics::INDEX_ECOMMERCE_ITEM_QUANTITY}**: The total number of items in this group.
777 * - **{@link Piwik\Metrics::INDEX_ECOMMERCE_ITEM_PRICE}**: The total price for the group of items.
778 * - **{@link Piwik\Metrics::INDEX_ECOMMERCE_ORDERS}**: The total number of orders this group of items
779 * belongs to. This will be <= to the total number
780 * of items in this group.
781 * - **{@link Piwik\Metrics::INDEX_NB_VISITS}**: The total number of visits that caused these items to be logged.
782 * - **ecommerceType**: Either {@link Piwik\Tracker\GoalManager::IDGOAL_CART} if the items in this group were
783 * abandoned by a visitor, or {@link Piwik\Tracker\GoalManager::IDGOAL_ORDER} if they
784 * were ordered by a visitor.
785 *
786 * **Limitations**
787 *
788 * Segmentation is not yet supported for this aggregation method.
789 *
790 * @param string $dimension One or more **log\_conversion\_item** columns to group aggregated data by.
791 * Eg, `'idaction_sku'` or `'idaction_sku, idaction_category'`.
792 * @return \Zend_Db_Statement A statement object that can be used to iterate through the query's
793 * result set. See [above](#queryEcommerceItems-result-set) to learn more
794 * about what this query selects.
795 * @api
796 */
797 public function queryEcommerceItems($dimension)
798 {
799 $query = $this->generateQuery(
800 // SELECT ...
801 implode(
802 ', ',
803 array(
804 "log_action.name AS label",
805 sprintf("log_conversion_item.%s AS labelIdAction", $dimension),
806 sprintf(
807 '%s AS `%d`',
808 self::getSqlRevenue('SUM(log_conversion_item.quantity * log_conversion_item.price)'),
809 Metrics::INDEX_ECOMMERCE_ITEM_REVENUE
810 ),
811 sprintf(
812 '%s AS `%d`',
813 self::getSqlRevenue('SUM(log_conversion_item.quantity)'),
814 Metrics::INDEX_ECOMMERCE_ITEM_QUANTITY
815 ),
816 sprintf(
817 '%s AS `%d`',
818 self::getSqlRevenue('SUM(log_conversion_item.price)'),
819 Metrics::INDEX_ECOMMERCE_ITEM_PRICE
820 ),
821 sprintf(
822 'COUNT(distinct log_conversion_item.idorder) AS `%d`',
823 Metrics::INDEX_ECOMMERCE_ORDERS
824 ),
825 sprintf(
826 'COUNT(distinct log_conversion_item.idvisit) AS `%d`',
827 Metrics::INDEX_NB_VISITS
828 ),
829 sprintf(
830 'CASE log_conversion_item.idorder WHEN \'0\' THEN %d ELSE %d END AS ecommerceType',
831 GoalManager::IDGOAL_CART,
832 GoalManager::IDGOAL_ORDER
833 )
834 )
835 ),
836
837 // FROM ...
838 array(
839 "log_conversion_item",
840 array(
841 "table" => "log_action",
842 "joinOn" => sprintf("log_conversion_item.%s = log_action.idaction", $dimension)
843 )
844 ),
845
846 // WHERE ... AND ...
847 implode(
848 ' AND ',
849 array(
850 'log_conversion_item.server_time >= ?',
851 'log_conversion_item.server_time <= ?',
852 'log_conversion_item.idsite IN (' . Common::getSqlStringFieldsArray($this->sites) . ')',
853 'log_conversion_item.deleted = 0'
854 )
855 ),
856
857 // GROUP BY ...
858 sprintf(
859 "ecommerceType, log_conversion_item.%s",
860 $dimension
861 ),
862
863 // ORDER ...
864 false
865 );
866
867 return $this->getDb()->query($query['sql'], $query['bind']);
868 }
869
870 /**
871 * Executes and returns a query aggregating action data (everything in the log_action table) and returns
872 * a DB statement that can be used to iterate over the result
873 *
874 * <a name="queryActionsByDimension-result-set"></a>
875 * **Result Set**
876 *
877 * Each row of the result set represents an aggregated group of actions. The following columns
878 * are in each aggregate row:
879 *
880 * - **{@link Piwik\Metrics::INDEX_NB_UNIQ_VISITORS}**: The total number of unique visitors that performed
881 * the actions in this group.
882 * - **{@link Piwik\Metrics::INDEX_NB_VISITS}**: The total number of visits these actions belong to.
883 * - **{@link Piwik\Metrics::INDEX_NB_ACTIONS}**: The total number of actions in this aggregate group.
884 *
885 * Additional data can be selected through the `$additionalSelects` parameter.
886 *
887 * _Note: The metrics calculated by this query can be customized by the `$metrics` parameter._
888 *
889 * @param array|string $dimensions One or more SELECT fields that will be used to group the log_action
890 * rows by. This parameter determines which log_action rows will be
891 * aggregated together.
892 * @param bool|string $where Additional condition for the WHERE clause. Can be used to filter
893 * the set of visits that are considered for aggregation.
894 * @param array $additionalSelects Additional SELECT fields that are not included in the group by
895 * clause. These can be aggregate expressions, eg, `SUM(somecol)`.
896 * @param bool|array $metrics The set of metrics to calculate and return. If `false`, the query will select
897 * all of them. The following values can be used:
898 *
899 * - {@link Piwik\Metrics::INDEX_NB_UNIQ_VISITORS}
900 * - {@link Piwik\Metrics::INDEX_NB_VISITS}
901 * - {@link Piwik\Metrics::INDEX_NB_ACTIONS}
902 * @param bool|\Piwik\RankingQuery $rankingQuery
903 * A pre-configured ranking query instance that will be used to limit the result.
904 * If set, the return value is the array returned by {@link Piwik\RankingQuery::execute()}.
905 * @param bool|string $joinLogActionOnColumn One or more columns from the **log_link_visit_action** table that
906 * log_action should be joined on. The table alias used for each join
907 * is `"log_action$i"` where `$i` is the index of the column in this
908 * array.
909 *
910 * If a string is used for this parameter, the table alias is not
911 * suffixed (since there is only one column).
912 * @param string $secondaryOrderBy A secondary order by clause for the ranking query
913 * @param int $timeLimitInMs Adds a MAX_EXECUTION_TIME hint to the query if $timeLimitInMs > 0
914 * @return mixed A Zend_Db_Statement if `$rankingQuery` isn't supplied, otherwise the result of
915 * {@link Piwik\RankingQuery::execute()}. Read [this](#queryEcommerceItems-result-set)
916 * to see what aggregate data is calculated by the query.
917 * @api
918 */
919 public function queryActionsByDimension(
920 $dimensions,
921 $where = '',
922 $additionalSelects = array(),
923 $metrics = false,
924 $rankingQuery = null,
925 $joinLogActionOnColumn = false,
926 $secondaryOrderBy = null,
927 $timeLimitInMs = -1
928 ) {
929 $tableName = self::LOG_ACTIONS_TABLE;
930 $availableMetrics = $this->getActionsMetricFields();
931
932 $select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics, $metrics);
933 $from = array($tableName);
934 $where = $this->getWhereStatement($tableName, self::ACTION_DATETIME_FIELD, $where);
935 $groupBy = $this->getGroupByStatement($dimensions, $tableName);
936
937 if ($joinLogActionOnColumn !== false) {
938 $multiJoin = is_array($joinLogActionOnColumn);
939 if (!$multiJoin) {
940 $joinLogActionOnColumn = array($joinLogActionOnColumn);
941 }
942
943 foreach ($joinLogActionOnColumn as $i => $joinColumn) {
944 $tableAlias = 'log_action' . ($multiJoin ? $i + 1 : '');
945
946 if (strpos($joinColumn, ' ') === false) {
947 $joinOn = $tableAlias . '.idaction = ' . $tableName . '.' . $joinColumn;
948 } else {
949 // more complex join column like if (...)
950 $joinOn = $tableAlias . '.idaction = ' . $joinColumn;
951 }
952
953 $from[] = array(
954 'table' => 'log_action',
955 'tableAlias' => $tableAlias,
956 'joinOn' => $joinOn
957 );
958 }
959 }
960
961 $orderBy = false;
962 if ($rankingQuery) {
963 $orderBy = '`' . Metrics::INDEX_NB_ACTIONS . '` DESC';
964 if ($secondaryOrderBy) {
965 $orderBy .= ', ' . $secondaryOrderBy;
966 }
967 }
968
969 $query = $this->generateQuery($select, $from, $where, $groupBy, $orderBy);
970
971 if ($rankingQuery !== null) {
972 $sumColumns = array_keys($availableMetrics);
973 if ($metrics) {
974 $sumColumns = array_intersect($sumColumns, $metrics);
975 }
976
977 $rankingQuery->addColumn($sumColumns, 'sum');
978
979 return $rankingQuery->execute($query['sql'], $query['bind'], $timeLimitInMs);
980 }
981
982 $query['sql'] = DbHelper::addMaxExecutionTimeHintToQuery($query['sql'], $timeLimitInMs);
983
984 return $this->getDb()->query($query['sql'], $query['bind']);
985 }
986
987 protected function getActionsMetricFields()
988 {
989 return array(
990 Metrics::INDEX_NB_VISITS => "count(distinct " . self::LOG_ACTIONS_TABLE . ".idvisit)",
991 Metrics::INDEX_NB_UNIQ_VISITORS => "count(distinct " . self::LOG_ACTIONS_TABLE . ".idvisitor)",
992 Metrics::INDEX_NB_ACTIONS => "count(*)",
993 );
994 }
995
996 /**
997 * Executes a query aggregating conversion data (everything in the **log_conversion** table) and returns
998 * a DB statement that can be used to iterate over the result.
999 *
1000 * <a name="queryConversionsByDimension-result-set"></a>
1001 * **Result Set**
1002 *
1003 * Each row of the result set represents an aggregated group of conversions. The
1004 * following columns are in each aggregate row:
1005 *
1006 * - **{@link Piwik\Metrics::INDEX_GOAL_NB_CONVERSIONS}**: The total number of conversions in this aggregate
1007 * group.
1008 * - **{@link Piwik\Metrics::INDEX_GOAL_NB_VISITS_CONVERTED}**: The total number of visits during which these
1009 * conversions were converted.
1010 * - **{@link Piwik\Metrics::INDEX_GOAL_REVENUE}**: The total revenue generated by these conversions. This value
1011 * includes the revenue from individual ecommerce items.
1012 * - **{@link Piwik\Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL}**: The total cost of all ecommerce items sold
1013 * within these conversions. This value does not
1014 * include tax, shipping or any applied discount.
1015 *
1016 * _This metric is only applicable to the special
1017 * **ecommerce** goal (where `idGoal == 'ecommerceOrder'`)._
1018 * - **{@link Piwik\Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_TAX}**: The total tax applied to every transaction in these
1019 * conversions.
1020 *
1021 * _This metric is only applicable to the special
1022 * **ecommerce** goal (where `idGoal == 'ecommerceOrder'`)._
1023 * - **{@link Piwik\Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING}**: The total shipping cost for every transaction
1024 * in these conversions.
1025 *
1026 * _This metric is only applicable to the special
1027 * **ecommerce** goal (where `idGoal == 'ecommerceOrder'`)._
1028 * - **{@link Piwik\Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT}**: The total discount applied to every transaction
1029 * in these conversions.
1030 *
1031 * _This metric is only applicable to the special
1032 * **ecommerce** goal (where `idGoal == 'ecommerceOrder'`)._
1033 * - **{@link Piwik\Metrics::INDEX_GOAL_ECOMMERCE_ITEMS}**: The total number of ecommerce items sold in each transaction
1034 * in these conversions.
1035 *
1036 * _This metric is only applicable to the special
1037 * **ecommerce** goal (where `idGoal == 'ecommerceOrder'`)._
1038 *
1039 * Additional data can be selected through the `$additionalSelects` parameter.
1040 *
1041 * _Note: This method will only query the **log_conversion** table. Other tables cannot be joined
1042 * using this method._
1043 *
1044 * @param array|string $dimensions One or more **SELECT** fields that will be used to group the log_conversion
1045 * rows by. This parameter determines which **log_conversion** rows will be
1046 * aggregated together.
1047 * @param bool|string $where An optional SQL expression used in the SQL's **WHERE** clause.
1048 * @param array $additionalSelects Additional SELECT fields that are not included in the group by
1049 * clause. These can be aggregate expressions, eg, `SUM(somecol)`.
1050 * @return \Zend_Db_Statement
1051 */
1052 public function queryConversionsByDimension($dimensions = array(), $where = false, $additionalSelects = array(), $extraFrom = [])
1053 {
1054 $dimensions = array_merge(array(self::IDGOAL_FIELD), $dimensions);
1055 $tableName = self::LOG_CONVERSION_TABLE;
1056 $availableMetrics = $this->getConversionsMetricFields();
1057
1058 $select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics);
1059
1060 $from = array_merge([$tableName], $extraFrom);
1061 $where = $this->getWhereStatement($tableName, self::CONVERSION_DATETIME_FIELD, $where);
1062 $groupBy = $this->getGroupByStatement($dimensions, $tableName);
1063 $orderBy = false;
1064 $query = $this->generateQuery($select, $from, $where, $groupBy, $orderBy);
1065
1066 return $this->getDb()->query($query['sql'], $query['bind']);
1067 }
1068
1069 /**
1070 * Creates and returns an array of SQL `SELECT` expressions that will each count how
1071 * many rows have a column whose value is within a certain range.
1072 *
1073 * **Note:** The result of this function is meant for use in the `$additionalSelects` parameter
1074 * in one of the query... methods (for example {@link queryVisitsByDimension()}).
1075 *
1076 * **Example**
1077 *
1078 * // summarize one column
1079 * $visitTotalActionsRanges = array(
1080 * array(1, 1),
1081 * array(2, 10),
1082 * array(10)
1083 * );
1084 * $selects = LogAggregator::getSelectsFromRangedColumn('visit_total_actions', $visitTotalActionsRanges, 'log_visit', 'vta');
1085 *
1086 * // summarize another column in the same request
1087 * $visitCountVisitsRanges = array(
1088 * array(1, 1),
1089 * array(2, 20),
1090 * array(20)
1091 * );
1092 * $selects = array_merge(
1093 * $selects,
1094 * LogAggregator::getSelectsFromRangedColumn('visitor_count_visits', $visitCountVisitsRanges, 'log_visit', 'vcv')
1095 * );
1096 *
1097 * // perform the query
1098 * $logAggregator = // get the LogAggregator somehow
1099 * $query = $logAggregator->queryVisitsByDimension($dimensions = array(), $where = false, $selects);
1100 * $tableSummary = $query->fetch();
1101 *
1102 * $numberOfVisitsWithOneAction = $tableSummary['vta0'];
1103 * $numberOfVisitsBetweenTwoAnd10 = $tableSummary['vta1'];
1104 *
1105 * $numberOfVisitsWithVisitCountOfOne = $tableSummary['vcv0'];
1106 *
1107 * @param string $column The name of a column in `$table` that will be summarized.
1108 * @param array $ranges The array of ranges over which the data in the table
1109 * will be summarized. For example,
1110 * ```
1111 * array(
1112 * array(1, 1),
1113 * array(2, 2),
1114 * array(3, 8),
1115 * array(8) // everything over 8
1116 * )
1117 * ```
1118 * @param string $table The unprefixed name of the table whose rows will be summarized.
1119 * @param string $selectColumnPrefix The prefix to prepend to each SELECT expression. This
1120 * prefix is used to differentiate different sets of
1121 * range summarization SELECTs. You can supply different
1122 * values to this argument to summarize several columns
1123 * in one query (see above for an example).
1124 * @param bool $restrictToReturningVisitors Whether to only summarize rows that belong to
1125 * visits of returning visitors or not. If this
1126 * argument is true, then the SELECT expressions
1127 * returned can only be used with the
1128 * {@link queryVisitsByDimension()} method.
1129 * @return array An array of SQL SELECT expressions, for example,
1130 * ```
1131 * array(
1132 * 'sum(case when log_visit.visit_total_actions between 0 and 2 then 1 else 0 end) as vta0',
1133 * 'sum(case when log_visit.visit_total_actions > 2 then 1 else 0 end) as vta1'
1134 * )
1135 * ```
1136 * @api
1137 */
1138 public static function getSelectsFromRangedColumn($column, $ranges, $table, $selectColumnPrefix, $restrictToReturningVisitors = false)
1139 {
1140 $selects = array();
1141 $extraCondition = '';
1142
1143 if ($restrictToReturningVisitors) {
1144 // extra condition for the SQL SELECT that makes sure only returning visits are counted
1145 // when creating the 'days since last visit' report
1146 $extraCondition = 'and log_visit.visitor_returning = 1';
1147 $extraSelect = "sum(case when log_visit.visitor_returning = 0 then 1 else 0 end) "
1148 . " as `" . $selectColumnPrefix . 'General_NewVisits' . "`";
1149 $selects[] = $extraSelect;
1150 }
1151
1152 foreach ($ranges as $gap) {
1153 if (count($gap) == 2) {
1154 $lowerBound = $gap[0];
1155 $upperBound = $gap[1];
1156
1157 $selectAs = "$selectColumnPrefix$lowerBound-$upperBound";
1158
1159 $selects[] = "sum(case when $table.$column between $lowerBound and $upperBound $extraCondition" .
1160 " then 1 else 0 end) as `$selectAs`";
1161 } else {
1162 $lowerBound = $gap[0];
1163
1164 $selectAs = $selectColumnPrefix . ($lowerBound + 1) . urlencode('+');
1165 $selects[] = "sum(case when $table.$column > $lowerBound $extraCondition then 1 else 0 end) as `$selectAs`";
1166 }
1167 }
1168
1169 return $selects;
1170 }
1171
1172 /**
1173 * Clean up the row data and return values.
1174 * $lookForThisPrefix can be used to make sure only SOME of the data in $row is used.
1175 *
1176 * The array will have one column $columnName
1177 *
1178 * @param $row
1179 * @param $columnName
1180 * @param bool $lookForThisPrefix A string that identifies which elements of $row to use
1181 * in the result. Every key of $row that starts with this
1182 * value is used.
1183 * @return array
1184 */
1185 public static function makeArrayOneColumn($row, $columnName, $lookForThisPrefix = false)
1186 {
1187 $cleanRow = array();
1188
1189 foreach ($row as $label => $count) {
1190 if (empty($lookForThisPrefix)
1191 || strpos($label, $lookForThisPrefix) === 0
1192 ) {
1193 $cleanLabel = substr($label, strlen($lookForThisPrefix));
1194 $cleanRow[$cleanLabel] = array($columnName => $count);
1195 }
1196 }
1197
1198 return $cleanRow;
1199 }
1200
1201 public function getDb()
1202 {
1203 /** @var ArchivingStatus $archivingStatus */
1204 $archivingStatus = StaticContainer::get(ArchivingStatus::class);
1205 $archivingLock = $archivingStatus->getCurrentArchivingLock();
1206 return new ArchivingDbAdapter(Db::getReader(), $archivingLock, $this->logger);
1207 }
1208 }
1209