热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

本篇文章主要介绍flink的关系型API

本篇文章主要介绍flink的关系型API,整个文章主要分为下面几个部分来介绍:什么是flink关系型APIflink关系型API的各版本演进flink关系型API执行原

本篇文章主要介绍flink的关系型API,整个文章主要分为下面几个部分来介绍:

  1. 什么是flink关系型API
  2. flink关系型API的各版本演进
  3. flink关系型API执行原理
  4. flink关系型API目前适用场景
  5. Table & SQL API介绍
  6. Table & SQL API举例
  7. 动态表
  8. 总结

一、什么是flink关系型API

当我们在使用flink做流式和批式任务计算的时候,往往会想到几个问题:

  1. 需要熟悉两套API : DataStream/DataSetAPI,API有一定难度,开发人员无法focus on business
  2. 需要有Java或Scala的开发经验
  3. flink同时支持批任务与流任务,如何做到API层的统一

flink已经拥有了强大的DataStream/DataSetAPI,满足流计算和批计算中的各种场景需求,但是关于以上几个问题,我们还需要提供一种关系型的API来实现flink API层的流与批的统一,那么这就是flink的Table & SQL API。

首先Table & SQL API是一种关系型API,用户可以像操作MySQL数据库表一样的操作数据,而不需要写Java代码完成flink function,更不需要手工的优化Java代码调优。另外,SQL作为一个非程序员可操作的语言,学习成本很低,如果一个系统提供SQL支持,将很容易被用户接受。

总结来说,关系型API的好处:

  1. 关系型API是声明式的
  2. 查询能够被有效的优化
  3. 查询可以高效的执行
  4. “Everybody” knows SQL

Table & SQL API是流处理和批处理统一的API层,如下图。flink在runtime层是统一的,因为flink将批任务看做流的一种特例来执行,然而在API层,flink为批和流提供了两套API(DataSet和DataStream)。所以Table & SQL API就统一了flink的API层,批数据上的查询会随着输入数据的结束而结束并生成DataSet,流数据的查询会一直运行并生成结果流。Table & SQL API做到了批与流上的查询具有同样的语法语义,因此不用改代码就能同时在批和流上执行。

图片 20

关于DataSet API和DataStream API对应的Table如下图:

图片 16

图片 17

二、flink关系型API的各版本演进

关于Table & SQL API,flink在0.9版本的时候,引进了Table API,支持Java和Scala两种语言,是一个类似于LINQ模式的API。用于对关系型数据进行处理。这系列 Table API的操作对象就是能够进行简单的关系型操作的结构化数据流。结构如下图。然而0.9版本的Table & SQL API有着很大的局限性,0.9版本Table API不能单独使用,必须嵌入到DataSet或者DataStream的程序中,对于批处理表的查询并不支持outer join、order by等操作。在流处理Table中只支持filters、union,不支持aggregations以及joins。并且,这个转化处理过程没有查询优化。整体来说0.9版本的flink Table API还不是十分好用。

图片 18

在后续的版本中,1.1.0引入了 SQL,因此在1.1.0 版本以后,flink 提供了两个语义的关系型API:语言内嵌的 Table API(用于 Java 和 Scala)以及标准 SQL。这两种 API 被设计用于在流和批的任务中处理数据在API层的统一,这意味着无论输入是批处理数据还是流数据,查询产生完全相同的结果。

在1.20版本之后逐渐增加SQL的功能,并对Table API做了大量的enhancement了。在1.2.0 版本中,flink的关系API在数据流中,支持关系操作包括投影、过滤和窗口聚合。

在1.30版本中开始支持各种流上SQL操作,例如SELECT、FROM、WHERE、UNION、aggregation和UDF能力。在2017年3月2日进行的flink meetup与2017年5月24日Strata会议,flink都有相应的topic讨论,未来在flink SQL方面会支持更细粒度的join操作和对dynamic table的支持。

三、flink关系型API执行原理

