日期和時間戳

日期而且時間戳數據類型在Databricks Runtime 7.0中發生了顯著變化。本文描述了:

  • 日期類型和關聯的日曆。

  • 時間戳類型及其與時區的關係。它還解釋了時區偏移分辨率的細節,以及Databricks Runtime 7.0使用的Java 8中新的時間API中細微的行為變化。

  • 用於構造日期和時間戳值的api。

  • 在Apache Spark驅動程序中收集日期和時間戳對象的常見陷阱和最佳實踐。

日期和日曆

一個日期是年、月和日字段的組合,如(year=2012, month=12, day=31)。但是,年、月和日字段的值具有約束,以確保日期值在現實世界中是有效的日期。例如,month的取值範圍為1 ~ 12,day的取值範圍為1 ~ 28、29、30或31(根據年份和月份)等等。的日期類型不考慮時區。

日曆

限製日期字段由許多可能的日曆之一定義。有些人,像陰曆,隻在特定地區使用。有些人,像公曆,隻在曆史上使用。事實上的國際標準是公曆它幾乎在世界各地用於民用目的。它於1582年被引入,並被擴展到1582年之前的日期。此擴展日曆稱為預期的公曆

Databricks Runtime 7.0使用了Proleptic Gregorian日曆,其他數據係統如pandas, R和Apache Arrow已經在使用它了。磚運行時的6。1582年以前的日期使用儒略曆,1582年以後的日期使用公曆。這是繼承的遺產java.sql.DateAPI,該API在Java 8中被java.time.LocalDate它使用的是預期公曆。

時間戳和時區

時間戳類型擴展了日期使用新字段類型:小時、分鍾、秒(可以有一個小數部分)和一個全局(會話作用域)時區。它定義了一個具體的時間瞬間。例如,(年=2012,月=12,天=31,小時=23,分鍾=59,秒=59.123456),會話時區UTC+01:00。當將時間戳值寫入非文本數據源(如Parquet)時,這些值隻是沒有時區信息的瞬間(如UTC中的時間戳)。如果寫入和讀取具有不同會話時區的時間戳值,您可能會看到小時、分鍾和秒字段的不同值,但它們是相同的具體時間瞬間。

小時、分鍾和秒字段有標準範圍:小時0-23,分鍾和秒0-59。Spark支持小數秒,最高可達微秒精度。分數的有效範圍為0 ~ 999999微秒。

在任何具體的瞬間,根據時區,你可以觀察到許多不同的掛鍾值:

牆上的時鍾

相反,掛鍾值可以表示許多不同的時間瞬間。

時區偏移量允許明確地將本地時間戳綁定到時間瞬間。通常,時區偏移量定義為與格林尼治標準時間(GMT)或的小時偏移量UTC + 0協調世界時).時區信息的這種表示消除了歧義,但是不方便。大多數人喜歡指出一個位置,如美國/ Los_AngelesorgydF4y2Ba歐洲/巴黎.區域抵消的這種額外抽象級別使工作變得更容易,但也帶來了複雜性。例如,您現在必須維護一個特殊的時區數據庫,以便將時區名稱映射到偏移量。由於Spark在JVM上運行,所以它將映射委托給Java標準庫,後者從Internet Assigned Numbers Authority時區數據庫(IANA TZDB)。此外,Java標準庫中的映射機製有一些細微差別,會影響Spark的行為。

從Java 8開始,JDK公開了一個不同的API用於日期時間操作和時區偏移解析,Databricks Runtime 7.0使用了這個API。盡管時區名稱到偏移量的映射具有相同的源IANA TZDB,但與Java 7相比,在Java 8及以上版本中實現的方式不同。

例如,查看1883年之前的時間戳美國/ Los_Angeles時區:1883-11-1000:00:00.今年與眾不同,因為在1883年11月18日,北美所有的鐵路都采用了新的標準時間係統。使用Java 7時間API,您可以獲得本地時間戳的時區偏移喂飼

java時間ZoneIdsystemDefault
res0java時間ZoneId美國/Los_Angeles
javasql時間戳返回對象的值“1883-11-10”就是).getTimezoneOffset/60.0
res18.0

