更改查询sql
Posted
技术标签:
【中文标题】更改查询sql【英文标题】:Change query sql 【发布时间】:2017-02-06 14:18:14 【问题描述】:我有以下查询来获取一年中 52 周的数据的平均值如下:
$dates = array();
$firstDate = date("Y-m-d", strtotime('first day of January 2016'));
$lastDate = date("Y-m-d", strtotime('last day of December 2016'));
for($i=strtotime($firstDate); $i<=strtotime($lastDate); $i+=86400 *7)
array_push($dates, date("Y-m-d", strtotime('monday this week', $i)));
for($i = 0; $i < count($dates); $i++)
$sql = "SELECT pr_products.product,
CONCAT(YEAR('".$dates[$i]."'),'-',LPAD(WEEK('".$dates[$i]."'),2,'0')) AS Week,
SUM(IF(sw_sowing.type = 'SW', sw_sowing.quantity,0)) AS PlantSowing,
SUM(IF(ROUND(DATEDIFF(TIMESTAMPADD(DAY,(6 WEEKDAY('".$dates[$i]."')),'".$dates[$i]."'), sw_sowing.date)/7) >= pr_products.week_production AND sw_sowing.type = 'SW',sw_sowing.quantity,0)) AS production
FROM (
SELECT max(sw_sowing.id) AS id
FROM sw_sowing
WHERE sw_sowing.status != 0
AND sw_sowing.id_tenant = :id_tenant
AND sw_sowing.status = 100
AND sw_sowing.date <= TIMESTAMPADD(DAY,(6-WEEKDAY('".$dates[$i]."')),'".$dates[$i]."')
GROUP BY sw_sowing.id_production_unit_detail
) AS sw
INNER JOIN sw_sowing ON sw_sowing.id = sw.id
INNER JOIN pr_products ON pr_products.id = sw_sowing.id_product
INNER JOIN pr_varieties ON sw_sowing.id_variety = pr_varieties.id
INNER JOIN pr_lands ON pr_lands.id = sw_sowing.id_land
WHERE pr_varieties.code != 1
AND sw_sowing.id_product = 1
AND sw_sowing.status = 100
GROUP BY pr_products.product
HAVING plantSowing > 0
ORDER BY pr_products.product";
我最初声明了两个变量,$firstdate
是开始日期,$lastDate
是结束日期。
然后我做一个 for 遍历这两个日期,并将每周星期一的日期保存在一个数组中。
然后我遍历那个新数组以每周获取我需要的数据。
注意:在查询中,变量 $dates[$i]
是每周的星期一日期。
无论如何,该查询运行良好,因为它为我提供了一年中 52 周所需的数据。问题是这需要一段时间。
我已经在mysql中索引了表,我改进了一点但还不够,查询实际上并不重,平均每个周期需要0.60
秒。
我想知道是否有可能删除我正在做的事情并在查询中添加我不知道的WHERE
,它比较两个日期并为我带来数据,或者是否有任何改进查询的方法。
我已经用答案的建议更新了查询:
$data = array();
$start = new DateTime('first monday of January 2016');
$end = new DateTime('last day of December 2016');
$datePeriod = new DatePeriod($start , new DateInterval('P7D') , $end);
$sql = "SELECT product AS product,
Week AS label,
ROUND(SUM(harvest)/SUM(production),2) AS value
FROM (
(
SELECT pr_products.product,
CONCAT(YEAR(:dates),'-', LPAD(WEEK(:dates1),2,'0')) AS Week,
SUM(IF(sw_sowing.type = 'SW', sw_sowing.quantity,0)) AS PlantSowing,
SUM(IF(ROUND(DATEDIFF(TIMESTAMPADD(DAY,(6-WEEKDAY(:dates2)),:dates3), sw_sowing.date)/7) >= pr_products.week_production AND sw_sowing.type = 'SW',sw_sowing.quantity,0)) AS production,
0 AS Harvest
FROM (
SELECT max(sw_sowing.id) AS id
FROM sw_sowing
WHERE sw_sowing.status != 0
AND sw_sowing.date <= TIMESTAMPADD(DAY,(6-WEEKDAY(:dates4)),:dates5)
GROUP BY sw_sowing.id_production_unit_detail
) AS sw
INNER JOIN sw_sowing ON sw_sowing.id = sw.id
INNER JOIN pr_products ON pr_products.id = sw_sowing.id_product
INNER JOIN pr_varieties ON sw_sowing.id_variety = pr_varieties.id
WHERE pr_varieties.code != 1
AND sw_sowing.id_product = 1
AND sw_sowing.status = 100
AND sw_sowing.id_tenant = :id_tenant
GROUP BY pr_products.product
HAVING plantSowing > 0
ORDER BY pr_products.product
)
UNION ALL
(
SELECT pr_products.product,
CONCAT(YEAR(:dates6),'-', LPAD(WEEK(:dates7),2,'0')) AS Week,
0 AS plantSowing,
0 AS Production,
SUM(pf_harvest.quantity) AS Harvest
FROM pf_harvest
INNER JOIN pr_products ON pr_products.id = pf_harvest.id_product
INNER JOIN pr_varieties ON pr_varieties.id = pf_harvest.id_variety
INNER JOIN pf_performance ON pf_performance.id = pf_harvest.id_performance
WHERE pf_harvest.date BETWEEN TIMESTAMPADD(DAY,(0-WEEKDAY(:dates8)),:dates9)
AND TIMESTAMPADD(DAY,(6-WEEKDAY(:dates10)),:dates11)
AND pr_varieties.code != 1
AND pf_harvest.id_product = 1
AND pf_performance.status = 100
AND pf_harvest.id_tenant = :id_tenant1
GROUP BY pr_products.product
ORDER BY pr_products.product
)
) AS sc
GROUP BY product, label
ORDER BY label";
$statement = $this->db->prepare($sql);
$id_tenant = $this->getIdTenant();
foreach($datePeriod AS $dates)
$values = [
':dates' => $dates->format('Y-m-d'),
':dates1' => $dates->format('Y-m-d'),
':dates2' => $dates->format('Y-m-d'),
':dates3' => $dates->format('Y-m-d'),
':dates4' => $dates->format('Y-m-d'),
':dates5' => $dates->format('Y-m-d'),
':dates6' => $dates->format('Y-m-d'),
':dates7' => $dates->format('Y-m-d'),
':dates8' => $dates->format('Y-m-d'),
':dates9' => $dates->format('Y-m-d'),
':dates10' => $dates->format('Y-m-d'),
':dates11' => $dates->format('Y-m-d'),
':id_tenant' => $id_tenant,
':id_tenant1' => $id_tenant
];
$result = $this->db->executePrepared($statement , $values);
$data[] = $result->fetchAll();
$this -> jsonReturnSuccess($data);
【问题讨论】:
在我看来,当您开发通过相同 sql 进行迭代的查询时,您遇到了常见的 N+1 问题。 ***.com/questions/97197/…。我对您的建议是重构您的查询以使用 weekday() 函数来识别星期一(星期一返回为 0)并将所有行检索到一个数据集中。我没有分析您的查询,也不太了解您的数据集,无法为您重构它,但这应该是可能的。将数据放入数组后,您可以循环遍历它以在 php 中获取所需的数据。 我正在尝试重构我的查询,我想删除 for,但我认为这很难,谢谢你的建议! 如果您可以为正在连接的表提供一些示例数据以及结果输出的前几行,我可以试一试。 【参考方案1】:您必须考虑到您的整个代码都需要改进。首先,利用 PHP 的资源来做一些改进。
日期期间
创建一年中的第一个和最后一个星期一之间的时间段。使用DatePeriod:
$start = new \DateTime('first monday of this year');
$end = new \DateTime('first day of next year');//The last date is never reached, so if the year ends in a monday, you won't have any problem
$datePeriod = new \DatePeriod($start , new \DateInterval('P7D') , $end);
foreach($datePeriod as $date)
echo $period->format('Y-m-d');
与您的 for
循环相比,它非常快。
array_push
注意:如果您使用 array_push() 向数组添加一个元素,最好使用 $array[] = 因为这样就没有调用函数的开销。
如手册所说,如果要向数组中添加元素,请使用 $array[]。
准备好的声明
我发现的其他常见问题,请使用prepared statement
。它可用于让您的查询获得“预编译”状态(简单示例):
$query = 'SELECT * FROM table WHERE id = :id';
$statement = $pdo->prepare($query);
$idArray = [1 , 2 , 3 , 4 , 5 , /** very long array, as your date list **/ ];
foreach($idArray as $id)
$statement->execute(array(':id' => $id));
$result = $statement->fetchAll();
N+1 问题
另一种方法是关于 N+1 问题。如果其他提示不足以加快速度,您可以使用本机函数 (array_map, array_walk, array_filter, etc...) 收集值并执行单个请求。
看看: What is SELECT N+1? https://secure.phabricator.com/book/phabcontrib/article/n_plus_one/
查询
最后,我需要有关您的查询的更多信息。您正在使用许多 mysql
函数。这是我得到的最后一个似是而非的暗示。但是,正如您所说,查询执行速度很快。尝试取出这些函数并检查脚本的执行是否得到改进。
更新
首先,我认为您在 MySQL 函数中使用了如此多的 PHP 变量。如果您只需要获取年份和月份 (yyyy-mm),请使用 DateTime::format()。
$date->format('Y-m');//2017-02
手册上有很多例子。
正如我之前所说,prepared statement 是一种“预编译”查询。您必须使用占位符(命名或位置)而不是变量来编写查询。上面的查询就是我的例子:
$query = "SELECT *
FROM
mytable
INNER JOIN mysecondtable ON (mytable.id = mysecondtable.id_mytable)
WHERE
mytable.date BETWEEN :start AND :end
AND mytable.value >= :value;";
你已经有了 foreach:
$data = array();
$start = new DateTime('first monday of January 2016');
$end = new DateTime('last day of December 2016');
$datePeriod = new DatePeriod($start , new DateInterval('P7D') , $end);
foreach($datePeriod AS $dates)
//your code
现在,您必须在 foreach
循环之外“准备”您的查询:
$statement = $this->db->prepare($query);
foreach($datePeriod AS $dates)
//your code
在你的 foreach 中,只需要使用占位符。
foreach($datePeriod AS $dates)
$values = [
'start' => $dates->format('Y-m-d'),
'end' => $dates->add(new DateInterval('P7D'))->format('Y-m-d'),//add 7 to reach a week
'value' => 10
];
$types = [
'start' => Column::BIND_PARAM_STR,
'end' => Column::BIND_PARAM_STR,
'value' => Column::BIND_PARAM_INT
]
//Phalcon PDO Adapter method
$result = $connection->executePrepared($statement , $values , $types);//The result is a PDOStatement object
$data[] = $result->fetchAll();
通过这些技巧,您可以大大缩短脚本的执行时间。
【讨论】:
感谢您的回答,我将查看每一步,关于我的查询,了解一年中的每个星期一很重要,因为我有“生产”,所以大问题是当我使用时,我必须等到完成 @F***Sierra 对于 DatePeriod,第一个for
不是必需的,正如您在我的示例中看到的那样。
太棒了!你能再给我一个例子吗?我会这样做,我会告诉你的!
我已经用foreach
循环更新了这个例子。至少,该示例功能齐全。
感谢它有效!所以我要重构我的查询!【参考方案2】:
没有循环,几乎没有 PHP 代码,只有一个 SQL:
SELECT
WEEK(date),
AVG(...)
FROM tbl
JOIN ...
WHERE date >= '2016-01-01'
AND date < '2016-01-01' + INTERVAL 1 YEAR
GROUP BY week(date);
我可能遗漏了一些细节,但这可能会为您指明有助于简化代码的方向。
6 周前的星期一(从今天开始,即星期二):
mysql> SET @d := CURDATE();
mysql> SELECT @d, DAYOFWEEK(@d), TO_DAYS(@d),
TO_DAYS(@d + INTERVAL 2 DAY) - DAYOFWEEK(@d) AS ThisMon;
+------------+---------------+-------------+---------+
| @d | DAYOFWEEK(@d) | TO_DAYS(@d) | ThisMon |
+------------+---------------+-------------+---------+
| 2017-02-07 | 3 | 736732 | 736731 |
+------------+---------------+-------------+---------+
mysql> SELECT FROM_DAYS(736731 - 6*7);
+-------------------------+
| FROM_DAYS(736731 - 6*7) |
+-------------------------+
| 2016-12-26 |
+-------------------------+
【讨论】:
但在我的情况下,我需要DATEDIFF
中的日期,我不知道该怎么做!
请解释DATEDIFF
的目的。并修复语法错误:...(6 WEEKDAY(...
在我的特殊情况下,我需要知道 2016 年每个星期一的日期,到那个日期我需要减去 6 周,因为这样系统被参数化以了解产品是否在生产中或者不是@RickJames
使用TO_DAYS(date)
和INTERVAL
进行一点算术运算即可计算得出。
我要回顾一下我该怎么做:)以上是关于更改查询sql的主要内容,如果未能解决你的问题,请参考以下文章