flink使用基于Apache Calcite这个SQL解析器做SQL语义解析。利用Calcite的查询优化框架与SQL解释器来进行SQL的解析、查询优化、逻辑树生成,得到Calcite的RelRoot类的一颗逻辑执行计划树,并最终生成flink的Table。Table里的执行计划会转化成DataSet或DataStream的计算,经历物理执行计划优化等步骤。但是,Table API和 SQL最终还是基于flink的已有的DataStream API和DataSet API,任何对于DataStream API和DataSet API的性能调优提升都能够自动地提升Table API或者SQL查询的效率。这两种API的查询都会用包含注册过的Table的catalog进行验证,然后转换成统一Calcite的logical plan。再利用 Calcite的优化器优化转换规则和logical plan。根据数据源的性质(流和批)使用不同的规则进行优化。最终优化后的plan转传成常规的flink DataSet或 DataStream程序。结构如下图: 

图片 19

3.1 Translation to Logical Plan

每次调用Table&SQL API,就会生成Flink 逻辑计划的节点。比如对groupBy和select的调用会生成节点Project、Aggregate、Project,而filter的调用会生成节点Filter。这些节点的逻辑关系,就会组成下图的一个Flink 自身数据结构表达的一颗逻辑树; 根据这个已经生成的Flink的 logical Plan,将它转换成calcite的logical Plan,这样我们才能用到calcite强大的优化规则。Flink由上往下依次调用各个节点的construct方法,将Flink节点转换成calcite的RelNode节点。

图片 21

3.2 Translation to DataStream Plan

优化逻辑计划并转换成Flink的物理计划,Flink的这部分实现统一封装在optimize方法里头。这部分涉及到多个阶段,每个阶段都是用Rule对逻辑计划进行优化和改进。声明定义于派生RelOptRule的一个类,然后再构造函数中要求传入RelOptRuleOperand对象,该对象需要传入这个Rule将要匹配的节点类型。如果这个自定义的Rule只用于LogicalTableScan节点,那么这个operand对象应该是operand(LogicalTableScan.class, any())。通过以上代码对逻辑计划进行了优化和转换,最后会将逻辑计划的每个节点转换成Flink Node,既可物理计划。

图片 22

 

3.3 Translation to Flink Program

图片 23

四、flink关系型API目前适用场景

4.1 目前支持范围

Batch SQL & Table API 支持:

  • Selection, Projection, Sort, Inner & Outer Joins, Set operations
  • Windows for Slide, Tumble, Session

Streaming Table API 支持:

  • Selection, Projection, Union
  • Windows for Slide, Tumble, Session

Streaming SQL:

  • Selection, Projection, Union, Tumble

4.2 目前使用场景

图片 24

五、 Table & SQL API介绍

Table API一般与DataSet或者DataStream紧密关联,可以通过一个DataSet或DataStream创建出一个Table,再用类似于filter, join, 或者 select关系型转化操作来转化为一个新的Table对象。最后将一个Table对象转回一个DataSet或DataStream。从内部实现上来说,所有应用于Table的转化操作都变成一棵逻辑表操作树,在Table对象被转化回DataSet或者DataStream之后,转化器会将逻辑表操作树转化为对等的DataSet或者DataStream操作符。

5.1 Table & SQL API的简单介绍

① Create a TableEnvironment

TableEnvironment对象是Table API和SQL集成的一个核心,支持以下场景:

  • 注册一个Table
  • 注册一个外部的catalog
  • 执行SQL查询
  • 注册一个用户自定义的function
  • 将DataStream或DataSet转成Table

一个查询中只能绑定一个指定的TableEnvironment,TableEnvironment可以通过来配置TableConfig来配置,通过TableConfig可以自定义查询优化以及translation的进程。

TableEnvironment执行过程如下:

  1. TableEnvironment.sql()为调用入口
  2. flink实现了个FlinkPlannerImpl,执行parse(sql),validate(sqlNode),rel(sqlNode)操作
  3. 生成Table

其中,LogicalRelNode是flink执行计算树里的叶子节点。

源码如下图:

图片 1

图片 2

② Register a Table

  1. 将一个Table注册给TableEnvironment图片 3
  2. 将一个TableSource注册给TableEnvironment,这里的TableSource指的是将数据存储系统的作为Table,例如MySQL, hbase, CSV, Kakfa, RabbitMQ等等
  3. 将一个外部的Catalog注册给TableEnvironment,访问外部系统的数据或文件
  4. 将DataStream或DataSet注册为Table图片 4