等價的Java 8 API返回不同的結果:

java時間ZoneId“美國/ Los_Angeles”).getRulesgetOffsetjava時間LocalDateTime解析“1883 - 11 - 10 - t00:00:00”))
java時間ZoneOffset-075258

在1883年11月18日之前,北美的時間是當地的事情,大多數城市和城鎮使用某種形式的當地太陽時間,由一個眾所周知的時鍾(例如,在教堂的尖塔上或在珠寶店的櫥窗上)維持。這就是為什麼你會看到如此奇怪的時區偏移。

該示例表明Java 8函數更精確,並考慮了IANA TZDB的曆史數據。在切換到Java 8時間API之後,Databricks Runtime 7.0從自動改進中受益,並在解決時區偏移方麵變得更加精確。

Databricks Runtime 7.0也切換到Proleptic公曆時間戳類型。的ISO SQL: 2016Standard聲明時間戳的有效範圍為from0001-01-0100:00:009999-12-3123:59:59.999999.Databricks Runtime 7.0完全符合標準,並支持這個範圍內的所有時間戳。與Databricks Runtime 6相比。X及以下,請注意下列子範圍:

  • 0001-01-01就是1582-10-03 . .23:59:59.999999.磚運行時的6。x和以下使用的是儒略曆,不符合標準。Databricks Runtime 7.0修複了這個問題,並在時間戳的內部操作中應用了Proleptic Gregorian日曆,例如獲取年、月、日等。由於不同的日曆,有些日期存在於Databricks Runtime 6。x及以下版本在Databricks Runtime 7.0中不存在。例如,1000-02-29不是一個有效的日期,因為1000在公曆中不是閏年。另外,Databricks Runtime 6。X及其以下將時區名稱錯誤地解析為此時間戳範圍的區域偏移量。

  • 1582-10-04就是1582-10-14 . .23:59:59.999999.與Databricks Runtime 6相比,這是Databricks Runtime 7.0中一個有效的本地時間戳範圍。X和以下的地方不存在這樣的時間戳。

  • 1582-10-15就是1899-12-31 . .23:59:59.999999.Databricks Runtime 7.0解析時區偏移正確使用曆史數據從IANA TZDB。對比Databricks Runtime 7.0, Databricks Runtime 6。在某些情況下,X和下麵可能會錯誤地解析時區名稱的區域偏移量,如上例所示。

  • 1900-01-01就是2036-12-31 . .23:59:59.999999.Databricks Runtime 7.0和Databricks Runtime 6。x及以下符合ANSI SQL標準,並在日期-時間操作中使用公曆,如獲取月中的哪一天。

  • 2037-01-01就是9999-12-31 . .23:59:59.999999.磚運行時的6。X和以下可能會錯誤地解析時區偏移量和夏令時偏移量。Databricks Runtime 7.0不支持。

將時區名稱映射到偏移量的另一個方麵是由於夏令時(DST)或切換到另一個標準時區偏移量而可能發生的本地時間戳重疊。例如,2019年11月3日02:00:00,美國大部分州將時鍾撥回1小時至01:00:00。本地時間戳2019-11-0301:30:00美國/ Los_Angeles可以映射到哪一個2019-11-0301:30:00UTC-08:00orgydF4y2Ba2019-11-0301:30:00UTC-07:00.如果您不指定偏移量,而隻是設置時區名稱(例如,2019-11-0301:30:00美國/ Los_Angeles), Databricks Runtime 7.0采用較早的偏移量,通常對應“summer”。這種行為與Databricks Runtime 6不同。X和以下的部分需要“冬季”抵消。在間隙的情況下,時鍾向前跳,沒有有效的偏移。對於典型的一小時夏令時更改,Spark將這樣的時間戳移動到與“夏季”時間對應的下一個有效時間戳。

從前麵的示例可以看到,時區名稱到偏移量的映射是不明確的,並且不是一對一的。在可能的情況下,例如,在構造時間戳時,我們建議指定精確的時區偏移量2019-11-0301:30:00UTC-07:00

