SQL计算用户留存率,原理及流程拆解

问题及分析

根据用户记录,按一定时间周期,计算用户留存量、留存率。

留存率 = 留存量 / 初始量 * 100%

从某一个时间段作为起点,作为初始用户,一年后这些用户还有多少,两年后还有多少... 以此即可做计算。这里有个问题,就是这个起点的用户,是否也是从更早以前即存在、并留存到现在的;如果把本身即是“留存”而来的,留存率是不准确的。所以,通常要以用户首次出现开始计算。

简单来说,就是从新用户开始,计算其N一个时间段后留存;而每个时间段,都有新用户,所以这是个斜三角形的表。

以客户的销售记录为例,分步拆解计算原理。

已有原始数据表,包含了每个客户的销售记录,包括销售时间、销售数量、销售金额等。按年计算客户留存。

第一步,计算出初级表。

按年(yr)计算每个客户(cust)的统计信息,销量(sales)、销售金额(amount)。逻辑上,该结果表有个惟一键 (yr+cust),后续所有计算都从该表出发。

表结构如下

CREATE TABLE `yrept` (
  `cust` int(11) NOT NULL DEFAULT 0 COMMENT '客户',
  `yr` int(11) NOT NULL DEFAULT 0 COMMENT '年份',
  `sales` int(11) NOT NULL DEFAULT 0 COMMENT '销量',
  `amount` decimal(30,2) NOT NULL DEFAULT 0.00 COMMENT '金额',
  UNIQUE KEY `cust_yr` (`cust`,`yr`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8
 COMMENT='每个客户cust在每个年份yr上的统计';
-- 数据表见附件
每个客户cust在每个年份yr上的统计

每个客户cust在每个年份yr上的统计

从原始数据表计算出一张每个时间段里每个用户的统计信息,

第二步,计算每个客户首次出现的年份。

SELECT cust,min(yr) as yr FROM `yrept` GROUP BY `cust`;

第三步,从原表yrept入手,扩展表,联入每个行对应客户的首次出现年份,预览数据。

原表yrept是每客户在各年份上的统计,那为该表增加一个字段,该行客户cust的首次出现的年份yr_1st ,语句如下

SELECT a.*,'|' as s,b.*,'|' as p, a.yr-b.yr_1st AS `later_yr`
FROM `yrept` a INNER JOIN (
    SELECT cust,min(yr) as yr_1st FROM `yrept` GROUP BY `cust`
) b ON a.cust=b.cust
WHERE 1
ORDER BY b.cust, b.yr_1st
;

另加一个计算列,相对于客户首次出现后的年数 later_yr。

汇总统计

上步结果表,关注其中的 cust, yr_1st, yr 三列,即是客户cust首次出现起,之后每年留存的客户信息(有对应记录即是留存)。对 yr_1st + yr 做汇总统计,即是每个年份新增客户数目、及之后每个年份的留存统计。

SELECT b.yr_1st,a.yr AS in_yr,count(*) as cnt
  ,count(DISTINCT a.cust) as unq_cust, SUM(a.sales) as sales 
  -- ,SUM(a.amount) as amount
FROM yrept a INNER JOIN (
    SELECT cust,min(yr) as yr_1st FROM yrept GROUP BY cust 
) b ON a.cust=b.cust 
WHERE 1 
GROUP BY b.yr_1st,in_yr 
ORDER BY b.yr_1st,in_yr
;

换个形式,以 将 yr 换成later_yr做统计,即客户cust首次出现起,N年后的留信息。事实这两种形式是等价的。

SELECT b.yr_1st,a.yr-b.yr_1st AS later_yr,count(*) as cnt
  ,count(DISTINCT a.cust) as unq_cust, SUM(a.sales) as sales
  -- ,SUM(a.amount) as amount
FROM yrept a INNER JOIN (
    SELECT cust,min(yr) as yr_1st FROM yrept GROUP BY cust
) b ON a.cust=b.cust
WHERE 1
GROUP BY b.yr_1st,later_yr
ORDER BY b.yr_1st,later_yr
;

以上两个结果,其中的 cnt 与unq_cust 两列,事实上也是等价的,就是惟一客户人数。上两条语句中,还有注释掉的一行是对总金额的计算,可以按销售金额来计算留存,有需要可选用。

按需要平摊成需要的形式

比如以yr_1st为行、按later_yr或in_year 分成多列展示。

计算留存率

观察上面平摊表,从新客户开始逐年的留存率的计算就很直观了。

如果对按右表整列求和,得到按年的所有新客户、逐年总留存率,可视为多年的新客户整体留存率,也是有意义的。虽然不太直观。

甚至,还可以对右表整列求和,并计算留率,可视为每年的所有客户(包括新客与留存客户)在之后每年的留存率。

MySQL语句实现字段拆分成多行

问题简述

如上图,拆分字段为多行,只使用MySQL本身功能,不借助其它工具。

下页结合实例详述原理,最后是完整的拆分语句。实际使用中,只需把最后一节的语句当作模板,修改字段名、表名即可。

核心知识点

    • 表连接关系 JOIN... ON 中使用小于号,而非通常用的等号
    • MySQL函数 SUBSTRING_INDEX() 详参MariaDB文档MySQL文档

示例数据表article_tags

文章标签表,文章编号aid,及文章标签`tags` ;即待拆分tags数据,分隔符为 " | ",竖线及前后空格共计三个字符。

CREATE TABLE `article_tags` (
  `aid` int(11) NOT NULL COMMENT 'article id',
  `tags` varchar(255) NOT NULL DEFAULT '',
  KEY `aid` (`aid`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8;

辅助表sp_idx

拆分过程需要一张辅助表,包含了正整数的序列的一列数据。可按下页语句构造1024行。

CREATE TABLE `sp_idx` (
 `id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
 `x` bit(1) NOT NULL DEFAULT b'0',
 PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='拆分辅助表(正整数序列)';
INSERT into `sp_idx`(`id`,`x`) values(1,0);
INSERT into `sp_idx`(`x`) SELECT `x` FROM `sp_idx`;
INSERT into `sp_idx`(`x`) SELECT `x` FROM `sp_idx`;
INSERT into `sp_idx`(`x`) SELECT `x` FROM `sp_idx`;
INSERT into `sp_idx`(`x`) SELECT `x` FROM `sp_idx`;
INSERT into `sp_idx`(`x`) SELECT `x` FROM `sp_idx`;
INSERT into `sp_idx`(`x`) SELECT `x` FROM `sp_idx`;
INSERT into `sp_idx`(`x`) SELECT `x` FROM `sp_idx`;
INSERT into `sp_idx`(`x`) SELECT `x` FROM `sp_idx`;
INSERT into `sp_idx`(`x`) SELECT `x` FROM `sp_idx`;
INSERT into `sp_idx`(`x`) SELECT `x` FROM `sp_idx`;

计算每条记录拆分条数

计算每行要拆分成的行数据,即有多少条数据通过分隔符拼接在一起。即计算字符串中删除分隔符后长度的减少量,即是 分隔符的个数*分隔符长度。分隔符个数+1 即是拆分条数。

这里使用char_length()计算长度;因为分隔符可能包含ASCII码,故不用length()。

SELECT r.`aid`, `tags`,
 (char_length(`tags`)-char_length(REPLACE(`tags`,' | ',''))) DIV char_length(' | ') + 1 as kw_count 
FROM `article_tags` r ;

联入入辅助表

按上步计算出的kw_count,联入相应条数的序列数据,从1 到 kw_count,序列数据从辅助表取。联表关系使用 < ,而不是通常使用的等于。

SELECT r.`aid`, `tags`
 ,x.`id`
FROM `article_tags` r INNER JOIN `sp_idx` x 
ON x.`id` <= (char_length(tags)-char_length(REPLACE(tags,' | ',''))) DIV char_length(' | ') + 1
;

按序号取子串

结果记录集最后一列id,事实上就是原始字符的排序号。每条记录按该序号截取子串即是结果了。这里使用 SUBSTRING_INDEX() 函数,截取前N段或后N段子串。

第一步,如下,截前N段子串的结果,再截最后一段即是每行所需结果。

SELECT r.`aid`, `tags`
 ,x.`id`, substring_index(`tags`,' | ',x.`id`) as step_1
 ,substring_index(substring_index(`tags`,' | ',x.`id`),' | ',-1) as step_2
FROM `article_tags` r
INNER JOIN `sp_idx` x 
on x.`id` <= (char_length(tags)-char_length(REPLACE(tags,' | ',''))) DIV char_length(' | ') + 1
;

总结整个过程

SELECT r.`aid`, `tags`
,x.`id` as idx, substring_index(substring_index(`tags`,' | ',x.`id`),' | ',-1) as piece
FROM `article_tags` r INNER JOIN `sp_idx` x 
ON x.`id` <= (char_length(`tags`)-char_length(REPLACE(`tags`,' | ',''))) DIV char_length(' | ') + 1 
WHERE r.`tags`!='' HAVING `piece` !='';

该语句,可以当作模板使用,其中:

  • `tags` 替换为待拆分字段(共4处),' | ' 为分隔字符串(共4处);
  • `piece`为拆分结果列,`idx` 为 `piece`在原始`tags`中的序号;
  • 比前面步骤增加了WHERE及HAVING子句,用来排除原表tags为空字符串的行、及拆分piece为空字符串的行。

辅助表`sp_idx`需要事先建好,事实上只要从是从1开始的整数序列即可;如果待拆分子串数超过1024,则要扩充本表,多跑几次该句即可:INSERT into `sp_idx`(`x`) SELECT `x` FROM `sp_idx`;

如果实在不方便建辅助表,可以使用`mysql`. `help_topic`表的help_topic_id字段(不推荐)。要能保证其完整性,而且最大拆分子串数不大于`help_topic`的总行数。而且还要多加个where条件 help_topic_id>0,否则会拆分出多余的空串行,因为help_topic_id字段是从0开始编号的。

附件:

示例数据及SQL脚本 split_field_to_lines_in_mysql.zip

SQL Server中truncate、delete和drop的异同点

 相同点:

truncate和不带where子句的delete,以及drop都会删除表内的数据

 不同点:

1. truncate和delete只删除数据不删除表的结构(定义)

drop语句将删除表的结构被依赖的约束(constrain)、触发器(trigger)、索引(index);依赖于该表的存储过程/函数将保留,但是变为 invalid 状态。

2. delete语句是数据库操作语言(dml),这个操作会放到rollback segement中,事务提交之后才生效;如果有相应的trigger,执行的时候将被触发。

truncate、drop是数据库定义语言(ddl),操作立即生效,原数据不放到rollback segment中,不能回滚,操作不触发trigger。

3.delete语句不影响表所占用的extent,高水线(high watermark)保持原位置不动。

显然drop语句将表所占用的空间全部释放。

truncate语句缺省情况下见空间释放到minextents个extent,除非使用reuse storage;truncate 会将高水线复位(回到最开始)。

4.速度,一般来说: drop> truncate > delete

5.安全性:小心使用 drop和truncate,尤其没有备份的时候.否则哭都来不及。

使用上,想删除部分数据行用delete,注意带上where子句. 回滚段要足够大.

想删除表,当然用drop

想保留表而将所有数据删除,如果和事务无关,用truncate即可。如果和事务有关,或者想触发trigger,还是用delete。

如果是整理表内部的碎片,可以用truncate跟上reuse stroage,再重新导入/插入数据。

来源 http://www.cnblogs.com/fengjun19912/articles/308418.html

数据库中乱码记录查找与删除/sql server剔除乱码行

乱码一般都是正常编码被错误的解析而导致。每个错误的编码,都会对应着一个正确的编码,也就是,出现频率高的乱码字符,其实就是出现高频字符(一般都是汉字)对应的乱码。
所以说,如果找到高频字符对应的乱码,在sql表里,使用like 查询即可以找到乱码行。
也就是说,查找到一系列高频乱码字符,就可以很容易的查找到绝大多数乱码行。
由于有乱码的表里存储的数据可能是不同行业、不同领域的信息,高频汉字可能不尽相同。(生命科学领域里,细、胞、蛋、白、质、核、酸等这些字频率会非常高;而计算机领域里,内、存、储、变、量、进、程等汉字会是调频汉字;其它领域亦类似)。
所以查找剔除乱码记录时,最好是整理一个最准确乱码字符表。
下面给出一个整理乱码表比较直接、有效的方法(是笔者所探索发现的方法):
1. 找到一定数量的乱码记录(建议在200条以上)
2. 把这些乱码整理出来,并拆分成单字,并统计每个单字出现频率。(建议使用excel+UE/editplus等工具)
3. 取最高频的50个单字(这里当然是要找明显是乱码的单字;也可以不是50个字,80个100个也行)
4. 使用UE或excel生成对应的50个sql like语句,形式如下
select * from [乱码表] where [乱码字段] like '%锛%'
select * from [乱码表] where [乱码字段] like '%绂%'
select * from [乱码表] where [乱码字段] like '%脠%'
select * from [乱码表] where [乱码字段] like '%┖%'
select * from [乱码表] where [乱码字段] like '%锲%'
......

为了查询方便,最把生成一个union语句,如下
select * from [乱码表] where [乱码字段] like '%悎%' union
select * from [乱码表] where [乱码字段] like '%灏%' union
select * from [乱码表] where [乱码字段] like '%楁%' union
select * from [乱码表] where [乱码字段] like '%闀%' union
select * from [乱码表] where [乱码字段] like '%寰%' union
select * from [乱码表] where [乱码字段] like '%阍%' union
.....
5. 使用上步生成的sql语句查询,查询到结果基本上全部都是乱码行(把乱码记录复制下来,下一步有用),把这些行的主键ID号复制粘贴到ue/editplus,整理成一个逗号拼接形式,就可以使用一个in语句删除
delete from [乱码表] where [主键id] in(num1,num2,num3,...)
[如果上步语句查询结果里有非乱码行,则明记下非乱码行的ID号,在生成delete ... in ... 语句时,剔除这些ID号,就可保证万无一失]
6. 如果需要更进一步删除乱码,可对上一步记下的乱码记录做以下步骤:
单字分隔-统计词频-取法高频乱码单字-整理sql like语句-查询-确认-生成delete... in...语句-删除
这一过程可以继续多次进行,直至没有乱码行为止。

[注]
1. “乱码”只是习惯说法。乱码单字其实也都是正常的字符——多半是比较生僻的汉字,类似于时下流行已久的“火星文”。其中当然也有一些常见的汉字。
2. 上面提到清理乱码方法是提到的"200条乱码记录"、"50个乱码单字"是笔者操作中使用,你也可以增减这些数目,以达到最佳的清理效果。

变态的Access2007:在Access2007中创建SQL传递查询(执行SQl语句)

1. 在“创建”选项卡中,单击“其他”组中的“查询设计”。
2. 单击“显示表”对话框中的“关闭”,而不添加任何表或查询。
3. 在“设计”选项卡中,单击“查询类型”工作组中的“传递”。
4. 单击“显示/隐藏”工作组中的“属性表”以显示查询的属性表。
5. 在查询的属性表中,将鼠标指针置于“ODBC 连接字符串”属性中,然后单击“生成”(...) 按钮。
利用“ODBC 连接字符串”属性,可以指定与要连接的数据库有关的信息。可以键入连接信息,或者单击“生成”,然后输入与要连接的服务器有关的信息。
6. 当提示您是否在连接字符串中保存密码时,如果希望将密码和登录名存储在连接字符串信息中,请单击“是”。
7. 如果查询不属于可返回记录的类型,请将“ReturnsRecords”属性设置为“No”。
8. 在“SQL 传递查询”窗口中,键入您的传递查询。例如,下面的传递查询在 Select 语句中使用 Microsoft SQL Server TOP 运算符,以仅返回示例数据库的“订单”表中的前 25 份订单:
Select TOP 25 orderid from orders
9. 若要运行查询,请单击“设计”选项卡的“结果”组中的“运行”。对于返回记录的 SQL 传递查询,请单击状态栏上的“数据表视图”。
10. 如果需要,Microsoft Access 将提示您输入有关服务器数据库的信息