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