ANSI SQL和Spark SQL時間戳

ANSI SQL標準定義了兩種類型的時間戳:

  • 時間戳沒有時間orgydF4y2Ba時間戳:本地時間戳為(一年一天小時一分鍾第二個).這些時間戳不綁定到任何時區,是掛鍾時間戳。

  • 時間戳時間:分區的時間戳為(一年一天小時一分鍾第二個TIMEZONE_HOURTIMEZONE_MINUTE).這些時間戳代表UTC時區中的一個瞬間+與每個值相關聯的時區偏移(以小時和分鍾為單位)。

a的時區偏移量時間戳時間不會影響時間戳所表示的物理時間點,因為它由其他時間戳組件給出的UTC時間瞬間完全表示。相反,時區偏移隻影響用於顯示的時間戳值的默認行為,日期/時間組件提取(例如,提取),以及其他需要知道時區的操作,例如在時間戳中添加月份。

Spark SQL將時間戳類型定義為時間戳會話時間,它是字段(一年一天小時一分鍾第二個會話TZ)一年通過第二個字段標識UTC時區中的一個時間瞬間,以及從SQL配置spark.sql.session.timeZone獲取SESSION TZ的位置。會話時區可以設置為:

  • 區抵消(+ | -) HH: mm.這種形式允許您明確地定義物理時間點。

  • 時區名稱,以地區ID的形式顯示區域/城市,如美國/ Los_Angeles.這種形式的時區信息會遇到前麵描述的一些問題,比如本地時間戳重疊。但是,對於任何區域ID,每個UTC時間瞬間都明確地與一個時區偏移相關聯,因此,每個基於區域ID的時區的時間戳都可以明確地轉換為帶有時區偏移的時間戳。默認情況下,會話時區設置為Java虛擬機的默認時區。

火花時間戳會話時間是不同的:

  • 時間戳沒有時間,因為該類型的值可以映射到多個物理時間瞬間,但的任何值時間戳會話時間是一個具體的物理時間瞬間。可以通過在所有會話中使用一個固定的時區偏移來模擬SQL類型,例如UTC+0。在這種情況下,您可以將UTC時間戳視為本地時間戳。

  • 時間戳時間,因為根據SQL標準的列值類型可以有不同的時區偏移量。Spark SQL不支持。

您應該注意到,與全局(會話作用域)時區相關聯的時間戳並不是Spark SQL新發明的東西。像Oracle這樣的rdbms提供了類似的時間戳類型:時間戳當地的時間

構造日期和時間戳

Spark SQL提供了幾種構造日期和時間戳值的方法:

  • 不帶參數的默認構造函數:CURRENT_TIMESTAMP ()而且當前日期()

  • 從其他原始的Spark SQL類型,如INT,字符串

  • 從外部類型,如Python datetime或Java類java.time.LocalDate/即時

  • 對諸如CSV、JSON、Avro、Parquet、ORC等數據源進行反序列化。

這個函數MAKE_DATE在Databricks Runtime 7.0中引入的三個參數-一年,一天——構造一個日期價值。所有輸入參數隱式轉換為INT盡可能的類型。該函數檢查生成的日期是否為預期公曆中的有效日期,否則返回.例如:

火花createDataFrame(((2020626),1000229),-4411)]、[“Y”“米”' D '])createTempView“YMD”dfsql'select make_date(Y, M, D) as date from YMD'dfprintSchema()
|--日期日期可以為空真正的

要打印DataFrame內容,請調用顯示()Action,它將日期轉換為執行器上的字符串,並將字符串傳輸給驅動程序以輸出到控製台上:

df顯示()
+-----------+|日期|+-----------+|2020-06-26||||-0044-01-01|+-----------+

類似地,您可以使用MAKE_TIMESTAMP功能。就像MAKE_DATE,它對日期字段執行相同的驗證,另外還接受時間字段HOUR(0-23)、MINUTE(0-59)和SECOND(0-60)。SECOND的類型為Decimal(精度= 8,比例= 6),因為秒可以通過小數部分傳遞,精度最高可達微秒。例如:

df火花createDataFrame(((2020628103130.123456),15821010012.0001),20192299291.0)]、[“年”“月”“天”“小時”“一分鍾”“第二”])df顯示()
+----+-----+---+----+------+---------+|一年||一天|小時|一分鍾|第二個|+----+-----+---+----+------+---------+|2020|6|28|10|31|30.123456||1582|10|10|0|1|2.0001||2019|2|29|9|29|1.0|+----+-----+---+----+------+---------+
dfselectExpr"make_timestamp(年,月,日,小時,分鍾,秒)作為make_timestamp "tsprintSchema()
|--MAKE_TIMESTAMP時間戳可以為空真正的

至於日期,使用show()操作打印ts DataFrame的內容。同樣地,顯示()將時間戳轉換為字符串,但現在它考慮了SQL配置定義的會話時區spark.sql.session.timeZone

ts顯示截斷
+--------------------------+|MAKE_TIMESTAMP|+--------------------------+|2020-06-28103130.123456||1582-10-10000102.0001|||+--------------------------+

Spark無法創建最後一個時間戳,因為該日期無效:2019不是閏年。

您可能會注意到,在前麵的示例中沒有時區信息。在這種情況下,Spark從SQL配置中獲取一個時區spark.sql.session.timeZone並將其應用於函數調用。還可以通過將其作為的最後一個參數傳遞來選擇不同的時區MAKE_TIMESTAMP.下麵是一個例子:

df火花createDataFrame(((2020628103130.UTC的),(15821010012“美國/ Los_Angeles”),20192289291“歐洲/莫斯科”)),“年”“月”“天”“小時”“一分鍾”“第二”' TZ '])dfdfselectExpr'make_timestamp(年,月,日,小時,分鍾,秒,TZ) as make_timestamp 'dfdfselectExpr"date_format(MAKE_TIMESTAMP, 'yyyy-MM-dd HH:mm:ss VV') AS TIMESTAMP_STRING"df顯示截斷
+---------------------------------+|TIMESTAMP_STRING|+---------------------------------+|2020-06-28133100歐洲/莫斯科||1582-10-10102400歐洲/莫斯科||2019-02-28092900歐洲/莫斯科|+---------------------------------+

如示例所示,Spark考慮了指定的時區,但將所有本地時間戳調整為會話時區。傳遞給的原始時區MAKE_TIMESTAMP函數丟失,因為時間戳會話時間Type假設所有值都屬於一個時區,它甚至不為每個值存儲一個時區。根據的定義時間戳會話時間, Spark使用UTC時區存儲本地時間戳,提取日期-時間字段或將時間戳轉換為字符串時使用會話時區。

此外,時間戳可以使用類型轉換從LONG類型構造。如果LONG列包含從1970-01-01 00:00:00 . z開始的秒數,則可以將其轉換為Spark SQL時間戳

選擇-123456789作為時間戳);1966-02-02052651

不幸的是,這種方法不允許指定秒的小數部分。

的值構造日期和時間戳是另一種方法字符串類型。你可以使用特殊的關鍵字來創建文字:

選擇時間戳“2020-06-28 22:17:33.123456歐洲/阿姆斯特丹的日期“2020-07-01”2020-06-282317331234562020-07-01

或者,您可以使用類型轉換,可以應用於列中的所有值:

選擇“2020-06-28 22:17:33.123456歐洲/阿姆斯特丹的作為時間戳),“2020-07-01”作為日期);2020-06-282317331234562020-07-01

輸入時間戳字符串被解釋為指定時區的本地時間戳,如果在輸入字符串中省略了時區,則解釋為會話時區的本地時間戳。可以將具有不尋常模式的字符串轉換為時間戳to_timestamp ()函數。中描述了受支持的模式格式化和解析的日期時間模式

選擇to_timestamp“28/6/2020 22.17.33”“dd / M / yyyy HH.mm.ss”);2020-06-28221733

如果沒有指定模式,函數的行為類似於