③ Query a Table

  • Table API
    Table API是一个Scala和Java的集成查询序言。与SQL不同的是,Table API的查询不是一个指定的sql字符串,而是调用指定的API方法。Table API中的每一个方法输入都是一个Table,输出也是一个新的Table。

通过table API来提交任务的话,也会经过calcite优化等阶段,基本流程和直接运行SQL类似:

  1. table API parser: flink会把table API表达的计算逻辑也表示成逻辑树,用treeNode表示;
  2. 在这棵树上的每个节点的计算逻辑用Expression来表示。
  3. Validate: 会结合catalog将树的每个节点的Unresolved Expression进行绑定,生成Resolved Expression;
  4. 生成Logical Plan: 依次遍历数的每个节点,调用construct方法将原先用treeNode表达的节点转成成用calcite 内部的数据结构relNode 来表达。即生成了LogicalPlan, 用relNode表示;
  5. 生成 optimized Logical Plan: 先基于calcite rules 去优化logical Plan,
  6. 再基于flink定制的一些优化rules去优化logical Plan;
  7. 生成Flink Physical Plan: 这里也是基于flink里头的rules将,将optimized LogicalPlan转成成Flink的物理执行计划;
  8. 将物理执行计划转成Flink Execution Plan: 就是调用相应的tanslateToPlan方法转换。图片 5
  • SQL

Flink SQL是基于Apache Calcite的实现的,Calcite实现了SQL标准解析。SQL查询是一个完整的sql字符串来查询。一条stream sql从提交到calcite解析、优化最后到flink引擎执行,一般分为以下几个阶段:

  1. Sql Parser: 将sql语句解析成一个逻辑树,在calcite中用SqlNode表示逻辑树;
  2. Sql Validator: 结合catalog去验证sql语法;
  3. 生成Logical Plan: 将sqlNode表示的逻辑树转换成Logical Plan, 用relNode表示;
  4. 生成 optimized Logical Plan: 先基于calcite rules 去优化logical Plan;
  5. 再基于flink定制的一些优化rules去优化logical Plan;
  6. 生成Flink Physical Plan: 这里也是基于flink里头的rules将,将optimized Logical Plan转成成Flink的物理执行计划;
  7. 将物理执行计划转成Flink Execution Plan: 就是调用相应的tanslateToPlan方法转换。

图片 6

③ Table&SQL API混合使用

Table API和SQL查询可以很容易的混合使用,因为它们的返回结果都是Table对象。一个基于Table API的查询可以基于一个SQL查询的结果。同样地,一个SQL查询可以被定义一个Table API注册TableEnvironment作为Table的查询结果。

④ 输出Table

为了将Table进行输出,我们可以使用TableSink。TableSink是一个通用的接口,支持各种各样的文件格式(e.g. CSV, Apache Parquet, Apache Avro),也支持各种各样的外部系统(e.g., JDBC, Apache HBase, Apache Cassandra, Elasticsearch),同样支持各种各样的MQ(e.g., Apache Kafka, RabbitMQ)。

批数据的导出Table使用BatchTableSink,流数据的导出Table使用的是AppendStreamTableSink、RetractStreamTableSink和 UpsertStreamTableSink.

图片 7

⑤ 解析Query并执行

Table&SQL API查询被解析成DataStream或DataSet程序。一次查询就是一个 logical query plan,解析这个logical query plan分为两步:

  1. 优化logical plan,
  2. 将logical plan转为DataStream或DataSet

一旦Table & SQL API解析完毕, Table & SQL API的查询就会被当做普通DataStream或DataSet被执行。

5.2 Table转为DataStream或DataSet

图片 8

5.3 Convert a Table into a DataSet

图片 9

5.4 Table & SQL API与Window

Window种类

  • tumbling window (GROUP BY)
  • sliding window (window functions)

① Tumbling Window

WX20171124-115353

② Sliding Window

WX20171124-115353

5.5 Table&SQL API与Stream Join

Joining streams to streams:

WX20171124-115758

六、 Table&SQL API举例

首先需要引入flink关系型api和scala的相关jar包:

WX20171124-115924

6.1 批数据的相关代码

WX20171124-120116WX20171124-120425

6.2 流数据的相关代码

