PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 1.1.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v1.1.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 6 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
1191 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 $createTableSql = 'CREATE TEMPORARY TABLE ' . $table . ' (idvisit BIGINT(10) UNSIGNED NOT NULL) ' . $engine;
284 // we do not insert the data right away using create temporary table ... select ...
285 // to avoid metadata lock see eg https://www.percona.com/blog/2018/01/10/why-avoid-create-table-as-select-statement/
286
287 $readerDb = Db::getReader();
288 try {
289 $readerDb->query($createTableSql);
290 } catch (\Exception $e) {
291 if ($readerDb->isErrNo($e, \Piwik\Updater\Migration\Db::ERROR_CODE_TABLE_EXISTS)) {
292 return;
293 }
294 throw $e;
295 }
296
297 $transactionLevel = new Db\TransactionLevel($readerDb);
298 $canSetTransactionLevel = $transactionLevel->canLikelySetTransactionLevel();
299
300 if ($canSetTransactionLevel) {
301 // i know this could be shortened to one if or one line but I want to make sure this line where we
302 // set uncomitted is easily noticable in the code as it could be missed quite easily otherwise
303 // we set uncommitted so we don't make the INSERT INTO... SELECT... locking ... we do not want to lock
304 // eg the visits table
305 if (!$transactionLevel->setUncommitted()) {
306 $canSetTransactionLevel = false;
307 }
308 }
309
310 if (!$canSetTransactionLevel) {
311 // transaction level doesn't work... we're instead executing the select individually and then insert the data
312 // this uses more memory but at least is not locking
313 $all = $readerDb->fetchAll($segmentSelectSql, $segmentSelectBind);
314 if (!empty($all)) {
315 // we're not using batchinsert since this would not support the reader DB.
316 $readerDb->query('INSERT INTO ' . $table . ' VALUES ('.implode('),(', array_column($all, 'idvisit')).')');
317 }
318 return;
319 }
320
321 $insertIntoStatement = 'INSERT INTO ' . $table . ' (idvisit) ' . $segmentSelectSql;
322 $readerDb->query($insertIntoStatement, $segmentSelectBind);
323
324 $transactionLevel->restorePreviousStatus();
325 }
326
327 public function generateQuery($select, $from, $where, $groupBy, $orderBy, $limit = 0, $offset = 0)
328 {
329 $segment = $this->segment;
330 $bind = $this->getGeneralQueryBindParams();
331
332 if (!$this->segment->isEmpty() && $this->isSegmentCacheEnabled()) {
333 // here we create the TMP table and apply the segment including the datetime and the requested idsite
334 // at the end we generated query will no longer need to apply the datetime/idsite and segment
335 $segment = new Segment('', $this->sites);
336
337 $segmentTable = $this->getSegmentTmpTableName();
338
339 $segmentWhere = $this->getWhereStatement('log_visit', 'visit_last_action_time');
340 $segmentBind = $this->getGeneralQueryBindParams();
341
342 $logQueryBuilder = StaticContainer::get('Piwik\DataAccess\LogQueryBuilder');
343 $forceGroupByBackup = $logQueryBuilder->getForcedInnerGroupBySubselect();
344 $logQueryBuilder->forceInnerGroupBySubselect(LogQueryBuilder::FORCE_INNER_GROUP_BY_NO_SUBSELECT);
345 $segmentSql = $this->segment->getSelectQuery('distinct log_visit.idvisit as idvisit', 'log_visit', $segmentWhere, $segmentBind, 'log_visit.idvisit ASC');
346 $logQueryBuilder->forceInnerGroupBySubselect($forceGroupByBackup);
347
348 $this->createTemporaryTable($segmentTable, $segmentSql['sql'], $segmentSql['bind']);
349
350 if (!is_array($from)) {
351 $from = array($segmentTable, $from);
352 } else {
353 array_unshift($from, $segmentTable);
354 }
355
356 $logTablesProvider = $this->getLogTableProvider();
357 $logTablesProvider->setTempTable(new LogTableTemporary($segmentTable));
358
359 foreach ($logTablesProvider->getAllLogTables() as $logTable) {
360 if ($logTable->getDateTimeColumn()) {
361 $whereTest = $this->getWhereStatement($logTable->getName(), $logTable->getDateTimeColumn());
362 if (strpos($where, $whereTest) === 0) {
363 // we don't need to apply the where statement again as it would have been applied already
364 // in the temporary table... instead it should join the tables through the idvisit index
365 $where = ltrim(str_replace($whereTest, '', $where));
366 if (stripos($where, 'and ') === 0) {
367 $where = substr($where, strlen('and '));
368 }
369 $bind = array();
370 break;
371 }
372 }
373
374 }
375
376 }
377
378 $query = $segment->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy, $limit, $offset);
379
380 $select = 'SELECT';
381 if ($this->queryOriginHint && is_array($query) && 0 === strpos(trim($query['sql']), $select)) {
382 $query['sql'] = trim($query['sql']);
383 $query['sql'] = 'SELECT /* ' . $this->queryOriginHint . ' */' . substr($query['sql'], strlen($select));
384 }
385
386 if (!$this->getSegment()->isEmpty() && is_array($query) && 0 === strpos(trim($query['sql']), $select)) {
387 $query['sql'] = trim($query['sql']);
388 $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));
389 }
390
391 return $query;
392 }
393
394 protected function getVisitsMetricFields()
395 {
396 return array(
397 Metrics::INDEX_NB_UNIQ_VISITORS => "count(distinct " . self::LOG_VISIT_TABLE . ".idvisitor)",
398 Metrics::INDEX_NB_UNIQ_FINGERPRINTS => "count(distinct " . self::LOG_VISIT_TABLE . ".config_id)",
399 Metrics::INDEX_NB_VISITS => "count(*)",
400 Metrics::INDEX_NB_ACTIONS => "sum(" . self::LOG_VISIT_TABLE . ".visit_total_actions)",
401 Metrics::INDEX_MAX_ACTIONS => "max(" . self::LOG_VISIT_TABLE . ".visit_total_actions)",
402 Metrics::INDEX_SUM_VISIT_LENGTH => "sum(" . self::LOG_VISIT_TABLE . ".visit_total_time)",
403 Metrics::INDEX_BOUNCE_COUNT => "sum(case " . self::LOG_VISIT_TABLE . ".visit_total_actions when 1 then 1 when 0 then 1 else 0 end)",
404 Metrics::INDEX_NB_VISITS_CONVERTED => "sum(case " . self::LOG_VISIT_TABLE . ".visit_goal_converted when 1 then 1 else 0 end)",
405 Metrics::INDEX_NB_USERS => "count(distinct " . self::LOG_VISIT_TABLE . ".user_id)",
406 );
407 }
408
409 public static function getConversionsMetricFields()
410 {
411 return array(
412 Metrics::INDEX_GOAL_NB_CONVERSIONS => "count(*)",
413 Metrics::INDEX_GOAL_NB_VISITS_CONVERTED => "count(distinct " . self::LOG_CONVERSION_TABLE . ".idvisit)",
414 Metrics::INDEX_GOAL_REVENUE => self::getSqlConversionRevenueSum(self::TOTAL_REVENUE_FIELD),
415 Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL => self::getSqlConversionRevenueSum(self::REVENUE_SUBTOTAL_FIELD),
416 Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_TAX => self::getSqlConversionRevenueSum(self::REVENUE_TAX_FIELD),
417 Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING => self::getSqlConversionRevenueSum(self::REVENUE_SHIPPING_FIELD),
418 Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT => self::getSqlConversionRevenueSum(self::REVENUE_DISCOUNT_FIELD),
419 Metrics::INDEX_GOAL_ECOMMERCE_ITEMS => "SUM(" . self::LOG_CONVERSION_TABLE . "." . self::ITEMS_COUNT_FIELD . ")",
420 );
421 }
422
423 private static function getSqlConversionRevenueSum($field)
424 {
425 return self::getSqlRevenue('SUM(' . self::LOG_CONVERSION_TABLE . '.' . $field . ')');
426 }
427
428 public static function getSqlRevenue($field)
429 {
430 return "ROUND(" . $field . "," . GoalManager::REVENUE_PRECISION . ")";
431 }
432
433 /**
434 * Helper function that returns an array with common metrics for a given log_visit field distinct values.
435 *
436 * The statistics returned are:
437 * - number of unique visitors
438 * - number of visits
439 * - number of actions
440 * - maximum number of action for a visit
441 * - sum of the visits' length in sec
442 * - count of bouncing visits (visits with one page view)
443 *
444 * For example if $dimension = 'config_os' it will return the statistics for every distinct Operating systems
445 * The returned array will have a row per distinct operating systems,
446 * and a column per stat (nb of visits, max actions, etc)
447 *
448 * 'label' Metrics::INDEX_NB_UNIQ_VISITORS Metrics::INDEX_NB_VISITS etc.
449 * Linux 27 66 ...
450 * Windows XP 12 ...
451 * Mac OS 15 36 ...
452 *
453 * @param string $dimension Table log_visit field name to be use to compute common stats
454 * @return DataArray
455 */
456 public function getMetricsFromVisitByDimension($dimension)
457 {
458 if (!is_array($dimension)) {
459 $dimension = array($dimension);
460 }
461 if (count($dimension) == 1) {
462 $dimension = array("label" => reset($dimension));
463 }
464 $query = $this->queryVisitsByDimension($dimension);
465 $metrics = new DataArray();
466 while ($row = $query->fetch()) {
467 $metrics->sumMetricsVisits($row["label"], $row);
468 }
469 return $metrics;
470 }
471
472 /**
473 * Executes and returns a query aggregating visit logs, optionally grouping by some dimension. Returns
474 * a DB statement that can be used to iterate over the result
475 *
476 * **Result Set**
477 *
478 * The following columns are in each row of the result set:
479 *
480 * - **{@link \Piwik\Metrics::INDEX_NB_UNIQ_VISITORS}**: The total number of unique visitors in this group
481 * of aggregated visits.
482 * - **{@link \Piwik\Metrics::INDEX_NB_VISITS}**: The total number of visits aggregated.
483 * - **{@link \Piwik\Metrics::INDEX_NB_ACTIONS}**: The total number of actions performed in this group of
484 * aggregated visits.
485 * - **{@link \Piwik\Metrics::INDEX_MAX_ACTIONS}**: The maximum actions perfomred in one visit for this group of
486 * visits.
487 * - **{@link \Piwik\Metrics::INDEX_SUM_VISIT_LENGTH}**: The total amount of time spent on the site for this
488 * group of visits.
489 * - **{@link \Piwik\Metrics::INDEX_BOUNCE_COUNT}**: The total number of bounced visits in this group of
490 * visits.
491 * - **{@link \Piwik\Metrics::INDEX_NB_VISITS_CONVERTED}**: The total number of visits for which at least one
492 * conversion occurred, for this group of visits.
493 *
494 * Additional data can be selected by setting the `$additionalSelects` parameter.
495 *
496 * _Note: The metrics returned by this query can be customized by the `$metrics` parameter._
497 *
498 * @param array|string $dimensions `SELECT` fields (or just one field) that will be grouped by,
499 * eg, `'referrer_name'` or `array('referrer_name', 'referrer_keyword')`.
500 * The metrics retrieved from the query will be specific to combinations
501 * of these fields. So if `array('referrer_name', 'referrer_keyword')`
502 * is supplied, the query will aggregate visits for each referrer/keyword
503 * combination.
504 * @param bool|string $where Additional condition for the `WHERE` clause. Can be used to filter
505 * the set of visits that are considered for aggregation.
506 * @param array $additionalSelects Additional `SELECT` fields that are not included in the group by
507 * clause. These can be aggregate expressions, eg, `SUM(somecol)`.
508 * @param bool|array $metrics The set of metrics to calculate and return. If false, the query will select
509 * all of them. The following values can be used:
510 *
511 * - {@link \Piwik\Metrics::INDEX_NB_UNIQ_VISITORS}
512 * - {@link \Piwik\Metrics::INDEX_NB_VISITS}
513 * - {@link \Piwik\Metrics::INDEX_NB_ACTIONS}
514 * - {@link \Piwik\Metrics::INDEX_MAX_ACTIONS}
515 * - {@link \Piwik\Metrics::INDEX_SUM_VISIT_LENGTH}
516 * - {@link \Piwik\Metrics::INDEX_BOUNCE_COUNT}
517 * - {@link \Piwik\Metrics::INDEX_NB_VISITS_CONVERTED}
518 * @param bool|\Piwik\RankingQuery $rankingQuery
519 * A pre-configured ranking query instance that will be used to limit the result.
520 * If set, the return value is the array returned by {@link \Piwik\RankingQuery::execute()}.
521 * @param bool|string $orderBy Order By clause to add (e.g. user_id ASC)
522 * @param int $timeLimitInMs Adds a MAX_EXECUTION_TIME query hint to the query if $timeLimitInMs > 0
523 *
524 * @return mixed A Zend_Db_Statement if `$rankingQuery` isn't supplied, otherwise the result of
525 * {@link \Piwik\RankingQuery::execute()}. Read {@link queryVisitsByDimension() this}
526 * to see what aggregate data is calculated by the query.
527 * @api
528 */
529 public function queryVisitsByDimension(array $dimensions = array(), $where = false, array $additionalSelects = array(),
530 $metrics = false, $rankingQuery = false, $orderBy = false, $timeLimitInMs = -1)
531 {
532 $tableName = self::LOG_VISIT_TABLE;
533 $availableMetrics = $this->getVisitsMetricFields();
534
535 $select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics, $metrics);
536 $from = array($tableName);
537 $where = $this->getWhereStatement($tableName, self::VISIT_DATETIME_FIELD, $where);
538 $groupBy = $this->getGroupByStatement($dimensions, $tableName);
539 $orderBys = $orderBy ? [$orderBy] : [];
540
541 if ($rankingQuery) {
542 $orderBys[] = '`' . Metrics::INDEX_NB_VISITS . '` DESC';
543 }
544
545 $query = $this->generateQuery($select, $from, $where, $groupBy, implode(', ', $orderBys));
546
547 if ($rankingQuery) {
548 unset($availableMetrics[Metrics::INDEX_MAX_ACTIONS]);
549
550 // INDEX_NB_UNIQ_FINGERPRINTS is only processed if specifically asked for
551 if (!$this->isMetricRequested(Metrics::INDEX_NB_UNIQ_FINGERPRINTS, $metrics)) {
552 unset($availableMetrics[Metrics::INDEX_NB_UNIQ_FINGERPRINTS]);
553 }
554
555 $sumColumns = array_keys($availableMetrics);
556
557 if ($metrics) {
558 $sumColumns = array_intersect($sumColumns, $metrics);
559 }
560
561 $rankingQuery->addColumn($sumColumns, 'sum');
562 if ($this->isMetricRequested(Metrics::INDEX_MAX_ACTIONS, $metrics)) {
563 $rankingQuery->addColumn(Metrics::INDEX_MAX_ACTIONS, 'max');
564 }
565
566 return $rankingQuery->execute($query['sql'], $query['bind'], $timeLimitInMs);
567 }
568
569 $query['sql'] = DbHelper::addMaxExecutionTimeHintToQuery($query['sql'], $timeLimitInMs);
570
571 return $this->getDb()->query($query['sql'], $query['bind']);
572 }
573
574 protected function getSelectsMetrics($metricsAvailable, $metricsRequested = false)
575 {
576 $selects = array();
577
578 foreach ($metricsAvailable as $metricId => $statement) {
579 if ($this->isMetricRequested($metricId, $metricsRequested)) {
580 $aliasAs = $this->getSelectAliasAs($metricId);
581 $selects[] = $statement . $aliasAs;
582 }
583 }
584
585 return $selects;
586 }
587
588 protected function getSelectStatement($dimensions, $tableName, $additionalSelects, array $availableMetrics, $requestedMetrics = false)
589 {
590 $dimensionsToSelect = $this->getDimensionsToSelect($dimensions, $additionalSelects);
591
592 $selects = array_merge(
593 $this->getSelectDimensions($dimensionsToSelect, $tableName),
594 $this->getSelectsMetrics($availableMetrics, $requestedMetrics),
595 !empty($additionalSelects) ? $additionalSelects : array()
596 );
597
598 $select = implode(self::FIELDS_SEPARATOR, $selects);
599 return $select;
600 }
601
602 /**
603 * Will return the subset of $dimensions that are not found in $additionalSelects
604 *
605 * @param $dimensions
606 * @param array $additionalSelects
607 * @return array
608 */
609 protected function getDimensionsToSelect($dimensions, $additionalSelects)
610 {
611 if (empty($additionalSelects)) {
612 return $dimensions;
613 }
614
615 $dimensionsToSelect = array();
616 foreach ($dimensions as $selectAs => $dimension) {
617 $asAlias = $this->getSelectAliasAs($dimension);
618 foreach ($additionalSelects as $additionalSelect) {
619 if (strpos($additionalSelect, $asAlias) === false) {
620 $dimensionsToSelect[$selectAs] = $dimension;
621 }
622 }
623 }
624
625 $dimensionsToSelect = array_unique($dimensionsToSelect);
626 return $dimensionsToSelect;
627 }
628
629 /**
630 * Returns the dimensions array, where
631 * (1) the table name is prepended to the field
632 * (2) the "AS `label` " is appended to the field
633 *
634 * @param $dimensions
635 * @param $tableName
636 * @param bool $appendSelectAs
637 * @param bool $parseSelectAs
638 * @return mixed
639 */
640 protected function getSelectDimensions($dimensions, $tableName, $appendSelectAs = true)
641 {
642 foreach ($dimensions as $selectAs => &$field) {
643 $selectAsString = $field;
644
645 if (!is_numeric($selectAs)) {
646 $selectAsString = $selectAs;
647 } else if ($this->isFieldFunctionOrComplexExpression($field)) {
648 // if complex expression has a select as, use it
649 if (!$appendSelectAs && preg_match('/\s+AS\s+(.*?)\s*$/', $field, $matches)) {
650 $field = $matches[1];
651 continue;
652 }
653
654 // if function w/o select as, do not alias or prefix
655 $selectAsString = $appendSelectAs = false;
656 }
657
658 $isKnownField = !in_array($field, array('referrer_data'));
659
660 if ($selectAsString == $field && $isKnownField) {
661 $field = $this->prefixColumn($field, $tableName);
662 }
663
664 if ($appendSelectAs && $selectAsString) {
665 $field = $this->prefixColumn($field, $tableName) . $this->getSelectAliasAs($selectAsString);
666 }
667 }
668
669 return $dimensions;
670 }
671
672 /**
673 * Prefixes a column name with a table name if not already done.
674 *
675 * @param string $column eg, 'location_provider'
676 * @param string $tableName eg, 'log_visit'
677 * @return string eg, 'log_visit.location_provider'
678 */
679 private function prefixColumn($column, $tableName)
680 {
681 if (strpos($column, '.') === false) {
682 return $tableName . '.' . $column;
683 } else {
684 return $column;
685 }
686 }
687
688 protected function isFieldFunctionOrComplexExpression($field)
689 {
690 return strpos($field, "(") !== false
691 || strpos($field, "CASE") !== false;
692 }
693
694 protected function getSelectAliasAs($metricId)
695 {
696 return " AS `" . $metricId . "`";
697 }
698
699 protected function isMetricRequested($metricId, $metricsRequested)
700 {
701 // do not process INDEX_NB_UNIQ_FINGERPRINTS unless specifically asked for
702 if ($metricsRequested === false) {
703 if ($metricId == Metrics::INDEX_NB_UNIQ_FINGERPRINTS) {
704 return false;
705 }
706 return true;
707 }
708 return in_array($metricId, $metricsRequested);
709 }
710
711 public function getWhereStatement($tableName, $datetimeField, $extraWhere = false)
712 {
713 $where = "$tableName.$datetimeField >= ?
714 AND $tableName.$datetimeField <= ?
715 AND $tableName.idsite IN (". Common::getSqlStringFieldsArray($this->sites) . ")";
716
717 if (!empty($extraWhere)) {
718 $extraWhere = sprintf($extraWhere, $tableName, $tableName);
719 $where .= ' AND ' . $extraWhere;
720 }
721
722 return $where;
723 }
724
725 protected function getGroupByStatement($dimensions, $tableName)
726 {
727 $dimensions = $this->getSelectDimensions($dimensions, $tableName, $appendSelectAs = false);
728 $groupBy = implode(", ", $dimensions);
729
730 return $groupBy;
731 }
732
733 /**
734 * Returns general bind parameters for all log aggregation queries. This includes the datetime
735 * start of entities, datetime end of entities and IDs of all sites.
736 *
737 * @return array
738 */
739 public function getGeneralQueryBindParams()
740 {
741 $bind = array($this->dateStart->toString(Date::DATE_TIME_FORMAT), $this->dateEnd->toString(Date::DATE_TIME_FORMAT));
742 $bind = array_merge($bind, $this->sites);
743
744 return $bind;
745 }
746
747 /**
748 * Executes and returns a query aggregating ecommerce item data (everything stored in the
749 * **log\_conversion\_item** table) and returns a DB statement that can be used to iterate over the result
750 *
751 * <a name="queryEcommerceItems-result-set"></a>
752 * **Result Set**
753 *
754 * Each row of the result set represents an aggregated group of ecommerce items. The following
755 * columns are in each row of the result set:
756 *
757 * - **{@link Piwik\Metrics::INDEX_ECOMMERCE_ITEM_REVENUE}**: The total revenue for the group of items.
758 * - **{@link Piwik\Metrics::INDEX_ECOMMERCE_ITEM_QUANTITY}**: The total number of items in this group.
759 * - **{@link Piwik\Metrics::INDEX_ECOMMERCE_ITEM_PRICE}**: The total price for the group of items.
760 * - **{@link Piwik\Metrics::INDEX_ECOMMERCE_ORDERS}**: The total number of orders this group of items
761 * belongs to. This will be <= to the total number
762 * of items in this group.
763 * - **{@link Piwik\Metrics::INDEX_NB_VISITS}**: The total number of visits that caused these items to be logged.
764 * - **ecommerceType**: Either {@link Piwik\Tracker\GoalManager::IDGOAL_CART} if the items in this group were
765 * abandoned by a visitor, or {@link Piwik\Tracker\GoalManager::IDGOAL_ORDER} if they
766 * were ordered by a visitor.
767 *
768 * **Limitations**
769 *
770 * Segmentation is not yet supported for this aggregation method.
771 *
772 * @param string $dimension One or more **log\_conversion\_item** columns to group aggregated data by.
773 * Eg, `'idaction_sku'` or `'idaction_sku, idaction_category'`.
774 * @return \Zend_Db_Statement A statement object that can be used to iterate through the query's
775 * result set. See [above](#queryEcommerceItems-result-set) to learn more
776 * about what this query selects.
777 * @api
778 */
779 public function queryEcommerceItems($dimension)
780 {
781 $query = $this->generateQuery(
782 // SELECT ...
783 implode(
784 ', ',
785 array(
786 "log_action.name AS label",
787 sprintf("log_conversion_item.%s AS labelIdAction", $dimension),
788 sprintf(
789 '%s AS `%d`',
790 self::getSqlRevenue('SUM(log_conversion_item.quantity * log_conversion_item.price)'),
791 Metrics::INDEX_ECOMMERCE_ITEM_REVENUE
792 ),
793 sprintf(
794 '%s AS `%d`',
795 self::getSqlRevenue('SUM(log_conversion_item.quantity)'),
796 Metrics::INDEX_ECOMMERCE_ITEM_QUANTITY
797 ),
798 sprintf(
799 '%s AS `%d`',
800 self::getSqlRevenue('SUM(log_conversion_item.price)'),
801 Metrics::INDEX_ECOMMERCE_ITEM_PRICE
802 ),
803 sprintf(
804 'COUNT(distinct log_conversion_item.idorder) AS `%d`',
805 Metrics::INDEX_ECOMMERCE_ORDERS
806 ),
807 sprintf(
808 'COUNT(distinct log_conversion_item.idvisit) AS `%d`',
809 Metrics::INDEX_NB_VISITS
810 ),
811 sprintf(
812 'CASE log_conversion_item.idorder WHEN \'0\' THEN %d ELSE %d END AS ecommerceType',
813 GoalManager::IDGOAL_CART,
814 GoalManager::IDGOAL_ORDER
815 )
816 )
817 ),
818
819 // FROM ...
820 array(
821 "log_conversion_item",
822 array(
823 "table" => "log_action",
824 "joinOn" => sprintf("log_conversion_item.%s = log_action.idaction", $dimension)
825 )
826 ),
827
828 // WHERE ... AND ...
829 implode(
830 ' AND ',
831 array(
832 'log_conversion_item.server_time >= ?',
833 'log_conversion_item.server_time <= ?',
834 'log_conversion_item.idsite IN (' . Common::getSqlStringFieldsArray($this->sites) . ')',
835 'log_conversion_item.deleted = 0'
836 )
837 ),
838
839 // GROUP BY ...
840 sprintf(
841 "ecommerceType, log_conversion_item.%s",
842 $dimension
843 ),
844
845 // ORDER ...
846 false
847 );
848
849 return $this->getDb()->query($query['sql'], $query['bind']);
850 }
851
852 /**
853 * Executes and returns a query aggregating action data (everything in the log_action table) and returns
854 * a DB statement that can be used to iterate over the result
855 *
856 * <a name="queryActionsByDimension-result-set"></a>
857 * **Result Set**
858 *
859 * Each row of the result set represents an aggregated group of actions. The following columns
860 * are in each aggregate row:
861 *
862 * - **{@link Piwik\Metrics::INDEX_NB_UNIQ_VISITORS}**: The total number of unique visitors that performed
863 * the actions in this group.
864 * - **{@link Piwik\Metrics::INDEX_NB_VISITS}**: The total number of visits these actions belong to.
865 * - **{@link Piwik\Metrics::INDEX_NB_ACTIONS}**: The total number of actions in this aggregate group.
866 *
867 * Additional data can be selected through the `$additionalSelects` parameter.
868 *
869 * _Note: The metrics calculated by this query can be customized by the `$metrics` parameter._
870 *
871 * @param array|string $dimensions One or more SELECT fields that will be used to group the log_action
872 * rows by. This parameter determines which log_action rows will be
873 * aggregated together.
874 * @param bool|string $where Additional condition for the WHERE clause. Can be used to filter
875 * the set of visits that are considered for aggregation.
876 * @param array $additionalSelects Additional SELECT fields that are not included in the group by
877 * clause. These can be aggregate expressions, eg, `SUM(somecol)`.
878 * @param bool|array $metrics The set of metrics to calculate and return. If `false`, the query will select
879 * all of them. The following values can be used:
880 *
881 * - {@link Piwik\Metrics::INDEX_NB_UNIQ_VISITORS}
882 * - {@link Piwik\Metrics::INDEX_NB_VISITS}
883 * - {@link Piwik\Metrics::INDEX_NB_ACTIONS}
884 * @param bool|\Piwik\RankingQuery $rankingQuery
885 * A pre-configured ranking query instance that will be used to limit the result.
886 * If set, the return value is the array returned by {@link Piwik\RankingQuery::execute()}.
887 * @param bool|string $joinLogActionOnColumn One or more columns from the **log_link_visit_action** table that
888 * log_action should be joined on. The table alias used for each join
889 * is `"log_action$i"` where `$i` is the index of the column in this
890 * array.
891 *
892 * If a string is used for this parameter, the table alias is not
893 * suffixed (since there is only one column).
894 * @param string $secondaryOrderBy A secondary order by clause for the ranking query
895 * @param int $timeLimitInMs Adds a MAX_EXECUTION_TIME hint to the query if $timeLimitInMs > 0
896 * @return mixed A Zend_Db_Statement if `$rankingQuery` isn't supplied, otherwise the result of
897 * {@link Piwik\RankingQuery::execute()}. Read [this](#queryEcommerceItems-result-set)
898 * to see what aggregate data is calculated by the query.
899 * @api
900 */
901 public function queryActionsByDimension(
902 $dimensions,
903 $where = '',
904 $additionalSelects = array(),
905 $metrics = false,
906 $rankingQuery = null,
907 $joinLogActionOnColumn = false,
908 $secondaryOrderBy = null,
909 $timeLimitInMs = -1
910 ) {
911 $tableName = self::LOG_ACTIONS_TABLE;
912 $availableMetrics = $this->getActionsMetricFields();
913
914 $select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics, $metrics);
915 $from = array($tableName);
916 $where = $this->getWhereStatement($tableName, self::ACTION_DATETIME_FIELD, $where);
917 $groupBy = $this->getGroupByStatement($dimensions, $tableName);
918
919 if ($joinLogActionOnColumn !== false) {
920 $multiJoin = is_array($joinLogActionOnColumn);
921 if (!$multiJoin) {
922 $joinLogActionOnColumn = array($joinLogActionOnColumn);
923 }
924
925 foreach ($joinLogActionOnColumn as $i => $joinColumn) {
926 $tableAlias = 'log_action' . ($multiJoin ? $i + 1 : '');
927
928 if (strpos($joinColumn, ' ') === false) {
929 $joinOn = $tableAlias . '.idaction = ' . $tableName . '.' . $joinColumn;
930 } else {
931 // more complex join column like if (...)
932 $joinOn = $tableAlias . '.idaction = ' . $joinColumn;
933 }
934
935 $from[] = array(
936 'table' => 'log_action',
937 'tableAlias' => $tableAlias,
938 'joinOn' => $joinOn
939 );
940 }
941 }
942
943 $orderBy = false;
944 if ($rankingQuery) {
945 $orderBy = '`' . Metrics::INDEX_NB_ACTIONS . '` DESC';
946 if ($secondaryOrderBy) {
947 $orderBy .= ', ' . $secondaryOrderBy;
948 }
949 }
950
951 $query = $this->generateQuery($select, $from, $where, $groupBy, $orderBy);
952
953 if ($rankingQuery !== null) {
954 $sumColumns = array_keys($availableMetrics);
955 if ($metrics) {
956 $sumColumns = array_intersect($sumColumns, $metrics);
957 }
958
959 $rankingQuery->addColumn($sumColumns, 'sum');
960
961 return $rankingQuery->execute($query['sql'], $query['bind'], $timeLimitInMs);
962 }
963
964 $query['sql'] = DbHelper::addMaxExecutionTimeHintToQuery($query['sql'], $timeLimitInMs);
965
966 return $this->getDb()->query($query['sql'], $query['bind']);
967 }
968
969 protected function getActionsMetricFields()
970 {
971 return array(
972 Metrics::INDEX_NB_VISITS => "count(distinct " . self::LOG_ACTIONS_TABLE . ".idvisit)",
973 Metrics::INDEX_NB_UNIQ_VISITORS => "count(distinct " . self::LOG_ACTIONS_TABLE . ".idvisitor)",
974 Metrics::INDEX_NB_ACTIONS => "count(*)",
975 );
976 }
977
978 /**
979 * Executes a query aggregating conversion data (everything in the **log_conversion** table) and returns
980 * a DB statement that can be used to iterate over the result.
981 *
982 * <a name="queryConversionsByDimension-result-set"></a>
983 * **Result Set**
984 *
985 * Each row of the result set represents an aggregated group of conversions. The
986 * following columns are in each aggregate row:
987 *
988 * - **{@link Piwik\Metrics::INDEX_GOAL_NB_CONVERSIONS}**: The total number of conversions in this aggregate
989 * group.
990 * - **{@link Piwik\Metrics::INDEX_GOAL_NB_VISITS_CONVERTED}**: The total number of visits during which these
991 * conversions were converted.
992 * - **{@link Piwik\Metrics::INDEX_GOAL_REVENUE}**: The total revenue generated by these conversions. This value
993 * includes the revenue from individual ecommerce items.
994 * - **{@link Piwik\Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL}**: The total cost of all ecommerce items sold
995 * within these conversions. This value does not
996 * include tax, shipping or any applied discount.
997 *
998 * _This metric is only applicable to the special
999 * **ecommerce** goal (where `idGoal == 'ecommerceOrder'`)._
1000 * - **{@link Piwik\Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_TAX}**: The total tax applied to every transaction in these
1001 * conversions.
1002 *
1003 * _This metric is only applicable to the special
1004 * **ecommerce** goal (where `idGoal == 'ecommerceOrder'`)._
1005 * - **{@link Piwik\Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING}**: The total shipping cost for every transaction
1006 * in these conversions.
1007 *
1008 * _This metric is only applicable to the special
1009 * **ecommerce** goal (where `idGoal == 'ecommerceOrder'`)._
1010 * - **{@link Piwik\Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT}**: The total discount applied to every transaction
1011 * in these conversions.
1012 *
1013 * _This metric is only applicable to the special
1014 * **ecommerce** goal (where `idGoal == 'ecommerceOrder'`)._
1015 * - **{@link Piwik\Metrics::INDEX_GOAL_ECOMMERCE_ITEMS}**: The total number of ecommerce items sold in each transaction
1016 * in these conversions.
1017 *
1018 * _This metric is only applicable to the special
1019 * **ecommerce** goal (where `idGoal == 'ecommerceOrder'`)._
1020 *
1021 * Additional data can be selected through the `$additionalSelects` parameter.
1022 *
1023 * _Note: This method will only query the **log_conversion** table. Other tables cannot be joined
1024 * using this method._
1025 *
1026 * @param array|string $dimensions One or more **SELECT** fields that will be used to group the log_conversion
1027 * rows by. This parameter determines which **log_conversion** rows will be
1028 * aggregated together.
1029 * @param bool|string $where An optional SQL expression used in the SQL's **WHERE** clause.
1030 * @param array $additionalSelects Additional SELECT fields that are not included in the group by
1031 * clause. These can be aggregate expressions, eg, `SUM(somecol)`.
1032 * @return \Zend_Db_Statement
1033 */
1034 public function queryConversionsByDimension($dimensions = array(), $where = false, $additionalSelects = array(), $extraFrom = [])
1035 {
1036 $dimensions = array_merge(array(self::IDGOAL_FIELD), $dimensions);
1037 $tableName = self::LOG_CONVERSION_TABLE;
1038 $availableMetrics = $this->getConversionsMetricFields();
1039
1040 $select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics);
1041
1042 $from = array_merge([$tableName], $extraFrom);
1043 $where = $this->getWhereStatement($tableName, self::CONVERSION_DATETIME_FIELD, $where);
1044 $groupBy = $this->getGroupByStatement($dimensions, $tableName);
1045 $orderBy = false;
1046 $query = $this->generateQuery($select, $from, $where, $groupBy, $orderBy);
1047
1048 return $this->getDb()->query($query['sql'], $query['bind']);
1049 }
1050
1051 /**
1052 * Creates and returns an array of SQL `SELECT` expressions that will each count how
1053 * many rows have a column whose value is within a certain range.
1054 *
1055 * **Note:** The result of this function is meant for use in the `$additionalSelects` parameter
1056 * in one of the query... methods (for example {@link queryVisitsByDimension()}).
1057 *
1058 * **Example**
1059 *
1060 * // summarize one column
1061 * $visitTotalActionsRanges = array(
1062 * array(1, 1),
1063 * array(2, 10),
1064 * array(10)
1065 * );
1066 * $selects = LogAggregator::getSelectsFromRangedColumn('visit_total_actions', $visitTotalActionsRanges, 'log_visit', 'vta');
1067 *
1068 * // summarize another column in the same request
1069 * $visitCountVisitsRanges = array(
1070 * array(1, 1),
1071 * array(2, 20),
1072 * array(20)
1073 * );
1074 * $selects = array_merge(
1075 * $selects,
1076 * LogAggregator::getSelectsFromRangedColumn('visitor_count_visits', $visitCountVisitsRanges, 'log_visit', 'vcv')
1077 * );
1078 *
1079 * // perform the query
1080 * $logAggregator = // get the LogAggregator somehow
1081 * $query = $logAggregator->queryVisitsByDimension($dimensions = array(), $where = false, $selects);
1082 * $tableSummary = $query->fetch();
1083 *
1084 * $numberOfVisitsWithOneAction = $tableSummary['vta0'];
1085 * $numberOfVisitsBetweenTwoAnd10 = $tableSummary['vta1'];
1086 *
1087 * $numberOfVisitsWithVisitCountOfOne = $tableSummary['vcv0'];
1088 *
1089 * @param string $column The name of a column in `$table` that will be summarized.
1090 * @param array $ranges The array of ranges over which the data in the table
1091 * will be summarized. For example,
1092 * ```
1093 * array(
1094 * array(1, 1),
1095 * array(2, 2),
1096 * array(3, 8),
1097 * array(8) // everything over 8
1098 * )
1099 * ```
1100 * @param string $table The unprefixed name of the table whose rows will be summarized.
1101 * @param string $selectColumnPrefix The prefix to prepend to each SELECT expression. This
1102 * prefix is used to differentiate different sets of
1103 * range summarization SELECTs. You can supply different
1104 * values to this argument to summarize several columns
1105 * in one query (see above for an example).
1106 * @param bool $restrictToReturningVisitors Whether to only summarize rows that belong to
1107 * visits of returning visitors or not. If this
1108 * argument is true, then the SELECT expressions
1109 * returned can only be used with the
1110 * {@link queryVisitsByDimension()} method.
1111 * @return array An array of SQL SELECT expressions, for example,
1112 * ```
1113 * array(
1114 * 'sum(case when log_visit.visit_total_actions between 0 and 2 then 1 else 0 end) as vta0',
1115 * 'sum(case when log_visit.visit_total_actions > 2 then 1 else 0 end) as vta1'
1116 * )
1117 * ```
1118 * @api
1119 */
1120 public static function getSelectsFromRangedColumn($column, $ranges, $table, $selectColumnPrefix, $restrictToReturningVisitors = false)
1121 {
1122 $selects = array();
1123 $extraCondition = '';
1124
1125 if ($restrictToReturningVisitors) {
1126 // extra condition for the SQL SELECT that makes sure only returning visits are counted
1127 // when creating the 'days since last visit' report
1128 $extraCondition = 'and log_visit.visitor_returning = 1';
1129 $extraSelect = "sum(case when log_visit.visitor_returning = 0 then 1 else 0 end) "
1130 . " as `" . $selectColumnPrefix . 'General_NewVisits' . "`";
1131 $selects[] = $extraSelect;
1132 }
1133
1134 foreach ($ranges as $gap) {
1135 if (count($gap) == 2) {
1136 $lowerBound = $gap[0];
1137 $upperBound = $gap[1];
1138
1139 $selectAs = "$selectColumnPrefix$lowerBound-$upperBound";
1140
1141 $selects[] = "sum(case when $table.$column between $lowerBound and $upperBound $extraCondition" .
1142 " then 1 else 0 end) as `$selectAs`";
1143 } else {
1144 $lowerBound = $gap[0];
1145
1146 $selectAs = $selectColumnPrefix . ($lowerBound + 1) . urlencode('+');
1147 $selects[] = "sum(case when $table.$column > $lowerBound $extraCondition then 1 else 0 end) as `$selectAs`";
1148 }
1149 }
1150
1151 return $selects;
1152 }
1153
1154 /**
1155 * Clean up the row data and return values.
1156 * $lookForThisPrefix can be used to make sure only SOME of the data in $row is used.
1157 *
1158 * The array will have one column $columnName
1159 *
1160 * @param $row
1161 * @param $columnName
1162 * @param bool $lookForThisPrefix A string that identifies which elements of $row to use
1163 * in the result. Every key of $row that starts with this
1164 * value is used.
1165 * @return array
1166 */
1167 public static function makeArrayOneColumn($row, $columnName, $lookForThisPrefix = false)
1168 {
1169 $cleanRow = array();
1170
1171 foreach ($row as $label => $count) {
1172 if (empty($lookForThisPrefix)
1173 || strpos($label, $lookForThisPrefix) === 0
1174 ) {
1175 $cleanLabel = substr($label, strlen($lookForThisPrefix));
1176 $cleanRow[$cleanLabel] = array($columnName => $count);
1177 }
1178 }
1179
1180 return $cleanRow;
1181 }
1182
1183 public function getDb()
1184 {
1185 /** @var ArchivingStatus $archivingStatus */
1186 $archivingStatus = StaticContainer::get(ArchivingStatus::class);
1187 $archivingLock = $archivingStatus->getCurrentArchivingLock();
1188 return new ArchivingDbAdapter(Db::getReader(), $archivingLock, $this->logger);
1189 }
1190 }
1191