為了提高可用性,Spark SQL可以識別所有接受字符串並返回時間戳或日期的方法中的特殊字符串值:

  • 時代是date的別名嗎1970-01-01或時間戳1970-01-0100:00:00Z

  • 現在是會話時區的當前時間戳或日期。在單個查詢中,它總是產生相同的結果。

  • 今天當前的開始日期為時間戳的當前日期日期類型。

  • 明天對時間戳來說是第二天的開始還是僅僅是第二天日期類型。

  • 昨天是當前一天的前一天還是它的開始時間戳類型。

例如:

選擇時間戳“昨天”時間戳“今天”時間戳“現在”時間戳“明天”2020-06-270000002020-06-280000002020-06-28230707182020-06-29000000選擇日期“昨天”日期“今天”日期“現在”日期“明天”2020-06-272020-06-282020-06-282020-06-29

Spark允許您創建數據集從驅動程序端現有的外部對象集合中創建相應類型的列。Spark將外部類型的實例轉換為語義上等價的內部表示。例如,創建一個數據集日期而且時間戳列,你可以使用:

進口datetimedf火花createDataFrame(((datetimedatetime202071000),datetime日期202071))),“時間戳”“日期”])df顯示()
+-------------------+----------+|時間戳|日期|+-------------------+----------+|2020-07-01000000|2020-07-01|+-------------------+----------+

PySpark在驅動端使用係統時區將Python的日期-時間對象轉換為內部Spark SQL表示,這可能與Spark的會話時區設置不同spark.sql.session.timeZone.內部值不包含關於原始時區的信息。以後對並行化日期和時間戳值的操作隻考慮Spark SQL會話的時區時間戳會話時間類型定義。

以類似的方式,Spark在Java和Scala api中識別以下類型為外部日期-時間類型:

  • java.sql.Date而且java.time.LocalDate的外部類型日期類型

  • java.sql.Timestamp而且java.time.Instant時間戳類型。

這是有區別的java.sql。*而且java.time。*類型。java.time.LocalDate而且java.time.Instant是在Java 8中添加的,類型基於Proleptic Gregorian日曆—Databricks Runtime 7.0及以上版本使用的日曆。java.sql.Date而且java.sql.Timestamp下麵有另一個日曆——混合日曆(自1582-10-15年以來的朱利安+格裏高利日曆),它與Databricks Runtime 6使用的舊日曆相同。下麵的x和。由於不同的日曆係統,Spark在轉換為內部Spark SQL表示時必須執行額外的操作,並將輸入日期/時間戳從一個日曆轉換為另一個日曆。rebase操作對於1900年之後的現代時間戳有一點開銷,而對於舊時間戳則更重要。

下麵的例子展示了如何從Scala集合中創建時間戳。第一個示例構造一個java.sql.Timestamp對象。的返回對象的值方法將輸入字符串解釋為默認JVM時區中的本地時間戳,這可能與Spark的會話時區不同。的實例java.sql.TimestamporgydF4y2Bajava.sql.Date在特定的時區,看一看java.text.SimpleDateFormat(和它的方法setTimeZone)或java.util.Calendar

Seqjavasql時間戳返回對象的值“2020-06-29 22:41:30”),javasql時間戳0))。toDF“t”).顯示
+-------------------+|ts|+-------------------+|2020-06-29224130.||1970-01-01030000|+-------------------+
Seqjava時間即時ofEpochSecond-12219261484 l),java時間即時時代).toDF“t”).顯示
+-------------------+|ts|+-------------------+|1582-10-15111213||1970-01-01030000|+-------------------+

類似地,你可以做一個日期列從收集的java.sql.DateorgydF4y2Bajava.sql.LocalDate.並行化的java.sql.LocalDate實例完全獨立於Spark的會話或JVM的默認時區,但對於並行化則不是這樣java.sql.Date實例。有細微差別:

  1. java.sql.Dateinstances表示驅動程序上默認JVM時區的本地日期。

  2. 為了正確地轉換到Spark SQL值,驅動程序和執行程序上的默認JVM時區必須是相同的。

Seqjava時間LocalDate2020229),java時間LocalDate現在).toDF“日期”).顯示
+----------+|日期|+----------+|2020-02-29||2020-06-29|+----------+