WX20171124-120640 WX20171124-120705 WX20171124-120729

七、动态表

flink 1.3以后,在flink SQL上支持动态表查询,也就是说动态表是持续更新,并且能够像常规的静态表一样查询的表。但是,与批处理表查询终止后返回一个静态表作为结果不同的是,动态表中的查询会持续运行,并根据输入表的修改产生一个持续更新的表。因此,结果表也是动态的。在动态表中运行查询并产生一个新的动态表,这是因为流和动态表是可以相互转换的。

流被转换为动态表,动态表使用一个持续查询进行查询,产生一个新的动态表。最后,结果表被转换成流。上面所说的只是逻辑模型,并不意味着实际执行的查询查询也是这个步骤。实际上,持续查询在内部被转换成传统的 DataStream 程序去执行。

动态表查询步骤如下:

  1. 在流中定义动态表
  2. 查询动态表
  3. 生成动态表

7.1 在流中定义动态表

动态表上的 SQL 查询的第一步是在流中定义一个动态表。这意味着我们必须指定流中的记录如何修改现有的动态表。流携带的记录必须具有映射到表的关系模式。在流中定义动态表有两种模式:append模式和update模式。

在append模式中,流中的每条记录是对动态表的插入修改。因此,流中的所有记录都append到动态表中,使得它的大小不断增长并且无限大。下图说明了append模式。append模式如下图。

图片 10

在update模式中,流中的记录可以作为动态表的插入、更新或者删除修改(append模式实际上是一种特殊的update模式)。当在流中通过update模式定义一个动态表时,我们可以在表中指定一个唯一的键属性。在这种情况下,更新和删除操作会带着键属性一起执行。更新模式如下图所示。

图片 11

 

7.2 查询动态表

一旦我们定义了动态表,我们可以在上面执行查询。由于动态表随着时间进行改变,我们必须定义查询动态表的意义。假定我们有一个特定时间的动态表的snapshot,这个snapshot可以作为一个标准的静态批处理表。我们将动态表 A 在点 t 的snapshot表示为 A[t],可以使用 SQL 查询来查询snapshot,该查询产生了一个标准的静态表作为结果,我们把在时间 t 对动态表 A 做的查询 q 的结果表示为 q(A[t])。如果我们反复在动态表的snapshot上计算查询结果,以获取进度时间点,我们将获得许多静态结果表,它们随着时间的推移而改变,并且有效的构成了一个动态表。我们在动态表的查询中定义如下语义。

查询 q 在动态表 A 上产生了一个动态表 R,它在每个时间点 t 等价于在 A[t] 上执行 q 的结果,即 R[t]=q(A[t])。该定义意味着在批处理表和流表上执行相同的查询 q 会产生相同的结果。

在下面的例子中,我们给出了两个例子来说明动态表查询的语义。在下图中,我们看到左侧的动态输入表 A,定义成append模式。在时间 t=8 时,A 由 6 行(标记成蓝色)组成。在时间 t=9 和 t=12 时,有一行追加到 A(分别用绿色和橙色标记)。我们在表 A 上运行一个如图中间所示的简单查询,这个查询根据属性 k 分组,并统计每组的记录数。在右侧我们看到了 t=8(蓝色),t=9(绿色)和 t=12(橙色)时查询 q 的结果。在每个时间点 t,结果表等价于在时间 t 时再动态表 A 上执行批查询。

图片 12

这个例子中的查询是一个简单的分组(但是没有窗口)聚合查询。因此,结果表的大小依赖于输入表的分组键的数量。此外,这个查询会持续更新之前产生的结果行,而不只是添加新行。

第二个例子展示了一个类似的查询,但是有一个很重要的差异。除了对属性 k 分组以外,查询还将记录每 5 秒钟分组为一个滚动窗口,这意味着它每 5 秒钟计算一次 k 的总数。 我们使用 Calcite 的分组窗口函数来指定这个查询。在图的左侧,我们看到输入表 A ,以及它在append模式下随着时间而改变。在右侧,我们看到结果表,以及它随着时间演变。

图片 13

与第一个例子的结果不同的是,这个结果表随着时间而增长,例如每 5 秒钟计算出新的结果行。虽然非窗口查询更新结果表的行,但是窗口聚合查询只追加新行到结果表中。

无论输入表什么时候更新,都不可能计算查询的完整结果。相反,查询编译成流应用,根据输入的变化持续更新它的结果。这意味着不是所有的有效 SQL 都支持,只有那些持续性的、递增的和高效计算的被支持。

7.3 生成动态表

查询动态表生成的动态表,其相当于查询结果。根据查询和它的输入表,结果表会通过插入、更新和删除持续更改,就像普通的mysql数据表一样。它可能是一个不断被更新的单行表,或是一个只插入不更新的表。

传统的mysql数据库在故障和复制的时候,通过日志重建表。比如 UNDO、REDO 和 UNDO/REDO 日志。UNDO 日志记录被修改元素之前的值来回滚不完整的事务,REDO 日志记录元素修改的新值来重做已完成事务丢失的改变,UNDO/REDO 日志同时记录了被修改元素的旧值和新值来撤销未完成的事务,并重做已完成事务丢失的改变。基于这些日志,动态表可以转换成两类更改日志流:REDO 流和 REDO+UNDO 流。

通过将表中的修改转换为流消息,动态表被转换为 redo+undo 流。插入修改生成一条新行的插入消息,删除修改生成一条旧行的删除消息,更新修改生成一条旧行的删除消息以及一条新行的插入消息。行为如下图所示。

图片 14

左侧显示了一个维护在append模式下的动态表,作为中间查询的输入。查询的结果转换为显示在底部的 redo+undo 流。输入表的第一条记录 (1,A) 作为结果表的一条新纪录,因此插入了一条消息 +(A,1) 到流中。第二条输入记录 k=‘A’(4,A) 导致了结果表中 (A,1) 记录的更新,从而产生了一条删除消息 -(A,1) 和一条插入消息 +(A,2)。所有的下游操作或数据汇总都需要能够正确处理这两种类型的消息。

在两种情况下,动态表会转换成 redo 流:要么它只是一个append表(即只有插入修改),要么它有一个唯一的键属性。动态表上的每一个插入修改会产生一条新行的插入消息到 redo 流。由于 redo 流的限制,只有带有唯一键的表能够进行更新和删除修改。如果一个键从动态表中删除,要么是因为行被删除,要么是因为行的键属性值被修改了,所以一条带有被移除键的删除消息发送到 redo 流。更新修改生成带有更新的更新消息,比如新行。由于删除和更新修改根据唯一键来定义,下游操作需要能够根据键来访问之前的值。下图描述如何将上述相同查询的结果表转换为 redo 流。

图片 15

插入到动态表的 (1,A) 产生了 +(A,1) 插入消息。产生更新的 (4,A) 生成了 *(A,2) 的更新消息。

Redo 流的通常做法是将查询结果写到仅append的存储系统,比如滚动文件或者 Kafka topic ,或者是基于key访问的数据存储,比如 Cassandra、关系型 MySQL。

切换到动态表发生的改变

在 1.2 版本中,flink 关系 API 的所有流操作,例如过滤和分组窗口聚合,只会产生新行,并且不能更新先前发布的结果。 相比之下,动态表能够处理更新和删除修改。

1.2 版本中的处理模型是动态表模型的一个子集, 通过附加模式将流转换为动态表,即一个无限增长的表。 由于所有操作仅接受插入更改并在其结果表上生成插入更改(即,产生新行),因此所有在动态append表上已经支持的查询,将使用重做模型转换回 DataStreams,仅用于append表。

最后,值得注意的是在开发代码中,我们无论是使用Table API还是SQL,优化和转换程序并不知道查询是通过 Table API还是 SQL来定义的。由于 Table API 和 SQL 在语义方面等同,只是在样式上有些区别而已。

八、总结

本篇文章整理了flink关系型API的相关知识,整体上来说,flink关系型API还是很好用的,其原理与实现结构清晰,存在很多可借鉴的地方。

 