為了避免任何與日曆和時區相關的問題,我們建議使用Java 8類型java.sql.LocalDate/即時作為Java/Scala時間戳或日期集合並行化的外部類型。

收集日期和時間戳

並行化的反向操作是從執行程序收集日期和時間戳回驅動程序,並返回外部類型的集合。例如上麵,可以拉DataFrame回到司機使用收集()行動:

df收集()
時間戳datetimedatetime20207100),日期datetime日期202071)))

Spark將UTC時區內的日期和時間戳列的內部值作為時間瞬間從執行者傳輸到驅動程序,並在驅動程序上按照係統時區執行到Python datetime對象的轉換,而不使用Spark SQL會話時區。收集()不同於顯示()上一節描述的操作。顯示()在將時間戳轉換為字符串時使用會話時區,並收集驅動程序上產生的字符串。

在Java和Scala api中,Spark默認執行如下轉換:

  • 火花SQL日期的實例java.sql.Date

  • 火花SQL時間戳的實例java.sql.Timestamp

這兩種轉換都是在驅動程序的默認JVM時區中執行的。通過這種方式,您可以使用相同的日期-時間字段Date.getDay ()getHour (),並使用Spark SQL函數一天小時,驅動程序上的默認JVM時區和執行程序上的會話時區應該是相同的。

類似於創建日期/時間戳java.sql.Date/時間戳, Databricks Runtime 7.0執行從預估公曆到混合日曆(朱利安+公曆)的重基。對於現代日期(1582年之後)和時間戳(1900年之後),這種操作幾乎是免費的,但對於古代日期和時間戳,它可能會帶來一些開銷。

您可以避免此類與日曆相關的問題,並要求Spark返回java.time類型,這是從Java 8開始添加的。如果您設置了SQL配置spark.sql.datetime.java8API.enabled真實的,Dataset.collect ()行動回報:

  • java.time.LocalDate對於火花SQL日期類型

  • java.time.Instant對於火花SQL時間戳類型

現在,轉換不會受到日曆相關問題的影響,因為Java 8類型和Databricks Runtime 7.0及以上版本都基於Proleptic Gregorian日曆。的收集()操作不依賴於默認的JVM時區。時間戳轉換完全不依賴於時區。日期轉換使用SQL配置中的會話時區spark.sql.session.timeZone.例如,考慮a數據集日期而且時間戳列,以及要設置的默認JVM時區歐洲/俄羅斯和會話時區設置為美國/ Los_Angeles

java跑龍套時區getDefault
res1java跑龍套時區太陽跑龍套日曆ZoneInfoid“歐洲/莫斯科”...
火花相依得到“spark.sql.session.timeZone”
字符串美國/Los_Angeles
df顯示
+-------------------+----------+|時間戳|日期|+-------------------+----------+|2020-07-01000000|2020-07-01|+-------------------+----------+

顯示()操作在會話時間打印時間戳美國/ Los_Angeles,但如果你收集數據集,它被轉換為java.sql.TimestamptoString方法打印歐洲/俄羅斯

df收集()
res16數組orgapache火花sql數組([2020-07-01100000.02020-07-01])
df收集() (0).木屐javasql時間戳) (0).toString
res18javasql時間戳2020-07-01100000.0

實際上,本地時間2020-07-01 00:00:00為UTC時間2020-07-01 t07:00:00 z。你可以觀察到,如果你啟用Java 8 API並收集數據集:

df收集()
res27數組orgapache火花sql數組([2020-07-01T070000Z2020-07-01])

你可以轉換java.time.Instant對象綁定到任何本地時間戳,獨立於全局JVM時區。這是優點之一java.time.Instantjava.sql.Timestamp.前者需要更改全局JVM設置,這會影響同一個JVM上的其他時間戳。因此,如果您的應用程序在不同的時區處理日期或時間戳,並且在使用Java或Scala向驅動程序收集數據時,應用程序之間不應該發生衝突Dataset.collect ()我們建議使用SQL配置切換到Java 8 APIspark.sql.datetime.java8API.enabled