推荐阅读
  • MyBatis多表查询与动态SQL使用
    本文介绍了MyBatis多表查询与动态SQL的使用方法,包括一对一查询和一对多查询。同时还介绍了动态SQL的使用,包括if标签、trim标签、where标签、set标签和foreach标签的用法。文章还提供了相关的配置信息和示例代码。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文详细介绍了在ASP.NET中获取插入记录的ID的几种方法,包括使用SCOPE_IDENTITY()和IDENT_CURRENT()函数,以及通过ExecuteReader方法执行SQL语句获取ID的步骤。同时,还提供了使用这些方法的示例代码和注意事项。对于需要获取表中最后一个插入操作所产生的ID或马上使用刚插入的新记录ID的开发者来说,本文提供了一些有用的技巧和建议。 ... [详细]
  • 本文详细介绍了Spring的JdbcTemplate的使用方法,包括执行存储过程、存储函数的call()方法,执行任何SQL语句的execute()方法,单个更新和批量更新的update()和batchUpdate()方法,以及单查和列表查询的query()和queryForXXX()方法。提供了经过测试的API供使用。 ... [详细]
  • 高质量SQL书写的30条建议
    本文提供了30条关于优化SQL的建议,包括避免使用select *,使用具体字段,以及使用limit 1等。这些建议是基于实际开发经验总结出来的,旨在帮助读者优化SQL查询。 ... [详细]
  • 本文介绍了通过mysql命令查看mysql的安装路径的方法,提供了相应的sql语句,并希望对读者有参考价值。 ... [详细]
  • 本文讨论了在数据库打开和关闭状态下,重新命名或移动数据文件和日志文件的情况。针对性能和维护原因,需要将数据库文件移动到不同的磁盘上或重新分配到新的磁盘上的情况,以及在操作系统级别移动或重命名数据文件但未在数据库层进行重命名导致报错的情况。通过三个方面进行讨论。 ... [详细]
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
  • 本文介绍了如何使用C#制作Java+Mysql+Tomcat环境安装程序,实现一键式安装。通过将JDK、Mysql、Tomcat三者制作成一个安装包,解决了客户在安装软件时的复杂配置和繁琐问题,便于管理软件版本和系统集成。具体步骤包括配置JDK环境变量和安装Mysql服务,其中使用了MySQL Server 5.5社区版和my.ini文件。安装方法为通过命令行将目录转到mysql的bin目录下,执行mysqld --install MySQL5命令。 ... [详细]
  • ubuntu用sqoop将数据从hive导入mysql时,命令: ... [详细]
  • 如何在php中将mysql查询结果赋值给变量
    本文介绍了在php中将mysql查询结果赋值给变量的方法,包括从mysql表中查询count(学号)并赋值给一个变量,以及如何将sql中查询单条结果赋值给php页面的一个变量。同时还讨论了php调用mysql查询结果到变量的方法,并提供了示例代码。 ... [详细]
  • 深入理解CSS中的margin属性及其应用场景
    本文主要介绍了CSS中的margin属性及其应用场景,包括垂直外边距合并、padding的使用时机、行内替换元素与费替换元素的区别、margin的基线、盒子的物理大小、显示大小、逻辑大小等知识点。通过深入理解这些概念,读者可以更好地掌握margin的用法和原理。同时,文中提供了一些相关的文档和规范供读者参考。 ... [详细]
  • Oracle seg,V$TEMPSEG_USAGE与Oracle排序的关系及使用方法
    本文介绍了Oracle seg,V$TEMPSEG_USAGE与Oracle排序之间的关系,V$TEMPSEG_USAGE是V_$SORT_USAGE的同义词,通过查询dba_objects和dba_synonyms视图可以了解到它们的详细信息。同时,还探讨了V$TEMPSEG_USAGE的使用方法。 ... [详细]
  • svnWebUI:一款现代化的svn服务端管理软件
    svnWebUI是一款图形化管理服务端Subversion的配置工具,适用于非程序员使用。它解决了svn用户和权限配置繁琐且不便的问题,提供了现代化的web界面,让svn服务端管理变得轻松。演示地址:http://svn.nginxwebui.cn:6060。 ... [详细]
  • 本文介绍了如何使用PHP代码将表格导出为UTF8格式的Excel文件。首先,需要连接到数据库并获取表格的列名。然后,设置文件名和文件指针,并将内容写入文件。最后,设置响应头部,将文件作为附件下载。 ... [详细]
author-avatar
西单女孩超炫
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有